入門
Notfound Message
了解 P2P 協議中的 notfound 消息,用於通知請求的數據不可用。
6 分鐘
Notfound 消息是比特幣 P2P 協議中用於回應 getdata 請求的消息, 當請求的交易或區塊不可用時發送。這是正常的協議行為,不表示錯誤。
Notfound 概述
Notfound 的作用:
正常流程:
┌─────────┐ getdata(txid) ┌─────────┐
│ Node A │ ──────────────→ │ Node B │
│ │ tx │ │
│ │ ←────────────── │ │
└─────────┘ └─────────┘
數據不可用:
┌─────────┐ getdata(txid) ┌─────────┐
│ Node A │ ──────────────→ │ Node B │
│ │ notfound │ │
│ │ ←────────────── │ │
└─────────┘ └─────────┘
為什麼數據可能不可用:
1. 交易已確認,不在 mempool
2. 區塊已被修剪
3. 從未收到過該數據
4. 競態條件(剛剛被移除) 消息格式
Notfound 消息結構:
┌─────────────────────────────────────────┐
│ count (varint) │
│ inventory: │
│ ┌─────────────────────────────────┐ │
│ │ type (4 bytes, uint32) │ │
│ │ hash (32 bytes) │ │
│ └─────────────────────────────────┘ │
│ × count │
└─────────────────────────────────────────┘
與 inv/getdata 格式相同!
庫存類型:
MSG_TX = 1
MSG_BLOCK = 2
MSG_WTX = 5
MSG_WITNESS_TX = 0x40000001
MSG_WITNESS_BLOCK = 0x40000002
範例(交易不存在):
count: 1
type: 1 (MSG_TX)
hash: abc123... (請求的 txid) 觸發條件
何時發送 notfound:
1. 交易請求
if getdata.type == MSG_TX:
tx = mempool.get(hash)
if tx is None:
# 交易不在 mempool
send_notfound(MSG_TX, hash)
常見原因:
- 交易已被確認
- 交易被驅逐
- 從未收到過
2. 區塊請求
if getdata.type == MSG_BLOCK:
block = read_block(hash)
if block is None:
# 區塊不存在
send_notfound(MSG_BLOCK, hash)
常見原因:
- 區塊已被修剪
- 請求了未知的區塊
- 區塊在分叉鏈上
3. 批量請求
對於 getdata 中的多個項目:
- 可用的直接返回
- 不可用的合併到一個 notfound 接收方處理
處理收到的 notfound:
def on_notfound(items):
for item in items:
# 從 in_flight 移除
in_flight.remove(item.hash)
if item.type == MSG_TX:
# 交易不可用
# 可能嘗試其他節點
if want_tx(item.hash):
request_from_other_peer(item.hash)
elif item.type == MSG_BLOCK:
# 區塊不可用
# 標記此節點沒有該區塊
peer.does_not_have(item.hash)
# 從其他節點請求
request_from_other_peer(item.hash)
重要:
- notfound 是正常行為
- 不增加 misbehavior 分數
- 不應斷開連接 與其他消息的關係
Notfound 在數據獲取流程中的位置:
完整流程:
inv → getdata → tx/block OR notfound
範例 1: 交易可用
peer_a: inv(tx_123)
peer_b: getdata(tx_123)
peer_a: tx(...)
範例 2: 交易不可用
peer_a: inv(tx_123)
# ... 一些時間過去,交易被確認 ...
peer_b: getdata(tx_123)
peer_a: notfound(tx_123)
範例 3: 混合回應
peer_b: getdata([tx_1, tx_2, tx_3])
peer_a: tx(tx_1) # 有
peer_a: tx(tx_2) # 有
peer_a: notfound([tx_3]) # 沒有
// notfound 通常在其他數據之後發送 常見場景
Notfound 的常見場景:
1. 交易競態
時間線:
T0: 節點 A 發送 inv(tx)
T1: tx 被確認進入區塊
T2: 節點 B 發送 getdata(tx)
T3: 節點 A 回覆 notfound(tx)
原因: tx 已從 mempool 移除
2. 修剪節點
- 只保留最近 N 個區塊
- 請求舊區塊會收到 notfound
- 需要從完整節點請求
3. 孤立交易清理
- 孤立交易可能被清理
- 請求時已不存在
4. Mempool 驅逐
- 低費率交易可能被驅逐
- 請求時已不在 mempool
// 這些都是正常情況 調試與監控
# 監控 notfound 消息
# 日誌設置
# bitcoin.conf
debug=net
# 日誌輸出:
# "Sending notfound for txid abc123 to peer=5"
# "Received notfound for 2 items from peer=3"
# 查看節點連接狀態
bitcoin-cli getpeerinfo | jq '.[] | {
id: .id,
addr: .addr,
bytesrecv_per_msg: .bytesrecv_per_msg.notfound
}'
# 如果頻繁收到 notfound:
# 1. 可能與修剪節點連接
# 2. 可能網路延遲導致競態
# 3. 通常不需要擔心
# 檢查是否為修剪節點
bitcoin-cli getpeerinfo | jq '.[] | {
id: .id,
services: .services
}'
# NODE_NETWORK_LIMITED (1024) 表示修剪節點 實現細節
Bitcoin Core 中的實現:
// 發送 notfound
void PeerManager::SendNotFound(CNode& node, const std::vector<CInv>& vNotFound) {
if (vNotFound.empty()) {
return;
}
connman.PushMessage(&node,
CNetMsgMaker(node.GetCommonVersion())
.Make(NetMsgType::NOTFOUND, vNotFound));
}
// 在處理 getdata 時
void PeerManager::ProcessGetData(CNode& node, std::vector<CInv>& vInv) {
std::vector<CInv> vNotFound;
for (const CInv& inv : vInv) {
bool sent = 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));
sent = true;
}
} else if (inv.type == MSG_BLOCK) {
// 嘗試讀取區塊...
}
if (!sent) {
vNotFound.push_back(inv);
}
}
// 發送所有找不到的項目
SendNotFound(node, vNotFound);
}
// 處理收到的 notfound
void PeerManager::ProcessNotFound(CNode& node, std::vector<CInv>& vInv) {
for (const CInv& inv : vInv) {
// 從 in-flight 移除
RemoveFromInFlight(node.GetId(), inv);
// 可能請求其他節點
if (inv.type == MSG_BLOCK) {
TryRequestBlockFromOtherPeer(inv.hash);
}
}
} 相關概念
- Getdata Message:數據請求消息
- Inv Message:庫存通告消息
- Message Types:P2P 消息類型
- Mempool:交易池
- Pruning:區塊鏈修剪
已複製連結