以太坊節點發現協議

lican123發表於2020-06-12

本檔前部分翻譯自以太坊定義的節點發現協議(版本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

相關文章