本檔前部分翻譯自以太坊定義的節點發現協議(版本4),後半部分給出了源碼實現的大體流程,以幫助理解。html
以太坊節點信息的存儲採用的是Kademlia
分佈式哈希表。理解節點發現協議主要是理解分佈式哈希表的原理,再加上定義的節點間通訊的報文格式,節點ID的定義,距離的計算,加在一塊兒就是以太坊的節點發現協議了。以太坊不一樣語言版本代碼實現上具體細節可能不一樣但大體流程思想是相同的。node
每一個節點都有一個secp256k1
橢圓曲線密碼學ID。節點的公鑰做爲標識或節點ID。節點之間的距離爲公鑰按位異或或者是公鑰的哈希值按位異或。計算公式以下:git
distance(n₁, n₂) = keccak256(n₁) XOR keccak256(n₂)
複製代碼
節點表在節點發現協議中用於保存鄰節點信息。鄰節點被存在一個包含有K桶的路由表中。協議中,即每一個K桶至多含有16個節點條目。每項按時間排序——最新發現更新的節點放在前,其餘在後。github
每當一個新節點被發現,就能夠插入相應的桶中。若是桶中少於
個條目,
可添加到桶中第一個條目。若是桶中已含有
項,桶中最先發現的節點
,須要經過發送ping包從新檢測其有效性。若是沒有收到來自
的回覆則認爲該節點已失效(下線),從路由表中移除並將
添加到桶的前部。數組
以太坊文檔中
Node Table
一節有部份內容錯誤,For each 0 ≤ i < 256, every node keeps a k-bucket for nodes of distance between 2i and 2i+1 from itself.
,應該是。建議閱讀論文Kademlia——A Peer-to-peer Information System Based on the XOR Metric。bash
爲了預防流量放大攻擊,必須驗證查詢的發送者是否參與了發現協議。若是數據包的發送者在過去12小時內發送了具備匹配ping哈希的有效pong響應,則認爲該數據包的發送者已通過驗證。tcp
一次查找會找到個距離目標節點最近的節點。節點查找發起後先選取
個距離目標節點最近的已知節點。隨後同時向這些節點發送
FindNode
包。其中,是一個參數,一般可設爲3。發起者繼續向先前查詢到的節點發送
FindNode
,如此不斷進行遞歸。對獲知的個離目標節點最近的節點,選取
個還沒有查詢過的節點向其發送
FindNode
。沒法快速響應的節點將被排除在外,除非他們作出響應。分佈式
若是一輪FindNode
查詢失敗,即沒有返回任何一個比目前節點中更近的節點,那麼將會繼續向個最近節點未被查詢過的節點中發送
FindNode
。ui
節點發現協議報文都是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桶呢?能夠公鑰哈希值按位異或後最高位的值(例如: 異或值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
![]()