date:20170801
基本設(shè)計
在以太坊生態(tài)系統(tǒng)中,應(yīng)用二進制接口是跟合約交互的標(biāo)準(zhǔn)途徑。從區(qū)塊鏈外部或者合約之間的交互都可以通過這個方式。數(shù)據(jù)根據(jù)它的類型編碼,我們將在這個文章中詳細論述。編碼不是自我描述的,所以如果要解碼就需要模式(schema)。
我們假設(shè)合約的接口函數(shù)是強類型的,在編譯的時候推斷,且是靜態(tài)的。沒有提供反省機制。我們假設(shè)所有的合約都定義這樣的接口,使得任何合約都可以在編譯的時候調(diào)用。
該描述文檔不描述接口是動態(tài)的合約或者另一種情況--在運行的時候才知道。這些情況是很重要的,因為他們可以構(gòu)建以太坊生態(tài)系統(tǒng)的豐富的內(nèi)建設(shè)施。(?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.)
函數(shù)選擇器
函數(shù)調(diào)用的調(diào)用數(shù)據(jù)的前四個字節(jié)指出了所調(diào)用的函數(shù)。它是函數(shù)簽名的keccak(SHA-3)的前四個字節(jié)(左邊,高位,大端存儲)。簽名被定義為基本原型的權(quán)威表達式。例如,函數(shù)名稱和用括號括起來的參數(shù)類型。參數(shù)類型通過一個逗號隔開-沒有使用空格。
參數(shù)編碼
從第五個字節(jié)開始,就是參數(shù)的編碼。這個編碼除了用在前四個字節(jié)指定函數(shù)外,也用在了其他地方。例如,返回值和時間參數(shù)也是用同樣的方式。
類型
有下面幾種基礎(chǔ)類型:
- 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.
- uint,int:各自是uint256,int256的同義詞(不是用來計算函數(shù)選擇器)
- bool:等價于 uint8,值被限制為0和1。
- fixed<M>x<N>:M位的有符號,固定點的小數(shù),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>的無符號變量
- fixed,ufixed:各自等價于fiexed128x19,ufixed128x19(不是用來計算函數(shù)選擇器)
- bytes<M>:M位的二進制類型,0 < M <= 32.
- function:等價于bytes24,一個地址,加函數(shù)選擇器
下面是(固定大小的)數(shù)組類型:
- <type>[M]:給定類型的固定長度的數(shù)組
下面是非固定大小的類型:
- bytes: 動態(tài)大小的字符串
- string:動態(tài)大小的unicode字符串,假設(shè)是UTF-8編碼
- <type>[]:給定類型的動態(tài)長度的數(shù)組
類型可以結(jié)合為匿名結(jié)構(gòu)體,通過把有限數(shù)量的參數(shù)用圓括號包圍,通過逗號隔開:
- (T1,T2,...,Tn):匿名結(jié)構(gòu)體(有序元組),由類型T1,...,Tn,n >= 0
可以組合成結(jié)構(gòu)體有結(jié)構(gòu)體,結(jié)構(gòu)體數(shù)組等結(jié)構(gòu)。
編碼的正式描述(?該章節(jié)尚未理解,翻譯出錯的概率很大,讀者可以直接查看原文)
我們現(xiàn)在開始正式描述編碼,它遵循下面的準(zhǔn)則。如果參數(shù)中有嵌套數(shù)組,它們非常有用:
屬性:
1. 為了獲取一個值而讀取的次數(shù),差不多是值在參數(shù)數(shù)組結(jié)構(gòu)里的深度。例如,如果要獲取a_i[k][l][r],就要讀取4次。在之前版本的ABI中,最壞的情況下,讀取次數(shù)和動態(tài)參數(shù)的總數(shù)線性相關(guān)。
2.變量的值或者數(shù)組元素不會插入其他值,而且可以重新定位。例如,它只使用相關(guān)的“addresses”。
我們區(qū)分了靜態(tài)和動態(tài)類型。靜態(tài)類型編碼在當(dāng)前位置。而動態(tài)類型編碼在當(dāng)前區(qū)塊之后的單獨分配的位置,
定義:以下的類型被稱為是“動態(tài)的”:* bytes* string* 對于任意類型T的數(shù)組T[] 任意動態(tài)類型T的數(shù)組T[k],且K > 0* (T1,....,Tk),對于1 <= i <= k,如果Ti都是動態(tài)的。
所有其他的類型稱為“靜態(tài)”。
定義:len(a)是任意字符串 a 的字節(jié)數(shù)。len(a)的類型假設(shè)為uint256。
我們定義enc,真實的編碼,作為ABI類型的映射值到二進制字符串,所以len(enc(X)),當(dāng)且僅當(dāng)X是動態(tài)的時候,依賴于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的靜態(tài)類型,為
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是一個動態(tài)類型。
注意,在動態(tài)的情況下,head(X(i))是很清晰,因為頭部的長度只依賴于類型而不是值。值是tail(X(i))起始的偏移相對于enc(X)的起始。 - 對于任意的T和k,T[k]
enc(X) = enc((X[0],...,X[k-1]))
例如,他會被當(dāng)做具有相同類型的,k個元素的匿名數(shù)組 - T[],X有k個元素(k假設(shè)為uint256的類型):
enc(X) = enc(k) enc([X[1],...,X[k]])
例如,它會被當(dāng)做靜態(tài)大小的數(shù)組來編碼 - k長度(被假設(shè)為uint256)的bytes
enc(X) = enc(k) pad_right(X),例如,bytes的數(shù)量被編碼為a
uint256 繼承了真實的值X作為byte序列,繼承了最小數(shù)量的零字節(jié),因此len(enc(X))是32的倍數(shù)。 - string:enc(X)=enc(enc_utf8(X)),例如X是utf-8編碼,值是bytes類型,并且更進一步的編碼。注意,子字符串編碼的長度是utf-8編碼的bytes數(shù),而不是字符數(shù)。
- uint<M>:enc(X)是大端編碼的X,對于負數(shù)左側(cè)填充0xff,正數(shù)左側(cè)填充0,最后長度為32的倍數(shù)
- address: 與uint160一致
- int<M>: enc(X)是x的大端2進制補碼編碼,對于負數(shù)左側(cè)填充0xff,正數(shù)左側(cè)填充0,最后長度為32的倍數(shù)
- 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)是一系列的字節(jié)X,通過零字節(jié)填充的32長度的序列。
注意,對于任意X,len(enc(x))都是32的整數(shù)倍。
函數(shù)選擇器和參數(shù)編碼
總之,調(diào)用f函數(shù),并傳遞a_1,...,a_n參數(shù)會被編碼為
function_selector(f) enc((a_1,...,a_n))
并且f的返回值v_1,...,v_k會被編碼為
enc((v_1,...v_k))
例如,返回值會組合為一個數(shù)組并且編碼。
例子
給定的合約如下所示:
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這個例子,如果我們想要調(diào)用baz,參數(shù)為69和true。我們就得傳遞總共68字節(jié),可以分解為:
- 0xcdcd77c0:方法的ID,它來自于ASCII編碼的baz(uint32,bool)的keccak哈希的前4個字節(jié)。
- 0x0000000000000000000000000000000000000000000000000000000000000045,第一個參數(shù),類型為uint32,值為69。
- 0x0000000000000000000000000000000000000000000000000000000000000001,第二個字節(jié),布爾量true,填充為32位。
合并為
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
返回值為一個布爾量,例如,如果返回false,它的輸出為單個字節(jié)數(shù)組
,0x0000000000000000000000000000000000000000000000000000000000000000,一個布爾量。
如果我們想要調(diào)用bar,參數(shù)為["abc","def"],我們就要傳遞總共68個字節(jié),可以分解為
- 0xfce353f6:方法ID。它來自于對bar(bytes3[2])的簽名。
- 0x6162630000000000000000000000000000000000000000000000000000000000
- 0x6465660000000000000000000000000000000000000000000000000000000000
合并為
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
如果我們想要調(diào)用sam,參數(shù)為"dave",true和[1,2,3]。我們總共會傳遞292個字節(jié),可以分解為:
- 0xa5643bf2:方法ID,來自于sam(bytes,bool,uint256[])的簽名,注意uint被替換為它的同義表述,uint256
- 0x0000000000000000000000000000000000000000000000000000000000000060:第一個個參數(shù)(動態(tài)類型)的偏移位置,單位為bytes,從參數(shù)塊的起始開始。這個例子為,0x60。
- 0x0000000000000000000000000000000000000000000000000000000000000001:第二個參數(shù),布爾量true。
- 0x00000000000000000000000000000000000000000000000000000000000000a0:第三個參數(shù)(動態(tài)類型)的偏移位置,單位為bytes。這個例子為,0xa0。
- 0x0000000000000000000000000000000000000000000000000000000000000004:第一個參數(shù)數(shù)據(jù)的一部分,它代表字節(jié)數(shù)組的長度。 這個例子是4.
- 0x6461766500000000000000000000000000000000000000000000000000000000:第一個參數(shù)的內(nèi)容:“dave”的UTF-8(這個例子等價于ASCII)編碼,用0填充為32字節(jié)。
- 0x0000000000000000000000000000000000000000000000000000000000000003:第三個元素的數(shù)據(jù)的一部分,它代表數(shù)組長度的。這個例子為,3.
- 0x0000000000000000000000000000000000000000000000000000000000000001:第三個參數(shù)的第一個元素
- 0x0000000000000000000000000000000000000000000000000000000000000002:第三個參數(shù)的第二個元素
- 0x0000000000000000000000000000000000000000000000000000000000000003:第三個參數(shù)的第三個元素
合并為:
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
動態(tài)類型的使用
調(diào)用一個函數(shù),f(uint,uint32[],bytes10,bytes),參數(shù)為(0x123,[0x456,0x789],"1234567890","Hello,world!"),會通過下面的方式編碼:
我們?nèi)?em>sha3(“f(uint256,uint32[],bytes10,bytes)”)的前四個字節(jié),例如0x8be65246。然后我們編碼四個參數(shù)的頭部。對于靜態(tài)類型的uint256和bytes10,直接傳遞他們的值。但是對于動態(tài)類型uint32[]和bytes10,我們使用他們在數(shù)據(jù)區(qū)域的偏移,測量值編碼的起始位置(例如,不包含函數(shù)簽名hash的前四個字節(jié))。它們是:
- 0x0000000000000000000000000000000000000000000000000000000000000123:(0x123擴展為32字節(jié))
- 0x0000000000000000000000000000000000000000000000000000000000000080:(第二個參數(shù)在數(shù)據(jù)區(qū)域的偏移,4*32bytes,其實是頭部的大小)
- 0x3132333435363738393000000000000000000000000000000000000000000000:("1234567890"右填充為32個字節(jié))
- 0x00000000000000000000000000000000000000000000000000000000000000e0:(第四個參數(shù)在數(shù)據(jù)區(qū)域的偏移 = 第一個動態(tài)參數(shù)的數(shù)據(jù)的偏移 + 第一個動態(tài)參數(shù)的數(shù)據(jù)的大小 = 4*32 + 3 * 32)
這之后,后面緊跟第一個動態(tài)參數(shù)的數(shù)據(jù)部分,[0x456,0x789]:
- 0x0000000000000000000000000000000000000000000000000000000000000002:(數(shù)組的長度,2)
- 0x0000000000000000000000000000000000000000000000000000000000000456:(第一個參數(shù))
- 0x0000000000000000000000000000000000000000000000000000000000000789:(第二個參數(shù))
最后我們會編碼第二個動態(tài)參數(shù)的數(shù)據(jù)部分,”Hello world!“
- 0x000000000000000000000000000000000000000000000000000000000000000d:(元素的數(shù)量(這個例子為bytes):13)
- 0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000:(”Hello,world!“右填充為32個字節(jié))
合而為一,編碼如下(函數(shù)選擇器之后要換行,為了清晰,每行32字節(jié)):
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000
事件
事件是以太坊日志/事件監(jiān)聽協(xié)議的抽象。日志實體提供了合約的地址,一系列但最多4個主題和任意長度的二進制數(shù)據(jù)。事件整合已存在的ABI,將他(和接口描述一起)翻譯為合適類型的結(jié)構(gòu)體。(?Events leverage the existing function ABI in order to interpret this (together with an interface spec) as a properly typed structure.)
給定一個事件名稱和一系列的事件參數(shù),我們可以將它分為兩個子系列:被索引的和沒有被索引的。被索引的,最多3個,通過事件簽名的Keccak哈希來組成日志實體的主題。另外沒有索引的組成事件的byte數(shù)組。
實際上,使用ABI的日志實體被描述為:
- address:合約的地址(以太坊本身支持)
- topics[0]: keccak(EVENT_NAME+”(“+EVENT_ARGS.map(canonical_type_of).join(”,”)+”)”) (canonical_type_of 是一個函數(shù),簡單的根據(jù)參數(shù)返回同義類型。例如,對于uint索引的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序列化函數(shù),用來返回一系列類型的函數(shù)返回,如上面所述)
JSON
合約接口的JSON形式通過一個函數(shù)數(shù)組 和/或 事件描述 給定。函數(shù)描述是一個JSON對象,具有如下的字段:
type: "function","construct"或者"fallback"(沒有名字的默認函數(shù))
name: 函數(shù)的名稱
-
inputs: 一個對象數(shù)組,每個對象包含:
- name:參數(shù)名稱
- type:參數(shù)類型的同義類型
outputs: 像inputs一樣的數(shù)組對象,如果沒有返回值,可以刪除這個字段
constant: 如果函數(shù)聲明為不改變區(qū)塊鏈狀態(tài)為true
payable: 如果函數(shù)接收以太幣,則為true,默認為false。
type可以刪除,默認為"function".
構(gòu)造函數(shù)和fallback函數(shù)沒有名稱或者輸出。fallback函數(shù)也沒有輸入。
發(fā)送非零個以太幣到非payable的函數(shù)會有異常,不要這么干。
一個事件的描述,是一個差不多字段的JSON對象。
type: 總是”event“
name: 事件的名稱
-
inputs: 一個對象數(shù)組,每個對象包含
- name:參數(shù)名稱
- type:參數(shù)類型的同義類型
- indexed:如果字段是日志的topic,則為true,如果是日志的數(shù)據(jù)部分,則為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": []
}]