ABI全称应用二进制接口说明。
调用一个函数的数据的前4个字节,指定了要调用的函数。
函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。
函数的返回类型并不是这个签名的一部分。然而JSON 描述中包含了即包含了输入也包含了输出。
我们需要区分静态和动态类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。以下类型被称为“动态”:
bytes
string
- 任意类型 T 的变长数组
T[]
- 任意动态类型 T 的定长数组
T[k]
(k >= 0
) - 由动态的
Ti
(1 <= i <= k
)构成的 元组tuple(T1,...,Tk)
所有其他类型都被称为“静态”。
简单例子
给定一个合约:
1 | pragma solidity ^0.4.16; |
baz调用
使用69和true做参数进行调用,我们总共需在传送68个字节,参数类型uint32和bool都是静态类型,所以进行直接编码,可以分解为:
0xcdcd77c0
:方法ID。这源自ASCII格式的baz(uint32,bool)
签名的 Keccak 哈希的前 4 字节。0x0000000000000000000000000000000000000000000000000000000000000045
:第一个参数,一个被用 0 值字节补充到 32 字节的 uint32 的值 。此处的45是十进制69的十六进制形式。0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数,一个被用 0 值字节补充到 32 字节的 boolean 值true
。
合起来就是:
1 | 0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001 |
它返回一个 bool
。比如它返回 false
,那么它的输出将是一个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000
,一个bool值。
bar调用
使用["abc", "def"]
做参数调用 bar
,我们总共需要传送68字节,参数类型bytes3[2]是静态类型,所以进行直接编码,可以分解为:
0xfce353f6
:方法ID。源自bar(bytes3[2])
的签名。0x6162630000000000000000000000000000000000000000000000000000000000
:第一个参数的第一部分,一个bytes3
值"abc"
(左对齐)。a对应ASCII码是61,b是62,c是63。0x6465660000000000000000000000000000000000000000000000000000000000
:第一个参数的第二部分,一个bytes3
值"def"
(左对齐)。d对应ASCII码是64,e是65,f是66。
合起来就是:
1 | 0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000 |
sam(bytes,bool,uint256[])调用
使用 "dave"
、true
和 [1,2,3]
作为参数调用 sam
,我们总共需要传送 292 字节,bytes和uint256[]是动态类型,会单独分配位置编码,bool则直接编码,可以分解为:
0xa5643bf2
:方法ID。源自sam(bytes,bool,uint256[])
的签名。注意,uint
被替换为了它的权威代表uint256
。0x0000000000000000000000000000000000000000000000000000000000000060
:第一个参数(动态类型)的数据部分的位置,即从参数编码块开始位置算起的字节数。在这里,是0x60
。即从此处开始(包含)到第5行(不包含)的偏移量。0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数:boolean 的 true。0x00000000000000000000000000000000000000000000000000000000000000a0
:第三个参数(动态类型)的数据部分的位置,由字节数计量。在这里,是0xa0
。0x0000000000000000000000000000000000000000000000000000000000000004
:第一个参数的数据部分,以字节数组的元素个数作为开始,在这里,是 4。0x6461766500000000000000000000000000000000000000000000000000000000
:第一个参数的内容:"dave"
的 UTF-8 编码(在这里等同于 ASCII 编码),并在右侧(低位)用 0 值字节补充到 32 字节。0x0000000000000000000000000000000000000000000000000000000000000003
:第三个参数的数据部分,以数组的元素个数作为开始,在这里,是 3。0x0000000000000000000000000000000000000000000000000000000000000001
:第三个参数的第一个数组元素。0x0000000000000000000000000000000000000000000000000000000000000002
:第三个参数的第二个数组元素。0x0000000000000000000000000000000000000000000000000000000000000003
:第三个参数的第三个数组元素。
合起来就是:
1 | 0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003 |
动态类型例子
用参数 (0x123, [0x456, 0x789], "1234567890", "Hello, world!")
进行对函数 f(uint,uint32[],bytes10,bytes)
的调用会通过以下方式进行编码:
取得 sha3("f(uint256,uint32[],bytes10,bytes)")
的前 4 字节,也就是 0x8be65246
。 然后我们对所有 4 个参数的头部进行编码。对静态类型 uint256
和 bytes10
是可以直接传过去的值;对于动态类型 uint32[]
和 bytes
,我们使用的字节数偏移量是它们的数据区域的起始位置,由需编码的值的开始位置算起(也就是说,不计算包含了函数签名的前 4 字节),这就是:
0x0000000000000000000000000000000000000000000000000000000000000123
(0x123
补充到 32 字节)0x0000000000000000000000000000000000000000000000000000000000000080
(第二个参数的数据部分起始位置的偏移量,4*32 字节,正好是头部的大小)0x3132333435363738393000000000000000000000000000000000000000000000
("1234567890"
从右边补充到 32 字节)0x00000000000000000000000000000000000000000000000000000000000000e0
(第四个参数的数据部分起始位置的偏移量 = 第一个动态参数的数据部分起始位置的偏移量 + 第一个动态参数的数据部分的长度 = 432 + 332,参考后文)
在此之后,跟着第一个动态参数的数据部分 [0x456, 0x789]
:
0x0000000000000000000000000000000000000000000000000000000000000002
(数组元素个数,2)0x0000000000000000000000000000000000000000000000000000000000000456
(第一个数组元素)0x0000000000000000000000000000000000000000000000000000000000000789
(第二个数组元素)
最后,我们将第二个动态参数的数据部分 "Hello, world!"
进行编码:
0x000000000000000000000000000000000000000000000000000000000000000d
(元素个数,在这里是字节数:13)0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000
("Hello, world!"
从右边补充到 32 字节)
最后,合并到一起的编码就是(为了清晰,在 函数选择器Function Selector 和每 32 字节之后加了换行):
1 | 0x8be65246 |
g(uint[][],string[])调用
来对一个签名为 g(uint[][],string[])
,参数值为 ([[1, 2], [3]], ["one", "two", "three"])
的函数来进行编码;但从最原子的部分开始:
首先我们将第一个根数组 [[1, 2], [3]]
的第一个嵌入的动态数组 [1, 2]
的长度和数据进行编码:
0x0000000000000000000000000000000000000000000000000000000000000002
(第一个数组中的元素数量 2;元素本身是1
和2
)0x0000000000000000000000000000000000000000000000000000000000000001
(第一个元素)0x0000000000000000000000000000000000000000000000000000000000000002
(第二个元素)
然后我们将第一个根数组 [[1, 2], [3]]
的第二个潜入的动态数组 [3]
的长度和数据进行编码:
0x0000000000000000000000000000000000000000000000000000000000000001
(第二个数组中的元素数量 1;元素数据是3
)0x0000000000000000000000000000000000000000000000000000000000000003
(第一个元素)
然后我们需要找到动态数组 [1, 2]
和 [3]
的偏移量。要计算这个偏移量,我们可以来看一下第一个根数组 [[1, 2], [3]]
编码后的具体数据:
1 | 0 - a - [1, 2] 的偏移量 |
偏移量 a
指向数组 [1, 2]
内容的开始位置,即第 2 行的开始(64 字节);所以 a = 0x0000000000000000000000000000000000000000000000000000000000000040
。
偏移量 b
指向数组 [3]
内容的开始位置,即第 5 行的开始(160 字节);所以 b = 0x00000000000000000000000000000000000000000000000000000000000000a0
。
然后我们对第二个根数组的嵌入字符串进行编码:
0x0000000000000000000000000000000000000000000000000000000000000003
(单词"one"
中的字符个数)0x6f6e650000000000000000000000000000000000000000000000000000000000
(单词"one"
的 utf8 编码)0x0000000000000000000000000000000000000000000000000000000000000003
(单词"two"
中的字符个数)0x74776f0000000000000000000000000000000000000000000000000000000000
(单词"two"
的 utf8 编码)0x0000000000000000000000000000000000000000000000000000000000000005
(单词"three"
中的字符个数)0x7468726565000000000000000000000000000000000000000000000000000000
(单词"three"
的 utf8 编码)
作为与第一个根数组的并列,因为字符串也属于动态元素,我们也需要找到它们的偏移量 c
, d
和 e
:
1 | 0 - c - "one" 的偏移量 |
偏移量 c
指向字符串 "one"
内容的开始位置,即第 3 行的开始(96 字节);所以 c = 0x0000000000000000000000000000000000000000000000000000000000000060
。
偏移量 d
指向字符串 "two"
内容的开始位置,即第 5 行的开始(160 字节);所以 d = 0x00000000000000000000000000000000000000000000000000000000000000a0
。
偏移量 e
指向字符串 "three"
内容的开始位置,即第 7 行的开始(224 字节);所以 e = 0x00000000000000000000000000000000000000000000000000000000000000e0
。
注意,根数组的嵌入元素的编码并不互相依赖,且具有对于函数签名 g(string[],uint[][])
所相同的编码。
然后我们对第一个根数组的长度进行编码:
0x0000000000000000000000000000000000000000000000000000000000000002
(第一个根数组的元素数量 2;这些元素本身是[1, 2]
和[3]
)
而后我们对第二个根数组的长度进行编码:
0x0000000000000000000000000000000000000000000000000000000000000003
(第二个根数组的元素数量 3;这些字符串本身是"one"
、"two"
和"three"
)
最后,我们找到根动态数组元素 [[1, 2], [3]]
和 ["one", "two", "three"]
的偏移量 f
和 g
。汇编数据的正确顺序如下:
1 | 0x2289b18c - 函数签名 |
偏移量 f
指向数组 [[1, 2], [3]]
内容的开始位置,即第 2 行的开始(64 字节);所以 f = 0x0000000000000000000000000000000000000000000000000000000000000040
。
偏移量 g
指向数组 ["one", "two", "three"]
内容的开始位置,即第 10 行的开始(320 字节);所以 g = 0x0000000000000000000000000000000000000000000000000000000000000140
。