跳到正文

以太坊虚拟机(EVM)

一份具备分叉意识的 EVM 指南,涵盖交易、消息调用帧、字节码、栈、内存、存储、Gas、REVERT、DELEGATECALL、预编译合约、收据和状态核对。

更新于

仅供教育参考,不构成投资或安全建议。EVM 行为取决于具体的链、分叉、状态、字节码、调用上下文、Gas 计费规则、客户端和协议升级。

直接答案

以太坊虚拟机是执行层的状态转换机,按照特定分叉的规则解释字节码。给定相同的有效前状态、顶层交易或消息、区块环境和分叉规范,符合规范的执行客户端必须算出相同的后状态或失败结果。EVM 不决定交易顺序,不提供共识最终性,不认证前端,也不会让每条 EVM 兼容链都具备与以太坊相同的安全性。

由外部签名的交易是顶层协议对象。合约之间的活动由嵌套的消息调用和调用帧组成,并非各自拥有交易 nonce、收据或哈希的独立交易。每个帧都有代码、程序计数器、256 位栈、内存、调用数据、返回数据、Gas 和执行上下文;持久存储属于账户,而瞬态存储在相应分叉规则下只存在于一笔交易期间。

工作原理

  1. 锁定执行快照:链及 chainId、网络、区块号和区块哈希、规范或最终化状态、分叉、执行客户端及规范修订版、前状态或状态根、交易类型、原始签名字节、交易哈希和收据。确定性以这一准确上下文为条件。
  2. 解码交易信封,并把执行前有效性与执行分开。验证签名和发送者、nonce、目的地址或合约创建、转账价值、调用数据、Gas 上限、费用字段、访问列表及各交易类型特有的字段。在纳入区块之前被拒绝的交易,并不是已经纳入并执行后发生状态回退的交易。
  3. 构建顶层消息和完整调用树。记录 CALLSTATICCALLDELEGATECALL、合约创建及预编译合约帧;调用者、上下文地址、代码地址、msg.sendermsg.value、价值转移、调用数据、返回数据、转发的 Gas 和成功标志。区块浏览器所谓的内部交易是跟踪视图,并非签名交易。
  4. 跟踪每个帧的程序计数器、栈、内存、调用数据、返回数据、日志、持久存储和瞬态存储日志。CALL 使用被调用方的地址和存储上下文;DELEGATECALL 在调用方的地址与存储上下文中执行目标代码,同时保留上游发送者和转账价值;STATICCALL 禁止修改状态。
  5. 应用目标分叉的 Gas 规则:固有 Gas、操作码动态成本、内存扩展、冷访问与热访问、调用转发、津贴、预编译收费、退款及退款上限。然后依据已用 Gas 和有效 Gas 价格,单独计算交易费用,不与转账价值混在一起。Gas 估算有条件限制,不是保证。
  6. 按作用域处理帧结果。如果祖先帧最终提交,RETURN 会提交当前帧。REVERT 会回退当前帧及其后代帧,返回数据,而且不一定耗尽当前帧的剩余 Gas;异常停机的规则不同。父帧可以捕获失败的低级调用并继续执行,因此即使子调用失败,顶层收据状态也可能是 1。顶层交易纳入后即使失败,仍会消耗发送者 nonce 和实际支付的 Gas。
  7. 将收据状态、已用 Gas、日志、创建地址和返回数据,与前后状态中的余额、nonce、代码、持久与瞬态存储、代币账本、执行跟踪和区块状态根进行核对。必要时使用独立客户端重新执行;代理实现、存储布局、预编译合约、编译器 EVM 目标和分叉升级,应与共识最终性分开审计。

算例

  • 已纳入区块的顶层回退及费用账本。 一笔类型 2 交易的 Gas 上限为 80,000,已用 Gas 为 52,000,基础费用为 20 gwei,最高优先费为 3 gwei,最高费用为 40 gwei。有效 Gas 价格是 min(40, 20 + 3) = 23 gwei;实际费用为 52,000 * 23 gwei = 0.001196 ETH,其中销毁 52,000 * 20 gwei = 0.001040 ETH,优先费为 52,000 * 3 gwei = 0.000156 ETH。未用的 28,000 gas 不收费。如果顶层执行回退,其存储、价值转移和日志影响都会撤销,但发送者 nonce 和实际费用仍会保留。
  • 被捕获的子调用失败。 合约 A 初始为 A.x = 5。它调用 B;B 写入 B.y = 9,发出日志后执行 REVERT。B 的写入和日志均被回退。A 观察到 success = false,写入 A.x = 7 并正常返回。最终收据状态为 1A.x = 7,B 则保持调用前的值。因此,顶层成功不能证明每个子调用都成功。
  • DELEGATECALL 的存储上下文。 代理的 slot0 = 5,实现账户的 slot0 = 99。实现代码读取槽位 0,加上 7 后写回。通过 DELEGATECALL 执行时,代理变为 slot0 = 12,实现账户仍为 slot0 = 99;所采用的是代理地址和存储上下文,上游发送者和转账价值保持不变。不兼容的存储布局可能损坏代理状态。
  • 受分叉约束的 SELFDESTRUCT。 在 EIP-6780 规则下,一个已有合约持有 2 ETH,并向受益人 B 执行 SELFDESTRUCT。B 收到 2 ETH,合约余额变为零,但这个既有账户、代码和存储不会删除。只有合约在同一笔交易中创建并自毁时,才仍适用删除行为。这是受分叉约束的规则,不能向过去或每条 EVM 链外推。

风险

  • 在错误的链、区块、分叉、客户端或前状态上重放。
  • 把非规范或已被重组替代的状态当作最终执行上下文。
  • 混淆执行前验证拒绝与纳入区块后的执行回退。
  • 假设待处理交易的模拟结果会与之后纳入时的状态一致。
  • 忽略顶层回退、异常停机或 Gas 耗尽。
  • 漏掉被父帧捕获的子调用失败。
  • 误读调用上下文、代码地址、调用方、发送者或转账价值。
  • DELEGATECALL 存储布局不匹配而损坏代理状态。
  • 允许重入攻击,或不安全地向外转移价值和控制权。
  • 信任未经验证的返回数据、成功标志或自定义错误。
  • 把日志或执行跟踪当作权威最终状态。
  • 错算冷访问、热访问、内存或调用转发的 Gas。
  • 错用退款、退款上限、津贴或 63/64 规则。
  • 使用错误的预编译地址、输入、Gas 规则或分叉语义。
  • 混淆持久存储、内存和瞬态存储的生命周期。
  • 预期在 STATICCALL 内能成功修改状态。
  • 沿用 EIP-6780 之前关于 SELFDESTRUCT 删除状态的假设。
  • 漏掉代理实现、管理员或编译器目标的漂移。
  • 运行规则分歧或未升级的执行客户端。
  • 从 EVM 兼容性推断共识、桥、代币、治理或最终性安全。

常见误区

  • Solidity 源代码就是链上直接执行的对象。
  • 已纳入区块的交易发生状态回退后不花费任何费用,nonce 也不变。
  • 收据状态为 1 就证明每个内部调用都如预期成功。
  • 事件或执行跟踪就是权威的资产与存储状态。
  • EVM 兼容性保证操作码、Gas、预编译合约、共识和安全性完全相同。

相关主题

来源

导航

搜索知识库...