Alert Source Discuss
⚠️ Review Standards Track: Core

EIP-3540: EOF - EVM 对象格式 v1

EOF 是一种用于 EVM 字节码的可扩展且带版本的容器格式,在部署时进行一次性验证。

Authors Alex Beregszaszi (@axic), Paweł Bylica (@chfast), Andrei Maiboroda (@gumb0), Matt Garnett (@lightclient), Piotr Dobaczewski (@pdobacz)
Created 2021-03-16
Requires EIP-3541, EIP-3860

摘要

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

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

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

动机

如今,链上部署的 EVM 字节码不包含任何预定义的结构。通常情况下,客户端会在运行时对代码进行 JUMPDEST 分析,每次执行之前都会进行验证。这不仅带来了开销,还为引入新功能或弃用现有功能带来了挑战。

在合约创建过程中验证代码允许代码版本控制,而无需在帐户中使用额外的版本字段。版本控制是引入或弃用功能的有用工具,特别是对于较大的更改(例如对控制流程的重大更改或帐户抽象等功能)。

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

它提供的第一个切实的功能是代码和数据的分离。这种分离对于链上代码验证器(例如第 2 层扩展工具(如 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 当前是一个未定义的指令,因此:它不弹出任何堆栈项,也不推送任何堆栈项,并且在执行时会导致异常中止。 这意味着以该指令开头的 legacy initcode 或已部署的 legacy 代码 将继续中止执行。

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

代码验证

我们为新合约创建引入 代码验证。为此,我们定义了一种称为 EVM 对象格式 (EOF) 的格式,其中包含版本指示器和与给定版本相关的有效性规则集。

Legacy 代码不受 EOF 代码验证的影响。

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

容器规范

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

容器以 EOF 前缀开头:

描述 长度  
magic 2 字节 0xEF00  
版本 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 MUST NOT 为 0
  2. section_kind MUST NOT 为 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_increase)+

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

标头

名称 长度 描述
magic 2 字节 0xEF00  
版本 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 4 字节 0x00000001-0xFFFFFFFF 32 位无符号大端整数,表示容器部分内容的长度
kind_data 1 字节 0xFF 数据大小部分的种类标记
data_size 2 字节 0x0000-0xFFFF 16 位无符号大端整数,表示数据部分内容的长度 (*)
terminator 1 字节 0x00 标记标头的结尾

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

正文

名称 长度 描述
types_section 变量 n/a 存储代码部分元数据
inputs 1 字节 0x00-0x7F 代码部分消耗的堆栈元素数
outputs 1 字节 0x00-0x7F 代码部分返回的堆栈元素数
max_stack_increase 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 的第一个字节开始
  • CODESIZECODECOPYEXTCODESIZEEXTCODECOPYEXTCODEHASHGAS 在 EOF 合约中被验证拒绝,没有替代方案
  • CALLDELEGATECALLSTATICCALL 在 EOF 合约中被验证拒绝,替代指令将在单独的 EIP 中引入。
  • 从 EOF 合约到非 EOF 合约(legacy 合约、EOA、空帐户)的 DELEGATECALL(或任何 EOF 的替代指令)是不允许的,并且它应该以与调用深度检查失败时相同的模式失败。我们允许 legacy 到 EOF 的路径,以便现有的代理合约能够使用 EOF 升级。

对于 legacy 合约:

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

注意 与 legacy 目标一样,上述 EXTCODECOPYEXTCODEHASHEXTCODESIZE 的行为不适用于创建中的 EOF 合约目标,即那些报告与没有代码的帐户相同。

理由

EVM 和/或帐户版本控制在过去几年中已被多次讨论。本提案旨在从中学习。 有关良好的起点,请参阅以太坊魔法师论坛上的“以太坊帐户版本控制”。

执行时验证与创建时验证

本规范引入了创建时验证,这意味着:

  • 所有带有 EOFn 前缀的已创建合约都符合版本 n 规则。这是一个非常强大且有用的属性。客户端可以信任已部署的代码格式良好。
  • 将来,这允许在 EOF 容器中序列化 JUMPDEST 映射,并消除执行之前所需的隐式 JUMPDEST 分析的需要。
  • 或者完全消除对 JUMPDEST 指令的需求。
  • 这有助于弃用 EVM 指令和/或功能。
  • 最大的缺点是必须在两个硬分叉中启用 EOF 代码的部署时验证。但是,第一步 (EIP-3541) 已经部署在伦敦。

另一种方法是对 EOF 进行执行时验证。每次执行合约时都会执行此操作,但客户端可能能够缓存验证结果。这种替代方法具有以下属性:

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

MAGIC

  1. 选择第一个字节 0xEF 是因为它已通过 EIP-3541 为此目的保留。

  2. 选择第二个字节 0x00 是为了避免与部署在 主网 上的三个合约发生冲突:
    • 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 代码。

节结构

我们已经考虑了节的不同问题:

  • 流式标头(即 section_header, section_data, section_header, section_data, ...)用于某些其他格式(例如 WebAssembly)。它们对于需要编辑(添加/删除节)的格式很方便。对于 EVM 来说,这不是一个有用的功能。适用于我们的案例的一个小好处是它们不需要特定的“标头终止符”。另一方面,它们似乎与代码分块/默克尔化的配合效果较差,因为最好将所有节头放在一个块中。
  • 是否具有标头终止符或编码 number_of_sectionstotal_size_of_headers。两者都引发了这些字段应该能够容纳多大值的问题。终止符字节似乎避免了选择太小尺寸的问题,而没有任何明显的不利因素,因此采用了这种方式。
  • (EOF1) 是否将节大小编码为固定的 16 位(容器节大小为 32 位)值或某种可变长度字段(例如 LEB128)。我们选择了固定大小。如果这在将来受到限制,则新的 EOF 版本可以更改格式。除了简化客户端实现之外,不使用 LEB128 也大大简化了链上解析。
  • 是否对所有 EOF 版本遵循的容器头有更多结构。为了允许将来针对分块和默克尔化(Verkle 化)优化的格式,决定保持其通用性,并仅为特定的 EOF 版本指定结构。

仅数据合约

请参阅部分 EIP-7480 中缺少 EXTDATACOPY

EOF1 合约只能 DELEGATECALL EOF1 合约

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

EOF1 容器具有大小限制

对 EOF 容器的大小施加 EOF 验证时间限制,可以参考 EVM 实现应能够在验证和处理容器时处理的容器大小的参考限制。为 EOF1 选择了 MAX_INITCODE_SIZE,因为它目前是合约创建所允许的。

鉴于该限制的主要原因之一是为了避免 JUMPDEST 分析的攻击媒介,并且 EOF 消除了对 JUMPDEST 分析的需求并为部署时分析引入了成本结构,因此将来可以增加甚至取消 EOF 的限制。

kind_data 可以是 0x04 而不是 0xff

将数据部分放在最后作为 0xff 的优势在于与它总是最后的事实保持一致。我们避免了一种情况,即新的节类型需要位于数据部分之前并破坏节类型排序。同时,数据部分放在最后是有利的,因为它是合约部署期间将数据附加到的部分。

向后兼容性

这是一个重大更改,因为任何以 0xEF 开头的代码以前都不可部署(并且如果执行会导致异常中止),但现在此类代码的某些子集可以成功部署和执行。

MAGIC 的选择保证了链上存在的任何合约都不会受到新规则的影响。

安全注意事项

预计通过预期的 EOF 扩展,验证将具有线性计算和空间复杂度。 我们认为验证成本已得到充分覆盖,原因如下:

  • EIP-3860 用于 initcode
  • 代码部署的极高单字节成本。

版权

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

Citation

Please cite this document as:

Alex Beregszaszi (@axic), Paweł Bylica (@chfast), Andrei Maiboroda (@gumb0), Matt Garnett (@lightclient), Piotr Dobaczewski (@pdobacz), "EIP-3540: EOF - EVM 对象格式 v1 [DRAFT]," Ethereum Improvement Proposals, no. 3540, March 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3540.