本文介绍了EVM对象格式(EOFv1)的统一规范,详细探讨了其结构、头部与主体的组成、代码执行语义以及新的指令与验证方法等。EOF的推出旨在改善以太坊虚拟机(EVM)的功能和灵活性,提供更好的代码执行环境和数据处理能力。
本统一规范应作为理解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_size。
pre_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_data是aux_data的一个子范围,其大小在RETURNCODE之前已知,等于pre_deploy_data_size - len(pre_deploy_data_section)。dynamic_aux_data是aux_data的其余部分。已部署容器头部中的data_size也被更新为等于len(data_section)。
总结来说,最终数据段中有pre_deploy_data_size字节在EOF容器部署之前是保证存在的,len(dynamic_aux_data)字节在EOF容器部署之后才被确认存在。
这影响到对数据段访问指令的验证和行为:DATALOAD、DATALOADN和DATACOPY,详见代码验证。
在上表中定义的类型上,还对容器格式施加了以下有效性约束:
15 字节types_size 必须是 4 的倍数types_size / 413 + 2*num_code_sections + types_size + code_size[0] + ... + code_size[num_code_sections-1] + data_size16 + 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) 新行为的引入。
pc 被设置为 0。pc 的作用域限于正在执行的代码段CALL、CALLCODE、DELEGATECALL、STATICCALL、SELFDESTRUCT、JUMP、JUMPI、PC、CREATE、CREATE2、CODESIZE、CODECOPY、EXTCODESIZE、EXTCODECOPY、EXTCODEHASH、GAS 指令在EOF合约中被弃用,并在验证时被拒绝。它们仅在传统合约中可用。EXTCODECOPY的目标账户为EOF合约,则它将从EF00复制最多2个字节,仿佛那是代码。EXTCODEHASH的目标账户为EOF合约,则它返回 0x9dbf3648db8210552e9c4f75c6a1c3057c0ca432043bd648be15fe7be05646f5 (EF00 的哈希,仿佛那是代码)。EXTCODESIZE的目标账户为EOF合约,则它返回 2。JUMPDEST 被重命名为 NOP,并保持不变地消耗 1 单位 gas。
CREATE和CREATE2指令有EOF代码作为 initcode(以 EF00 魔法开头)
RETURNDATACOPY (0x3E) 指令
CALLDATACOPY 相同)。注意 类似于传统目标,EXTCODECOPY、EXTCODEHASH 和 EXTCODESIZE 的上述行为不适用于创建中的EOF合约目标,即这些报告与无代码账户的结果相同。
创建交易(to 為空的交易),data 包含EOF代码(以 EF00 魔法开头),被解释为在 data 中具有EOF initcontainer 和 calldata 的连接:
data 用于这些计算。data 分割为 initcontainer 和 calldata 的方式:
initcontainer 大小。initcontainer 和它的所有子容器。
initcontainer 还必须在头部声明的 data_size 等于实际 data_section 大小。initcontainer 不包含 RETURN 或 STOPdata 中紧接着 initcontainer 的 calldata 被视为传递到执行框架中的calldata。new_address 为 keccak256(sender || sender_nonce)[12:]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 设置为更新后的部署容器200 * deployed_code_size gas注意 传统合约和传统创建交易不得部署 EOF 代码,这一行为来自EIP-3541并未发生修改。
在 EOF 代码中引入了以下指令:
RJUMP (0xe0) 指令
offset,设置 pc = offset + pc + 3RJUMPI (0xe1) 指令
conditionpc += 3condition != 0,读取 int16 操作数 offset 并设置 pc += offsetRJUMPV (0xe2) 指令
max_indexcasepc += 2case > max_index(越界情况),通过设置 pc += (max_index + 1) * 2 后退pc + case * 2 的2字节操作数解释为 int16,定义为 offset,并设置 pc += (max_index + 1) * 2 + offsetcurrent_code_idx 用于存储当前正在执行的代码段索引return_stack 用于存储成对 (code_section, pc)。
(0,0) 推入 return stackCALLF (0xe3) 指令
idx1024 < 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 设置为 0RETF (0xe4) 指令
return_stack 中弹出 val,将 current_code_idx 设置为 val.code_section 并将 pc 设置为 val.pcJUMPF (0xe5) 指令
idx1024 < len(stack) + types[idx].max_stack_height - types[idx].inputs,执行结果异常中止current_code_idx 设置为 idxpc = 0EOFCREATE (0xec) 指令
32000 gasinitcontainer_indexvalue、salt、input_offset、input_size[input_offset, input_size]EOFCREATE 的容器中,索引为 initcontainer_index 的 initcode EOF 子容器
initcontainer 为该 EOF 容器,其长度为字节 initcontainer_size6 * ((initcontainer_size + 31) // 32) gas(哈希费用)value
input_offset:input_size] 被用作 calldatasender 账户的 noncenew_address 为 keccak256(0xff || sender || salt || keccak256(initcontainer))[12:]accessed_addresses 和地址冲突的行为与 CREATE2 相同(对于 CREATE2 的规则来自 EIP-684 和 EIP-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 gasRETURNCODE (0xee) 指令
uint8 立即数 deploy_container_indexaux_data_offset 和 aux_data_size,指向将被追加到已部署容器数据中的内存区段EOFCREATE 调用者帧(除非在创建交易的最顶层帧中调用)。deploy_container_index 和 aux_data 来构建部署合约(见上文)DATALOAD (0xd0) 指令
offset[offset, offset+32] 并将值推入栈中DATALOADN (0xd1) 指令
DATALOAD,但是将偏移量作为16位立即数值而不是从栈中引用DATASIZE (0xd2) 指令
DATACOPY (0xd3) 指令
mem_offset、offset、sizemem_offset + size 的内存扩展并扣除内存扩展的费用3 * ((size + 31) // 32) gas 以进行拷贝[offset, offset+size],并从 mem_offset 开始写入内存DUPN (0xe6) 指令
immn = imm + 1n 个(基于 1的)栈项在栈顶被复制stack_height >= nSWAPN (0xe7) 指令
immn = imm + 1n + 1 个栈项与栈顶项交换(基于 1的)。stack_height >= n + 1EXCHANGE (0xe8) 指令
immn = imm >> 4 + 1, m = imm & 0x0F + 1n + 1 个栈项与第 n + m + 1 个栈项交换(基于 1的)。stack_height >= n + m + 1RETURNDATALOAD (0xf7) 指令
offsetoffset 开始读取 32 字节单元的返回数据缓冲区offset + 32 > len(returndata buffer),结果为零填充(与 CALLDATALOAD 相同)。请参见修改的行为部分中 RETURNDATACOPY 的匹配行为。EXTCALL (0xf8)、EXTDELEGATECALL (0xf9)、EXTSTATICCALL (0xfb)
CALL、DELEGATECALL 和 STATICCALL 指令,如在EIP-7069中所述,除了运行时操作数栈检查。尤其:gas_limit。output_offset 和 output_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 操作数不得指向立即操作数,且不得指向代码边界之外CALLF 和 JUMPF 操作数不得超过 num_code_sectionsCALLF 操作数不得指向输出为 0x80 的段(非返回)JUMPF 操作数必须指向一个具有与其所在段相等或 fewer 的输出数量的代码段,或者指向一个输出为 0x80 的段(非返回)0x80,且为非返回,若且唯若该段不包含 RETF 指令或对返回段的 JUMPF。
JUMPF 的段本身也是非返回的。(0, 0x80, max_stack_height)(0 输入非返回函数)EOFCREATE 的 initcontainer_index 必须小于 num_container_sectionsEOFCREATE 通过 initcontainer_index 指向的子容器必须有 len(data_section) 等于 data_size,即数据段内容与头部中声明的大小完全一致(见 数据段生命周期)EOFCREATE 通过 initcontainer_index 指向的子容器 不得 包含 RETURN 或 STOP 指令。RETURNCODE 的 deploy_container_index 必须小于 num_container_sectionsRETURNCODE 指向的子容器必须 不得 包含 RETURNCODE 指令。DATALOADN 的 immediate + 32 必须在 pre_deploy_data_size 范围内(见 数据段生命周期)
dynamic_aux_data 部分)需要使用 DATALOAD 或 DATACOPY 进行访问CALLF / JUMPF 指令可达,并且第 0 段隐式可达。RETURNCODE 和任一 RETURN 或 STOP 是错误。RETURNCODE 和 EOFCREATE 引用是错误。stack_height_... 下面指的是此函数可访问的栈值的数量,即不考虑调用函数帧的值(但包括此函数的输入)。RJUMP / RJUMPI / RJUMPV 指令。 向后跳转 指的是具有相对于零的偏移量小于 0 的任何 RJUMP / RJUMPI / RJUMPV 指令,包括跳转到同一跳转指令(例如 RJUMP(-3))。RETF、JUMPF,STOP、RETURN、RETURNCODE、REVERT、INVALID。stack_height_min 和 stack_height_max。指令在对代码的单线性过程扫描中进行扫描。stack_height_min = stack_height_max = types[current_section_index].inputs。在扫描过程中,对每条指令:
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].outputsJUMPF:stack_height_min >= types[target_section_index].inputsstack_height_min 必须至少等于指令所需的输入数量,RETF 和 JUMPF,其他终止指令没有额外的检查。这意味着在到达EVM执行结束指令时,额外的项留在栈上是允许的。CALLF 和 JUMPF,检查可能的栈溢出:如果 stack_height_max > 1024 - types[target_section_index].max_stack_height + types[target_section_index].inputs,验证失败。stack_height_min 和 stack_height_max,更新这两个高度:
CALLF:stack_height_min += types[target_section_index].outputs - types[target_section_index].inputs,stack_height_max += types[target_section_index].outputs - types[target_section_index].inputs,stack_height_min += instruction_outputs - instruction_inputs, stack_height_max += instruction_outputs - instruction_inputs,RJUMP)的下一个指令。RJUMP、RJUMPI 或 RJUMPV 的所有目标指令。RJUMPstack_height_min和 stack_height_max 等于在 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 中计算的边界。target_stack_min == current_stack_min && target_stack_max == current_stack_max。如果它们不相等,则验证失败,即我们看到向后跳转到不同的栈高度。types[current_code_index].max_stack_height 必须与验证过程中观察到的最大栈高度匹配已注释的EOF格式容器示例,演示EOF的几个关键特性,可以在evmone项目代码库中的此测试文件中找到。
这些是演变为该规范的各个EIP。
- 原文链接: github.com/ipsilon/eof/b...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!