“Mega EOF 终局”规范(EOFv1)
本文介绍了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_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) 新行为的引入。
修改的行为
- 执行从代码段 0 的第一个字节开始,
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。- 注:不再执行 jumpdest 分析。
- EOF 合约不得部署传统代码(在代码验证阶段自然被拒绝)
- 如果从传统合约执行,若
CREATE和CREATE2指令有EOF代码作为 initcode(以EF00魔法开头)- 部署失败(在栈上返回 0)
- 调用者的nonce未更新,且initcode执行所需gas未消耗
RETURNDATACOPY (0x3E)指令- 行为与传统相同,但将特殊中止行为变更为零填充行为(与
CALLDATACOPY相同)。
- 行为与传统相同,但将特殊中止行为变更为零填充行为(与
注意 类似于传统目标,EXTCODECOPY、EXTCODEHASH 和 EXTCODESIZE 的上述行为不适用于创建中的EOF合约目标,即这些报告与无代码账户的结果相同。
创建交易
创建交易(to 為空的交易),data 包含EOF代码(以 EF00 魔法开头),被解释为在 data 中具有EOF initcontainer 和 calldata 的连接:
- 应用 EIP-3860 中定义的传统创建交易的固有gas费用规则和限制。交易的整个
data用于这些计算。 - 找到将
data分割为initcontainer和calldata的方式:- 解析 EOF 头部
- 通过从头部读取所有段大小并加上头部大小计算
initcontainer大小。
- 递归验证
initcontainer和它的所有子容器。- 与一般验证不同,
initcontainer还必须在头部声明的data_size等于实际data_section大小。 - 验证包括检查
initcontainer不包含RETURN或STOP
- 与一般验证不同,
- 如果 EOF 头部解析或完整容器验证失败,交易在被视为有效和失败。初始化代码执行不会消耗gas,仅收取固有创建交易费用。
- 交易
data中紧接着initcontainer的calldata被视为传递到执行框架中的calldata。 - 执行容器并扣除执行耗费的gas。
- 计算
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_sizegas
注意 传统合约和传统创建交易不得部署 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
- 在实例化 VM 上下文时,将初始值
-
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)指令- 扣除
32000gas - 如果当前帧处于静态模式,异常失败并中止。
- 读取 uint8 操作数
initcontainer_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- 失败时返回 0 进栈,调用者的 nonce 不被更新且初始化代码执行不会消耗 gas。
- 调用者的内存切片 [
input_offset:input_size] 被用作 calldata - 执行容器并扣除执行耗费的gas。EIP-150 的 63/64 规则适用。
- 增加
sender账户的 nonce - 计算
new_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_sizegas
- 扣除
-
RETURNCODE (0xee)指令- 加载
uint8立即数deploy_container_index - 从栈中弹出两个值:
aux_data_offset和aux_data_size,指向将被追加到已部署容器数据中的内存区段 - 成本为 0 gas + 可能的 aux 数据的内存扩展费用
- 结束初始化代码帧执行并将控制返回给
EOFCREATE调用者帧(除非在创建交易的最顶层帧中调用)。 - 使用
deploy_container_index和aux_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_offset、offset、size - 执行对
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)- 替代
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的段(非返回)- 任一段的输入或输出数不得超过 127
- 段类型的输出值为
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进行访问
- 超出这些边界的那部分数据段(
- 不允许存在不可达代码段,即每个代码段必须从第 0 段通过一系列的
CALLF/JUMPF指令可达,并且第 0 段隐式可达。 - 容器中同时包含
RETURNCODE和任一RETURN或STOP是错误。 - 子容器在其父容器中未被引用是错误。
- 指定的子容器同时被
RETURNCODE和EOFCREATE引用是错误。
栈验证
- 代码基本块必须以每个块可通过向前跳转或指令顺序流动的方式进行排序。换句话说,没有仅通过向后跳转可达的基本块。
- 这意味着没有指令可以不可达,但这是更强的要求。
- 验证流程不需要实际的操作数栈实现,仅需跟踪栈高度。
- 计算和空间复杂度为 O(len(code))。每条指令仅访问一次。
- 每个代码段都独立验证。
stack_height_...下面指的是此函数可访问的栈值的数量,即不考虑调用函数帧的值(但包括此函数的输入)。- 向前跳转 指的是具有相对于零的偏移量大于或等于 0 的任何
RJUMP/RJUMPI/RJUMPV指令。 向后跳转 指的是具有相对于零的偏移量小于 0 的任何RJUMP/RJUMPI/RJUMPV指令,包括跳转到同一跳转指令(例如RJUMP(-3))。 - 终止指令:
- 结束函数执行的:
RETF、JUMPF, - 终止整个 EVM 执行的:
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 中计算的值。 - 否则指令已被预先访问(通过先前看到的前向跳转)。更新该指令的记录栈高度边界,以便它们包含在 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 中计算的边界。
- 如果指令没有记录栈高度(首次访问),记录指令的
- 如果后继是通过向后跳转访问的,则检查目标边界是否等于在 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。
- ?EIP-3540: EOF - EVM对象格式 v1 历史
- ?EIP-3670: EOF - 代码验证 历史
- ?EIP-4200: EOF - 静态相对跳转 历史
- ?EIP-4750: EOF - 功能 历史
- ?EIP-5450: EOF - 栈验证 历史
- ?EIP-6206: EOF - JUMPF指令 历史
- ?EIP-7480: EOF - 数据段访问指令 历史
- ?EIP-663: 无限SWAP和DUP指令 历史
- ?EIP-7069: 重新设计的CALL指令 (不需要EOF) 历史
- ?EIP-7620: EOF - 合约创建指令 历史
- ?EIP-7698: EOF - 创建交易 历史
- 原文链接: github.com/ipsilon/eof/b...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~