“Mega EOF 终局”规范(EOFv1)

  • ipsilon
  • 发布于 2025-03-12 18:57
  • 阅读 25

本文介绍了EVM对象格式(EOFv1)的统一规范,详细探讨了其结构、头部与主体的组成、代码执行语义以及新的指令与验证方法等。EOF的推出旨在改善以太坊虚拟机(EVM)的功能和灵活性,提供更好的代码执行环境和数据处理能力。

"Mega EOF 终局" 规范 (EOFv1)

序言

本统一规范应作为理解EVM对象格式所提议的各种变更的指南。有关EIP的列表见附录,EIP作为官方规范。

虽然EOF是可扩展的,但在本文中我们讨论第一版EOFv1。

容器

EVM字节码传统上是一个无结构的指令序列。EOF引入了容器的概念,为字节码带来了结构。该容器由一个头部和若干部分组成。

container := header, body
header := 
    magic, version, 
    kind_types, types_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 EOF 前缀
version 1 字节 0x01 EOF 版本
kind_types 1 字节 0x01 类型大小段的种类标记
types_size 2 字节 0x0004-0x1000 表示类型部分内容长度的 16 位无符号大端整数
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-0x80 代码段返回的栈元素数量或对于不返回函数为0x80
max_stack_height 2 字节 0x0000-0x03FF 代码段中栈中放置的元素的最大数量,包括输入
code_section 变量 n/a 任意字节序列
container_section 变量 n/a 任意字节序列
data_section 变量 n/a 任意字节序列

数据段生命周期

对于尚未部署的EOF容器data_section只是最终data_section在部署后的一个部分。 我们将其定义为pre_deploy_data_section,并将该容器头部中声明的data_size定义为pre_deploy_data_sizepre_deploy_data_size >= len(pre_deploy_data_section),这一预期在部署过程中会有更多数据被追加到pre_deploy_data_section

pre_deploy_data_section
|                                       |
 \___________pre_deploy_data_size______/

对于已部署的EOF容器,最终data_section变为:

pre_deploy_data_section | static_aux_data | dynamic_aux_data
|                         |             |                  |
|                          \___________aux_data___________/
|                                       |                  |
 \___________pre_deploy_data_size______/                   |
|                                                          |
 \________________________data_size_______________________/

其中:

  • aux_data是追加到pre_deploy_data_section中的数据,在RETURNCODE指令时见新行为
  • static_aux_dataaux_data的一个子范围,其大小在RETURNCODE之前已知,等于pre_deploy_data_size - len(pre_deploy_data_section)
  • dynamic_aux_dataaux_data的其余部分。

已部署容器头部中的data_size也被更新为等于len(data_section)

总结来说,最终数据段中有pre_deploy_data_size字节在EOF容器部署之前是保证存在的,len(dynamic_aux_data)字节在EOF容器部署之后才被确认存在。 这影响到对数据段访问指令的验证和行为:DATALOADDATALOADNDATACOPY,详见代码验证

容器验证

在上表中定义的类型上,还对容器格式施加了以下有效性约束:

  • 最小有效头部大小为 15 字节
  • types_size 必须是 4 的倍数
  • 代码段的数量必须等于 types_size / 4
  • 已部署容器的总大小(不包括容器段)必须为 13 + 2*num_code_sections + types_size + code_size[0] + ... + code_size[num_code_sections-1] + data_size
  • 至少包含一个容器段的已部署容器的总大小必须为 16 + 2*num_code_sections + types_size + code_size[0] + ... + code_size[num_code_sections-1] + data_size + 2*num_container_sections + container_size[0] + ... + container_size[num_container_sections-1]
  • 尚未部署的容器的总大小可能会比上述值低至 data_size,这与数据段在部署期间的重写和调整大小有关(详见数据段生命周期
  • 容器的总大小不得超过 MAX_INITCODE_SIZE(如在EIP-3860中定义)

执行语义

在EOF环境中执行的代码其行为与传统代码有所不同。我们可以将这些不同分解为 i) 对现有行为的变化和 ii) 新行为的引入。

修改的行为

  • 执行从代码段 0 的第一个字节开始,pc 被设置为 0。
  • pc 的作用域限于正在执行的代码段
  • CALLCALLCODEDELEGATECALLSTATICCALLSELFDESTRUCTJUMPJUMPIPCCREATECREATE2CODESIZECODECOPYEXTCODESIZEEXTCODECOPYEXTCODEHASHGAS 指令在EOF合约中被弃用,并在验证时被拒绝。它们仅在传统合约中可用。
  • 如果从传统合约执行,若EXTCODECOPY的目标账户为EOF合约,则它将从EF00复制最多2个字节,仿佛那是代码。
  • 如果从传统合约执行,若EXTCODEHASH的目标账户为EOF合约,则它返回 0x9dbf3648db8210552e9c4f75c6a1c3057c0ca432043bd648be15fe7be05646f5EF00 的哈希,仿佛那是代码)。
  • 如果从传统合约执行,若EXTCODESIZE的目标账户为EOF合约,则它返回 2。
  • 指令 JUMPDEST 被重命名为 NOP,并保持不变地消耗 1 单位 gas。
    • 注:不再执行 jumpdest 分析。
  • EOF 合约不得部署传统代码(在代码验证阶段自然被拒绝)
  • 如果从传统合约执行,若CREATECREATE2指令有EOF代码作为 initcode(以 EF00 魔法开头)
    • 部署失败(在栈上返回 0)
    • 调用者的nonce未更新,且initcode执行所需gas未消耗
  • RETURNDATACOPY (0x3E) 指令
    • 行为与传统相同,但将特殊中止行为变更为零填充行为(与 CALLDATACOPY 相同)。

注意 类似于传统目标,EXTCODECOPYEXTCODEHASHEXTCODESIZE 的上述行为不适用于创建中的EOF合约目标,即这些报告与无代码账户的结果相同。

创建交易

创建交易(to 為空的交易),data 包含EOF代码(以 EF00 魔法开头),被解释为在 data 中具有EOF initcontainercalldata 的连接:

  1. 应用 EIP-3860 中定义的传统创建交易的固有gas费用规则和限制。交易的整个 data 用于这些计算。
  2. 找到将 data 分割为 initcontainercalldata 的方式:
    • 解析 EOF 头部
    • 通过从头部读取所有段大小并加上头部大小计算 initcontainer 大小。
  3. 递归验证 initcontainer 和它的所有子容器。
    • 与一般验证不同,initcontainer 还必须在头部声明的 data_size 等于实际 data_section 大小。
    • 验证包括检查 initcontainer 不包含 RETURNSTOP
  4. 如果 EOF 头部解析或完整容器验证失败,交易在被视为有效和失败。初始化代码执行不会消耗gas,仅收取固有创建交易费用。
  5. 交易 data 中紧接着 initcontainercalldata 被视为传递到执行框架中的calldata。
  6. 执行容器并扣除执行耗费的gas。
    1. 计算 new_addresskeccak256(sender || sender_nonce)[12:]
    2. 成功的执行以初始化代码执行 RETURNCODE{deploy_container_index}(aux_data_offset, aux_data_size) 指令结束(见下文)。之后:
      • 从容器中 deploy_container_index 的 EOF 子容器加载部署合约,执行 RETURNCODE 时使用
      • 将数据段与 (aux_data_offset, aux_data_offset + aux_data_size) 内存区段连接,并更新头部中的数据大小
      • deployed_code_size 更新为部署容器大小
      • deployed_code_size > MAX_CODE_SIZE 指令异常中止
      • state[new_address].code 设置为更新后的部署容器
  7. 扣除 200 * deployed_code_size gas

注意 传统合约和传统创建交易不得部署 EOF 代码,这一行为来自EIP-3541并未发生修改。

新行为

在 EOF 代码中引入了以下指令:

  • RJUMP (0xe0) 指令
    • 扣除 2 单位 gas
    • 读取 int16 操作数 offset,设置 pc = offset + pc + 3
  • RJUMPI (0xe1) 指令
    • 扣除 4 单位 gas
    • 从栈中弹出一个值,condition
    • 设置 pc += 3
    • 如果 condition != 0,读取 int16 操作数 offset 并设置 pc += offset
  • RJUMPV (0xe2) 指令
    • 扣除 4 单位 gas
    • 读取 uint8 操作数 max_index
    • 从栈中弹出一个值,case
    • 设置 pc += 2
    • 如果 case > max_index(越界情况),通过设置 pc += (max_index + 1) * 2 后退
    • 否则将 pc + case * 2 的2字节操作数解释为 int16,定义为 offset,并设置 pc += (max_index + 1) * 2 + offset
  • 引入新的 VM 上下文变量
    • current_code_idx 用于存储当前正在执行的代码段索引
    • 新的 return_stack 用于存储成对 (code_section, pc)
      • 在实例化 VM 上下文时,将初始值 (0,0) 推入 return stack
  • CALLF (0xe3) 指令
    • 扣除 5 单位 gas
    • 读取 uint16 操作数 idx
    • 如果 1024 < len(stack) + types[idx].max_stack_height - types[idx].inputs,执行结果异常中止
    • 如果 1024 <= len(return_stack),执行结果异常中止
    • return_stack 中推入新的元素 (current_code_idx, pc+3)
    • current_code_idx 更新为 idx 并将 pc 设置为 0
  • RETF (0xe4) 指令
    • 扣除 3 单位 gas
    • return_stack 中弹出 val,将 current_code_idx 设置为 val.code_section 并将 pc 设置为 val.pc
  • JUMPF (0xe5) 指令
    • 扣除 5 单位 gas
    • 读取 uint16 操作数 idx
    • 如果 1024 < len(stack) + types[idx].max_stack_height - types[idx].inputs,执行结果异常中止
    • current_code_idx 设置为 idx
    • 设置 pc = 0
  • EOFCREATE (0xec) 指令
    • 扣除 32000 gas
    • 如果当前帧处于静态模式,异常失败并中止。
    • 读取 uint8 操作数 initcontainer_index
    • 从栈中弹出 valuesaltinput_offsetinput_size
    • 执行(并收费)内存扩展,使用 [input_offset, input_size]
    • 加载在执行 EOFCREATE 的容器中,索引为 initcontainer_index 的 initcode EOF 子容器
      • initcontainer 为该 EOF 容器,其长度为字节 initcontainer_size
    • 扣除 6 * ((initcontainer_size + 31) // 32) gas(哈希费用)
    • 检查调用深度限制以及调用者余额是否足以转移 value
      • 失败时返回 0 进栈,调用者的 nonce 不被更新且初始化代码执行不会消耗 gas。
    • 调用者的内存切片 [input_offset:input_size] 被用作 calldata
    • 执行容器并扣除执行耗费的gas。EIP-150 的 63/64 规则适用。
      • 增加 sender 账户的 nonce
      • 计算 new_addresskeccak256(0xff || sender || salt || keccak256(initcontainer))[12:]
      • accessed_addresses 和地址冲突的行为与 CREATE2 相同(对于 CREATE2 的规则来自 EIP-684EIP-2929 适用于 EOFCREATE
      • 初始化代码执行不成功会导致将 0 压入栈中
        • 如果执行 REVERTed,可以填充 返回数据
      • 成功的执行以初始化代码执行 RETURNCODE{deploy_container_index}(aux_data_offset, aux_data_size) 指令结束(见下文)。之后:
        • 从容器中 deploy_container_index 的 EOF 子容器加载部署合约,执行 RETURNCODE 时使用
        • 将数据段与 (aux_data_offset, aux_data_offset + aux_data_size) 的内存片段连接,并更新头部中的数据大小
        • deployed_code_size 更新为部署容器大小
        • deployed_code_size > MAX_CODE_SIZE 指令异常中止
        • state[new_address].code 设置为更新后的部署容器
        • new_address 推入栈中
    • 扣除 200 * deployed_code_size gas
  • RETURNCODE (0xee) 指令
    • 加载 uint8 立即数 deploy_container_index
    • 从栈中弹出两个值:aux_data_offsetaux_data_size,指向将被追加到已部署容器数据中的内存区段
    • 成本为 0 gas + 可能的 aux 数据的内存扩展费用
    • 结束初始化代码帧执行并将控制返回给 EOFCREATE 调用者帧(除非在创建交易的最顶层帧中调用)。
    • 使用 deploy_container_indexaux_data 来构建部署合约(见上文)
    • 若追加后数据段大小将溢出最大数据段大小或下溢(即小于头部中声明的数据段大小),指令异常中止
  • DATALOAD (0xd0) 指令
    • 扣除 4 单位 gas
    • 从栈中弹出一个值 offset
    • 从活动容器的数据段中读取 [offset, offset+32] 并将值推入栈中
    • 如果超出数据边界,则用 0 填充
  • DATALOADN (0xd1) 指令
    • 扣除 3 单位 gas
    • 类似于 DATALOAD,但是将偏移量作为16位立即数值而不是从栈中引用
  • DATASIZE (0xd2) 指令
    • 扣除 2 单位 gas
    • 将活动容器数据段的大小推入栈中
  • DATACOPY (0xd3) 指令
    • 扣除 3 单位 gas
    • 从栈中弹出 mem_offsetoffsetsize
    • 执行对 mem_offset + size 的内存扩展并扣除内存扩展的费用
    • 扣除 3 * ((size + 31) // 32) gas 以进行拷贝
    • 从活动容器的数据段中写入 [offset, offset+size],并从 mem_offset 开始写入内存
    • 如果超出数据边界,则用 0 填充
  • DUPN (0xe6) 指令
    • 扣除 3 单位 gas
    • 读取 uint8 操作数 imm
    • n = imm + 1
    • n 个(基于 1的)栈项在栈顶被复制
    • 栈验证:stack_height >= n
  • SWAPN (0xe7) 指令
    • 扣除 3 单位 gas
    • 读取 uint8 操作数 imm
    • n = imm + 1
    • n + 1 个栈项与栈顶项交换(基于 1的)。
    • 栈验证:stack_height >= n + 1
  • EXCHANGE (0xe8) 指令
    • 扣除 3 单位 gas
    • 读取 uint8 操作数 imm
    • n = imm >> 4 + 1, m = imm & 0x0F + 1
    • n + 1 个栈项与第 n + m + 1 个栈项交换(基于 1的)。
    • 栈验证:stack_height >= n + m + 1
  • RETURNDATALOAD (0xf7) 指令
    • 扣除 3 单位 gas
    • 从栈中弹出 offset
    • 将一项推入栈中,从 offset 开始读取 32 字节单元的返回数据缓冲区
    • 如果 offset + 32 > len(returndata buffer),结果为零填充(与 CALLDATALOAD 相同)。请参见修改的行为部分中 RETURNDATACOPY 的匹配行为。
  • EXTCALL (0xf8)EXTDELEGATECALL (0xf9)EXTSTATICCALL (0xfb)

    • 替代 CALLDELEGATECALLSTATICCALL 指令,如在EIP-7069中所述,除了运行时操作数栈检查。尤其:
    • 删除输入的 gas_limit
    • 删除 output_offsetoutput_size
    • gas_limit 将设置为 (gas_left / 64) * 63(如同调用者使用 gas() 而不是 gas_limit)。
    • 不允许的 EXTDELEGATECALL 到非 EOF 合约(传统合约,EOA,空账户),并返回 1(与 calee 帧 reverts时相同)以表示失败。只消耗 EXTDELEGATECALL 的初始费用(类似于调用深度检查),且目标地址仍然变为“热”。它允许传统到EOF路径的现有代理合约能够使用EOF升级。
    • 不会对 target_address 进行地址修剪,如果地址超过 20 字节,则操作将异常中止。

    注意:替代指令 EXT*CALL 在传统代码中继续被视为 未定义

代码验证

  • 不使用未分配的指令
  • 具有立即操作数的指令不得在代码段末尾截断
  • RJUMP / RJUMPI / RJUMPV 操作数不得指向立即操作数,且不得指向代码边界之外
  • CALLFJUMPF 操作数不得超过 num_code_sections
  • CALLF 操作数不得指向输出为 0x80 的段(非返回)
  • JUMPF 操作数必须指向一个具有与其所在段相等或 fewer 的输出数量的代码段,或者指向一个输出为 0x80 的段(非返回)
  • 任一段的输入或输出数不得超过 127
  • 段类型的输出值为 0x80,且为非返回,若且唯若该段不包含 RETF 指令或对返回段的 JUMPF
    • 特别是,仅包含对非返回段的 JUMPF 的段本身也是非返回的。
  • 第一个代码段必须具有类型签名 (0, 0x80, max_stack_height)(0 输入非返回函数)
  • EOFCREATEinitcontainer_index 必须小于 num_container_sections
  • EOFCREATE 通过 initcontainer_index 指向的子容器必须有 len(data_section) 等于 data_size,即数据段内容与头部中声明的大小完全一致(见 数据段生命周期
  • EOFCREATE 通过 initcontainer_index 指向的子容器 不得 包含 RETURNSTOP 指令。
  • RETURNCODEdeploy_container_index 必须小于 num_container_sections
  • RETURNCODE 指向的子容器必须 不得 包含 RETURNCODE 指令。
  • DATALOADNimmediate + 32 必须在 pre_deploy_data_size 范围内(见 数据段生命周期
    • 超出这些边界的那部分数据段(dynamic_aux_data 部分)需要使用 DATALOADDATACOPY 进行访问
  • 不允许存在不可达代码段,即每个代码段必须从第 0 段通过一系列的 CALLF / JUMPF 指令可达,并且第 0 段隐式可达。
  • 容器中同时包含 RETURNCODE 和任一 RETURNSTOP 是错误。
  • 子容器在其父容器中未被引用是错误。
  • 指定的子容器同时被 RETURNCODEEOFCREATE 引用是错误。

栈验证

  • 代码基本块必须以每个块可通过向前跳转或指令顺序流动的方式进行排序。换句话说,没有仅通过向后跳转可达的基本块。
    • 这意味着没有指令可以不可达,但这是更强的要求。
  • 验证流程不需要实际的操作数栈实现,仅需跟踪栈高度。
  • 计算和空间复杂度为 O(len(code))。每条指令仅访问一次。
  • 每个代码段都独立验证。
  • stack_height_... 下面指的是此函数可访问的栈值的数量,即不考虑调用函数帧的值(但包括此函数的输入)。
  • 向前跳转 指的是具有相对于零的偏移量大于或等于 0 的任何 RJUMP / RJUMPI / RJUMPV 指令。 向后跳转 指的是具有相对于零的偏移量小于 0 的任何 RJUMP / RJUMPI / RJUMPV 指令,包括跳转到同一跳转指令(例如 RJUMP(-3))。
  • 终止指令:
    • 结束函数执行的:RETFJUMPF
    • 终止整个 EVM 执行的:STOPRETURNRETURNCODEREVERTINVALID
  • 对于代码中的每条指令,操作数栈的高度边界记录为 stack_height_minstack_height_max。指令在对代码的单线性过程扫描中进行扫描。
  • 第一个指令的 stack_height_min = stack_height_max = types[current_section_index].inputs

在扫描过程中,对每条指令:

  1. 检查此指令是否具有记录的栈高度边界。如果没有,这意味着它既没有被之前的前向跳转引用,也不属于顺序指令流,这段代码验证失败。
  2. 确定该指令对操作数栈的影响:
    1. 检查记录的栈高度边界是否满足指令要求。特别是:
      • 对于 CALLF,以下条件必须成立:stack_height_min >= types[target_section_index].inputs
      • 对于 RETF,以下条件必须成立:stack_height_max == stack_height_min == types[current_code_index].outputs
      • JUMPF 的栈验证依赖于目标段的“非返回”状态
        • 针对返回段的 JUMPF(只能来自返回段):stack_height_min == stack_height_max == type[current_section_index].outputs + type[target_section_index].inputs - type[target_section_index].outputs
        • 针对非返回段的 JUMPFstack_height_min >= types[target_section_index].inputs
      • 对于任何其他指令 stack_height_min 必须至少等于指令所需的输入数量,
      • 除了 RETFJUMPF,其他终止指令没有额外的检查。这意味着在到达EVM执行结束指令时,额外的项留在栈上是允许的。
    2. 对于 CALLFJUMPF,检查可能的栈溢出:如果 stack_height_max > 1024 - types[target_section_index].max_stack_height + types[target_section_index].inputs,验证失败。
    3. 根据指令执行后新栈的 stack_height_minstack_height_max,更新这两个高度:
      • 对于 CALLFstack_height_min += types[target_section_index].outputs - types[target_section_index].inputsstack_height_max += types[target_section_index].outputs - types[target_section_index].inputs
      • 对于其他非终止指令:stack_height_min += instruction_outputs - instruction_inputsstack_height_max += instruction_outputs - instruction_inputs
      • 终止指令不需要更新栈高度。
  3. 确定可以跟随当前指令的继承指令列表:
    1. 所有指令(除了终止指令和 RJUMP)的下一个指令。
    2. RJUMPRJUMPIRJUMPV 的所有目标指令。
  4. 对于每个后继指令:
    1. 检查指令是否在代码中存在(即执行中不可发生"跳出"代码)。
      • 这意味着最后一条指令可以是终止指令或 RJUMP
    2. 如果后继是通过前向跳转或从上一个指令的顺序流访问的:
      1. 如果指令没有记录栈高度(首次访问),记录指令的 stack_height_minstack_height_max 等于在 2.3 中计算的值。
      2. 否则指令已被预先访问(通过先前看到的前向跳转)。更新该指令的记录栈高度边界,以便它们包含在 2.3 中计算的边界,即 target_stack_min = min(target_stack_min, current_stack_min)target_stack_max = max(target_stack_max, current_stack_max),其中 (target_stack_min, target_stack_max) 是后继边界,(current_stack_min, current_stack_max) 是 2.3 中计算的边界。
    3. 如果后继是通过向后跳转访问的,则检查目标边界是否等于在 2.3 中计算的值,即 target_stack_min == current_stack_min && target_stack_max == current_stack_max。如果它们不相等,则验证失败,即我们看到向后跳转到不同的栈高度。
  • 一个函数的最大数据栈不得超过 1023
  • types[current_code_index].max_stack_height 必须与验证过程中观察到的最大栈高度匹配

示例

已注释的EOF格式容器示例,演示EOF的几个关键特性,可以在evmone项目代码库中的此测试文件中找到。

附录:原始EIPs

这些是演变为该规范的各个EIP。

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

0 条评论

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