在 Solidity 中,keccak256 是最常用的哈希函数,用于计算任意输入数据的 Keccak-256 哈希值(输出为 32 字节)。它是以太坊底层密码学的基础,广泛应用于生成唯一标识、数据完整性校验、签名验证等场景。
1. 什么是 Keccak256?
- Keccak-256 是 SHA-3 竞赛的获胜算法,但后来 NIST 对 SHA-3 做了细微调整(填充方式不同)。以太坊和 Solidity 采用的是原始的 Keccak-256(即 Keccak 提交给 NIST 的版本),而不是最终的 SHA-3 标准。两者的输出在相同输入下不同,需要注意兼容性。
- 输入可以是任意长度的字节序列,输出固定为 256 位(32 字节)。
- 在 Solidity 中,
keccak256是一个内置的全局函数,无需导入。
2. 基本语法
solidity
keccak256(bytes memory data) returns (bytes32)
- 参数 :必须是
bytes类型(动态字节数组)。通常需要将待哈希的数据(如字符串、数字、地址、多个值的组合)先转换为bytes。 - 返回值 :
bytes32类型的哈希值。
示例
solidity
// 哈希一个字符串
bytes32 hash = keccak256("Hello, Solidity!");
// 哈希一个数字(需要转换为bytes)
uint num = 42;
bytes32 hash2 = keccak256(abi.encodePacked(num));
// 哈希地址
address addr = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
bytes32 hash3 = keccak256(abi.encodePacked(addr));
3. 如何准备输入数据
由于 keccak256 只接受 bytes 类型,我们需要将不同类型的数据转换成 bytes。常用方法有:
3.1 abi.encodePacked(...) ------ 非标准打包(紧密打包)
将多个参数紧密连接成字节序列,不添加填充。适用于需要节省空间且不关心潜在碰撞风险的场景。
solidity
bytes memory packed = abi.encodePacked(x, y, z);
bytes32 hash = keccak256(packed);
⚠️ 警告 :abi.encodePacked 可能会产生哈希碰撞,因为不同输入可能打包成相同的字节串。例如:
abi.encodePacked("a", "bc")和abi.encodePacked("ab", "c")都会得到"abc"。abi.encodePacked(uint16(0x1234), uint16(0x5678))和abi.encodePacked(uint32(0x12345678))可能产生相同结果。
最佳实践 :若需避免碰撞,应使用 abi.encode(见下文)或在打包元素间加入分隔符(如固定长度编码、使用 bytes.concat 并手动加分隔符)。
3.2 abi.encode(...) ------ 标准 ABI 编码
对参数进行标准的 ABI 编码,每个参数会按规则填充到 32 字节(静态类型)或添加长度前缀(动态类型)。编码结果唯一,不会产生碰撞,但占用空间更大。
solidity
bytes memory encoded = abi.encode(x, y, z);
bytes32 hash = keccak256(encoded);
优点 :无碰撞风险,适合需要确定性的场景(如生成合约地址、签名哈希)。
缺点:长度较长,gas 成本稍高。
3.3 bytes.concat(...) ------ 连接多个 bytes 或 bytesN
从 Solidity 0.8.4 开始,可以使用 bytes.concat 将多个 bytes/bytesN 连接成一个 bytes。
solidity
bytes memory combined = bytes.concat(bytes("a"), bytes("bc"));
bytes32 hash = keccak256(combined);
4. 常见应用场景
4.1 生成唯一标识符(ID)
在合约中经常需要根据某些属性生成唯一的 ID,例如根据代币的元数据生成 NFT 的 tokenURI 或根据订单信息生成订单 ID。
solidity
function generateOrderId(address buyer, uint amount) public pure returns (bytes32) {
return keccak256(abi.encodePacked(buyer, amount, block.timestamp));
}
4.2 密码学验证
- 数字签名 :通常对消息的哈希进行签名,而非消息本身。例如 EIP-191 和 EIP-712 规范都使用
keccak256计算结构化数据的哈希。 - Merkle 树 :在验证白名单或批量证明时,利用
keccak256构建 Merkle 树并计算节点哈希。
4.3 数据完整性校验
将大数据的哈希存储在链上,链下提供数据,链上重新计算哈希并比对,确保数据未被篡改。
4.4 派生地址或密钥
通过哈希算法从种子生成新地址或密钥(注意安全性,通常结合 salt)。
5. 安全注意事项
- 避免使用
abi.encodePacked拼接可能重叠的数据 :如前所述,abi.encodePacked("a", "bc")与abi.encodePacked("ab", "c")结果相同,导致两个不同含义的输入产生同一哈希。若必须使用,应在元素间加入分隔符(如固定长度、特殊符号)。 - 区分动态类型与静态类型 :动态类型(如
string、bytes)在abi.encodePacked中直接拼接内容,没有长度前缀,增加了碰撞风险。而abi.encode会添加长度前缀,确保唯一性。 - 不要将
keccak256视为随机数生成器:虽然哈希看起来随机,但其输出完全由输入决定,不是真正的随机数。 - 防范重放攻击:当哈希用于签名时,应包含链 ID、合约地址等上下文信息,防止签名在不同链或合约间被重放。
6. 与其他哈希函数的对比
| 函数 | 输出长度 | 说明 |
|---|---|---|
keccak256 |
32 字节 | 最常用,内置于 Solidity,gas 成本低。 |
sha256 |
32 字节 | 通过预编译合约支持,gas 成本较高,与比特币等系统兼容。 |
sha3 |
可变 | 是最终标准 SHA-3,但 Solidity 未内置,需通过预编译调用(地址 0x09)。 |
ripemd160 |
20 字节 | 通过预编译合约支持,用于比特币地址生成等场景。 |
在大多数情况下,keccak256 是首选,因为其 gas 成本最低且使用方便。
7. 完整示例
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HashExample {
// 使用 abi.encode 安全地组合数据
function safeHash(uint x, string memory s, address a) public pure returns (bytes32) {
return keccak256(abi.encode(x, s, a));
}
// 使用 abi.encodePacked(有碰撞风险)
function riskyHash(uint x, string memory s, address a) public pure returns (bytes32) {
return keccak256(abi.encodePacked(x, s, a));
}
// 验证数据完整性
function verifyData(string memory data, bytes32 expectedHash) public pure returns (bool) {
return keccak256(abi.encodePacked(data)) == expectedHash;
}
// 生成 NFT 的 tokenURI 标识符
function generateTokenId(address owner, uint index) public pure returns (bytes32) {
return keccak256(abi.encodePacked(owner, index));
}
}
8. 总结
keccak256是 Solidity 中最核心的哈希函数,输入bytes,输出bytes32。- 使用
abi.encode可以安全无碰撞地组合多类型数据。 - 使用
abi.encodePacked更节省 gas,但必须警惕潜在的哈希碰撞,必要时加入分隔符或使用固定长度编码。