跳至主要內容
進階

Dust Limits

深入了解比特幣的 Dust Limit 機制,為什麼微小金額的輸出會被視為不經濟,以及如何計算和處理。

10 分鐘

什麼是 Dust?

Dust(灰塵)是指金額太小、以至於花費它們的手續費會超過其價值的交易輸出。 Bitcoin Core 定義了 Dust Limit(灰塵限制),低於此限制的輸出被視為不經濟(uneconomical), 預設情況下不會被節點中繼或包含在區塊中。

Dust 的問題

對網路的影響

  • • 永久增大 UTXO 集合
  • • 消耗節點記憶體和磁碟
  • • 可被用於 DoS 攻擊
  • • 降低整體效能

對用戶的影響

  • • 花費成本超過價值
  • • 錢包顯示無法使用的餘額
  • • 可能洩露隱私(dust 攻擊)
  • • 增加 UTXO 管理複雜度

Dust Limit 計算

計算公式

Dust Limit 基於花費該輸出所需的最小交易大小計算。一個輸出被視為 dust,當:

輸出金額 < (輸入大小 + 32) × dustRelayFee / 1000

其中:
- 輸入大小:花費此輸出需要的輸入位元組數
- 32:輸出序列化開銷
- dustRelayFee:預設 3000 sat/kvB(3 sat/vB)

不同腳本類型的輸入大小

腳本類型 輸入大小 (vB) Dust Limit
P2PKH (Legacy) 148 vB 546 sats
P2SH ~91 vB 540 sats
P2WPKH (SegWit) 68 vB 294 sats
P2WSH ~68 vB 330 sats
P2TR (Taproot) 57.5 vB 330 sats

* 計算基於 dustRelayFee = 3000 sat/kvB

實現細節

Bitcoin Core 實現

// src/policy/policy.cpp
bool IsDust(const CTxOut& txout, const CFeeRate& dustRelayFeeIn)
{
    // 如果輸出不可花費(如 OP_RETURN),不是 dust
    if (txout.scriptPubKey.IsUnspendable())
        return false;

    // 獲取花費此輸出所需的輸入大小
    size_t nSize = GetDustInputSize(txout);

    // 加上輸出本身的開銷
    nSize += 32 + 4 + 1 + 8;  // prevout + sequence + scriptLen + amount

    // 計算 dust 閾值
    return (txout.nValue < dustRelayFeeIn.GetFee(nSize));
}

// 計算輸入大小
size_t GetDustInputSize(const CTxOut& txout)
{
    // P2PKH: 32 + 4 + 1 + 107 + 4 = 148 bytes
    // P2WPKH: 32 + 4 + 1 + 1 + 4 + (1 + 72 + 1 + 33) / 4 = ~68 vbytes
    // P2TR: 32 + 4 + 1 + 1 + 4 + (1 + 64) / 4 = ~57.5 vbytes

    if (txout.scriptPubKey.IsPayToScriptHash()) {
        return 91;
    } else if (txout.scriptPubKey.IsPayToWitnessScriptHash()) {
        return 68;
    } else if (txout.scriptPubKey.IsPayToTaproot()) {
        return 58;
    }
    // Default: P2PKH
    return 148;
}

TypeScript 實現

enum ScriptType {
  P2PKH = 'p2pkh',
  P2SH = 'p2sh',
  P2WPKH = 'p2wpkh',
  P2WSH = 'p2wsh',
  P2TR = 'p2tr',
}

// 花費不同類型輸出所需的輸入大小(vbytes)
const INPUT_SIZES: Record = {
  [ScriptType.P2PKH]: 148,
  [ScriptType.P2SH]: 91,
  [ScriptType.P2WPKH]: 68,
  [ScriptType.P2WSH]: 68,
  [ScriptType.P2TR]: 58,
};

// 輸出序列化開銷
const OUTPUT_OVERHEAD = 32;  // prevout hash + index + sequence + script overhead

interface DustCalculator {
  dustRelayFee: number;  // sats per 1000 vbytes
}

function getDustLimit(
  scriptType: ScriptType,
  dustRelayFee: number = 3000  // 預設 3 sat/vB
): number {
  const inputSize = INPUT_SIZES[scriptType];
  const totalSize = inputSize + OUTPUT_OVERHEAD;

  // dust limit = size * feeRate / 1000
  return Math.ceil((totalSize * dustRelayFee) / 1000);
}

function isDust(
  amount: number,
  scriptType: ScriptType,
  dustRelayFee: number = 3000
): boolean {
  const limit = getDustLimit(scriptType, dustRelayFee);
  return amount < limit;
}

// 使用範例
console.log('P2PKH dust limit:', getDustLimit(ScriptType.P2PKH));  // 546
console.log('P2WPKH dust limit:', getDustLimit(ScriptType.P2WPKH));  // 294
console.log('P2TR dust limit:', getDustLimit(ScriptType.P2TR));  // 330

console.log('Is 500 sats P2WPKH dust?', isDust(500, ScriptType.P2WPKH));  // false
console.log('Is 200 sats P2WPKH dust?', isDust(200, ScriptType.P2WPKH));  // true

// 計算在不同手續費率下的有效性
function getEffectiveValue(
  amount: number,
  scriptType: ScriptType,
  feeRate: number  // sat/vB
): number {
  const inputSize = INPUT_SIZES[scriptType];
  const spendCost = inputSize * feeRate;
  return amount - spendCost;
}

// 當 feeRate = 10 sat/vB 時
console.log('1000 sats P2WPKH effective value at 10 sat/vB:',
  getEffectiveValue(1000, ScriptType.P2WPKH, 10));  // 320 sats
console.log('500 sats P2WPKH effective value at 10 sat/vB:',
  getEffectiveValue(500, ScriptType.P2WPKH, 10));  // -180 sats (負數!)

配置選項

節點設定

# 查看當前 dust relay fee
bitcoin-cli getmempoolinfo | jq '.minrelaytxfee'

# 設定自訂 dust relay fee(bitcoin.conf)
# 降低 dust limit(接受更小的輸出)
dustrelayfee=0.00001000  # 1000 sat/kvB = 1 sat/vB

# 完全禁用 dust 檢查(不推薦)
# dustrelayfee=0

# 提高 dust limit(更嚴格)
dustrelayfee=0.00005000  # 5000 sat/kvB = 5 sat/vB

注意: 修改 dustrelayfee 只影響本地節點的中繼策略。即使你的節點接受較低金額的輸出, 其他節點可能仍會拒絕中繼,導致交易無法傳播。

Dust 攻擊

什麼是 Dust 攻擊?

Dust 攻擊是一種隱私攻擊,攻擊者向大量地址發送微小金額(剛好高於 dust limit), 然後追蹤這些 UTXO 被花費時與哪些其他 UTXO 合併,從而建立地址關聯。

Dust 攻擊流程:

1. 攻擊者識別目標
   [地址 A] [地址 B] [地址 C] ...

2. 發送微小金額(如 546 sats)到每個地址
   攻擊者 → 546 sats → 地址 A
   攻擊者 → 546 sats → 地址 B
   攻擊者 → 546 sats → 地址 C

3. 等待用戶花費這些 dust
   用戶創建交易,合併多個 UTXO:
   輸入 1: 地址 A 的 dust (546 sats)
   輸入 2: 地址 B 的正常 UTXO (0.1 BTC)
   輸入 3: 地址 C 的正常 UTXO (0.2 BTC)

4. 攻擊者分析鏈上數據
   現在知道地址 A、B、C 可能屬於同一用戶

防禦措施

✓ 推薦做法

  • 不花費 dust:讓 dust UTXO 保持未花費
  • 隔離 dust:在單獨的錢包中接收 dust
  • Coin control:手動選擇要合併的 UTXO
  • 使用 CoinJoin:混淆輸入來源

錢包處理

  • • 標記可疑的小額輸入
  • • 提供「凍結 UTXO」選項
  • • 警告用戶合併 dust 的風險
  • • 自動排除 dust 於 coin selection

經濟性 Dust

動態 Dust 閾值

雖然協議的 dust limit 是固定的,但「經濟性 dust」會隨著實際手續費率變化:

// 計算在給定手續費率下的經濟性 dust 閾值
function getEconomicDustLimit(
  scriptType: ScriptType,
  currentFeeRate: number  // sat/vB
): number {
  const inputSize = INPUT_SIZES[scriptType];
  // 當花費成本等於金額時,就是經濟性 dust 閾值
  return inputSize * currentFeeRate;
}

// 不同手續費率下的經濟性 dust(P2WPKH)
const feeRates = [1, 5, 10, 20, 50, 100];
for (const rate of feeRates) {
  const limit = getEconomicDustLimit(ScriptType.P2WPKH, rate);
  console.log(`${rate} sat/vB: ${limit} sats`);
}

// 輸出:
// 1 sat/vB: 68 sats
// 5 sat/vB: 340 sats
// 10 sat/vB: 680 sats
// 20 sat/vB: 1360 sats
// 50 sat/vB: 3400 sats
// 100 sat/vB: 6800 sats

經濟性 Dust 示例

手續費率 P2PKH P2WPKH P2TR
1 sat/vB 148 sats 68 sats 58 sats
10 sat/vB 1,480 sats 680 sats 580 sats
50 sat/vB 7,400 sats 3,400 sats 2,900 sats
100 sat/vB 14,800 sats 6,800 sats 5,800 sats

UTXO 管理策略

UTXO 整合

在低手續費期間整合小額 UTXO 是一個好策略:

interface UTXO {
  txid: string;
  vout: number;
  amount: number;  // sats
  scriptType: ScriptType;
}

function shouldConsolidate(
  utxos: UTXO[],
  currentFeeRate: number,
  targetFeeRate: number  // 預期未來手續費率
): UTXO[] {
  const toConsolidate: UTXO[] = [];

  for (const utxo of utxos) {
    const economicLimit = getEconomicDustLimit(utxo.scriptType, targetFeeRate);

    // 如果 UTXO 在未來可能變成經濟性 dust
    // 且當前花費它是有利的
    if (utxo.amount < economicLimit * 2) {  // 留有餘裕
      const currentCost = INPUT_SIZES[utxo.scriptType] * currentFeeRate;
      if (utxo.amount > currentCost) {
        toConsolidate.push(utxo);
      }
    }
  }

  return toConsolidate;
}

// 示例:在 1 sat/vB 時整合
const utxos: UTXO[] = [
  { txid: 'abc', vout: 0, amount: 1000, scriptType: ScriptType.P2WPKH },
  { txid: 'def', vout: 1, amount: 5000, scriptType: ScriptType.P2WPKH },
  { txid: 'ghi', vout: 0, amount: 500, scriptType: ScriptType.P2WPKH },
];

const consolidate = shouldConsolidate(utxos, 1, 50);
// 建議整合:1000 sats 和 500 sats 的 UTXO
// 因為在 50 sat/vB 時它們會變成經濟性 dust

錢包建議

✓ 推薦做法

  • • 在低費率期間整合小額 UTXO
  • • 使用 SegWit/Taproot 降低 dust limit
  • • 設定最小接收金額
  • • 監控 UTXO 集合健康度

✗ 避免

  • • 在高費率期間花費小額 UTXO
  • • 創建接近 dust limit 的輸出
  • • 忽略 UTXO 管理
  • • 接受來路不明的小額付款

特殊情況

OP_RETURN 輸出

OP_RETURN 輸出是不可花費的,因此不受 dust limit 限制:

# OP_RETURN 輸出可以是 0 sats
OP_RETURN   // 金額:0 sats,這是合法的

# 因為 OP_RETURN 不會進入 UTXO 集合
# 不會造成永久的狀態膨脹

Anchor Outputs

Lightning Network 使用的 anchor outputs 有特殊的 dust 處理:

# Lightning anchor outputs
# 固定金額:330 sats(剛好高於 P2WSH dust limit)

Anchor Output:
  金額: 330 sats
  腳本:  OP_CHECKSIG OP_IFDUP OP_NOTIF OP_16 OP_CSV OP_ENDIF

# 用途:
# - 允許 CPFP 費用提升
# - 確保承諾交易可以被確認
# - 16 區塊後任何人都可以花費(清理機制)

未來發展

相關提案和討論

  • 動態 Dust Limit: 根據當前 mempool 狀態動態調整 dust limit。
  • UTXO 過期: 讓長期未使用的 dust UTXO 可以被「回收」。
  • Ephemeral Anchors: 允許 0 值輸出作為臨時 anchor,必須在同一區塊內花費。

總結

  • 保護 UTXO 集合:Dust limit 防止微小輸出永久佔用節點資源
  • 類型相關:SegWit/Taproot 有更低的 dust limit(~300 sats vs ~550 sats)
  • 經濟性考量:實際的「不經濟」閾值取決於當前手續費率
  • 隱私風險:Dust 攻擊可用於追蹤地址關聯
已複製連結
已複製到剪貼簿