EIP-8219 详解:在 EVM 层原生实现检查算术(Checked Arithmetic)操作码
EIP-8219 详解在 EVM 层原生实现检查算术Checked Arithmetic操作码【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPsEIP-8219 是一项处于 Draft 状态的 Ethereum Core 提案它向 EVM 引入SAFEADD0x0c、SAFESUB0x0d、SAFEMUL0x0e、SAFEDIV0x0f四个自带溢出、下溢与除零检查的无符号 256 位算术操作码出错时直接以空 returndata 回滚当前调用帧等价于REVERT(0, 0)。阅读本文后你将掌握该提案的完整规范、单条指令即可替代编译器多指令安全检查模式的原理、与 EIP-1051 / EIP-6888 两类旗标方案的优劣对比以及官方给出的逐操作码测试用例与 Gas 定价依据。背景安全检查为何成为智能合约的沉重负担高层 EVM 语言默认都会在算术运算周围生成溢出检查Solidity 自 v0.8.0 起、Vyper 自诞生起即如此。这些检查对正确性至关重要但 EVM 本身不提供任何原生支持编译器只能围绕每一次算术操作合成多条指令的检查模式。一个典型的检查加法会编译为类似如下的序列跳转到安全加法子例程复制操作数执行加法检查是否溢出跳回调用点这段序列至少包含一次条件跳转还可能包含更多跳转。原始ADD操作码仅消耗 3 Gas而一次编译器检查加法大约消耗 79 Gas——纯粹为了安全性付出了约 25 倍的代价而且这个开销会乘以网络中每个合约、每个函数、每次算术操作累积放大。基准测试数据EIP-8219 使用 Solidity 0.8.33开启优化器、200 runs与 Vyper 0.4.3在等价的 ERC-20 合约上隔离单次检查加法进行基准测试结果如下指标Solidity 0.8.33Vyper 0.4.3使用SAFEADD每次检查加法的执行 Gas~793 76~413 385每次检查加法的字节码12 字节14 字节1 字节每次检查加法的部署 Gas2,3522,604~200Gas 缩减比例93.7%87.8%—需要说明的是上表的字节码与部署数据是单次检查加法的开销。基准合约中每个包含两次一次在transfer、一次在transferFrom因此整个合约维度的总量Solidity 为 22 字节与 4,704 部署 GasVyper 为 26 字节与 5,208 部署 Gas是单次的两倍执行 Gas 不受影响因为一次调用只会执行其中一个。在 Solidity 中单次加法的成本是调用点的展开成本被跳入的溢出检查辅助函数只发射一次由合约内所有检查加法共享。危险的选择为了省 Gas 放弃安全由于检查算术昂贵开发者和审计者长期面临绕过它的压力。Solidity 提供unchecked {}块Vyper 提供unsafe_add()、unsafe_sub()等相关内建函数。这些机制有时是合适的例如可证明不会溢出的循环计数器但它们的存在催生了一种危险权衡开发者在安全性未被证明的情况下仅仅为了省 Gas 就使用非检查算术。这种激励导致的后果包括代码可读性与可审计性下降充斥着unchecked/unsafe真实世界中存在因未检查减法被利用的攻击事件。原生检查算术操作码可以消除这一权衡当一次安全加法只比原始加法贵 2 Gas5 vs 3、且字节码体积不再增加时就没有任何有意义的理由去关闭溢出保护。与既往方案的对比为什么旗标Flag方案不够好在 EVM 层面做溢出检测此前已有两项提案均收录于本仓库EIP-1051: Overflow checking for the EVM——由现有算术操作码设置ovf与sovf旗标并提供OFV0x0c与SOVF0x0d操作码来检查并清除旗标。注意其0x0c/0x0d槽位恰好是 EIP-8219 希望使用的编号范围。EIP-6888: Arithmetic verification at EVM level——引入carry与overflow两个旗标配合JUMPC、JUMPO条件跳转操作码其旗标作用域与程序计数器相同且JUMPC/JUMPO的 Gas 定价为与JUMPI相同的G_high。两项提案共享同一个根本局限仍然需要多条指令的模式。每次算术操作之后合约必须执行一次读取旗标的操作码外加一次条件跳转——至少 3 条指令而非 1 条。此外它们都为 EVM 引入了跨指令存续的隐式状态旗标给静态分析、形式化验证和编译器优化带来复杂性。旗标还会造成微妙的顺序依赖如果合约连续执行两次加法却只检查一次旗标第一次溢出会被静默丢失。EIP-8219 采取完全不同的路线每个安全操作码都是自包含、无状态的单条指令——要么产生正确结果要么回滚。没有旗标、没有隐式状态、没有多指令模式一个操作码完成了过去许多操作码的工作。规范四个新的检查算术操作码参数与总体布局常量值FORK_BLOCKTBD待定四个新操作码位于0x0c–0x0f区间紧跟在现有算术操作码组ADD至SIGNEXTEND即0x01–0x0b之后。这些槽位目前均未分配。操作码助记符Gas说明0x0cSAFEADD5检查无符号加法溢出时回滚0x0dSAFESUB5检查无符号减法下溢时回滚0x0eSAFEMUL7检查无符号乘法溢出时回滚0x0fSAFEDIV7检查无符号除法除零时回滚回滚行为当任意安全算术操作码检测到错误条件溢出、下溢或除零时执行 MUST 以空 returndata 回滚当前调用帧语义上等价于执行REVERT(0, 0)。SAFEADD0x0c输入栈top - 0 为atop - 1 为b。输出栈top - 0 为a b。伪代码result a b (mod 2^256) if result a: revert(0, 0) push result错误条件当a b 2^256 - 1无符号溢出时回滚。这里借助无符号加法溢出当且仅当结果小于任一操作数这一性质完成检测仅需一次比较。SAFESUB0x0d输入栈top - 0 为atop - 1 为b。输出栈top - 0 为a - b。 栈顺序与现有SUB操作码一致a在栈顶b在下结果为a - b。伪代码if b a: revert(0, 0) push a - b错误条件当b a无符号下溢时回滚。SAFEMUL0x0e输入栈top - 0 为atop - 1 为b。输出栈top - 0 为a * b。伪代码if a 0: push 0 else: result a * b (mod 2^256) if result / a ! b: revert(0, 0) push result错误条件当a ! 0且a * b 2^256 - 1无符号溢出时回滚当a 0时结果恒为 0无论b取值如何都不回滚。溢出检测利用乘法逆运算若(a * b) / a ! b则必然发生了截断即溢出。SAFEDIV0x0f输入栈top - 0 为atop - 1 为b。输出栈top - 0 为a / b。 栈顺序与现有DIV操作码一致a为被除数在栈顶b为除数。伪代码if b 0: revert(0, 0) push a / b (unsigned integer division, truncating)错误条件当b 0除零时回滚。无符号整数除法不可能溢出因此不检查溢出。设计理由Rationale为什么直接回滚而非使用旗标EIP-1051 与 EIP-6888 的旗标方案存在三个缺陷多指令开销检查旗标仍至少需要两条额外指令读旗标 条件跳转只是缩减而非消除检查算术的开销。隐式状态旗标引入了在指令之间存续的隐藏状态使静态分析、形式化验证和编译器优化传递复杂化。旗标静默丢失若在两次旗标检查之间执行了多次算术操作较早的溢出条件会被静默覆盖在手写汇编与编译器输出中都会产生一类微妙的缺陷。直接回滚的操作码无状态、自包含不会静默丢失错误条件同时由于无需任何额外指令而提供了最大限度的 Gas 节省。回滚而非异常停机Exceptional HaltEVM 内部的异常停机如执行INVALID会耗尽全部剩余 Gas而这些操作码触发的是回滚不会耗尽全部 Gas。这一区别很重要因为检查算术常用于滑点检查等守卫场景——在那里回滚路径是正常预期的结果若耗尽全部 Gas 将显得过度惩罚。为什么要占用四个操作码要在单条操作码内完成基础算术运算的目标恰好需要四个新操作码。虽然代价不小但这是合理的新部署合约对这四个操作码的预期高频率使用可以佐证这一点。EIP 作者在 Devcon VII 之前进行的分析见 Devcon 演讲 EVM Charts 2024: Whats hot? Whats not? by Dominic Bruetsch | Devcon SEA发现现有四个对应操作码位居使用量 Top-35加之检查算术如今已是 Solidity 与 Vyper 的默认行为构成了对高频使用的强烈预期。操作码放置位置0x0c–0x0f紧接在现有算术组0x01ADD至0x0bSIGNEXTEND之后。这种连续放置体现了它们与现有算术操作码的语义关联也简化了使用跳转表或操作码范围检查的 EVM 解释器的实现。四个槽位当前均未分配。Gas 成本依据SAFEADD与SAFESUB定价 5 Gas底层算术操作的基础成本3 GasGverylow组加上 2 Gas 的检查费比较与条件回滚这 2 Gas 溢价保守地覆盖了额外执行开销。SAFEMUL与SAFEDIV定价 7 Gas基础成本5 GasGlow组加上 2 Gas 检查费。SAFEMUL内部需要一次除法来验证结果SAFEDIV需要对除数做一次零比较。相对底层操作两项检查的计算开销都很小。现有MUL与DIV操作码本就要花 5 Gas因此 2 Gas 溢价与SAFEADD/SAFESUB保持一致。为什么只做无符号 256 位本 EIP 只定义无符号安全算术。有符号溢出检测二进制补码涉及不同的边界条件与边角情况例如INT256_MIN / -1会溢出。为避免让本提案超载有符号变体SSAFEADD、SSAFESUB、SSAFEMUL、SSAFEDIV推迟到配套 EIP 中提出。无符号算术覆盖了绝大多数智能合约运算——Solidity 的uint256与 Vyper 的uint256是主导性数值类型这也是不考虑更小数据类型的理由。为什么包含 SAFEDIV现有DIV操作码在除数为零时会静默返回 0。虽然这是明确定义的行为但几乎从不是期望的语义——除零几乎总是逻辑错误。如今编译器会在DIV之前发射显式零检查以在除零时回滚。SAFEDIV消除了这个模式提供与其他安全操作码相同的 Gas 与字节码节省。为什么以空 returndata 回滚安全算术操作码以空 returndata零长度返回缓冲区回滚而不是编码特定错误消息。这一设计对编译器中立EVM 规范不强制 ABI 编码不同语言使用不同的错误编码方案。空 returndata 也与 Vyper 当前实现溢出回滚的方式REVERT(0, 0)一致Solidity 则会编码Panic(0x11)载荷——这些操作码用该载荷换取了 Gas 与字节码的节省。向后兼容性0x0c、0x0d、0x0e、0x0f四个操作码当前未分配执行时表现为INVALID耗尽全部 Gas。因此该 EIP 的引入不破坏任何现有合约所有现有算术操作码ADD、SUB、MUL、DIV等完全不变使用unchecked {}Solidity或unsafe_add()Vyper的合约继续发射原始ADD、SUB、MUL、DIV操作码语义不变编译器通过针对分叉后的 EVM 版本编译来选择启用新操作码为更早 EVM 版本编译的合约继续以完全相同的方式运行。测试用例以下所有测试用例均基于无符号 256 位整数。MAX表示2^256 - 1即0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff。“Reverts” 表示操作码触发带空 returndata 的回滚且不产生栈输出。SAFEADD#ab期望结果Gas112352010010053100010054MAX - 11MAX55MAX1Reverts561MAXReverts57MAXMAXReverts582^1282^128 - 12^129 - 1590005SAFESUB#ab期望结果Gas15325210010005310001005401Reverts550MAXReverts5612Reverts57MAXMAX058MAX0MAX5SAFEMUL#ab期望结果Gas13412720MAX073MAX00741MAXMAX75MAX1MAX762^1282^128Reverts772^1282^128 - 12^256 - 2^12878MAX2Reverts790007SAFEDIV#ab期望结果Gas11033721010173100Reverts740507500Reverts76MAX1MAX77MAXMAX1781MAX07这些用例覆盖了每个操作码的边界SAFEADD与SAFEMUL验证恰好等于MAX时不回滚、超过MAX时回滚SAFEMUL专门覆盖了a 0时结果为零且不回滚的边角情况SAFEDIV则同时验证了0 / 5 0与0 / 0回滚两条路径可用于实现端的差分测试与共识测试。安全考量Gas 定价这些操作码的 Gas 成本被保守地定得高于其非检查对应物以避免定价过低、防止资源耗尽型攻击向量。更可读、更安全的智能合约一旦被采纳这些操作码让开发者能够写出更可读、更安全的代码而无需承受当前检查算术所带来的权衡。当安全加法只比原始加法贵 2 Gas、且不再增加字节码体积时用非检查算术省 Gas的动机将不复存在这从激励机制层面整体提升了网络上的合约安全性。小结EIP-8219 以四个自包含、无状态的新操作码把 Solidity/Vyper 默认启用的安全检查从编译器合成的多指令模式压缩为单条原生指令执行 Gas 从约 79/41 降至 5字节码从 12/14 字节降至 1 字节部署 Gas 每处从 2,352/2,604 降至约 200。相比 EIP-1051 与 EIP-6888 的旗标方案直接回滚设计避免了隐式状态与静默旗标丢失问题也天然兼容 Vyper 现有的REVERT(0, 0)语义。该提案当前仍为 Draft 状态FORK_BLOCK待定且明确将带符号变体SSAFE*留给配套 EIP——读者可继续深入本仓库中的 EIP-1051、EIP-6888 原文以及 LICENSE 了解版权约定。【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考