Alert Source Discuss
⚠️ Review Standards Track: Core

EIP-4750: EOF - 函数

带有 `CALLF` 和 `RETF` 指令的函数的独立部分

Authors Andrei Maiboroda (@gumb0), Alex Beregszaszi (@axic), Paweł Bylica (@chfast)
Created 2022-01-10
Requires EIP-3540, EIP-3670, EIP-5450

摘要

引入在 EOF 格式 (EIP-3540) 字节码中拥有多个代码段的能力,每个代码段代表一个独立的子程序/函数。引入两个新的操作码 CALLFRETF,用于调用和从这样的函数返回。不允许动态跳转指令。

动机

目前,在 EVM 中,一切都是动态跳转。像 Solidity 这样的语言以静态方式生成大多数跳转(即,在跳转之前,将目标地址压入栈中,PUSHn .. JUMP)。但不幸的是,由于增加了验证/分析的要求,大多数 EVM 解释器无法使用它。这也限制了它们进行优化并可能降低跳转的成本。

EIP-4200 引入了静态跳转指令,从而消除了大多数动态跳转用例的需求,但并非所有问题都可以通过它们解决。

此 EIP 旨在消除对动态跳转的需求并禁止动态跳转,因为它提供了这些跳转所使用的最重要的功能:调用和从函数返回。

此外,它旨在通过编码每个给定函数的输入和输出数量,以及隔离每个函数的堆栈(即,函数无法读取调用者/被调用者的堆栈)来改善分析机会。

规范

类型段

EOF 容器的类型段必须遵守以下要求:

  1. 该段是一个元数据列表,其中类型段中的元数据索引对应于代码段索引。因此,类型段大小必须为 n * 4 字节,其中 n 是代码段的数量。
  2. 每个元数据项都有 3 个属性:一个 uint8 inputs,一个 uint8 outputs 和一个 uint16 max_stack_increase注意:这意味着输入和输出的堆栈限制为 255。这进一步限制为 127 个堆栈项,因为输入和输出字节的最高位保留供将来使用(outputs == 0x80 已经在 EOF1 中用于表示不返回的函数,这是在单独的 EIP 中引入的)。max_stack_increaseEIP-5450 中进一步定义。
  3. 第 0 个代码段必须有 0 个输入且不能返回。

请参阅 EIP-3540 以查看格式良好的 EOF 字节码的完整结构。

EVM 中的新执行状态

引入一个返回堆栈,与操作数堆栈分开。它是一个项目堆栈,表示函数执行完成后要返回的执行状态。每个项目都由代码段索引和代码段中的偏移量(PC 值)组成。

注意:实现可以自由选择堆栈项的特定编码。在下面的规范中,我们假设表示形式是两个无符号整数:code_section_indexoffset

返回堆栈最多限制为 1024 个项目。

此外,EVM 跟踪当前执行段的索引 - current_section_index

新指令

我们引入两个新指令:

  1. CALLF (0xe3) - 调用函数
  2. RETF (0xe4) - 从函数返回

如果代码是旧字节码,则任何这些指令都会导致异常停止。(注意:这意味着行为没有改变。

首先,我们定义几个辅助值:

  • type[i].inputs = type_section_contents[i * 4] - 第 i 个代码段的输入数量
  • type[i].outputs = type_section_contents[i * 4 + 1] - 第 i 个代码段的输出数量
  • type[i].max_stack_increase = type_section_contents[i * 4 + 2:i * 4 + 4] - 第 i 个代码段的最大操作数堆栈高度增加

如果代码是有效的 EOF1,则以下执行规则适用:

CALLF

  1. 有一个立即数参数,target_section_index,编码为 16 位无符号大端值。
  2. 注意: EOF 验证 EIP-5450 保证操作数堆栈具有足够的项目用作被调用者的输入。
  3. 如果操作数堆栈大小超过 1024 - type[target_section_index].max_stack_increase(即,如果被调用的函数可能超出全局堆栈高度限制),则执行会导致异常停止。这也保证了调用后的堆栈高度在限制范围内。
  4. 如果返回堆栈已经有 1024 个项目,则执行会导致异常停止。
  5. 收取 5 gas。
  6. 不弹出任何内容,也不将任何内容推送到操作数堆栈。
  7. 将一个项目推送到返回堆栈:

    (code_section_index = current_section_index, 
    offset = PC_post_instruction)
    

    PC_post_instruction 下,我们指的是 CALLF 的整个立即数参数之后的 PC 位置。

    注意: EOF 验证 EIP-5450 保证始终有一个指令跟随 CALLF(因为终止指令或无条件跳转必须是该段中的最后一个指令),因此 PC_post_instruction 始终指向段边界内的指令。

  8. current_section_index 设置为 target_section_index,将 PC 设置为 0,并在被调用的段中继续执行。

RETF

  1. 没有立即数参数。
  2. 注意: EOF 验证 EIP-5450 保证操作数堆栈具有确切数量的项目用作输出。
  3. 收取 3 gas。
  4. 不弹出任何内容,也不将任何内容推送到操作数堆栈。
  5. 从返回堆栈中弹出一个项目,并将 current_section_indexPC 设置为此项目中的值。

注意: 0th 代码段不返回的 EOF 验证要求(在单独的 EIP 中引入了不返回的段)保证了 RETF 之前返回堆栈不能为空。

代码验证

除了上述容器格式验证规则外,我们还扩展了代码段验证规则(如 EIP-3670 中所定义)。

  1. EIP-3670 的代码验证规则应用于每个代码段。
  2. 如果任何 CALLF 的立即数参数大于或等于代码段的总数,则代码段无效。
  3. RJUMPRJUMPIRJUMPV 立即数参数值(跳转目标相对偏移量)验证:
    1. 如果偏移量指向段边界之外的位置,则代码段无效。
    2. 如果偏移量指向紧随 CALLF 指令之后的两个字节之一,则代码段无效。
  4. 不允许无法访问的代码段,即每个代码段都可以通过一系列 CALLF / JUMPF(在单独的 EIP 中引入了 JUMPF)指令从第 0 个代码段访问(第 0 个代码段始终可访问)。

禁止的指令

动态跳转指令 JUMP (0x56) 和 JUMPI (0x57) 无效,并且它们的操作码未定义。

JUMPDEST (0x5b) 指令已重命名为 NOP(“无操作”),而行为保持不变:它不弹出任何内容,也不将任何内容推送到操作数堆栈,并且除了 PC 递增和收取 1 gas 之外,没有其他影响。

PC (0x58) 指令变为无效,并且其操作码未定义。

注意: 此更改意味着不再需要对 EOF 代码进行 JUMPDEST 分析。

执行

  1. 执行从第 0 个代码段的第一个字节开始,并将 PC 设置为 0
  2. 返回堆栈初始化为空。
  3. 不再执行堆栈下溢检查。注意: EOF 验证 EIP-5450 保证它在运行时不会发生。
  4. 不再执行堆栈溢出检查,除非在 CALLF 期间,如上所述。

理由

顶层帧中的 RETF 结束执行 vs 异常停止 vs 验证期间不允许

在顶层帧中,RETF 的替代逻辑可能是在代码验证期间允许它,并使其:

  • 如果返回堆栈被 RETF 清空,则结束执行,或者
  • 如果返回堆栈在 RETF 之前为空,则异常停止。

由于顶层帧(第 0 个代码段)不返回的验证规则(在单独的 EIP 中引入了不返回的段)已经取代了它,因为出于其他原因,验证函数的不返回状态本身很有价值。因此,所有对顶层帧中 RETF 的运行时行为的考虑都已过时。

“最小”函数类型

让我们考虑一个带有单个指令 RETF 的简单函数。 这样的函数具有 inputs = 0, outputs = 0 的“最小”类型。 但是,任何其他类型(例如 inputs = k, outputs = k)对于这样的函数也是有效的。 已经考虑过强制对所有函数使用“最小”类型。 这需要额外的验证规则,以检查函数中的任何指令是否访问了底部的堆栈操作数。 编译器可以遵守此规则,但这会导致相当大的麻烦。 另一方面,它为 EVM 实现提供了接近零的好处。 最后,已决定不强制执行此操作。

代码段限制和指令大小

代码段的数量限制为 1024。这需要 2 字节的 CALLF 立即数,并为将来增加限制留出空间。已经讨论了 256 的限制(1 字节立即数),并且有人担心它可能不够。

NOP 指令

我们没有弃用 JUMPDEST,而是将其重新用作 NOP 指令,因为 JUMPDEST 实际上是一个“无操作”指令,并且已经在各种上下文中使用。它对于某些链下工具可能很有用,例如,对 EVM 实现进行基准测试(NOP 指令的性能是 EVM 解释器循环的性能),作为强制代码对齐的填充,作为动态代码组合中的占位符。

弃用 JUMPDEST 分析

JUMPDEST 分析的目的是在代码中找到有效的 JUMPDEST 字节,这些字节恰好不在 PUSH 立即数数据中。只有动态跳转指令(JUMPJUMPI)才需要目标地址为 JUMPDEST 指令。相对静态跳转(RJUMPRJUMPIRJUMPV)没有此要求,并且在 EOF 指令验证期间,在部署时会验证相对静态跳转。因此,如果没有动态跳转指令,则不需要 JUMPDEST 分析。

向后兼容性

此更改不会对向后兼容性构成任何风险,因为它仅针对 EOF1 合约引入,对于 EOF1 合约,不允许部署未定义的指令,因此没有使用这些指令的现有合约。新指令不是为旧字节码(非 EOF 格式的代码)引入的。

新的执行状态和多段控制流不会对向后兼容性构成任何风险,因为它是执行单个代码段的概括。执行现有合约(包括旧版和 EOF1)没有用户可观察到的变化。

安全注意事项

引入指令的 gas 成本反映了在解释字节码时操作返回堆栈条目并跳转到内存中的正确代码段偏移量的需求。

在实现 EOF 容器验证算法时,需要仔细考虑这些新指令。

版权

通过 CC0 放弃版权及相关权利。

Citation

Please cite this document as:

Andrei Maiboroda (@gumb0), Alex Beregszaszi (@axic), Paweł Bylica (@chfast), "EIP-4750: EOF - 函数 [DRAFT]," Ethereum Improvement Proposals, no. 4750, January 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4750.