ABI说明

Catalogue
  1. 1. 简单例子
  2. 2. baz调用
  3. 3. bar调用
  4. 4. sam(bytes,bool,uint256[])调用
  5. 5. 动态类型例子
  6. 6. g(uint[][],string[])调用
  7. 7. 参考资料

ABI全称应用二进制接口说明。

调用一个函数的数据的前4个字节,指定了要调用的函数。

函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。

函数的返回类型并不是这个签名的一部分。然而JSON 描述中包含了即包含了输入也包含了输出。

我们需要区分静态和动态类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。以下类型被称为“动态”:

  • bytes
  • string
  • 任意类型 T 的变长数组 T[]
  • 任意动态类型 T 的定长数组 T[k]k >= 0
  • 由动态的 Ti1 <= i <= k)构成的 元组tuple (T1,...,Tk)

所有其他类型都被称为“静态”。

简单例子

给定一个合约:

1
2
3
4
5
6
7
pragma solidity ^0.4.16;

contract Foo {
function bar(bytes3[2]) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes, bool, uint[]) public pure {}
}

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则直接编码,可以分解为:

  1. 0xa5643bf2:方法ID。源自 sam(bytes,bool,uint256[]) 的签名。注意,uint 被替换为了它的权威代表 uint256
  2. 0x0000000000000000000000000000000000000000000000000000000000000060:第一个参数(动态类型)的数据部分的位置,即从参数编码块开始位置算起的字节数。在这里,是 0x60 。即从此处开始(包含)到第5行(不包含)的偏移量。
  3. 0x0000000000000000000000000000000000000000000000000000000000000001:第二个参数:boolean 的 true。
  4. 0x00000000000000000000000000000000000000000000000000000000000000a0:第三个参数(动态类型)的数据部分的位置,由字节数计量。在这里,是 0xa0
  5. 0x0000000000000000000000000000000000000000000000000000000000000004:第一个参数的数据部分,以字节数组的元素个数作为开始,在这里,是 4。
  6. 0x6461766500000000000000000000000000000000000000000000000000000000:第一个参数的内容:"dave"的 UTF-8 编码(在这里等同于 ASCII 编码),并在右侧(低位)用 0 值字节补充到 32 字节。
  7. 0x0000000000000000000000000000000000000000000000000000000000000003:第三个参数的数据部分,以数组的元素个数作为开始,在这里,是 3。
  8. 0x0000000000000000000000000000000000000000000000000000000000000001:第三个参数的第一个数组元素。
  9. 0x0000000000000000000000000000000000000000000000000000000000000002:第三个参数的第二个数组元素。
  10. 0x0000000000000000000000000000000000000000000000000000000000000003:第三个参数的第三个数组元素。

合起来就是:

1
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003

动态类型例子

用参数 (0x123, [0x456, 0x789], "1234567890", "Hello, world!") 进行对函数 f(uint,uint32[],bytes10,bytes) 的调用会通过以下方式进行编码:

取得 sha3("f(uint256,uint32[],bytes10,bytes)") 的前 4 字节,也就是 0x8be65246。 然后我们对所有 4 个参数的头部进行编码。对静态类型 uint256bytes10 是可以直接传过去的值;对于动态类型 uint32[]bytes,我们使用的字节数偏移量是它们的数据区域的起始位置,由需编码的值的开始位置算起(也就是说,不计算包含了函数签名的前 4 字节),这就是:

  • 0x00000000000000000000000000000000000000000000000000000000000001230x123 补充到 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
2
3
4
5
6
7
8
9
10
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000

g(uint[][],string[])调用

来对一个签名为 g(uint[][],string[]),参数值为 ([[1, 2], [3]], ["one", "two", "three"]) 的函数来进行编码;但从最原子的部分开始:

首先我们将第一个根数组 [[1, 2], [3]] 的第一个嵌入的动态数组 [1, 2] 的长度和数据进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第一个数组中的元素数量 2;元素本身是 12)
  • 0x0000000000000000000000000000000000000000000000000000000000000001 (第一个元素)
  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第二个元素)

然后我们将第一个根数组 [[1, 2], [3]] 的第二个潜入的动态数组 [3] 的长度和数据进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000001 (第二个数组中的元素数量 1;元素数据是 3)
  • 0x0000000000000000000000000000000000000000000000000000000000000003 (第一个元素)

然后我们需要找到动态数组 [1, 2][3] 的偏移量。要计算这个偏移量,我们可以来看一下第一个根数组 [[1, 2], [3]] 编码后的具体数据:

1
2
3
4
5
6
7
0 - a                                                                - [1, 2] 的偏移量
1 - b - [3] 的偏移量
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的计数
3 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
4 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的计数
6 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码

偏移量 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, de

1
2
3
4
5
6
7
8
9
0 - c                                                                - "one" 的偏移量
1 - d - "two" 的偏移量
2 - e - "three" 的偏移量
3 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
6 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
7 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
8 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码

偏移量 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"] 的偏移量 fg。汇编数据的正确顺序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0x2289b18c                                                            - 函数签名
0 - f - [[1, 2], [3]] 的偏移量
1 - g - ["one", "two", "three"] 的偏移量
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [[1, 2], [3]] 的元素计数
3 - 0000000000000000000000000000000000000000000000000000000000000040 - [1, 2] 的偏移量
4 - 00000000000000000000000000000000000000000000000000000000000000a0 - [3] 的偏移量
5 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的元素计数
6 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
7 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
8 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的元素计数
9 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码
10 - 0000000000000000000000000000000000000000000000000000000000000003 - ["one", "two", "three"] 的元素计数
11 - 0000000000000000000000000000000000000000000000000000000000000060 - "one" 的偏移量
12 - 00000000000000000000000000000000000000000000000000000000000000a0 - "two" 的偏移量
13 - 00000000000000000000000000000000000000000000000000000000000000e0 - "three" 的偏移量
14 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
16 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
17 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
18 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
19 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码

偏移量 f 指向数组 [[1, 2], [3]] 内容的开始位置,即第 2 行的开始(64 字节);所以 f = 0x0000000000000000000000000000000000000000000000000000000000000040

偏移量 g 指向数组 ["one", "two", "three"] 内容的开始位置,即第 10 行的开始(320 字节);所以 g = 0x0000000000000000000000000000000000000000000000000000000000000140

参考资料