EIP-7620: EOF 合约创建
引入 `EOFCREATE` 和 `RETURNCODE` 指令
Authors | Alex Beregszaszi (@axic), Paweł Bylica (@chfast), Andrei Maiboroda (@gumb0), Piotr Dobaczewski (@pdobacz) |
---|---|
Created | 2024-02-12 |
Requires | EIP-170, EIP-684, EIP-2929, EIP-3540, EIP-3541, EIP-3670 |
Table of Contents
摘要
EVM Object Format (EOF) 移除了使用 CREATE
或 CREATE2
指令创建合约的可能性。我们引入了一种新的/替代方法,以 EOFCREATE
和 RETURNCODE
指令对的形式,提供一种使用 EOF 容器创建合约的方式。
动机
此 EIP 使用了 EIP-3540 中的术语,该 EIP 引入了 EOF 格式。
EOF 的目标是移除代码可观察性,这是使用传统风格创建交易、CREATE
或 CREATE2
的传统 EVM 合约创建逻辑的先决条件,因为 initcode 和 code 都可以被 EVM 访问和操作。基于相同的原则,EOF 移除了像 CODECOPY
和 EXTCODECOPY
这样的操作码,引入了 EOF 子容器作为替代方案,以满足创建其他合约的工厂合约的需求。
此 EIP 中引入的新指令在 EOF 容器上运行,从而实现了传统 EVM 拥有的工厂合约用例。
规范
本文档中的关键词“必须”,“不得”,“必需”,“应”,“不应”,“建议”,“不建议”,“可以”和“可选”应解释为 RFC 2119 和 RFC 8174 中所述。
在未明确列出的地方,EOF 合约创建的规则以及 EOFCREATE
指令应与 CREATE2
指令的规则相同或类似。这包括但不限于:
accessed_addresses
上的行为和地址冲突 (EIP-684 和 EIP-2929)- 为
EOFCREATE
initcode 创建的 EVM 执行帧 - 内存,帐户上下文等。 - 新创建合约的帐户的 nonce 递增 EIP-161
- 用于创建捐赠的余额检查和转移(
value
参数)
参数
常量 | 值 |
---|---|
TX_CREATE_COST |
在 Ethereum Execution Layer Specs 中定义为 32000 |
STACK_DEPTH_LIMIT |
在 Ethereum Execution Layer Specs 中定义为 1024 |
GAS_CODE_DEPOSIT |
在 Ethereum Execution Layer Specs 中定义为 200 |
MAX_CODE_SIZE |
在 EIP-170 中定义为 24576 |
我们引入了两个新的指令,它们与 EIP-3540 在同一区块号上激活:
EOFCREATE
(0xec
)RETURNCODE
(0xee
)
如果代码是传统字节码,则任何这些指令都会导致异常停止。 (注意:这意味着行为没有改变。)
执行语义
- 指令
CREATE
,CREATE2
已过时,并且在 EOF 合约中被验证拒绝。它们仅在传统合约中可用。 - 如果指令
CREATE
和CREATE2
具有 EOF 代码作为 initcode(以EF00
魔数开头)- 部署失败(在堆栈上返回 0)
- 调用者的 nonce 不会更新,并且不会消耗 initcode 执行的 gas
在传统字节码执行的上下文中,任何这些指令(EOFCREATE
,RETURNCODE
)都会导致异常停止。 (注意:这意味着行为没有改变。)
新合约创建流程概述
在 EOF EVM 中,通过 InitcodeTransaction
传递 EOF 容器 (initcontainer
),将新字节码引入到状态中。这样的容器可以包括任意深度嵌套的子容器。initcontainer
及其子容器根据适用于所讨论的 EOF 版本的各种验证规则进行递归验证。接下来,执行 initcontainer
的第 0 个代码段,并且可能最终调用 RETURNCODE
指令,该指令将引用要最终部署到地址的子容器。
InitcodeTransactions
在 EIP-7873 中详细定义。
EOFCREATE
指令反过来替代了 CREATE
和 CREATE2
传统指令,允许工厂合约创建其他合约。与 InitcodeTransaction
的主要区别在于,选择 initcontainer
作为调用 EOFCREATE
的 EOF 容器的子容器之一(而不是 transaction.initcodes
之一)。值得注意的是,此时不执行任何验证,因为在部署包含 EOFCREATE
的工厂合约时已经执行了验证。
有关每个指令的详细信息,请参见以下各节。
EOFCREATE
- 扣除
TX_CREATE_COST
gas - 如果当前帧处于
static-mode
,则异常失败而停止。 - 读取立即数操作数
initcontainer_index
,编码为 8 位无符号值 - 从操作数堆栈中弹出
salt
,input_offset
,input_size
,value
- 使用
[input_offset, input_size]
执行(并收取费用)内存扩展 - 在执行
EOFCREATE
的容器中,加载initcontainer_index
处的 initcode EOF 子容器- 令
initcontainer
为该 EOF 容器
- 令
- 检查当前调用深度是否低于
STACK_DEPTH_LIMIT
以及调用者余额是否足以转移value
- 如果失败,则在堆栈上返回 0,调用者的 nonce 不会更新,并且不会消耗 initcode 执行的 gas。
- 调用者的内存切片
[input_offset:input_size]
用作 calldata - 执行容器并扣除执行 gas。来自 EIP-150 的 63/64 规则适用。
- 递增
sender
帐户的 nonce - 将
new_address
计算为keccak256(0xff || sender32 || salt)[12:]
,其中sender32
是发送者地址,左侧用零填充为 32 字节 - initcode 执行不成功会导致将
0
推送到堆栈上- 如果执行
REVERT
,则可以填充 returndata
- 如果执行
- 成功执行以 initcode 执行
RETURNCODE{deploy_container_index}(aux_data_offset, aux_data_size)
指令结束(请参见下文)。在那之后:- 在执行
RETURNCODE
的容器中,加载deploy_container_index
处的部署 EOF 子容器 - 将数据段与
(aux_data_offset, aux_data_offset + aux_data_size)
内存段连接起来,并在需要时更新标头中的数据大小。 - 如果更新后的部署容器大小超过
MAX_CODE_SIZE
,则该指令会异常中止 - 将
state[new_address].code
设置为更新后的部署容器 - 将
new_address
推送到堆栈上
- 在执行
- 扣除
GAS_CODE_DEPOSIT * deployed_code_size
gas
RETURNCODE
- 读取立即数操作数
deploy_container_index
,编码为 8 位无符号值 - 从操作数堆栈中弹出两个值:
aux_data_offset
,aux_data_size
,指向将附加到已部署容器的数据的内存段 - 消耗 0 gas + aux 数据的可能内存扩展
- 结束 initcode 帧执行并将控制权返回给 EOFCREATE 调用者帧,在该帧中,
deploy_container_index
和aux_data
用于构造已部署的合约(请参见上文) - 如果在附加之后,数据段大小将溢出最大数据段大小或下溢(即小于标头中声明的数据段大小),则该指令会异常中止
代码验证
我们扩展了代码段验证规则(如 EIP-3670 中所定义)。
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
deploy_container_index
指向的子容器不得包含RETURNCODE
指令- 容器同时包含
RETURNCODE
和RETURN
或STOP
是一种错误 - 子容器从未在其父容器中被引用是一种错误
- 给定的子容器同时被
RETURNCODE
和EOFCREATE
引用是一种错误 RJUMP
,RJUMPI
和RJUMPV
立即数参数值(跳转目标相对偏移量)验证:如果偏移量指向紧跟在EOFCREATE
或RETURNCODE
指令之后的字节,则代码段无效。
数据段生命周期
对于尚未部署的 EOF 容器,data_section
只是部署后最终 data_section
的一部分。
让我们将其定义为 pre_deploy_data_section
并将 pre_deploy_data_size
定义为该容器的标头中声明的 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
是在RETURNCODE
指令上附加到pre_deploy_data_section
的数据。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)
个字节。
这会影响数据段访问指令的验证和行为:DATALOAD
,DATALOADN
和 DATACOPY
,请参见 EIP-7480。
理由
数据段附加
在合约创建期间附加数据段,并且还需要在标头中更新其大小。考虑了替代设计,其中:
- 引入了数据段的附加段类型
- 引入了描述子容器的附加字段
- 将会覆盖数据段而不是附加到数据段,因此需要在部署之前用 0 字节填充
所有这些替代方案要么使原本简单的数据结构复杂化,要么带走了有用的功能(例如数据段的动态大小部分)。
new_address
哈希方案中的 keccak256(initcontainer)
最初提出 new_address = keccak256(0xff || sender || salt || keccak256(initcontainer))[12:]
作为计算新创建合约地址的方式,类似于 CREATE2
使用的方式,但不完全相同。
但是,这种替代方法与代码不可观察性背道而驰,因为它锁定了 initcontainer 的内容,例如,阻止了在将来的升级中对其进行重写。而且,它似乎不必要地昂贵:EOFCREATE
只能选取其子容器之一,但是每次执行 EOFCREATE
都需要重新计算哈希值。
考虑了其他消除代码可观察性的方法,但保留了某种形式的代码见证。但是,仅保留 sender
和 salt
允许工厂合约的实现者(即包含 EOFCREATE
指令的合约)无论如何都通过 salt
包含代码见证,如果这对于特定用例是必要的。因此,保持 new_address
公式最小化是最灵活的方法,并具有更好的关注点分离。
从 new_address
哈希中删除 keccak256(initcontainer)
还具有使 new_address
独立于元数据段(在单独的 EIP 中为 EOF 提议)的优势,这是一个期望的属性。不幸的是,如果工厂想要选择提交到特定的 initcontainer
,则需要将其包含在 salt
中,这将包括元数据段。
EOFCREATE
堆栈参数顺序
与传统指令 *CALL
相比,EIP-7069 中的 EXT*CALL
指令已更改了其堆栈参数顺序。我们遵循相同的更改,以使 EOFCREATE
堆栈参数顺序与 EXTCALL
的顺序匹配。
向后兼容性
此更改不会对向后兼容性构成任何风险,因为它与 EIP-3540 同时引入。新指令不会为传统字节码(非 EOF 格式的代码)引入,并且传统字节码的合约创建选项不会更改。
带有 EF00
initcode 的 CREATE
和 CREATE2
调用会尽早失败,而不会执行 initcode。以前,在这两种情况下,initcode 执行都会开始并在第一个未定义的指令 EF
上失败。
测试用例
创建交易,CREATE
和 CREATE2
不能使其 code 以 0xEF
开头,但这些情况已经在 EIP-3541 中涵盖。但是,必须添加新的用例,其中 CREATE
或 CREATE2
的 initcode 是(有效或无效地)EOF 格式的:
Initcode | 预期结果 |
---|---|
0xEF |
initcode 开始执行并失败 |
0xEF01 |
initcode 开始执行并失败 |
0xEF5f |
initcode 开始执行并失败 |
0xEF00 |
CREATE / CREATE2 尽早失败,返回 0 并保持发件人 nonce 不变 |
0xEF0001 |
如上 |
有效的 EOFv1 容器 | 如上 |
安全注意事项
EOF InitcodeTransaction
(在 EIP-7873 中指定)需要详细的审查和讨论,因为这是外部未验证的代码进入状态的地方。除其他外:
- 其复杂性是否受到控制,从而排除了任何 DoS 尝试
- 它是否已正确定价并始终收费
- 验证是否全面并且不允许将有问题的代码保存到状态中
版权
版权及相关权利通过 CC0 放弃。
Citation
Please cite this document as:
Alex Beregszaszi (@axic), Paweł Bylica (@chfast), Andrei Maiboroda (@gumb0), Piotr Dobaczewski (@pdobacz), "EIP-7620: EOF 合约创建 [DRAFT]," Ethereum Improvement Proposals, no. 7620, February 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7620.