智慧合約從入門到精通:呼叫資料的佈局和ABI

區塊鏈技術發表於2018-05-24

簡介:本文將介紹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:enc(X)是按大端序編碼X,並在左側高位補足0,使之為32位元組的倍數。

address:按uint160編碼。

int: 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: 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
  },
  { ...
  } ...
]
複製程式碼

相關文章