點對點網路協定

點對點網路協定是 以太坊 客戶端交換資訊的方式。在你筆電上執行的客戶端程式,會透過這樣的協定去找到其他的客戶端。所有交易、區塊的傳播,也是透過這個協定來進行。

( 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 傳來的 pongpong 裡也要加上 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

當客戶端想要綁定某個節點時,客戶端首先檢查

  1. 節點是否存在 db
  2. db 記載該節點沒有回應 findnode 失敗的記錄。
  3. 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 。客戶端會

  1. 綁定六個種子節點
  2. 綁定 db 中隨機選擇 30 個以下年紀小於 5 天的節點。

這兩種綁定完成之後,客戶端執行 lookup(self) ,其中 self 是客戶端自己 ID 的 SHA3 雜湊值。

選擇節點 Selecting peers (向外的 TCP 連線)

大致而言,以太坊客戶端會選擇一半來自 lookup_buffer ,一半來自 table

以太坊節點啟動時,一個任務執行器(Task Runner)會一直存入 dbtable 資料,直到建立外聯 TCP 數量達到 maxpeers 的一半(預設 13)。

任務執行器預設會有 16 個任務同步執行。任務有兩種: dial_taskdiscover_taskdiscover_task 會以隨機 256 位元字串 t 執行 lookup (t)dial_task 會試圖與其他節點建立連線。這個任務執行前,會檢查該節點

  1. 現在沒正在被 dial
  2. 並非已連線的節點
  3. 並非自己
  4. 沒被黑名單
  5. 最近沒被 dial 過

攻擊模型

Marcus, Heilman and Goldberg (2018) 是個很好的開始。除了有些相關比特幣的點對點網路攻擊文獻回顧,內文提到的日蝕攻擊也有許多衍生的攻擊方式。

(TBD)

參考資料

Marcus, Y., Heilman, E., & Goldberg, S. (2018). Low-Resource Eclipse Attacks on Ethereum’s Peer-to-Peer Network.

附註

1. 限於篇幅不介紹,論文有解說

results matching ""

    No results matching ""