给 EVM 装一面溢出旗:EIP-1051 照搬 CPU 的进位与溢出标记思路 图 1
给 EVM 装一面溢出旗:EIP-1051 照搬 CPU 的进位与溢出标记思路 · 图 1

先补背景:EVM 的整数为什么会溢出

EVM 的每个字是 256 位无符号整数,所有加减乘默认按模 2 的 256 次方运算。两个数相加理想结果超过上限时,留下的只是低 256 位,值会直接绕回接近零的小数;减法借位过头则绕回接近上限的大数。这个行为本身是确定性的、所有节点一致的,不算故障——真正的危险在于合约逻辑把它当成了普通数值继续往下算。2018 年前后大量合约的安全缺陷正出自这里:余额计算被绕回,攻击者用一笔几乎为 0 的转账把账面余额推到天文数字。

EIP-1051 的方案:两面旗加两个读取指令

提案在 2018 年 5 月创建,状态 Stagnant。它往 EVM 状态里加两个旗标:无符号溢出旗标 ovf 和有符号溢出旗标 sovf。规则是:ADD、SUB、MUL 这三条算术指令执行时,如果无符号视角的理想结果越界,ovf 置位;sovf 包含 ovf 的全部场景,再额外覆盖有符号视角的越界,例如两个同号数相加得到异号结果这类经典判定。然后新增两个零参数操作码:OFV(编号 0x0c)压栈当前 ovf 旗标值并把它清零,SOVF(编号 0x0d)对 sovf 做同样的事。

用起来就像一段批处理:先连做一串算术,隔一段插一条 OFV,如果返回 1 再决定回滚、补偿还是报警。提案的动机段明确说了这个节奏——检查不必跟在每条指令后面,可以周期性进行。

为什么长得像 CPU

理据段落把设计来源写得很坦率:任何溢出方案都必须保住存量合约的行为,这就排除了直接改算术指令语义的路;另一条路是加一个开关,开启后溢出即抛错,但那限制了溢出的处理方式,只能一刀切地中止。提案选择复刻真实 CPU 的做法——主流处理器本来就带进位旗标和溢出旗标,软件在一段运算之后查询旗标即可。无符号与有符号要分两旗,是因为有符号溢出未必伴随无符号溢出,两者的判定条件不同。

它和其他溢出路线的关系

以太坊世界后来实际走通的防线在编译器层:Solidity 从 0.8 版本起内建检查算术,更早年代靠 SafeMath 库逐处包装。协议层也出现过另一条思路的提案,直接新增一组溢出即失败或置标志位的指令(例如带 SAFE 前缀的算术操作码一线),那是”每条指令自带保险”;EIP-1051 则是”指令不管、旗标集中管”。两条路线的取舍点很清楚:逐指令检查写得直白但字节码膨胀,旗标方案省指令但要求程序员记得隔段查询,忘了查就等于没有。EIP-1051 至今停在 Stagnant,没有进入任何主网升级,读它时请把它当作一条设计参照系,而不是现行规则。

一笔直觉账

设想合约里有一段循环累加上千个数字再一次性扣款。逐指令检查的写法要给上千次加法各配一条检查,Gas 和字节码双份开销;旗标方案把上千次加法裸跑完,末尾一条 OFV 收网——只要中途任何一次加越了界,旗标都还挂着 1,一次查询全部兜住。风险也在这里:如果这段中间有别的代码读了 OFV 并清零,旗标历史就被抹掉,后面的逻辑看到的是”没溢出”。旗标是共享状态,共享状态就有被误清的可能。

快速问答

问:旗标会被外部交易重置吗? 答:它是 EVM 执行期间的状态,不写进持久存储,每条交易从干净的执行环境开始,不存在跨交易遗留。

问:sovf 和 ovf 为什么不能合成一面旗? 答:同一次运算在无符号视角正常、有符号视角越界的情况真实存在(大数相加绕回被读成负数就是例子),单旗会漏判。

问:今天部署合约还要注意溢出吗? 答:用主流编译器内建检查之后常规算术自带保护;手写内联汇编、非常规类型转换仍是需要人盯的缝隙,这条提案讨论的风险在那条缝里依然成立。

风险提示:本文只做协议机制说明,不构成投资建议;合约代码安全请以当期编译器与审计实践为准。