以太坊ETH交易部分分析

本文轉載於https://www.8btc.com/article/265557

交易結構

交易結構定義在 core/types/transaction.go 中:

 

這個 atomic 是 go 語言的一個包 sync/atomic,用來實現原子操作。在這個結構體中, data 爲數據字段,其餘三個爲緩存。下面是計算hash的函數:

計算哈希前,首先會從緩存 tx.hash 中獲取,如果取到,則直接返回值。沒有,則使用rlpHash 計算:

hash 的計算方式爲:先將交易的 tx.data 進行 rlpEncode 編碼(定義在:core/types/transaction.go 中)

然後再進行算法爲 Keccak256 的哈希計算。即:txhash=Keccak256(rlpEncode(tx.data))

Transaction 中,data 爲 txdata 類型的,定義於同文件中,裏面詳細規定了交易的具體字段:

這些字段的詳細解釋如下:

  • AccountNonce:此交易的發送者已發送過的交易數(可防止重放攻擊)
  • Price:此交易的 gas price
  • GasLimit:本交易允許消耗的最大 gas 數量
  • Recipient:交易的接收者地址,如果這個字段爲 nil 的話,則這個交易爲“合約創建”類型交易
  • Amount:交易轉移的以太幣數量,單位是 wei
  • Payload:交易可以攜帶的數據,在不同類型的交易中有不同的含義
  • V R S:交易的簽名數據

我們會發現,交易中沒有包含發送者地址這條數據,這是因爲這個地址已包含在簽名信息中,後面我們會分析到相關代碼,另外,以太坊節點還會提供 JSON RPC 服務,供外部調用來傳輸數據。傳輸的數據格式爲 json,因此,本文件中,還定義了交易的 json 類型數據結構,以及相關的轉換函數。

 

函數爲:MarshalJSON()和 UnmarshlJSON(),這兩個函數會調用core/types/gen_tx_json.go 文件中的同名函數進行內外部數據類型的轉換。

交易存儲

交易的獲取與存儲函數爲:Get/WriteTXLookupEntries ,定義在 core/database_util.go中。

 

對於每個傳入的區塊,該函數會讀取塊中的每一條交易來分別處理。首先建立條目(entry),數據類型爲:txLookupEntry。內容包括區塊哈希、區塊號以及交易索引(交易 在區塊中的位置),然後將此 entry 進行 rlp 編碼作爲存入數據庫的 value。key 部分與區塊存儲類似,組成結構爲交易前綴+交易哈希。

此函數的調用主要在 core/blockchain.go 中,比如 WriteBlockAndState()會將區塊寫入數據庫,處理 body 部分時需要分別處理每條交易。而 WriteBlockAndState 是在miner/worker.go 中 wait 函數調用的。mainer/worker.go 中 newWorker 函數在創建新礦工時,會調用 worker.wait().

交易類型

在源碼中交易只有一種數據結構,如果非要給交易分個類的話,我認爲交易可以分爲三種:轉賬的交易、創建合約的交易、執行合約的交易。web3.js 提供了發送交易的接口:

 

web3.eth.sendTransaction(transactionObject [, callback]) (web3.js 在internal/jsre/deps 中)

參數是一個對象,如果在發送交易的時候指定不同的字段,區塊鏈節點就可以識別出對應類型的交易。

轉賬交易

​ 轉賬是最簡單的一種交易,這裏轉賬是指從一個賬戶向另一個賬戶發送以太幣。發送轉賬交易的時候只需要指定交易的發送者、接收者、轉幣的數量。使用 web3.js 發送轉賬交易應該像這樣:

value 是轉移的以太幣數量,單位是 wei,對應的是源碼中的 Amount 字段。to 對應的是源碼中的 Recipient

創建合約交易

​ 創建合約指的是將合約部署到區塊鏈上,這也是通過發送交易來實現。在創建合約的交易中,to 字段要留空不填,在 data 字段中指定合約的二進制代碼,from 字段是交易的發送者也是合約的創建者。

data 字段對應的是源碼中的 Payload 字段。

執行合約交易

調用合約中的方法,需要將交易的 to 字段指定爲要調用的合約的地址,通過 data 字段指定要調用的方法以及向該方法傳遞的參數。

data 字段需要特殊的編碼規則,具體細節可以參考 Ethereum Contract ABI(自己拼接字段既不方便又容易出錯,所以一般都使用封裝好的 SDK(比如 web3.js) 來調用合約)。

交易執行

​ 按照以太坊架構設計,交易的執行可大致分爲內外兩層結構:第一層是虛擬機外,包括執行前將 Transaction 類型轉化成 Message,創建虛擬機(EVM)對象,計算一些 Gas 消耗,以及執行交易完畢後創建收據(Receipt)對象並返回等;第二層是虛擬機內,包括執行 轉帳,和創建合約並執行合約的指令數組。

 

虛擬機外

執行 tx 的入口函數是 Process()函數,在 core/state_processor.go 中。

​ Process()函數的核心是一個 for 循環,它將 Block 裏的所有 tx 逐個遍歷執行。具體的執行函數爲同個 go 文件中的 ApplyTransaction()函數,它每次執行 tx, 會返回一個收據(Receipt)對象。Receipt 結構體的聲明如下(core/types/receipt.go):

​ Receipt 中有一個 Log 類型的數組,其中每一個 Log 對象記錄了 Tx 中一小步的操作。所以,每一個 tx 的執行結果,由一個 Receipt 對象來表示;更詳細的內容,由一組 Log 對象來記錄。這個 Log 數組很重要,比如在不同 Ethereum 節點(Node)的相互同步過程中, 待同步區塊的 Log 數組有助於驗證同步中收到的 block 是否正確和完整,所以會被單獨同步(傳輸)。

Receipt 的 PostState 保存了創建該 Receipt 對象時,整個 Block 內所有“帳戶”的當時狀態。Ethereum 裏用 stateObject 來表示一個賬戶 Account,這個賬戶可轉帳(transfer value), 可執行 tx, 它的唯一標示符是一個 Address 類型變量。 這個 Receipt.PostState 就是當時所在 Block 裏所有 stateObject 對象的 RLP Hash 值。

Bloom 類型是一個 Ethereum 內部實現的一個 256bit 長 Bloom Filter。 Bloom Filter 概念定義可見 wikipediahttp://blog.csdn.net/jiaomeng/article/details/1495500 它可用來快速驗證一個新收到的對象是否處於一個已知的大量對象集合之中。這裏 Receipt 的 Bloom, 被用以驗證某個給定的 Log 是否處於 Receipt 已有的 Log 數組中。

​ 我們來看下 StateProcessor.ApplyTransaction()的具體實現,它的基本流程如下圖:

​ ApplyTransaction()首先根據輸入參數分別封裝出一個 Message 對象和一個 EVM 對象,然後加上一個傳入的 GasPool 類型變量,執行 core/state_transition.go 中的ApplyMessage(),而這個函數又調用同 go 文件中 TransitionDb()函數完成 tx 的執行,待TransitionDb()返回之後,創建一個收據 Receipt 對象,最後返回該 Recetip 對象,以及整個tx 執行過程所消耗 Gas 數量。

GasPool 對象是在一個 Block 執行開始時創建,並在該 Block 內所有 tx 的執行過程中共享,對於一個 tx 的執行可視爲“全局”存儲對象; Message 由此次待執行的 tx 對象轉化而來,並攜帶了解析出的 tx 的(轉帳)轉出方地址,屬於待處理的數據對象;EVM 作爲Ethereum 世界裏的虛擬機(Virtual Machine),作爲此次 tx 的實際執行者,完成轉帳和合約(Contract)的相關操作。

我們來細看下 TransitioinDb()的執行過程(/core/state_transition.go)。假設有StateTransition 對象 st, 其成員變量 initialGas 表示初始可用 Gas 數量,gas 表示即時可用Gas 數量,初始值均爲 0,於是 st.TransitionDb() 可由以下步驟展開:

首先執行 preCheck()函數,檢查:1.交易中的 nonce 和賬戶 nonce 是否爲同一個。2. 檢查 gas 值是否合適(<=64 )

  • 購買 Gas。首先從交易的(轉帳)轉出方賬戶扣除一筆 Ether,費用等於tx.data.GasLimit * tx.data.Price; 同 時 st.initialGas = st.gas = tx.data.GasLimit; 然 後(GasPool) gp –= st.gas 。
  • 計算 tx 的固有 Gas 消耗 – intrinsicGas。它分爲兩個部分,每一個 tx 預設的消耗量,這個消耗量還因 tx 是否含有(轉帳)轉入方地址而略有不同;以及針對tx.data.Payload 的 Gas 消耗,Payload 類型是[]byte,關於它的固有消耗依賴於[]byte 中非 0 字節和 0 字節的長度。最終,st.gas –= intrinsicGas
  • EVM 執行。如果交易的(轉帳)轉入方地址(tx.data.Recipient)爲空,即contractCreation,調用 EVM 的 Create()函數;否則,調用 Call()函數。無論哪個函數返回後,更新 st.gas。
  • 計算本次執行交易的實際 Gas 消耗: requiredGas = st.initialGas – st.gas
  • 償退 Gas。它包括兩個部分:首先將剩餘 st.gas 折算成 Ether,歸還給交易的(轉帳)轉出方賬戶;然後,基於實際消耗量 requiredGas,系統提供一定的補償,數量爲 refundGas。refundGas 所折算的 Ether 會被立即加在(轉帳)轉出方賬戶上, 同時 st.gas += refundGas,gp += st.gas,即剩餘的 Gas 加上系統補償的 Gas,被一起歸併進 GasPool,供之後的交易執行使用。
  • 獎勵所屬區塊的挖掘者:系統給所屬區塊的作者,亦即挖掘者賬戶,增加一筆金額,數額等於 st.data,Price * (st.initialGas – st.gas)。注意,這裏的 st.gas 在步驟 5 中被加上了 refundGas, 所以這筆獎勵金所對應的 Gas,其數量小於該交易實際消耗量 requiredGas。

由上可見,除了步驟 3 中 EVM 函數的執行,其他每個步驟都在圍繞着 Gas 消耗量作文章。

 

步驟 5 的償退機制很有意思,設立它的目的何在?目前爲止我只能理解它可以避免交易執行過程中過快消耗 Gas,至於對其全面準確的理解尚需時日。

步驟 6 是獎勵機制,沒什麼好說的。

Ethereum 中每個交易(transaction,tx)對象在被放進 block 時,都是經過數字簽名的, 這樣可以在後續傳輸和處理中隨時驗證 tx 是否經過篡改。Ethereum 採用的數字簽名是橢圓曲線數字簽名算法(Elliptic Cure Digital Signature Algorithm,ECDSA)。ECDSA 相比於基於大質數分解的 RSA 數字簽名算法,可以在提供相同安全級別(in bits)的同時,僅需更短的公鑰(public key)。這裏需要特別留意的是,tx 的轉帳轉出方(發送方)地址,就是對該 tx 對象作 ECDSA 簽名計算時所用的公鑰 publicKey。

Ethereum 中的數字簽名計算過程所生成的簽名(signature), 是一個長度爲 65bytes 的字節數組,它被截成三段放進 tx 中,前 32bytes 賦值給成員變量 R, 再 32bytes 賦值給 S,1byte 賦給 V,當然由於 R、S、V 聲明的類型都是*big.Int, 上述賦值存在[]byte –> big.Int 的類型轉換。

當需要恢復出 tx 對象的轉帳轉出方地址時(比如在需要執行該交易時),Ethereum 會先從 tx 的 signature 中恢復出公鑰,再將公鑰轉化成一個 common.Address 類型的地址,signature 由 tx 對象的三個成員變量 R,S,V 轉化成字節數組[]byte 後拼接得到。

Ethereum 對此定義了一個接口 Signer, 用來執行掛載簽名,恢復公鑰,對 tx 對象做哈希等操作。 接口定義是在:/ core/types/transaction_signing.go 的:

這個接口主要做的就是恢復發送地址、生成簽名格式、生成交易哈希、驗證等。

生成數字簽名的函數叫 SignTx(),最根源是定義在 core/types/transaction_signing.go(mobile/accounts.go 中也有 SignTx,但是這個函數是調用 accounts/keystore/keystore.go中的 SignTX,最終又調用 types.SignTx),它會先調用其函數生成 signature, 然後調用tx.WithSignature()將 signature 分段賦值給 tx 的成員變量 R,S,V。

​ Signer 接口中,恢復(提取?)轉出方地址的函數爲:Sender,Sender returns the address derived from the signature (V, R, S) using secp256k1。使用到的參數是:Signer 和 Transaction ,該函數定義在core/types/transaction_signing.go 中

​ Sender()函數體中,signer.Sender()會從本次數字簽名的簽名字符串(signature)中恢復出公鑰,並轉化爲 tx 的(轉帳)轉出方地址。此函數最終會調用同文件下的 recoverPlain 函數來進行恢復

在上文提到的 ApplyTransaction()實現中,Transaction 對象需要首先被轉化成 Message接口,用到的AsMessage()函數即調用了此處的 Sender()。調用路徑爲: AsMessage->transaction_signing.Sender(兩個參數的)–>sender(單個參數的) 在 Transaction 對象 tx 的轉帳轉出方地址被解析出以後,tx 就被完全轉換成了Message 類型,可以提供給虛擬機 EVM 執行了。

虛擬機內:

​ 每個交易(Transaction)帶有兩部分內容(參數)需要執行:

  1. 轉帳,由轉出方地址向轉入方地址轉帳一筆以太幣 Ether;
  2. 攜帶的[]byte 類型成員變量 Payload,其每一個 byte 都對應了一個單獨虛擬機指令。這些內容都是由 EVM(Ethereum Virtual Machine)對象來完成 的。EVM 結構體是 Ethereum 虛擬機機制的核心,它與協同類的 UML 關係圖如下:

 

​ 其中 Context 結構體分別攜帶了 Transaction 的信息(GasPrice, GasLimit),Block 的信息(Number, Difficulty),以及轉帳函數等,提供給 EVM;StateDB 接口是針對 state.StateDB 結構體設計的本地行爲接口,可爲 EVM 提供 statedb 的相關操作; Interpreter 結構體作爲解釋器,用來解釋執行 EVM 中合約(Contract)的指令(Code)。

​ 注意,EVM 中定義的成員變量 Context 和 StateDB, 僅僅聲明瞭變量名而無類型,而變量名同時又是其類型名,在 Golang 中,這種方式意味着宗主結構體可以直接調用該成員變量的所有方法和成員變量,比如 EVM 調用 Context 中的 Transfer()。

交易的轉帳操作由 Context 對象中的 TransferFunc 類型函數來實現,類似的函數類型,還有 CanTransferFunc, 和 GetHashFunc。這三個類型的函數變量 CanTransfer, Transfer, GetHash,在 Context 初始化時從外部傳入,目前使用的均是一個本地實現。可見目前的轉帳函數 Transfer()的邏輯非常簡單,轉帳的轉出賬戶減掉一筆以太幣,轉入賬戶加上一筆以太幣。由於 EVM 調用的 Transfer()函數實現完全由 Context 提供,所以,假設如果基於 Ethereum 平臺開發,需要設計一種全新的“轉帳”模式,那麼只需寫一個新的 Transfer()函數實現,在 Context 初始化時賦值即可。

有朋友或許會問,這裏 Transfer()函數中對轉出和轉入賬戶的操作會立即生效麼?萬一兩步操作之間有錯誤發生怎麼辦?答案是不會立即生效。StateDB 並不是真正的數據庫, 只是一行爲類似數據庫的結構體。它在內部以 Trie 的數據結構來管理各個基於地址的賬 戶,可以理解成一個 cache;當該賬戶的信息有變化時,變化先存儲在 Trie 中。僅當整個Block 要被插入到 BlockChain 時,StateDB 裏緩存的所有賬戶的所有改動,纔會被真正的提交到底層數據庫。

合約的創建和賦值:

合約(Contract)是 EVM 用來執行(虛擬機)指令的結構體。Contract 的結構定義於:core/vm/contract.go 中,在這些成員變量裏,caller 是轉帳轉出方地址(賬戶),self 是轉入方地址,不過它們的類型都用接口 ContractRef 來表示;Code 是指令數組,其中每一個 byte 都對應於一個預定義的虛擬機指令;CodeHash 是 Code 的 RLP 哈希值;Input 是數據數組,是指令所操作的數據集合;Args 是參數。

​ 有意思的是 self 這個變量,爲什麼轉入方地址要被命名成 self 呢? Contract 實現了ContractRef 接口,返回的恰恰就是這個 self 地址。

func (c *Contract) Address() common.Address { return c.self.Address()

}

​ 所以當 Contract 對象作爲一個 ContractRef 接口出現時,它返回的地址就是它的 self地址。那什麼時候 Contract 會被類型轉換成 ContractRef 呢?當 Contract A 調用另一個Contract B 時,A 就會作爲 B 的 caller 成員變量出現。Contract 可以調用 Contract,這就爲系統在業務上的潛在擴展,提供了空間。

創建一個 Contract 對象時,重點關注對 self 的初始化,以及對 Code, CodeAddr 和Input 的賦值。

另外,StateDB 提供方法 SetCode(),可以將指令數組 Code 存儲在某個 stateObject 對象中; 方法 GetCode(),可以從某個 stateObject 對象中讀取已有的指令數組 Code。

 

1 2 3
func (self *StateDB) SetCode(addr common.Address, code []byte) /func (self

 

*StateDB) GetCode(addr common.Address) code []byte

​ stateObject (core/state/state_object.go)是 Ethereum 裏用來管理一個賬戶所有信息修改的結構體,它以一個 Address 類型變量爲唯一標示符。StateDB 在內部用一個巨大的map 結構來管理這些 stateObject 對象。所有賬戶信息-包括 Ether 餘額,指令數組 Code,該賬戶發起合約次數 nonce 等-它們發生的所有變化,會首先緩存到 StateDB 裏的某個stateObject 裏,然後在合適的時候,被 StateDB 一起提交到底層數據庫。

 

​ EVM(core/vm/evm.go)中 目前有五個函數可以創建並執行 Contract,按照作用和調用方式,可以分成兩類:

  • ​ Create(), Call(): 二者均在 StateProcessor 的 ApplyTransaction()被調用以執行單個交易,並且都有調用轉帳函數完成轉帳。
  • ​ CallCode(), DelegateCall(), StaticCall():三者由於分別對應於不同的虛擬機指令(1 byte)操作,不會用以執行單個交易,也都不能處理轉帳。考慮到與執行交易的相關性,這裏着重探討 Create()和 Call()。先來看 Call(),它用來處理(轉帳)轉入方地址不爲空的情況:

 

Call()函數的邏輯可以簡單分爲以上 6 步。其中步驟(3)調用了轉帳函數 Transfer(),轉入賬戶 caller, 轉出賬戶 addr;步驟(4)創建一個 Contract 對象,並初始化其成員變量 caller, self(addr), value 和 gas; 步驟(5)賦值 Contract 對象的 Code, CodeHash, CodeAddr 成員變量;步驟(6) 調用 run()函數執行該合約的指令,最後 Call()函數返回。相關代碼可見:

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) 

 

{

if evm.vmConfig.NoRecursion && evm.depth > 0 {//如果設置了“禁用 call”,並且depth 正確,直接返回

return nil, gas, nil

}

// Fail if we're trying to execute above the call depth limit

if evm.depth > int(params.CallCreateDepth) {//如果 call 的棧深度超過了預設值, 報錯

return nil, gas, ErrDepth

}

// Fail if we're trying to transfer more than the available balance

if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {//檢查發出賬戶是否有足夠的錢(實際實現的函數定義在 core/evm.go/CanTransfer()中)但目前還不知道是怎麼調用的

return nil, gas, ErrInsufficientBalance

}

var (

to = AccountRef(addr)

snapshot = evm.StateDB.Snapshot()

)

if !evm.StateDB.Exist(addr) {//建立賬戶

precompiles := PrecompiledContractsHomestead

if evm.ChainConfig().IsByzantium(evm.BlockNumber) { precompiles = PrecompiledContractsByzantium

}

if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {

return nil, gas, nil

}

evm.StateDB.CreateAccount(addr)

}

evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)//轉移

// initialise a new contract and set the code that is to be used by the

// E The contract is a scoped environment for this execution context

// only.

contract := NewContract(caller, to, value, gas)//建立合約contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr),

evm.StateDB.GetCode(addr))

ret, err = run(evm, snapshot, contract, input)

// When an error was returned by the EVM or when setting the creation code

// above we revert to the snapshot and consume any gas remaining. Additionally

// when we're in homestead this also counts for code storage gas errors. if err != nil {

evm.StateDB.RevertToSnapshot(snapshot) if err != errExecutionReverted {

contract.UseGas(contract.Gas)

}

}

return ret, contract.Gas, err

}

​ 因爲此時(轉帳)轉入地址不爲空,所以直接將入參 addr 初始化 Contract 對象的 self 地址,並可從 StateDB 中(其實是以 addr 標識的賬戶 stateObject 對象)讀取出相關的 Code 和CodeHash 並賦值給 contract 的成員變量。注意,此時轉入方地址參數 addr 同時亦被賦值予 contract.CodeAddr。

 

再來看看 EVM.Create(),它用來處理(轉帳)轉入方地址爲空的情況。

與 Call()相比,Create()因爲沒有 Address 類型的入參 addr,其流程有幾處明顯不同:

  • ​ 步驟(3)中創建一個新地址 contractAddr,作爲(轉帳)轉入方地址,亦作爲Contract 的 self 地址;
  • ​ 步驟(6)由於 contracrAddr 剛剛新建,db 中尚無與該地址相關的 Code 信息, 所以會將類型爲[]byte 的入參 code,賦值予 Contract 對象的 Code 成員;
  • ​ 步驟(8)將本次執行合約的返回結果,作爲 contractAddr 所對應賬戶(stateObject 對象)的 Code 儲存起來,以備下次調用。

​ 還有一點隱藏的比較深,Call()有一個入參 input 類型爲[]byte,而 Create()有一個入參code 類型同樣爲[]byte,沒有入參 input,它們之間有無關係?其實,它們來源都是Transaction 對象 tx 的成員變量 Payload!調用 EVM.Create()或 Call()的入口在StateTransition.TransitionDb()中,當 tx.Recipent 爲空時,tx.data.Payload 被當作所創建Contract 的 Code;當 tx.Recipient 不爲空時,tx.data.Payload 被當作 Contract 的 Input。

 

預編譯合約

​ EVM 中執行合約(指令)的函數是 run(),在 core/vm/evm.go 中其實現代碼如下: 可見如果待執行的 Contract 對象恰好屬於一組預編譯的合約集合-此時以指令地址CodeAddr 爲匹配項-那麼它可以直接運行;沒有經過預編譯的 Contract,纔會由Interpreter 解釋執行。這裏的”預編譯”,可理解爲不需要編譯(解釋)指令(Code)。預編譯的合約,其邏輯全部固定且已知,所以執行中不再需要 Code,僅需 Input 即可。

在代碼實現中,預編譯合約只需實現兩個方法 Required()和 Run()即可,這兩方法僅需一個入參 input。

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/ core/vm/contracts.go

 

type PrecompiledContract interface { RequiredGas(input []byte) uint64 Run(input []byte) ([]byte, error)

}

func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contrat) (ret []byte, err error) {

gas := p.RequiredGas(input) if contract.UseGas(gas) {

return p.Run(input)

}

return nil, ErrOutOfGas

}

目前,Ethereuem 代碼中已經加入了多個預編譯合約,功能覆蓋了包括橢圓曲線密鑰恢復,SHA-3(256bits)哈希算法,RIPEMD-160 加密算法等等。相信基於自身業務的需求,二次開發者完全可以加入自己的預編譯合約,大大加快合約的執行速度。

 

解釋器執行合約的指令

解釋器 Interpreter 用來執行(非預編譯的)合約指令。它的結構體 UML 關係圖如下所示:

​ Interpreter 結構體通過一個 Config 類型的成員變量,間接持有一個包括 256 個operation 對象在內的數組 JumpTable。operation 是做什麼的呢?

每個 operation 對象正對 應 一 個 已 定 義 的 虛 擬 機 指 令 , 它 所 含 有 的 四 個 函 數 變 量 execute, gasCost, validateStack, memorySize 提供了這個虛擬機指令所代表的所有操作。每個指令長度1byte,Contract 對象的成員變量 Code 類型爲[]byte,就是這些虛擬機指令的任意集合,operation 對象的函數操作,主要會用到 Stack,Memory, IntPool 這幾個自定義的數據結構。

​ 這樣一來,Interpreter 的 Run()函數就很好理解了,其核心流程就是逐個 byte 遍歷入參 Contract 對象的 Code 變量,將其解釋爲一個已知的 operation,然後依次調用該operation 對象的四個函數,流程示意圖如下:

operation 在操作過程中,會需要幾個數據結構: Stack,實現了標準容器 -棧的行爲;Memory,一個字節數組,可表示線性排列的任意數據;還有一個 intPool,提供對big.Int 數據的存儲和讀取。

已定義的 operation,種類很豐富,包括:

  • ​ 算術運算:ADD,MUL,SUB,DIV,SDIV,MOD,SMOD,EXP…;
  • ​ 邏輯運算:LT,GT,EQ,ISZERO,AND,XOR,OR,NOT…;
  • ​ 業務功能:SHA3,ADDRESS,BALANCE,ORIGIN,CALLER,GASPRICE,LOG1,LOG2…等等需要特別注意的是 LOGn 指令操作,它用來創建 n 個 Log 對象,這裏 n 最大是 4。還記得 Log 在何時被用到麼?每個交易(Transaction,tx)執行完成後,會創建一個 Receipt 對象用來記錄這個交易的執行結果。Receipt 攜帶一個 Log 數組,用來記錄 tx 操作過程中的所有變動細節,而這些 Log,正是通過合適的 LOGn 指令-即合約指令數組(Contract.Code) 中的單個 byte,在其對應的 operation 裏被創建出來的。每個新創建的 Log 對象被緩存在StateDB 中的相對應的 stateObject 裏,待需要時從 StateDB 中讀取。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章