跳至主要內容
進階

Pruning

深入了解 Bitcoin Core 的區塊鏈修剪機制,如何在有限硬碟空間下運行完整驗證節點。

10 分鐘

什麼是 Pruning?

Pruning(修剪)是 Bitcoin Core 的一項功能,允許節點在完成驗證後刪除舊的區塊數據,只保留 UTXO 集合和最近的區塊。 這讓用戶可以在有限的硬碟空間下運行完整驗證節點。

存儲需求比較

~600 GB
完整區塊鏈
(持續增長)
~10 GB
修剪節點(最小)
(550 MiB 區塊)
~8 GB
UTXO 集合
(必須保留)

運作原理

保留的數據

✓ 永久保留

  • UTXO 集合:所有未花費的交易輸出
  • 區塊頭:所有區塊的 80 位元組頭部
  • 區塊索引:區塊位置和元數據
  • 鏈狀態:當前鏈頂和驗證狀態

✗ 被刪除

  • 舊區塊體:完整交易數據
  • 舊區塊撤銷數據:用於重組的回滾數據
  • 已花費輸出:歷史 UTXO

修剪流程

初始區塊下載(IBD)
        ↓
接收區塊 → 完整驗證 → 更新 UTXO → 寫入磁碟
                                      ↓
                              區塊數據超過限制?
                                   ↙    ↘
                                 否       是
                                 ↓        ↓
                               繼續    刪除最舊區塊
                                          ↓
                                    保留區塊頭
                                          ↓
                                    釋放磁碟空間

目標大小

修剪目標是保留的區塊數據量,不包括 UTXO 集合和索引。最小值是 550 MiB,建議至少 1000 MiB:

# 最小修剪(550 MiB 區塊數據)
bitcoind -prune=550

# 建議設定(2 GB 區塊數據)
bitcoind -prune=2000

# 保留約一週的區塊(~10 GB)
bitcoind -prune=10000

# 配置文件設定
echo "prune=2000" >> ~/.bitcoin/bitcoin.conf

配置選項

修剪模式

設定 說明 磁碟使用
prune=0 禁用修剪(預設) ~600 GB+
prune=1 手動修剪模式 可變
prune=550 自動修剪(最小) ~10 GB
prune=10000 自動修剪(10 GB) ~18 GB

手動修剪

# 啟用手動修剪模式
bitcoind -prune=1

# 手動修剪到指定高度
bitcoin-cli pruneblockchain 800000

# 返回實際修剪到的高度
# {
#   "result": 800000
# }

# 查看修剪狀態
bitcoin-cli getblockchaininfo | grep prune

相關 RPC 命令

# 查看區塊鏈資訊
bitcoin-cli getblockchaininfo
# {
#   "pruned": true,
#   "pruneheight": 750000,
#   "prune_target_size": 576716800,
#   ...
# }

# 手動觸發修剪
bitcoin-cli pruneblockchain 800000

# 查看磁碟使用
bitcoin-cli gettxoutsetinfo
# {
#   "disk_size": 8234567890,  # UTXO 集合大小
#   ...
# }

功能限制

修剪節點的限制

  • 無法提供歷史區塊: 修剪節點不能為其他節點提供已刪除的區塊數據。
  • 無法重新掃描: rescanblockchain 只能掃描保留的區塊。
  • 無法使用 txindex: 交易索引需要完整區塊數據,與修剪不兼容。
  • 錢包導入受限: 導入舊地址可能無法找到歷史交易。

仍然可行的功能

  • • 完整交易驗證(當前和未來)
  • • 接收和發送比特幣
  • • 驗證新區塊和交易
  • • 提供區塊頭給 SPV 客戶端
  • • 提供保留範圍內的區塊
  • • 參與交易中繼

實現細節

區塊文件結構

Bitcoin Core 將區塊存儲在 blk*.dat 文件中,每個文件最大約 128 MB:

~/.bitcoin/blocks/
├── blk00000.dat      # 區塊數據
├── blk00001.dat
├── ...
├── blkNNNNN.dat
├── rev00000.dat      # 撤銷數據(用於重組)
├── rev00001.dat
├── ...
└── index/            # LevelDB 索引
    ├── 000001.log
    ├── CURRENT
    └── ...

修剪演算法

interface PruneState {
  pruneTarget: number;      // 目標保留大小(位元組)
  currentSize: number;      // 當前區塊數據大小
  pruneHeight: number;      // 已修剪到的高度
  minBlocksToKeep: number;  // 最少保留區塊數
}

class BlockPruner {
  private readonly MIN_BLOCKS = 288;  // 約 2 天的區塊

  async maybePrune(state: PruneState): Promise {
    // 檢查是否需要修剪
    if (state.currentSize <= state.pruneTarget) {
      return;
    }

    const chainHeight = await this.getChainHeight();
    const minKeepHeight = chainHeight - this.MIN_BLOCKS;

    // 從最舊的區塊開始刪除
    let heightToDelete = state.pruneHeight + 1;

    while (
      state.currentSize > state.pruneTarget &&
      heightToDelete < minKeepHeight
    ) {
      await this.pruneBlock(heightToDelete);
      heightToDelete++;
    }

    console.log(`Pruned to height ${heightToDelete - 1}`);
  }

  private async pruneBlock(height: number): Promise {
    // 1. 刪除區塊數據文件中的數據
    await this.deleteBlockData(height);

    // 2. 刪除對應的撤銷數據
    await this.deleteUndoData(height);

    // 3. 更新索引(保留區塊頭)
    await this.updateIndex(height, { pruned: true });
  }
}

// 文件級修剪
class FileBasedPruner {
  private readonly FILE_SIZE = 128 * 1024 * 1024;  // 128 MB

  async pruneOldFiles(
    currentSize: number,
    targetSize: number
  ): Promise {
    let prunedSize = 0;
    const files = await this.getBlockFiles();

    // 按順序刪除舊文件
    for (const file of files) {
      if (currentSize - prunedSize <= targetSize) {
        break;
      }

      // 檢查文件中的區塊是否都可以刪除
      if (await this.canPruneFile(file)) {
        const fileSize = await this.deleteFile(file);
        prunedSize += fileSize;
        console.log(`Deleted ${file}, freed ${fileSize} bytes`);
      }
    }

    return prunedSize;
  }

  private async canPruneFile(file: string): Promise {
    // 檢查文件中是否有需要保留的區塊
    const blocks = await this.getBlocksInFile(file);
    const minHeight = await this.getMinKeepHeight();

    return blocks.every(b => b.height < minHeight);
  }
}

網路影響

區塊服務能力

修剪節點會在 P2P 網路中廣播其能提供的區塊範圍:

服務標誌位:
- NODE_NETWORK (1)        : 可以提供完整區塊歷史
- NODE_NETWORK_LIMITED (1024) : 只能提供最近的區塊

修剪節點使用 NODE_NETWORK_LIMITED,表示:
- 可以提供最近 288 個區塊(約 2 天)
- 不能提供更早的區塊
- 仍然可以中繼新交易和區塊

IBD 對等節點選擇

新節點在進行初始區塊下載時,會優先連接具有 NODE_NETWORK 標誌的完整節點,因為修剪節點無法提供完整歷史。

錢包注意事項

新錢包

對於新創建的錢包,修剪節點運作正常。錢包只需要追蹤從創建時間之後的交易。

導入密鑰

警告:導入舊密鑰

如果導入一個有歷史交易的密鑰,修剪節點可能無法找到相關交易:

# 這可能不會找到所有歷史交易
bitcoin-cli importprivkey "your-private-key"

# 解決方案 1:使用 rescanblockchain 掃描保留的區塊
bitcoin-cli rescanblockchain 800000  # 只能掃描保留的範圍

# 解決方案 2:使用完整節點導入
# 或使用外部區塊瀏覽器查看歷史

# 解決方案 3:使用 descriptor 錢包指定出生高度
bitcoin-cli importdescriptors '[{
  "desc": "wpkh(your-xpub/0/*)#checksum",
  "timestamp": "2023-01-01"
}]'

與其他方案比較

方案 存儲 驗證 信任
完整節點 ~600 GB 完整 無需信任
修剪節點 ~10 GB 完整 無需信任
SPV 錢包 ~50 MB 僅 PoW 信任節點
託管錢包 0 完全信任

結論

修剪節點提供與完整節點相同的安全保證,因為它在刪除數據之前已經驗證了所有內容。 唯一的代價是無法為其他節點提供歷史區塊。

設定指南

全新安裝

# 1. 創建配置文件
mkdir -p ~/.bitcoin
cat > ~/.bitcoin/bitcoin.conf << 'EOF'
# 啟用修剪,保留 2 GB 區塊數據
prune=2000

# 其他推薦設定
server=1
txindex=0  # 修剪模式下必須禁用
EOF

# 2. 啟動節點
bitcoind

# 3. 等待同步完成
bitcoin-cli getblockchaininfo

轉換現有節點

# 1. 停止節點
bitcoin-cli stop

# 2. 編輯配置文件
echo "prune=2000" >> ~/.bitcoin/bitcoin.conf

# 3. 如果之前啟用了 txindex,必須禁用
sed -i 's/txindex=1/txindex=0/' ~/.bitcoin/bitcoin.conf

# 4. 重新啟動
bitcoind

# 節點會在運行時自動開始修剪舊區塊

注意: 如果之前啟用了 txindex,需要在轉換前禁用它,否則節點會報錯。 禁用 txindex 後需要重新建立索引或刪除索引文件。

最佳實踐

✓ 推薦

  • • 設定 prune >= 1000(至少 1 GB)
  • • 使用 SSD 以獲得更好效能
  • • 定期備份錢包文件
  • • 監控磁碟空間使用

✗ 避免

  • • 不要同時啟用 txindex
  • • 不要頻繁導入舊密鑰
  • • 不要期望提供歷史區塊
  • • 不要設定過小的修剪目標

總結

  • 完整驗證:修剪節點驗證所有交易和區塊,安全性與完整節點相同
  • 低存儲需求:只需約 10 GB 即可運行完整驗證節點
  • 保留 UTXO:所有未花費輸出都被保留,支援正常交易
  • 有限服務:無法為其他節點提供歷史區塊,無法使用 txindex
已複製連結
已複製到剪貼簿