進階
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 攻擊可用於追蹤地址關聯
已複製連結