🎉 Gate.io動態 #创作者激励计划# 火熱進行中!報名參與並發帖解鎖 $2,000 創作大獎!
🌟 參與攻略:
1️⃣ 點擊連結進入報名頁面 👉️ https://www.gate.io/questionnaire/6550
2️⃣ 點擊“是”按鈕提交報名
3️⃣ 在動態完成發帖,提升發帖量和互動量,解鎖獲獎資格!
📌 只要是與加密相關內容均可參與發帖!
🎁 茶具套裝、Gate x 國際米蘭保溫杯、Gate.io 紀念章、點卡等好禮等你來拿!獲獎者還將獲得專屬社區流量扶持,助力您提升影響力,增長粉絲!
活動截止至:5月6日00:00 (UTC+8)
活動詳情:https://www.gate.io/announcements/article/44513
以太坊主流客戶端:Geth整體架構
這篇文章是 Geth 源碼系列的第一篇,通過這個系列,我們將搭建一個研究 Geth 實現的框架,開發者可以根據這個框架深入自己感興趣的部分研究。這個系列共有六篇文章,在第一篇文章中,將研究執行層客戶端 Geth 的設計架構以及 Geth 節點的啓動流程。Geth 代碼更新的速度很快,後續看到的代碼可能會有所不同,但是整體的設計大體一致,新的代碼也可以用同樣的思路閱讀。
01\以太坊客戶端
以太坊在進行 The Merge 升級之前,以太坊只有一個客戶端,這個客戶端及負責交易的執行,也會負責區塊鏈的共識,保證區塊鏈以一定的順序產生新的區塊。在 The Merge 升級之後,以太坊客戶端分爲了執行層和共識層,執行層負責交易的執行、狀態和數據的維護,共識層則負責共識功能的實現,執行層和共識層通過 API 來通信。執行層和共識層有各自的規範,客戶端可以使用不同的語言來實現,但是要符合對應的規範,其中 Geth 就是執行層客戶端的一種實現。當前主流的執行層和共識層客戶端有如下實現:
執行層
共識層
02\執行層簡介
可以將以太坊執行層看作是一個由交易驅動的狀態機,執行層最基礎的職能就是通過 EVM 執行交易來更新狀態數據。除了交易執行之外,還有保存並驗證區塊和狀態數據,運行 p2p 網路並維護交易池等功能。
交易由用戶(或者程序)按照以太坊執行層規範定義的格式生成,用戶需要對交易進行籤名,如果交易是合法的(Nonce 連續、籤名正確、gas fee 足夠、業務邏輯正確),那麼交易最終就會被 EVM 執行,從而更新以太坊網路的狀態。這裏的狀態是指數據結構、數據和數據庫的集合,包括外部帳戶地址、合約地址、地址餘額以及代碼和數據。
執行層負責執行交易以及維護交易執行之後的狀態,共識層負責選擇哪些交易來執行。EVM 則是這個狀態機中的狀態轉換函數,函數的輸入會來源於多個地方,有可能來源於共識層提供的最新區塊信息,也有可能來源於 p2p 網路下載的區塊。
共識層和執行層通過 Engine API 來進行通信,這是執行層和共識層之間唯一的通信方式。如果共識層拿到了出塊權,就會通過 Engine API 讓執行層產出新的區塊,如果沒有拿到出塊權,就會同步最新的區塊讓執行層驗證和執行,從而與整個以太坊網路保持共識。
執行層從邏輯上可以分爲 6 個部分:
下圖展示了執行層的關鍵流程,以及每個部分的職能:
對於執行層(這裏暫時只討論 Full Node),有三個關鍵流程:
03\源碼結構
go-ethereum 的代碼結構很龐大,但其中很多代碼屬於輔助代碼和單元測試,在研究 Geth 源碼時,只需要關注協議的核心實現,各個模塊功能如下。需要重點關注 core、eth、ethdb、node、p2p、rlp、trie & triedb 等模塊:
04\執行層模塊劃分
外部訪問 Geth 節點有兩種形式,一種是通過 RPC,另外一種是通過 Console。RPC 適合開放給外部的用戶來使用,Console 適合節點的管理者使用。但無論是通過 RPC 還是 Console,都是使用內部已經封裝好的能力,這些能力通過分層的方式來構建。
最外層就是 API 用於外部訪問節點的各項能力,Engine API 用於執行層和共識層之間的通信,Eth API 用於外部用戶或者程序發送交易,獲取區塊信息,Net API 用於獲取 p2p 網路的狀態等等。 比如用戶通過 API 發送了一個交易,那麼這個交易最終會被提交到交易池中,通過交易池來管理,再比如用戶需要獲取一個區塊數據,那麼就需要調用數據庫的能力去獲取對應的區塊。
在 API 的下一層就核心功能的實現,包括交易池、交易打包、產出區塊、區塊和狀態的同步等等。這些功能再往下就需要依賴更底層的能力,比如交易池、區塊和狀態的同步需要依賴 p2p 網路的能力,區塊的產生以及從其他節點同步過來的區塊需要被驗證才能寫入到本地的數據庫,這些就需要依賴 EVM 和數據存儲的能力。
執行層核心數據結構
Ethereum
在 eth/backend.go 中的 Ethereum 結構是整個以太坊協議的抽象,基本包括了以太坊中的主要組件,但 EVM 是一個例外,它會在每次處理交易的時候實例化,不需要隨着整個節點初始化,下文中的 Ethereum 都是指這個結構體:
type Ethereum struct { // 以太坊配置,包括鏈配置 config *ethconfig.Config // 交易池,用戶的交易提交之後先到交易池 txPool *txpool.TxPool // 用於跟蹤和管理本地交易(local transactions) localTxTracker *locals.TxTracker // 區塊鏈結構 blockchain *core.BlockChain // 是以太坊節點的網絡層核心組件,負責處理所有與其他節點的通信,包括區塊同步、交易廣播和接收,以及管理對等節點連接 handler *handler // 負責節點發現和節點源管理 discmix *enode.FairMix // 負責區塊鏈數據的持久化存儲 chainDb ethdb.Database // 負責處理各種內部事件的發布和訂閱 eventMux *event.TypeMux // 共識引擎 engine consensus.Engine // 管理用戶帳戶和密鑰 accountManager *accounts.Manager // 管理日志過濾器和區塊過濾器 filterMaps *filtermaps.FilterMaps // 用於安全關閉 filterMaps 的通道,確保在節點關閉時正確清理資源 closeFilterMaps chan chan struct{} // 爲 RPC API 提供後端支持 APIBackend *EthAPIBackend // 在 PoS 下,與共識引擎協作驗證區塊 miner *miner.Miner // 節點接受的最低gas價格 gasPrice *big.Int // 網路 ID networkID uint64 // 提供網路相關的 RPC 服務,允許通過 RPC 查詢網路狀態 netRPCService *ethapi.NetAPI // 管理P2P網路連接,處理節點發現和連接建立並提供底層網路傳輸功能 p2pServer *p2p.Server // 保護可變字段的並發訪問 lock sync.RWMutex // 跟蹤節點是否正常關閉,在異常關閉後幫助恢復 shutdownTracker *shutdowncheck.ShutdownTracker }
Node
在 node/node.go 中的 Node 是另一個核心的數據結構,它作爲一個容器,負責管理和協調各種服務的運行。在下面的結構中,需要關注一下 lifecycles 字段,Lifecycle 用來管理內部功能的生命週期。比如上面的 Ethereum 抽象就需要依賴 Node 才能啓動,並且在 lifecycles 中註冊。這樣可以將具體的功能與節點的抽象分離,提升整個架構的擴展性,這個 Node 需要與 devp2p 中的 Node 區分開。
type Node struct { eventmux *event.TypeMux config *Config // 帳戶管理器,負責管理錢包和帳戶 accman *accounts.Manager log log.Logger keyDir string keyDirTemp bool dirLock *flock.Flock stop chan struct{} // p2p 網路實例 server *p2p.Server startStopLock sync.Mutex // 跟蹤節點生命週期狀態(初始化、運行中、已關閉) state int lock sync.Mutex // 所有註冊的後端、服務和輔助服務 lifecycles []Lifecycle // 當前提供的 API 列表 rpcAPIs []rpc.API // 爲 RPC 提供的不同訪問方式 http *httpServer ws *httpServer httpAuth *httpServer wsAuth *httpServer ipc *ipcServer inprocHandler *rpc.Server databases map[*closeTrackingDB]struct{} }
如果以一個抽象的維度來看以太坊的執行層,以太坊作爲一臺世界計算機,需要包括三個部分,網路、計算和存儲,那麼以太坊執行層中與這三個部分相對應的組件是:
devp2p
以太坊本質還是一個分布式系統,每個節點通過 p2p 網路與其他節點相連。以太坊中的 p2p 網路協議的實現就是 devp2p。
devp2p 有兩個核心功能,一個是節點發現,讓節點在接入網路時能夠與其他節點建立聯系;另一個是數據傳輸服務,在與其他節點建立聯系之後,就可以想換交換數據。
在 p2p/enode/node.go 中的 Node 結構代表了 p2p 網路中一個節點,其中 enr.Record 結構中存儲了節點詳細信息的鍵值對,包括身分信息(節點身分所使用的籤名算法、公鑰)、網路信息(IP 地址,端口號)、支持的協議信息(比如支持 eth/68 和 snap 協議)和其他的自定義信息,這些信息通過 RLP 的方式編碼,具體的規範在 eip-778 中定義:
type Node struct { // 節點記錄,包含節點的各種屬性 r enr.Record // 節點的唯一標識符,32字節長度 id ID // hostname 跟蹤節點的DNS名稱 hostname string // 節點的IP地址 ip netip.Addr // UDP端口 udp uint16 // TCP端口 tcp uint16 }// enr.Recordtype Record struct { // 序列號 seq uint64 // 籤名 signature []byte // RLP 編碼後的記錄 raw []byte // 所有鍵值對的排序列表 pairs []pair }
在 p2p/discover/table.go 中的 Table 結構是 devp2p 實現節點發現協議的核心數據結構,它實現了類似 Kademlia 的分布式哈希表,用於維護和管理網路中的節點信息。
printf("type Table struct { mutex sync.Mutex // 按距離索引已知節點 buckets [nBuckets]*bucket // 引導節點 nursery []*enode.Node rand reseedingRandom ips netutil.DistinctNetSet revalidation tableRevalidation // 已知節點的數據庫 db *enode.DB net transport cfg Config log log.Logger // 週期性的處理網路中的各種事件 refreshReq chan chan struct{} revalResponseCh chan revalidationResponse addNodeCh chan addNodeOp addNodeHandled chan bool trackRequestCh chan trackRequestOp initDone chan struct{} closeReq chan struct{} closed chan struct{} // 增加和移除節點的接口 nodeAddedHook func(*bucket, *tableNode) nodeRemovedHook func(*bucket, *tableNode)} world!");
ethdb
ethdb 完成以太坊數據存儲的抽象,提供統一的存儲接口,底層具體的數據庫可以是 leveldb,也可以是 pebble 或者其他的數據庫。可以有很多的擴展,只要在接口層面保持統一。
有些數據(如區塊數據)可以通過 ethdb 接口直接對底層數據庫進行讀寫,其他的數據存儲接口都是建立的 ethdb 的基礎上,比如數據庫有很大部分的數據是狀態數據,這些數據會被組織成 MPT 結構,在 Geth 中對應的實現是 trie,在節點運行的過程中,trie 數據會產生很多中間狀態,這些數據不能直接調用 ethdb 進行讀寫,需要 triedb 來管理這些數據和中間狀態,最後才通過 ethdb 來持久化。
在 ethdb/database.go 中定義底層數據庫的讀寫能力的接口,但沒有包括具體的實現,具體的實現將由不同的數據庫自身來實現。比如 leveldb 或者 pebble 數據庫。在 Database 中定義了兩層數據讀寫的接口,其中 KeyValueStore 接口用於存儲活躍的、可能頻繁變化的數據,如最新的區塊、狀態等。AncientStore 則用於處理歷史區塊數據,這些數據一旦寫入就很少改變。
// 數據庫的頂層接口type Database interface { KeyValueStore AncientStore}// KV 數據的讀寫接口type KeyValueStore interface { KeyValueReader KeyValueWriter KeyValueStater KeyValueRangeDeleter Batcher Iteratee Compacter io.Closer}// 處理老數據的讀寫的接口type AncientStore interface { AncientReader AncientWriter AncientStater io.Closer}
EVM
EVM 是以太坊這個狀態機的狀態轉換函數,所有狀態數據的更新都只能通過 EVM 來進行,p2p 網路可以接受到交易和區塊信息,這些信息被 EVM 處理之後會成爲狀態數據庫的一部分。EVM 屏蔽了底層硬件的不同,讓程序在不同平台的 EVM 上執行都能得到一致的結果。這是一種很成熟的設計方式,Java 語言中 JVM 也是類似的設計。
EVM 的實現有三個主要的組件,core/vm/evm.go 中的 EVM 結構體定義了 EVM 的總體結構及依賴,包括執行上下文,狀態數據庫依賴等等; core/vm/interpreter.go 中的 EVMInterpreter 結構體定義了解釋器的實現,負責執行 EVM 字節碼;core/vm/contract.go 中的 Contract 結構體封裝合約調用的具體參數,包括調用者、合約代碼、輸入等等,並且在 core/vm/opcodes.go 中定義了當前所有的操作碼:
// EVMtype EVM struct { // 區塊上下文,包含區塊相關信息 Context BlockContext // 交易上下文,包含交易相關信息 TxContext // 狀態數據庫,用於訪問和修改帳戶狀態 StateDB StateDB // 當前調用深度 depth int // 鏈配置參數 chainConfig *params.ChainConfig chainRules params.Rules // EVM配置 Config Config // 字節碼解釋器 interpreter *EVMInterpreter // 中止執行的標志 abort atomic.Bool callGasTemp uint64 // 預編譯合約映射 precompiles map[common.Address]PrecompiledContract jumpDests map[common.Hash]bitvec }type EVMInterpreter struct { // 指向所屬的EVM實例 evm *EVM // 操作碼跳轉表 table *JumpTable // Keccak256哈希器實例,在操作碼間共享 hasher crypto.KeccakState // Keccak256哈希結果緩衝區 hasherBuf common.Hash // 是否爲只讀模式,只讀模式下不允許狀態修改 readOnly bool // 上一次CALL的返回數據,用於後續重用 returnData []byte }type Contract struct { // 調用者地址 caller common.Address // 合約地址 address common.Address jumpdests map[common.Hash]bitvec analysis bitvec // 合約字節碼 Code []byte // 代碼哈希 CodeHash common.Hash // 調用輸入 Input []byte // 是否爲合約部署 IsDeployment bool // 是否爲系統調用 IsSystemCall bool // 可用gas量 Gas uint64 // 調用附帶的 ETH 數量 value *uint256.Int }
其他模塊實現
執行層的功能通過分層的方式來實現,其他的模塊和功能都是在這三個核心組件的基礎之上構建起來的。這裏介紹一下幾個核心的模塊。
在 eth/protocols 下有當前以太坊的p2p網路子協議的實現。有 eth/68 和 snap 子協議,這個些子協議都是在 devp2p 上構建的。
eth/68 是以太坊的核心協議,協議名稱就是 eth,68 是它的版本號,然後在這個協議的基礎之上又實現了交易池(TxPool)、區塊同步(Downloader)和交易同步(Fetcher)等功能。snap 協議用於新節點加入網路時快速同步區塊和狀態數據的,可以大大減少新節點啓動的時間。
ethdb 提供了底層數據庫的讀寫能力,由於以太坊協議中有很多復雜的數據結構,直接通過 ethdb 無法實現這些數據的管理,所以在 ethdb 上又實現了 rawdb 和 statedb 來分別管理區塊和狀態數據。
EVM 則貫穿所有的主流程,無論是區塊構建還是區塊驗證,都需要用 EVM 執行交易。
05\Geth 節點啓動流程
Geth 的啓動會分爲兩個階段,第一階段會初始化節點所需要啓動的組件和資源,第二節點會正式啓動節點,然後對外服務。
節點初始化
在啓動一個 geth 節點時,會涉及到以下的代碼:
各模塊的初始化如下:
節點的初始化會在 cmd/geth/config.go 中的 makeFullNode 中完成,重點會初始化以下三個模塊
在第一步會初始化 node/node.go 中的 Node 結構,就是整個節點容器,所有的功能都需要在這個容器中運行,第二步會初始化 Ethereum 結構,其中包括以太坊各種核心功能的實現,Etherereum 也需要註冊到 Node 中,第三步就是註冊 Engine API 到 Node 中。
其中 Node 初始化就是創建了一個 Node 實例,然後初始化 p2p server、帳號管理以及 http 等暴露給外部的協議端口。
Ethereum 的初始化就會復雜很多,大多數的核心功能都是在這裏初始化。首先會初始化化 ethdb,並從存儲中加載鏈配置,然後創建共識引擎,這裏的共識引擎不會執行共識操作,而只是會對共識層返回的結果進行驗證,如果共識層發生了提款請求,也會在這裏完成實際的提款操作。然後再初始化 Block Chain 結構和交易池。
這些都完成之後就會初始化 handler,handler 是所有 p2p 網路請求的處理入口,包括交易同步、區塊下載等等,是以太坊實現去中心化運行的關鍵組件。在這些都完成之後,就會將一些在 devp2p 基礎之上實現的子協議,比如 eth/68、snap 等註冊到 Node 容器中,最後 Ethereum 會作爲一個 lifecycle 註冊到 Node 容器中,Ethereum 初始化完成。
最後 Engine API 的初始化相對簡單,只是將 Engine API 註冊到 Node 中。到這裏,節點初始化就全部完成了。
節點啓動
在完成節點的初始化之後,就需要啓動節點了,節點啓動的流程相對簡單,只需要將已經註冊的 RPC 服務和 Lifecycle 全部啓動,那麼整個節點就可以向外部提供服務了。
06\總結
在深入理解以太坊執行層的實現之前,需要對以太坊有一個整體的認識,可以將以太坊整體看作是一個交易驅動的狀態機,執行層負責交易的執行和狀態的變更,共識層則負責驅動執行層運行,包括讓執行層產出區塊、決定交易的順序、爲區塊投票、以及讓區塊獲得最終性。由於這個狀態機是去中心化的,所以需要通過 p2p 網路與其他的節點通信,共同維護狀態數據的一致性。
在執行層不負責決定交易的順序,只負責執行交易並記錄交易執行之後的狀態變化。這裏的記錄有兩種形式,一種是以區塊的方式將所有的狀態變化都記錄下來,另一種是在數據庫中記錄當前的狀態。同時執行層也是交易的入口,通過交易池來存儲還沒有被打包進區塊的交易。如果其他的節點需要獲取區塊、狀態和交易數據,執行層就會通過 p2p 網路將這些信息發送出去。
對於執行層,有三個核心模塊:計算、存儲和網路。計算對應 EVM 的實現,存儲則對應了 ethdb 的實現,網路對了 devp2p 的實現。有了這樣的整體認識之後,就可以深入去理解每一個子模塊,而不會迷失在具體的細節中。
07\Ref
[1]
[2]
[3]
[4]
[5]
[6]
·END·
內容 | Ray
編輯 & 排版 | 環環
設計 | Daisy