应用二进制接口说明¶
基本设计¶
在 以太坊 生态系统中, 应用二进制接口 是从区块链外部与合约进行交互以及合约与合约间进行交互的一种标准方式。 数据会根据其类型按照这份手册中说明的方法进行编码。这种编码并不是可以自描述的,而是需要一种特定的概要(schema)来进行解码。
我们假定合约函数的接口都是强类型的,且在编译时是可知的和静态的;不提供自我检查机制。我们假定在编译时,所有合约要调用的其他合约接口定义都是可用的。
这份手册并不针对那些动态合约接口或者仅在运行时才可获知的合约接口。如果这种场景变得很重要,你可以使用 以太坊 生态系统中其他更合适的基础设施来处理它们。
函数选择器¶
一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak 哈希的前 4 字节(高位在左的大端序) (译注:这里的“高位在左的大端序“,指最高位字节存储在最低位地址上的一种串行化编码方式,即高位字节在左)。 这种签名被定义为基础原型的规范表达,基础原型即是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。
注解
函数的返回类型并不是这个签名的一部分。在 Solidity 的函数重载 中,返回值并没有被考虑。这是为了使对函数调用的解析保持上下文无关。 然而 应用二进制接口 的 JSON 描述中包含了即包含了输入也包含了输出。(参考 JSON ABI)。
参数编码¶
从第5字节开始是被编码的参数。这种编码也被用在其他地方,比如,返回值和事件的参数也会被用同样的方式进行编码,而用来指定函数的4个字节则不需要再进行编码。
类型¶
以下是基础类型:
uint<M>
:M
位的无符号整数,0 < M <= 256
、M % 8 == 0
。例如:uint32
,uint8
,uint256
。int<M>
:以 2 的补码作为符号的M
位整数,0 < M <= 256
、M % 8 == 0
。address
:除了字面上的意思和语言类型的区别以外,等价于uint160
。在计算和 函数选择器 中,通常使用address
。uint
、int
:uint256
、int256
各自的同义词。在计算和 函数选择器 中,通常使用uint256
和int256
。bool
:等价于uint8
,取值限定为 0 或 1 。在计算和 函数选择器 中,通常使用bool
。fixed<M>x<N>
:M
位的有符号的固定小数位的十进制数字8 <= M <= 256
、M % 8 == 0
、且0 < N <= 80
。其值v
即是v / (10 ** N)
。(也就是说,这种类型是由 M 位的二进制数据所保存的,有 N 位小数的十进制数值。译者注。)ufixed<M>x<N>
:无符号的fixed<M>x<N>
。fixed
、ufixed
:fixed128x18
、ufixed128x18
各自的同义词。在计算和 函数选择器 中,通常使用fixed128x18
和ufixed128x18
。bytes<M>
:M
字节的二进制类型,0 < M <= 32
。function
:一个地址(20 字节)之后紧跟一个 函数选择器 (4 字节)。编码之后等价于bytes24
。
以下是定长数组类型:
<type>[M]
:有M
个元素的定长数组,M >= 0
,数组元素为给定类型。注解
尽管此ABI规范可以表示零个元素的定长数组,但编译器不支持它们。
以下是非定长类型:
bytes
:动态大小的字节序列。string
:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。<type>[]
:元素为给定类型的变长数组。
可以将若干类型放到一对括号中,用逗号分隔开,以此来构成一个 元组:
(T1,T2,...,Tn)
:由T1
,…,Tn
,n >= 0
构成的 元组。
用 元组 构成 元组、用 元组 构成数组等等也是可能的。另外也可以构成“零元组(zero-tuples)”,就是 n = 0
的情况。
Mapping 到 ABI 类型¶
Solidity supports all the types presented above with the same names with the exception of tuples. On the other hand, some Solidity types are not supported by the ABI. The following table shows on the left column Solidity types that are not part of the ABI, and on the right column the ABI types that represent them.
Solidity | ABI |
---|---|
address payable | address |
contract | address |
enum | uint8 |
struct | tuple |
警告
Before version 0.8.0
enums could have more than 256 members and were represented by the
smallest integer type just big enough to hold the value of any member.
编码的设计准则¶
The encoding is designed to have the following properties, which are especially useful if some arguments are nested arrays:
1. The number of reads necessary to access a value is at most the depth of the value inside the argument array structure, i.e. four reads are needed to retrieve
a_i[k][l][r]
. In a previous version of the ABI, the number of reads scaled linearly with the total number of dynamic parameters in the worst case.2. The data of a variable or array element is not interleaved with other data and it is relocatable, i.e. it only uses relative “addresses”.
编码的形式化说明¶
我们现在来正式讲述编码,它具有如下属性,如果参数是嵌套的数组,这些属性非常有用:
属性:
1、读取的次数取决于参数数组结构中的最大深度;也就是说,要取得
a_i[k][l][r]
需要读取 4 次。在先前的ABI版本中,在最糟的情况下,读取的次数会随着动态参数的总数而线性地增长。2、一个变量或数组元素的数据,不会被插入其他的数据,并且是可以再定位的;也就是说,它们只会使用相对的“地址”。
我们需要区分静态和动态类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。
定义: 以下类型被称为“动态”:
bytes
string
- 任意类型 T 的变长数组
T[]
- 任意动态类型 T 的定长数组
T[k]
(k >= 0
) - 由动态的
Ti
(1 <= i <= k
)构成的 元组(T1,...,Tk)
所有其他类型都被称为“静态”。
定义: len(a)
是一个二进制字符串 a
的字节长度。len(a)
的类型被呈现为 uint256
。
我们把实际的编码 enc
定义为一个由ABI类型到二进制字符串的值的映射;因而,当且仅当 X
的类型是动态的,len(enc(X))
(即 X
经编码后的实际长度,译者注)才会依赖于 X
的值。
定义: 对任意ABI值 X
,我们根据 X
的实际类型递归地定义 enc(X)
。
(T1,...,Tk)
对于k >= 0
且任意类型T1
,…,Tk
enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k))
这里,
X = (X(1), ..., X(k))
,并且 当Ti
为静态类型时,head
和tail
被定义为head(X(i)) = enc(X(i))
andtail(X(i)) = ""
(空字符串)否则,比如
Ti
是动态类型时,它们被定义为head(X(i)) = enc(len(head(X(1)) ... head(X(k-1)) tail(X(1)) ... tail(X(i-1))))
tail(X(i)) = enc(X(i))
注意,在动态类型的情况下,由于 head 部分的长度仅取决于类型而非值,所以
head(X(i))
是定义明确的。它的值是从enc(X)
的开头算起的,tail(X(i))
的起始位在enc(X)
中的偏移量。T[k]
对于任意T
和k
:enc(X) = enc((X[0], ..., X[k-1]))
即是说,它就像是个由相同类型的
k
个元素组成的 元组 那样被编码的。T[]
当X
有k
个元素(k
被呈现为类型uint256
):enc(X) = enc(k) enc([X[1], ..., X[k]])
即是说,它就像是个由静态大小
k
的数组那样被编码的,且由元素的个数作为前缀。具有
k
(呈现为类型uint256
)长度的bytes
:enc(X) = enc(k) pad_right(X)
,即是说,字节数被编码为uint256
,紧跟着实际的X
的字节码序列,再在高位(左侧)补上可以使len(enc(X))
成为 32 的倍数的最少数量的 0 值字节数据。string
:enc(X) = enc(enc_utf8(X))
,即是说,X
被 utf-8 编码,且在后续编码中将这个值解释为bytes
类型。注意,在随后的编码中使用的长度是其 utf-8 编码的字符串的字节数,而不是其字符数。uint<M>
:enc(X)
是在X
的大端序编码的高位(左侧)补充若干 0 值字节以使其长度成为 32 字节。address
:与uint160
的情况相同。int<M>
:enc(X)
是在X
的大端序的 2 的补码编码的高位(左侧)添加若干字节数据以使其长度成为 32 字节;对于负数,添加值为0xff
(即 8 位全为 1,译者注)的字节数据,对于非负数,添加 0 值(即 8 位全为 0,译者注)字节数据。bool
:与uint8
的情况相同,1
用来表示true
,0
表示false
。fixed<M>x<N>
:enc(X)
就是enc(X * 10**N)
,其中X * 10**N
可以理解为int256
。fixed
:与fixed128x18
的情况相同。ufixed<M>x<N>
:enc(X)
就是enc(X * 10**N)
,其中X * 10**N
可以理解为uint256
。ufixed
:与ufixed128x18
的情况相同。bytes<M>
:enc(X)
就是X
的字节序列加上为使长度成为 32 字节而添加的若干 0 值字节。
注意,对于任意的 X
,len(enc(X))
都是 32 的倍数。
函数选择器 和参数编码¶
大体而言,一个以 a_1, ..., a_n
为参数的对 f
函数的调用,会被编码为
function_selector(f) enc((a_1, ..., a_n))
f
的返回值 v_1, ..., v_k
会被编码为
enc((v_1, ..., v_k))
也就是说,返回值会被组合为一个 元组 进行编码。
例子¶
给定一个合约:
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 {}
}
这样,对于我们的例子 Foo
,如果我们想用 69
和 true
做参数调用 baz
,我们总共需要传送 68 字节,可以分解为:
0xcdcd77c0
:方法ID。这源自ASCII格式的baz(uint32,bool)
签名的 Keccak 哈希的前 4 字节。0x0000000000000000000000000000000000000000000000000000000000000045
:第一个参数,一个被用 0 值字节补充到 32 字节的 uint32 值69
。0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数,一个被用 0 值字节补充到 32 字节的 boolean 值true
。
合起来就是:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
它返回一个 bool
。比如它返回 false
,那么它的输出将是一个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000
,一个bool值。
如果我们想用 ["abc", "def"]
做参数调用 bar
,我们总共需要传送68字节,可以分解为:
0xfce353f6
:方法ID。源自bar(bytes3[2])
的签名。0x6162630000000000000000000000000000000000000000000000000000000000
:第一个参数的第一部分,一个bytes3
值"abc"
(左对齐)。0x6465660000000000000000000000000000000000000000000000000000000000
:第一个参数的第二部分,一个bytes3
值"def"
(左对齐)。
合起来就是:
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
如果我们想用 "dave"
、true
和 [1,2,3]
作为参数调用 sam
,我们总共需要传送 292 字节,可以分解为:
0xa5643bf2
:方法ID。源自sam(bytes,bool,uint256[])
的签名。注意,uint
被替换为了它的权威代表uint256
。0x0000000000000000000000000000000000000000000000000000000000000060
:第一个参数(动态类型)的数据部分的位置,即从参数编码块开始位置算起的字节数。在这里,是0x60
。0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数:boolean 的 true。0x00000000000000000000000000000000000000000000000000000000000000a0
:第三个参数(动态类型)的数据部分的位置,由字节数计量。在这里,是0xa0
。0x0000000000000000000000000000000000000000000000000000000000000004
:第一个参数的数据部分,以字节数组的元素个数作为开始,在这里,是 4。0x6461766500000000000000000000000000000000000000000000000000000000
:第一个参数的内容:"dave"
的 UTF-8 编码(在这里等同于 ASCII 编码),并在右侧(低位)用 0 值字节补充到 32 字节。0x0000000000000000000000000000000000000000000000000000000000000003
:第三个参数的数据部分,以数组的元素个数作为开始,在这里,是 3。0x0000000000000000000000000000000000000000000000000000000000000001
:第三个参数的第一个数组元素。0x0000000000000000000000000000000000000000000000000000000000000002
:第三个参数的第二个数组元素。0x0000000000000000000000000000000000000000000000000000000000000003
:第三个参数的第三个数组元素。
合起来就是:
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
(第四个参数的数据部分起始位置的偏移量 = 第一个动态参数的数据部分起始位置的偏移量 + 第一个动态参数的数据部分的长度 = 4*32 + 3*32,参考后文)
在此之后,跟着第一个动态参数的数据部分 [0x456, 0x789]
:
0x0000000000000000000000000000000000000000000000000000000000000002
(数组元素个数,2)0x0000000000000000000000000000000000000000000000000000000000000456
(第一个数组元素)0x0000000000000000000000000000000000000000000000000000000000000789
(第二个数组元素)
最后,我们将第二个动态参数的数据部分 "Hello, world!"
进行编码:
0x000000000000000000000000000000000000000000000000000000000000000d
(元素个数,在这里是字节数:13)0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000
("Hello, world!"
从右边补充到 32 字节)
最后,合并到一起的编码就是(为了清晰,在 函数选择器 和每 32 字节之后加了换行):
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000
让我们使用相同的原理来对一个签名为 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]]
编码后的具体数据:
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
, d
和 e
:
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"]
的偏移量 f
和 g
。汇编数据的正确顺序如下:
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
。
事件¶
事件,是 以太坊 的日志/事件监视协议的一个抽象。日志项提供了合约的地址、一系列的主题(最高 4 项)和一些任意长度的二进制数据。为了使用合适的类型数据结构来演绎这些功能(与接口定义一起),事件沿用了既存的 ABI 函数。
给定了事件名称和事件参数之后,我们将其分解为两个子集:已索引的和未索引的。已索引的部分,最多有 3 个,被用来与事件签名的 Keccak 哈希一起组成日志项的主题。未索引的部分就组成了事件的字节数组。
这样,一个使用 ABI 的日志项就可以描述为:
address
:合约地址(由 以太坊 真正提供);topics[0]
:keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")")
(canonical_type_of
是一个可以返回给定参数的权威类型的函数,例如,对uint indexed foo
它会返回uint256
)。如果事件被声明为anonymous
,那么topics[0]
不会被生成;topics[n]
:EVENT_INDEXED_ARGS[n - 1]
(EVENT_INDEXED_ARGS
是已索引的EVENT_ARGS
);data
:abi_serialise(EVENT_NON_INDEXED_ARGS)
(EVENT_NON_INDEXED_ARGS
是未索引的EVENT_ARGS
,abi_serialise
是一个用来从某个函数返回一系列类型值的ABI序列化函数,就像上文所讲的那样)。
对于所有定长的Solidity类型,EVENT_INDEXED_ARGS
数组会直接包含32字节的编码值。然而,对于 动态长度的类型 ,包含 string
、bytes
和数组,
EVENT_INDEXED_ARGS
会包含编码值的 Keccak 哈希 而不是直接包含编码值。这样就允许应用程序更有效地查询动态长度类型的值(通过把编码值的哈希设定为主题),
但也使应用程序不能对它们还没查询过的已索引的值进行解码。对于动态长度的类型,应用程序开发者面临在对预先设定的值(如果参数已被索引)的快速检索和对任意数据的清晰处理(需要参数不被索引)之间的权衡。
开发者们可以通过定义两个参数(一个已索引、一个未索引)保存同一个值的方式来解决这种权衡,从而既获得高效的检索又能清晰地处理任意数据。
JSON¶
合约接口的JSON格式是由一个函数和/或事件描述的数组所给定的。一个函数的描述是一个有如下字段的JSON对象:
type
:"function"
、"constructor"
或"fallback"
(未命名的 “缺省” 函数)name
:函数名称;inputs
:对象数组,每个数组对象会包含:name
:参数名称;type
:参数的权威类型(详见下文)components
:供 元组 类型使用(详见下文)
outputs
:一个类似于inputs
的对象数组,如果函数无返回值时可以被省略;payable
:如果函数接受 以太币 ,为true
;缺省为false
;stateMutability
:为下列值之一:pure
(指定为不读取区块链状态),view
(指定为不修改区块链状态),nonpayable
(默认值:不接收 Ether)和payable
(与上文payable
一样)。
type
可以被省略,缺省为 "function"
。
构造函数 Constructor 和 fallback 函数没有 name
或 outputs
。Fallback 函数也没有 inputs
。
注解
向 non-payable(即不接受 以太币 )的函数发送非零值的 以太币 会回退交易。
注解
状态可变性 nonpayable
是默认的,不用显示指定。
一个事件描述是一个有极其相似字段的 JSON 对象:
type
:总是"event"
;name
:事件名称;inputs
:对象数组,每个数组对象会包含:name
:参数名称;type
:参数的权威类型(相见下文);components
:供 元组 类型使用(详见下文);indexed
:如果此字段是日志的一个主题,则为true
;否则为false
。
anonymous
:如果事件被声明为anonymous
,则为true
。
例如,
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.7.0;
contract Test {
constructor () { b = 0x12345678901234567890123456789012; }
event Event(uint indexed a, bytes32 b);
event Event2(uint indexed a, bytes32 b);
function foo(uint a) public { Event(a, b); }
bytes32 b;
}
可由如下 JSON 来表示:
[{
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event2"
}, {
"type":"function",
"inputs": [{"name":"a","type":"uint256"}],
"name":"foo",
"outputs": []
}]
处理 元组 类型¶
尽管名称被有意地不作为 ABI 编码的一部分,但将它们包含进JSON来显示给最终用户是非常合理的。其结构会按下列方式进行嵌套:
一个拥有 name
、 type
和潜在的 components
成员的对象描述了某种类型的变量。
直至到达一个 元组 类型且到那点的存储在 type
属性中的字符串以 tuple
为前缀,也就是说,在 tuple
之后紧跟一个 []
或有整数 k
的 [k]
,才能确定一个 元组。
元组 的组件元素会被存储在成员 components
中,它是一个数组类型,且与顶级对象具有同样的结构,只是在这里不允许已索引的(indexed
)数组元素。
作为例子,代码
pragma solidity >0.7.4;
pragma abicoder v2;
contract Test {
struct S { uint a; uint[] b; T[] c; }
struct T { uint x; uint y; }
function f(S memory, T memory, uint) public pure { }
function g() public pure returns (S memory, T memoryt, uint) {}
}
可由如下 JSON 来表示:
[
{
"name": "f",
"type": "function",
"inputs": [
{
"name": "s",
"type": "tuple",
"components": [
{
"name": "a",
"type": "uint256"
},
{
"name": "b",
"type": "uint256[]"
},
{
"name": "c",
"type": "tuple[]",
"components": [
{
"name": "x",
"type": "uint256"
},
{
"name": "y",
"type": "uint256"
}
]
}
]
},
{
"name": "t",
"type": "tuple",
"components": [
{
"name": "x",
"type": "uint256"
},
{
"name": "y",
"type": "uint256"
}
]
},
{
"name": "a",
"type": "uint256"
}
],
"outputs": []
}
]
非标准打包模式¶
Solidity 支持一种非标准打包模式:
- 函数选择器 不进行编码,
- 长度低于 32 字节的类型,既不会进行补 0 操作,也不会进行符号扩展,以及
- 动态类型会直接进行编码,并且不包含长度信息。
例如,对 int1, bytes1, uint16, string
用数值 -1, 0x42, 0x2424, "Hello, world!"
进行编码将生成如下结果
0xff42242448656c6c6f2c20776f726c6421
^^ int1(-1)
^^ bytes1(0x42)
^^^^ uint16(0x2424)
^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field
更具体地说,每个静态大小的类型都尽可能多地按它们的数值范围使用了字节数,而动态大小的类型,像 string
、 bytes
或 uint[]
,在编码时没有包含其长度信息。
这意味着一旦有两个动态长度的元素,编码就会变得有歧义了。