HiBlock区块链社区

应用二进制接口(ABI) 说明——Solidity中文文档(7)

2018-05-04  本文已影响5人  宇宙永恒
image

写在前面: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

类 型

以下是基础类型:

以下是定长数组类型:

以下是非定长类型:

可以将有限的若干类型放到一对括号中,用逗号分隔开,以此来构成一个 元组tuple:

用 元组tuple 构成 元组tuple、用 元组tuple 构成数组等等也是可能的。

image

5

编码的形式化说明

我们现在来正式讲述编码,它具有如下属性,如果参数是嵌套的数组,这些属性非常有用:

属性:

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

定义: 以下类型被称为“动态”:

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

定义: len(a) 是一个二进制字符串 a 的字节长度。len(a) 的类型被呈现为 uint256。

我们把实际的编码 enc 定义为一个由ABI类型到二进制字符串的值的映射;因而,当且仅当 X 的类型是动态的,len(enc(X)) (即 X 经编码后的实际长度,译者注)才会依赖于 X 的值。

定义: 对任意ABI值 X,我们根据 X 的实际类型递归地定义 enc(X)。

注意,对于任意的 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 字节,可以分解为:

合起来就是:

0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001

它返回一个 bool。比如它返回 false,那么它的输出将是一个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000,一个bool值。

如果我们想用 ["abc", "def"] 做参数调用 bar,我们总共需要传送68字节,可以分解为:

合起来就是:

0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000

如果我们想用 "dave"、true 和 [1,2,3] 作为参数调用 sam,我们总共需要传送 292 字节,可以分解为:

合起来就是:

0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003

8

动态类型的使用

用参数 (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 字节),这就是:

在此之后,跟着第一个动态参数的数据部分 [0x456, 0x789]:

最后,我们将第二个动态参数的数据部分 "Hello, world!" 进行编码:

最后,合并到一起的编码就是(为了清晰,在 函数选择器Function Selector 和每 32 字节之后加了换行):

0x8be65246
 0000000000000000000000000000000000000000000000000000000000000123
 0000000000000000000000000000000000000000000000000000000000000080
 3132333435363738393000000000000000000000000000000000000000000000
 00000000000000000000000000000000000000000000000000000000000000e0
 0000000000000000000000000000000000000000000000000000000000000002
 0000000000000000000000000000000000000000000000000000000000000456
 0000000000000000000000000000000000000000000000000000000000000789
 000000000000000000000000000000000000000000000000000000000000000d
 48656c6c6f2c20776f726c642100000000000000000000000000000000000000

9

事 件

事件,是 以太坊Ethereum 的日志/事件监视协议的一个抽象。日志项提供了合约的地址、一系列的主题(最高 4 项)和一些任意长度的二进制数据。为了使用合适的类型数据结构来演绎这些功能(与接口定义一起),事件沿用了既存的 ABI 函数。

给定了事件名称和事件参数之后,我们将其分解为两个子集:已索引的和未索引的。已索引的部分,最多有 3 个,被用来与事件签名的 Keccak 哈希一起组成日志项的主题。未索引的部分就组成了事件的字节数组。

这样,一个使用 ABI 的日志项就可以描述为:

对于所有定长的Solidity类型,EVENT_INDEXED_ARGS 数组会直接包含32字节的编码值。然而,对于 动态长度的类型 ,包含 string、bytes 和数组, EVENT_INDEXED_ARGS 会包含编码值的 Keccak 哈希 而不是直接包含编码值。这样就允许应用程序更有效地查询动态长度类型的值(通过把编码值的哈希设定为主题), 但也使应用程序不能对它们还没查询过的已索引的值进行解码。

对于动态长度的类型,应用程序开发者面临在对预先设定的值(如果参数已被索引)的快速检索和对任意数据的清晰处理(需要参数不被索引)之间的权衡。 开发者们可以通过定义两个参数(一个已索引、一个未索引)保存同一个值的方式来解决这种权衡,从而既获得高效的检索又能清晰地处理任意数据。

10

JSON

合约接口的JSON格式是由一个函数和/或事件描述的数组所给定的。一个函数的描述是一个有如下字段的JSON对象:

type 可以被省略,缺省为 "function"。

Constructor 和 fallback 函数没有 name 或 outputs。Fallback 函数也没有 inputs。

向 non-payable(即不接受 以太币Ether )的函数发送非零值的 以太币Ether 会导致其丢失。不要这么做。

一个事件描述是一个有极其相似字段的 JSON 对象:

例如,

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 支持一种非标准打包模式:

例如,对 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[],在编码时没有包含其长度信息。 这意味着一旦有两个动态长度的元素,编码就会变得有歧义了。

延伸阅读:智能合约-Solidity官方文档(1)

安装Solidity编译器-Solidity官方文档(2)

根据例子学习Solidity-Solidity官方文档(3)

深入理解Solidity之源文件及合约结构——Solidity中文文档(4)

安全考量——Solidity中文文档(5)

合约的元数据——Solidity中文文档(6)

本文内容来源于HiBlock区块链社区翻译小组,感谢全体译者的辛苦工作。

:本文为solidity翻译的第七部分《应用二进制接口(ABI) 说明》,特发布出来邀请solidity爱好者、开发者做公开的审校,您可以添加微信baobaotalk_com,验证输入“solidity”,然后将您的意见和建议发送给我们,也可在文末“留言”区留言,或通过原文链接访问我们的Github。有效的建议我们会收纳并及时改进,同时将送一份小礼物给您以示感谢。

活动推荐

主题:Blockathon,挑战区块链开发,敢不敢来!(点击了解详情)

5月25-27日,Blockathon2018北京站,招募100名开发者一起挑战区块链开发。

开发者免费,报名需审核。识别下图二维码或点击“阅读原文”即可报名参加。

image

点击“阅读原文”即可报名。

上一篇 下一篇

猜你喜欢

热点阅读