Solidity語言學習筆記————45、應用二進位制介面(ABI)說明

FLy_鵬程萬里發表於2018-07-11

應用二進位制介面(ABI) 說明

基本設計

在以太坊生態系統中, 應用二進位制介面Application Binary Interface(ABI) 是從區塊鏈外部與合約進行互動以及合約與合約間進行互動的一種標準方式。 資料會根據其型別按照這份手冊中說明的方法進行編碼。這種編碼並不是可以自描述的,而是需要一種特定的概要(schema)來進行解碼。

我們假定合約函式的介面都是強型別的,且在編譯時是可知的和靜態的;不提供自我檢查機制。我們假定在編譯時,所有合約要呼叫的其他合約介面定義都是可用的。

這份手冊並不針對那些動態合約介面或者僅在執行時才可獲知的合約介面。如果這種場景變得很重要,你可以使用以太坊生態系統中其他更合適的基礎設施來處理它們。

函式選擇器

一個函式呼叫資料的前 4 位元組,指定了要呼叫的函式。這就是某個函式簽名的 Keccak(SHA-3)雜湊的前 4 位元組(高位在左的大端序)(譯註:這裡的“高位在左的大端序“,指最高位位元組儲存在最低位地址上的一種序列化編碼方式,即高位位元組在左)。 這種簽名被定義為基礎原型的規範表達,基礎原型即是函式名稱加上由括號括起來的引數型別列表,引數型別間由一個逗號分隔開,且沒有空格。

引數編碼

從第5位元組開始是被編碼的引數。這種編碼也被用在其他地方,比如,返回值和事件的引數也會被用同樣的方式進行編碼,而用來指定函式的4個位元組則不需要再進行編碼。

型別

以下是基礎型別:

  • uint<M>M 位的無符號整數,0 < M <= 256M % 8 == 0。例如:uint32uint8uint256
  • int<M>:以 2 的補碼作為符號的 M 位整數,0 < M <= 256M % 8 == 0
  • address:除了字面上的意思和語言型別的區別以外,等價於 uint160。在計算和函式選擇器中,通常使用 address
  • uintintuint256int256 各自的同義詞。在計算和 函式選擇器中,通常使用 uint256 和 int256
  • bool:等價於 uint8,取值限定為 0 或 1 。在計算和 函式選擇器中,通常使用 bool
  • fixed<M>x<N>M 位的有符號的固定小數位的十進位制數字 8 <= M <= 256M % 8 ==0、且 0 < N <= 80。其值 v 即是 v / (10 ** N)。(也就是說,這種型別是由 M 位的二進位制資料所儲存的,有 N 位小數的十進位制數值。譯者注。)
  • ufixed<M>x<N>:無符號的 fixed<M>x<N>
  • fixedufixedfixed128x19ufixed128x19 各自的同義詞。在計算和 函式選擇器中,通常使用 fixed128x19 和 ufixed128x19
  • bytes<M>M 位元組的二進位制型別,0 < M <= 32
  • function:一個地址(20 位元組)之後緊跟一個 函式選擇器(4 位元組)。編碼之後等價於 bytes24

以下是定長陣列型別:

  • <type>[M]:有 M 個元素的定長陣列,M > 0,陣列元素為給定型別。

以下是非定長型別:

  • bytes:動態大小的位元組序列。
  • string:動態大小的 unicode 字串,通常呈現為 UTF-8 編碼。
  • <type>[]:元素為給定型別的變長陣列。

可以將有限的若干型別放到一對括號中,用逗號分隔開,以此來構成一個 元組:

  • (T1,T2,...,Tn):由 T1,...,Tn,n >= 0 構成的元組。

用元組構成元組、用元組構成陣列等等也是可能的。

註解
除了元組以外,Solidity 支援以上所有型別的名稱。ABI 元組是利用 Solidity 的 structs 編碼得到的。

編碼的形式化說明

我們現在來正式講述編碼,它具有如下屬性,如果引數是巢狀的陣列,這些屬性非常有用:

屬性:

  1. 讀取的次數取決於引數陣列結構中的最大深度;也就是說,要取得 a_i[k][l][r] 需要讀取 4 次。在先前的ABI版本中,在最糟的情況下,讀取的次數會隨著動態引數的總數而線性地增長。

  2. 一個變數或陣列元素的資料,不會被插入其他的資料,並且是可以再定位的;也就是說,它們只會使用相對的“地址”。

我們需要區分靜態和動態型別。靜態型別會被直接編碼,動態型別則會在當前資料塊之後單獨分配的位置被編碼。

定義: 以下型別被稱為“動態”:

  • bytes
  • string
  • 任意型別 T 的變長陣列 T[]
  • 任意動態型別 T 的定長陣列 T[k] (k > 0
  • Ti (1 <= i <= k)為任意動態型別的元組 (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) 是 元組的第 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 個元素組成的元組那樣被編碼的。
  • 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 * 10**N),其中 X * 10**N 可以理解為 int256
  • fixed:與 fixed128x19 的情況相同。
  • ufixed<M>x<N>enc(X) 就是 enc(X * 10**N),其中 X * 10**N 可以理解為 uint256
  • ufixed:與 ufixed128x19 的情況相同。
  • bytes<M>enc(X) 就是 X 的位元組序列加上為使長度稱為 32 的倍數而新增的若干 0 值位元組。

注意,對於任意的 Xlen(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 值 tr這裡寫程式碼片ue。

合起來就是:

0xcdcd77c0000000000000000000000000000000000000000000000000000000000000004500000000000000000000000000000000000000000000000000000000000000011

它返回一個 bool。比如它返回 false,那麼它的輸出將是一個位元組陣列 0x0000000000000000000000000000000000000000000000000000000000000000,一個bool值。

如果我們想用 ["abc", "def"] 做引數呼叫 bar,我們總共需要傳送68位元組,可以分解為:

  • 0xfce353f6:方法ID。源自 bar(bytes3[2]) 的簽名。
  • 0x6162630000000000000000000000000000000000000000000000000000000000:第一個引數的第一部分,一個 bytes3 值"abc" (左對齊)。
  • 0x6465660000000000000000000000000000000000000000000000000000000000:第一個引數的第二部分,一個 bytes3 值"def" (左對齊)。

合起來就是:

0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
  • 1

如果我們想用

"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 (第四個引數的資料部分起始位置的偏移量 = 第一個動態引數的資料部分起始位置的偏移量 + 第一個動態引數的資料部分的長度 = 4*32 + 3*32,參考後文)

在此之後,跟著第一個動態引數的資料部分 [0x456, 0x789]

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (陣列元素個數,2)
  • 0x0000000000000000000000000000000000000000000000000000000000000456 (第一個陣列元素)
  • 0x0000000000000000000000000000000000000000000000000000000000000789 (第二個陣列元素)

最後,我們將第二個動態引數的資料部分 "Hello, world!" 進行編碼:

  • 0x000000000000000000000000000000000000000000000000000000000000000d (元素個數,在這裡是位元組數:13)
  • 0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000 ("Hello, world!" 從右邊補充到 32 位元組)

最後,合併到一起的編碼就是(為了清晰,在 函式選擇器和每 32 位元組之後加了換行):

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

事件

事件,是以太坊的日誌/事件監視協議的一個抽象。日誌項提供了合約的地址、一系列的主題(最高 4 項)和一些任意長度的二進位制資料。為了使用合適的型別資料結構來演繹這些功能(與介面定義一起),事件沿用了既存的 ABI 函式。

給定了事件名稱和事件引數之後,我們將其分解為兩個子集:已索引的和未索引的。已索引的部分,最多有 3 個,被用來與事件簽名的 Keccak 雜湊一起組成日誌項的主題。未索引的部分就組成了事件的位元組陣列。

這樣,一個使用 ABI 的日誌項就可以描述為:

  • address:合約地址(由以太坊真正提供);
  • 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);
  • dataabi_serialise(EVENT_NON_INDEXED_ARGS) (EVENT_NON_INDEXED_ARGS 是未索引的 EVENT_ARGSabi_serialise 是一個用來從某個函式返回一系列型別值的ABI序列化函式,就像上文所講的那樣)。

對於所有定長的Solidity型別,EVENT_INDEXED_ARGS 陣列會直接包含32位元組的編碼值。然而,對於 動態長度的型別 ,包含 stringbytes 和陣列, EVENT_INDEXED_ARGS 會包含編碼值的 Keccak 雜湊 而不是直接包含編碼值。這樣就允許應用程式更有效地查詢動態長度型別的值(通過把編碼值的雜湊設定為主題), 但也使應用程式不能對它們還沒查詢過的已索引的值進行解碼。對於動態長度的型別,應用程式開發者面臨在對預先設定的值(如果引數已被索引)的快速檢索和對任意資料的清晰處理(需要引數不被索引)之間的權衡。 開發者們可以通過定義兩個引數(一個已索引、一個未索引)儲存同一個值的方式來解決這種權衡,從而既獲得高效的檢索又能清晰地處理任意資料。

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": []
}]

處理元組型別

儘管名稱被有意地不作為 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 操作,也不會進行符號擴充套件,以及
  • 動態型別會直接進行編碼,並且不包含長度資訊。

例如,對 int1bytes1uint16string 用數值 -1, 0x42, 0x2424, "Hello, world!" 進行編碼將生成如下結果

0xff42242448656c6c6f2c20776f726c6421
  ^^                                 int1(-1)
    ^^                               bytes1(0x42)
      ^^^^                           uint16(0x2424)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field

更具體地說,每個靜態大小的型別都儘可能多地按它們的數值範圍使用了位元組數,而動態大小的型別,像 string、 bytes 或 uint[],在編碼時沒有包含其長度資訊。 這意味著一旦有兩個動態長度的元素,編碼就會變得有歧義了。



相關文章