【译】Diving Into The Ethereum VM Part 4 - How To Decipher A Smart Contract Method Call
在本文中,我們將看到Solidity和EVM如何使外部程序能夠調用合約的方法并使其狀態發生變化。
“外部程序”不限于DApp / JavaScript。 任何可以使用HTTP RPC與以太坊節點進行通信的程序都可以通過創建事務來與部署在區塊鏈上的任何契約進行交互。
創建一個事務就像創建一個HTTP請求。 Web服務器將接受您的HTTP請求并更改數據庫。 一個交易將被網絡接受,并且底層區塊鏈被擴展以包括狀態改變。
事務對于智能合同來說是HTTP請求對于Web服務。
如果EVM裝配和Solidity數據表示不熟悉,請參閱本系列以前的文章以了解更多信息:
- EVM匯編代碼簡介。
- 如何表示固定長度的數據類型。
- 如何表示動態數據類型。
合同交易
我們來看一個將狀態變量設置為0x1的事務。 我們想與之交互的合約有一個setter和一個getter的變量a :
雜注扎實0.4.11; 合同C { uint256 a; 函數setA(uint256 _a){ a = _a; } 函數getA()返回(uint256){ 返回一個; } }該合同部署在測試網絡Rinkeby上。 隨意使用Etherscan在地址0x62650ae5 ...處檢查它。
我創建了一個可以調用setA(1)的事務。 在地址0x7db471e5 ...處檢查此事務。
交易的輸入數據是:
0xee919d500000000000000000000000000000000000000000000000000000000000000001對于EVM來說,這僅僅是36個字節的原始數據。 它被傳遞給未經處理的智能合同,作為calldata 。 如果智能聯系人是一個Solidity程序,那么它會將這些輸入字節解釋為方法調用,并為setA(1)執行適當的匯編代碼。
輸入數據可以分解為兩個子部分:
#方法選擇器(4字節) 0xee919d5 #第一個參數(32字節) 00000000000000000000000000000000000000000000000000000000000000001前四個字節是方法選擇器。 輸入數據的其余部分是32字節塊的方法參數。 在這種情況下,只有1個參數,值為0x1 。
方法選擇器是方法簽名的kecccak256散列。 在這種情況下,方法簽名是setA(uint256) ,它是方法的名稱和參數的類型。
我們來計算Python中的方法選擇器。 首先,散列方法簽名:
#安裝pyethereum https://github.com/ethereum/pyethereum/#installation >從ethereum.utils導入sha3 > sha3(“setA(uint256)”).hex() 'ee919d50445cd9f463621849366a537968fe1ce096894b0d0c001528383d4769'然后獲取哈希的前4個字節:
> sha3(“setA(uint256)”)[0:4] .hex() 'ee919d50'應用程序二進制接口(ABI)
就EVM而言,交易的輸入數據( calldata )只是一個字節序列。 EVM對調用方法沒有內置支持。
智能合約可以選擇通過以結構化方式處理輸入數據來模擬方法調用,如前一部分所示。
如果EVM上的語言都同意應該如何解釋輸入數據,那么他們可以很容易地相互操作。 合同應用程序二進制接口 (ABI)指定了一種通用編碼方案。
我們已經看到ABI如何編碼像setA(1)這樣的簡單方法調用。 在后面的章節中,我們將看到如何編碼具有更復雜參數的方法調用。
調用Getter
如果您正在調用的方法更改狀態,則整個網絡必須同意。 這將需要一筆交易,并且花費你的燃氣。
像getA()這樣的getter方法不會改變任何東西。 我們可以將方法調用發送到本地以太坊節點,而不是要求整個網絡進行計算。 eth_call RPC請求允許您在本地模擬事務。 這對于只讀方法或氣體用量估計很有用。
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 ”\ --data' { “jsonrpc”:“2.0”, “id”:1, “method”:“eth_call”, “params”:[ { “to”:“0x62650ae5c5777d1660cc17fcd4f48f6a66b9a4c2”, “data”:“0xd46300fd” }, “最新” ] } “EVM執行計算并返回原始字節:
{ “jsonrpc”: “2.0”, “ID”:1, “結果”: “0x0000000000000000000000000000000000000000000000000000000000000001” }根據ABI,字節應該被解釋為值0x1 。
用于外部方法調用的程序集
現在讓我們看看編譯的合約如何處理原始輸入數據以進行方法調用。 考慮定義setA(uint256)的合同:
雜注扎實0.4.11; 合同C { uint256 a; //注意:`應付'使組件更簡單一些 函數setA(uint256 _a)應付{ a = _a; } }編譯:
solc --bin --asm --optimize call.sol被調用方法的匯編代碼位于sub_0下的合約主體中:
sub_0:程序集{ mstore(0x40,0x60) 和(div(calldataload(0x0),0x100000000000000000000000000000000000000000000000000000000),0xffffffff) 0xee919d50 DUP2 EQ TAG_2 jumpi TAG_1: 為0x0 DUP1 還原 TAG_2: tag_3 calldataload(為0x4) 跳(tag_4) tag_3: 停止 tag_4: / *“call.sol”:95:96 a * / 為0x0 / *“call.sol”:95:101 a = _a * / DUP2 swap1 sstore tag_5: 流行的 跳出來 auxdata:0xa165627a7a7230582016353b5ec133c89560dea787de20e25e96284d67a632e9df74dd981cc4db7a0a0029 }有兩個樣板代碼與本次討論無關,但僅供參考:
- mstore(0x40, 0x60)位于頂部,用于保存sha3哈希的內存中的前64個字節。 合同是否需要它總是存在的。
- auxdata用于驗證發布的源代碼與部署的字節碼相同。 這是可選的,但可以編譯到編譯器中。
讓我們將剩余的匯編代碼分成兩部分以便于分析:
首先,用于匹配選擇器的帶注釋的程序集:
//加載前4個字節作為方法選擇器 和(div(calldataload(0x0),0x100000000000000000000000000000000000000000000000000000000),0xffffffff) //如果選擇器匹配`0xee919d50`,轉到setA 0xee919d50 DUP2 EQ TAG_2 jumpi //沒有匹配的方法。 失敗并恢復。 TAG_1: 為0x0 DUP1 還原 // setA的正文 TAG_2: ...除了在開始從呼叫數據加載4個字節的比特洗牌之外,它是直截了當的。 為了清楚起見,低級別偽代碼中的匯編邏輯如下所示:
methodSelector = calldata [0:4] 如果methodSelector ==“0xee919d50”: goto tag_2 // goto setA 其他: //沒有匹配的方法。 失敗并恢復。 還原實際方法調用的帶注釋的程序集:
// setA TAG_2: //方法調用后到哪里去 tag_3 //加載第一個參數(值為0x1)。 calldataload(為0x4) //執行方法。 跳(tag_4) tag_4: // sstore(0x0,0x1) 為0x0 DUP2 swap1 sstore tag_5: 流行的 //程序結束后,會轉到tag_3并停止 跳 tag_3: //程序結束 停止在進入方法體之前,程序集有兩件事:
在低級偽代碼中:
//保存方法調用后返回的位置。 @returnTo = tag_3 tag_2:// setA //將調用數據的參數加載到堆棧上。 @ arg1 = calldata [4:4 + 32] tag_4:// a = _a sstore(0x0,@ arg1) tag_5 //返回 跳(@returnTo) tag_3: 停止將兩部分組合在一起:
methodSelector = calldata [0:4] 如果methodSelector ==“0xee919d50”: goto tag_2 // goto setA 其他: //沒有匹配的方法。 失敗。 還原 @returnTo = tag_3 tag_2:// setA(uint256 _a) @ arg1 = calldata [4:36] tag_4:// a = _a sstore(0x0,@ arg1) tag_5 //返回 跳(@returnTo) tag_3: 停止 有趣的瑣事:恢復的操作碼是 fd 。 但是你不會在黃皮書中找到它的規范,或者在代碼中實現。 實際上, fd 實際上并不存在! 這是一個無效的操作。 當EVM遇到無效操作時,它會放棄并恢復狀態作為副作用。處理多種方法
Solidity編譯器如何為具有多個方法的合同生成裝配體?
雜注扎實0.4.11; 合同C { uint256 a; uint256 b; 函數setA(uint256 _a){ a = _a; } 函數setB(uint256 _b){ b = _b; } }簡單。 更多的if-else分支一個接一個:
// methodSelector = calldata [0:4] 和(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] 如果methodSelector ==“0x9cdcf9b”: goto tag_2 elsif methodSelector ==“0xee919d50”: goto tag_3 其他: //找不到匹配的方法。 失敗。 還原用于復雜方法調用的ABI編碼
不要擔心零。 沒關系。對于方法調用,事務輸入數據的前四個字節總是方法選擇器。 然后方法參數以32個字節的塊為單位。 ABI編碼規范詳細說明了更復雜類型的參數是如何編碼的,但讀取會非常痛苦。
學習ABI編碼的另一個策略是使用pyethereum的ABI編碼函數來研究如何對不同類型的數據進行編碼。 我們將從簡單的案例開始,并構建更復雜的類型。
首先,導入encode_abi函數:
從ethereum.abi導入encode_abi對于有三個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]。 零填充到32個字節。 0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000000000000000000000000000000000000000000003 // int256 [3]。 0000000000000000000000000000000000000000000000000000000000000004 0000000000000000000000000000000000000000000000000000000000000005 0000000000000000000000000000000000000000000000000000000000000006動態數組的ABI編碼
ABI引入了一個間接層來對動態數組進行編碼,遵循稱為頭尾編碼的方案。
這個想法是動態數組的元素被封裝在事務的calldata的尾部。 參數(“頭”)是對數組元素所在的calldata的引用。
如果我們調用一個包含3個動態數組的方法,則參數會像這樣編碼(為了清晰起見添加了注釋和換行符):
> encode_abi( [“uint256 []”,“uint256 []”,“uint256 []”], [[0xa1,0xa2,0xa3],[0xb1,0xb2,0xb3],[0xc1,0xc2,0xc3]] ).HEX() / ************* HEAD(32 * 3字節)************* / // arg1:查看陣列數據的位置0x60 0000000000000000000000000000000000000000000000000000000000000060 // arg2:查看數組數據的位置0xe0 00000000000000000000000000000000000000000000000000000000000000e0 // arg3:查看陣列數據的位置0x160 0000000000000000000000000000000000000000000000000000000000000160 / ************* TAIL(128 ** 3個字節)************* / //位置0x60。 arg1的數據。 //長度后跟元素。 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000a1 00000000000000000000000000000000000000000000000000000000000000a2 00000000000000000000000000000000000000000000000000000000000000a3 //位置0xe0。 數據為arg2。 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000b1 00000000000000000000000000000000000000000000000000000000000000b2 00000000000000000000000000000000000000000000000000000000000000b3 //位置0x160。 arg3的數據。 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000c1 00000000000000000000000000000000000000000000000000000000000000c2 00000000000000000000000000000000000000000000000000000000000000c3所以head有三個32字節的參數,指向尾部的位置,它包含三個動態數組的實際數據。
例如,第一個參數是0x60 ,指向calldata的第96個( 0x60 )字節。 如果你看第96個字節,它是一個數組的開始。 前32個字節是長度,后面是三個元素。
可以混合動態和靜態參數。 這里有一個(static, dynamic, static)參數的例子。 靜態參數按原樣編碼,而第二個動態數組的數據放置在尾部:
> encode_abi( [“uint256”,“uint256 []”,“uint256”], [0xaaaa,[0xb1,0xb2,0xb3],0xbbbb] ).HEX() / ************* HEAD(32 * 3字節)************* / // arg1:0xaaaa 000000000000000000000000000000000000000000000000000000000000aaaa // arg2:查看陣列數據的位置0x60 0000000000000000000000000000000000000000000000000000000000000060 // arg3:0xbbbb 000000000000000000000000000000000000000000000000000000000000bbbb / ************* TAIL(128字節)************* / //位置0x60。 數據為arg2。 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000b1 00000000000000000000000000000000000000000000000000000000000000b2 00000000000000000000000000000000000000000000000000000000000000b3很多零,但沒關系。
編碼字節
字符串和字節數組也被頭尾編碼。 唯一的區別是這些字節以32字節的塊形式緊密包裝,如下所示:
> encode_abi( [“string”,“string”,“string”], [“aaaa”,“bbbb”,“cccc”] ).HEX() // arg1:查看字符串數據的位置0x60 0000000000000000000000000000000000000000000000000000000000000060 // arg2:查看字符串數據的位置0xa0 00000000000000000000000000000000000000000000000000000000000000a0 // arg3:查看字符串數據的位置0xe0 00000000000000000000000000000000000000000000000000000000000000e0 // 0x60(96)。 arg1的數據 0000000000000000000000000000000000000000000000000000000000000004 6161616100000000000000000000000000000000000000000000000000000000 // 0xa0(160)。 數據為arg2 0000000000000000000000000000000000000000000000000000000000000004 6262626200000000000000000000000000000000000000000000000000000000 // 0xe0(224)。 arg3的數據 0000000000000000000000000000000000000000000000000000000000000004 6363636300000000000000000000000000000000000000000000000000000000對于每個字符串/字節數組,前32個字節對??長度進行編碼,后跟字節。
如果字符串大于32個字節,則使用多個32字節的塊:
//編碼48個字節的字符串數據 ethereum.abi.encode_abi( [“串”], [“a”*(32 + 16)] ).HEX() 0000000000000000000000000000000000000000000000000000000000000020 //字符串的長度是0x30(48) 0000000000000000000000000000000000000000000000000000000000000030 6161616161616161616161616161616161616161616161616161616161616161 6161616161616161616161616161616100000000000000000000000000000000嵌套陣列
嵌套數組每個嵌套有一個間接方向。
> encode_abi( [ “uint256 [] []”], [[[0xa1,0xa2,0xa3],[0xb1,0xb2,0xb3],[0xc1,0xc2,0xc3]]] ).HEX() // arg1:outter數組位于位置0x20。 0000000000000000000000000000000000000000000000000000000000000020 // 0x20。 每個元素都是內部數組的位置。 0000000000000000000000000000000000000000000000000000000000000003 0000000000000000000000000000000000000000000000000000000000000060 00000000000000000000000000000000000000000000000000000000000000e0 0000000000000000000000000000000000000000000000000000000000000160 // 0x60處的數組[0] 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000a1 00000000000000000000000000000000000000000000000000000000000000a2 00000000000000000000000000000000000000000000000000000000000000a3 //數組[1]在0xe0 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000b1 00000000000000000000000000000000000000000000000000000000000000b2 00000000000000000000000000000000000000000000000000000000000000b3 //數組[2]在0x160 0000000000000000000000000000000000000000000000000000000000000003 00000000000000000000000000000000000000000000000000000000000000c1 00000000000000000000000000000000000000000000000000000000000000c2 00000000000000000000000000000000000000000000000000000000000000c3雅,很多零。
氣體成本和ABI編碼設計
為什么ABI將方法選擇器截斷為只有4個字節? 如果我們不使用sha256的全部32個字節,那么對于不同的方法是否會出現不幸的碰撞? 如果截斷是為了節省成本,為什么還要在方法選擇器中節省28個字節,如果它正在浪費更多的字節和零填充?
這兩個設計選擇似乎是矛盾的......直到我們考慮交易的天然氣成本。
- 每筆交易支付21000美元。
- 4支付每個零字節的數據或代碼進行交易。
- 對于交易的每個非零字節的數據或代碼支付68。
啊哈! 零點便宜17倍,因此零點填充并不像看起來那么糟糕。
方法選擇器是一個密碼散列,它是偽隨機的。 一個隨機字符串往往會有大部分非零字節,因為每個字節只有0.3%(1/255)的可能性為0。
- 填充到32字節的0x1花費192個氣體。
4 * 31(零字節)+68(1個非零字節)
- sha256很可能有32個非零字節,這大約需要2176個氣體
32 * 68
- 截斷為4字節的sha256將花費約272氣體
32 * 4
ABI展示了另一個由天然氣成本結構激勵的低級別設計的例子。
負整數...
負整數通常使用稱為Two's Complement的方案來表示。 int8編碼的值-1將全部為1 1111 1111 。
ABI使用1來填充負整數,所以-1會填充為:
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff小負數主要是1秒,因此耗費相當多的天然氣。
ˉ\ _(ツ)_ /ˉ
結論
要與智能合約交互,請將其發送至原始字節。 它執行一些計算,可能會改變它自己的狀態,然后返回原始字節。 方法調用實際上不存在。 這是ABI創造的集體幻想。
ABI被指定為低級格式,但在功能上它更像是跨語言RPC框架的序列化格式。
我們可以在DApp和Web App的架構層之間進行類比:
- 區塊鏈就像支持數據庫。
- 合同就像一個網絡服務。
- 交易就像一個請求。
- ABI是數據交換格式,如協議緩沖區 。
如果你喜歡這篇文章,你應該在Twitter @hayeah 上關注我 。
在這篇關于EVM的文章系列中,我寫到:
- EVM匯編代碼簡介。
- 如何表示固定長度的數據類型。
- 如何表示動態數據類型。
- ABI如何編碼外部方法調用。
https://medium.com/@hayeah/how-to-decipher-a-smart-contract-method-call-8ee980311603
總結
以上是生活随笔為你收集整理的【译】Diving Into The Ethereum VM Part 4 - How To Decipher A Smart Contract Method Call的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【译】Diving Into The E
- 下一篇: 【译】Diving Into The E