执行客户端(execution client)要同时处理区块同步、交易池、状态存储、JSON-RPC 与共识对接,以太坊虚拟机(Ethereum Virtual Machine,EVM)只是其中一块。EVM 不知道区块从哪里来,也不决定一笔交易的 gas 价格由哪条规则给出,它接收字节码、调用数据、执行环境与 gas,返回返回数据、剩余 gas、异常状态与一组状态变更。
把这条边界画清楚,才能理解 go-ethereum、evmone 与 revm 为什么处在不同的位置:一个是客户端内置的解释器,一个是跨语言的独立库,一个是被当作依赖导入的 Rust 框架。下面只讨论通用客户端里 EVM 的嵌入方式与多实现一致性如何保障,不展开并行调度与多引擎设计,那部分见文末延伸阅读。
EVM 与客户端其余部分的边界
无论哪一个实现,EVM 与宿主之间的交互都收敛到同一组操作:读账户的余额、nonce 与代码,读存储槽,读历史区块哈希;写回存储变更,发出日志,创建或删除账户;以及 gas 的收支。差别只在于这组操作以什么形式暴露。
go-ethereum 的 core/vm 把 EVM 实现为一个结构体加一个解释器循环。解释器按 fork 选择一张有 256 个条目的跳表(jump table),每个条目包含执行函数、常量 gas、动态 gas 计算函数、栈高度约束与内存需求。循环的顺序是取操作码、校验栈、扣常量 gas、计算动态 gas、必要时扩展内存、调用执行函数,gas 不足统一返回 ErrOutOfGas。状态访问走 StateDB,区块与交易上下文走 BlockContext 与 TxContext。内在 gas、nonce 校验、手续费扣除与退款计算不在解释器里,而在状态转换层完成。
revm 的结构更显式:Evm 由 Context 与构造器组成,Context 又由三块构成,环境(Transaction、Block、Cfg)、Journal 与 Database。Database 是一个只有四个必需方法的接口:basic 读账户、code_by_hash 读代码、storage 读存储槽、block_hash 读历史区块哈希。这些数据被读入后缓存在 Journal 中,Journal 同时记录执行期间的变更,并在执行结束后交回状态差异。解释器不直接接触数据库,而是通过 Host 特征访问,该特征按区块、交易、配置、数据库与 Journal 分组,覆盖 sload、sstore、tload、tstore、账户加载、日志与自毁。
evmone 把这条边界做成了一个 C 语言的 ABI。它实现 EVMC(Ethereum Client-VM Connector API),该接口被定义为 EVM 与客户端之间的低层 ABI,客户端侧规定 EVM 访问环境与状态的方式。因为边界是 C ABI,evmone 可以被其他语言写的客户端加载;Erigon 的复盘提到,它通过 EVMC 把 evmone 接入了自己的执行层,并额外包了一层 C++ 代码来降低接口开销。
三种实现的定位差异
| 实现 | 语言与许可 | 形态 | 状态接口 | 典型复用方式 |
|---|---|---|---|---|
go-ethereum core/vm | Go,库代码 LGPL-3.0,cmd/ 下为 GPL-3.0 | 客户端内置解释器 | StateDB | 被 fork 为 L2 执行客户端,例如 OP Stack 的 op-geth |
| evmone | C++20,Apache-2.0 | 独立模块,通过 EVMC 暴露 | EVMC 宿主接口 | 被 Silkworm 等客户端加载为执行模块 |
| revm | Rust,MIT | 库与框架,按 crate 拆分 | Database 与 Host 特征 | Reth、Foundry、Helios 等直接作为依赖,也是 L2 与 zkVM 的常见选择 |
三种形态对应三种工程取舍。内置解释器可以和自己的状态层、跳表与追踪器一起优化,代价是很难被别的客户端复用。独立库可以跨语言复用,但必须穿过一层通用接口,接口两侧的调用开销就是成本。同一篇复盘提到,EVMC 设计上允许宿主调用 EVM、也允许 EVM 反调宿主,后者在通过 CGo 接入 Erigon 时带来了可观的接口开销,最终需要补充 C++ 代码来消除。这条经验说明,把最快的 EVM 接进客户端,与让客户端立刻变快,是两件事。
库形态的收益在生态层面更明显。revm 的文档列出的使用方包括区块构建者、Reth 与 Helios 这类客户端、Foundry 与 Hardhat 这类工具、多条 L2 以及若干 zkVM 项目。反过来,基于 go-ethereum 做 fork 的路线在 L2 侧出现了收缩信号:Optimism 文档显示 op-geth 已于 2026 年 5 月 31 日结束支持,且不支持当前激活的 Karst 硬分叉,主推的执行客户端换成基于 Reth 与 revm 的 op-reth。
一致性不是自测出来的:state test 与多客户端测试
共识要求所有节点对同一段字节码得出完全相同的状态。任何单客户端自测都只能证明自洽,不能证明一致。
历史上不一致的代价可以量化。2016 年 11 月 24 日,以太坊在区块 2686351 发生分叉:当导致空账户删除的交易以 out-of-gas 结束时,go-ethereum 没有回滚这些删除,Parity 回滚了。少数派链在区块 2686516 附近被放弃,约 165 个区块的产出作废;go-ethereum 在 v1.5.3 中修正日志机制以匹配 Parity 的行为,官方说明同时指出 Parity 在“out-of-gas 调用预编译合约”这一更窄的场景里也存在不一致。这次修订给 EIP-161 补上了“状态回滚时空账户删除也应回滚”的说明。客户端代码里还留着另一处同类痕迹:go-ethereum 的状态日志对 RIPEMD160 预编译地址 0x03 保留一个显式 touch 标记,源码注释说明它用于复现区块 1714175 的空账户 touch/revert 特例。规范与实现之间的这类硬编码兼容会长期存在,也是多实现一致性需要逐例核对的原因。
行业应对的方法是让规范与测试向量同源。执行层规范以 Python 参考实现(Execution Layer Specification,常称 EELS)的形式维护,测试框架从规范出发生成测试向量(fixture)。execution-spec-tests 原本是生成测试向量的 Python 框架与用例集合,2025 年 11 月整体迁入 ethereum/execution-specs(旧仓库随后归档),官方说明客户端的 fixture 发布位置不变,规格与测试从此同源。
状态测试(state test)的判定方式很直接:给定执行前状态 pre、环境 env、交易 transaction 与按分叉划分的期望结果 post,客户端应用该交易后必须匹配 post 中记录的状态根 hash 与日志摘要 logs;对应当失败的交易,则以 expectException 描述预期异常类型。区块测试在此基础上覆盖区块级处理。这些向量既能通过各客户端自带的测试入口直接执行,go-ethereum 用 evm statetest、Besu 用 evmtool state-test、Nethermind 用 nethtest、evmone 用 evmone-statetest 与 evmone-blockchaintest;也能放进 Hive,用 Engine API 向客户端发送区块载荷并校验响应与状态同步,这是最接近生产行为的测试形态。ethpandaops 的 hive-tests 仓库把这类测试做成每日运行的流水线,覆盖 Besu、Erigon、EthereumJS、Ethrex、go-ethereum、Nethermind、Nimbus-EL 与 Reth。
测试的边界同样要说清楚。它只能覆盖已经被写下来的行为,每引入一个分叉,就多出一段尚未被向量固定的窗口;它约束的是可观测语义,不是性能,也不是内部结构。
指令分派:跳表、生成式 switch 与内联
解释器的主循环每执行一条指令都要先定位它的处理函数。跳表的做法是查一次 256 项数组,再通过函数指针调用。Go 无法穿过函数值内联,因此每条指令都要付出一次数组加载加一次间接调用。
go-ethereum 在 2026 年提交过把分派改成生成式 switch 的改动(PR #35144 与替代它的 PR #35638)。对操作码字节做密集 switch 会编译成跳转表,编译器可以把热点处理函数内联进循环。改动分三层:fork 间行为稳定的热点操作码单独成 case,常量 gas 与栈上下界直接写成常数;带动态 gas 但 fork 间不变的少数操作码仍走表计费,但按名字调用处理函数;凡是随 fork 变化的操作码(CALL、CREATE、SSTORE、SLOAD、日志与复制类)继续通过当前 fork 的表分派。原解释器循环保留为参考实现,测试同时跑两条循环并比对输出、gas、错误、退款、日志与状态根,另有针对任意字节码的差分模糊测试。
PR #35638 内的 A/B 基准给出的数字如下,条件为 2000 个主网区块(区块号 25677501 至 25679500)、geth-benchmark-1 机器、go1.27.0、每侧 3 次运行:吞吐从 391.2 MGas/s 升至 424.9 MGas/s,newPayload 平均耗时从 71.6 ms 降至 65.1 ms,执行阶段 p50 从 37.47 ms 降至 31.58 ms。截至 2026 年 9 月 18 日,该 PR 在 GitHub 上仍为 open、未被合并,因此这些数字属于 PR 内部基准,不是发行版测量,引用时应按待验证处理。
evmone 走了另一条路。它的默认 baseline 解释器只做最基本的 JUMPDEST 分析;可选的 advanced 解释器使用间接调用线索化(indirect call threading),把加载后的 EVM 程序表示为一张指向虚拟指令实现函数的指针表,并按基本块预计算 gas 与栈需求,在执行到块入口时一次校验。代价是执行前的字节码分析更重。
gas 计量:常量与动态分离,冷热访问按槽计价
解释器必须在执行操作码之前算清费用。go-ethereum 把它拆成常量部分与动态部分:常量 gas 直接扣,动态 gas 由处理函数按栈、内存与状态计算;内存扩展费用随所需字数超线性增长,因此解释器要先算出操作码要求的内存大小再计费。
EIP-2929 之后,gas 计量与状态访问层被绑在了一起。该提案要求每笔交易维护已访问地址与已访问存储槽两个集合,存储槽的元素类型是 Set[Tuple[Address, Bytes32]];首次访问某个槽收 COLD_SLOAD_COST,即 2100 gas,重复访问收 WARM_STORAGE_READ_COST,即 100 gas,首次访问某个地址收 COLD_ACCOUNT_ACCESS_COST,即 2600 gas。访问集合属于交易上下文,物理上由客户端的执行环境持有,解释器每次 SLOAD 或调用都要回来查询并更新它。这也是改解释器很难脱离状态层单独完成的原因。
按块预计算 gas 有可观测的副作用。evmone 的文档明确指出:因为整个基本块的需求被提前检查,可能出现在逐条计费下本会执行、提前计费下不会执行的外部可见操作;文档认为这不构成共识问题,因为执行以硬异常终止且所有效果都被回滚,但它可能产生不同的执行追踪,或以不同的异常类型结束。这类差异正好落在测试向量约束的范围内。
内存与调用帧:把分配挪出热路径
每一次合约调用都需要栈与内存。EVM 的栈有 1024 项上限,内存按需扩展并计费,返回数据也需要缓冲区。这些对象的生命周期短、创建频率高,因此实现普遍倾向于复用而非每次分配:go-ethereum 在解释器入口创建一份 Memory 对象,操作码要求更大内存时按字扩展;revm 的上下文层持有一个池化条目(item-pooling)的 FrameStack,按索引复用调用帧。
预编译:原生实现与依赖取舍
预编译合约是少数几个固定地址上的原生实现,执行时由宿主直接计算,不解释字节码,解释器只负责路由与 gas 计费,部分预编译的费用还与输入长度相关。
独立库在这一层的取舍值得单独看。evmone 的文档说明了两处妥协:ecrecover 由 evmone 自己实现,性能有下降;expmod 默认使用桩实现,只对已知输入给出正确响应,要拿到完整实现需要在构建时打开 EVMONE_PRECOMPILES_GMP=1,并引入 GMP 作为构建与运行时依赖。这反映了一个通用问题:预编译的正确性要求对任意输入都成立,而把大整数运算、椭圆曲线恢复与 Keccak 哈希等原生代码做到与其它实现逐位一致,本身也是一处容易藏分歧的地方。
JIT 为什么没有成为主流选择
EVM 客户端里的 JIT(just-in-time compilation,即时编译)只有过一次正式尝试:ethereum/evmjit 基于 LLVM,在运行时把合约代码编译成机器码,用来替换客户端里基于解释器的 EVM。该仓库现已归档,README 写明项目不再维护、不要用于任何重要用途。
停止维护的原因 README 没有说明,从技术条件看有几处障碍。EVM 的跳转目标来自栈上的值,代码在运行时可被读取,程序结构要到执行时才确定,编译器很难像在 JVM 那样在编译期获得稳定的控制流图;单次调用可用的计算量还受区块 gas 上限约束,收益总量存在天花板。相对成熟的替代是把分析放到静态阶段,EOF(EVM Object Format,EIP-7692)正是这个方向的尝试,它包含容器格式、静态相对跳转、栈验证与代码段数据段分离等条目。但这条路线目前并不活跃:EOF 在 2025 年 4 月 28 日被移出 Fusaka 升级范围,EIPs PR #9703 当天合并,只覆盖 Fusaka 阶段的移除;EIP-7692 本身的状态标为 stagnant,Glamsterdam 的官方范围清单 EIP-7773 里也没有它。第三方跟踪站点 eipsinsight 的变更记录显示,EIP-7692 在 2026 年 8 月 27 日的 Glamsterdam 范围整理中被移出候选桶,这一条只有第三方来源。因此短期内通用客户端的优化主战场仍是解释器,编译执行尚无落地空间。
什么条件下这些优化不奏效
执行不再是耗时主体时,收益递减。在上面引用的 go-ethereum 基准里,执行阶段 p50 从 37.47 ms 降到 31.58 ms,而区块总时间 p50 从 62.1 ms 降到 56.2 ms:按同一张表的账,状态读取、状态哈希与提交合计约占总时间的三分之一,再算上引擎层开销接近一半,解释器优化的天花板由这个比例决定。负载越偏向状态访问与磁盘 I/O,优化解释器的边际收益越低。
fork 差异让优化无法全局套用。跳表按 fork 生成,生成式 switch 也只能内联 fork 间行为稳定的操作码;元数据随 fork 变化的操作码必须留在表分派路径上,否则就会写出错误的常量。
语义约束划定了优化的边界。优化不得改变日志顺序、追踪钩子顺序、异常类型与 gas 计量,这些都被测试向量固定。evmone 那个基本块级提前检查可能提前终止的例子,正是优化与可观测语义之间需要显式权衡的地方。
一致性成本随实现数量上升。每多一个实现,共识边界上就多一处可能分歧的地方;测试向量能压缩这个面,但不能消除它。多实现带来的冗余价值与这份成本,是同一条权衡的两端。
资料来源
- revm 文档,Introduction:https://bluealloy.github.io/revm/
- revm 文档,Architecture:https://bluealloy.github.io/revm/architecture.html
- revm,
Database特征:https://docs.rs/revm/latest/revm/context/trait.Database.html - revm,
Host特征:https://docs.rs/revm-interpreter/latest/revm_interpreter/trait.Host.html - revm 仓库与使用者列表:https://github.com/bluealloy/revm
- evmone 仓库说明(baseline 与 advanced 解释器、预编译取舍):https://github.com/ethereum/evmone
- evmone,Efficient gas calculation algorithm for EVM:https://github.com/ethereum/evmone/blob/master/docs/efficient_gas_calculation_algorithm.md
- EVMC,Ethereum Client-VM Connector API:https://github.com/ethereum/evmc
- Silkworm 项目起源复盘(EVMC 与 CGo 接口开销):https://erigon.substack.com/p/staged-sync-and-short-history-of
- go-ethereum 解释器循环:https://github.com/ethereum/go-ethereum/blob/master/core/vm/interpreter.go
- go-ethereum 跳表定义:https://github.com/ethereum/go-ethereum/blob/master/core/vm/jump_table.go
- go-ethereum PR #35638(生成式 switch 与 PGO 内联,含基准条件):https://github.com/ethereum/go-ethereum/pull/35638
- go-ethereum 仓库与许可说明:https://github.com/ethereum/go-ethereum
- go-ethereum 状态转换层(内在 gas、nonce 校验、退款):https://github.com/ethereum/go-ethereum/blob/master/core/state_transition.go
- 以太坊黄皮书(内存扩展费用函数):https://ethereum.github.io/yellowpaper/paper.pdf
- EIP-2929,Gas cost increases for state access opcodes:https://eips.ethereum.org/EIPS/eip-2929
- execution-specs 仓库(规范与测试向量):https://github.com/ethereum/execution-specs
- The Weld is Complete(execution-spec-tests 合并公告):https://steel.ethereum.foundation/blog/2025-11-04_weld_final/
- 状态测试格式说明:https://steel.ethereum.foundation/docs/execution-specs/running_tests/test_formats/state_test/
- 直接执行测试向量与各客户端入口:https://steel.ethereum.foundation/docs/execution-specs/running_tests/consume/direct/
- ethpandaops/hive-tests(多客户端每日一致性测试):https://github.com/ethpandaops/hive-tests
- 以太坊基金会安全公告,2016-11-24 共识缺陷:https://blog.ethereum.org/2016/11/25/security-alert-11242016-consensus-bug-geth-v1-4-19-v1-5-2
- go-ethereum v1.5.3 发行说明:https://github.com/ethereum/go-ethereum/releases/tag/v1.5.3
- ethereum/evmjit 仓库(已归档):https://github.com/ethereum/evmjit
- EIP-7692,EVM Object Format (EOFv1) Meta:https://eips.ethereum.org/EIPS/eip-7692
- EIP-7607 移除 EOF 的说明(EIPs PR #9703):https://github.com/ethereum/EIPs/pull/9703
- EIP-7773,Hardfork Meta - Glamsterdam(官方范围清单,不含 EOF):https://eips.ethereum.org/EIPS/eip-7773
- Optimism 文档,op-geth 结束支持公告:https://docs.optimism.io/notices/op-geth-deprecation
- 第三方跟踪站点 eipsinsight,Glamsterdam 升级范围与变更记录:https://eipsinsight.com/upgrade/glamsterdam