本文内容来源于HiBlock区块链社区翻译小组,感谢全体译者的辛苦工作。
写在前面:HiBlock区块链社区成立了翻译小组,翻译区块链相关的技术文档及资料,本文为Solidity文档翻译的第七部分《应用二进制接口(ABI) 说明》,特发布出来邀请solidity爱好者、开发者做公开的审校,您可以添加微信baobaotalk_com,验证输入“solidity”,然后将您的意见和建议发送给我们,也可以在文末“留言”区留言,有效的建议我们会采纳及合并进下一版本,同时将送一份小礼物给您以示感谢。
1
基本设计
在 以太坊Ethereum 生态系统中, 应用二进制接口Application Binary Interface(ABI) 是从区块链外部与合约进行交互以及合约与合约间进行交互的一种标准方式。 数据会根据其类型按照这份手册中说明的方法进行编码。这种编码并不是可以自描述的,而是需要一种特定的概要(schema)来进行解码。
我们假定合约函数的接口都是强类型的,且在编译时是可知的和静态的;不提供自我检查机制。我们假定在编译时,所有合约要调用的其他合约接口定义都是可用的。
这份手册并不针对那些动态合约接口或者仅在运行时才可获知的合约接口。如果这种场景变得很重要,你可以使用 以太坊Ethereum 生态系统中其他更合适的基础设施来处理它们。
2
函数选择器
一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak(SHA-3)哈希的前 4 字节(高位在左的大端序)(译注:这里的“高位在左的大端序“,指最高位字节存储在最低位地址上的一种串行化编码方式,即高位字节在左)。 这种签名被定义为基础原型的规范表达,基础原型即是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。
3
参数编码
从第5字节开始是被编码的参数。这种编码也被用在其他地方,比如,返回值和事件的参数也会被用同样的方式进行编码,而用来指定函数的4个字节则不需要再进行编码。
4
类 型
以下是基础类型:
uint:M位的无符号整数,0
int:以 2 的补码作为符号的M位整数,0
address:除了字面上的意思和语言类型的区别以外,等价于uint160。在计算和函数选择器Function Selector中,通常使用address。
uint、int:uint256、int256 各自的同义词。在计算和函数选择器Function Selector中,通常使用uint256和int256。
bool:等价于uint8,取值限定为 0 或 1 。在计算和函数选择器Function Selector中,通常使用bool。
fixedx:M位的有符号的固定小数位的十进制数字8
ufixedx:无符号的fixedx。
fixed、ufixed:fixed128x19、ufixed128x19各自的同义词。在计算和函数选择器Function Selector中,通常使用fixed128x19 和ufixed128x19。
bytes:M字节的二进制类型,0
function:一个地址(20 字节)之后紧跟一个函数选择器Function Selector(4 字节)。编码之后等价于bytes24。
以下是定长数组类型:
[M]:有M个元素的定长数组,M > 0,数组元素为给定类型。
以下是非定长类型:
bytes:动态大小的字节序列。
string:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。
[]:元素为给定类型的变长数组。
可以将有限的若干类型放到一对括号中,用逗号分隔开,以此来构成一个 元组tuple:
(T1,T2,...,Tn):由T1,...,Tn,n >= 0构成的元组tuple。
用 元组tuple 构成 元组tuple、用 元组tuple 构成数组等等也是可能的。
5
编码的形式化说明
我们现在来正式讲述编码,它具有如下属性,如果参数是嵌套的数组,这些属性非常有用:
属性:
1、读取的次数取决于参数数组结构中的最大深度;也就是说,要取得a_i[k][l][r]需要读取 4 次。在先前的ABI版本中,在最糟的情况下,读取的次数会随着动态参数的总数而线性地增长。
2、一个变量或数组元素的数据,不会被插入其他的数据,并且是可以再定位的;也就是说,它们只会使用相对的“地址”。
我们需要区分静态和动态类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。
定义: 以下类型被称为“动态”:
bytes
string
任意类型T的变长数组T[]
任意动态类型T的定长数组T[k](k > 0)
Ti(1
所有其他类型都被称为“静态”。
定义: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-1)) tail(X(0)) ... tail(X(k-1))
这里,X(i)是 元组tuple 的第i个要素,并且 当Ti为静态类型时,head和tail被定义为head(X(i)) = enc(X(i)) and tail(X(i)) = ""(空字符串)
否则,比如Ti是动态类型时,它们被定义为head(X(i)) = enc(len(head(X(0)) ... head(X(k-1)) tail(X(0)) ... 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个元素组成的 元组tuple 那样被编码的。
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:enc(X)是在X的大端序编码的前边(左边)补充若干 0 值字节以使其长度成为 32 的倍数。
address:与uint160的情况相同。
int:enc(X)是在X的大端序的 2 的补码编码的高位(左侧)添加若干字节数据以使其长度成为 32 的倍数;对于负数,添加值为0xff(即 8 位全为 1,译者注)的字节数据,对于正数,添加 0 值(即 8 位全为 0,译者注)字节数据。
bool:与uint8的情况相同,1用来表示true,表示 false。
fixedx:enc(X)就是enc(X * 10**N),其中X * 10**N可以理解为int256。
fixed:与fixed128x19的情况相同。
ufixedx:enc(X)就是enc(X * 10**N),其中X * 10**N可以理解为uint256。
ufixed:与ufixed128x19的情况相同。
bytes:enc(X)就是X的字节序列加上为使长度称为 32 的倍数而添加的若干 0 值字节。
注意,对于任意的X,len(enc(X))都是 32 的倍数。
6
函数选择器 和参数编码
大体而言,一个以a_1, ..., a_n为参数的对 f 函数的调用,会被编码为function_selector(f) enc((a_1, ..., a_n))
f的返回值v_1, ..., v_k会被编码为enc((v_1, ..., v_k))
也就是说,返回值会被组合为一个 元组tuple 进行编码。
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 {}
}
这样,对于我们的例子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
8
动态类型的使用
取得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 字节)
最后,合并到一起的编码就是(为了清晰,在 函数选择器Function Selector 和每 32 字节之后加了换行):
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000
9
事 件
事件,是 以太坊Ethereum 的日志/事件监视协议的一个抽象。日志项提供了合约的地址、一系列的主题(最高 4 项)和一些任意长度的二进制数据。为了使用合适的类型数据结构来演绎这些功能(与接口定义一起),事件沿用了既存的 ABI 函数。
给定了事件名称和事件参数之后,我们将其分解为两个子集:已索引的和未索引的。已索引的部分,最多有 3 个,被用来与事件签名的 Keccak 哈希一起组成日志项的主题。未索引的部分就组成了事件的字节数组。
这样,一个使用 ABI 的日志项就可以描述为:
address:合约地址(由以太坊Ethereum真正提供);
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 哈希 而不是直接包含编码值。这样就允许应用程序更有效地查询动态长度类型的值(通过把编码值的哈希设定为主题), 但也使应用程序不能对它们还没查询过的已索引的值进行解码。
对于动态长度的类型,应用程序开发者面临在对预先设定的值(如果参数已被索引)的快速检索和对任意数据的清晰处理(需要参数不被索引)之间的权衡。 开发者们可以通过定义两个参数(一个已索引、一个未索引)保存同一个值的方式来解决这种权衡,从而既获得高效的检索又能清晰地处理任意数据。
10
JSON
合约接口的JSON格式是由一个函数和/或事件描述的数组所给定的。一个函数的描述是一个有如下字段的JSON对象:
type:"function"、"constructor"或"fallback"(未命名的 "缺省" 函数)
name:函数名称;
inputs:对象数组,每个数组对象会包含:
name:参数名称;
type:参数的权威类型(详见下文)
components:供 元组tuple 类型使用(详见下文)
outputs:一个类似于inputs的对象数组,如果函数无返回值时可以被省略;
payable:如果函数接受以太币Ether,为true;缺省为false;
stateMutability:为下列值之一:pure(指定为不读取区块链状态),view(指定为不修改区块链状态),nonpayable和payable(与上文payable一样)。
constant:如果函数被指定为pure或view则为true。
type可以被省略,缺省为"function"。
Constructor 和 fallback 函数没有name或outputs。Fallback 函数也没有inputs。
向 non-payable(即不接受 以太币Ether )的函数发送非零值的 以太币Ether 会导致其丢失。不要这么做。
一个事件描述是一个有极其相似字段的 JSON 对象:
type:总是"event";
name:事件名称;
inputs:对象数组,每个数组对象会包含:
name:参数名称;
type:参数的权威类型(相见下文);
components:供 元组tuple 类型使用(详见下文);
indexed:如果此字段是日志的一个主题,则为true;否则为false。
anonymous:如果事件被声明为anonymous,则为true。
例如,
pragma solidity ^0.4.0;
contract Test {
function Test() public { 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": []
}]
处理 元组tuple 类型
尽管名称被有意地不作为 ABI 编码的一部分,但将它们包含进JSON来显示给最终用户是非常合理的。其结构会按下列方式进行嵌套:
一个拥有name、type和潜在的components成员的对象描述了某种类型的变量。 直至到达一个 元组tuple 类型且到那点的存储在type属性中的字符串以tuple为前缀,也就是说,在tuple之后紧跟一个[]或有整数k的[k],才能确定一个 元组tuple。 元组tuple 的组件元素会被存储在成员components中,它是一个数组类型,且与顶级对象具有同样的结构,只是在这里不允许已索引的(indexed)数组元素。
作为例子,代码
pragma solidity ^0.4.19;
pragma experimental ABIEncoderV2;
contract Test {
struct S { uint a; uint[] b; T[] c; }
struct T { uint x; uint y; }
function f(S s, T t, uint a) public { }
function g() public returns (S s, T t, uint a) {}
}
可由如下 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": []
}
]
11
非标准打包模式
Solidity 支持一种非标准打包模式:
函数选择器 不进行编码,
长度低于 32 字节的类型,既不会进行补 0 操作,也不会进行符号扩展,以及
动态类型会直接进行编码,并且不包含长度信息。
例如,对int1, bytes1, uint16, string用数值-1, 0x42, 0x2424, "Hello, world!"进行编码将生成如下结果
更具体地说,每个静态大小的类型都尽可能多地按它们的数值范围使用了字节数,而动态大小的类型,像string、bytes或uint[],在编码时没有包含其长度信息。 这意味着一旦有两个动态长度的元素,编码就会变得有歧义了。
领取专属 10元无门槛券
私享最新 技术干货