ABI全称应用二进制接口说明。
调用一个函数的数据的前4个字节,指定了要调用的函数。
函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。
函数的返回类型并不是这个签名的一部分。然而JSON 描述中包含了即包含了输入也包含了输出。
我们需要区分静态和动态类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。以下类型被称为“动态”:
bytesstring- 任意类型 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。