Solidity語言學習筆記————45、應用二進位制介面(ABI)說明
應用二進位制介面(ABI) 說明
基本設計
在以太坊生態系統中, 應用二進位制介面Application Binary Interface(ABI) 是從區塊鏈外部與合約進行互動以及合約與合約間進行互動的一種標準方式。 資料會根據其型別按照這份手冊中說明的方法進行編碼。這種編碼並不是可以自描述的,而是需要一種特定的概要(schema)來進行解碼。
我們假定合約函式的介面都是強型別的,且在編譯時是可知的和靜態的;不提供自我檢查機制。我們假定在編譯時,所有合約要呼叫的其他合約介面定義都是可用的。
這份手冊並不針對那些動態合約介面或者僅在執行時才可獲知的合約介面。如果這種場景變得很重要,你可以使用以太坊生態系統中其他更合適的基礎設施來處理它們。
函式選擇器
一個函式呼叫資料的前 4 位元組,指定了要呼叫的函式。這就是某個函式簽名的 Keccak(SHA-3)雜湊的前 4 位元組(高位在左的大端序)(譯註:這裡的“高位在左的大端序“,指最高位位元組儲存在最低位地址上的一種序列化編碼方式,即高位位元組在左)。 這種簽名被定義為基礎原型的規範表達,基礎原型即是函式名稱加上由括號括起來的引數型別列表,引數型別間由一個逗號分隔開,且沒有空格。
引數編碼
從第5位元組開始是被編碼的引數。這種編碼也被用在其他地方,比如,返回值和事件的引數也會被用同樣的方式進行編碼,而用來指定函式的4個位元組則不需要再進行編碼。
型別
以下是基礎型別:
uint<M>
:M
位的無符號整數,0 < M <= 256
、M % 8 == 0
。例如:uint32
,uint8
,uint256
。int<M>
:以 2 的補碼作為符號的M
位整數,0 < M <= 256
、M % 8 == 0
。address
:除了字面上的意思和語言型別的區別以外,等價於uint160
。在計算和函式選擇器中,通常使用address
。uint
、int
:uint256
、int256
各自的同義詞。在計算和 函式選擇器中,通常使用uint256
和int256
。bool
:等價於uint8
,取值限定為0
或1
。在計算和 函式選擇器中,通常使用bool
。fixed<M>x<N>
:M
位的有符號的固定小數位的十進位制數字8 <= M <= 256
、M % 8 ==0
、且0 < N <= 80
。其值v
即是v / (10 ** N)
。(也就是說,這種型別是由M
位的二進位制資料所儲存的,有N
位小數的十進位制數值。譯者注。)ufixed<M>x<N>
:無符號的fixed<M>x<N>
。fixed
、ufixed
:fixed128x19
、ufixed128x19
各自的同義詞。在計算和 函式選擇器中,通常使用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 編碼得到的。 |
編碼的形式化說明
我們現在來正式講述編碼,它具有如下屬性,如果引數是巢狀的陣列,這些屬性非常有用:
屬性:
讀取的次數取決於引數陣列結構中的最大深度;也就是說,要取得
a_i[k][l][r]
需要讀取 4 次。在先前的ABI版本中,在最糟的情況下,讀取的次數會隨著動態引數的總數而線性地增長。一個變數或陣列元素的資料,不會被插入其他的資料,並且是可以再定位的;也就是說,它們只會使用相對的“地址”。
我們需要區分靜態和動態型別。靜態型別會被直接編碼,動態型別則會在當前資料塊之後單獨分配的位置被編碼。
定義: 以下型別被稱為“動態”:
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))
andtail(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 值位元組。
注意,對於任意的 X
,len(enc(X))
都是 32 的倍數。
函式選擇器和引數編碼
大體而言,一個以 a_1, ..., a_n
為引數的對 f
函式的呼叫,會被編碼為
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
);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格式是由一個函式和/或事件描述的陣列所給定的。一個函式的描述是一個有如下欄位的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 操作,也不會進行符號擴充套件,以及
- 動態型別會直接進行編碼,並且不包含長度資訊。
例如,對 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[]
,在編碼時沒有包含其長度資訊。 這意味著一旦有兩個動態長度的元素,編碼就會變得有歧義了。
相關文章
- 如何理解以太坊ABI - 應用程式二進位制介面
- Solidity語言學習筆記————1、初識Solidity語言Solid筆記
- Solidity語言學習筆記————38、Solidity彙編Solid筆記
- Solidity語言學習筆記————35、抽象合約和介面Solid筆記抽象
- Solidity語言學習筆記————36、 庫Solid筆記
- Solidity語言學習筆記————37、Using forSolid筆記
- Solidity語言學習筆記————4、常量Solid筆記
- Solidity語言學習筆記————33、事件(Events)Solid筆記事件
- Solidity語言學習筆記————12、陣列Solid筆記陣列
- Solidity語言學習筆記————34、繼承Solid筆記繼承
- Solidity語言學習筆記————7、單位和全域性變數Solid筆記變數
- Solidity語言學習筆記————43、安全考量Solid筆記
- Solidity語言學習筆記————42、提示和技巧Solid筆記
- Solidity語言學習筆記————28、純函式Solid筆記函式
- Solidity語言學習筆記————26、回退函式Solid筆記函式
- Solidity語言學習筆記————16、對映MappingSolid筆記APP
- Solidity語言學習筆記————32、建立合約Solid筆記
- Solidity語言學習筆記————41、記憶體佈局Solid筆記記憶體
- Solidity語言學習筆記————39、獨立彙編Solid筆記
- Solidity語言學習筆記————25、作用域和宣告Solid筆記
- Solidity語言學習筆記————27、檢視函式Solid筆記函式
- Solidity語言學習筆記————10、布林型、整型Solid筆記
- Solidity語言學習筆記————3、Remix的基本使用Solid筆記REM
- Solidity語言學習筆記————15、結構體StructSolid筆記結構體Struct
- Solidity語言學習筆記————18、字串和函式Solid筆記字串函式
- Solidity語言學習筆記————14、左值運算子Solid筆記
- Solidity語言學習筆記————9、左值運算子Solid筆記
- Solidity語言學習筆記————17、原始檔對映Solid筆記
- Solidity語言學習筆記————5、全域性變數Solid筆記變數
- Solidity語言學習筆記————2、使用編譯器Solid筆記編譯
- Solidity語言學習筆記————30、函式過載Solid筆記函式
- Go語言核心36講(Go語言實戰與應用二)--學習筆記Go筆記
- HWS二進位制筆記筆記
- Solidity語言學習筆記————26、Assert, Require, Revert 和 ExceptionsSolid筆記UIException
- Solidity語言學習筆記————22、可見性和GettersSolid筆記
- Solidity語言學習筆記————20、函式修飾符Solid筆記函式
- 《JavaScript語言精粹》學習筆記二JavaScript筆記
- C語言學習筆記——位運算C語言筆記