跳至主要內容
進階

Orphan Transactions

深入了解 Bitcoin Core 如何處理孤立交易,即引用未知父交易的交易。

10 分鐘

什麼是 Orphan Transactions?

Orphan transactions(孤立交易)是指引用了未知輸入的交易。當節點收到一筆交易, 但其父交易(提供輸入的交易)尚未到達時,該交易會被暫時存放在孤立交易池中。

孤立交易場景:

正常順序:
  TX_A (父) ──────→ TX_B (子)
     先到達           後到達
     ✓ 接受           ✓ 接受(父已知)

亂序到達:
  TX_B (子) ──────→ TX_A (父)
     先到達           後到達
     ? 孤立!         ✓ 接受
     (父未知)        ↓
                    TX_B 從孤立池移到 mempool

產生原因

  • 1
    網路傳播順序

    子交易可能比父交易更早到達某些節點

  • 2
    CPFP(Child Pays For Parent)

    用戶發送子交易來加速父交易確認

  • 3
    Package Relay

    交易包中的依賴關係

處理機制

interface OrphanTx {
  tx: Transaction;
  fromPeer: number;        // 來源節點
  timeExpire: number;      // 過期時間
  missingInputs: string[]; // 缺失的父交易 ID
}

class OrphanPool {
  private orphans: Map = new Map();
  private byPrevOut: Map> = new Map();

  // 限制
  private readonly MAX_ORPHAN_TRANSACTIONS = 100;
  private readonly ORPHAN_TX_EXPIRE_TIME = 20 * 60; // 20 分鐘

  // 添加孤立交易
  addOrphanTx(tx: Transaction, fromPeer: number): boolean {
    const txid = tx.txid;

    // 1. 檢查是否已存在
    if (this.orphans.has(txid)) {
      return false;
    }

    // 2. 檢查大小限制
    if (tx.size > 100000) {  // 100KB 限制
      return false;
    }

    // 3. 如果池滿了,隨機驅逐一個
    if (this.orphans.size >= this.MAX_ORPHAN_TRANSACTIONS) {
      this.evictRandom();
    }

    // 4. 添加到池中
    const orphan: OrphanTx = {
      tx,
      fromPeer,
      timeExpire: Date.now() + this.ORPHAN_TX_EXPIRE_TIME * 1000,
      missingInputs: this.findMissingInputs(tx),
    };

    this.orphans.set(txid, orphan);

    // 5. 建立按父交易查詢的索引
    for (const input of tx.inputs) {
      const prevOutKey = `${input.txid}:${input.vout}`;
      if (!this.byPrevOut.has(prevOutKey)) {
        this.byPrevOut.set(prevOutKey, new Set());
      }
      this.byPrevOut.get(prevOutKey)!.add(txid);
    }

    return true;
  }

  // 當父交易到達時處理孤立交易
  onParentReceived(parentTxid: string): Transaction[] {
    const resolved: Transaction[] = [];

    // 找到所有等待這個父交易的孤立交易
    for (const orphan of this.orphans.values()) {
      if (orphan.missingInputs.includes(parentTxid)) {
        // 移除已解決的依賴
        orphan.missingInputs = orphan.missingInputs.filter(
          id => id !== parentTxid
        );

        // 如果所有依賴都解決了
        if (orphan.missingInputs.length === 0) {
          resolved.push(orphan.tx);
          this.removeOrphan(orphan.tx.txid);
        }
      }
    }

    return resolved;  // 返回可以處理的交易
  }

  // 定期清理過期的孤立交易
  expire(): number {
    const now = Date.now();
    let removed = 0;

    for (const [txid, orphan] of this.orphans) {
      if (orphan.timeExpire < now) {
        this.removeOrphan(txid);
        removed++;
      }
    }

    return removed;
  }
}

限制與保護

孤立交易池限制

限制 目的
最大數量 100 筆 防止記憶體耗盡
單筆大小 100 KB 防止大交易 DoS
過期時間 20 分鐘 清理無效的孤立交易
每個節點限制 追蹤來源 防止單節點濫用

DoS 防護

  • 攻擊者可能發送大量引用不存在父交易的孤立交易
  • 隨機驅逐策略防止特定交易被保留
  • 大小限制防止記憶體耗盡
  • 過期機制自動清理

解決流程

class MempoolManager {
  private mempool: Mempool;
  private orphanPool: OrphanPool;

  // 接收新交易
  async processTransaction(tx: Transaction, fromPeer: number): Promise {
    // 1. 嘗試添加到 mempool
    const result = await this.mempool.acceptTransaction(tx);

    if (result.success) {
      // 2. 成功!檢查是否有孤立交易在等待這個
      const resolved = this.orphanPool.onParentReceived(tx.txid);

      // 3. 遞迴處理解決的孤立交易
      for (const orphanTx of resolved) {
        await this.processTransaction(orphanTx, fromPeer);
      }
    } else if (result.error === 'missing-inputs') {
      // 4. 缺少父交易,添加到孤立池
      this.orphanPool.addOrphanTx(tx, fromPeer);

      // 5. 可選:請求缺失的父交易
      for (const input of tx.inputs) {
        if (!this.mempool.hasTx(input.txid)) {
          this.requestTx(fromPeer, input.txid);
        }
      }
    }
    // 其他錯誤:丟棄交易
  }
}

孤立交易 vs 孤立區塊

特性 孤立交易 孤立區塊
缺失 父交易 父區塊
儲存位置 記憶體 記憶體
最大數量 100 無硬限制
常見原因 CPFP、網路延遲 競爭區塊、同步

總結

  • 定義:引用未知父交易的交易
  • 處理:暫存在孤立池,父交易到達後處理
  • 限制:最多 100 筆,單筆最大 100KB
  • 過期:20 分鐘後自動清理
已複製連結
已複製到剪貼簿