Alert Source Discuss
⚠️ Review Standards Track: Core

EIP-4200: EOF - 静态相对跳转

带有有符号立即数的 `RJUMP`、`RJUMPI` 和 `RJUMPV` 指令,用于编码跳转目标

Authors Alex Beregszaszi (@axic), Andrei Maiboroda (@gumb0), Paweł Bylica (@chfast)
Created 2021-07-16
Requires EIP-3540, EIP-3670

摘要

引入了三个新的 EVM 跳转指令(RJUMPRJUMPIRJUMPV),它们将目标编码为有符号立即数。 这些指令在大多数(但不是全部)用例中都很有用,并且可以降低成本。

动机

一个反复出现讨论的话题是 EVM 只有一种用于动态跳转的机制。 它们提供了一个非常灵活的架构,只有 2 (!) 条指令。 然而,这种灵活性是有代价的:它使代码分析更加复杂,并且(部分地)导致了需要 JUMPDEST 标记。

在很多情况下,控制流实际上是静态的,不需要任何动态行为,尽管并非所有用例都可以通过静态跳转来解决。

有多种方法可以减少对动态跳转的需求,一些例子:

  1. 通过对函数/子程序的原生支持
  2. “返回调用者”指令
  3. 具有动态索引的“switch-case”表

此更改并未试图解决这些问题,而是引入了一个最小的功能集,以允许编译器决定哪个选项最适合给定的用例。 预计编译器将几乎专门使用 RJUMP / RJUMPI,但返回调用者的情况将继续使用 JUMP

此功能并不妨碍 EVM 以后引入其他形式的控制流。 RJUMP / RJUMPI 可以有效地与更高级别的函数声明共存,其中静态相对跳转应用于函数内控制流。

这些指令的主要好处是降低了 gas 成本(在部署和执行时)并改善了分析特性。

规范

我们在激活 EIP-3540 的同一区块号上引入了三个新指令:

  1. RJUMP (0xe0) - 相对跳转
  2. RJUMPI (0xe1) - 条件相对跳转
  3. RJUMPV (0xe2) - 通过跳转表相对跳转

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

如果代码是有效的 EOF1:

  1. RJUMP relative_offsetPC 设置为 PC_post_instruction + relative_offset
  2. RJUMPI relative_offset 从堆栈中弹出一个值(condition),并将 PC 设置为 PC_post_instruction + ((condition == 0) ? 0 : relative_offset)
  3. RJUMPV max_index relative_offset+ 从堆栈中弹出一个值(case),并将 PC 设置为 PC_post_instruction + ((case > max_index) ? 0 : relative_offset[case])

立即数参数 relative_offset 被编码为 16 位有符号(二进制补码)大端值。 根据 PC_post_instruction,我们指的是整个立即数值之后的 PC 位置。

RJUMPV 的立即数编码更加特殊:无符号 8 位 max_index 值确定跳转表中的最大索引。 后面的 relative_offset 值的数量是 max_index+1。 这允许最大 256 的表大小。RJUMPV 的编码必须至少有一个 relative_offset,因此它将至少占用 4 个字节。 此外,case > max_index 条件的失败意味着在许多用例中,人们会将 默认 路径放置在 RJUMPV 指令之后。 一个有趣的特性是 RJUMPV 0 relative_offset 是一个反转的 RJUMPI,在许多情况下可以使用它来代替 ISZERO RJUMPI relative_offset

我们还扩展了 EIP-3670 的验证算法,以验证每个 RJUMP / RJUMPI / RJUMPV 都有一个指向指令的 relative_offset。 这意味着它不能指向 PUSHn / RJUMP / RJUMPI / RJUMPV 的立即数数据。 它不能指向代码边界之外。 允许指向 JUMPDEST,但不是必需的。

因为目标是预先验证的,所以这些指令的成本低于它们的动态对应指令:RJUMP 应该花费 2,而 RJUMPIRJUMPV 应该花费 4。

原理

相对寻址

我们选择相对寻址是为了支持可重定位的代码。 这也意味着可以注入代码片段。 在此 EIP 之前,已经看到一种用于实现相同目标的技术是注入诸如 PUSHn PC ADD JUMPI 之类的代码。

我们看不到相对寻址有任何明显的缺点,它也允许我们弃用 PC 指令。

立即数大小

有符号 16 位立即数意味着可能的最大跳转距离是 32767。如果 PC=0 处的字节码以 RJUMP 开头,则可以跳转到 PC=32770

给定 MAX_CODE_SIZE = 24576(在 EIP-170 中)和 MAX_INITCODE_SIZE = 49152(在 EIP-3860 中),我们认为 16 位立即数足够大。

具有 8 位立即数的版本仅允许将 PC 向后移动 125 个字节或向前移动 127 个字节。 虽然对于许多 for 循环来说,这似乎是一个足够好的距离,但对于跨函数跳转来说,它可能不够好。 另外,由于 16 位立即数与动态跳转在此类情况下的占用空间大小相同(3 字节:JUMP PUSH1 n),因此我们认为指令越少越好。

如果需要具有其他大小(例如 8 位、24 位或 32 位)的立即数编码,则可以引入新的操作码,类似于存在多个 PUSH 指令的方式。

PUSHn JUMP 序列

如果我们选择绝对寻址,那么 RJUMP 可以被视为类似于序列 PUSHn JUMP(并且 RJUMPI 类似于 PUSHn JUMPI)。 在这种情况下,有人可能会争辩说,应该对这些序列进行折扣,而不是引入新指令,因为 EVM 可以优化它们。

我们认为这是一个糟糕的方向:

  1. 它进一步复杂化了已经复杂的 gas 计算规则。
  2. 并且它要么需要共识定义的 EVM 代码内部表示,要么强制 EVM 实现自行进行优化。

这两者都存在风险。 此外,我们认为 EVM 实现应该可以自由选择它们应用的优化,并且节省的成本不需要以任何代价传递下去。

此外,它需要对当前实现进行潜在的重大更改,这些实现依赖于没有前瞻的逐个流式执行。

与动态跳转的关系

目标不是完全取代 EVM 当前的控制流系统,而是对其进行扩充。 在许多情况下,动态跳转很有用,例如返回调用者。

可以引入一种新机制,用于拥有预定义的有效跳转目标表,并动态提供此表中的索引,以完成某种形式的动态跳转。 这对于有效编码某种形式的“switch-case”语句非常有用。 它也可以用于“返回调用者”的情况,但这可能效率低下或者很尴尬。

缺少 JUMPDEST

JUMPDEST 有两个目的:

  1. 为了有效地划分代码——这对于预先计算给定(即 JUMPDEST 之间的指令)的总 gas 使用量以及 JIT/AOT 转换非常有用。
  2. 为了明确显示有效位置(否则任何非数据位置都将有效)。

静态跳转不需要此功能,因为分析器可以在 jumpdest 分析期间轻松地区分静态跳转立即数中的目标。

这里有两个好处:

  1. 不浪费一个字节用于 JUMPDEST 也意味着每次跳转目标在部署期间节省 200 gas。
  2. 鉴于 JUMPDEST 本身花费 1 gas 并且在跳转期间被“执行”,因此每次跳转在执行期间节省额外的 1 gas。

RJUMPV 回退情况

如果在 RJUMPV 指令执行中未找到匹配项(即默认情况),执行将继续而不会分支。 这允许在参数中填充 0,并允许程序员选择实现方式。 替代选项包括在没有匹配项时异常中止。

向后兼容性

此更改不会对向后兼容性构成任何风险,因为它是在 EIP-3540 的同时引入的。 新指令未针对旧版字节码(非 EOF 格式的代码)引入。

测试用例

验证

有效情况

  • JUMPDEST 作为目标的 RJUMP / RJUMPI / RJUMPV
    • relative_offset 为正/负/0
  • JUMPDEST 以外的指令作为目标的 RJUMP / RJUMPI / RJUMPV
    • relative_offset 为正/负/0
  • RJUMPV 具有从 1 到 256 的各种有效表大小
  • RJUMP 作为代码段中的最后一条指令

无效情况

  • 具有截断立即数的 RJUMP / RJUMPI / RJUMPV
  • RJUMPI / RJUMPV 作为代码段中的最后一条指令
  • RJUMP / RJUMPI / RJUMPV 目标超出代码段边界
  • RJUMP / RJUMPI / RJUMPV 目标推送数据
  • RJUMP / RJUMPI / RJUMPV 目标另一个 RJUMP / RJUMPI / RJUMPV 立即数参数

执行

  • 旧代码中的 RJUMP / RJUMPI / RJUMPV 中止执行
  • RJUMP
    • relative_offset 为正/负/0
  • RJUMPI
    • relative_offset 为正/负/0
      • condition 等于 0
      • condition 不等于 0
  • RJUMPV 0 relative_offset
    • case 等于 0
    • case 不等于 0
  • RJUMPV 具有包含正、负、0 偏移的表
    • case 等于 0
    • case 不等于 0
    • case 超出表边界 (case > max_index, 回退情况)
    • case > 255

安全考虑

实现 EOF 容器验证算法时,应仔细考虑添加带有立即数参数的新指令。

静态相对跳转执行不需要运行时检查跳转目标。 它大大降低了执行成本。 因此,新指令的 gas 成本也可以大大降低。

RJUMPV 指令相对偏移表最多可以有 256 个单字节条目,因此读取偏移量不可能是潜在的攻击面。

版权

CC0 下放弃版权及相关权利。

Citation

Please cite this document as:

Alex Beregszaszi (@axic), Andrei Maiboroda (@gumb0), Paweł Bylica (@chfast), "EIP-4200: EOF - 静态相对跳转 [DRAFT]," Ethereum Improvement Proposals, no. 4200, July 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4200.