死磕以太坊源碼分析之EVM如何調用ABI編碼的外部方法前端
配合如下代碼進行閱讀:https://github.com/blockchainGuide/java
寫文不易,給個小關注,有什麼問題能夠指出,便於你們交流學習。git
abi是什麼?
前面咱們認識到的是智能合約直接在EVM上的表示方式,可是,好比我想用java端程序去訪問智能合約的某個方法,難道讓java開發人員琢磨透彙編和二進制的表示,再去對接?
這明顯是不可能的,爲此abi產生了。這是一個通用可讀的json格式的數據,任何別的客戶端開發人員或者別的以太坊節點只要指定要調用的方法,經過abi將其解析爲字節碼並傳遞給evm,evm來計算處理該字節碼並返回結果給前端。abi就起到這麼一個做用,相似於傳統的客戶端和服務器端地址好交互規則,好比json格式的數據,而後進行交互。github
在本系列的上一篇文章中咱們看到了Solidity
是如何在EVM存儲器中表示覆雜數據結構的。可是若是沒法交互,數據就是沒有意義的。智能合約就是數據和外界的中間體。web
在這篇文章中咱們將會看到Solidity
和EVM
可讓外部程序來調用合約的方法並改變它的狀態。數據庫
「外部程序」不限於DApp/JavaScript
。任何可使用HTTP RPC
與以太坊節點通訊的程序,均可以經過建立一個交易與部署在區塊鏈上的任何合約進行交互。json
建立一個交易就像發送一個HTTP
請求。Web
的服務器會接收你的HTTP
請求,而後改變數據庫。交易會被網絡接收,底層的區塊鏈會擴展到包含改變的狀態。數組
交易對於智能合約就像HTTP
請求對於Web
服務器。緩存
讓咱們來看一下將狀態變量設置在0x1
位置上的交易。咱們想要交互的合約有一個對變量a
的設置者和獲取者:服務器
pragma solidity ^0.4.11; contract C { uint256 a; function setA(uint256 _a) { a = _a; } function getA() returns(uint256) { return a; } }
這個合約部署在Rinkeby測試網上。能夠隨意使用Etherscan,並搜索地址 0x62650ae5…進行查看。
我建立了一個能夠調用setA(1)
的交易,能夠在地址0x7db471e5…上查看該交易。
交易的輸出數據是:
0xee919d500000000000000000000000000000000000000000000000000000000000000001
對於EVM而言,這只是36字節的元數據。它對元數據不會進行處理,會直接將元數據做爲calldata
傳遞給智能合約。若是智能合約是個Solidity程序,那麼它會將這些輸入字節解釋爲方法調用,併爲setA(1)
執行適當的彙編代碼。
輸入數據能夠分紅兩個子部分:
# 方法選擇器(4字節) 0xee919d5 #第一個參數(32字節) 00000000000000000000000000000000000000000000000000000000000000001
前面的4個字節是方法選擇器,剩下的輸入數據是方法的參數,32個字節的塊。在這個例子中,只有一個參數,值是0x1
。
方法選擇器是方法簽名的 kecccak256 哈希值。在這個例子中方法的簽名是setA(uint256)
,也就是方法名稱和參數的類型。
讓咱們用Python來計算方法選擇器。首先,哈希方法簽名:
# 安裝pyethereum [https://github.com/ethereum/pyethereum/#installation](https://github.com/ethereum/pyethereum/#installation)> from ethereum.utils import sha3> sha3("setA(uint256)").hex()'ee919d50445cd9f463621849366a537968fe1ce096894b0d0c001528383d4769'
而後獲取哈希值的前4字節:
> sha3("setA(uint256)")[0:4].hex() 'ee919d50'
對於EVM而言,交易的輸入數據(calldata
)只是一個字節序列。EVM內部不支持調用方法。
智能合約能夠選擇經過以結構化的方式處理輸入數據來模擬方法調用,就像前面所說的那樣。
若是EVM上的全部語言都贊成相同的方式解釋輸入數據,那麼它們就能夠很容易進行交互。 合約應用二進制接口(ABI)指定了一個通用的編碼模式。
咱們已經看到了ABI是如何編碼一個簡單的方法調用,例如SetA(1)
。在後面章節中咱們將會看到方法調用和更復雜的參數是如何編碼的。
若是你調用的方法改變了狀態,那麼整個網絡必需要贊成。這就須要有交易,並消耗gas。
一個獲取者如getA()
不會改變任何東西。咱們能夠將方法調用發送到本地的以太坊節點,而不用請求整個網絡來執行計算。一個eth_call
RPC請求能夠容許你在本地模擬交易。這對於只讀方法或gas使用評估比較有幫助。
一個eth_call
就像一個緩存的HTTP GET請求。
製做一個eth_call
來調用 getA
方法,經過返回值來獲取狀態a
。首先,計算方法選擇器:
>>> sha3("getA()")[0:4].hex() 'd46300fd'
因爲沒有參數,輸入數據就只有方法選擇器了。咱們能夠發送一個eth_call
請求給任意的以太坊節點。對於這個例子,咱們依然將請求發送給 infura.io的公共以太坊節點:
$ curl -X POST \-H "Content-Type: application/json" \"[https://rinkeby.infura.io/YOUR_INFURA_TOKEN](https://rinkeby.infura.io/YOUR_INFURA_TOKEN)" \--data '{"jsonrpc": "2.0","id": 1,"method": "eth_call","params": [{"to": "0x62650ae5c5777d1660cc17fcd4f48f6a66b9a4c2","data": "0xd46300fd"},"latest"]}'
根據ABI,該字節應該會解釋爲0x1
數值。
如今來看看編譯的合約是如何處理源輸入數據的,並以此來製做一個方法調用。思考一個定義了setA(uint256)
的合約:
pragma solidity ^0.4.11; contract C { uint256 a; // 注意: `payable` 讓彙編簡單一點點 function setA(uint256 _a) payable { a = _a; } }
編譯:
solc --bin --asm --optimize call.sol
調用方法的彙編代碼在合約內部,在sub_0
標籤下:
sub_0: assembly { mstore(0x40, 0x60) and(div(calldataload(0x0), 0x100000000000000000000000000000000000000000000000000000000), 0xffffffff) 0xee919d50 dup2 eq tag_2 jumpi tag_1: 0x0 dup1 revert tag_2: tag_3 calldataload(0x4) jump(tag_4) tag_3: stop tag_4: /* "call.sol":95:96 a */ 0x0 /* "call.sol":95:101 a = _a */ dup2 swap1 sstore tag_5: pop jump // 跳出 auxdata: 0xa165627a7a7230582016353b5ec133c89560dea787de20e25e96284d67a632e9df74dd981cc4db7a0a0029 }
這裏有兩個樣板代碼與此討論是無關的,可是僅供參考:
mstore(0x40, 0x60)
爲sha3哈希保留了內存中的前64個字節。無論合約是否須要,這個都會存在的。auxdata
用來驗證發佈的源碼與部署的字節碼是否相同的。這個是可選擇的,可是嵌入到了編譯器中將剩下的彙編代碼分紅兩個部分,這樣容易分析一點:
首先,匹配選擇器的註釋彙編代碼:
// 加載前4個字節做爲方法選擇器 and(div(calldataload(0x0), 0x100000000000000000000000000000000000000000000000000000000), 0xffffffff) // 若是選擇器匹配`0xee919d50`, 跳轉到 setA 0xee919d50 dup2 eq tag_2 jumpi // 匹配失敗,返回並還原 tag_1: 0x0 dup1 revert // setA函數 tag_2: ...
除了開始從調用數據裏面加載4字節時的位轉移,其餘的都是很是清晰明朗的。爲了清晰可見,給出了彙編邏輯的低級僞代碼:
methodSelector = calldata[0:4] if methodSelector == "0xee919d50": goto tag_2 // 跳轉到setA else: // 匹配失敗,返回並還原 revert
實際方法調用的註釋彙編代碼:
// setA tag_2: // 方法調用以後跳轉的地方 tag_3 // 加載第一個參數(數值0x1). calldataload(0x4) // 執行方法 jump(tag_4) tag_4: // sstore(0x0, 0x1) 0x0 dup2 swap1 sstore tag_5: pop //程序的結尾,將會跳轉到 tag_3並中止 jump tag_3: // 程序結尾 stop
在進入方法體以前,彙編代碼作了兩件事情:
低級的僞代碼:
// 保存位置,方法調用結束後返回此位置 @returnTo = tag_3 tag_2: // setA // 從調用數據裏面加載參數到棧中 @arg1 = calldata[4:4+32] tag_4: // a = _a sstore(0x0, @arg1) tag_5 // 返回 jump(@returnTo) tag_3: stop
將這兩部分組合起來:
methodSelector = calldata[0:4] if methodSelector == "0xee919d50": goto tag_2 // goto setA else: // 無匹配方法。失敗 revert @returnTo = tag_3 tag_2: // setA(uint256 _a) @arg1 = calldata[4:36] tag_4: // a = _a sstore(0x0, @arg1) tag_5 // 返回 jump(@returnTo) tag_3: stop
有趣的小細節:
revert
的操做碼是fd
。可是在黃皮書中你不會找到它的詳細說明,或者在代碼中找到它的實現。實際上,fd
不是確實存在的!這是個無效的操做。當EVM遇到了一個無效的操做,它會放棄而且會有還原狀態的反作用。
Solidity編譯器是如何爲有多個方法的合約產生彙編代碼的?
pragma solidity ^0.4.11; contract C { uint256 a; uint256 b; function setA(uint256 _a) { a = _a; } function setB(uint256 _b) { b = _b; } }
簡單,只要一些if-else
分支就能夠了:
// methodSelector = calldata[0:4] and(div(calldataload(0x0), 0x100000000000000000000000000000000000000000000000000000000), 0xffffffff) // if methodSelector == 0x9cdcf9b 0x9cdcf9b dup2 eq tag_2 // SetB jumpi // elsif methodSelector == 0xee919d50 dup1 0xee919d50 eq tag_3 // SetA jumpi
僞代碼:
methodSelector = calldata[0:4] if methodSelector == "0x9cdcf9b": goto tag_2 elsif methodSelector == "0xee919d50": goto tag_3 else: // Cannot find a matching method. Fail. revert
對於一個方法調用,交易輸入數據的前4個字節老是方法選擇器。跟在後面的32字節塊就是方法參數。 ABI編碼規範顯示了更加複雜的參數類型是如何被編碼的,可是閱讀起來很是的痛苦。
另外一個學習ABI編碼的方式是使用 pyethereum的ABI編碼函數 來研究不一樣數據類型是如何編碼的。咱們會從簡單的例子開始,而後創建更復雜的類型。
首先,導出encode_abi
函數:
from ethereum.abi import encode_abi
對於一個有3個uint256
類型參數的方法(例如foo(uint256 a, uint256 b, uint256 c)
),編碼參數只是簡單的依次對uint256
數值進行編碼:
# 第一個數組列出了參數的類型 # 第二個數組列出了參數的值 > encode_abi(["uint256", "uint256", "uint256"],[1, 2, 3]).hex() 0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000000000000000000000000000000000000000000003
小於32字節的類型會被填充到32字節:
> encode_abi(["int8", "uint32", "uint64"],[1, 2, 3]).hex() 0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000000000000000000000000000000000000000000003
對於定長數組,元素仍是32字節的塊(若是必要的話會填充0),依次排列:
> encode_abi( ["int8[3]", "int256[3]"], [[1, 2, 3], [4, 5, 6]] ).hex() // int8[3]. Zero-padded to 32 bytes. 0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000000000000000000000000000000000000000000003 // int256[3]. 0000000000000000000000000000000000000000000000000000000000000004 0000000000000000000000000000000000000000000000000000000000000005 0000000000000000000000000000000000000000000000000000000000000006
ABI介紹了一種間接的編碼動態數組的方法,遵循一個叫作頭尾編碼的模式。
該模式其實就是動態數組的元素被打包到交易的調用數據尾部,參數(「頭」)會被引用到調用數據裏,這裏就是數組元素。
若是咱們調用的方法有3個動態數組,參數的編碼就會像這樣(添加註釋和換行爲了更加的清晰):
> encode_abi( ["uint256[]", "uint256[]", "uint256[]"], [[0xa1, 0xa2, 0xa3], [0xb1, 0xb2, 0xb3], [0xc1, 0xc2, 0xc3]] ).hex() /************* HEAD (32*3 bytes) *************/ // 參數1: 數組數據在0x60位置 0000000000000000000000000000000000000000000000000000000000000060 // 參數2:數組數據在0xe0位置 00000000000000000000000000000000000000000000000000000000000000e0 // 參數3: 數組數據在0x160位置 0000000000000000000000000000000000000000000000000000000000000160 /************* TAIL (128**3 bytes) *************/ // 0x60位置。參數1的數據 // 長度後跟這元素 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000a1 00000000000000000000000000000000000000000000000000000000000000a2 00000000000000000000000000000000000000000000000000000000000000a3 // 0xe0位置。參數2的數據 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000b1 00000000000000000000000000000000000000000000000000000000000000b2 00000000000000000000000000000000000000000000000000000000000000b3 //0x160位置。參數3的數據 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000c1 00000000000000000000000000000000000000000000000000000000000000c2 00000000000000000000000000000000000000000000000000000000000000c3
HEAD
部分有32字節參數,指出TAIL
部分的位置,TAIL
部分包含了3個動態數組的實際數據。
舉個例子,第一個參數是0x60
,指出調用數據的第96個(0x60
)字節。若是你看一下第96個字節,它是數組的開始地方。前32字節是長度,後面跟着的是3個元素。
混合動態和靜態參數是可能的。這裏有個(static
,dynamic
,static
)參數。靜態參數按原樣編碼,而第二個動態數組的數據放到了尾部:
> encode_abi( ["uint256", "uint256[]", "uint256"], [0xaaaa, [0xb1, 0xb2, 0xb3], 0xbbbb] ).hex() /************* HEAD (32*3 bytes) *************/ // 參數1: 0xaaaa 000000000000000000000000000000000000000000000000000000000000aaaa // 參數2:數組數據在0x60位置 0000000000000000000000000000000000000000000000000000000000000060 // 參數3: 0xbbbb 000000000000000000000000000000000000000000000000000000000000bbbb /************* TAIL (128 bytes) *************/ // 0x60位置。參數2的數據 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000b1 00000000000000000000000000000000000000000000000000000000000000b2 00000000000000000000000000000000000000000000000000000000000000b3
字符串和字節數組一樣是頭尾編碼。惟一的區別是字節數組會被緊密的打包成一個32字節的塊,就像:
> encode_abi( ["string", "string", "string"], ["aaaa", "bbbb", "cccc"] ).hex() // 參數1: 字符串數據在0x60位置 0000000000000000000000000000000000000000000000000000000000000060 // 參數2:字符串數據在0xa0位置 00000000000000000000000000000000000000000000000000000000000000a0 // 參數3:字符串數據在0xe0位置 00000000000000000000000000000000000000000000000000000000000000e0 // 0x60 (96)。 參數1的數據 0000000000000000000000000000000000000000000000000000000000000004 6161616100000000000000000000000000000000000000000000000000000000 // 0xa0 (160)。參數2的數據 0000000000000000000000000000000000000000000000000000000000000004 6262626200000000000000000000000000000000000000000000000000000000 // 0xe0 (224)。參數3的數據 0000000000000000000000000000000000000000000000000000000000000004 6363636300000000000000000000000000000000000000000000000000000000
對於每一個字符串/字節數組,前面的32字節是編碼長度,後面跟着纔是字符串/字節數組的內容。
若是字符串大於32字節,那麼多個32字節塊就會被使用:
// 編碼字符串的48字節 ethereum.abi.encode_abi( ["string"], ["a" * (32+16)] ).hex() 0000000000000000000000000000000000000000000000000000000000000020 //字符串的長度爲0x30 (48) 0000000000000000000000000000000000000000000000000000000000000030 6161616161616161616161616161616161616161616161616161616161616161 6161616161616161616161616161616100000000000000000000000000000000
嵌套數組中每一個嵌套有一個間接尋址。
> encode_abi( ["uint256[][]"], [[[0xa1, 0xa2, 0xa3], [0xb1, 0xb2, 0xb3], [0xc1, 0xc2, 0xc3]]] ).hex() //參數1:外層數組在0x20位置上 0000000000000000000000000000000000000000000000000000000000000020 // 0x20。每一個元素都是裏層數組的位置 0000000000000000000000000000000000000000000000000000000000000003 0000000000000000000000000000000000000000000000000000000000000060 00000000000000000000000000000000000000000000000000000000000000e0 0000000000000000000000000000000000000000000000000000000000000160 // array[0]在0x60位置上 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000a1 00000000000000000000000000000000000000000000000000000000000000a2 00000000000000000000000000000000000000000000000000000000000000a3 // array[1] 在0xe0位置上 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000b1 00000000000000000000000000000000000000000000000000000000000000b2 00000000000000000000000000000000000000000000000000000000000000b3 // array[2]在0x160位置上 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000c1 00000000000000000000000000000000000000000000000000000000000000c2 00000000000000000000000000000000000000000000000000000000000000c3
爲何ABI將方法選擇器截斷到4個字節?若是咱們不使用sha256的整個32字節,會不會不幸的碰到不一樣方法發生衝突的狀況? 若是這個截斷是爲了節省成本,那麼爲何在用更多的0來進行填充時,而僅僅只爲了節省方法選擇器中的28字節而截斷呢?
這種設計看起來互相矛盾……直到咱們考慮到一個交易的gas成本。
啊哈!0要便宜17倍,0填充如今看起來沒有那麼不合理了。
方法選擇器是一個加密哈希值,是個僞隨機。一個隨機的字符串傾向於擁有不少的非0字節,由於每一個字節只有0.3%(1/255)的機率是0。
0x1
填充到32字節成本是192 gasABI展現了另一個底層設計的奇特例子,經過gas成本結構進行激勵。
負整數….
通常使用叫作 補碼的方式來表達負整數。int8
類型-1
的數值編碼會都是1。1111 1111
。
ABI用1來填充負整數,因此-1
會被填充爲:
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
越大的負整數(-1
大於-2
)1越多,會花費至關多的gas。
與智能合約交互,你須要發送原始字節。它會進行一些計算,可能會改變本身的狀態,而後會返回給你原始字節。方法調用實際上不存在,這是ABI創造的集體假象。
ABI被指定爲一個低級格式,可是在功能上更像一個跨語言RPC框架的序列化格式。
咱們能夠在DApp和Web App的架構層面之間進行類比:
翻譯自 https://medium.com/@hayeah/diving-into-the-ethereum-vm-part-2-storage-layout-bc5349cb11b7