跳至主要內容
進階

Transaction Fees

交易手續費:費率估算、RBF 和 CPFP 機制詳解

18 分鐘

概述

比特幣交易手續費是用戶支付給礦工的費用,以激勵他們將交易打包進區塊。 手續費由交易大小(虛擬字節)和當前網路擁堵程度決定。

手續費計算: 手續費 = 費率 (sat/vB) × 交易虛擬大小 (vBytes)。 礦工優先選擇費率高的交易打包。

手續費計算

交易大小

交易大小單位:

1. Bytes (實際大小)
   - 交易的原始字節數
   - SegWit 前的計算方式

2. Weight Units (WU)
   - 非 witness 數據: 1 byte = 4 WU
   - Witness 數據: 1 byte = 1 WU
   - 最大區塊: 4,000,000 WU

3. Virtual Bytes (vB)
   - vBytes = Weight / 4
   - 手續費計算的標準單位

範例 (P2WPKH 交易):
┌─────────────────────────────────────────────┐
│ 組件              │ Bytes │ WU    │ vBytes │
├─────────────────────────────────────────────┤
│ 非 witness        │   87  │  348  │   87   │
│ Witness           │  107  │  107  │ 26.75  │
├─────────────────────────────────────────────┤
│ 總計              │  194  │  455  │ 113.75 │
└─────────────────────────────────────────────┘

常見交易大小

典型交易大小 (vBytes):

P2PKH (Legacy):
- 1 輸入 1 輸出: ~192 vB
- 1 輸入 2 輸出: ~226 vB
- 2 輸入 2 輸出: ~374 vB

P2WPKH (Native SegWit):
- 1 輸入 1 輸出: ~109 vB
- 1 輸入 2 輸出: ~141 vB
- 2 輸入 2 輸出: ~208 vB

P2TR (Taproot):
- 1 輸入 1 輸出: ~111 vB
- 1 輸入 2 輸出: ~154 vB
- 2 輸入 2 輸出: ~211 vB

估算公式:
vBytes ≈ 10.5 + (inputs × 68) + (outputs × 31)  # P2WPKH
vBytes ≈ 10.5 + (inputs × 57.5) + (outputs × 43) # P2TR

費率估算

Bitcoin Core 估算

# 估算確認所需費率
bitcoin-cli estimatesmartfee 6      # 6 區塊內確認
bitcoin-cli estimatesmartfee 144    # 1 天內確認 (144 區塊)

# 結果
{
  "feerate": 0.00012345,  # BTC/kvB
  "blocks": 6
}

# 轉換為 sat/vB
# 0.00012345 BTC/kvB = 12.345 sat/vB

估算算法

Bitcoin Core 費率估算:

1. 追蹤歷史交易
   - 記錄交易進入 mempool 的費率
   - 記錄交易被確認時的區塊數

2. 分桶統計
   - 按費率範圍分桶
   - 統計各桶的確認時間分佈

3. 預測
   - 給定目標確認區塊數
   - 找到成功率 > 95% 的最低費率

限制:
- 需要運行一段時間收集數據
- 極端情況下可能不準確
- 不考慮未來 mempool 變化

TypeScript 實作

interface FeeEstimate {
  fastestFee: number;    // 下一區塊
  halfHourFee: number;   // ~30 分鐘
  hourFee: number;       // ~1 小時
  economyFee: number;    // 經濟模式
  minimumFee: number;    // 最低中繼費率
}

// 從多個來源獲取費率估算
async function getFeeEstimates(): Promise<FeeEstimate> {
  const sources = [
    fetchMempoolSpace(),
    fetchBitcoinCore(),
    fetchBlockstream(),
  ];

  const estimates = await Promise.all(sources);

  // 取中位數作為估算
  return {
    fastestFee: median(estimates.map(e => e.fastestFee)),
    halfHourFee: median(estimates.map(e => e.halfHourFee)),
    hourFee: median(estimates.map(e => e.hourFee)),
    economyFee: median(estimates.map(e => e.economyFee)),
    minimumFee: 1,  // 最低 1 sat/vB
  };
}

// 計算交易手續費
function calculateFee(
  txVsize: number,
  feeRate: number  // sat/vB
): bigint {
  return BigInt(Math.ceil(txVsize * feeRate));
}

// 估算交易大小
function estimateTxVsize(
  inputCount: number,
  outputCount: number,
  inputType: 'p2pkh' | 'p2wpkh' | 'p2tr' = 'p2wpkh'
): number {
  const overhead = 10.5;  // version + locktime + counts

  const inputSize = {
    p2pkh: 148,
    p2wpkh: 68,
    p2tr: 57.5,
  }[inputType];

  const outputSize = {
    p2pkh: 34,
    p2wpkh: 31,
    p2tr: 43,
  }[inputType];

  return Math.ceil(
    overhead +
    inputCount * inputSize +
    outputCount * outputSize
  );
}

RBF (Replace-By-Fee)

BIP-125 規則

RBF (Replace-By-Fee):
允許用更高費率的交易替換未確認交易

啟用條件:
- 原交易至少一個輸入的 nSequence < 0xFFFFFFFE

替換規則 (BIP-125):
1. 新交易必須花費原交易的所有輸入
2. 新交易費率必須高於原交易
3. 新交易總費用 >= 原交易 + 增量中繼費
4. 不能替換超過 100 筆後代交易
5. 新交易必須支付足夠費用覆蓋頻寬成本

nSequence 值:
- 0xFFFFFFFF: 禁用 RBF 和時間鎖
- 0xFFFFFFFE: 禁用 RBF,啟用時間鎖
- < 0xFFFFFFFE: 啟用 RBF

Full RBF

Full RBF (Bitcoin Core 24.0+):

傳統 Opt-in RBF:
- 只有明確標記的交易可替換
- nSequence < 0xFFFFFFFE

Full RBF:
- 所有未確認交易都可替換
- 無論 nSequence 值
- 配置: -mempoolfullrbf=1

爭議:
- 支持者: 更真實反映礦工行為
- 反對者: 影響 0 確認交易場景

實際情況:
- 大多數礦工啟用 full RBF
- 0 確認交易不應被信任

RBF 實作

// 創建 RBF 可替換交易
function createRBFTransaction(
  inputs: UTXO[],
  outputs: Output[],
  feeRate: number
): Transaction {
  return {
    version: 2,
    inputs: inputs.map(utxo => ({
      txid: utxo.txid,
      vout: utxo.vout,
      scriptSig: Buffer.alloc(0),
      sequence: 0xFFFFFFFD,  // 啟用 RBF
    })),
    outputs,
    locktime: 0,
  };
}

// 創建 RBF 替換交易
function createRBFReplacement(
  originalTx: Transaction,
  newFeeRate: number,
  newOutputs?: Output[]
): Transaction {
  // 計算原交易費用
  const originalFee = calculateTxFee(originalTx);
  const originalVsize = calculateVsize(originalTx);
  const originalFeeRate = Number(originalFee) / originalVsize;

  // 確保新費率更高
  if (newFeeRate <= originalFeeRate) {
    throw new Error('New fee rate must be higher');
  }

  // 創建替換交易
  const outputs = newOutputs || originalTx.outputs;
  const newTx = {
    version: 2,
    inputs: originalTx.inputs.map(input => ({
      ...input,
      sequence: 0xFFFFFFFD,
    })),
    outputs,
    locktime: 0,
  };

  // 調整找零輸出以匹配新費率
  adjustChangeOutput(newTx, newFeeRate);

  return newTx;
}

// Bitcoin Core RPC
async function bumpFee(
  rpc: RPCClient,
  txid: string,
  options?: { fee_rate?: number; replaceable?: boolean }
): Promise<{ txid: string; origfee: number; fee: number }> {
  return rpc.call('bumpfee', [txid, options]);
}

CPFP (Child-Pays-For-Parent)

基本概念

CPFP (Child-Pays-For-Parent):
通過高費率子交易提升低費率父交易的確認優先級

原理:
┌─────────────────────────────────────┐
│ 父交易 (低費率)                     │
│ Fee: 1000 sat, Size: 200 vB         │
│ Fee Rate: 5 sat/vB                  │
└─────────────────┬───────────────────┘
                  │ 花費父交易輸出
                  ▼
┌─────────────────────────────────────┐
│ 子交易 (高費率)                     │
│ Fee: 4000 sat, Size: 100 vB         │
│ Fee Rate: 40 sat/vB                 │
└─────────────────────────────────────┘

Package Fee Rate:
(1000 + 4000) / (200 + 100) = 16.67 sat/vB

礦工會一起打包父子交易以獲得更高收益

CPFP 實作

interface CPFPContext {
  parentTx: Transaction;
  parentFee: bigint;
  parentVsize: number;
  targetFeeRate: number;
}

// 計算 CPFP 子交易所需費用
function calculateCPFPFee(
  ctx: CPFPContext,
  childVsize: number
): bigint {
  const { parentFee, parentVsize, targetFeeRate } = ctx;

  // 總費用 = 目標費率 × 總大小
  const totalVsize = parentVsize + childVsize;
  const totalFeeNeeded = BigInt(Math.ceil(totalVsize * targetFeeRate));

  // 子交易需要補足差額
  const childFee = totalFeeNeeded - parentFee;

  // 確保子交易費用為正
  if (childFee <= 0n) {
    return BigInt(Math.ceil(childVsize * targetFeeRate));
  }

  return childFee;
}

// 創建 CPFP 子交易
function createCPFPChild(
  parentTx: Transaction,
  outputIndex: number,  // 要花費的父交易輸出
  destination: Uint8Array,
  targetFeeRate: number
): Transaction {
  const parentTxid = calculateTxid(parentTx);
  const parentOutput = parentTx.outputs[outputIndex];
  const parentVsize = calculateVsize(parentTx);
  const parentFee = calculateTxFee(parentTx);

  // 估算子交易大小 (1 輸入 1 輸出)
  const childVsize = estimateTxVsize(1, 1, 'p2wpkh');

  // 計算所需費用
  const childFee = calculateCPFPFee(
    { parentTx, parentFee, parentVsize, targetFeeRate },
    childVsize
  );

  // 子交易輸出金額 = 父交易輸出 - 子交易費用
  const outputValue = parentOutput.value - childFee;

  if (outputValue <= 546n) {  // dust threshold
    throw new Error('Output would be dust after CPFP fee');
  }

  return {
    version: 2,
    inputs: [{
      txid: parentTxid,
      vout: outputIndex,
      scriptSig: Buffer.alloc(0),
      sequence: 0xFFFFFFFD,
    }],
    outputs: [{
      value: outputValue,
      scriptPubKey: destination,
    }],
    locktime: 0,
  };
}

Package Relay

Package Relay (進行中):

問題:
- 當前 mempool 單獨評估每筆交易
- 低於最低費率的父交易無法進入 mempool
- CPFP 無法工作

解決方案:
- 允許提交交易包
- 整體評估包的費率
- 父交易可低於最低費率

狀態:
- Bitcoin Core 25.0+ 部分支持
- 完整 package relay 仍在開發

用途:
- 閃電網路 anchor outputs
- 更靈活的 CPFP

費率策略

選擇策略

type Priority = 'high' | 'medium' | 'low' | 'economy';

interface FeeStrategy {
  targetBlocks: number;
  maxFeeRate: number;
  allowRBF: boolean;
}

const strategies: Record<Priority, FeeStrategy> = {
  high: {
    targetBlocks: 1,
    maxFeeRate: 500,  // sat/vB
    allowRBF: true,
  },
  medium: {
    targetBlocks: 6,
    maxFeeRate: 100,
    allowRBF: true,
  },
  low: {
    targetBlocks: 144,  // ~1 day
    maxFeeRate: 20,
    allowRBF: true,
  },
  economy: {
    targetBlocks: 1008,  // ~1 week
    maxFeeRate: 5,
    allowRBF: true,
  },
};

async function selectFeeRate(
  priority: Priority,
  estimates: FeeEstimate
): Promise<number> {
  const strategy = strategies[priority];

  let feeRate: number;
  switch (priority) {
    case 'high':
      feeRate = estimates.fastestFee;
      break;
    case 'medium':
      feeRate = estimates.halfHourFee;
      break;
    case 'low':
      feeRate = estimates.hourFee;
      break;
    case 'economy':
      feeRate = estimates.economyFee;
      break;
  }

  // 不超過最大限制
  return Math.min(feeRate, strategy.maxFeeRate);
}

動態調整

// 監控交易確認狀態並自動調整
class FeeMonitor {
  private pendingTxs: Map<string, PendingTx> = new Map();

  async monitorAndBump(
    rpc: RPCClient,
    txid: string,
    options: {
      maxFeeRate: number;
      targetBlocks: number;
      checkInterval: number;  // ms
    }
  ): Promise<string> {
    const startBlock = await rpc.call('getblockcount');
    let currentTxid = txid;

    return new Promise((resolve, reject) => {
      const interval = setInterval(async () => {
        try {
          // 檢查是否已確認
          const tx = await rpc.call('gettransaction', [currentTxid]);
          if (tx.confirmations > 0) {
            clearInterval(interval);
            resolve(currentTxid);
            return;
          }

          // 檢查是否需要加速
          const currentBlock = await rpc.call('getblockcount');
          const elapsed = currentBlock - startBlock;

          if (elapsed >= options.targetBlocks / 2) {
            // 超過一半時間還沒確認,嘗試 RBF
            const newEstimate = await rpc.call('estimatesmartfee', [
              options.targetBlocks - elapsed,
            ]);

            const result = await rpc.call('bumpfee', [currentTxid, {
              fee_rate: Math.min(
                newEstimate.feerate * 100000,
                options.maxFeeRate
              ),
            }]);

            currentTxid = result.txid;
            console.log(`Bumped fee: ${txid} -> ${currentTxid}`);
          }
        } catch (e) {
          clearInterval(interval);
          reject(e);
        }
      }, options.checkInterval);
    });
  }
}

最佳實踐

  • 使用 SegWit:P2WPKH/P2TR 比 Legacy 節省 30-50% 費用
  • 啟用 RBF:允許後續調整費率
  • 批量交易:合併多筆支付減少總大小
  • UTXO 整合:低費率時整合小額 UTXO
  • 適時選擇:非緊急交易選擇低擁堵時段
  • 多源估算:參考多個費率估算來源

相關資源

已複製連結
已複製到剪貼簿