2025-04-28 132

以太坊主流客户端:Geth整体架构

这篇文章是Geth源码系列的第一篇,通过这个系列,我们将搭建一个研究Geth实现的框架,开发者可以根据这个框架深入自己感兴趣的部分研究。这个系列共有六篇文章,在第一篇文章中,将研究执行层客户端Geth的设计架构以及Geth节点的启动流程。Geth代码更新的速度很快,后续看到的代码可能会有所不同,但是整体的设计大体一致,新的代码也可以用同样的思路阅读。

01\以太坊客户端

以太坊在进行TheMerge升级之前,以太坊只有一个客户端,这个客户端及负责交易的执行,也会负责区块链的共识,保证区块链以一定的顺序产生新的区块。在TheMerge升级之后,以太坊客户端分为了执行层和共识层,执行层负责交易的执行、状态和数据的维护,共识层则负责共识功能的实现,执行层和共识层通过API来通信。执行层和共识层有各自的规范,客户端可以使用不同的语言来实现,但是要符合对应的规范,其中Geth就是执行层客户端的一种实现。当前主流的执行层和共识层客户端有如下实现:

执行层Geth:由以太坊基金会直接资助的团队维护,使用Go语言开发,是公认的最稳定、久经考验的客户端Nethermind:由Nethermind团队开发和维护,使用C#语言开发,早期获以太坊基金会和Gitcoin社区资助Besu:最初由ConsenSys的PegaSys团队开发,现为Hyperledger社区项目,使用Java语言开发Erigon:由Erigon团队开发和维护,获以太坊基金会、BNBChain资助。2017年从Geth分叉而来,目标是提升同步速度和磁盘效率Reth:由Paradigm主导开发,开发语言是Rust,强调模块化和高性能,目前已经趋近成熟,可以在生产环境使用共识层Prysm:由PrysmaticLabs维护,是以太坊最早的共识层客户端之一,用Go语言开发,专注于可用性和安全性,早期获以太坊基金会资助Lighthouse:由SigmaPrime团队维护,使用Rust语言开发,主打高性能和企业级安全,适用于高负载场景Teku:早起由ConsenSys的PegaSys团队开发,后成为HyperledgerBesu社区的一部分,使用Java语言开发Nimbus:由StatusNetwork团队开发和维护,使用Nim语言开发,专为资源受限设备(如手机、物联网设备)优化,目标是在嵌入式系统中实现轻量化运行02\执行层简介

可以将以太坊执行层看作是一个由交易驱动的状态机,执行层最基础的职能就是通过EVM执行交易来更新状态数据。除了交易执行之外,还有保存并验证区块和状态数据,运行p2p网络并维护交易池等功能。

交易由用户(或者程序)按照以太坊执行层规范定义的格式生成,用户需要对交易进行签名,如果交易是合法的(Nonce连续、签名正确、gasfee足够、业务逻辑正确),那么交易最终就会被EVM执行,从而更新以太坊网络的状态。这里的状态是指数据结构、数据和数据库的集合,包括外部账户地址、合约地址、地址余额以及代码和数据。

执行层负责执行交易以及维护交易执行之后的状态,共识层负责选择哪些交易来执行。EVM则是这个状态机中的状态转换函数,函数的输入会来源于多个地方,有可能来源于共识层提供的最新区块信息,也有可能来源于p2p网络下载的区块。

共识层和执行层通过EngineAPI来进行通信,这是执行层和共识层之间唯一的通信方式。如果共识层拿到了出块权,就会通过EngineAPI让执行层产出新的区块,如果没有拿到出块权,就会同步最新的区块让执行层验证和执行,从而与整个以太坊网络保持共识。

执行层从逻辑上可以分为6个部分:

EVM:负责执行交易,交易执行也是修改状态数的唯一方式存储:负责state以及区块等数据的存储交易池:用于用户提交的交易,暂时存储,并且会通过p2p网络在不同节点之间传播p2p网络:用于发现节点、同步交易、下载区块等等功能RPC服务:提供访问节点的能力,比如用户向节点发送交易,共识层和执行层之间的交互BlockChain:负责管理以太坊区块链数据

下图展示了执行层的关键流程,以及每个部分的职能:

对于执行层(这里暂时只讨论FullNode),有三个关键流程:

如果是新加入以太坊的节点,需要通过p2p网络从其他的节点同步区块和状态数据,如果是FullSync,会从创世区块开始逐个下载区块,验证区块并通过EVM重建状态数据库,如果是SnapSync,则跳过全部区块验证的过程,直接下载最新checkpoint的状态数据和以后的区块数据如果是已经同步到最新状态的节点,那么就会持续通过EngineAPI从共识层获取到当前最新产出的区块,并验证区块,然后通过EVM执行区块中所有的交易来更新状态数据库,并将区块写入本地链如果是已经同步到最新状态,并且共识层拿到了出块权的节点,就会通过EngineAPI驱动执行层产出最新的区块,执行层从交易池获取交易并执行,然后组装成区块通过EngineAPI传递给共识层,由共识层将区块广播到共识层p2p网络03\源码结构

go-ethereum的代码结构很庞大,但其中很多代码属于辅助代码和单元测试,在研究Geth源码时,只需要关注协议的核心实现,各个模块功能如下。需要重点关注core、eth、ethdb、node、p2p、rlp、trie&triedb等模块:

accounts:管理以太坊账户,包括公私钥对的生成、签名验证、地址派生等beacon:处理与以太坊信标链(BeaconChain)的交互逻辑,支持权益证明(PoS)共识的合并(TheMerge)后功能build:构建脚本和编译配置(如Dockerfile、跨平台编译支持)cmd:命令行工具入口,包含多个子命令common:通用工具类,如字节处理、地址格式转换、数学函数consensus:定义consensusengine,包括之前的工作量证明(Ethash)和单机权益证明(Clique)以及Beaconengine等console:提供交互式JavaScript控制台,允许用户通过命令行直接与以太坊节点交互(如调用Web3API、管理账户、查询区块链数据)core:区块链核心逻辑,处理区块/交易的生命周期管理、状态机、Gas计算等crypto:加密算法实现,包括椭圆曲线(secp256k1)、哈希(Keccak-256)、签名验证docs:文档(如设计规范、API说明)eth:以太坊网络协议的完整实现,包括节点服务、区块同步(如快速同步、归档模式)、交易广播等ethclient:实现以太坊客户端库,封装JSON-RPC接口,供Go开发者与以太坊节点交互(如查询区块、发送交易、部署合约)ethdb:数据库抽象层,支持LevelDB、Pebble、内存数据库等,存储区块链数据(区块、状态、交易)ethstats:收集并上报节点运行状态到统计服务,用于监控网络健康状态event:实现事件订阅与发布机制,支持节点内部模块间的异步通信(如新区块到达、交易池更新)graphql:提供GraphQL接口,支持复杂查询(替代部分JSON-RPC功能)internal:内部工具或限制外部访问的代码log:日志系统,支持分级日志输出、上下文日志记录mertrics:性能指标收集(Prometheus支持)miner:挖矿相关逻辑,生成新区块并打包交易(PoW场景下)node:节点服务管理,整合p2p、RPC、数据库等模块的启动与配置p2p:点对点网络协议实现,支持节点发现、数据传输、加密通信params:定义以太坊网络参数(主网、测试网、创世区块配置)rlp:实现以太坊专用的数据序列化协议RLP(RecursiveLengthPrefix),用于编码/解码区块、交易等数据结构rpc:实现JSON-RPC和IPC接口,供外部程序与节点交互signer:交易签名管理(硬件钱包集成)tests:集成测试和状态测试,验证协议兼容性trie&triedb:默克尔帕特里夏树(MerklePatriciaTrie)的实现,用于高效存储和管理账户状态、合约存储04\执行层模块划分

外部访问Geth节点有两种形式,一种是通过RPC,另外一种是通过Console。RPC适合开放给外部的用户来使用,Console适合节点的管理者使用。但无论是通过RPC还是Console,都是使用内部已经封装好的能力,这些能力通过分层的方式来构建。

最外层就是API用于外部访问节点的各项能力,EngineAPI用于执行层和共识层之间的通信,EthAPI用于外部用户或者程序发送交易,获取区块信息,NetAPI用于获取p2p网络的状态等等。比如用户通过API发送了一个交易,那么这个交易最终会被提交到交易池中,通过交易池来管理,再比如用户需要获取一个区块数据,那么就需要调用数据库的能力去获取对应的区块。

在API的下一层就核心功能的实现,包括交易池、交易打包、产出区块、区块和状态的同步等等。这些功能再往下就需要依赖更底层的能力,比如交易池、区块和状态的同步需要依赖p2p网络的能力,区块的产生以及从其他节点同步过来的区块需要被验证才能写入到本地的数据库,这些就需要依赖EVM和数据存储的能力。

执行层核心数据结构

Ethereum

在eth/backend.go中的Ethereum结构是整个以太坊协议的抽象,基本包括了以太坊中的主要组件,但EVM是一个例外,它会在每次处理交易的时候实例化,不需要随着整个节点初始化,下文中的Ethereum都是指这个结构体:

typeEthereumstruct{//以太坊配置,包括链配置config*ethconfig.Config//交易池,用户的交易提交之后先到交易池txPool*txpool.TxPool//用于跟踪和管理本地交易(localtransactions)localTxTracker*locals.TxTracker//区块链结构blockchain*core.BlockChain//是以太坊节点的网络层核心组件,负责处理所有与其他节点的通信,包括区块同步、交易广播和接收,以及管理对等节点连接handler*handler//负责节点发现和节点源管理discmix*enode.FairMix//负责区块链数据的持久化存储chainDbethdb.Database//负责处理各种内部事件的发布和订阅eventMux*event.TypeMux//共识引擎engineconsensus.Engine//管理用户账户和密钥accountManager*accounts.Manager//管理日志过滤器和区块过滤器filterMaps*filtermaps.FilterMaps//用于安全关闭filterMaps的通道,确保在节点关闭时正确清理资源closeFilterMapschanchanstruct{}//为RPCAPI提供后端支持APIBackend*EthAPIBackend//在PoS下,与共识引擎协作验证区块miner*miner.Miner//节点接受的最低gas价格gasPrice*big.Int//网络IDnetworkIDuint64//提供网络相关的RPC服务,允许通过RPC查询网络状态netRPCService*ethapi.NetAPI//管理P2P网络连接,处理节点发现和连接建立并提供底层网络传输功能p2pServer*p2p.Server//保护可变字段的并发访问locksync.RWMutex//跟踪节点是否正常关闭,在异常关闭后帮助恢复shutdownTracker*shutdowncheck.ShutdownTracker}

Node

在node/node.go中的Node是另一个核心的数据结构,它作为一个容器,负责管理和协调各种服务的运行。在下面的结构中,需要关注一下lifecycles字段,Lifecycle用来管理内部功能的生命周期。比如上面的Ethereum抽象就需要依赖Node才能启动,并且在lifecycles中注册。这样可以将具体的功能与节点的抽象分离,提升整个架构的扩展性,这个Node需要与devp2p中的Node区分开。

typeNodestruct{eventmux*event.TypeMuxconfig*Config//账户管理器,负责管理钱包和账户accman*accounts.Managerloglog.LoggerkeyDirstringkeyDirTempbooldirLock*flock.Flockstopchanstruct{}//p2p网络实例server*p2p.ServerstartStopLocksync.Mutex//跟踪节点生命周期状态(初始化、运行中、已关闭)stateintlocksync.Mutex//所有注册的后端、服务和辅助服务lifecycles[]Lifecycle//当前提供的API列表rpcAPIs[]rpc.API//为RPC提供的不同访问方式http*httpServerws*httpServerhttpAuth*httpServerwsAuth*httpServeripc*ipcServerinprocHandler*rpc.Serverdatabasesmap[*closeTrackingDB]struct{}}

如果以一个抽象的维度来看以太坊的执行层,以太坊作为一台世界计算机,需要包括三个部分,网络、计算和存储,那么以太坊执行层中与这三个部分相对应的组件是:

网络:devp2p计算:EVM存储:ethdb

devp2p

以太坊本质还是一个分布式系统,每个节点通过p2p网络与其他节点相连。以太坊中的p2p网络协议的实现就是devp2p。

devp2p有两个核心功能,一个是节点发现,让节点在接入网络时能够与其他节点建立联系;另一个是数据传输服务,在与其他节点建立联系之后,就可以想换交换数据。

在p2p/enode/node.go中的Node结构代表了p2p网络中一个节点,其中enr.Record结构中存储了节点详细信息的键值对,包括身份信息(节点身份所使用的签名算法、公钥)、网络信息(IP地址,端口号)、支持的协议信息(比如支持eth/68和snap协议)和其他的自定义信息,这些信息通过RLP的方式编码,具体的规范在eip-778中定义:

typeNodestruct{//节点记录,包含节点的各种属性renr.Record//节点的唯一标识符,32字节长度idID//hostname跟踪节点的DNS名称hostnamestring//节点的IP地址ipnetip.Addr//UDP端口udpuint16//TCP端口tcpuint16}//enr.RecordtypeRecordstruct{//序列号sequint64//签名signature[]byte//RLP编码后的记录raw[]byte//所有键值对的排序列表pairs[]pair}

在p2p/discover/table.go中的Table结构是devp2p实现节点发现协议的核心数据结构,它实现了类似Kademlia的分布式哈希表,用于维护和管理网络中的节点信息。

printf("typeTablestruct{mutexsync.Mutex//按距离索引已知节点buckets[nBuckets]*bucket//引导节点nursery[]*enode.NoderandreseedingRandomipsnetutil.DistinctNetSetrevalidationtableRevalidation//已知节点的数据库db*enode.DBnettransportcfgConfigloglog.Logger//周期性的处理网络中的各种事件refreshReqchanchanstruct{}revalResponseChchanrevalidationResponseaddNodeChchanaddNodeOpaddNodeHandledchanbooltrackRequestChchantrackRequestOpinitDonechanstruct{}closeReqchanstruct{}closedchanstruct{}//增加和移除节点的接口nodeAddedHookfunc(*bucket,*tableNode)nodeRemovedHookfunc(*bucket,*tableNode)}world!");

ethdb

ethdb完成以太坊数据存储的抽象,提供统一的存储接口,底层具体的数据库可以是leveldb,也可以是pebble或者其他的数据库。可以有很多的扩展,只要在接口层面保持统一。

有些数据(如区块数据)可以通过ethdb接口直接对底层数据库进行读写,其他的数据存储接口都是建立的ethdb的基础上,比如数据库有很大部分的数据是状态数据,这些数据会被组织成MPT结构,在Geth中对应的实现是trie,在节点运行的过程中,trie数据会产生很多中间状态,这些数据不能直接调用ethdb进行读写,需要triedb来管理这些数据和中间状态,最后才通过ethdb来持久化。

在ethdb/database.go中定义底层数据库的读写能力的接口,但没有包括具体的实现,具体的实现将由不同的数据库自身来实现。比如leveldb或者pebble数据库。在Database中定义了两层数据读写的接口,其中KeyValueStore接口用于存储活跃的、可能频繁变化的数据,如最新的区块、状态等。AncientStore则用于处理历史区块数据,这些数据一旦写入就很少改变。

//数据库的顶层接口typeDatabaseinterface{KeyValueStoreAncientStore}//KV数据的读写接口typeKeyValueStoreinterface{KeyValueReaderKeyValueWriterKeyValueStaterKeyValueRangeDeleterBatcherIterateeCompacterio.Closer}//处理老数据的读写的接口typeAncientStoreinterface{AncientReaderAncientWriterAncientStaterio.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中定义了当前所有的操作码:

//EVMtypeEVMstruct{//区块上下文,包含区块相关信息ContextBlockContext//交易上下文,包含交易相关信息TxContext//状态数据库,用于访问和修改账户状态StateDBStateDB//当前调用深度depthint//链配置参数chainConfig*params.ChainConfigchainRulesparams.Rules//EVM配置ConfigConfig//字节码解释器interpreter*EVMInterpreter//中止执行的标志abortatomic.BoolcallGasTempuint64//预编译合约映射precompilesmap[common.Address]PrecompiledContractjumpDestsmap[common.Hash]bitvec}typeEVMInterpreterstruct{//指向所属的EVM实例evm*EVM//操作码跳转表table*JumpTable//Keccak256哈希器实例,在操作码间共享hashercrypto.KeccakState//Keccak256哈希结果缓冲区hasherBufcommon.Hash//是否为只读模式,只读模式下不允许状态修改readOnlybool//上一次CALL的返回数据,用于后续重用returnData[]byte}typeContractstruct{//调用者地址callercommon.Address//合约地址addresscommon.Addressjumpdestsmap[common.Hash]bitvecanalysisbitvec//合约字节码Code[]byte//代码哈希CodeHashcommon.Hash//调用输入Input[]byte//是否为合约部署IsDeploymentbool//是否为系统调用IsSystemCallbool//可用gas量Gasuint64//调用附带的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/main.go:geth节点启动入口cmd/geth/config.go(makeFullNode):加载配置,初始化节点node/node.go:初始化以太坊节点的核心容器node.rpcstack.go:初始化RPC模块accounts.manager.go:初始化accountManagereth/backend.go:初始化Ethereum实例node/node.goOpenDatabaseWithFreezer:初始化chaindbeth/ethconfig/config.go:初始化共识引擎实例(这里的共识引擎并不真正参与共识,只是会验证共识层的结果,以及处理validator的提款请求)core/blockchain.go:初始化blockchaincore/filterMaps.go:初始化filtermapscore/txpool/blobpool/blobpool.go:初始化blob交易池core/txpool/legacypool/legacypool.go:初始化普通交易池cord/txpool/locals/tx_tracker.go:本地交易追踪(需要配置开启本地交易追踪,本地交易会被更高优先级处理)eth/handler.go:初始化协议的Handler实例miner/miner.go:实例化交易打包的模块(原挖矿模块)eth/api_backend.go:实例化RPC服务eth/gasprice/gasprice.go:实例化gas价格查询服务internal/ethapi/api.go:实例化P2P网络RPCAPInode/node.go(RegisterAPIs):注册RPCAPInode/node.go(RegisterProtocols):注册p2p的Ptotocolsnode/node.go(RegisterLifecycle):注册各个组件的生命周期cmd/utils/flags.go(RegisterFilterAPI):注册FilterRPCAPIcmd/utils/flags.go(RegisterGraphQLService):注册GraphQLRPCAPI(如果配置了的话)cmd/utils/flags.go(RegisterEthStatsService):注册EthStatsRPCAPI(如果配置了的话)eth/catalyst/api.go:注册EngineAPI

节点的初始化会在cmd/geth/config.go中的makeFullNode中完成,重点会初始化以下三个模块

在第一步会初始化node/node.go中的Node结构,就是整个节点容器,所有的功能都需要在这个容器中运行,第二步会初始化Ethereum结构,其中包括以太坊各种核心功能的实现,Etherereum也需要注册到Node中,第三步就是注册EngineAPI到Node中。

其中Node初始化就是创建了一个Node实例,然后初始化p2pserver、账号管理以及http等暴露给外部的协议端口。

Ethereum的初始化就会复杂很多,大多数的核心功能都是在这里初始化。首先会初始化化ethdb,并从存储中加载链配置,然后创建共识引擎,这里的共识引擎不会执行共识操作,而只是会对共识层返回的结果进行验证,如果共识层发生了提款请求,也会在这里完成实际的提款操作。然后再初始化BlockChain结构和交易池。

这些都完成之后就会初始化handler,handler是所有p2p网络请求的处理入口,包括交易同步、区块下载等等,是以太坊实现去中心化运行的关键组件。在这些都完成之后,就会将一些在devp2p基础之上实现的子协议,比如eth/68、snap等注册到Node容器中,最后Ethereum会作为一个lifecycle注册到Node容器中,Ethereum初始化完成。

最后EngineAPI的初始化相对简单,只是将EngineAPI注册到Node中。到这里,节点初始化就全部完成了。

节点启动

在完成节点的初始化之后,就需要启动节点了,节点启动的流程相对简单,只需要将已经注册的RPC服务和Lifecycle全部启动,那么整个节点就可以向外部提供服务了。

06\总结

在深入理解以太坊执行层的实现之前,需要对以太坊有一个整体的认识,可以将以太坊整体看作是一个交易驱动的状态机,执行层负责交易的执行和状态的变更,共识层则负责驱动执行层运行,包括让执行层产出区块、决定交易的顺序、为区块投票、以及让区块获得最终性。由于这个状态机是去中心化的,所以需要通过p2p网络与其他的节点通信,共同维护状态数据的一致性。

在执行层不负责决定交易的顺序,只负责执行交易并记录交易执行之后的状态变化。这里的记录有两种形式,一种是以区块的方式将所有的状态变化都记录下来,另一种是在数据库中记录当前的状态。同时执行层也是交易的入口,通过交易池来存储还没有被打包进区块的交易。如果其他的节点需要获取区块、状态和交易数据,执行层就会通过p2p网络将这些信息发送出去。

对于执行层,有三个核心模块:计算、存储和网络。计算对应EVM的实现,存储则对应了ethdb的实现,网络对了devp2p的实现。有了这样的整体认识之后,就可以深入去理解每一个子模块,而不会迷失在具体的细节中。

 

07\Ref

[1]https://ethereum.org/zh/what-is-ethereum/

[2]https://epf.wiki/#/wiki/protocol/architecture

[3]https://clientdiversity.org/#distribution

[4]https://github.com/ethereum/devp2p

[5]https://github.com/ethereum/execution-specs

[6]https://github.com/ethereum/consensus-specs

·END·

内容|Ray

编辑&排版|环环

设计|Daisy

Join now ?

立即创建 账号,开始交易