進階
Getdata Message
了解 P2P 協議中的 getdata 消息,用於請求交易、區塊等具體數據。
8 分鐘
Getdata 消息是比特幣 P2P 協議中用於請求具體數據的消息類型。 當節點收到 inv 通告後,使用 getdata 請求感興趣的交易或區塊的完整數據。
Getdata 消息概述
Getdata 在數據獲取中的角色:
標準流程:
┌─────────┐ inv(items) ┌─────────┐
│ Node A │ ──────────────→ │ Node B │
│ │ getdata(items) │ │
│ │ ←────────────── │ │
│ │ tx / block │ │
│ │ ──────────────→ │ │
└─────────┘ └─────────┘
特點:
1. 格式與 inv 相同
2. 用於請求具體數據
3. 回應是對應的數據消息
可請求的數據類型:
- 交易 (tx)
- 區塊 (block)
- 緊湊區塊 (cmpctblock)
- Merkle 區塊 (merkleblock)
- 見證交易 (witnesstx) 消息格式
Getdata 消息結構:
┌─────────────────────────────────────────┐
│ count (varint) │
│ inventory: │
│ ┌─────────────────────────────────┐ │
│ │ type (4 bytes, uint32) │ │
│ │ hash (32 bytes) │ │
│ └─────────────────────────────────┘ │
│ × count │
└─────────────────────────────────────────┘
與 inv 格式完全相同!
庫存類型 (type):
MSG_TX = 1
MSG_BLOCK = 2
MSG_FILTERED_BLOCK = 3 // merkleblock
MSG_CMPCT_BLOCK = 4
MSG_WTX = 5
MSG_WITNESS_TX = 0x40000001
MSG_WITNESS_BLOCK = 0x40000002
範例(請求一個交易):
01 // count = 1
01 00 00 00 // type = MSG_TX
abc123... // txid (32 bytes) 請求處理
節點如何回應 getdata:
對於交易請求 (MSG_TX / MSG_WTX):
1. 檢查 mempool
2. 如果找到,發送 tx 消息
3. 如果沒找到,發送 notfound
對於區塊請求 (MSG_BLOCK):
1. 檢查本地區塊數據
2. 如果找到,發送 block 消息
3. 如果沒找到,發送 notfound
對於緊湊區塊請求 (MSG_CMPCT_BLOCK):
1. 檢查是否支持緊湊區塊
2. 發送 cmpctblock 消息
對於 Merkle 區塊請求 (MSG_FILTERED_BLOCK):
1. 檢查是否有 bloom filter
2. 發送 merkleblock + 匹配的交易
代碼邏輯:
switch (inv.type) {
case MSG_TX:
tx = mempool.get(inv.hash);
if (tx) send_tx(tx);
else send_notfound(inv);
break;
case MSG_BLOCK:
block = ReadBlockFromDisk(inv.hash);
if (block) send_block(block);
else send_notfound(inv);
break;
} 見證數據請求
請求帶見證數據的內容:
MSG_WITNESS_TX (0x40000001):
- 請求帶見證的交易
- 回應包含完整 SegWit 數據
- 用於 SegWit 節點之間
MSG_WITNESS_BLOCK (0x40000002):
- 請求帶見證的區塊
- 區塊內所有交易包含見證
- 完整的 SegWit 區塊
服務標誌:
- NODE_WITNESS (1 << 3) 表示支持見證數據
- 檢查對方服務標誌再請求
請求邏輯:
if (peer.services & NODE_WITNESS) {
// 可以請求見證數據
getdata(MSG_WITNESS_BLOCK, hash);
} else {
// 只能請求剝離的數據
getdata(MSG_BLOCK, hash);
}
// 見證請求類型 = 基本類型 | MSG_WITNESS_FLAG
// MSG_WITNESS_FLAG = 1 << 30 請求排隊與超時
管理待處理的請求:
請求隊列:
class Peer {
std::map<uint256, int64_t> in_flight; // hash → 請求時間
std::deque<CInv> request_queue; // 待請求
};
超時處理:
GETDATA_TX_TIMEOUT = 60s // 交易請求超時
GETDATA_BLOCK_TIMEOUT = 120s // 區塊請求超時
超時後:
1. 從 in_flight 移除
2. 可能向其他節點請求
3. 可能增加對方的 misbehavior 分數
並發限制:
MAX_BLOCKS_IN_FLIGHT = 16 // 每個節點最多待處理區塊
MAX_TX_IN_FLIGHT = 100 // 每個節點最多待處理交易
防止:
- 單個節點壟斷下載
- 資源耗盡攻擊
- 長時間等待 Notfound 回應
當數據不可用時:
Notfound 消息:
┌─────────────────────────────────────────┐
│ count (varint) │
│ inventory: (same as inv/getdata) │
└─────────────────────────────────────────┘
觸發條件:
1. 請求的交易不在 mempool
2. 請求的區塊未存儲
3. 數據已被修剪
處理 notfound:
1. 從 in_flight 移除
2. 可能向其他節點請求
3. 不增加 misbehavior(正常情況)
範例場景:
- 交易已確認,不再在 mempool
- 請求了修剪節點沒有的舊區塊
- 競態條件導致數據消失
// notfound 是正常的協議行為,不是錯誤 優化策略
Getdata 的優化:
1. 批量請求
- 累積多個請求
- 一次發送減少往返
- 單個 getdata 最多 50000 項
2. 來源選擇
- 從多個節點請求不同區塊
- 並行下載加速同步
- 避免單點瓶頸
3. 優先級
- 區塊優先於交易
- 鏈頂區塊優先於舊區塊
- 緊急數據立即請求
4. 重試策略
- 超時後換節點請求
- 保持請求歷史
- 避免重複請求失敗節點
代碼:
void ScheduleRequests() {
// 按優先級排序
sort(requests, by_priority);
// 分配給不同節點
for (auto& req : requests) {
auto peer = SelectBestPeer(req);
peer.send_getdata(req);
}
} 調試與監控
# 監控 getdata 活動
# 查看節點下載狀態
bitcoin-cli getpeerinfo | jq '.[] | {
id: .id,
addr: .addr,
inflight: .inflight
}'
# inflight 顯示正在請求的區塊
# 日誌設置
# bitcoin.conf
debug=net
# 日誌輸出:
# "sending getdata (37 bytes) peer=5"
# "received getdata with 3 items from peer=3"
# "sending notfound for txid=abc123"
# 查看區塊下載進度
bitcoin-cli getblockchaininfo | jq '{
blocks: .blocks,
headers: .headers,
verificationprogress: .verificationprogress
}' 實現細節
Bitcoin Core 中的實現:
// 發送 getdata
void PeerManager::SendGetData(CNode& node, std::vector<CInv>& vInv) {
if (vInv.empty()) return;
// 記錄 in_flight
for (const auto& inv : vInv) {
node.m_tx_relay->m_in_flight.emplace(inv.hash, GetTime());
}
connman.PushMessage(&node,
CNetMsgMaker(node.GetCommonVersion())
.Make(NetMsgType::GETDATA, vInv));
}
// 處理收到的 getdata
void PeerManager::ProcessGetData(CNode& node, std::vector<CInv>& vInv) {
std::vector<CInv> vNotFound;
for (const CInv& inv : vInv) {
bool found = false;
if (inv.type == MSG_TX || inv.type == MSG_WTX) {
auto tx = mempool.get(GetTxIdFromInv(inv));
if (tx) {
connman.PushMessage(&node,
CNetMsgMaker(node.GetCommonVersion())
.Make(NetMsgType::TX, *tx));
found = true;
}
} else if (inv.type == MSG_BLOCK) {
// 讀取區塊...
}
if (!found) {
vNotFound.push_back(inv);
}
}
if (!vNotFound.empty()) {
connman.PushMessage(&node,
CNetMsgMaker(node.GetCommonVersion())
.Make(NetMsgType::NOTFOUND, vNotFound));
}
} 相關概念
- Inv Message:庫存通告消息
- Message Types:P2P 消息類型
- Block Download:區塊下載策略
- Transaction Broadcast:交易廣播
- Compact Blocks:緊湊區塊
已複製連結