跳至主要內容
進階

Version Handshake

了解比特幣 P2P 協議的版本握手過程,建立連接時的能力協商機制。

12 分鐘

版本握手是比特幣 P2P 連接建立的第一步。雙方交換 version 和 verack 消息, 協商協議版本、交換服務能力、並確認連接成功建立。

握手流程概述

完整的版本握手:

┌─────────┐                         ┌─────────┐
│ Node A  │      TCP Connect        │ Node B  │
│(initiator)│ ────────────────────→ │(listener)│
│         │                         │         │
│         │      version            │         │
│         │ ────────────────────→   │         │
│         │      version            │         │
│         │ ←────────────────────   │         │
│         │      verack             │         │
│         │ ────────────────────→   │         │
│         │      verack             │         │
│         │ ←────────────────────   │         │
│         │                         │         │
│         │  [connection ready]     │         │
└─────────┘                         └─────────┘

握手完成後:
- 雙方知道對方的能力
- 可以開始正常通訊
- 協議版本已確定

Version 消息格式

Version 消息結構:

┌─────────────────────────────────────────────────┐
│ version (4 bytes, int32)                        │
│ services (8 bytes, uint64)                      │
│ timestamp (8 bytes, int64)                      │
│ addr_recv (26 bytes)                            │
│ addr_from (26 bytes)                            │
│ nonce (8 bytes, uint64)                         │
│ user_agent (variable, string)                   │
│ start_height (4 bytes, int32)                   │
│ relay (1 byte, bool) - optional                 │
└─────────────────────────────────────────────────┘

欄位說明:

version:
- 協議版本號
- 當前: 70016

services:
- 提供的服務位掩碼
- 詳見下方服務標誌

timestamp:
- 發送時間(Unix epoch)

addr_recv:
- 接收方的地址

addr_from:
- 發送方的地址

nonce:
- 隨機數,用於檢測自連接

user_agent:
- 軟件識別字串
- 如 "/Satoshi:25.0.0/"

start_height:
- 發送方的區塊高度

relay:
- 是否想接收交易中繼
- BIP-37 添加

服務標誌

Services 欄位的位定義:

NODE_NETWORK        = (1 << 0)   // 1
  - 完整區塊鏈節點
  - 可以提供完整區塊

NODE_GETUTXO        = (1 << 1)   // 2
  - 支持 getutxo (BIP-64)
  - 很少使用

NODE_BLOOM          = (1 << 2)   // 4
  - 支持 bloom 過濾 (BIP-37)
  - 用於 SPV 客戶端

NODE_WITNESS        = (1 << 3)   // 8
  - 支持 SegWit (BIP-144)
  - 可以提供見證數據

NODE_COMPACT_FILTERS = (1 << 6)  // 64
  - 支持緊湊區塊過濾 (BIP-157/158)

NODE_NETWORK_LIMITED = (1 << 10) // 1024
  - 修剪節點
  - 只有最近 288 個區塊

NODE_P2P_V2         = (1 << 11)  // 2048
  - 支持 v2 P2P 協議 (BIP-324)

典型組合:
完整節點: 1033 = NODE_NETWORK | NODE_WITNESS | NODE_NETWORK_LIMITED
修剪節點: 1097 = 上述 + NODE_COMPACT_FILTERS

Verack 消息

Verack 消息格式:

┌─────────────────────────────────────────┐
│ (empty payload - 0 bytes)               │
└─────────────────────────────────────────┘

作用:
- 確認收到 version 消息
- 表示準備好開始通訊

完整 P2P 消息:
┌──────────────────────────────────────────────┐
│ magic (4 bytes)                              │
│ command: "verack" (12 bytes, padded)         │
│ length: 0 (4 bytes)                          │
│ checksum: 0x5df6e0e2 (4 bytes)               │
│ payload: (empty)                             │
└──────────────────────────────────────────────┘

握手狀態機:
INIT → (send version) → VERSION_SENT
     → (recv version) → VERSION_RECEIVED
     → (send verack)  → VERACK_SENT
     → (recv verack)  → ESTABLISHED

協議版本協商

版本號的使用:

確定共同版本:
common_version = min(my_version, peer_version)

版本歷史:
60001: 添加 ping nonce (BIP-31)
70001: 添加 relay 欄位 (BIP-37)
70002: 添加 sendheaders (BIP-130)
70012: 添加 sendcmpct (BIP-152)
70013: 添加 feefilter (BIP-133)
70014: Witness 強制 (BIP-144)
70015: sendheaders, feefilter 強制
70016: addrv2, wtxidrelay (BIP-155, 339)

版本檢查:
if (peer_version < MIN_PEER_PROTO_VERSION) {
    // 版本太舊,斷開連接
    disconnect();
}

MIN_PEER_PROTO_VERSION = 31800  // 非常舊

// 實際上會拒絕不支持 SegWit 的節點

握手後的消息

Verack 之後的初始消息:

在 verack 之前可以發送:
- wtxidrelay (必須在 verack 前)
- sendaddrv2 (必須在 verack 前)

Verack 之後立即發送:
1. sendheaders
   - 請求使用 headers 通告區塊

2. sendcmpct
   - 協商緊湊區塊模式

3. feefilter
   - 發送費率過濾器

4. getaddr
   - 請求更多節點地址

5. getheaders / getblocks
   - 開始同步區塊鏈

完整初始化序列:
version → verack → sendheaders → sendcmpct
→ feefilter → getaddr → getheaders

自連接檢測

使用 nonce 檢測自連接:

問題:
- 節點可能無意中連接到自己
- 通過 NAT 或代理時可能發生
- 浪費連接槽位

解決:
1. 生成隨機 nonce
2. 發送 version 包含 nonce
3. 收到 version 時檢查 nonce

檢測邏輯:
// 發送時
my_nonce = random64();
send_version(..., nonce=my_nonce);

// 接收時
if (received_nonce == my_nonce) {
    // 自連接!
    disconnect();
    return;
}

// 也檢查其他連接
for (conn : connections) {
    if (received_nonce == conn.sent_nonce) {
        // 多條連接到同一個節點
        disconnect();
        return;
    }
}

錯誤處理

握手失敗的情況:

1. 超時
   - 未在規定時間內完成握手
   - 默認超時: 60 秒

2. 版本不兼容
   - 版本太舊
   - 服務不兼容

3. 魔數不匹配
   - 不同網路(主網 vs 測試網)
   - 損壞的消息

4. 自連接
   - nonce 匹配
   - 立即斷開

5. 無效數據
   - 格式錯誤
   - 不合理的值

錯誤回應:
- 早期斷開連接
- 可能發送 reject 消息(已棄用)
- 記錄到日誌

// 握手失敗是正常的,不應報警

調試與監控

# 監控版本握手

# 查看節點版本信息
bitcoin-cli getpeerinfo | jq '.[] | {
  id: .id,
  addr: .addr,
  version: .version,
  subver: .subver,
  services: .services,
  startingheight: .startingheight
}'

# 查看本節點版本
bitcoin-cli getnetworkinfo | jq '{
  version: .version,
  subversion: .subversion,
  protocolversion: .protocolversion,
  localservices: .localservices
}'

# 日誌設置
# bitcoin.conf
debug=net

# 日誌輸出:
# "Added connection peer=5"
# "Sending version to peer=5"
# "Received version from peer=5: version 70016, services 1033"
# "Sending verack to peer=5"
# "connection established peer=5"

# 常見 user_agent:
# /Satoshi:25.0.0/     - Bitcoin Core
# /btcd:0.24.0/        - btcd
# /Bitcoin Knots:25.0/ - Bitcoin Knots

實現細節

Bitcoin Core 中的實現:

// 發送 version
void PeerManager::PushVersion(CNode& node) {
    int64_t nTime = GetTime();
    uint64_t nonce = GetRand(std::numeric_limits<uint64_t>::max());
    int nNodeStartingHeight = chainman.ActiveHeight();

    CService addr_you = node.addr;
    CService addr_me = GetLocalAddress(&node.addr);

    connman.PushMessage(&node,
        CNetMsgMaker(INIT_PROTO_VERSION).Make(
            NetMsgType::VERSION,
            PROTOCOL_VERSION,
            nLocalServices,
            nTime,
            CAddress(addr_you, NODE_NONE),
            CAddress(addr_me, nLocalServices),
            nonce,
            strSubVersion,
            nNodeStartingHeight,
            true  // relay
        ));

    node.m_local_nonce = nonce;
}

// 處理 version
void PeerManager::ProcessVersionMessage(CNode& node, CDataStream& vRecv) {
    int32_t nVersion;
    uint64_t nServices;
    int64_t nTime;
    // ... 解析欄位 ...

    // 檢查版本
    if (nVersion < MIN_PEER_PROTO_VERSION) {
        node.fDisconnect = true;
        return;
    }

    // 檢查自連接
    if (nNonce == node.m_local_nonce) {
        node.fDisconnect = true;
        return;
    }

    // 發送 verack
    connman.PushMessage(&node,
        CNetMsgMaker(INIT_PROTO_VERSION).Make(NetMsgType::VERACK));

    node.nVersion = nVersion;
    node.nServices = nServices;
    node.fSuccessfullyConnected = true;
}

相關概念

  • P2P Protocol:點對點協議
  • Message Types:P2P 消息類型
  • BIP-324:v2 加密傳輸
  • Node Discovery:節點發現
  • Peer Management:節點管理
已複製連結
已複製到剪貼簿