以太坊源碼分析—交易的執行

前言

以太坊是一個運行智能合約的平臺,被稱做可編程的區塊鏈,容許用戶將編寫的智能合約部署在區塊鏈上運行。而運行合約的主體即是以太坊虛擬機(EVM)golang

區塊 交易 合約

區塊鏈由區塊(Block)組成,而區塊中打包必定數量的交易(Transaction),交易多是一個單純的轉帳操做,也多是調用一個智能合約,不管是哪種,EVM在運行(excute)交易時都會建立合約(Contract)數據庫

外部帳戶 合約帳戶

以太坊中的帳戶有兩類編程

  • 外部帳戶 由帳戶持有人的私鑰控制的真實存在的帳戶
  • 合約帳戶 由合約代碼控制,保存着合約代碼

一筆交易老是有發送方(sender),接收方(recipient)和數額(value) 三要素。發送方將必定數額的ETH轉移到接收方的帳戶,在單純的轉帳交易中,接收方是外部帳戶。而在調用智能合約的交易時,接收方是合約帳戶。segmentfault

gas

如同現實中的稅費同樣,交易也須要將支付少許的費用,稱爲gas,費用支付給礦工,這能夠激勵礦工打包交易到區塊,也使得區塊鏈避免惡意運算攻擊。gas由交易的發送者使用ETH購買,在執行交易的每一步都會消耗gas,若是gas用完了,交易狀態會被回退,但消耗的gas不會返還。網絡

交易執行

以太坊是一個基於交易的狀態機,一筆交易可使以太坊從一個狀態(state)切換到另外一個狀態,即交易的執行伴隨着狀態的改變。
交易執行的入口在 core/state_processor.goProcess()方法,下面是該方法的輪廓app

func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts,[]*types.Log,uint64,error) {
    ......
    var (
        usedGas = new(uint) 
        header = block.Header()
        gp = new(GasPool).AddGas(block.GasLimit())
    )
    for i, tx := range block.Transactions() {
        receipt, _, _ := ApplyTransaction(p.config, p.bc, nil, gp, statedb, header, tx, usedGas, cfg)
        receipts = append(receipts, receipt)
        allLogs = append(allLogs, receipt.Logs...)        
    }
    p.engine.Finalize(p.bc. header, statedb, block.Transactions(), block.Uncles(), receipts)
    ......
}

Process()方法對block中的每一個交易tx調用ApplyTransaction()來執行交易,入參state存儲了各個帳戶的信息,如帳戶餘額、合約代碼(僅對合約帳戶而言),咱們姑且將其理解爲一個內存中的數據庫。其中每一個帳戶以state object表示區塊鏈

ApplyTransaction()方法完成如下功能ui

  • 調用AsMessage()tx爲參數生成core.Message。也就是將tx中的一些字段存入Message,再從tx的數字簽名中反解出txsender,重點關注其中的data字段:若是是普通的轉帳交易,該字段爲空,若是是建立一個新的合約,該字段爲新的合約的代碼,若是是執行一個已經在區塊鏈上存在的合約,該參數爲合約代碼的輸入參數
  • 調用NewEVMContext()建立一個EVM運行上下文vm.Context。注意其中的Coinbase字段須要填入的礦工的地址,Transfer是具體的轉帳方法,其實就是操做senderrecipient的帳戶餘額
  • 調用NewEVM()建立一個虛擬機運行環境EVM,它主要做用是聚集以前的信息以及建立一個代碼解釋器(Interpreter),這個解釋器以後會用來解釋並執行合約代碼
  • 接下來就是調用ApplyMessage()將以上的信息施加在以太坊當前狀態上,使得狀態機發生狀態變換

ApplyMessage()的頂層比較簡單,它建立一個StateTransition結構並調用其TransitionDb()方法,StateTransition表示一次以太訪的狀態轉移 其定義以下:spa

type StateTransition struct {
    gp  *GasPool
    msg Message
    gas  uint64
    gasPrice  *big,Int
    initialGas   uint64
    value   *big.Int
    data    []byte
    state   vm.StateDB
    evm    *vm.EVM
}

其中的字段都是以前ApplyTransaction()方法中建立的結構獲得。一次狀態轉移包括如下流程code

  • nonce檢查:交易的nonce值用於標識這是sender發起的交易的序號,該值老是等於上一筆交易的nonce值遞增1,當咱們檢查發現當前Apply的這筆交易與該sender期待的nonce不一致時,就會拒絕這次狀態轉換
  • gas預購:sender預購這次轉換須要的gas,簡單說來就是扣除sender帳戶的ETH(變化反映在stateDB),扣除的數量卻決於交易設定的gasPricegasLimit的乘積,單位是gwei
  • 合約帳戶建立: 若是交易的recipient爲空的話,標識這筆交易須要建立一個合約,那麼就建立一個合約帳戶(反映在state object)
  • 價值轉移:每筆交易都伴隨着價值轉移,即ETHsender帳戶發送到receipt帳戶,若是建立了合約,還要執行合約代碼

TransitionDB()完成這樣的狀態轉換,其實現流程以下:
TransitionDb.png

最終由交易的receipt是否爲空決定是使用evm.Create()仍是evm.Call(),不管是哪一種,最終都是建立一個Contract結構,而後調用run()方法運行之。注意,即便是外部帳戶之間普通的轉帳也會調用Call()run(),只是因爲receipt上沒有代碼,運行會很快結束而已。run()最終調用InterpreterRun()方法。

前面提到過,在調用NewEVM()時建立了一個解釋器(Interpreter)

func NewInterpreter(evm *EVM,cfg Config)  *Interpreter {
     switch {
         case evm.ChainConfig().IsConstantinople(evm.BlockNumber):
             cfg.JumpTable = constantinopleInstructionSet
         case evm.ChainConfig().IsByzantium(evm.BlockNumber):
             cfg.JumpTable = byzantiumInstructionSet
         case evm.ChainConfig().IsHomestead(evm.BlockNumber):
             cfg.JumpTable = homesteadInstructionSet
         default:
             cfg.JumpTable = fromtierInstructionSet  
     }
     return &Interpreter{
         evm:      evm,
         cfg:      cfg,
         ......
     }
}

根據當前Block的高度,計算出它處於以太坊演進的階段,獲得該階段支持的指令集(InstructionSet),新的階段在兼容老的階段的全部指令前提下,再增長了獨特的新指令。最終存儲在Interpretercfg字段

合約代碼本質上上是由Solidity語言編譯後造成的EVM字節碼,字節碼中的操做也正是指令集中定義的指令

再回到Run()方法,其大概流程以下

Run.png

EVM逐字節的解析合約代碼並調用excute()方法運行,直到運行完成或者gas提早耗盡。

關於具體的EVM指令解釋方式和虛擬機內部內存等內部實現,參考本系列文章

小結

  1. 在以太坊中,交易的執行是由EVM完成的,網絡中的全部全節點都會去執行每一筆交易(這樣全部人的狀態才能夠保持一致)
  2. 交易分爲普通轉帳和執行(建立)智能合約,二者都由sender付費,後者相比前者,EVM要額外執行合約的字節碼
相關文章
相關標籤/搜索