1. 程式人生 > >以太坊原始碼學習 – EVM

以太坊原始碼學習 – EVM

學習文件連結:here

一、虛擬機器外

主要功能:

執行前將Transaction型別轉化成Message,建立虛擬機器(EVM)物件,計算一些Gas消耗,以及執行交易完畢後建立收據(Receipt)物件並返回
  • 1

1.1 入口 和 返回值

檔案:/core/state_processor.go  --- Process()

for i, tx := range block.Transactions() {
    statedb.Prepare(tx.Hash(), block.Hash(), i)
    receipt, _, err := ApplyTransaction(p.config, p.bc, nil, gp, statedb, header, tx, totalUsedGas, cfg)
    if err != nil { return nil, nil, nil, err } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) } //將block裡面所有的tx逐個遍歷執行,ApplyTransaction, 每次執行完返回一個收據(Receipt)物件

我們來看下Receipt結構體:

type Receipt struct {
    // Consensus fields
    PostState         []byte   `json:"root"`
    Failed            bool     `json:"failed"`
    CumulativeGasUsed *big.Int `json:"cumulativeGasUsed" gencodec:"required"`
    Bloom             Bloom    `json:"logsBloom"         gencodec:"required"`
    Logs              []*Log   `json:"logs"              gencodec:"required"`

    // Implementation fields (don't reorder!)
    TxHash          common.Hash    `json:"transactionHash" gencodec:"required"`
    ContractAddress common.Address `json:"contractAddress"`
    GasUsed         *big.Int       `json:"gasUsed" gencodec:"required"`
}

解釋:

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

PostState:  儲存了建立該Receipt物件時,整個Block內所有“帳戶”的當時狀態。Ethereum 裡用stateObject來表示一個賬戶Account,這個賬戶可轉帳(transfer value), 可執行tx, 它的唯一標示符是一個Address型別變數。 這個Receipt.PostState 就是當時所在Block裡所有stateObject物件的RLP Hash值。

Bloom: Ethereum內部實現的一個256bit長Bloom Filter。 Bloom Filter概念定義可見wikipedia,它可用來快速驗證一個新收到的物件是否處於一個已知的大量物件集合之中。這裡Receipt的Bloom,被用以驗證某個給定的Log是否處於Receipt已有的Log陣列中。

1.2 封裝EVM物件和Message物件

我們來看一下ApplyTransaction():

檔案:/core/state_processor.go  --- ApplyTransaction()

//=====Message物件=====
msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
if err != nil { return nil, nil, err }

//=====EVM物件=====
context := NewEVMContext(msg, header, bc, author)
vmenv := vm.NewEVM(context, statedb, config, cfg)

//完成tx的執行
_, gas, failed, err := ApplyMessage(vmenv, msg, gp)

//建立一個收據Receipt物件,最後返回該Recetip物件,以及整個tx執行過程所消耗Gas數量。
... 

我們來看一下ApplyMessage()

檔案:/core/state_transition.go  --- ApplyMessage()

//發現呼叫了TransitionDb()
, _, gasUsed, failed, err := st.TransitionDb()

我們來看一下TransitionDb()

檔案:/core/state_transition.go  --- TransitionDb()

//購買gas
//計算tx固有gas
//EVM執行 //計算本次執行交易的實際gas消耗 //償退gas //獎勵所屬區塊的挖掘者

二、 虛擬機器內

包括執行轉帳,和建立合約並執行合約的指令陣列

2.1 EVM結構體

我們來看一下EVM的結構體:

檔案:/core/vm/evm.go

type EVM struct {

    Context --攜帶輔助資訊:Transaction的資訊(GasPrice, GasLimit),Block的資訊(Number, Difficulty),以及轉帳函式等 StateDB StateDB --為EVM提供statedb的相關操作 depth int chainConfig *params.ChainConfig chainRules params.Rules vmConfig Config interpreter *Interpreter --直譯器,用來解釋執行EVM中合約的指令 abort int32 }

2.2 完成轉賬

交易的轉帳操作由Context物件中的TransferFunc型別函式來實現,類似的函式型別,還有CanTransferFunc, 和GetHashFunc。
檔案:/core/evm.go --Transfer()

db.SubBalance(sender, amount)  //轉出賬戶減到一定金額以太幣
db.AddBalance(recipient, amount) //轉入賬戶增加一定金額以太幣

//注意:轉出和轉入賬戶的操作不會立即生效,StateDB 並不是真正的資料庫,只是一行為類似資料庫的結構體它在內部以Trie的資料結構來管理各個基於地址的賬戶,可以理解成一個cache;當該賬戶的資訊有變化時,變化先儲存在Trie中。僅當整個Block要被插入到BlockChain時,StateDB 裡快取的所有賬戶的所有改動,才會被真正的提交到底層資料庫。

2.3 合約的建立、賦值

我們先來看一下contract 結構體

檔案:/core/vm/contract.go  

type Contract struct {
    CallerAddress common.Address caller ContractRef //轉賬轉出方地址 self ContractRef //轉入方地址 jumpdests destinations // result of JUMPDEST analysis. Code []byte //指令陣列,其中每一個byte都對應於一個預定義的虛擬機器指令 CodeHash common.Hash CodeAddr *common.Address Input []byte //資料陣列,是指令所操作的資料集合 Gas uint64 value *big.Int Args []byte DelegateCall bool }
建立合約: call(),create() -- 二者均在StateProcessor的ApplyTransaction()被呼叫以執行單個交易,並且都有呼叫轉帳函式完成轉帳。

我們來看一下call()

檔案:/core/vm/call.go  

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) //賦值Contract物件 contract := NewContract(caller, to, value, gas) contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr)) //呼叫run,執行該合約的指令 ret, err = run(evm, snapshot, contract, input) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != errExecutionReverted { contract.UseGas(contract.Gas) } } return ret, contract.Gas, err

2.4 預編譯合約

我們來看一下run():

檔案:/core/vm/run.go  

if contract.CodeAddr != nil {
    precompiles := PrecompiledContractsHomestead
    if evm.ChainConfig().IsByzantium(evm.BlockNumber) { precompiles = PrecompiledContractsByzantium } if p := precompiles[*contract.CodeAddr]; p != nil { return RunPrecompiledContract(p, input, contract) } } return evm.interpreter.Run(snapshot, contract, input)
可見如果待執行的Contract物件恰好屬於一組預編譯的合約集合-此時以指令地址CodeAddr為匹配項-那麼它可以直接執行;沒有經過預編譯的Contract,才會由Interpreter解釋執行。這裡的"預編譯",可理解為不需要編譯(解釋)指令(Code)。預編譯的合約,其邏輯全部固定且已知,所以執行中不再需要Code,僅需Input即可。

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

2.5 直譯器執行合約的指令

我們來看一下interpreter.go

可以看到一個Config結構體

檔案:/core/vm/.interpreter.go

type Config struct {
    Debug bool EnableJit bool ForceJit bool Tracer Tracer NoRecursion bool DisableGasMetering bool EnablePreimageRecording bool JumpTable [256]operation // }
operation: 每個operation物件正對應一個已定義的虛擬機器指令,它所含有的四個函式變數execute, gasCost, validateStack, memorySize 提供了這個虛擬機器指令所代表的所有操作。每個指令長度1byte,Contract物件的成員變數Code型別為[]byte,就是這些虛擬機器指令的任意集合。operation物件的函式操作,主要會用到Stack,Memory, IntPool 這幾個自定義的資料結構。

然後我們看一下interpreter.run()

檔案: 檔案:/core/vm/.interpreter.go --run()

核心: 逐個byte遍歷入參Contract物件的Code變數,將其解釋為一個已知的operation,然後依次呼叫該operation物件的四個函式

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

需要特別注意的是LOGn指令操作,它用來建立n個Log物件,這裡n最大是4。還記得Log在何時被用到麼?每個交易(Transaction,tx)執行完成後,會建立一個Receipt物件用來記錄這個交易的執行結果。Receipt攜帶一個Log陣列,用來記錄tx操作過程中的所有變動細節,而這些Log,正是通過合適的LOGn指令-即合約指令陣列(Contract.Code)中的單個byte,在其對應的operation裡被創建出來的。每個新建立的Log物件被快取在StateDB中的相對應的stateObject裡,待需要時從StateDB中讀取。