EIP3540: EOF - EVM对象格式v1

  • ethereum
  • 发布于 2025-03-19 14:32
  • 阅读 50

本文介绍了一种为以太坊虚拟机(EVM)设计的可扩展和版本化的容器格式(EOF),通过在部署时进行校验,提供了代码与数据的分离,有助于将来新功能的引入与旧功能的废弃。EOF格式的设计可以减少运行时的负担,提升合约的管理效率。并且,该格式包括了一些新特性,如静态跳转和多字节操作码的支持。

摘要

我们引入了一种可扩展和版本化的 EVM 容器格式,在部署时进行一次性验证。这里描述的版本带来了代码和数据分离的实际好处,允许将来轻松引入各种更改。这个变化依赖于 EIP-3541 中引入的保留字节。

总之,EOF 字节码具有以下布局:

magic, version, (section_kind, section_size_or_sizes)+, 0, <section contents>

动机

当前链上部署的 EVM 字节码没有预定义的结构。代码通常在客户端中进行验证,在运行时每次执行之前都会进行 JUMPDEST 分析。这不仅带来了开销,还给引入新特性或弃用现有特性带来了挑战。

在合约创建过程中验证代码允许在帐户中不增加额外版本字段的情况下进行代码版本控制。版本控制是引入或弃用功能的有用工具,尤其适用于较大的更改(例如控制流的重大更改或诸如帐户抽象等功能)。

此 EIP 中描述的格式引入了一个简单且可扩展的容器,对客户端和语言所需的更改进行了最小化,并引入了验证。

它提供的第一个具体特性是代码和数据的分离。此分离对于链上代码验证器(如层二扩容工具(如 Optimism)所使用的那些)尤其有利,因为它们可以区分代码和数据(这还包括部署代码和构造函数参数)。目前,它们 a) 需要在合约部署之前进行更改; b) 实施脆弱的方法;或 c) 实施昂贵且有限制的跳转分析。代码和数据的分离可以简化使用并为此类用例实现显著的 gas 节省。此外,各种(静态)分析工具也可以受益,尽管链下工具已经能够处理现有代码,因此影响较小。

一份非详尽的可以受益于此格式的建议更改清单:

  • 包括一个 JUMPDEST 表格(以避免在执行时进行分析)和/或完全移除 JUMPDEST
  • 引入静态跳转(带有相对地址)和跳转表,并同时禁止动态跳转。
  • 没有任何变通的多字节操作码。
  • 将函数表示为单独的代码部分,而不是子例程。
  • 为不同的用例引入特殊部分,尤其是帐户抽象。

规范

文档中的关键字 “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, 和 “OPTIONAL” 应按 RFC 2119 和 RFC 8174 中的描述进行解释。

为了确保状态中的每个 EOF 格式合约都是有效的,我们需要防止已经部署(且未验证)的合约被识别为这种格式。这是通过选择一个 magic 字节序列来实现的,该序列在任何已部署的合约中都不存在。

备注

如果代码以 MAGIC 开头,则被认为是 EOF 格式;否则被视为 legacy 代码。为清楚起见,MAGIC 及其版本号 n 被表示为 EOFn 前缀,例如 EOF1 前缀

EOF 格式的合约是使用在单独 EIP 中引入的新指令创建的。

操作码 0xEF 目前是未定义的指令,因此:它不弹出任何堆栈项目,也不推送任何堆栈项目,并且在执行时会导致异常中止。这意味着以此指令开头的上一个 initcode 或已部署的 legacy code 将继续导致执行中止。

除非另有说明,所有整数以大端字节顺序编码。

代码验证

我们引入 代码验证 用于新合约创建。为此,我们定义了一种称为 EVM 对象格式(EOF)的格式,包含一个版本指示符,以及与给定版本关联的有效性规则集。

legacy 代码不会受到 EOF 代码验证的影响。

代码验证在合约创建期间执行,并将在单独的 EIP 中详细说明。EOF 格式本身及其正式验证将在以下部分中描述。

容器规范

EOF 容器是一种二进制格式,能够提供 EOF 版本号和 EOF 部分列表。

容器以 EOF 前缀开始:

描述 长度
magic 2 字节 0xEF00
version 1 字节 0x01–0xFF EOF 版本号

EOF 前缀后至少有一个节头。每个节头包含两个字段,section_kindsection_sizesection_size_list,具体取决于种类。当允许多个此类部分时,section_size_list 是一个大小值列表,以项数计数后跟随项。

描述 长度
section_kind 1 字节 0x01–0xFF uint8
section_size 2 字节 0x0000–0xFFFF uint16
section_size_list 动态 n/a uint16, uint16+

节头的列表以 section headers terminator byte 0x00 结束。正文内容紧接其后。

容器验证规则

  1. version 必须不为 0
  2. section_kind 必须不为 0。值 0 保留用于 section headers terminator byte
  3. 必须至少有一个节(因此节头)。
  4. 节外的杂散字节必须不出现。这包括最后一个节之后的尾随字节。

EOF 版本 1

EOF 版本 1 由多个 EIP 组成,包括本 EIP。此规范中的某些值仅进行了简要讨论。要理解 EOF 的完整范围,需要深入审查每个 EIP。

容器

EOF 版本 1 容器由 headerbody 组成。

container := header, body
header := 
    magic, version, 
    kind_type, type_size, 
    kind_code, num_code_sections, code_size+,
    [kind_container, num_container_sections, container_size+,]
    kind_data, data_size,
    terminator
body := types_section, code_section+, container_section*, data_section
types_section := (inputs, outputs, max_stack_height)+

注: , 是一个连接运算符,+ 应理解为 “一个或多个” 前一项,`应理解为 “零个或多个” 前一项,[item]` 应理解为一个可选项。*

头部

名称 长度 描述
magic 2 字节 0xEF00
version 1 字节 0x01 EOF 版本
kind_type 1 字节 0x01 类型部分的种类标记
type_size 2 字节 0x0004-0x1000 表示类型部分内容长度的 16 位无符号大端整型,每个代码部分 4 字节
kind_code 1 字节 0x02 代码大小部分的种类标记
num_code_sections 2 字节 0x0001-0x0400 表示代码部分数量的 16 位无符号大端整型
code_size 2 字节 0x0001-0xFFFF 表示代码部分内容长度的 16 位无符号大端整型
kind_container 1 字节 0x03 容器大小部分的种类标记
num_container_sections 2 字节 0x0001-0x0100 表示容器部分数量的 16 位无符号大端整型
container_size 2 字节 0x0001-0xFFFF 表示容器部分内容长度的 16 位无符号大端整型
kind_data 1 字节 0x04 数据大小部分的种类标记
data_size 2 字节 0x0000-0xFFFF 表示数据部分内容长度的 16 位无符号大端整型 (*)
terminator 1 字节 0x00 标记头部的结束

(*) 对于尚未部署的容器,这可以大于实际内容长度。

正文

名称 长度 描述
types_section 可变 n/a 存储代码节元数据
inputs 1 字节 0x00-0x7F 代码节消耗的堆栈元素数量
outputs 1 字节 0x00-0x7F 代码节返回的堆栈元素数量
max_stack_height 2 字节 0x0000-0x03FF 代码节在操作数栈上放置的最大元素数量
code_section 可变 n/a 任意字节码
container_section 可变 n/a 任意的 EOF 格式容器
data_section 可变 n/a 任意字节序列

outputs 的特殊值 0x80 被指定为表示非返回函数,如单独 EIP 中所定义。

EOF 版本 1 验证规则

对容器格式施加以下有效性约束:

  • types_size 必须能被 4 整除。
  • 代码节的数量必须等于 types_size / 4
  • 数据主体长度对于尚未部署的容器可能短于 data_size
  • 容器的总大小不得超过 MAX_INITCODE_SIZE(如 EIP-3860 中定义)。

执行语义的变化

对于一个 EOF 合约:

  • 执行从代码节 0 的第一个字节开始。
  • CODESIZE, CODECOPY, EXTCODESIZE, EXTCODECOPY, EXTCODEHASH, GAS 被 EOF 合约的验证拒绝,没有替代品。
  • CALL, DELEGATECALL, STATICCALL 被 EOF 合约的验证拒绝,替代指令将单独在 EIP 中引入。
  • 从 EOF 合约调用非 EOF 合约(legacy 合约、EOA、空帐户)的 DELEGATECALL(或 EOF 的任何替代指令)被禁止,并且应以调用深度检查失败的相同方式失败。我们允许生态 Legacy 到 EOF 路径,以便现有代理合约能够使用 EOF 升级。

对于 legacy 合约:

  • 如果 EXTCODECOPY 的目标帐户是 EOF 合约,则它将从 EF00 复制最多 2 字节,作为那将是代码的样子。
  • 如果 EXTCODEHASH 的目标帐户是 EOF 合约,则它将返回 0x9dbf3648db8210552e9c4f75c6a1c3057c0ca432043bd648be15fe7be05646f5EF00 的哈希值,作为那将是代码的样子)。
  • 如果 EXTCODESIZE 的目标帐户是 EOF 合约,则将返回 2。

:与 legacy 目标一样,上述 EXTCODECOPY, EXTCODEHASHEXTCODESIZE 的行为不适用于正在创建中的 EOF 合约,即那些报告的结果与无代码的帐户相同。

理由

EVM 和/或帐户版本控制在过去几年中被多次讨论。此提案旨在从中学习。 请参阅“Ethereum account versioning”在 Ethereum Magicians Fellowship 论坛上,作为良好的起点。

执行时间与创建时间验证

该规范引入创建时间验证,这意味着:

  • 所有以 EOFn 前缀创建的合约根据版本 n 规则是有效的。这是一个非常强大和有用的属性。客户端可以信任已部署的代码是合理的。
  • 将来,这允许在 EOF 容器中序列化 JUMPDEST 映射,消除执行前所需隐式 JUMPDEST 分析的需求。
  • 或完全移除 JUMPDEST 指令的需求。
  • 这有助于弃用 EVM 指令和/或功能。
  • 最大的不利之处是,EOF 代码的部署时间验证必须在两个硬分叉中启用。然而,第一步 (EIP-3541) 已在伦敦部署。

替代方案是对 EOF 进行执行时间验证。这在每次执行合约时都会进行,然而客户端可能能够缓存验证结果。这种 替代 方法具有以下特性:

  • 由于验证是共识级执行步骤,这意味着执行始终需要整个代码。这使 代码Merkle化 不切实际。
  • 可以通过一个硬分叉启用。
  • 更好的向后兼容性:以 0xEF 字节或 EOF 前缀 开头的数据合约可以部署。然而,这是一项可疑的好处。

MAGIC

  1. 第一个字节 0xEFEIP-3541 保留用于此目的。

  2. 第二个字节 0x00 被选择以避免与在 Mainnet 上部署的三个合约发生冲突:

    • 0xca7bf67ab492b49806e24b6e2e4ec105183caa01: EFF09f918bf09f9fa9
    • 0x897da0f23ccc5e939ec7a53032c5e80fd1a947ec: EF
    • 0x6e51d4d9be52b623a3d3a2fa8d3c5e3e01175cd0: EF
  3. 在其伦敦分叉区块时,公共测试网络(Goerli,Ropsten,Rinkeby,Kovan 和 Sepolia)中不存在以 0xEF 字节开头的合约。

: 这个 EIP 必须不在包含以 MAGIC 开头的字节码且不有效 EOF 的链上启用。

EOF 版本范围以 1 开头

版本号 0 不会在 EOF 中使用,因此我们可以将 legacy 代码称为 EOF0。另外,实现可能使用 API,其中 0 版本号表示 legacy 代码。

部分结构

我们考虑了各部分的问题:

  • 在一些其它格式(如 WebAssembly)中使用了流式头部(即 section_header, section_data, section_header, section_data, ...)。它们对于受到编辑(添加/删除部分)的格式很方便。但对于 EVM 来说这不是一个有用的特性。一个适用于我们情况的微小好处是,它们不需要特定的 “头终止符”。另一方面,它们似乎不太适合代码分块/Merkle化,因为最好将所有节头放在同一个块中。
  • 是否应具有头终止符或编码 number_of_sectionstotal_size_of_headers。这两个都会引发一个问题,即这些字段能容纳多大的值。使用终止字节似乎避免了选择过小大小的问题,而没有明显的缺点,因此这是一条已采取的路径。
  • (EOF1) 是否将部分大小编码为固定的 16 位值或某种可变长度字段(例如 LEB128)。我们选择了固定大小,因为它简化了客户端实现,并且 16 位似乎足够,因为当前暴露的代码大小限制为 24576 字节(详见 EIP-170EIP-3860)。如果将来这一点受到限制,则可以通过新的一种 EOF 版本更改格式。除了简化客户端实现,不使用 LEB128 也大大简化了链上解析。
  • 是否应为所有未来遵循的 EOF 版本提供更多的容器头部结构。为了适应未来优化块和Merkle化(verkleization)的格式,决定保持其通用性,并仅为特定的 EOF 版本指定其结构。

数据-only 合约

参见 EIP-7480 中的 Lack of EXTDATACOPY 部分。

EOF1 合约只可 DELEGATECALL EOF1 合约

当前合约可以以三种不同的方式自毁(通过 SELFDESTRUCT 直接,自通过 CALLCODE 和通过 DELEGATECALL 间接)。 EIP-3670 禁用前两种可能性,然而第三种可能性仍然存在。允许 EOF1 合约只 DELEGATECALL 其他 EOF1 合约,允许以下强有力的声明:EOF1 合约永远无法被销毁。基于 SELFDESTRUCT 的攻击对 EOF1 合约完全消失。这包括被销毁的库合约(例如 Parity Multisig)。

EOF1 容器有大小限制

对 EOF 容器实施 EOF 验证时间限制提供了一个参考限制,即 EVM 实现应该能够在验证和处理容器时处理容器的大小。MAX_INITCODE_SIZE 被选定为 EOF1,因为它是合约创建当前允许的大小。

鉴于这一限制的主要原因之一是为了避免对 JUMPDEST 分析的攻击向量,而 EOF 去掉了 JUMPDEST 分析的需求,并引入了用于部署时分析的成本结构,在未来此限制可以增加甚至取消。

向后兼容性

这是一个破坏性更改,因为任何以 0xEF 开头的代码在此之前无法部署(并且如果执行将导致异常中止),但现在这种代码的某些子集可以成功部署和执行。

选择 MAGIC 确保链上现有的合约不受新规则的影响。

安全考虑

预计将来的 EOF 扩展,验证将具有线性的计算和空间复杂性。 我们认为验证成本得到了足够的覆盖。

  • EIP-3860 针对 initcode
  • 每字节部署 code 的高成本。

版权

版权和相关权利通过 CC0 放弃。

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

0 条评论

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