寫在前面:HiBlock區塊鏈社區成立了翻譯小組,翻譯區塊鏈相關的技術文檔及資料,本文爲Solidity文檔翻譯的第七部分《應用二進制接口(ABI) 說明》,特發佈出來邀請solidity愛好者、開發者作公開的審校,您能夠添加微信baobaotalk_com,驗證輸入「solidity」,而後將您的意見和建議發送給咱們,也能夠在文末「留言」區留言,有效的建議咱們會採納及合併進下一版本,同時將送一份小禮物給您以示感謝。數組
在 以太坊Ethereum 生態系統中, 應用二進制接口Application Binary Interface(ABI) 是從區塊鏈外部與合約進行交互以及合約與合約間進行交互的一種標準方式。 數據會根據其類型按照這份手冊中說明的方法進行編碼。這種編碼並非能夠自描述的,而是須要一種特定的概要(schema)來進行解碼。安全
咱們假定合約函數的接口都是強類型的,且在編譯時是可知的和靜態的;不提供自我檢查機制。咱們假定在編譯時,全部合約要調用的其餘合約接口定義都是可用的。微信
這份手冊並不針對那些動態合約接口或者僅在運行時纔可獲知的合約接口。若是這種場景變得很重要,你可使用 以太坊Ethereum 生態系統中其餘更合適的基礎設施來處理它們。數據結構
一個函數調用數據的前 4 字節,指定了要調用的函數。這就是某個函數簽名的 Keccak(SHA-3)哈希的前 4 字節(高位在左的大端序)(譯註:這裏的「高位在左的大端序「,指最高位字節存儲在最低位地址上的一種串行化編碼方式,即高位字節在左)。 這種簽名被定義爲基礎原型的規範表達,基礎原型便是函數名稱加上由括號括起來的參數類型列表,參數類型間由一個逗號分隔開,且沒有空格。函數
從第5字節開始是被編碼的參數。這種編碼也被用在其餘地方,好比,返回值和事件的參數也會被用一樣的方式進行編碼,而用來指定函數的4個字節則不須要再進行編碼。學習
如下是基礎類型:區塊鏈
uint<M>:M 位的無符號整數,0 < M <= 25六、M % 8 == 0。例如:uint32,uint8,uint256。ui
int<M>:以 2 的補碼做爲符號的 M 位整數,0 < M <= 25六、M % 8 == 0。編碼
address:除了字面上的意思和語言類型的區別之外,等價於 uint160。在計算和 函數選擇器Function Selector中,一般使用 address。翻譯
uint、int:uint25六、int256 各自的同義詞。在計算和 函數選擇器Function Selector中,一般使用 uint256 和 int256。
bool:等價於 uint8,取值限定爲 0 或 1 。在計算和 函數選擇器Function Selector中,一般使用 bool。
fixed<M>x<N>:M 位的有符號的固定小數位的十進制數字 8 <= M <= 25六、M % 8 ==0、且 0 < N <= 80。其值 v 便是 v / (10 ** N)。(也就是說,這種類型是由 M 位的二進制數據所保存的,有 N 位小數的十進制數值。譯者注。)
ufixed<M>x<N>:無符號的 fixed<M>x<N>。
fixed、ufixed:fixed128x1九、ufixed128x19 各自的同義詞。在計算和 函數選擇器Function Selector中,一般使用 fixed128x19 和 ufixed128x19。
bytes<M>:M 字節的二進制類型,0 < M <= 32。
function:一個地址(20 字節)以後緊跟一個 函數選擇器Function Selector(4 字節)。編碼以後等價於 bytes24。
如下是定長數組類型:
如下是非定長類型:
bytes:動態大小的字節序列。
string:動態大小的 unicode 字符串,一般呈現爲 UTF-8 編碼。
<type>[]:元素爲給定類型的變長數組。
能夠將有限的若干類型放到一對括號中,用逗號分隔開,以此來構成一個 元組tuple:
用 元組tuple 構成 元組tuple、用 元組tuple 構成數組等等也是可能的。
咱們如今來正式講述編碼,它具備以下屬性,若是參數是嵌套的數組,這些屬性很是有用:
屬性:
一、讀取的次數取決於參數數組結構中的最大深度;也就是說,要取得 a_i[k][l][r] 須要讀取 4 次。在先前的ABI版本中,在最糟的狀況下,讀取的次數會隨着動態參數的總數而線性地增加。
二、一個變量或數組元素的數據,不會被插入其餘的數據,而且是能夠再定位的;也就是說,它們只會使用相對的「地址」。
咱們須要區分靜態和動態類型。靜態類型會被直接編碼,動態類型則會在當前數據塊以後單獨分配的位置被編碼。
定義: 如下類型被稱爲「動態」:
bytes
string
任意類型 T 的變長數組 T[]
任意動態類型 T 的定長數組 T[k] (k > 0)
Ti (1 <= i <= k)爲任意動態類型的元組tuple (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-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<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 * 10N),其中 X * 10N 能夠理解爲 int256。
fixed:與 fixed128x19 的狀況相同。
ufixed<M>x<N>:enc(X) 就是 enc(X * 10N),其中 X * 10N 能夠理解爲 uint256。
ufixed:與 ufixed128x19 的狀況相同。
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))
也就是說,返回值會被組合爲一個 元組tuple 進行編碼。
給定一個合約:
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 (第四個參數的數據部分起始位置的偏移量 = 第一個動態參數的數據部分起始位置的偏移量 + 第一個動態參數的數據部分的長度 = 432 + 332,參考後文)
在此以後,跟着第一個動態參數的數據部分 [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
事件,是 以太坊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 哈希 而不是直接包含編碼值。這樣就容許應用程序更有效地查詢動態長度類型的值(經過把編碼值的哈希設定爲主題), 但也使應用程序不能對它們還沒查詢過的已索引的值進行解碼。
對於動態長度的類型,應用程序開發者面臨在對預先設定的值(若是參數已被索引)的快速檢索和對任意數據的清晰處理(須要參數不被索引)之間的權衡。 開發者們能夠經過定義兩個參數(一個已索引、一個未索引)保存同一個值的方式來解決這種權衡,從而既得到高效的檢索又能清晰地處理任意數據。
合約接口的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": [] } ]
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[],在編碼時沒有包含其長度信息。 這意味着一旦有兩個動態長度的元素,編碼就會變得有歧義了。
延伸閱讀:智能合約-Solidity官方文檔(1)
根據例子學習Solidity-Solidity官方文檔(3)
深刻理解Solidity之源文件及合約結構——Solidity中文文檔(4)
本文內容來源於HiBlock區塊鏈社區翻譯小組,感謝全體譯者的辛苦工做。
注:本文爲solidity翻譯的第七部分《應用二進制接口(ABI) 說明》,特發佈出來邀請solidity愛好者、開發者作公開的審校,您能夠添加微信baobaotalk_com,驗證輸入「solidity」,而後將您的意見和建議發送給咱們,也可在文末「留言」區留言,或經過原文連接訪問咱們的Github。有效的建議咱們會收納並及時改進,同時將送一份小禮物給您以示感謝。
如下是咱們的社區介紹,歡迎各類合做、交流、學習:)