本文介绍了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 / 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) 新行为的引入。
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
或 STOP
data
中紧接着 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 + 3
RJUMPI (0xe1)
指令
condition
pc += 3
condition != 0
,读取 int16 操作数 offset
并设置 pc += offset
RJUMPV (0xe2)
指令
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
current_code_idx
用于存储当前正在执行的代码段索引return_stack
用于存储成对 (code_section, pc)
。
(0,0)
推入 return stackCALLF (0xe3)
指令
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
设置为 0RETF (0xe4)
指令
return_stack
中弹出 val
,将 current_code_idx
设置为 val.code_section
并将 pc
设置为 val.pc
JUMPF (0xe5)
指令
idx
1024 < len(stack) + types[idx].max_stack_height - types[idx].inputs
,执行结果异常中止current_code_idx
设置为 idx
pc = 0
EOFCREATE (0xec)
指令
32000
gasinitcontainer_index
value
、salt
、input_offset
、input_size
[input_offset, input_size]
EOFCREATE
的容器中,索引为 initcontainer_index
的 initcode EOF 子容器
initcontainer
为该 EOF 容器,其长度为字节 initcontainer_size
6 * ((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
压入栈中
REVERT
ed,可以填充 返回数据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_index
aux_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
、size
mem_offset + size
的内存扩展并扣除内存扩展的费用3 * ((size + 31) // 32)
gas 以进行拷贝[offset, offset+size]
,并从 mem_offset
开始写入内存DUPN (0xe6)
指令
imm
n = imm + 1
n
个(基于 1的)栈项在栈顶被复制stack_height >= n
SWAPN (0xe7)
指令
imm
n = imm + 1
n + 1
个栈项与栈顶项交换(基于 1的)。stack_height >= n + 1
EXCHANGE (0xe8)
指令
imm
n = imm >> 4 + 1
, m = imm & 0x0F + 1
n + 1
个栈项与第 n + m + 1
个栈项交换(基于 1的)。stack_height >= n + m + 1
RETURNDATALOAD (0xf7)
指令
offset
offset
开始读取 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_sections
CALLF
操作数不得指向输出为 0x80
的段(非返回)JUMPF
操作数必须指向一个具有与其所在段相等或 fewer 的输出数量的代码段,或者指向一个输出为 0x80
的段(非返回)0x80
,且为非返回,若且唯若该段不包含 RETF
指令或对返回段的 JUMPF
。
JUMPF
的段本身也是非返回的。(0, 0x80, max_stack_height)
(0 输入非返回函数)EOFCREATE
的 initcontainer_index
必须小于 num_container_sections
EOFCREATE
通过 initcontainer_index
指向的子容器必须有 len(data_section)
等于 data_size
,即数据段内容与头部中声明的大小完全一致(见 数据段生命周期)EOFCREATE
通过 initcontainer_index
指向的子容器 不得 包含 RETURN
或 STOP
指令。RETURNCODE
的 deploy_container_index
必须小于 num_container_sections
RETURNCODE
指向的子容器必须 不得 包含 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].outputs
JUMPF
:stack_height_min >= types[target_section_index].inputs
stack_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
的所有目标指令。RJUMP
stack_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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!