Solidity 预编译合约详解
目录
一、什么是预编译合约
以太坊预编译的行为就像内置在以太坊协议里的智能合约。
它们位于地址 0x01 到 0x0a,是一些被认为足够重要、值得用高效原生实现的加密操作。这些操作在协议层执行(而非在 EVM 字节码里跑),因此比用 Solidity 实现高效得多。
为什么需要它们?某些运算(如椭圆曲线配对、模幂)如果用 Solidity/EVM 操作码硬写,会消耗天文数字的 Gas,甚至超出区块上限。把它们做成预编译,价格固定且便宜。
二、四大用途
- 椭圆曲线数字签名恢复(ecrecover);
- 哈希方法(兼容 Bitcoin、Zcash);
- 内存操作(拷贝);
- 椭圆曲线数学(支撑零知识证明系统)。
三、完整预编译目录(0x01–0x0a)
| 地址 | 名称 | 功能 |
|---|---|---|
| 0x01 | ecRecover | 从哈希和签名 (v,r,s) 恢复签名者地址 |
| 0x02 | SHA-256 | SHA-256 哈希(兼容 Bitcoin 交易验证) |
| 0x03 | RIPEMD-160 | RIPEMD-160 哈希,输出 20 字节(Bitcoin 地址用) |
| 0x04 | Identity | 高效拷贝连续内存(EVM 没有原生 memcpy) |
| 0x05 | Modexp | 模幂 (base^exp) mod modulus(RSA 的核心) |
| 0x06 | ecAdd | BN-128 曲线上两点相加(ZK) |
| 0x07 | ecMul | 曲线点乘标量(ZK) |
| 0x08 | ecPairing | 配对验证(ZK 证明校验) |
| 0x09 | Blake2 | Blake2 哈希(Zcash 偏好) |
| 0x0a | Point Evaluation | KZG 承诺验证(EIP-4844 blob 扩容) |
0x01 ecRecover(重要陷阱)
从签名恢复地址。关键陷阱:验证失败时它返回零地址而非 revert,开发者必须显式检查:
address signer = ecrecover(hash, v, r, s);
require(signer != address(0), "invalid signature");
这是签名重放/伪造类漏洞的常见根源——忘了检查零地址。
0x02/0x03 SHA-256 与 RIPEMD-160
哈希任意字节数据。RIPEMD-160 输出 20 字节,但 EVM 按 32 字节操作,需要位移:
function hashRIPEMD160(bytes calldata data) public view returns (bytes20 h) {
(bool ok, bytes memory out) = address(3).staticcall(data);
require(ok);
h = bytes20(abi.decode(out, (bytes32)) << 96); // 左移 96 位取高 20 字节
}
0x04 Identity
高效拷贝连续内存区域。EVM 没有原生 memcpy,用它一次拷多个 32 字节字,比逐字 MLOAD/MSTORE 快。
0x05 Modexp
模幂 (base^exp) mod mod,是 RSA 密码学的核心(下一篇 RSA 签名篇详用):
function modExp(uint256 base, uint256 exp, uint256 mod) public view returns (uint256) {
bytes memory data = abi.encode(32, 32, 32, base, exp, mod); // 三个长度 + 三个值
(bool ok, bytes memory result) = address(5).staticcall(data);
require(ok, "modExp failed");
return abi.decode(result, (uint256));
}
输入格式:base 长度、exp 长度、mod 长度(各 32 字节),再跟三个实际值。
0x06/0x07/0x08 ecAdd / ecMul / ecPairing
在 BN-128(Barreto-Naehrig) 曲线上做运算(与 ECDSA 的曲线不同),支撑零知识证明:
- ecAdd:两点相加;
- ecMul:点乘标量;
- ecPairing:验证配对等式(证明校验的核心)。
由 EIP-196(add/mul)和 EIP-197(pairing)引入,EIP-1108 大幅降价。Tornado Cash 的 ZK 证明验证器就用它们(Module 13)。
0x09 Blake2 与 0x0a Point Evaluation
Blake2(EIP-152)是 Zcash 偏好的哈希。Point Evaluation(EIP-4844,Dencun 引入)验证 KZG 承诺,支撑 blob 扩容——给定 blob 承诺和 ZK 证明,证明无效则 revert。
四、如何调用预编译
除 ecRecover 有 Solidity 内置包装外,大多数预编译要用 staticcall 直接调用其地址:
(bool success, bytes memory output) = address(precompileNumber).staticcall(encodedInput);
require(success);
// 按需解码 output
关键注意:虽然预编译从不修改状态,但调用它的函数不能标 pure——因为编译器无法推断 staticcall 保持状态不变,只能标 view。
Yul 写法(SHA-256):
function hashSha256Yul(uint256 n) public view returns (bytes32) {
assembly {
mstore(0, n)
let ok := staticcall(gas(), 2, 0, 32, 0, 32) // 地址 2 = SHA-256
if iszero(ok) { revert(0,0) }
return(0, 32)
}
}
五、链兼容性警告
预编译的可用性在不同 EVM 兼容链上不一致。例如 zkSync 不支持 ecrecover 和加密预编译,因为大多数加密算法在零知识证明系统里验证起来计算昂贵。跨链部署时务必确认目标链支持你用的预编译。
六、总结
- 预编译是内置在协议层的”合约”,位于 0x01–0x0a,高效实现关键加密操作;
- 0x01 ecrecover(失败返回零地址,必须检查)、0x02/03 哈希、0x04 内存拷贝、0x05 模幂(RSA)、0x06-08 椭圆曲线(ZK)、0x09 Blake2、0x0a KZG;
- 多数用
staticcall调用其地址,调用函数只能标 view 不能 pure; - 跨链时注意兼容性(如 zkSync 不支持加密预编译)。
七、动手练习项目:预编译工具箱 PrecompileToolkit
项目目标
亲手用 staticcall 调用多个预编译(sha256、ripemd160、modexp、identity),并和 Solidity 原生实现对比 Gas,理解预编译的高效。部署到 Sepolia。
合约要求
PrecompileToolkit.sol
sha256Precompile(bytes calldata data) external view returns (bytes32):staticcall 0x02;ripemd160Precompile(bytes calldata data) external view returns (bytes20):staticcall 0x03 + 位移取高 20 字节;modExp(uint256 base, uint256 exp, uint256 mod) external view returns (uint256):staticcall 0x05,正确编码三个长度 + 三个值;memcpy(bytes calldata data) external view returns (bytes memory):staticcall 0x04 拷贝;recoverSigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s) external pure returns (address):用内置 ecrecover + 零地址检查;- 对照:
sha256Native用 Solidity 内置sha256()、modExpNaive用循环幂(小指数),对比 Gas。
测试要求(Foundry)
test_Sha256MatchesNative:预编译结果 == 内置sha256();test_RipeMd160:对已知输入断言正确的 20 字节输出;test_ModExp:modExp(3, 2, 5)== 4(3²=9, 9 mod 5 = 4),modExp(2, 10, 1000)== 24(1024 mod 1000);test_EcrecoverZeroAddressCheck:传入无效签名,断言函数 revert(而非返回零地址);test_ModExpVsNaive_Gas:大指数下对比预编译 vs 朴素循环幂的 Gas,证明预编译便宜得多;test_CannotBePure:(编译期验证)调用预编译的函数标 pure 会编译失败,理解为何只能 view。
Sepolia 部署与验证步骤
- 部署 PrecompileToolkit;
- Etherscan 调
modExp(3,2,5)得 4、sha256Precompile(...)对照已知哈希; - 用一个真实签名调
recoverSigner验证恢复出正确地址; - 故意传错签名观察 revert;
- 对比 modExp 预编译和朴素实现的 Gas report。
进阶挑战(可选)
- 用 ecAdd(0x06)/ecMul(0x07) 在 BN-128 曲线上做一次点加和点乘,为理解 ZK(Module 13)打基础;
- 写注释回答:为什么 ecrecover 失败返回零地址而不是 revert?这个设计导致过哪些真实漏洞,如何防御?