Understanding RBF in caravan-fees: How to Balance Simplicity and Flexibility

Understanding RBF in caravan-fees: How to Balance Simplicity and Flexibility

Deep Dive into RBF Implementation in caravan-fees: Balancing Simplicity and Flexibility

A technical exploration of Replace-By-Fee implementation for Caravan Wallet

Introduction

As part of my Summer of Bitcoin open source fellowship, I've been working on creating the caravan-fees package and an important feature of this package was to give developer's the ability to implement Replace-By-Fee (RBF) functionality in their wallets.

This blog post will delve into the technical details of our implementation, focusing on how we've built this feature and how users can extend it for more advanced use cases.

Understanding RBF

Replace-By-Fee is a protocol that replaces unconfirmed transactions with new versions that include higher fees. This feature is crucial for adapting to changing network conditions and ensuring timely confirmation of transactions. However, RBF comes with specific rules that can make fee calculation challenging.

Key RBF Rules for Fee Calculation

1. The Higher Fee Rule

Core Requirement: The new transaction must pay a higher fee than the original.

Transaction TypeFee AmountStatusDescription
Original Tx1000 satoshisBaselineInitial transaction
Valid RBF Tx1100 satoshis✅ ValidHigher fee, accepted
Invalid RBF Tx900 satoshis❌ InvalidLower fee, rejected

Interesting Edge Cases

The Minimal Increase Problem
Transaction Sequence:
Original    → 1000 satoshis
Replacement → 1001 satoshis (+1 sat)
Issue       → Valid but potential DDoS vector
Fee Rate vs Absolute Fee Comparison
MetricOriginal TransactionNew TransactionImpact
Size1000 bytes1500 bytes🔺 Increased
Total Fee2000 satoshis2100 satoshis🔺 Higher absolute
Fee Rate2 sat/byte1.4 sat/byte🔻 Lower rate
Miner AppealBaselinePotentially less attractive⚠️ Trade-off

The Pinning Problem Explained

Alice's Initial Transaction (B):
┌────────────────────────┐
│ Input:  1.5 BTC        │
│ Output: 1 BTC → BigEx  │
│ Change: 0.49995 BTC    │
│ Fee:    5,000 sats     │
└────────────────────────┘
           ⬇
BigEx's Sweep Transaction (C):
┌────────────────────────┐
│ Input 1:  1 BTC (Alice)│
│ Input 2+: 99 BTC       │
│ Output:   99.9999 BTC  │
│ Fee:      10,000 sats  │
└────────────────────────┘

Challenge: Alice must now pay 15,000 satoshis (sum of both fees) to replace her transaction

2. The Input Control Rule

This rule prevents "fee sniping" by restricting unconfirmed input usage.

Transaction TypeInputs UsedStatusExplanation
OriginalA (confirmed), B (unconfirmed)BaseInitial state
Valid RBFA, B, C (confirmed)Maintains unconfirmed inputs
Invalid RBFA, B, D (unconfirmed)Introduces new unconfirmed input

The Replacement Cycle Attack

3. The Bandwidth Payment Rule

Formula for Minimum Fee Calculation:

Required Fee = Original Fee + (Incremental Relay × Replacement Size)

Historical Context:

BIP-125 (2015):
├── Original Focus: Absolute fees
├── Current Reality: Fee rates more important
└── Primary Purpose: Anti-DDoS protection

Deep Dive: The Bandwidth Rule Mystery

💭 "The replacement transaction must pay for its own bandwidth at or above the rate set by the node's minimum relay fee setting..."

Let's break down this puzzling rule that had me scratching my head:

The Rule in Simple Terms

If:
- Minimum relay fee = 1 sat/byte
- Replacement tx size = 500 bytes
Then:
- Must pay ≥ 500 sats more than original

Initial Assumptions vs Reality

What We ThoughtWhat It Actually Is
Higher fee rate requirementAnti-DDoS measure
Miner incentive mechanismNetwork protection
Modern fee market adaptationHistorical artifact from 2015

Historical Context

Timeline:
2015 (BIP-125 Era)
├── Miners: Focused on absolute fees
├── Network: Different priorities
└── Context: Pre-modern fee market

2024 (Current)
├── Miners: Prioritize fee rates
├── Network: More sophisticated
└── Result: Rule feels misaligned

The Anti-DDoS Angle

Consider this attack scenario:

Original Tx Fee:     1000 sats
Replacement 1:       1001 sats (+1)
Replacement 2:       1002 sats (+1)
Replacement 3:       1003 sats (+1)
...and so on

Problem: Network spam with minimal fee increases
Solution: Force meaningful fee increases with:
Fee = original_fee + (incremental_relay × replacement_size)

The Rule Redundancy Question

🤔 Question: If Rule #4 (bandwidth) encompasses Rule #3 (higher fee), why have both?

Answer: Code-Level Separation

// Rule #4: Economic rationality check
bool sufficientFee = pool.GetModifiedFee(ptx) >=
    pool.GetModifiedFee(origTx) + extraFeePaid;

// Rule #3: Base fee requirement check
for (const CTxMemPool::txiter it : allConflicting) {
    if (nModifiedFees < it->GetModifiedFee()) {
        return state.DoS(0, false,
            REJECT_INSUFFICIENTFEE, "insufficient fee");
    }
}

Why Two Checks?

RulePurposeCheck Type
Rule #4 (Bandwidth)Anti-DDoSSystem Protection
Rule #3 (Higher Fee)Basic RequirementEconomic Incentive

💡 Key Insight: The separation provides both clarity and robustness in the fee-bumping mechanism

This layered approach ensures:

  1. Basic economic incentives are maintained
  2. Network is protected from abuse
  3. Code remains clear and maintainable
  4. Each rule serves a distinct purpose

Implementation in caravan-fees

Core Fee Calculation Algorithm

// Base fee calculation
const minRequiredFee = BigNumber.max(originalTxFee, targetFeeForNewSize);

// RBF fee rate determination
get rbfFeeRate(): string {
  return new BigNumber(this.minimumRBFFee).dividedBy(this.vsize).toString();
}

// Minimum RBF fee calculation
get minimumRBFFee(): Satoshis {
  const minReplacementFee = new BigNumber(this.feeRate)
    .plus(this._incrementalRelayFeeRate)
    .multipliedBy(this.vsize);

  const targetFeeBasedOnUserRate = new BigNumber(
    this.targetFeeRate,
  ).multipliedBy(this.vsize);

  return BigNumber.max(minReplacementFee, targetFeeBasedOnUserRate)
    .integerValue(BigNumber.ROUND_CEIL)
    .toString();
}

Implementation Scenarios

Scenario 1: Smaller Replacement Transaction

AspectOriginal TxReplacement TxNotes
Size500 vbytes300 vbytes🔻 40% smaller
Fee5,000 sats5,000 satsMinimum required
Fee Rate10 sat/vbyte~16.67 sat/vbyte🔺 Higher effective rate
Calculation-max(5000, 300×15)Using original as minimum

Scenario 2: Larger Replacement Transaction

ParameterOriginalReplacementChange
Size500 vbytes700 vbytes+40%
Fee5,000 sats8,400 sats+68%
Fee Rate10 sat/vbyte12 sat/vbyte+20%
Required-max(5000, 8400)8,400 required

RBF Strategies

Cancellation Approach

Original Transaction:
┌────────────────────────┐
│ Inputs: 0.3 BTC total  │
│ Outputs: Split payment │
│ Fee: 5,000 sats        │
└────────────────────────┘

Cancellation Transaction:
┌────────────────────────┐
│ Same inputs            │
│ Single output          │
│ Higher fee: 6,000 sats │
└────────────────────────┘

Acceleration Strategy

Original Transaction:
┌────────────────────────┐
│ Input: 0.5 BTC         │
│ Payment: 0.4 BTC       │
│ Change: 0.09995 BTC    │
│ Fee: 5,000 sats        │
└────────────────────────┘

Accelerated Transaction:
┌────────────────────────┐
│ Inputs: 0.6 BTC total  │
│ Same payment           │
│ Adjusted change        │
│ Fee: 15,000 sats       │
└────────────────────────┘

Advanced Development Techniques

1. Dynamic Fee Strategy

class DynamicFeeStrategy extends BaseFeeStrategy {
  calculateFee(mempool, transaction) {
    // Mempool-aware fee calculation
  }
}

2. Privacy-Enhanced UTXO Selection

function selectPrivacyEnhancingUTXOs(availableUTXOs, targetAmount) {
  // Privacy-focused selection logic
}

3. Batch RBF Processing

function createBatchRBF(transactions, newFeeRate) {
  // Multi-transaction replacement logic
}

Success Metrics

ObjectiveImplementationBenefit
RBF Rule ComplianceFee validation checks✅ Network acceptance
Fee Rate OptimizationDynamic calculation✅ Cost efficiency
Transaction Size HandlingAdaptive fee scaling✅ Flexibility
SecurityInput validation✅ Attack prevention

This implementation in caravan-fees provides a robust foundation for RBF functionality, balancing the technical requirements of the Bitcoin network with practical usability concerns.