做者:0x7F@知道創宇404區塊鏈安全研究團隊html
區塊鏈的火熱程度一直以直線上升,其中以區塊鏈 2.0 —— 以太坊爲表明,不斷的爲傳統行業帶來革新,同時也推進區塊鏈技術發展。node
區塊鏈是一種分佈式數據存儲、點對點傳輸、共識機制、加密算法等計算機技術的新型應用模式,這是一個典型的去中心化應用,創建在 p2p 網絡之上;本文以學習和分析以太坊運做原理爲目的,將以太坊網絡架構做爲一個切入點,逐步深刻分析,最終對以太坊網絡架構有個大體的瞭解。git
經過學習以太坊網絡架構,能夠更容易的對網絡部分的源碼進行審計,便於後續的協議分析,來發現未知的安全隱患;除此以外,目前基於 p2p 網絡的成熟的應用很是少,藉助分析以太坊網絡架構的機會,能夠學習一套成熟的 p2p 網絡運行架構。算法
本文側重於數據鏈路的創建和交互,不涉及網絡模塊中的節點發現、區塊同步、廣播等功能模塊。docker
其中第 三、四、5 三個小節是第 2 節「網絡架構」的子內容,做爲詳細的補充。json
在介紹以太坊網絡架構以前,首先簡單分析下 Geth 的總體啓動流程,便於後續的理解和分析。數組
以太坊源碼目錄安全
初始化工做服務器
Geth 的 main()
函數很是的簡潔,經過 app.Run()
來啓動程序:網絡
其簡潔是得力於 Geth 使用了 gopkg.in/urfave/cli.v1
擴展包,該擴展包用於管理程序的啓動,以及命令行解析,其中 app
是該擴展包的一個實例。
在 Go 語言中,在有 init()
函數的狀況下,會默認先調用 init()
函數,而後再調用 main()
函數;Geth 幾乎在 ./cmd/geth/main.go#init()
中完成了全部的初始化操做:設置程序的子命令集,設置程序入口函數等,下面看下 init()
函數片斷:
在以上代碼中,預設了 app實例的值,其中 app.Action = geth做爲 app.Run()調用的默認函數,而 app.Commands保存了子命令實例,經過匹配命令行參數能夠調用不一樣的函數(而不調用 app.Action),使用 Geth 不一樣的功能,如:開啓帶控制檯的 Geth、使用 Geth 創造創世塊等。
節點啓動流程
不管是經過 geth()
函數仍是其餘的命令行參數啓動節點,節點的啓動流程大體都是相同的,這裏以 geth()
爲例:
其中makeFullNode()函數將返回一個節點實例,而後經過startNode()啓動。在 Geth 中,每個功能模塊都被視爲一個服務,每個服務的正常運行驅動着 Geth 的各項功能;makeFullNode()經過解析命令行參數,註冊指定的服務。如下是 makeFullNode()代碼片斷:
而後經過 startNode()
啓動各項服務並運行節點。如下是 Geth 啓動流程圖:
每一個服務正常運行,相互協做,構成了 Geth:
經過 main()
函數的調用,最終啓動了 p2p 網絡,這一小節對網絡架構作詳細的分析。
三層架構
以太坊是去中心化的數字貨幣系統,自然適用 p2p 通訊架構,而且在其上還支持了多種協議。在以太坊中,p2p 做爲通訊鏈路,用於負載上層協議的傳輸,能夠將其分爲三層結構:
TCP/IP
中的網絡層及如下的封裝。p2p 通訊鏈路層
從最下層開始逐步分析,第三層是由 Go 語言所封裝的網絡 IO 層,這裏就跳過了,直接分析 p2p 通訊鏈路層。p2p 通訊鏈路層主要作了三項工做:
p2p 源碼分析
p2p 一樣做爲 Geth 中的一項服務,經過「0x03 Geth 啓動」中 startNode()
啓動,p2p 經過其 Start()
函數啓動。如下是 Start()
函數代碼片斷:
上述代碼中,設置了 p2p 服務的基礎參數,並根據用戶參數開啓節點發現(節點發現不在本文的討論範圍內),隨後開啓 p2p 服務監聽,最後開啓單獨的協程用於處理報文。如下分爲服務監聽和報文處理兩個模塊來分析。
服務監聽
經過 startListening()
的調用進入到服務監聽的流程中,隨後在該函數中調用 listenLoop
用一個無限循環處理接受鏈接,隨後經過 SetupConn()
函數爲正常的鏈接創建 p2p 通訊鏈路。在 SetupConn()
中調用 setupConn()
來作具體工做,如下是 setupConn()
的代碼片斷:
setupConn()
函數中主要由 doEncHandshake()
函數與客戶端交換密鑰,並生成臨時共享密鑰,用於本次通訊加密,並建立一個幀處理器 RLPXFrameRW
;再調用 doProtoHandshake()
函數爲本次通訊協商遵循的規則和事務,包含版本號、名稱、容量、端口號等信息。在成功創建通訊鏈路,完成協議握手後,處理流程轉移到報文處理模塊。
下面是服務監聽函數調用流程:
報文處理
p2p.Start()
經過調用 run()
函數處理報文,run()
函數用無限循環等待事務,好比上文中,新鏈接完成握手包後,將由該函數來負責。run()
函數中支持多個命令的處理,包含的命令有服務退出清理、發送握手包、添加新節點、刪除節點等。如下是 run()
函數結構:
爲了理清整個網絡架構,本文直接討論 addpeer
分支:當一個新節點添加服務器節點時,將進入到該分支下,根據以前的握手信息,爲上層協議生成實例,而後調用 runPeer()
,最終經過 p.run()
進入報文的處理流程中。
繼續分析 p.run()
函數,其開啓了讀取數據和 ping
兩個協程,用於處理接收報文和維持鏈接,隨後經過調用 startProtocols()
函數,調用指定協議的 Run()
函數,進入具體協議的處理流程。
下面是報文處理函數調用流程
p2p 通訊鏈路交互流程
這裏總體看下 p2p 通訊鏈路的處理流程,以及對數據包的封裝。
在 p2p 通訊鏈路的創建過程當中,第一步就是協商共享密鑰,該小節說明下密鑰的生成過程。
迪菲-赫爾曼密鑰交換
p2p 網絡中使用到的是「迪菲-赫爾曼密鑰交換」技術[1]。迪菲-赫爾曼密鑰交換(英語:Diffie–Hellman key exchange,縮寫爲D-H) 是一種安全協議。它可讓雙方在徹底沒有對方任何預先信息的條件下經過不安全信道建立起一個密鑰。
簡單來講,連接的兩方生成隨機的私鑰,經過隨機的私鑰獲得公鑰。而後雙方交換各自的公鑰,這樣雙方均可以經過本身隨機的私鑰和對方的公鑰來生成一個一樣的共享密鑰(shared-secret)。後續的通信使用這個共享密鑰做爲對稱加密算法的密鑰。其中對於 A、B公私鑰對知足這樣的數學等式:ECDH(A私鑰, B公鑰) == ECDH(B私鑰, A公鑰)
。
共享密鑰生成
在 p2p 網絡中由 doEncHandshake()
方法完成密鑰的交換和共享密鑰的生成工做。下面是該函數的代碼片斷:
若是做爲服務端監聽鏈接,收到新鏈接後調用 receiverEncHandshake()
函數,若做爲客戶端向服務端發起請求,則調用 initiatorEncHandshake()
函數;兩個函數區別不大,都將交換密鑰,並生成共享密鑰,initiatorEncHandshake()
僅僅是做爲發起數據的一端;最終執行完後,調用 newRLPXFrameRW()
建立幀處理器。
從服務端的角度來看,將調用 receiverEncHandshake()
函數來建立共享密鑰,如下是該函數的代碼片斷:
共享密鑰生成的過程:
如下是共享密鑰生成圖示:
得出共享密鑰後,客戶端和服務端就可使用共享密鑰作對稱加密,完成對通訊的加密。
在共享密鑰生成完畢後,初始化了 RLPXFrameRW
幀處理器;其 RLPXFrameRW
幀的目的是爲了在單個鏈接上支持多路複用協議。其次,因爲幀分組的消息爲加密數據流產生了自然的分界點,更便於數據的解析,除此以外,還能夠對發送的數據進行驗證。
RLPXFrameRW
幀包含了兩個主要函數,WriteMsg()
用於發送數據,ReadMsg()
用於讀取數據;如下是 WriteMsg()
的代碼片斷:
結合以太坊 RLPX 的文檔[2]和上述代碼,能夠分析出 RLPXFrameRW
幀的結構。在通常狀況下,發送一次數據將產生五個數據包:
接收方按照一樣的格式對數據包進行解析和驗證。
RLP編碼 (遞歸長度前綴編碼)提供了一種適用於任意二進制數據數組的編碼,RLP 已經成爲以太坊中對對象進行序列化的主要編碼方式,便於對數據結構的解析。比起 json 數據格式,RLP 編碼使用更少的字節。
在以太坊的網絡模塊中,全部的上層協議的數據包要交互給 p2p 鏈路時,都要首先經過 RLP 編碼;從 p2p 鏈路讀取數據,也要先進行解碼才能操做。
以太坊中 RLP 的編碼規則[3]。
這裏以 LES 協議爲上層協議的表明,分析在以太坊網絡架構中應用協議的工做原理。
LES 服務由 Geth 初始化時啓動,調用源碼 les 下的 NewLesServer()
函數開啓一個 LES 服務並初始化,並經過 NewProtocolManager()
實現以太坊子協議的接口函數。其中 les/handle.go
包含了 LES 服務交互的大部分邏輯。
回顧上文 p2p 網絡架構,最終 p2p 底層經過 p.Run()
啓動協議,在 LES 協議中,也就是調用 LES 協議的 Run()
函數:
能夠看到重要的處理邏輯都被包含在 handle()
函數中,handle()
函數的主要功能包含 LES 協議握手和消息處理,下面是 handle()
函數片斷:
在 handle()
函數中首先進行協議握手,其實現函數是 ./les/peer.go#Handshake()
,經過服務端和客戶端交換握手包,互相獲取信息,其中包含有:協議版本、網絡號、區塊頭哈希、創世塊哈希等值。隨後用無線循環處理通訊的數據,如下是報文處理的邏輯:
處理一個請求的詳細流程是:
RLPXFrameRW
幀處理器,獲取請求的數據。RLP
編碼將二進制數據序列化。msg.Code
的判斷,執行相應的功能。RLP
編碼,共享密鑰加密,轉換爲 RLPXFrameRW
,最後發送給請求方。下面是 LES 協議處理流程:
經過本文的分析,對以太坊網絡架構有了大體的瞭解,便於後續的分析和代碼審計;在安全方面來說,由協議所帶的安全問題每每比本地的安全問題更爲嚴重,應該對網絡層面的安全問題給予更高的關注。
從本文也能夠看到,以太坊網絡架構很是的完善,具備極高的魯棒性,這也證實了以太坊是能夠被市場所承認的區塊鏈系統。除此以外,因爲 p2p 網絡方向的資料較少,以太坊的網絡架構也能夠做爲學習 p2p 網絡的資料。