Ethereum 预编译(precompiles)的行为类似于内置在 Ethereum 协议中的智能合约。这九个预编译存在于地址 0x01 到 0x09 中。
预编译的用途分为四类:
- 椭圆曲线数字签名恢复
- 与 Bitcoin 和 Zcash 交互的哈希方法
- 内存复制
- 为零知识证明提供椭圆曲线数学计算的方法
这些操作被认为非常有用,因此需要具备高 Gas 效率的机制来执行它们。如果用 Solidity 实现这些算法,Gas 效率会大打折扣。
预编译不会在智能合约内部执行,它们是 Ethereum 客户端规范的一部分。你可以在这里查看 Geth Client 中的预编译列表。由于它们是协议规范,因此被列在了 Ethereum Yellow Paper 中(附录 E)。
使用 Solidity 调用预编译智能合约
大多数预编译没有 Solidity 包装器(ecRecover 是唯一的例外)。你需要使用 addressOfPrecompile.staticcall(...) 直接调用该地址,或者使用汇编。
虽然没有一个预编译合约会改变状态,但调用它们的 Solidity 函数不能是 pure 函数,因为 Solidity 编译器无法推断出 staticcall 不会改变状态。
地址 0x01:ecRecover
ECRecover 是用于从哈希和该哈希的数字签名中恢复地址的预编译,即如果签名有效,则确定是谁签署了它。(在我们的教程中了解有关如何使用 Solidity 数字签名 的更多信息)。
示例:
function recoverSignature(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public view returns (address) {
address r = ecrecover(hash, v, r, s);
require(r != address(0), "signature is invalid");
}
注意:当签名未能通过哈希验证时,ecrecover 不会 revert。它会返回零地址。你应始终显式检查这一点,或者更好的做法是使用 Openzeppelin 库来为你处理此问题。如果你不知道自己在做什么,签名操作可能会出现很多问题!
地址 0x02 和 0x03:SHA-256 和 RIPEMD-160
这两个预编译都会对 calldata 中提供的字节进行哈希处理。下面是使用 SHA256 的示例。为了简单起见,我们将对一个 uint256 进行哈希处理:
function hashSha256(uint256 numberToHash) public view returns (bytes32 h) {
(bool ok, bytes memory out) = address(2).staticcall(abi.encode(numberToHash));
require(ok);
h = abi.decode(out, (bytes32));
}
这是使用 RIPEMD-160 的示例:
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);
}
尽管 RIPEMD-160 返回 20 字节,但 EVM 只能以 32 字节的增量进行处理,这就是为什么上面的示例代码中使用了位移和类型转换。
为什么 Ethereum 要支持 SHA-256 和 RIPEMD-160?就像 Ethereum 大量使用 keccak256 一样,Bitcoin 大量使用了 SHA-256。然而,Bitcoin 地址使用 RIPEMD-160 来对公钥进行哈希处理,从而使公开地址更加紧凑。这与 Ethereum 取 ECDSA 公钥的 keccak256 哈希结果的后 20 个字节(160 位,类似于 RIPEMD)的做法类似。
使用 Yul 汇编
由于提前知道返回大小,因此无需使用 returndatasize 操作码。在 Yul(以及在操作码中),staticcall 接受六个参数:
- args
- 转发的 gas
- 在内存中查找要进行哈希处理的数据的位置
- 要进行哈希处理的数据大小(32 字节)
- 写入输出的位置
- 输出的大小
在下面的代码中,我们将 uint256 写入内存,然后将其传递到地址 2 进行哈希处理。
function hashSha256Yul(uint256 numberToHash) public view returns (bytes32) {
assembly {
mstore(0, numberToHash) // store number in the zeroth memory word
let ok := staticcall(gas(), 2, 0, 32, 0, 32)
if iszero(ok) {
revert(0,0)
}
return(0, 32)
}
}
地址 0x04:Identity
Identity 预编译将内存的一个区域复制到另一个区域。Ethereum 没有 memcopy 操作码(将内存中一个区域复制到另一个区域的操作码)。通常,你必须将一个字的内存 MLOAD 到堆栈上,然后再 MSTORE 它来进行复制,并且必须逐字复制。借助 Identity 预编译,你可以一次性复制一组连续的 32 字节字,而不是一次复制一个字节。
地址 0x05:Modexp
ECDSA 不支持公钥加密。如果应用程序有这种用例,那么必须使用传统的 RSA 加密。从宏观上看,RSA 的工作原理是获取一条消息,计算其接收者公钥次幂后再对某个极大的数取模。得到的数字就是加密后的消息。由于这严重限制了消息的长度,因此典型的消息交换工作方式是加密一个对称密钥(例如 AES-256)并将其发送给接收者。然后,接收者可以使用 AES-256 密钥解密消息。
使用 RSA 签名消息的过程正好相反。发送者将消息的哈希值进行其私钥次幂运算后对大数(这是公开已知的)取模。结果即为消息的签名。接收者可以通过将签名进行公钥次幂运算并对该大数取模,查看结果是否等于该消息的哈希值来验证签名。
Ethereum 没有针对 RSA 的公钥基础设施。然而,Ethereum 地址可以通过对其 Ethereum 地址进行 RSA 签名来证明其拥有某个 RSA 公钥。注意,反向操作是行不通的。使用 ECDSA 签署 RSA 公钥是不安全的,因为任何人都可以使用 ECDSA 签署任意字符串,包括 RSA 公钥。
你可以在我们关于该主题的另一篇文章中了解 RSA with Solidity 的应用。
下面是在 Solidity 中将 modExp 与 uint256 结合使用的示例:
function modExp(uint256 base, uint256 exp, uint256 mod) public view returns (uint256) {
bytes memory precompileData = abi.encode(32, 32, 32, base, exp, mod);
(bool ok, bytes memory data) = address(5).staticcall(precompileData);
require(ok, "expMod failed");
return abi.decode(data, (uint256));
}
地址 0x06、0x07 和 0x08:ecAdd、ecMul 和 ecPairing(EIP-196 和 EIP-197)
这些预编译用于提高 零知识证明密码学 的效率。实际上,你可以在 Tornado Cash 零知识证明验证器中看到全部这三个预编译的使用:
椭圆曲线加法:staticcall to address(6)
椭圆曲线乘法:staticcall to address(7)
椭圆曲线配对:static call to address(8)
这些操作仅支持 BN-128 Barreto-Naehrig 椭圆曲线。它们与用于数字签名的椭圆曲线不同。
ecAdd 和 ecMul 是在 EIP-196 EIP-196 中添加的,而 ecPairing 是在 EIP-197 中添加的。
你可以在我们的其他教程中了解这些预编译的工作原理:
ecAdd、ecMul 和 ecPairing 的 Gas 成本
随着 EIP-1108 的引入,这些预编译的 Gas 成本较其原始规范有所降低。用户应参考该 EIP 以获取有关其 Gas 成本的最新信息,而不是参考各自最初的 EIP 规范。
地址 0x09:Blake2(EIP-152)
Blake2 哈希是 Zcash 首选的哈希算法。与 SHA-256 和 RIPEMD-160 类似,添加 Blake2 是为了使 Ethereum 能够验证关于该区块链上交易的声明。此预编译在 EIP-152 中被添加,该提案中提供了一些示例代码。
地址 0xa:点评估预编译(EIP-4844)
Decun 硬分叉在地址 10(地址 0xa)添加了一个预编译 precompile at address 10 (address 0xa),用于验证 KZG commitments。也就是说,给定一个 blob 承诺和一个零知识证明,如果证明无效,该预编译就会 revert。
其他链上的预编译
智能合约开发者在将 Solidity 代码复制到其他 EVM 兼容链时应小心谨慎,因为这些链上的预编译可能与 Ethereum 的预编译不匹配。例如,zksync 不支持 ecrecover 及其他密码学预编译。(其技术原因是大多数密码学算法并非对 SNARK 友好,从零知识证明的角度来看,验证它们的成本很高)。
了解更多
本材料是我们 Solidity bootcamp 的一部分。你也可以通过我们免费的 Solidity course 免费学习 Solidity。
原载于 2023 年 4 月 16 日