本檔前部分翻譯自以太坊定義的節點發現協議(版本4),后半部分給出了源碼實現的大致流程,以幫助理解。
以太坊節點信息的存儲采用的是Kademlia
分布式哈希表。理解節點發現協議主要是理解分布式哈希表的原理,再加上定義的節點間通信的報文格式,節點ID的定義,距離的計算,加在一起就是以太坊的節點發現協議了。以太坊不同語言版本代碼實現上具體細節可能不同但大致流程思想是相同的。
第一部分——節點發現協議定義
節點ID
每個節點都有一個secp256k1
橢圓曲線密碼學ID。節點的公鑰作為標識或節點ID。節點之間的距離為公鑰按位異或或者是公鑰的哈希值按位異或。計算公式如下:
distance(n₁, n₂) = keccak256(n₁) XOR keccak256(n₂)
節點表
節點表在節點發現協議中用於保存鄰節點信息。鄰節點被存在一個包含有K桶的路由表中。協議中\(k=16\),即每個K桶至多含有16個節點條目。每項按時間排序——最新發現更新的節點放在前,其他在后。
每當一個新節點\(N_1\)被發現,就可以插入相應的桶中。如果桶中少於\(k\)個條目,\(N_1\)可添加到桶中第一個條目。如果桶中已含有\(k\)項,桶中最早發現的節點\(N_2\),需要通過發送ping包重新檢測其有效性。如果沒有收到來自\(N_2\)的回復則認為該節點已失效(下線),從路由表中移除並將\(N_1\)添加到桶的前部。
以太坊文檔中
Node Table
一節有部分內容錯誤,For each 0 ≤ i < 256, every node keeps a k-bucket for nodes of distance between 2i and 2i+1 from itself.
,應該是\([2^i,2^{i+1})\) 。建議閱讀論文Kademlia——A Peer-to-peer Information System Based on the XOR Metric。
端點驗證
為了預防流量放大攻擊,必須驗證查詢的發送者是否參與了發現協議。如果數據包的發送者在過去12小時內發送了具有匹配ping哈希的有效pong響應,則認為該數據包的發送者已經過驗證。
遞歸查找
一次查找會找到\(k\)個距離目標節點最近的節點。節點查找發起后先選取\(a\)個距離目標節點最近的已知節點。隨后同時向這些節點發送FindNode
包。其中,\(a\)是一個參數,通常可設為3。發起者繼續向先前查詢到的節點發送FindNode
,如此不斷進行遞歸。對獲知的\(k\)個離目標節點最近的節點,選取\(a\)個尚未查詢過的節點向其發送FindNode
。無法快速響應的節點將被排除在外,除非他們做出響應。
如果一輪FindNode
查詢失敗,即沒有返回任何一個比目前節點中更近的節點,那么將會繼續向\(k\)個最近節點未被查詢過的節點中發送FindNode
。
報文協議
節點發現協議報文都是UDP報文,報文中最大的是1280字節。
packet = packet-header || packet-data
數據包頭部:
packet-header = hash || signature || packet-type
hash = keccak256(signature || packet-type || packet-data)
signature = sign(packet-type || packet-data)
當在同一UDP端口上運行多個協議時,hash
可使分組格式可識別。除此並無其他目的。每個包都由節點公鑰來簽名,簽名是一個編碼長度為65字節數組,簽名值r,s
,簽名驗證值v
。
消息類型packet-type
占單字節。包有效數據在消息類型后面。數據包頭部之后的數據用RLP進行編碼。根據EIP-8,實現應忽略列表中的任何其他元素以及列表后的任何額外數據。
Ping Packet (0x01)
packet-data = [version, from, to, expiration]
version = 4
from = [sender-ip, sender-udp-port, sender-tcp-port]
to = [recipient-ip, recipient-udp-port, 0]packet-data = [ver
expiration
字段是UNIX時間戳,如果一個數據包的時戳過期了可能會無法處理。收到ping數據包后,接收節點應回復pong數據包。並可考慮將發送節點添加到節點表中。
如果在過去12小時內未與發送方進行任何通信,則除了pong之外還應發送ping以驗證對端節點。
Pong Packet (0x02)
packet-data = [to, ping-hash, expiration]
Pong是ping的響應。ping-hash
須與相應的ping包hash
一致。實現時應該忽略那些不含有ping包hash
的pong包。
FindNode Packet (0x03)
packet-data = [target, expiration]
FindNode
包用於請求距離目的節點近的節點。目標節點ID是一個65字節長度的secp256k1
橢圓曲線公鑰。當接收到FindNode
,接收端需要回復在本地節點表中距離請求目的節點最近的16個節點。
為了對抗流量放大攻擊,只有被驗證過的FindNode
發送者才會被回復鄰節點信息。
Neighbors Packet (0x04)
packet-data = [nodes, expiration]
nodes = [[ip, udp-port, tcp-port, node-id], ... ]
FindNode
包的響應。
存在的問題及建議
凡含有expiration
字段的數據包都是用於防止數據重放的。因為是絕對時間戳,節點時鍾必要要十分准確以正確驗證時戳的有效性。自從2016年協議發布后起,已經接收到無數的因為用戶的時鍾不准確造成的錯誤報告。
端點驗證是不嚴密是因為FindNode
的發送方永遠法確定接收端十分接收到足夠的pong。Geth按如下方式處理:如果在最近12小時內未與收件人進行通信,請通過發送ping啟動該過程。等待來自另一方的ping,回復它然后發送FindNode
。
第二部分——節點發現協議代碼實現流程
流程圖
節點如何加入到對應的K桶
計算節點之間的距離很簡單,直接按位異或后的值即為兩節點之間的距離值,但節點應該加入那個K桶呢?可以公鑰哈希值按位異或后最高位的值(例如: 異或值0000 ... 0000 0101
,則桶距離為3 ),則將節點放入第3個桶中。
為什么?
主要是要理解二叉樹的拆分過程:
對每一個節點,都可以按照自己的視角對整個二叉樹進行拆分。拆分的規則是:先從根節點開始,把不包含自己的那個子樹拆分出來;然后在剩下的子樹再拆分不包含自己的下一層子樹;以此類推,直到最后只剩下自己。
拆分的最后一個K桶(距離自己最近的那個K桶),只有最后1位不同,異或值為0000 ... 0000 0001
,最高位為1,第一個K桶;拆分的倒數第二個K桶,異或值為0000 ... 0000 001x
,最高位為2,第二個K桶;依此類推......
在具體實現細節上,以太坊節點節點公鑰是512位,計算距離時的ID是取節點公鑰的哈希,值為256位。所以節點路由表由256個K桶組成,每個K桶最多16個節點。
參考文檔:
Node Discovery Protocol v4
聊聊分布式散列表(DHT)的原理——以 Kademlia(Kad) 和 Chord 為例
Kademlia——A Peer-to-peer Information System Based on the XOR Metric