title | tags | ||
---|---|---|---|
07. ABI编码公式 |
|
《WTF Solidity 内部标准》教程将介绍 Solidity 智能合约中的存储布局,内存布局,以及 ABI 编码规则,帮助大家理解 Solidity 的内部规则。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity-Internals
这一讲,我们介绍基于Solidity 文档总结的 ABI 编码公式。
首先,我们定义公式会使用到的符号的意义:
T
: 任意变量类型。(T1, T2,...,Tn)
: 由T1
...Tn
等类型的变量组成的元组。len(a)
:a
的字节长度,用于计算数据偏移量。e(X)
: 变量X
的 ABI 编码。head(X)
:X
变量的头部编码。tail(X)
:X
变量的尾部编码。pad_right(X)
: 在X
的值的右侧补若干个0
,使其长度成为32
字节。
我们将 Solidity 合约的 ABI 编码公式分为两部分,第一部分为递归编码公式,第二部分为类型编码公式。逻辑就是将复杂类型使用递归公式转换成简单类型进行编码。
递归编码公式会将复杂类型的编码以递归的方式转换为简单类型的编码。
- 元组(结构体也会转换成元组进行编码)
对于元组X = (T1,...,Tk)
,其中k>=0
并且T1
...Tn
为任意类型,有:
e(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k))
其中X(n)
为元组X
的第n
个元素。也就是说,元组X
的编码由两部分组成,第一部分是头部编码,第二部分是尾部编码,它们都按照元素在元组中的顺序排列。
那么head(Xi)
和tail(Xi)
如何定义?
- 如果
X(i)
的类型Ti
为静态类型,则直接在头部进行编码,没有尾部编码:head(X(i)) = e(X(i))
tail(X(i)) = ""
(为空)
- 如果
X(i)
的类型Ti
为动态类型,那么会在尾部进行编码,头部编码为尾部编码tail(X(i))
的偏移量:head(X(i)) = e(len(head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(i-1))))
(这是从head(X1)
到tail(X(i-1))
的字节长度,也是tail(X(i))
的偏移量)tail(X(i)) = e(X(i))
- 定长数组
对于长度为k
的定长数组X = T[k]
,其中k>=0
并且T
为任意类型,有:
e(X) = e((X[0], ..., X[k-1]))
其中X[n]
为数组X
的第n
个元素。也就是说,将它当作由相同类型的k
个元素组成的元组那样被编码的。
- 不定长数组
对于长度为k
的不定长数组X = T[]
,其中其中k>=0
并且T
为任意类型,有:
e(X) = e(k) e((X[0], ..., X[k-1]))
其中长度k
当作uint256
类型编码。也就是说,它的编码有两部分,第一部分为长度k
的编码,第二部分为等效的定长数组的编码。
类型编码公式以穷举的方式给出简单类型的 ABI 编码规则。
-
uint<M>
:M
位的无符号整数,M
的取值为0
到256
之间的可以整除 8 的整数,比如uint8
,uint32
,uint256
(uint
是uint256
的同义词)。编码时,会在它们左侧补充若干0
以使其长度成为32
字节。 -
address
:地址类型,与uint160
的编码方式相同。address payable
和contract
类型的变量也使用相同的编码方式。 -
bool
:1
表示true
,0
表示false
,编码方式与uint8
的情况相同。 -
bytes<M>
:长度为M
字节的定长字节数组,0 < M <= 32
,编码时会在右侧补若干0
使其长度成为32
字节。 -
bytes
: 若X
是长度为k
(k
的类型为uint256
)的不定长字节数组,则enc(X) = enc(k) pad_right(X)
,也就是先编码长度k
,再编码内容。编码内容时会在右侧补若干0
使其长度成为32
字节的倍数。 -
string
: 会先用UTF-8
编码为bytes
,然后使用bytes
的规则进行 ABI 编码。
以上是常用类型的 ABI 编码规则,不常用类型(如int
,fixed
,ufixed
)的规则见文档。
function testAbiUintTuple() public pure returns (bytes memory){
uint x = 1;
return abi.encode((x));
}
(x)
是元组,根据 ABI 编码公式,e((x)) = head(x) tail(x)
。
x
为uint
类型,所以head(x) = e(x)
,tail(x) = ""
,所以(x)
的 ABI 编码为
0x0000000000000000000000000000000000000000000000000000000000000001
function testAbiArray() public pure returns (bytes memory){
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 2;
x[2] = 3;
return abi.encode(x);
}
(x)
是元组,根据 ABI 编码公式,e((x)) = head(x) tail(x)
。x
为uint[]
类型(不定长数组),是动态类型,因此:
-
head(x) = len(head(x))
也就是0x20
-
tail(x) = e(x) = e(k) e(x[0]) e(x[1]) e(x[2])
,其中k = 3
是数组的长度。
所以(x)
的 ABI 编码为:
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
function testAbiDynamicArray() public pure returns (bytes memory){
uint x = 99;
uint[] memory y = new uint[](3);
y[0] = 1;
y[1] = 2;
y[2] = 3;
string memory z = "WTF";
bytes memory encodedX = abi.encode(x);
bytes memory encodedY = abi.encode(y);
bytes memory encodedZ = abi.encode(z);
return abi.encodePacked(encodedX, encodedY, encodedZ);
}
由于y
和z
都是动态类型,因此(x, y, z)
元组为动态类型,((x, y, z))
元组也是动态类型,根据 ABI 编码公式,e(((x, y, z))) = head(((x, y, z))) tail(((x, y, z)))
。
其中head(((x, y, z))) = len(head(((x, y, z))))
为0x20
,因为((x, y, z))
只有一个元素(x, y, z)
,虽然这个元素也是一个元组。tail(((x, y, z))) = e((x, y, z))
(这里我们脱了一层括号,开始编码元组的元素了)。
到目前为止的编码为:
0x
0000000000000000000000000000000000000000000000000000000000000020
e((x, y, z))
下面我们来计算e((x, y, z))
,由于它是动态元组,因此e((x, y, z)) = head(x) head(y) head(z) tail(x) tail(y) tail(z)
。到目前为止的编码为:
0x
0000000000000000000000000000000000000000000000000000000000000020
head(x)
head(y)
head(z)
tail(x)
tail(y)
tail(z)
下面我们依次计算x
,y
和z
的编码。x
为uint
类型,属于静态类型,值为99
(也就是0x63
)。因此,head(x) = e(x)
,tail(x) = ""
。到目前为止的编码为:
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000063
head(y)
head(z)
tail(y)
tail(z)
接下来,我们来看y
的编码。y
为uint[]
类型,是动态类型,长度为3
。因此有
-
head(y) = e(len(head(x) head(y) head(z) tail(x))) = 0x60
-
tail(y) = e(y) = e(3) e(y[0]) e(y[1]) e(y[2])
到目前为止的编码为:
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000063
0000000000000000000000000000000000000000000000000000000000000060
head(z)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
tail(z)
最后,我们来看z
的编码。z
为string
类型,是动态类型;WTF
的UTF-8
编码为575446
,长度为3
字节。因此有:
-
head(z) = e(len(head(x) head(y) head(z) tail(x) tail(y))) = 0xe0
-
tail(z) = e(z) = e(3) pad_right(575446)
呼,最后,我们就得到了((uint x, uint[] y, string z))
的 ABI 编码:
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000063
0000000000000000000000000000000000000000000000000000000000000060
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000003
5754460000000000000000000000000000000000000000000000000000000000
如果你能自己推导出((uint x, uint[] y, string z))
的编码,说明你已经掌握了 ABI 编码的规律!
这一讲,我们介绍了 Solidity 合约的 ABI 编码公式,有了它,再复杂的编码也不怕。它的核心思想是使用递归的方法把复杂类型的编码转换成简单类型的编码。