本文深入探讨了以太坊执行层规范(EELS),重点介绍了其目的、核心模块以及在最近的硬分叉中引入协议的一些不太为人所知的概念。EELS是以太坊执行客户端核心组件的Python参考实现,重点在于可读性和清晰性,通常用于原型设计新的EIP。
在本文中,我们将深入探讨 以太坊执行层规范 (EELS),重点介绍其目的、它定义的核心模块,以及最近硬分叉引入协议的一些不太为人所知的概念。
EELS 是 以太坊执行客户端核心组件的 Python 参考实现,非常注重可读性和清晰度。它是 黄皮书 的一个对程序员更友好且最新的替代品,并且经常用于 新 EIP 的原型设计。EELS 还提供了每个分叉的协议的完整 快照,以及连续快照之间渲染的 差异。
EELS 不实现 JSON-RPC API 或点对点网络。但是,可以使用外部 RPC 提供程序来获取和验证 EELS 的区块,并且验证后可以将生成的状态存储在本地数据库中。
在每个快照中,该实现可以粗略地分为两个部分:
查看每个快照中的文件结构,我们会发现如下内容:
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 消息开始,一直到区块级别的行为。
EVM 消息由位于 vm/interpreter 模块中的 process_message_call
函数处理。这是 EVM 的主要入口点,在交易执行期间调用,我们将在以下部分中看到。
def process_message_call(message: Message) -> MessageCallOutput
如其签名所示,此函数接受包含多个字段的 message
。值得注意的是,它包括一个 caller
(在 Solidity 中也称为 msg.sender
)、一个 target
和一个 current_target
(在合约创建交易中可能不同)、要执行的 code
、gas
限制和要转移的 value
(以 wei 为单位)。
函数执行分支取决于 target
为空还是指向地址。如果为空,则该消息被视为合约创建(部署地址为 current_target
),并由 process_create_message
处理。否则,在处理 EIP-7702 逻辑以加载 EOA 代码委托后,该消息将路由到 process_message
。
1
,其运行时代码通过 执行(通过 process_message
)预加载到 message.code
中的 init code 来计算(我们将在“用户交易”部分中确切地了解这是如何发生的)。process_message
中,消息的 value
从调用者转移到目标,然后 message.code
在 循环 中执行。操作码一次执行一个,它们的实现(例如,ADD
)负责更新 EVM 状态(程序计数器、堆栈、返回数据等)。最后,返回一个 MessageCallOutput
,其中包含消息处理的结果。这包括剩余 gas、gas 退款、事件日志和返回数据等信息。
系统交易是随着 Cancun 分叉 (2024) 中的 EIP-4788 相对较晚引入的。与由外部各方提交给节点的常规 [用户] 交易不同,系统交易是直接在节点的执行客户端中创建和执行的。它们还表现出几个 特性:
tx.origin
和 msg.sender
)都设置为 SYSTEM_ADDRESS = 0xfff..ffe
。目前,使用系统合约的 EIP 包括 EIP-4788、EIP-2935、EIP-7002 和 EIP-7251。这些合约的实现可以在 ethereum/sys-asm 存储库中找到。它们通常使用 Nick 的方法 部署,该方法 (i) 使用“无钥匙单次使用不可控地址”来部署系统合约,并且 (ii) 促进可预测的多链部署。
系统交易由 process_system_transaction
函数执行,该函数有两种形式:checked(如果目标地址不包含代码或交易失败(导致区块无效)则引发错误)和 unchecked(你猜对了 - 不执行任何检查):
WithdrawalRequest
( EIP-7002) 和 ConsolidationRequest
( EIP-7251) 预部署合约。这些是系统合约,在包含它们的分叉激活时 必须 部署。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
让我们逐步了解其逻辑——有很多:
对 tx 字段执行初始静态验证 performed,确保:
2**64-1
(EIP-2681),使用来自 tx 和区块的信息构造一个 EVM 消息。诸如发送者(又名 origin)、目标、预编译合约和访问列表条目之类的地址被标记为已访问(或预热),从而使与之交互的成本更低。tx 的 data
根据 tx 是否为合约创建 解释:
to
地址加载。process_message_call
函数 执行 EVM 消息。selfdestruct
ed 的帐户)是 destroyed,这意味着它们从 状态trie树 中删除。值得庆幸的是,此函数中完成的所有工作使后续函数变得更加简单。
区块由 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 是:
WithdrawalRequest
系统合约触发从其 EL 提款凭据中提款和退出。ConsolidationRequest
系统合约)。值得注意的是,请求的有效性通常无法在执行层中得到完全验证。这正是它们被称为“请求”的原因:它们本身没有单方面触发操作的权限。相反,期望合约在执行层中执行尽可能多的验证,然后再将数据传递给共识层以进行最终验证。
现在进入执行层的主要功能!state_transition
函数负责执行新区块(可能已从网络中的其他节点接收),验证其有效性并将其附加到链。
def state_transition(chain: BlockChain, block: Block) -> None
首先,验证区块头以确保其 内容 在其自身和与父区块头的关系中都具有逻辑意义(例如,新区块的时间戳应大于父区块的时间戳)。接下来,使用来自链(id、状态)和区块头和区块体的相关信息,通过上面讨论的 apply_body
函数执行区块。此调用修改链状态,并且生成的输出根据区块头进行验证。区块提议者 在标头中记录的实际结果和value之间的任何差异都将导致 InvalidBlock
异常。但是,如果执行和验证都成功,则会将新区块附加到链的本地副本,并删除不再需要处理新区块的任何旧区块。虽然该协议仅要求最后 255 个区块继续处理新区块,但实际客户端通常存储更多区块以处理潜在的链 reorgs 。
唷!这有很多内容要介绍,但是你现在应该对以太坊执行层中的组件和功能有充分的了解。如果你想更深入地研究任何特定部分,我强烈建议你自己探索 Python 规范 - 我在整篇文章中包含的链接应该有助于指导你的阅读。否则,execution-apis 和 consensus-specs 存储库可能是继续学习的好地方,因为它们各自涵盖了以太坊客户端的不同但互补的方面。
- 原文链接: openzeppelin.com/news/ho...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!