17. Solidity:哈希运算、ABI编码解码
17.1 哈希运算
哈希函数(hash function)是一个密码学概念,它可以将任意长度的消息转换为一个固定长度的值,这个值也称作哈希(hash)。
Hash的性质
一个好的哈希函数应该具有以下几个特性:
- 单向性:从输入的消息到它的哈希的正向运算简单且唯一确定,而反过来非常难,只能靠暴力枚举。
- 灵敏性:输入的消息改变一点对它的哈希改变很大。
- 高效性:从输入的消息到哈希的运算高效。
- 均一性:每个哈希值被取到的概率应该基本相等。
-
抗碰撞性:
- 弱抗碰撞性:给定一个消息x,找到另一个消息x'使得hash(x) = hash(x')是困难的。
- 强抗碰撞性:找到任意x和x',使得hash(x) = hash(x')是困难的。
Hash的应用
- 生成数据唯一标识
- 加密签名
- 安全加密
Keccak256
Keccak256函数是solidity中最常用的哈希函数,用法非常简单:
哈希 = keccak256(数据);
例子:
contract HashContract {
function hash(string memory text, uint num, address addr) external pure returns (bytes32) {
return keccak256(abi.encodePacked(text, num, addr));
}
function collision(string memory text0, string memory text1) external pure returns (bytes32) {
return keccak256(abi.encodePacked(text0, text1));
}
}
代码使用encodePacked对输入的参数进行打包,encodePacked会对数据进行压缩,encode则会对参数进行补零。具体差异下一节进行介绍。
需要注意的是,encodePacked由于是紧密拼接,可能会造成输入参数不同,输出hash值相同的情况,例如:
对于collision()
方法,输入参数为:AAA,BBB和输入参数为:AA,ABBB,哈希结果是相同的。本质上这并不是hash碰撞,而是由于encodePacked的编码特性造成的。但是在编写代码时要注意避免类似的错误发生。
17.2 ABI编码解码
Solidity中,ABI编码有4个函数:abi.encode
, abi.encodePacked
, abi.encodeWithSignature
, abi.encodeWithSelector
。而ABI解码有1个函数:abi.decode
,用于解码abi.encode
的数据。
17.2.1 ABI编码
定义以下数据,进行编码测试:
uint public x;
address public addr;
uint[3] public arr;
使用以下方法对上述参数进行初始化:
function initialize(uint _x, address _addr, uint[3] memory _arr) external {
x = _x;
addr = _addr;
arr = _arr;
}
在Remix中输入参数为:
123,0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,[1,2,3]
abi.encode
将给定参数利用ABI规则编码。ABI
被设计出来跟智能合约交互,他将每个参数填充为32字节的数据,并拼接在一起。如果你要和合约交互,你要用的就是abi.encode
。
function encode() external view returns (bytes memory) {
return abi.encode(x, addr, arr);
}
0x000000000000000000000000000000000000000000000000000000000000007b0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
由于abi.encode将每个数据都填充为32字节,中间有很多0。
abi.encodePacked
将给定参数根据其所需最低空间编码。它类似 abi.encode,但是会把其中填充的很多0省略。当你想省空间,并且不与合约交互的时候,可以使用abi.encodePacked,例如算一些数据的hash时。
function encodePacked() external view returns (bytes memory) {
return abi.encodePacked(x, addr, arr);
}
注意:encodePacked不支持结构体类型。
0x000000000000000000000000000000000000000000000000000000000000007b5b38da6a701c568545dcfcb03fcb875f56beddc4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
abi.encodeWithSignature
与abi.encode功能类似,只不过第一个参数为函数签名,比如"foo(uint256,address)"。当调用其他合约的时候可以使用。
function encodeWithSignature() external view returns (bytes memory) {
return abi.encodeWithSignature("foo(uint256,address,uint256[3])", x, addr, arr);
}
0xcde005b8000000000000000000000000000000000000000000000000000000000000007b0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
等同于在abi.encode
编码结果前加上了4字节的`函数选择器: 函数选择器就是通过函数名和参数进行签名处理(Keccak–Sha3)来标识函数,可以用于不同合约之间的函数调用。
abi.encodeWithSelector
与abi.encodeWithSignature功能类似,只不过第一个参数为函数选择器,为函数签名Keccak哈希的前4个字节。
function encodeWithSelector() external view returns (bytes memory) {
return abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,uint256[3])")), x, addr, arr);
}
0xcde005b8000000000000000000000000000000000000000000000000000000000000007b0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
与abi.encodeWithSignature结果一样。
17.2.2 ABI解码
abi.decode
abi.decode用于解码abi.encode生成的二进制编码,将它还原成原本的参数。
function decode(bytes memory data) external pure returns (uint _x, address _addr, uint[3] memory _arr) {
(_x, _addr, _arr) = abi.decode(data, (uint, address, uint[3]));
}
我们将abi.encode的二进制编码输入给decode,将解码出原来的参数:

注意:
- 只能对encode编码的bytes进行解码。
- 必须知道数据结构类型才能进行解码。