客户端 besu、reth
以太坊执行层(EL)拥有多种客户端实现,其中 Besu 和 Reth 是两个重要的代表。它们的主要职责是处理交易执行、维护全局状态以及通过 Engine API 与共识层(Consensus Layer,CL)协同工作。
Besu
Besu 是一个基于 Java 开发的以太坊执行层客户端。
- 架构设计:Besu 采用多模块的 Gradle 项目结构,主要模块包括:
evm:负责以太坊虚拟机(EVM)的行为,实现了每个操作码(opcode)的功能。ethereum:包含api(与以太坊状态交互)和core(数据存储和共识设置)。besu:定义了所有命令行界面(CLI)参数,并包含主程序入口。config:负责配置的组装、验证以及创世信息。
- 核心组件:
BesuControllerBuilder:管理设置客户端所需的所有组件并返回控制器。ProtocolSpec与MainnetProtocolSpec:定义了协议内部的具体运作规则,涵盖了从 Frontier 阶段至今的所有主网规范。MainnetEVMs:为 EVM 提供适用于不同硬分叉的操作。
- 测试体系:Besu 拥有严谨的测试流程,包括单元测试、集成测试、接受测试(运行多个节点以创建共识算法任务)以及引用测试(来自以太坊基金会的标准化测试)。
Reth
Reth 是一个注重性能的执行层客户端实现,其架构围绕高效的同步和存储设计。
-
流水线架构(Pipeline):Reth 的核心驱动力是其同步流水线,该流水线被划分为 12 个可配置阶段(Stages),按顺序(由上至下)执行:
-
HeaderStage与BodyStage:负责验证区块头并通过 P2P 下载区块数据。 -
SenderRecoveryStage:执行耗时较高的操作,从交易签名中恢复发送者地址。 -
ExecutionStage:通过 REVM 执行交易,生成收据(receipts)和变更集(change sets)。 -
MerkleStage:在执行后计算并验证状态根是否准确。 -
历史索引阶段:如
IndexStorageHistoryStage和IndexAccountHistoryStage,允许查询任意区块号的历史数据。
-
-
存储与数据库:
- Reth 主要使用 MDBX 数据库,并通过
Provider抽象层进行访问,以便未来能够以最小改动更换底层数据库。 - 使用 Codecs(编解码器) 优化存储空间,例如通过
Compact特性压缩无符号整数的领先零。
- Reth 主要使用 MDBX 数据库,并通过
-
特色组件:
BlockchainTree:当同步接近链尖(Tip)时,在内存中进行状态根验证和执行.Pruner(修剪器):允许运行全节点(Full Node)模式,在区块最终确定(Finalized)后删除旧的变更集和历史索引以节省空间。
Engine API
Engine API 是位于执行层(EL)中的一个标准化接口,专门用于共识层(CL)与执行层之间的内部通信。它是以太坊转向权益证明(PoS)架构后的核心组件,确保了共识逻辑与交易执行的清晰分离。
核心定义与功能
- 通信桥梁:执行层客户端(如 Besu、Reth、Geth)作为一个“执行引擎”,通过 Engine API 接收来自共识层客户端(如 Lighthouse、Prysm)的指令。
- 职责分离:在这种设计下,共识层负责处理共识规则和分叉选择,而执行层则专注于验证区块头、执行交易以及维护全局状态。
- 内部使用:该 API 不同于面向用户的 JSON-RPC API(如 DApp 使用的 API),它仅供 CL 访问,不向公众开放。
技术特性
- 通信协议:使用基于 HTTP 的 JSON-RPC 接口。
- 身份验证:为了安全起见,通信必须通过 JWT(JSON Web Token)令牌进行身份验证,以确保只有授权的共识层客户端能够驱动执行引擎。
- 驱动机制:执行层不能自主运行,必须由共识层通过调用 Engine API 方法来驱动其功能。
核心方法
Engine API 主要通过以下几类端点实现协同工作:
engine_newPayload:用于处理负载的验证与插入。当共识层收到新的信标区块时,会提取其中的执行负载并调用此方法,要求执行层验证交易合法性并更新状态。执行层会返回VALID(有效)、INVALID(无效)或SYNCING(同步中)等状态。engine_forkchoiceUpdated:用于管理状态同步并触发区块构建。共识层通过此方法通知执行层最新的规范链头(Head)、安全区块(Safe)和最终确定的区块(Finalized)哈希。如果节点被选为提议者,此调用还会包含负载属性以启动区块构建任务。engine_exchangeCapabilities:在节点启动时,双方通过此方法协商支持的 API 版本(如 V1、V2、V3),以确保兼容性并启用新功能。
典型工作流程
- 节点启动:CL 与 EL 交换功能列表并建立通信。
- 同步与更新:CL 持续发送
forkchoiceUpdated指令,告知 EL 全局网络达成的共识状态,引导 EL 进行状态同步。 - 验证者操作:当验证者需要提议区块时,CL 通过 API 获取 EL 构建好的执行负载;当收到他人提议的区块时,CL 调用
newPayload让 EL 执行状态转换函数(STF)以确认区块有效性。
新有效载荷(engine_newPayload)的具体验证步骤?
在以太坊架构中,engine_newPayload 是执行层(EL)验证由共识层(CL)传递的“执行有效载荷”(Execution Payload)的核心方法。该过程实质上是触发执行层的状态转换函数(STF),以确保新区块符合协议规则。
具体验证步骤可以分为以下三个阶段:
区块头验证(Header Verification)
在执行任何交易之前,执行引擎首先对载荷的头部信息进行一系列“轻量级”的一致性检查:
- 父块哈希验证:确认载荷头部的父块哈希(parent hash)在本地链中存在,且与预期相符。
- 区块连续性:验证当前区块高度(number)是否正好是父块高度加 1。
- 时间戳检查:确保当前区块的时间戳大于其父块的时间戳。
- Gas 相关校验:
- 验证区块使用的 Gas($H_{gasUsed}$)不超过区块 Gas 限制($H_{gasLimit}$)。
- Gas 限制波动检查:$H_{gasLimit}$ 的变动幅度不能超过前一区块的 1/1024。
- Base Fee 校验:验证 EIP-1559 的基本费用是否根据上个区块的 Gas 使用情况进行了准确更新。
- 合并(Post-Merge)特定规则:
- 验证难度值(Difficulty)必须为 0。
- 验证 Nonce 必须为全零(0x00…0)。
- 验证叔块哈希(ommersHash)为空列表的哈希值。
- 对于 Cancun 升级后的载荷,还会检查 Blob Gas 使用量是否符合限制。
交易执行与状态转换(Transaction Execution)
一旦头部验证通过,EL 将进入核心执行阶段:
- 环境初始化:将区块头中的 coinbase(提款地址)、gas limit 和 timestamp 等信息作为上下文传递给 EVM(以太坊虚拟机)。
- 逐笔执行交易:在 EVM 中按顺序处理区块内的所有交易。
- 合法性检查:包括验证签名、检查 Nonce 是否匹配、扣除 Gas 费用等。
- 状态修改:如果交易合法,将修改账户余额、合约代码及存储数据。
- 报错处理:如果区块中存在任何一笔无效交易,整个区块将被视为无效(Invalid),因为这会污染整个区块的状态。
结果汇总与状态确认
执行完成后,EL 会进行最后的一致性比对:
- 状态根比对:在所有交易执行完毕后,EL 会计算最终的状态根哈希(State Root),并确认其与载荷头部的 root 属性一致。
- 提款处理:对于支持提款(Withdrawals)的区块(如 Shanghai 升级后),EL 会将提款金额增加到相应地址的余额中,并计入最终状态。
- 返回状态标识:EL 向 CL 返回验证结果:
- VALID:所有验证通过,状态转换成功。
- INVALID:头部验证失败或交易执行出错。
- SYNCING:执行层仍在同步中,暂时无法验证该载荷。
- ACCEPTED:基础检查通过,但由于节点处于浅状态(Shallow state)等原因尚未完成完整执行。
状态机
以太坊是一个通用计算系统,其核心运作模式是一个状态机(State Machine)。这意味着它会根据接收到的输入(即交易),在不同的状态之间进行转换。
全局状态(Global State)
与比特币仅维护全局未花费交易输出(UTXO)的设计不同,以太坊维护着一个全局状态。
- 内容组成:状态是指所有账户的综合集合,包括地址、余额、智能合约的代码与存储数据,以及当前的系统和网络状态。
- 数据结构:这些数据通过 Merkle-Patricia Tries (MPT) 这种特定的数据结构进行组织,并存储在底层的键值数据库(如 LevelDB, Pebble 或 MDBX)中。
状态转换函数(STF)
状态机通过**状态转换函数(STF)**来实现从旧状态到新状态的迁移。
- 触发机制:状态转换由**交易(Transactions)**触发。当交易在执行引擎中被处理时,如果被判定为合法,就会导致以太坊网络的状态发生改变。
- 执行环境(EVM):**以太坊虚拟机(EVM)**充当了状态机的虚拟 CPU。它负责处理智能合约逻辑,并确保无论在何种硬件平台上运行,所有节点都能得到确定且一致的计算结果,从而达成共识。
执行层(EL)在状态机中的角色
在以太坊转向权益证明(PoS)后,执行层客户端(如 Besu, Reth, Geth)的主要职责被简化为执行状态转换函数。
- 通信流程:共识层(CL)通过 Engine API 调用
engine_newPayload方法,将执行负载传达给执行层。执行层随后运行 STF 进行验证。 - 具体验证步骤:
- 验证区块头:检查父块哈希、时间戳、Gas 限制(变动幅度需在 1/1024 以内)以及 EIP-1559 基础费用等。
- 环境初始化:将区块头中的 coinbase、Gas 限制和时间戳等作为上下文传递给 EVM。
- 逐笔执行交易:在 EVM 中按顺序处理交易,验证签名、Nonce 并扣除费用。
- 最终状态提交:一旦所有交易执行完毕且验证通过,系统会计算新的状态根哈希并提交,完成状态更新。
状态的一致性与完整性
为了确保状态机的鲁棒性,执行层采取了严格的验证措施:
- 原子性:如果区块中存在任何一笔无效交易,整个区块都会被视为无效,因为它会“污染”整个状态转换过程。
- 状态根验证:在每个区块处理结束时,计算出的状态根哈希必须与区块头中的预期值匹配,以确保所有节点对“全局状态”的看法完全一致。
- 同步机制:新加入的节点可以通过**全同步(Full Sync)从创世块开始重播所有交易来重建状态,或通过快照同步(Snap Sync)**下载特定时间点的状态快照并进行愈合(Healing),以快速赶上网络的最新状态。
EVM Data Locations
以太坊虚拟机(EVM)在执行智能合约时,主要通过四个核心数据位置来管理和操作数据:Stack(栈)、Memory(内存)、**Storage(存储)**和 Calldata(调用数据)。
Stack (栈)
- 结构与原则:栈是一个简单的 LIFO(后进先出)数据结构,支持 PUSH(压入)和 POP(弹出)操作。
- 用途:它是大多数 EVM 操作码工作的场所,用于存储中间计算值并为操作码提供参数。
- 限制:
- 最大容量:最多可容纳 1024个项目,每个项目大小为 32字节(即 1个字/Word)。
- 访问限制:通过
DUP和SWAP操作码,仅能访问栈顶的 16个项目。
- 生命周期:每次合约执行完成后,栈都会被重置。
Memory (内存)
- 结构:内存是一个字节数组,所有位置最初都定义为零。
- 用途:存储与整个程序相关的临时(临时性)数据。由于栈有严格的容量限制,内存通过索引访问任意大小的数据,作为栈的补充。
- 动态扩展与成本:
- 内存在逻辑上可以无限扩展(达 $2^{256}$ 字节),但实际上受 Gas 成本限制。
- 内存扩展(Memory Expansion):内存按“页”(每页 1个字,即32字节)进行动态分配,扩展页数越多,消耗的 Gas 越高。
- 操作:常用的操作码包括
MSTORE(存储一个字)、MSTORE8(存储一个字节)和MLOAD(读取一个字)。
Storage (存储)
- 结构:存储被设计为一个按字寻址的字数组(键值数据库),拥有 $2^{256}$ 个插槽,每个插槽 32 字节。
- 持久性:与内存不同,存储与以太坊账户相关联,并且作为世界状态(World State)的一部分在交易之间持久化。
- 成本与优化:
- 由于存储操作需要跨所有节点同步,因此
SSTORE(写入)操作非常昂贵。 - Solidity 等高级语言通常会将多个小变量打包进同一个 32 字节的插槽中以优化成本。
- 由于存储操作需要跨所有节点同步,因此
- 底层实现:合约账户包含一个存储根(Storage Root),指向该合约专属的梅克尔-帕特里夏树(Merkle Patricia Trie)。
Calldata (调用数据)
- 特性:调用数据是只读的输入数据,通过交易或消息调用指令传递给 EVM。
- 结构:它以字节序列的形式存储。
- 访问方式:
CALLDATALOAD:从指定偏移量读取 32 字节到栈上。CALLDATACOPY:将调用数据的某一部分复制到内存中。