Solidity RSA 签名详解:在 Gas 效率上击败 ECDSA 与默克尔树
目录
- 一、概览
- 二、RSA 签名如何工作
- 三、Gas 成本对比
- 四、为什么 RSA 能赢 ECDSA
- 五、技术实现:modexp 预编译
- 六、公钥存储难题与变形合约
- 七、访问列表优化
- 八、密钥长度与安全
- 九、要点与局限
- 十、总结
- 十一、动手练习项目:RSA 白名单空投 RsaAllowlist
一、概览
本文把 RSA 签名作为 ECDSA 和默克尔树之外、用于预售白名单和空投的更省 Gas 的替代方案。核心洞察:借助模幂预编译(0x05),RSA 验证在链上出乎意料地便宜。
二、RSA 签名如何工作
RSA 依赖大整数分解的计算困难性。
密钥生成:
- 选两个大素数
p、q; - 计算
n = p × q(公共模数); - 选小素数
e(可硬编码为 3); - 计算
d = e⁻¹ mod t,其中t = (p-1)(q-1)(私钥); - 公钥
(n, e),私钥d。
签名:签名者对消息 m 哈希后计算 s = h(m)^d mod n。
验证:验证者检查 s^e mod n == hash(消息)。
用于白名单:签名变成 s = buyerAddress^d mod n(项目方对买家地址签名),链上验证 msg.sender == s^e mod n。买家提交 s,合约算 s^e mod n 看是否等于自己的地址——等于就说明项目方授权过这个地址。
三、Gas 成本对比
| 方案 | Gas |
|---|---|
| ECDSA | 29,293(不含交易发起 8,293) |
| 默克尔树 | 30,517+(随树增大) |
| RSA-896 | 26,850 |
| RSA-1024 | 27,033 |
| RSA-2048 | 29,271 |
RSA-896 比 ECDSA 省约 2,500 gas,比默克尔树省更多。
四、为什么 RSA 能赢 ECDSA
效率来自模幂预编译(0x05)的定价方式:
预编译给”把数字升到低次幂”赋予很低的价格,执行成本只有几百 gas。
因为 e 可以是 3 这样的小数,s^3 mod n 极便宜。主要的 Gas 开销其实在 calldata:1024 位密钥下签名占 128 字节,按 16 gas/字节 = 2,048 gas。但即便如此,总量仍低于 ECDSA 的总开销。
五、技术实现:modexp 预编译
以太坊原生只支持 256 位运算,RSA 需要大数运算(1024/2048 位),所以用 EIP-198 引入的 0x05 预编译做模幂:
- 把 base、exponent、modulus 按 ABI 编码格式载入内存(先三个长度,再三个值);
- 调用地址 0x05;
- 从内存取结果。
(具体调用格式见上一篇预编译的 modExp 例子。)
六、公钥存储难题与变形合约
难题:1024 位 RSA 公钥需要 4 个存储槽,用 SLOAD 读取要 8,400 gas——已经超过 ECDSA 的优势了。
解法:把公钥存在合约的字节码里,而不是存储。用 EXTCODECOPY 读取只要 2,600 gas,远低于 8,400。
但白名单需要可作废能力(换一批白名单)。字节码不可变怎么办?用变形合约(metamorphic contract):
- 用 CREATE2 部署一个确定性地址的合约;
- 合约字节码含:(a) 自毁机制 + (b) RSA 公钥;
- 把合约地址存在 immutable 变量;
- 用 EXTCODECOPY 读公钥(2,600 gas);
- 作废时:触发自毁,在同一地址重新部署含新公钥的合约。
地址不变是因为 CREATE2 取决于初始化代码而非运行时代码——这就实现了”变形”(同地址换内容)。
注意:EIP-6780 后 selfdestruct 行为受限,变形合约模式在新版以太坊上可能不再可行,需用其他方式(如代理)实现可作废。
七、访问列表优化
EIP-2930 访问列表(Module 9)可再省约 100 gas。因为这个设计要读外部合约(存公钥的那个),可以预声明会访问的外部地址来预热(呼应访问列表篇)。
八、密钥长度与安全
密钥越大,分解难度指数级增长:
- 已被分解的最大 RSA 密钥:829 位;
- 分解成本:约 940 万美元(云超算);
- 行业共识:896 位对大多数价值 < $100,000 的代币足够安全。
理由:每多 1 位,数字规模翻倍。攻击者的成本收益分析使大多数 NFT/代币预售成为不划算的攻击目标。但高价值场景应用 2048 位。
九、要点与局限
要和现有方案竞争,方案必须:
- 能作废(像 ECDSA 换签名地址、默克尔树换根);
- 最小化卖方开销(避免基于 mapping 白名单的百万级 gas);
- 成本低于 8,200 gas(含存储读取);
- 安全参数。
局限:文章明确这是概念验证(PoC),建议生产前谨慎。它未处理 PKCS 填充等标准化问题——真实 RSA 签名需要正确的填充方案防止伪造,这是 PoC 省略但生产必须补的。
十、总结
- RSA 用
s^e mod n == hash(地址)验证白名单,e 取小值(如 3)让模幂极便宜; - RSA-896 约 26,850 gas,省过 ECDSA(29,293)和默克尔树(30,517+);
- 主要开销在 calldata(签名 128 字节),计算靠 0x05 modexp 预编译;
- 公钥存字节码(EXTCODECOPY 2,600)而非存储(SLOAD 8,400);
- 可作废靠变形合约(CREATE2 + selfdestruct 重部署,注意 EIP-6780 限制);
- 是 PoC,未处理 PKCS 填充,生产需谨慎。
十一、动手练习项目:RSA 白名单空投 RsaAllowlist
项目目标
实现一个用 RSA 签名做白名单的空投合约,链下用 RSA 私钥对地址签名、链上用 modexp 预编译验证,并与 ECDSA 版本对比 Gas。部署到 Sepolia。
合约要求
1. 链下密钥与签名脚本(Python/JS)
- 生成 RSA 密钥对(e=3 或 65537,n 取 1024 位);
- 对白名单里每个地址签名:
s = address^d mod n(或对hash(address)签,更安全); - 输出每个地址的签名 s(128 字节)。
2. RsaAllowlist.sol
bytes public modulus;(n,存字节码或 immutable 引用——简化版可先存储,进阶再优化);uint256 public constant E = 3;(或 65537);claim(bytes calldata signature) external:- 用 modexp 预编译算
recovered = signature^E mod n; requirerecovered 对应的地址 ==msg.sender(或 == hash(msg.sender));mapping claimed防重复,发空投;
- 用 modexp 预编译算
verify(address account, bytes calldata sig) public view returns (bool):暴露验证逻辑便于测试。
3. EcdsaAllowlist.sol(对照)
- 用 ecrecover + 项目方签名地址做同样的白名单,便于 Gas 对比。
测试要求(Foundry)
test_ValidSignatureClaims:用链下生成的合法 RSA 签名,对应地址能领取;test_InvalidSignatureReverts:错误签名 verify 返回 false、claim revert;test_WrongAccountReverts:A 的签名给 B 用,revert(签名绑定地址);test_NoDoubleClaim:重复领取 revert;test_ModExpCorrect:单测signature^E mod n计算正确(用已知小数验证 modexp 逻辑);test_GasVsEcdsa:对比 RSA claim 和 ECDSA claim 的 Gas,记录差异(RSA-1024 应在 27,000 量级);test_DifferentKeySizes:用 896/1024/2048 位密钥各测一次,对比 calldata 增大带来的 Gas 变化。
Sepolia 部署与验证步骤
- 链下生成密钥、对你的测试地址签名;
- 部署 RsaAllowlist(传入 modulus n)和 EcdsaAllowlist;
- 用 RSA 签名调 claim,Etherscan 上观察 Gas;
- 用 ECDSA 版本领同样的空投,对比 Gas;
- 故意用别人的签名尝试,观察 revert。
进阶挑战(可选)
- 把公钥从存储改到字节码(部署一个只含公钥的合约,用 EXTCODECOPY 读),实测 SLOAD(8400) vs EXTCODECOPY(2600) 的 Gas 差;
- 加上正确的 PKCS#1 v1.5 填充验证,理解为什么裸 RSA(无填充)不安全;
- 写注释回答:为什么 e 取小值(3)能让验证便宜?为什么 calldata 是 RSA 方案的主要 Gas 开销?