以太坊引擎内幕:执行层如何实际运作

本文深入探讨了以太坊执行层规范(EELS),重点介绍了其目的、核心模块以及在最近的硬分叉中引入协议的一些不太为人所知的概念。EELS是以太坊执行客户端核心组件的Python参考实现,重点在于可读性和清晰性,通常用于原型设计新的EIP。

简介

在本文中,我们将深入探讨 以太坊执行层规范 (EELS),重点介绍其目的、它定义的核心模块,以及最近硬分叉引入协议的一些不太为人所知的概念。

EELS以太坊执行客户端核心组件的 Python 参考实现,非常注重可读性和清晰度。它是 黄皮书 的一个对程序员更友好且最新的替代品,并且经常用于 新 EIP 的原型设计。EELS 还提供了每个分叉的协议的完整 快照,以及连续快照之间渲染的 差异

EELS 不实现 JSON-RPC API 或点对点网络。但是,可以使用外部 RPC 提供程序来获取和验证 EELS 的区块,并且验证后可以将生成的状态存储在本地数据库中。

架构

在每个快照中,该实现可以粗略地分为两个部分:

  • EVM 模块:这些模块实现了 EVM 的所有核心功能,包括 gas 计算、堆栈和内存管理、操作码预编译合约 实现,以及一个将所有内容联系在一起以处理 EVM 消息的解释器。
  • 区块链执行模块:这些模块定义了核心协议组件,例如区块、交易和状态 trie 树,以及在将区块和交易添加到链之前对其进行处理和验证的逻辑。

查看每个快照中的文件结构,我们会发现如下内容:

src/ethereum/prague/
├── blocks.py
├── bloom.py
├── exceptions.py
├── fork_types.py
├── fork.py
├── requests.py
├── state.py
├── transactions.py
├── trie.py
├── utils/
└── vm
    ├── eoa_delegation.py
    ├── exceptions.py
    ├── gas.py
    ├── instructions/
    ├── interpreter.py
    ├── memory.py
    ├── precompiled_contracts/
    ├── runtime.py
    └── stack.py

根据所检查的分叉,可能会根据该特定版本中引入的 EIP 添加或删除一些文件。上面的结构对应于 Prague 分叉,这是撰写本文时在主网上线的版本。

主要功能

现在我们对 EELS 是什么以及它的结构有了基本的了解,让我们使用自下而上的方法深入研究执行层的核心功能——从 EVM 消息开始,一直到区块级别的行为。 ELSpec

EVM 消息

EVM 消息由位于 vm/interpreter 模块中的 process_message_call 函数处理。这是 EVM 的主要入口点,在交易执行期间调用,我们将在以下部分中看到。

def process_message_call(message: Message) -> MessageCallOutput

如其签名所示,此函数接受包含多个字段的 message。值得注意的是,它包括一个 caller(在 Solidity 中也称为 msg.sender)、一个 target 和一个 current_target(在合约创建交易中可能不同)、要执行的 codegas 限制和要转移的 value(以 wei 为单位)。

函数执行分支取决于 target 为空还是指向地址。如果为空,则该消息被视为合约创建(部署地址为 current_target),并由 process_create_message 处理。否则,在处理 EIP-7702 逻辑以加载 EOA 代码委托后,该消息将路由到 process_message

  • 在合约创建路径中,新合约的 nonce 设置为 1,其运行时代码通过 执行(通过 process_message)预加载到 message.code 中的 init code 来计算(我们将在“用户交易”部分中确切地了解这是如何发生的)。
  • process_message 中,消息的 value 从调用者转移到目标,然后 message.code循环 中执行。操作码一次执行一个,它们的实现(例如,ADD)负责更新 EVM 状态(程序计数器、堆栈、返回数据等)。

最后,返回一个 MessageCallOutput,其中包含消息处理的结果。这包括剩余 gas、gas 退款、事件日志和返回数据等信息。

系统交易

系统交易是随着 Cancun 分叉 (2024) 中的 EIP-4788 相对较晚引入的。与由外部各方提交给节点的常规 [用户] 交易不同,系统交易是直接在节点的执行客户端中创建和执行的。它们还表现出几个 特性

  • 交易 origin 和 EVM 消息 caller(在 Solidity 中称为 tx.originmsg.sender)都设置为 SYSTEM_ADDRESS = 0xfff..ffe
  • 目标是系统合约:一个常规的有状态合约(不同于无状态的预编译合约),其中系统地址具有特殊的写入权限。与任何合约一样,存储在系统合约中的数据是公开可访问的。如果目标地址不存在代码,则消息处理必须静默失败。
  • 对系统合约的调用必须执行完成,不计入区块的 gas 限制,并且不遵循 EIP-1559 燃烧语义 - 即,不应将任何 value 作为调用的一部分转移。

目前,使用系统合约的 EIP 包括 EIP-4788EIP-2935EIP-7002EIP-7251。这些合约的实现可以在 ethereum/sys-asm 存储库中找到。它们通常使用 Nick 的方法 部署,该方法 (i) 使用“无钥匙单次使用不可控地址”来部署系统合约,并且 (ii) 促进可预测的多链部署。

系统交易由 process_system_transaction 函数执行,该函数有两种形式:checked(如果目标地址不包含代码或交易失败(导致区块无效)则引发错误)和 unchecked你猜对了 - 不执行任何检查):

  • checked 变体用于调用 WithdrawalRequest ( EIP-7002) 和 ConsolidationRequest ( EIP-7251) 预部署合约。这些是系统合约,在包含它们的分叉激活时 必须 部署。
  • unchecked 变体用于调用 BeaconRoots ( EIP-4788) 和 HistoryStorage ( EIP-2935) 系统合约。
def process_system_transaction(
    block_env: vm.BlockEnvironment,
    target_address: Address,
    system_contract_code: Bytes,
    data: Bytes,
) -> MessageCallOutput

此函数是 process_message_call 的一个精简包装器,我们在上一节中讨论过。它首先构造一个 TransactionEnvironment,将 origin 设置为 SYSTEM_ADDRESS,gas 限制设置为 30M,并将大多数其他字段留空。然后,它创建一个以 SYSTEM_ADDRESS 作为调用者的 EVM 消息,并包括作为参数接收到 process_system_transaction 的目标地址、合约代码和数据。最后,它将执行委派给 process_message_call 并返回结果。

用户交易

用户交易——或仅交易——是那些由外部实体签名并发送到节点以包含在区块链上的交易。有多种 交易类型,其中 FeeMarketTransaction 是用户执行链上操作(例如,token 转移)最常见的交易类型。

这些交易由 process_transaction 函数执行:

def process_transaction(
    block_env: vm.BlockEnvironment,
    block_output: vm.BlockOutput,
    tx: Transaction,
    index: Uint, # Index of the tx in the block
) -> None

让我们逐步了解其逻辑——有很多:

  1. 交易(或 tx)包含在 trie 树 中,其根成为区块头的一部分。
  2. 对 tx 字段执行初始静态验证 performed,确保:

    • gas 限制涵盖 tx 的 intrinsic cost
    • nonce 低于 2**64-1 (EIP-2681),
    • 如果是合约创建 tx,则 init code 低于 48 KB 限制(最大运行时代码大小的两倍)。
  3. 发送者的地址从 tx 签名恢复(没有 from 字段!)。然后,根据当前链状态 验证 tx:

    • gas 限制必须符合区块的剩余 gas(考虑到区块的 gas 限制),
    • gas 费参数必须满足 EIP-1559
    • nonce 必须与发送者的 nonce 匹配,
    • 发送者的余额必须涵盖最大 gas 费和任何正在转移的 value (ETH)。
    • 其他 检查 根据 tx 类型应用。
  4. 发送者的 nonce 递增,并且从其余额中减去最大 gas 费。(他们稍后将获得未使用的 gas 退款。)
  5. 如果适用,则处理 EIP-2930 访问列表。(此列表包含 tx 计划访问的地址和存储密钥。)
  6. 使用来自 tx 和区块的信息构造一个 EVM 消息。诸如发送者(又名 origin)、目标、预编译合约和访问列表条目之类的地址被标记为已访问(或预热),从而使与之交互的成本更低。tx 的 data 根据 tx 是否为合约创建 解释

    • 如果是,则 data 成为新合约的 init code,并且部署地址从发送者的地址和 nonce 派生。
    • 否则,data 成为 EVM 消息的数据(又名 Solidity 中的 calldata),并且可执行代码从 tx 的 to 地址加载。
  7. 通过前面讨论的 process_message_call 函数 执行 EVM 消息。
  8. Gas退款适用于任何未使用的 gas,并且任何 优先级费用 (小费) 通过直接将它们添加到提议者的余额来支付给区块提议者 (又名 coinbase)。
  9. 标记为要删除的帐户(即,在此 tx 中创建然后 selfdestruct ed 的帐户)是 destroyed,这意味着它们从 状态trie树 中删除。
  10. 创建一个 tx 收据 并将其添加到 收据 trie 树 中,其中包含使用的 gas、发出的日志和成功/失败标志。日志也添加到整个区块日志中。

值得庆幸的是,此函数中完成的所有工作使后续函数变得更加简单。

区块执行

区块由 apply_body 函数执行,该函数采用一个包含诸如链 id 和状态等信息的环境,以及包含在区块中的交易列表和要处理的任何 验证器提款

def apply_body(
    block_env: vm.BlockEnvironment,
    transactions: Tuple[LegacyTransaction | Bytes, ...],
    withdrawals: Tuple[Withdrawal, ...],
) -> vm.BlockOutput

该实现非常简单,因为它主要建立在前面部分讨论的操作之上。它首先处理两个 unchecked system transactions:一个到 BeaconRoots 合约 ( EIP-4788),另一个到 HistoryStorage 合约 ( EIP-2935)。接下来,它解码并处理要包含在区块中的用户交易列表——按顺序且一次一个。第三步是处理验证器提款,其中提款地址将其递增的余额直接 written 到链状态,绕过常规 ETH 转移。最后,处理通用请求(我们将在接下来介绍),并返回 区块执行输出。此输出聚合来自所有执行步骤的结果,包括诸如使用的 gas、事件日志和交易trie树之类的信息。

通用请求

请求 的概念是在 EIP-7685 中引入的,该请求包含在 Pectra 升级中。请求可以 定义 为“要求在共识层上识别执行层上的操作的行为”。

例如,EIP-6110 定义了一种请求类型,该请求涉及将 ETH 存入 deposit contract(在执行层上)以在共识层上创建一个验证器。另外两个使用请求并且目前在主网上处于活动状态的 EIP 是:

  • EIP-7002:允许验证器通过 WithdrawalRequest 系统合约触发从其 EL 提款凭据中提款和退出。
  • EIP-7251:允许验证器具有更大的有效余额,同时保持 32 ETH 的下限(通过 ConsolidationRequest 系统合约)。

值得注意的是,请求的有效性通常无法在执行层中得到完全验证。这正是它们被称为“请求”的原因:它们本身没有单方面触发操作的权限。相反,期望合约在执行层中执行尽可能多的验证,然后再将数据传递给共识层以进行最终验证。

状态转换

现在进入执行层的主要功能state_transition 函数负责执行新区块(可能已从网络中的其他节点接收),验证其有效性并将其附加到链。

def state_transition(chain: BlockChain, block: Block) -> None

首先,验证区块头以确保其 内容 在其自身和与父区块头的关系中都具有逻辑意义(例如,新区块的时间戳应大于父区块的时间戳)。接下来,使用来自链(id、状态)和区块头和区块体的相关信息,通过上面讨论的 apply_body 函数执行区块。此调用修改链状态,并且生成的输出根据区块头进行验证区块提议者 在标头中记录的实际结果和value之间的任何差异都将导致 InvalidBlock 异常。但是,如果执行和验证都成功,则会将新区块附加到链的本地副本,并删除不再需要处理新区块的任何旧区块。虽然该协议仅要求最后 255 个区块继续处理新区块,但实际客户端通常存储更多区块以处理潜在的链 reorgs

结论

唷!这有很多内容要介绍,但是你现在应该对以太坊执行层中的组件和功能有充分的了解。如果你想更深入地研究任何特定部分,我强烈建议你自己探索 Python 规范 - 我在整篇文章中包含的链接应该有助于指导你的阅读。否则,execution-apisconsensus-specs 存储库可能是继续学习的好地方,因为它们各自涵盖了以太坊客户端的不同但互补的方面。

  • 原文链接: openzeppelin.com/news/ho...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。