给模数运算开预编译:EIP-198 让合约跑 RSA 验证 图 1
给模数运算开预编译:EIP-198 让合约跑 RSA 验证 · 图 1

智能合约里最不起眼也最基础的一块算术,2017 年之前其实跑不动:大整数的模幂。EIP-198(定稿于拜占庭时期,状态 Final)把这块运算做成了预编译合约——以太坊地址 0x05 从「空地址」变成一条内置指令,收到三个数就返回「底数的指数次幂、对模数取余」,客户端用原生代码直接算。这个看似平凡的加法,是后来所有链上非对称密码学的地基之一。

输入格式:三段大数的信封

预编译的输入不是三个字段,而是一封自描述的信封:开头 32 字节声明底数长度,再来 32 字节声明指数长度、32 字节声明模数长度;随后按声明依次附上三段大整数,每段右对齐、不足补零。输出长度等于模数长度。提案给了完整测试向量,其中最经典的一组是费马小定理的直接体现:输入底数 3、指数 2、模数 5,输出 9——因为 3 的平方是 9,对 5 取余仍是 9 本身;同组测试里还有一条 2048 位级别的大数用例,那就是提案真正关心的工作负载。长度字段前置的设计让这个预编译能接受任意宽度的输入,代价是校验逻辑要多写几行——规范专门规定长度异常时按零补齐处理,保证任何输入都有确定性的输出。

给模数运算开预编译:EIP-198 让合约跑 RSA 验证 图 2
给模数运算开预编译:EIP-198 让合约跑 RSA 验证 · 图 2

Gas 公式:从基准测试倒推

定价是这份 EIP 的技术含量所在。成本模型基于「大数乘法次数」:设 max_base_mod 为底数与模数长度的较大者(字节),iteration_count 是长度除以 8 向上取整,指数部分按位处理——每读到 1 比特做一次乘法、做一次平方,读 0 比特也要做平方,提案把这个折算写成一段可读的 Python 伪码。最终费用等于乘法次数乘以 Gquaddivisor(提案定值的除数常数),再向上取整。所有参数的标定来自实测基准:作者团队跑不同位宽的模幂,把执行时间拟合到这个公式上,让每单位 Gas 对应的原生运算成本与 EVM 其他运算保持可比。摘要里那句「在预编译内,2048 位模幂消耗约 44 万 Gas」就是给 RSA 场景的直白报价:按当时区块上限,验证一次 2048 位 RSA 签名完全负担得起,而在纯 EVM 里同一件事会烧穿整块 Gas。

为什么 EVM 自己算不动

合约层面的模幂要靠循环调用乘法预置库,每个 256 位 limbs 的乘法、进位、归约都是真实的字节码指令;2048 位宽意味着每个数拆成 8 个 limb,schoolbook 乘法一轮就是几十条指令,整条蒙哥马利链跑下来是数十万条。预编译路线把这些全部折叠进客户端的一次原生调用,还顺带获得了实现优化空间(大数库、快速模约)。这条路线的哲学代价也值得记录:预编译是「不透明件」,客户端各自优化、共识只认输入输出,调试与可证明性都比纯字节码难——这也是多年后社区讨论「哪些预编译值得 EVM 化」的背景音。

后续命运

EIP-198 之后,0x05 地址一路成为更重型的密码学件:以太坊的配对运算预编译建立在同一套机制上,而模幂自身的计费公式被后续提案(调整乘法次数估计、封顶输入宽度)反复修订,价格表属于活文档。对读者的实际含义是:地址与运算语义没变过,变的只是价格与上限,凡涉及具体 Gas 数字都应查当期规范。

快速问答

问:预编译会被硬分叉改掉吗? 答:语义改动需要共识变更,历史上只调过定价与输入上限,接口稳定。

问:普通合约什么时候用得到它? 答:验 RSA 签名、跑某些门限密码协议、或者需要大数取幂的自定义方案;日常 ERC-20 用不到。

问:为什么不干脆让 EVM 支持任意宽度整数? 答:那会改变全部算术操作码的复杂度模型,牵一发动全身;定点加预编译是低成本路径。

常见误区

一是把模幂理解成普通幂运算,忘记模数决定了输出长度与整个成本模型;二是把提案里的 Gas 常数当成永恒,定价表是活文档;三是以为预编译「免费」,它只是把成本从字节码层搬进了原生层计价。

风险提示:本文解释协议原语,不构成投资建议;当前计费参数以以太坊当期规范为准。