应用二进制接口描述

翻译原文

date:20170801

基本设计

在以太坊生态系统中,应用二进制接口是跟合约交互的标准途径。从区块链外部或者合约之间的交互都可以通过这个方式。数据根据它的类型编码,我们将在这个文章中详细论述。编码不是自我描述的,所以如果要解码就需要模式(schema)。

我们假设合约的接口函数是强类型的,在编译的时候推断,且是静态的。没有提供反省机制。我们假设所有的合约都定义这样的接口,使得任何合约都可以在编译的时候调用。

该描述文档不描述接口是动态的合约或者另一种情况--在运行的时候才知道。这些情况是很重要的,因为他们可以构建以太坊生态系统的丰富的内建设施。(?This specification does not address contracts whose interface is dynamic or otherwise known only at run-time. Should these cases become important they can be adequately handled as facilities built within the Ethereum ecosystem.)

函数选择器

函数调用的调用数据的前四个字节指出了所调用的函数。它是函数签名的keccak(SHA-3)的前四个字节(左边,高位,大端存储)。签名被定义为基本原型的权威表达式。例如,函数名称和用括号括起来的参数类型。参数类型通过一个逗号隔开-没有使用空格。

参数编码

从第五个字节开始,就是参数的编码。这个编码除了用在前四个字节指定函数外,也用在了其他地方。例如,返回值和时间参数也是用同样的方式。

类型

有下面几种基础类型:

  • uint<M>:M位的无符号类型的整形,0 < M <= 256,M % 8 == 0,例如,uint32,uint8,uint256。
  • int<M>:M位的有符号整形,0 < M <= 256,M % 8 == 0。
  • address:等价于uint160,except for the assumed interpretation and language typing.
  • uintint:各自是uint256,int256的同义词(不是用来计算函数选择器)
  • bool:等价于 uint8,值被限制为0和1。
  • fixed<M>x<N>:M位的有符号,固定点的小数,0 < M <= 256,M % 8 == 0,且0 < N <= 80, 意味着值v为v/(10 ** N).(?signed fixed-point decimal number of M bits, 0 < M <= 256, M % 8 ==0, and 0 < N <= 80, which denotes the value v as v / (10 ** N).)
  • ufixed<M>x<N>fixed<M>x<N>的无符号变量
  • fixedufixed:各自等价于fiexed128x19ufixed128x19(不是用来计算函数选择器)
  • bytes<M>:M位的二进制类型,0 < M <= 32.
  • function:等价于bytes24,一个地址,加函数选择器

下面是(固定大小的)数组类型:

  • <type>[M]:给定类型的固定长度的数组

下面是非固定大小的类型:

  • bytes: 动态大小的字符串
  • string:动态大小的unicode字符串,假设是UTF-8编码
  • <type>[]:给定类型的动态长度的数组

类型可以结合为匿名结构体,通过把有限数量的参数用圆括号包围,通过逗号隔开:

  • (T1,T2,...,Tn):匿名结构体(有序元组),由类型T1,...,Tn,n >= 0

可以组合成结构体有结构体,结构体数组等结构。

编码的正式描述(?该章节尚未理解,翻译出错的概率很大,读者可以直接查看原文)

我们现在开始正式描述编码,它遵循下面的准则。如果参数中有嵌套数组,它们非常有用:

属性:

1. 为了获取一个值而读取的次数,差不多是值在参数数组结构里的深度。例如,如果要获取a_i[k][l][r],就要读取4次。在之前版本的ABI中,最坏的情况下,读取次数和动态参数的总数线性相关。
2.变量的值或者数组元素不会插入其他值,而且可以重新定位。例如,它只使用相关的“addresses”。

我们区分了静态和动态类型。静态类型编码在当前位置。而动态类型编码在当前区块之后的单独分配的位置,

定义:以下的类型被称为是“动态的”:* bytes* string* 对于任意类型T的数组T[] 任意动态类型T的数组T[k],且K > 0* (T1,....,Tk),对于1 <= i <= k,如果Ti都是动态的。

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

定义:len(a)是任意字符串 a 的字节数。len(a)的类型假设为uint256

我们定义enc,真实的编码,作为ABI类型的映射值到二进制字符串,所以len(enc(X)),当且仅当X是动态的时候,依赖于X的值。

定义:对于任意的ABI的值X,我们递归定义enc(X)依赖于X的类型

  • 对于任意k>=0,任意类型的T1,...Tk
    enc(X) = head(X(1)) ... head(X(k-1)) tail(X(0)) .... tail(X(k-1))
    其中X(i)是组件的第i个值,head和tail定义为Ti的静态类型,为
    head(X(i)) = enc(X(i)) and tail(X(i)) = “” (空字符串)
    并且
    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))
    另外一种情况,例如 如果Ti是一个动态类型。
    注意,在动态的情况下,head(X(i))是很清晰,因为头部的长度只依赖于类型而不是值。值是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长度(被假设为uint256)的bytes
    enc(X) = enc(k) pad_right(X),例如,bytes的数量被编码为a
    uint256 继承了真实的值X作为byte序列,继承了最小数量的零字节,因此len(enc(X))是32的倍数。
  • string:enc(X)=enc(enc_utf8(X)),例如X是utf-8编码,值是bytes类型,并且更进一步的编码。注意,子字符串编码的长度是utf-8编码的bytes数,而不是字符数。
  • uint<M>:enc(X)是大端编码的X,对于负数左侧填充0xff,正数左侧填充0,最后长度为32的倍数
  • address: 与uint160一致
  • int<M>: enc(X)是x的大端2进制补码编码,对于负数左侧填充0xff,正数左侧填充0,最后长度为32的倍数
  • bool:与uint8一致,1为true,0为false。
  • fixed<M>x<N>:enc(X) 是 enc(X * 10N),其中X * 10N 是int256的解释
  • fixed:和fixed128x19一致
  • unfixed<M>x<N>: enc(X)和enc(X * 10 ** N)一直,其中 X * 10 ** N是uint256的解释
  • ufixed: 和ufixed128x19一样
  • bytes<M>: enc(X)是一系列的字节X,通过零字节填充的32长度的序列。

注意,对于任意X,len(enc(x))都是32的整数倍。

函数选择器和参数编码

总之,调用f函数,并传递a_1,...,a_n参数会被编码为

function_selector(f) enc((a_1,...,a_n))

并且f的返回值v_1,...,v_k会被编码为

enc((v_1,...v_k))

例如,返回值会组合为一个数组并且编码。

例子

给定的合约如下所示:

pragma solidity ^0.4.0;

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

因此,对于Foo这个例子,如果我们想要调用baz,参数为69true。我们就得传递总共68字节,可以分解为:

  • 0xcdcd77c0:方法的ID,它来自于ASCII编码的baz(uint32,bool)的keccak哈希的前4个字节。
  • 0x0000000000000000000000000000000000000000000000000000000000000045,第一个参数,类型为uint32,值为69。
  • 0x0000000000000000000000000000000000000000000000000000000000000001,第二个字节,布尔量true,填充为32位。

合并为

0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001

返回值为一个布尔量,例如,如果返回false,它的输出为单个字节数组
,0x0000000000000000000000000000000000000000000000000000000000000000,一个布尔量。

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

  • 0xfce353f6:方法ID。它来自于对bar(bytes3[2])的签名。
  • 0x6162630000000000000000000000000000000000000000000000000000000000
  • 0x6465660000000000000000000000000000000000000000000000000000000000

合并为

0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000

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

  • 0xa5643bf2:方法ID,来自于sam(bytes,bool,uint256[])的签名,注意uint被替换为它的同义表述,uint256
  • 0x0000000000000000000000000000000000000000000000000000000000000060:第一个个参数(动态类型)的偏移位置,单位为bytes,从参数块的起始开始。这个例子为,0x60。
  • 0x0000000000000000000000000000000000000000000000000000000000000001:第二个参数,布尔量true。
  • 0x00000000000000000000000000000000000000000000000000000000000000a0:第三个参数(动态类型)的偏移位置,单位为bytes。这个例子为,0xa0。
  • 0x0000000000000000000000000000000000000000000000000000000000000004:第一个参数数据的一部分,它代表字节数组的长度。 这个例子是4.
  • 0x6461766500000000000000000000000000000000000000000000000000000000:第一个参数的内容:“dave”的UTF-8(这个例子等价于ASCII)编码,用0填充为32字节。
  • 0x0000000000000000000000000000000000000000000000000000000000000003:第三个元素的数据的一部分,它代表数组长度的。这个例子为,3.
  • 0x0000000000000000000000000000000000000000000000000000000000000001:第三个参数的第一个元素
  • 0x0000000000000000000000000000000000000000000000000000000000000002:第三个参数的第二个元素
  • 0x0000000000000000000000000000000000000000000000000000000000000003:第三个参数的第三个元素

合并为:

0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003

动态类型的使用

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

我们取sha3(“f(uint256,uint32[],bytes10,bytes)”)的前四个字节,例如0x8be65246。然后我们编码四个参数的头部。对于静态类型的uint256bytes10,直接传递他们的值。但是对于动态类型uint32[]bytes10,我们使用他们在数据区域的偏移,测量值编码的起始位置(例如,不包含函数签名hash的前四个字节)。它们是:

  • 0x0000000000000000000000000000000000000000000000000000000000000123:(0x123扩展为32字节)
  • 0x0000000000000000000000000000000000000000000000000000000000000080:(第二个参数在数据区域的偏移,4*32bytes,其实是头部的大小)
  • 0x3132333435363738393000000000000000000000000000000000000000000000:("1234567890"右填充为32个字节)
  • 0x00000000000000000000000000000000000000000000000000000000000000e0:(第四个参数在数据区域的偏移 = 第一个动态参数的数据的偏移 + 第一个动态参数的数据的大小 = 4*32 + 3 * 32)

这之后,后面紧跟第一个动态参数的数据部分,[0x456,0x789]:

  • 0x0000000000000000000000000000000000000000000000000000000000000002:(数组的长度,2)
  • 0x0000000000000000000000000000000000000000000000000000000000000456:(第一个参数)
  • 0x0000000000000000000000000000000000000000000000000000000000000789:(第二个参数)

最后我们会编码第二个动态参数的数据部分,”Hello world!“

  • 0x000000000000000000000000000000000000000000000000000000000000000d:(元素的数量(这个例子为bytes):13)
  • 0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000:(”Hello,world!“右填充为32个字节)

合而为一,编码如下(函数选择器之后要换行,为了清晰,每行32字节):

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

事件

事件是以太坊日志/事件监听协议的抽象。日志实体提供了合约的地址,一系列但最多4个主题和任意长度的二进制数据。事件整合已存在的ABI,将他(和接口描述一起)翻译为合适类型的结构体。(?Events leverage the existing function ABI in order to interpret this (together with an interface spec) as a properly typed structure.)

给定一个事件名称和一系列的事件参数,我们可以将它分为两个子系列:被索引的和没有被索引的。被索引的,最多3个,通过事件签名的Keccak哈希来组成日志实体的主题。另外没有索引的组成事件的byte数组。

实际上,使用ABI的日志实体被描述为:

  • address:合约的地址(以太坊本身支持)
  • topics[0]: keccak(EVENT_NAME+”(“+EVENT_ARGS.map(canonical_type_of).join(”,”)+”)”) (canonical_type_of 是一个函数,简单的根据参数返回同义类型。例如,对于uint索引的foo,它会返回 uint256。)如果事件被描述为anonymoustopics[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序列化函数,用来返回一系列类型的函数返回,如上面所述)

JSON

合约接口的JSON形式通过一个函数数组 和/或 事件描述 给定。函数描述是一个JSON对象,具有如下的字段:

  • type: "function","construct"或者"fallback"(没有名字的默认函数)

  • name: 函数的名称

  • inputs: 一个对象数组,每个对象包含:

    • name:参数名称
    • type:参数类型的同义类型
  • outputs: 像inputs一样的数组对象,如果没有返回值,可以删除这个字段

  • constant: 如果函数声明为不改变区块链状态为true

  • payable: 如果函数接收以太币,则为true,默认为false。

type可以删除,默认为"function".

构造函数和fallback函数没有名称或者输出。fallback函数也没有输入。

发送非零个以太币到非payable的函数会有异常,不要这么干。

一个事件的描述,是一个差不多字段的JSON对象。

  • type: 总是”event“

  • name: 事件的名称

  • inputs: 一个对象数组,每个对象包含

    • name:参数名称
    • type:参数类型的同义类型
    • indexed:如果字段是日志的topic,则为true,如果是日志的数据部分,则为false
  • anonymous: 如果事件被声明为anonymous,则为true。

例如,

pragma solidity ^0.4.0;

contract Test {
  function Test(){ b = 0x12345678901234567890123456789012; }
  event Event(uint indexed a, bytes32 b)
  event Event2(uint indexed a, bytes32 b)
  function foo(uint a) { 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": []
}]
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,219评论 0 4
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,257评论 0 16
  • 我是不是更像一个秘书,和彦哥语音,感觉他流露出对我前途的惋惜。他问,我已经有多久没有写trans了
    木同之阅读 119评论 0 0
  • 我不喜欢把一辈子都给定格掉,但人不是有好几个“一辈子”嘛,七年就是一辈子,对于接下来七年的这辈子的我来说最重要的三...
    米斯特左脑阅读 758评论 7 4
  • 01 有人说,他能看到我的未来。我怀着好奇心,问他:“那你帮我算一卦吧!我想知道我的未来是什么样。”对话框那边停留...
    魅格体阅读 1,191评论 0 2