Solidity 预编译合约详解

理解 0x01-0x0a 各预编译合约(ecrecover/sha256/modexp/椭圆曲线运算等)及其调用方式

6 分钟阅读
Solidity 预编译合约详解

Solidity 预编译合约详解

目录


一、什么是预编译合约

以太坊预编译的行为就像内置在以太坊协议里的智能合约

它们位于地址 0x01 到 0x0a,是一些被认为足够重要、值得用高效原生实现的加密操作。这些操作在协议层执行(而非在 EVM 字节码里跑),因此比用 Solidity 实现高效得多。

为什么需要它们?某些运算(如椭圆曲线配对、模幂)如果用 Solidity/EVM 操作码硬写,会消耗天文数字的 Gas,甚至超出区块上限。把它们做成预编译,价格固定且便宜。

二、四大用途

  1. 椭圆曲线数字签名恢复(ecrecover);
  2. 哈希方法(兼容 Bitcoin、Zcash);
  3. 内存操作(拷贝);
  4. 椭圆曲线数学(支撑零知识证明系统)。

三、完整预编译目录(0x01–0x0a)

地址名称功能
0x01ecRecover从哈希和签名 (v,r,s) 恢复签名者地址
0x02SHA-256SHA-256 哈希(兼容 Bitcoin 交易验证)
0x03RIPEMD-160RIPEMD-160 哈希,输出 20 字节(Bitcoin 地址用)
0x04Identity高效拷贝连续内存(EVM 没有原生 memcpy)
0x05Modexp模幂 (base^exp) mod modulus(RSA 的核心)
0x06ecAddBN-128 曲线上两点相加(ZK)
0x07ecMul曲线点乘标量(ZK)
0x08ecPairing配对验证(ZK 证明校验)
0x09Blake2Blake2 哈希(Zcash 偏好)
0x0aPoint EvaluationKZG 承诺验证(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

  1. sha256Precompile(bytes calldata data) external view returns (bytes32):staticcall 0x02;
  2. ripemd160Precompile(bytes calldata data) external view returns (bytes20):staticcall 0x03 + 位移取高 20 字节;
  3. modExp(uint256 base, uint256 exp, uint256 mod) external view returns (uint256):staticcall 0x05,正确编码三个长度 + 三个值;
  4. memcpy(bytes calldata data) external view returns (bytes memory):staticcall 0x04 拷贝;
  5. recoverSigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s) external pure returns (address):用内置 ecrecover + 零地址检查;
  6. 对照:sha256Native 用 Solidity 内置 sha256()modExpNaive 用循环幂(小指数),对比 Gas。

测试要求(Foundry)

  1. test_Sha256MatchesNative:预编译结果 == 内置 sha256()
  2. test_RipeMd160:对已知输入断言正确的 20 字节输出;
  3. test_ModExpmodExp(3, 2, 5) == 4(3²=9, 9 mod 5 = 4),modExp(2, 10, 1000) == 24(1024 mod 1000);
  4. test_EcrecoverZeroAddressCheck:传入无效签名,断言函数 revert(而非返回零地址);
  5. test_ModExpVsNaive_Gas:大指数下对比预编译 vs 朴素循环幂的 Gas,证明预编译便宜得多;
  6. test_CannotBePure:(编译期验证)调用预编译的函数标 pure 会编译失败,理解为何只能 view。

Sepolia 部署与验证步骤

  1. 部署 PrecompileToolkit;
  2. Etherscan 调 modExp(3,2,5) 得 4、sha256Precompile(...) 对照已知哈希;
  3. 用一个真实签名调 recoverSigner 验证恢复出正确地址;
  4. 故意传错签名观察 revert;
  5. 对比 modExp 预编译和朴素实现的 Gas report。

进阶挑战(可选)

  • 用 ecAdd(0x06)/ecMul(0x07) 在 BN-128 曲线上做一次点加和点乘,为理解 ZK(Module 13)打基础;
  • 写注释回答:为什么 ecrecover 失败返回零地址而不是 revert?这个设计导致过哪些真实漏洞,如何防御?

💬 评论