以太坊是一個運行智能合約的平臺,被稱做可編程的區塊鏈,容許用戶將編寫的智能合約部署在區塊鏈上運行。而運行合約的主體即是以太坊虛擬機(EVM
)golang
區塊鏈由區塊
(Block
)組成,而區塊
中打包必定數量的交易
(Transaction
),交易
多是一個單純的轉帳操做,也多是調用一個智能合約,不管是哪種,EVM
在運行(excute
)交易時都會建立合約
(Contract
)數據庫
以太坊中的帳戶有兩類編程
外部帳戶
由帳戶持有人的私鑰控制的真實存在的帳戶合約帳戶
由合約代碼控制,保存着合約代碼一筆交易
老是有發送方
(sender
),接收方
(recipient
)和數額
(value
) 三要素。發送方將必定數額的ETH
轉移到接收方的帳戶,在單純的轉帳交易中,接收方是外部帳戶。而在調用智能合約的交易時,接收方是合約帳戶。segmentfault
如同現實中的稅費同樣,交易
也須要將支付少許的費用,稱爲gas
,費用支付給礦工,這能夠激勵礦工打包交易到區塊,也使得區塊鏈避免惡意運算攻擊。gas
由交易的發送者使用ETH
購買,在執行交易的每一步都會消耗gas
,若是gas
用完了,交易狀態會被回退,但消耗的gas
不會返還。網絡
以太坊是一個基於交易的狀態機,一筆交易可使以太坊從一個狀態(state
)切換到另外一個狀態,即交易的執行伴隨着狀態的改變。
交易執行的入口在 core/state_processor.go
的Process()
方法,下面是該方法的輪廓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
的數字簽名中反解出tx
的sender
,重點關注其中的data
字段:若是是普通的轉帳交易,該字段爲空,若是是建立一個新的合約,該字段爲新的合約的代碼
,若是是執行一個已經在區塊鏈上存在的合約,該參數爲合約代碼的輸入參數
NewEVMContext()
建立一個EVM
運行上下文vm.Context
。注意其中的Coinbase
字段須要填入的礦工的地址,Transfer
是具體的轉帳方法,其實就是操做sender
和recipient
的帳戶餘額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
),扣除的數量卻決於交易設定的gasPrice
和gasLimit
的乘積,單位是gwei
。recipient
爲空的話,標識這筆交易須要建立一個合約,那麼就建立一個合約帳戶(反映在state object
)ETH
從sender
帳戶發送到receipt
帳戶,若是建立了合約,還要執行合約代碼TransitionDB()
完成這樣的狀態轉換,其實現流程以下:
最終由交易的receipt
是否爲空決定是使用evm.Create()
仍是evm.Call()
,不管是哪一種,最終都是建立一個Contract
結構,而後調用run()
方法運行之。注意,即便是外部帳戶之間普通的轉帳也會調用Call()
和run()
,只是因爲receipt
上沒有代碼,運行會很快結束而已。run()
最終調用Interpreter
的Run()
方法。
前面提到過,在調用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
),新的階段在兼容老的階段的全部指令前提下,再增長了獨特的新指令。最終存儲在Interpreter
的cfg
字段
合約代碼本質上上是由Solidity
語言編譯後造成的EVM字節碼
,字節碼中的操做也正是指令集中定義的指令
再回到Run()
方法,其大概流程以下
EVM
逐字節的解析合約代碼並調用excute()
方法運行,直到運行完成或者gas
提早耗盡。
關於具體的EVM
指令解釋方式和虛擬機內部棧
和內存
等內部實現,參考本系列文章
EVM
完成的,網絡中的全部全節點都會去執行每一筆交易(這樣全部人的狀態才能夠保持一致)sender
付費,後者相比前者,EVM
要額外執行合約的字節碼