點對點網路協定
點對點網路協定是 以太坊 客戶端交換資訊的方式。在你筆電上執行的客戶端程式,會透過這樣的協定去找到其他的客戶端。所有交易、區塊的傳播,也是透過這個協定來進行。
( p2p 網路示意圖)
節點透過 RLPx 傳輸加密過的訊息。 RLPx Node Discovery Protocol v4 節點發現協定。新版的 v5 已存在,但仍在實驗中,並沒有在主網路上使用。
這個協定的設計,從 2015 年以太坊上線以來沒怎麼變動過。目前(2018 年 4 月) Marcus, Heilman and Goldberg (2018) 是唯一詳細記載 p2p 行為的文件。本文內容主要也會是摘取自那篇論文。
Kademlia 的異同
以太坊的點對點協定是修改 Kademlia DHT。Kademlia 原本是設計在點對點網路中,有效地找尋、儲存內容(例如文字、影片資源)。但在以太坊的應用中,只作為找尋新的節點使用。
在原來的 Kademlia 網路中,每個內容都有個相應總長 b 位元的鍵( key),並儲存在所有 ID (也是 b 位元) 最「接近」的節點。所謂接近,是以 Kademlia 定義的距離而言:兩個 b 位元的 t1 與 t2 ,之間的距離為 t1 XOR t2 ,並將這個 XOR 的結果以整數詮釋。
>>> t1 = int('00111100', 2)
>>> t2 = int('00001101', 2)
>>> t1^t2
49
節點會去找尋離想要的資源最近節點,問他們有沒有資源,或是最近的節點是什麼。如果沒有找到資源就一直問最近的節點,直到找到資源為止(或所有最近的節點都問完)。
在以太坊中,同樣也有 XOR 的距離單位。但以太訪節點不需要找尋儲存的資源,而是要找尋其他節點。客戶端一開始會產生一個隨機的 t ,在自己的桶子(bucket)中找尋 k=16 個 ID 最接近 t 的節點。接著再和那些節點訪問 k 個最接近 t 的節點。因此現在總共有 k*k 個節點了,再從中選 k 個,去和他們再問 k 個,直到沒有新的節點被發現。
節點身份
節點的 ID 是一個 b = 512 位元 (或 64 位元組)的橢圓曲線 ECDSA 公鑰。
一般節點以 enode://{ID}@{IP}:{port}
格式記載。如以下範例:
enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@52.16.188.185:30303
網路連線
UDP 連線
UDP 用來交換點對點網路的資訊。用 UDP 交換的訊息有四種類別:
- 發起方發出
ping
,接收方回應pong
,這對主要用來偵測節點是否還有反應。 - 發起方發出
findnode
,接收方回應neighbor
,這是用來向詢問前述 16 個鄰近節點。
所有的 UDP 訊息都有加時間戳,並用發送者的 ECDSA 公鑰(也就是節點 ID)加密且認證過。為了避免重放攻擊,客戶端會丟棄任何相差本地時間 20 秒以上的訊息。為了避免偽造 IP 傳來的 pong
, pong
裡也要加上 ping
的雜湊值。
TCP 連線
所有「區塊鏈」的資訊則是透過 TCP 連線交換,也是加密、認證過的。客戶端可以設定 TCP 連線的總數上限 maxpeers
,預設為 25 。
網路資訊儲存方式
客戶端會以兩種方式儲存其他節點的資訊。第一種是長期的資料庫 db
,這會存在硬碟中,當客戶端重啟時資料不會消滅。第二種是短期的資料庫 table
,這在客戶端重啟時會清空。
行為 \ 儲存方式 | db |
table |
---|---|---|
客戶端重啟時 | 保留記錄 | 清空記錄 |
記載內容 | 節點 ID 、IP 地址、TCP 端口、UDP 端口、上次送 ping 的時間、上次收過 pong 的時間,以及該節點無法回應 findnode 的次數 |
節點 ID 、IP 地址、TCP 端口、UDP 端口 |
記錄上限 | 無上限 | 包含 256 個水桶,每個水桶可以包含 k=16 筆記錄 |
新增記錄 | 有收過正確 pong 訊息的節點即新增 |
把新節點加入對應的桶子 1,若桶子已滿,會去 ping 桶子中最老的節點。老節點有回應則不新增新節點,無回應則以新節點取代。 |
清理記錄 | 每小時會清理掉 db 中,上次收到 pong 的時間超過 1 天的節點。 |
節點如果四次無法回應 findnode ,會被移出 table 。 |
存入資料
進入 db 與 table 的節點資訊,大概會經由下列方式而來。
種子節點 Bootstrap Nodes
客戶端第一次啟動時,只知道 6 個寫死在客戶端中的節點。例如:寫死在 Geth 客戶端中的種子節點
綁定 Bonding
當客戶端想要綁定某個節點時,客戶端首先檢查
- 節點是否存在
db
db
記載該節點沒有回應findnode
失敗的記錄。db
記載該節點 24 小時內有成功回應pong
。
如果三個條件成立,客戶端會立即將節點加到 table
裡面。否則客戶端會 ping
該節點,如果節點正確回應 pong
,則綁定成功。如果綁定成功,則會把節點加入或更新到 db
中,也會嘗試加入 table
。
外部發起的 ping
Unsolicited pings
客戶端接收從其他節點發起的 ping
,這種直接回應 pong
就完成綁定了。
Lookup
客戶端也可透過 lookup(t)
來找尋節點。這是前述節點發掘,透過不斷迭代的 findnode
詢問,找到最後 16 個最接近 t 的節點。
這 16 個節點會被加入 lookup_buffer
這個資料結構(FIFO queue)中。
播種 Seeding
這是客戶端剛啟動時,要讓其他節點得到這個客戶端的新 ID 。客戶端會
- 綁定六個種子節點
- 綁定
db
中隨機選擇 30 個以下年紀小於 5 天的節點。
這兩種綁定完成之後,客戶端執行 lookup(self)
,其中 self
是客戶端自己 ID 的 SHA3 雜湊值。
選擇節點 Selecting peers (向外的 TCP 連線)
大致而言,以太坊客戶端會選擇一半來自 lookup_buffer
,一半來自 table
。
以太坊節點啟動時,一個任務執行器(Task Runner)會一直存入 db
與 table
資料,直到建立外聯 TCP 數量達到 maxpeers
的一半(預設 13)。
任務執行器預設會有 16 個任務同步執行。任務有兩種: dial_task
與 discover_task
。discover_task
會以隨機 256 位元字串 t
執行 lookup (t)
。dial_task
會試圖與其他節點建立連線。這個任務執行前,會檢查該節點
- 現在沒正在被 dial
- 並非已連線的節點
- 並非自己
- 沒被黑名單
- 最近沒被 dial 過
攻擊模型
Marcus, Heilman and Goldberg (2018) 是個很好的開始。除了有些相關比特幣的點對點網路攻擊文獻回顧,內文提到的日蝕攻擊也有許多衍生的攻擊方式。
(TBD)
參考資料
附註
1. 限於篇幅不介紹,論文有解說 ↩