1. 程式人生 > >智能合約從入門到精通:調用數據的布局和ABI

智能合約從入門到精通:調用數據的布局和ABI

區塊鏈 智能合約 以太坊

簡介:本文將介紹Solidity語言的調用數據的布局和ABI詳解。其中調用數據的布局將主要介紹以太坊合約間調用時的消息格式ABI。

好久時間沒有更新文章,前文中我們介紹了Solidity的特性與內部機制,本文我們將Solidity的調用數據的布局和ABI詳解。

調用數據的布局(Layout of CallData)

當Solidity合約被部署後,從某個帳戶調用這個合約,需要輸入的數據是需要符合the ABI specification, ABI是以太坊的一種合約間調用時的一個消息格式。類似Webservice裏的SOAP協議一樣;也就是定義操作函數簽名,參數編碼,返回結果編碼等。

ABI詳解


函數
基本設計思想
使用ABI協議時必須要求在編譯時知道類型,也就是說不支持動態語言那樣的聲明的變量還會變類型的情況。由於協議假設合約在編譯期間知道另一個合約的接口定義,所以協議內沒有明確定義存的內容類型(協議非類型內省)。
所以這個協議並不支持合約接口是動態的,或者是僅在運行時才知道類型的情況。如果這些情況很重要,可以使用以太坊生態系統建立自己的基礎設施來解決這個問題。

函數選擇器
一個函數調用的前四個字節數據指定了要調用的函數簽名。計算方式是使用函數簽名的keccak256的哈希,取4個字節。
bytes4(keccak256("foo(uint32,bool)"))
函數簽名使用基本類型的典型格式(canonical expression)定義,如果有多個參數使用,隔開,要去掉表達式中的所有空格。

參數編碼
由於前面的函數簽名使用了四個字節,參數的數據將從第五個字節開始。參數的編碼方式與返回值,事件的編碼方式一致,後面一起介紹。
支持的類型
支持的類型可參考原文2。支持的類型裏面有一些比較特殊的是動態內容的類型,比如string,需要存儲的空間是不固定的。
編碼方式
針對數組參數中的嵌套數組的優化:
訪問一個參數屬性需要的讀取次數,在一個數組結構中最多是數組的深度,比如a_i[k][l][r],最多4次。在之前的ABI協議版本中,在最差情況下讀取次數會隨著總的動態類型的參數量線性增長。
變量的值或數組的元素間不應該是隔開存儲的,可支持重定位,比如使用相對地址來定位。
區分了動態內容類型和固定大小的類型。固定大小的類型按原位置存儲到當前塊。動態類型的數據則獨立存儲在其它數據塊。

動態內容類型的定義
bytes
string
T[] 某個類型的不定長數組
T[k] 某個類型的定長數組
所有其它類型則稱為固定大小的類型。
長度函數的定義
len(a)是二進制字符串a的中的字節數。len(a)的結果類型是uint256。
我們定義enc,編碼函數,是一個ABI類型值到二進制串的映射函數,也就是ABI類型到二進制字符串的映射函數。由此len(enc(X))的結果也將因為X是不是動態內容類型而有所不同(也就是說動態內容類型的編碼方式稍有不同)。
進一步定義
對於任何ABI的值,根據X的類型不同,我們遞歸定義enc(X),如下:
對於X是任意類型的T和長度值k,T[k]。
enc(X) = head(X[0]) ... head(x[k-1] tail(X[0]) ... tail(X[k-1])
對於X[i],如果其為固定大小的類型,head函數定義為,head(X[i]) = enc(X[i])。tail函數定義為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])
而對於是動態長度的類型的X[i],雖然其長度不確定,但head(X[i])所存值其實是非常明確的,頭部中只是存的一個偏移值(offset),這是偏移是實際存儲內容處相對enc(X)整個編碼內容開始位置來定義的。
上面這個表達式看得有點雲裏霧裏的,但如果沒有理解錯的話,固定大小的類型在head裏就依次編碼了,動態內容類型只在head裏放了一個從開始到真正內容開始的偏移,在偏移處才真正放內容,內容如果是變長的,就用len(enc(X))函數計算一個值放在前面,標識這個值有多大的內容。
T[] 其中X有k個元素。其中k被認為是uint256,所以enc(k)實際是編碼一個uint256。
enc(X) = enc(k) enc(X[1], ..., X[k])
它被以一個靜態長度的數據來編碼,但將數組所含元素的個長度作為前綴。
具體類型的編碼方式
具體編碼方式由於細節太多,不完全保證翻譯正確,如果要自己實現這樣的細節,建議再仔細研究原文文檔,下面翻譯僅做參考。
bytes,長度k,長度值k是uint256。
enc(X) = enc(k) pad_right(X),先將長度按uint256編碼,然後緊跟字節序列格式的X,再用零補足,來保證len(enc(X))是32字節(256位)的倍數。
string
enc(X) = enc(enc_UTF8(X)),這裏的utf-8編碼被按字節解釋及編碼;所以後續涉及的長度都是指按字節算的,不是按字符計算。
uint<M>:enc(X)是按大端序編碼X,並在左側高位補足0,使之為32字節的倍數。
address:按uint160編碼。
int<M>: enc(X) 是X的按大端序值2的補碼表示,如果是負數左側用1補足,正數左側用0被足,直到是32的倍數。
bool:按uint8編碼。1代表true,0代表false。
fixedx: enc(X) is enc(X 2N) where X 2N is interpreted as a int256.
fixed: as in the fixed128x128 case
ufixedx: enc(X) is enc(X 2N) where X 2N is interpreted as a uint256.
ufixed: as in the ufixed128x128 case
bytes<M>: enc(X) 是將字節序列用0補足為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])
這裏的[a_1, ..., a_n]和[v_1, ..., v_k],是定長數組,長度分別是n和k。嚴格說來,[a_1, ..., a_n]是一個含不同類型元素的數組。但即便如此,編碼仍然是明確的,因為實際上我們並沒有使用這樣一種類型T。
例子

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

如果要調用baz(69, true),要傳的字節拆解如下:
0xcdcd77c0: 使用函數選擇器確定的函數ID。通過bytes4(keccak256("baz(uint32,bool)"))。
0x0000000000000000000000000000000000000000000000000000000000000045。第一個參數,uint32位的值69,並補位到32字節。
0x0000000000000000000000000000000000000000000000000000000000000001。第二值boolean類型值true。補位到32字節。
所以最終的串值為:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
返回結果是一個bool值,在這裏,返回的是false。所以輸出就是一個bool。
0x0000000000000000000000000000000000000000000000000000000000000000
動態類型的使用例子

如果我們要值用(0x123, [0x456, 0x789], "1234567890", "Hello, world!")調用函數f(uint,uint32[],bytes10,bytes),編碼拆解如下:
bytes4(sha3("f(uint256,uint32[],bytes10,bytes)"))計算MethodID值。對於固定大小的類型值uint256和bytes10,直接編碼值。而對於動態內容類型值uint32[]和bytes,我們先編碼偏移值,偏移值是整個值編碼的開始到真正存這個數據的偏移值(這裏不計算頭四個用於表示函數簽名的字節)。所以依次為:
0x0000000000000000000000000000000000000000000000000000000000000123,32字節的0x123。
0x0000000000000000000000000000000000000000000000000000000000000080 (第二個參數的由於是動態內容類型,所以這裏存儲偏移值,4*32 字節,剛好是頭部部分的大小)
0x3132333435363738393000000000000000000000000000000000000000000000 ("1234567890" 在右側補0到32字節大小)
0x00000000000000000000000000000000000000000000000000000000000000e0 (第四個參數的偏移 = 第一個動態參數的偏移值 + 第一個動態參數的大小 = 432 + 332 動態長度的計算見後)
尾部部分的第一個動態參數,[0x456, 0x789]編碼拆解如下:
0x0000000000000000000000000000000000000000000000000000000000000002 (整個數組的長度,2)。0x0000000000000000000000000000000000000000000000000000000000000456 (第一個元素)
0x0000000000000000000000000000000000000000000000000000000000000789(第二個元素)
最後我們來看看第二個動態參數的的編碼,Hello, world!。
0x000000000000000000000000000000000000000000000000000000000000000d (元素的字節長度,13)
0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000 ("Hello, world!" 補位到32字節,裏面是按ascii編碼的,可以查查對應的編碼。)
最終我們得到了下述的編碼,為了清晰在函數簽名的四個字節後,加了一個換行。
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000

Events 事件

Events是抽象出來的以太坊的日誌,事件監聽協議。日誌實體包含合約的地址,一系列的最多可以達到4個的Topic,和任意長度的二進制數據內容。Events依賴ABI函數來解釋,日誌實體被當成為一個自定義數據結構。
事件有一個給定的事件名稱,一系列的事件參數,我們將他們分為兩個類別:需要索引的和不需要索引的。需要索引的,可以最多允許有三個,包含使用Keccak hash算法哈希過的事件簽名,來組成現在日誌實體的Topic。那些不需要索引的組成了Events的字節數組。
一個日誌實體使用ABI描述如下:
address: 合約的地址。(由以太坊內部提供)
topics[0]: keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")"),其中的canonical_type_of是返回函數的規範型(Canonical form),如,uint indexed foo,返回的應該是uint256。如果事件本身是匿名定義的,那麽Topic[0]將不會自動生成。
Topics[n]: EVENT_INDEXED_ARGS[n-1],其中的EVENT_INDEXED_ARGS表示指定成要索引的事件參數。
data: abi_serialise(EVENT_NON_INDEXED_ARGS)使用ABI協議序列化的沒有指定為索引的其它的參數。abi_serialise()是ABI序列函數,用來返回一系列的函數定義的類型值。
JSON格式
合約接口的JSON格式。包含一系列的函數和或事件的描述。一個描述函數的JSON包含下述的字段:
type: 可取值有function,constructor,fallback(無名稱的默認函數)
inputs: 一系列的對象,每個對象包含下述屬性:
name: 參數名稱
type: 參數的規範型(Canonical Type)。
outputs: 一系列的類似inputs的對象,如果無返回值時,可以省略。
constant: true表示函數聲明自己不會改變區塊鏈的狀態。
payable: true表示函數可以接收ether,否則表示不能。
其中type字段可以省略,默認是function類型。構造器函數和回退函數沒有name或outputs。回退函數也沒有inputs。
向不支持payable發送ether將會引發異常,禁止這麽做。
事件用JSON描述時幾乎是一樣的:
type: 總是event
name: 事件的名稱
inputs: 一系列的對象,每個對象包含下述屬性:
name: 參數名稱
type: 參數的規範型(Canonical Type)。
indexed: true代表這個這段是日誌主題的一部分,false代表是日誌數據的一部分。
anonymous: true代表事件是匿名聲明的。
示例:

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

在Javascript中的使用示例:

var Test = eth.contract(
[
    {
        "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": []
    }
]);
var theTest = new Test(addrTest);

// examples of usage:
// every log entry ("event") coming from theTest (i.e. Event & Event2):
var f0 = eth.filter(theTest);
// just log entries ("events") of type "Event" coming from theTest:
var f1 = eth.filter(theTest.Event);
// also written as
var f1 = theTest.Event();
// just log entries ("events") of type "Event" and "Event2" coming from theTest:
var f2 = eth.filter([theTest.Event, theTest.Event2]);
// just log entries ("events") of type "Event" coming from theTest with indexed parameter ‘a‘ equal to 69:
var f3 = eth.filter(theTest.Event, {‘a‘: 69});
// also written as
var f3 = theTest.Event({‘a‘: 69});
// just log entries ("events") of type "Event" coming from theTest with indexed parameter ‘a‘ equal to 69 or 42:
var f4 = eth.filter(theTest.Event, {‘a‘: [69, 42]});
// also written as
var f4 = theTest.Event({‘a‘: [69, 42]});

// options may also be supplied as a second parameter with `earliest`, `latest`, `offset` and `max`, as defined for `eth.filter`.
var options = { ‘max‘: 100 };
var f4 = theTest.Event({‘a‘: [69, 42]}, options);

var trigger;
f4.watch(trigger);

// call foo to make an Event:
theTest.foo(69);

// would call trigger like:
//trigger(theTest.Event, {‘a‘: 69, ‘b‘: ‘0x12345678901234567890123456789012‘}, n);
// where n is the block number that the event triggered in.

實現:

// e.g. f4 would be similar to:
web3.eth.filter({‘max‘: 100, ‘address‘: theTest.address, ‘topics‘: [ [69, 42] ]});
// except that the resultant data would need to be converted from the basic log entry format like:
{
  ‘address‘: theTest.address,
  ‘topics‘: [web3.sha3("Event(uint256,bytes32)"), 0x00...0045 /* 69 in hex format */],
  ‘data‘: ‘0x12345678901234567890123456789012‘,
  ‘number‘: n
}
// into data good for the trigger, specifically the three fields:
  Test.Event // derivable from the first topic
  {‘a‘: 69, ‘b‘: ‘0x12345678901234567890123456789012‘} // derivable from the ‘indexed‘ bool in the interface, the later ‘topics‘ and the ‘data‘
  n // from the ‘number‘

事件結果:

[ {
  ‘event‘: Test.Event,
  ‘args‘: {‘a‘: 69, ‘b‘: ‘0x12345678901234567890123456789012‘},
  ‘number‘: n
  },
  { ...
  } ...
]

智能合約從入門到精通:調用數據的布局和ABI