reth

函数入口:reth/crates/evm/evm/src/execute.rs: L61

fn execute(
    mut self,
    block: &RecoveredBlock<<Self::Primitives as NodePrimitives>::Block>,
) -> Result<BlockExecutionOutput<<Self::Primitives as NodePrimitives>::Receipt>, Self::Error>
  • 根据 block header 和 db 创建 evm

    db 中可以获取到执行本 block 的初始状态,包括 account,storage 等。

    db 可以是 rpc db,当需要获取初始状态时,如果 db 中不存在,则可直接去链上查询父区块的状态并缓存下来。

    geth 的 db 存储本 block 前所有 block 的状态,导致很难重放一个区块。

  • 根据 evm 等创建 block executor
  • block 预处理

    根据经历的 hard fork 执行系统合约,更新 db

  • 执行 block 中的每笔 tx

    • 限制所有 tx 的 gas limit 总额不可超过 block 的 gas limit
    • evm 执行交易,输出消耗的 gas 和 state 变化情况

      • 检查 tx 执行环境和参数

        • 根据 tx.type 将 tx 转为几种类型:legacy tx,EIP-2930 access list tx,EIP-1558 dynamic fee tx,EIP-4844 blob tx,EIP-7702 EOA account code tx,自定义 tx

          根据不同类型的 tx 进行相应的检查,比如 chain id 是否正确,gas price 不低于 block basefee,小费上限(reth: max_priority_fee_per_gas,geth: GasTipCap)不超过 gas fee 上限(reth: max_fee_per_gas,geth: GasFeeCap)等

        • 检查交易执行前的固有 gas 是否超过 gas limit

          包括交易的基础固定成本(如以太坊中普通转账交易的 21,000 gas)、访问列表 gas 和输入数据 gas

        • 从 db 中加载 tx caller 账户到 state,包括 nonce、账户余额和 code。检查 tx nonce 是否和 tx caller nonce 相同。计算最大可能的 tx fee 并检查是否超过 tx caller 的账户余额。
      • tx 预处理:为 tx 的执行准备 evm state

        • 从 db 中加载 precompiles,access list 和矿工账户到 state
        • 对 tx caller 账户预扣 gas fee,增加 tx caller nonce,并将其账户状态标记为 touched,表示账户状态发生了变化,不能从 state 中删除
        • 处理 EIP-7702 授权列表,并返回已创建账户的 gas 退款金额

          • 授权列表处理:遍历授权列表,验证每个条目的签名和 nonce,若验证失败则跳过并返还部分 gas
          • gas 退款机制:类似 EIP-2929 的访问列表优化,无效授权或委托代码执行后的存储清理可能触发 gas 退款
          • 增加授权账户 nonce,并将其账户状态标记为 touched
      • 执行 tx,比如转账、合约调用等,计算实际消耗的 gas,如果失败则计算需要退还给 tx caller 的 gas.

        如果 tx to 的账户发生变化,也将其状态标记为 touched

      • tx 后处理

        • 将剩余的 gas fee 退给 tx caller,包含 tx 预处理中算出的 EIP-7702 退款
        • 奖励矿工,并将矿工账户状态标记为 touched
        • 返回实际使用了的 gas 和 state,即状态发生变化的账户,如 tx caller,tx to,矿工,系统合约账户
  • block 后处理

    • 根据经历的 hard fork 收集 EIP-6110 deposit 请求,执行 EIP-7002 withdrawal request 系统合约,更新 state
    • 收集区块的余额变化,可能包括区块奖励,叔块奖励、withdrawal 或非正常状态变化(DAO 分叉),更新到 state
  • 将 state 更新到 db

geth

reth 和 geth 逻辑相近,reth 使用 rust 实现的,geth 使用 go 实现的,reth 的逻辑分散在多个代码仓库中,而 geth 集中在一个代码仓库中。

reth 只需要获取到父区块的状态便可重放当前区块,而 geth 则需要获取之前所有区块的状态才能重放当前区块。

geth 处理一个区块的函数入口:go-ethereum/core/state_processor.go: L57

func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error)