本篇文章分析的源碼地址為:https://github.com/ethereum/go-ethereum
分支:master
commit id: 257bfff316e4efb8952fbeb67c91f86af579cb0a
引言
以太坊的智能合約是一個(gè)非常棒的想法卜高,所以學(xué)習(xí)以太坊一定要學(xué)一下智能合約谬以。而在以太坊源碼里一喘,evm 模塊實(shí)現(xiàn)了執(zhí)行智能合約的虛擬機(jī),無論是合約的創(chuàng)建還是調(diào)用,都是由 evm 模塊完成。所以學(xué)習(xí)以太坊也一定要學(xué)習(xí) evm 模塊抗斤。這篇文章里,我們通過源代碼丈咐,看一下這個(gè)模塊是如何實(shí)現(xiàn)的瑞眼。
需要說明的是,如果想要完整了解以太坊智能合約的實(shí)現(xiàn)棵逊,達(dá)到自己也能寫一個(gè)類似功能的程度伤疙,只學(xué)習(xí) evm 模塊是不夠的,還需要學(xué)習(xí)智能合約的編譯器 solidity 項(xiàng)目辆影。evm 只是實(shí)現(xiàn)了一個(gè)虛擬機(jī)徒像,它在以太坊區(qū)塊鏈環(huán)境中,逐條地解釋執(zhí)行智能合約的指令蛙讥,而這是相對(duì)比較簡(jiǎn)單的一步锯蛀。更重要且更有難度的是,如何設(shè)計(jì)一個(gè)智能合約語言次慢,以及如何編譯它旁涤。但我在編譯原理方面懂得也不多,時(shí)間有限就先不深究 solidity 語言的設(shè)計(jì)和編譯了迫像。
(solidity 語言的設(shè)計(jì)很簡(jiǎn)單劈愚,但仍然是圖靈完備的。因此如果要學(xué)習(xí)編譯原理闻妓,拿它來作為一個(gè)實(shí)踐項(xiàng)目學(xué)習(xí)起來應(yīng)該會(huì)非常棒菌羽。)
evm 實(shí)現(xiàn)結(jié)構(gòu)
我們先看一下 evm 模塊的整體實(shí)現(xiàn)結(jié)構(gòu)的示意圖:
其實(shí) evm 的整體設(shè)計(jì)還是挺簡(jiǎn)單的。我們先簡(jiǎn)單說明一下示意圖由缆,后面各小節(jié)會(huì)針對(duì)各功能塊進(jìn)行詳細(xì)的說明注祖。
首先 evm 模塊的核心對(duì)象是 EVM
,它代表了一個(gè)以太坊虛擬機(jī)犁功,用于創(chuàng)建或調(diào)用某個(gè)合約氓轰。每次處理一個(gè)交易對(duì)象時(shí),都會(huì)創(chuàng)建一個(gè) EVM
對(duì)象浸卦。
EVM
對(duì)象內(nèi)部主要依賴三個(gè)對(duì)象:解釋器 Interpreter
、虛擬機(jī)相關(guān)配置對(duì)象 vm.Config
案糙、以太坊狀態(tài)數(shù)據(jù)庫 StateDB
限嫌。
StateDB
主要的功能就是用來提供數(shù)據(jù)的永久存儲(chǔ)和查詢靴庆。關(guān)于這個(gè)對(duì)象的詳細(xì)信息,可以查看我寫的這篇文章怒医。
Interpreter
是一個(gè)接口炉抒,在代碼中由 EVMInterpreter
實(shí)現(xiàn)具體功能。這是一個(gè)解釋器對(duì)象稚叹,循環(huán)解釋執(zhí)行給定的合約指令焰薄,直接遇到退出指令。每執(zhí)行一次指令前扒袖,都會(huì)做一些檢查操作塞茅,確保 gas、椉韭剩空間等充足野瘦。但各指令真正的解釋執(zhí)行代碼卻不在這個(gè)對(duì)象中,而是記錄在 vm.Config
的 JumpTable
字段中飒泻。
vm.Config
為虛擬機(jī)和解釋器提供了配置信息鞭光,其中最重要的就是 JumpTable
。JumpTable
是 vm.Config
的一個(gè)字段泞遗,它是一個(gè)由 256 個(gè) operation
對(duì)象組成的數(shù)組惰许。解釋器每拿到一個(gè)準(zhǔn)備執(zhí)行的新指令時(shí),就會(huì)從 JumpTable
中獲取指令相關(guān)的信息史辙,即 operation
對(duì)象汹买。這個(gè)對(duì)象中包含了解釋執(zhí)行此條指令的函數(shù)、計(jì)算指令的 gas 消耗的函數(shù)等髓霞。
在代碼中卦睹,根據(jù)以太坊的版本不同,JumpTable
可能指向四個(gè)不同的對(duì)象:constantinopleInstructionSet
方库、byzantiumInstructionSet
结序、homesteadInstructionSet
、frontierInstructionSet
纵潦。這四套指令集多數(shù)指令是相同的徐鹤,只是隨著版本的更新,新版本比舊版本支持更多的指令集邀层。
大體了解了 evm 模塊的設(shè)計(jì)結(jié)構(gòu)以后返敬,我們?cè)诤竺娴男」?jié)里,從源碼的角度詳細(xì)看一下各個(gè)功能塊的實(shí)現(xiàn)寥院。
以太坊虛擬機(jī):EVM
EVM
對(duì)象是 evm 模塊對(duì)外導(dǎo)出的最重要的對(duì)象劲赠,它代表了一個(gè)以太坊虛擬機(jī)。利用這個(gè)對(duì)象,以太坊就可以創(chuàng)建和調(diào)用合約了凛澎。下面我們從虛擬機(jī)的創(chuàng)建霹肝、合約的創(chuàng)建、合約的調(diào)用三個(gè)方面來詳細(xì)介紹一下塑煎。
創(chuàng)建 EVM
前面我們提到過沫换,每處理一筆交易,就要?jiǎng)?chuàng)建一個(gè) EVM
來執(zhí)行交易中的數(shù)據(jù)最铁。這是在 ApplyTransaction
這個(gè)函數(shù)中體現(xiàn)的:
func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, uint64, error) {
msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
if err != nil {
return nil, 0, err
}
// Create a new context to be used in the EVM environment
context := NewEVMContext(msg, header, bc, author)
// Create a new environment which holds all relevant information
// about the transaction and calling mechanisms.
vmenv := vm.NewEVM(context, statedb, config, cfg)
// Apply the transaction to the current state (included in the env)
_, gas, failed, err := ApplyMessage(vmenv, msg, gp)
if err != nil {
return nil, 0, err
}
......
}
ApplyTransaction
函數(shù)的功能就是將交易的信息記錄到以太坊狀態(tài)數(shù)據(jù)庫(state.StateDB)中讯赏,這其中包括轉(zhuǎn)賬信息和執(zhí)行合約信息(如果交易中有合約的話)。這個(gè)函數(shù)一開始調(diào)用 Transaction.AsMessage
將一個(gè) Transaction
對(duì)象轉(zhuǎn)換成 Message
對(duì)象冷尉,我覺得這個(gè)轉(zhuǎn)換主要是為了語義上的一致漱挎,因?yàn)樵诤霞s中,msg
全局變量記錄了附帶當(dāng)前合約的交易的信息网严,可能是為了一致识樱,這里也將「交易」轉(zhuǎn)變成「消息」傳給 EVM
對(duì)象。
然后 ApplyTransaction
創(chuàng)建了 Context
對(duì)象震束,這個(gè)對(duì)象中主要包含了一些訪問當(dāng)前區(qū)塊鏈數(shù)據(jù)的方法怜庸。
接下來 ApplyTransaction
利用這些參數(shù),調(diào)用 vm.NewEVM
函數(shù)創(chuàng)建了以太坊虛擬機(jī)垢村,通過 ApplyMessage
執(zhí)行相關(guān)功能割疾。ApplyMessage
只有一行代碼,它最終調(diào)用了 StateTransition.TransitionDb
方法。我們來看一下這個(gè)方法:
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
// 執(zhí)行一些檢查
if err = st.preCheck(); err != nil {
return
}
......
// 判斷是否是創(chuàng)建新合約
contractCreation := msg.To() == nil
......
if contractCreation {
// 調(diào)用 EVM.Create 創(chuàng)建合約
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
} else {
// 不是創(chuàng)建合約,則調(diào)用 EVM.Call 調(diào)用合約
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
......
// 歸還剩余的 gas眶熬,并將已消耗的 gas 計(jì)入礦工賬戶中
st.refundGas()
st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
return ret, st.gasUsed(), vmerr != nil, err
}
StateTransition
對(duì)象記錄了在處理一筆交易過程中的狀態(tài)數(shù)據(jù),比如 gas 的花費(fèi)等麻昼。在 StateTransition.TransitionDb
這個(gè)方法中,首先調(diào)用 StateTransaction.preCheck
檢查交易的 Nonce 值是否正確馋辈,并從交易的發(fā)送者賬戶中「購買」交易中規(guī)定數(shù)量的 gas抚芦,如果「購買」失敗,那么此次的交易就失敗了迈螟。
接下來 StateTransaction.TransitionDb
判斷當(dāng)前的交易是否創(chuàng)建合約叉抡,這是通過交易的接收者是否為空來判斷的。如果需要?jiǎng)?chuàng)建合約答毫,則調(diào)用 EVM.Create
進(jìn)行創(chuàng)建褥民;如果不是,則調(diào)用 EVM.Call
執(zhí)行合約代碼洗搂。(如果是一筆簡(jiǎn)單的轉(zhuǎn)賬交易消返,接收者肯定不為空载弄,那么就會(huì)調(diào)用 EVM.Call
實(shí)現(xiàn)轉(zhuǎn)賬功能)。
EVM
對(duì)象執(zhí)行完相關(guān)功能后侦副,StateTransaction.TransitionDb
調(diào)用 StateTransaction.refundGas
將未用完的 gas 還給交易的發(fā)送者侦锯。然后將消耗的 gas 計(jì)入礦工賬戶中驼鞭。
上面是創(chuàng)建 EVM
并調(diào)用其相關(guān)方法的環(huán)境說明秦驯。說了這么多「前戲」,我們現(xiàn)在正式介紹 EVM
的創(chuàng)建:
func NewEVM(ctx Context, statedb StateDB, chainConfig *params.ChainConfig, vmConfig Config) *EVM {
evm := &EVM{
Context: ctx,
StateDB: statedb,
vmConfig: vmConfig,
chainConfig: chainConfig,
chainRules: chainConfig.Rules(ctx.BlockNumber),
interpreters: make([]Interpreter, 0, 1),
}
if chainConfig.IsEWASM(ctx.BlockNumber) {
panic("No supported ewasm interpreter yet.")
}
// vmConfig.EVMInterpreter will be used by EVM-C, it won't be checked here
// as we always want to have the built-in EVM as the failover option.
evm.interpreters = append(evm.interpreters, NewEVMInterpreter(evm, vmConfig))
evm.interpreter = evm.interpreters[0]
return evm
}
NewEVM
函數(shù)用來創(chuàng)建一個(gè)新的虛擬機(jī)對(duì)象挣棕,它有四個(gè)參數(shù)译隘,含義分別如下:
- ctx: 提供訪問當(dāng)前區(qū)塊鏈數(shù)據(jù)和挖礦環(huán)境的函數(shù)和數(shù)據(jù)
- statedb: 以太坊狀態(tài)數(shù)據(jù)庫對(duì)象
- chainConfig: 當(dāng)前節(jié)點(diǎn)的區(qū)塊鏈配置信息
- vmConfig: 虛擬機(jī)配置信息
注意 vmConfig
參數(shù)中的數(shù)據(jù)可能是不全的,比如缺少 Config.JumpTable
數(shù)據(jù)(Config.JumpTable
會(huì)在 NewEVMInterpreter
中填充)洛心。
NewEVM
函數(shù)的實(shí)現(xiàn)也相關(guān)簡(jiǎn)單固耘,除了把參數(shù)記錄下來以外,主要就是調(diào)用 NewEVMInterpreter
創(chuàng)建一個(gè)解釋器對(duì)象词身。我們順便看一下這個(gè)函數(shù)的實(shí)現(xiàn):
func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter {
// We use the STOP instruction whether to see
// the jump table was initialised. If it was not
// we'll set the default jump table.
if !cfg.JumpTable[STOP].valid {
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 = frontierInstructionSet
}
}
return &EVMInterpreter{
evm: evm,
cfg: cfg,
gasTable: evm.ChainConfig().GasTable(evm.BlockNumber),
}
}
這個(gè)方法在創(chuàng)建解釋器之前厅目,首先填充了 Config.JumpTable
字段》ㄑ希可以看到這里跟據(jù)以太坊版本不同损敷,使用了不同的對(duì)象填充這個(gè)字段。然后函數(shù)生成一個(gè) EVMInterpreter
對(duì)象并返回(這里還填充了 gasTable
這個(gè)字段深啤,但此處我們先暫時(shí)忽略拗馒,在「gas 的消耗」小節(jié)中再詳細(xì)說明)。
總之溯街,以太坊在每處理一筆交易時(shí)诱桂,都會(huì)調(diào)用 NewEVM
函數(shù)創(chuàng)建 EVM
對(duì)象,哪怕不涉及合約呈昔、只是一筆簡(jiǎn)單的轉(zhuǎn)賬挥等。 NewEVM
的實(shí)現(xiàn)也很簡(jiǎn)單,只是記錄相關(guān)的參數(shù)堤尾,同時(shí)創(chuàng)建一個(gè)解釋器對(duì)象肝劲。Config.JumpTable
字段在開始時(shí)是無效的,在創(chuàng)建解釋器對(duì)象時(shí)對(duì)其進(jìn)行了填充哀峻。
創(chuàng)建合約
上面我們已經(jīng)看到涡相,如果交易的接收者為空,則代表此條交易的目的是要?jiǎng)?chuàng)建一條合約剩蟀,因此調(diào)用 EVM.Create
執(zhí)行相關(guān)的功能〈呋龋現(xiàn)在我們就來看看這個(gè)方法是如何實(shí)現(xiàn)合約的創(chuàng)建的。
先來看看 EVM.Create
的代碼:
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
contractAddr = crypto.CreateAddress(
caller.Address(), evm.StateDB.GetNonce(caller.Address()))
return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr)
}
EVM.Create
的代碼很簡(jiǎn)單育特,就是通過當(dāng)前合約的創(chuàng)建者地址和其賬戶中的 Nonce 值丙号,計(jì)算出來一個(gè)地址值先朦,作為合約的地址。然后將這個(gè)地址和其它參數(shù)傳給 EVM.create
方法犬缨。這里唯一需要注意的是由于用到了賬戶的 Nonce 值喳魏,所以同一份合約代碼,每次創(chuàng)建合約時(shí)得到的合約地址都是不一樣的(因?yàn)楹霞s是通過發(fā)送交易創(chuàng)建怀薛,而每發(fā)送一次交易 Nonce 值都會(huì)改變)刺彩。
創(chuàng)建合約的重點(diǎn)在 EVM.create
方法中,下面是這個(gè)方法的第一部分代碼:
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address) ([]byte, common.Address, uint64, error) {
// 檢查合約創(chuàng)建的遞歸調(diào)用次數(shù)
if evm.depth > int(params.CallCreateDepth) {
return nil, common.Address{}, gas, ErrDepth
}
// 檢查合約創(chuàng)建者是否有足夠的以太幣
if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, common.Address{}, gas, ErrInsufficientBalance
}
// 增加合約創(chuàng)建者的 Nonce 值
nonce := evm.StateDB.GetNonce(caller.Address())
evm.StateDB.SetNonce(caller.Address(), nonce+1)
......
}
第一段代碼比較簡(jiǎn)單枝恋,主要是一些檢查创倔,并修改合約創(chuàng)建者的賬戶的 Nonce 值。
第一個(gè) if 判斷中的 EVM.depth
記錄者合約的遞歸調(diào)用次數(shù)焚碌。在 solidity 語言中畦攘,允許在合約中通過 new
關(guān)鍵字創(chuàng)建新的合約對(duì)象,但這種「在合約中創(chuàng)建合約」的遞歸調(diào)用是有限制的十电,這也是這個(gè) if 判斷的意義知押。EVM.depth
在解釋器對(duì)象開始運(yùn)行時(shí)(即 EVMInterpreter.Run
中)加 1,解釋器運(yùn)行結(jié)束后減 1(不知道為什么不把這個(gè)加減操作放在 EVM.create
中進(jìn)行)鹃骂。
我們繼續(xù)看 EVM.create
的下一段代碼:
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address) ([]byte, common.Address, uint64, error) {
......
contractHash := evm.StateDB.GetCodeHash(address)
if evm.StateDB.GetNonce(address) != 0 ||
(contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
return nil, common.Address{}, 0, ErrContractAddressCollision
}
// Create a new account on the state
snapshot := evm.StateDB.Snapshot()
evm.StateDB.CreateAccount(address)
if evm.ChainConfig().IsEIP158(evm.BlockNumber) {
evm.StateDB.SetNonce(address, 1)
}
evm.Transfer(evm.StateDB, caller.Address(), address, value)
......
}
這段代碼在狀態(tài)數(shù)據(jù)庫中創(chuàng)建了合約賬戶台盯,并將交易中約定好的以太幣數(shù)值(value
)轉(zhuǎn)到這個(gè)新創(chuàng)建的賬戶里。這里在創(chuàng)建新賬戶前偎漫,會(huì)先檢查這個(gè)賬戶是否已經(jīng)存在爷恳,如果已經(jīng)存在就直接返回錯(cuò)誤了。
我們繼續(xù)下面的代碼:
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address) ([]byte, common.Address, uint64, error) {
......
contract := NewContract(caller, AccountRef(address), value, gas)
contract.SetCodeOptionalHash(&address, codeAndHash)
if evm.vmConfig.NoRecursion && evm.depth > 0 {
return nil, address, gas, nil
}
......
ret, err := run(evm, contract, nil, false)
......
if err == nil && !maxCodeSizeExceeded {
createDataGas := uint64(len(ret)) * params.CreateDataGas
if contract.UseGas(createDataGas) {
evm.StateDB.SetCode(address, ret)
} else {
err = ErrCodeStoreOutOfGas
}
}
......
return ret, address, contract.Gas, err
這段代碼首先創(chuàng)建了一個(gè) Contract
對(duì)象象踊。一個(gè) Contract
對(duì)象包含和維護(hù)了合約在執(zhí)行過程中的必要信息温亲,比如合約創(chuàng)建者、合約自身地址杯矩、合約剩余 gas栈虚、合約代碼和代碼的 jumpdests 記錄(關(guān)于 jumpdests 記錄請(qǐng)參看后面的「跳轉(zhuǎn)指令」小節(jié))。
創(chuàng)建 Contract
對(duì)象以后史隆,緊跟著的是一個(gè) if 判斷魂务。如果以太坊虛擬機(jī)被配置成不可遞歸創(chuàng)建合約,而當(dāng)前創(chuàng)建合約的過程正是在遞歸過程中泌射,則直接返回成功粘姜,但并沒有返回合約代碼(第一個(gè)返回參數(shù))。(我其實(shí)沒太看懂這個(gè)判斷和處理熔酷,如果不能遞歸孤紧,為什么直接返回成功呢?)
接著代碼調(diào)用 run
函數(shù)運(yùn)行合約的代碼拒秘。我們一會(huì)再詳細(xì)看這個(gè) run
函數(shù)号显,先來看看 run
返回后的處理臭猜。如果運(yùn)行成功,且合約代碼沒有超過長(zhǎng)度限制(maxCodeSizeExceeded
為 false押蚤,一會(huì)再說這個(gè)變量)蔑歌,則調(diào)用 StateDB.SetCode
將合約代碼存儲(chǔ)到以太坊狀態(tài)數(shù)據(jù)庫的合約賬戶中,當(dāng)然存儲(chǔ)需要消耗一定數(shù)據(jù)的 gas揽碘。
你可能會(huì)奇怪為什么存儲(chǔ)的合約代碼是合約運(yùn)行后的返回碼次屠,而不是原來交易中的數(shù)據(jù)(即 Transaction.data.Payload
,或者說 EVM.Create
方法的 code
參數(shù))钾菊。這是因?yàn)楹霞s源代碼在編譯成二進(jìn)制數(shù)據(jù)時(shí)帅矗,除了合約原有的代碼外,編譯器還另外插入了一些代碼煞烫,以便執(zhí)行相關(guān)的功能。對(duì)于創(chuàng)建來說累颂,編譯器插入了執(zhí)行合約「構(gòu)造函數(shù)」(即合約對(duì)象的 constructor
方法)的代碼滞详。因此在將編譯器編譯后的二進(jìn)制提交以太坊節(jié)點(diǎn)創(chuàng)建合約時(shí),EVM
執(zhí)行這段二進(jìn)制代碼紊馏,實(shí)際上主要執(zhí)行了合約的 constructor
方法料饥,然后將合約的其它代碼返回,所以才會(huì)有這里的 ret
變量作為合約的真正代碼存儲(chǔ)到狀態(tài)數(shù)據(jù)庫中朱监。
對(duì)于 maxCodeSizeExceeded
變量岸啡,它記錄的是合約的代碼(即 ret
變量)是否超過某一長(zhǎng)度。前面的代碼段中 run
函數(shù)后面省略的代碼主要是處理與這個(gè)變量相關(guān)的邏輯赫编。我覺得這個(gè)并不重要巡蘸,所以就沒有放上來。
下面我們就來看看 run
函數(shù)擂送,看它是怎么執(zhí)行一個(gè)合約創(chuàng)建的過程的:
func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, error) {
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)
}
}
for _, interpreter := range evm.interpreters {
if interpreter.CanRun(contract.Code) {
if evm.interpreter != interpreter {
// Ensure that the interpreter pointer is set back
// to its current value upon return.
defer func(i Interpreter) {
evm.interpreter = i
}(evm.interpreter)
evm.interpreter = interpreter
}
return interpreter.Run(contract, input, readOnly)
}
}
return nil, ErrNoCompatibleInterpreter
}
這個(gè)函數(shù)不長(zhǎng)悦荒,所以我一下子全放上來了。函數(shù)前半部分判斷合約的地址是否是一些特殊地址嘹吨,如果是則執(zhí)行其對(duì)應(yīng)的對(duì)象的 Run
方法搬味。關(guān)于這些特殊地址的詳細(xì)信息,請(qǐng)參看后面的「預(yù)定義合約」小節(jié)蟀拷,這里就不展開細(xì)說了碰纬。
run
函數(shù)的后半部分代碼是一個(gè) for 循環(huán),從當(dāng)前 EVM
對(duì)象中選擇一個(gè)可以運(yùn)行的解釋器问芬,運(yùn)行當(dāng)前的合約并返回悦析。當(dāng)前源代碼中只有一個(gè)版本的解釋器,就是 EVMInterpreter
愈诚,我們現(xiàn)在先大體上看一下這個(gè)對(duì)象的的 Run
方法:
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
......
in.evm.depth++
defer func() { in.evm.depth-- }()
......
var (
pc = uint64(0) // program counter
)
for atomic.LoadInt32(&in.evm.abort) == 0 {
......
op = contract.GetOp(pc)
operation := in.cfg.JumpTable[op]
......
res, err := operation.execute(&pc, in, contract, mem, stack)
......
switch {
case err != nil:
return nil, err
case operation.reverts:
return res, errExecutionReverted
case operation.halts:
return res, nil
case !operation.jumps:
pc++
}
}
return nil, nil
}
我將此方法中當(dāng)前我們關(guān)心的代碼在這里展示出來她按∨S纾可見這個(gè)方法就是從給定的代碼的第 0 個(gè)字節(jié)開始執(zhí)行,直到退出酌泰。
我們前面說過媒佣,編譯合約時(shí)編譯器會(huì)插入一些代碼。在剛才展示的創(chuàng)建合約的過程中陵刹,執(zhí)行的就是這些插入的代碼默伍。所以創(chuàng)建合約的過程并沒有見到有「創(chuàng)建」的 go 代碼,而是和調(diào)用合約一樣衰琐,只是執(zhí)行編譯出來的指令也糊。
為了更清楚的了解這一過程,我們來看一個(gè)實(shí)例羡宙。我使用 solidity 編譯器將下面這個(gè)簡(jiǎn)單的合約編譯成指令的匯編形式狸剃。合約源代碼如下:
contract test {
uint256 x;
constructor() public {
x = 0xfafa;
}
function multiply(uint256 a) public view returns(uint256){
return x * a;
}
function multiplyState(uint256 a) public returns(uint256){
x = a * 7;
return x;
}
}
編譯成匯編代碼后,最前面一部分的指令如下:
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1
jumpi /* 跳轉(zhuǎn)到 tag_1 */
0x00
dup1
revert
tag_1:
/* "../test.sol":63:111 constructor() public {... */
pop
0xfafa /* 0xfafa 關(guān)鍵數(shù)字 */
0x00
dup2
swap1
sstore
pop
dataSize(sub_0)
dup1
dataOffset(sub_0)
0x00
codecopy
0x00
return
stop
sub_0: assembly {
......
我們不需要看懂每條指令狗热,也能明白這其中的意思钞馁。指令一開始作了一些操作,然后會(huì)跳轉(zhuǎn)到 tag_1
處繼續(xù)執(zhí)行匿刮。在 tag_1
處的代碼僧凰,我們可以看到 0xfafa 這個(gè)關(guān)鍵數(shù)字,這是我們?cè)?constructor
方法中為 x
設(shè)置的初始值熟丸。所以我們可以知道 tag_1
處的代碼執(zhí)行了合約的初始化工作训措。最后通過 codecopy
指令,將 sub_0
處至最后的數(shù)據(jù)拷貝出來并返回光羞。所以合約真正的代碼是從 sub_0
開始到結(jié)尾處的數(shù)據(jù)绩鸣。這也證實(shí)了我們前面說的,存儲(chǔ)到狀態(tài)數(shù)據(jù)庫中的合約代碼狞山,是從 run
函數(shù)返回后的數(shù)據(jù)全闷。
調(diào)用合約
EVM
對(duì)象有三個(gè)方法實(shí)現(xiàn)合約的調(diào)用,它們分別是:
EVM.Call
EVM.CallCode
EVM.DelegateCall
EVM.StaticCall
我們先說一下這四個(gè)調(diào)用合約的方法的差異萍启,然后再通過 EVM.Call
看一下調(diào)用合約的代碼是如何實(shí)現(xiàn)的总珠。
EVM.Call
實(shí)現(xiàn)的基本的合約調(diào)用的功能,沒什么特殊的勘纯。后面的三個(gè)調(diào)用方式都是與 EVM.Call
比較產(chǎn)生的差異局服。所以這里只介紹后面三個(gè)調(diào)用方式的特殊性。
EVM.CallCode 和 EVM.DelegateCall
首先需要了解的是驳遵,EVM.CallCode
和 EVM.DelegateCall
的存在是為了實(shí)現(xiàn)合約的「庫」的特性淫奔。我們知道編程語言都有自己的庫,比如 go 的標(biāo)準(zhǔn)庫堤结,C++ 的 STL 或 boost唆迁。作為合約的編寫語言鸭丛,solidity 也想有自己的庫。但與普通語言的實(shí)現(xiàn)不同唐责,solidity 寫出來的代碼要想作為庫被調(diào)用鳞溉,必須和普通合約一樣,布署到區(qū)塊鏈上取得一個(gè)固定的地址鼠哥,其它合約才能調(diào)用這個(gè)「庫合約」提供的方法熟菲。但合約又涉及到一些特有的屬性,比如合約的調(diào)用者朴恳、自身地址抄罕、自身所擁有的以太幣的數(shù)量等。如果我們直接去調(diào)用「庫合約」的代碼于颖,用到這些屬性時(shí)必然是「庫合約」自己的屬性呆贿,但這可能不是我們想要的。
例如恍飘,設(shè)想一個(gè)「庫合約」的方法實(shí)現(xiàn)了一個(gè)這樣的操作:從自已賬戶中給指定賬戶轉(zhuǎn)一筆錢榨崩。如果這里的「自己賬戶」指的是「庫合約」的賬戶,那肯定是不現(xiàn)實(shí)的(因?yàn)闆]有人會(huì)出錢布署一個(gè)有很多以太幣的合約章母,并且讓別人把這些幣轉(zhuǎn)走)。
此時(shí) EVM.DelegateCall
就派上用場(chǎng)了翩剪。這個(gè)調(diào)用方式將「庫合約」的調(diào)用者乳怎,設(shè)置成自己的調(diào)用者;將「庫合約」的地址前弯,設(shè)置成自己的地址(但代碼還是「庫合約」的代碼)蚪缀。如此一來,「庫合約」的屬性恕出,就完全和自己的屬性一樣了询枚,「庫合約」的代碼就像是自己的寫的代碼一樣。
例如 A 賬戶調(diào)用了 B 合約浙巫,而在 B 合約中通過 DelegateCall
調(diào)用了 C 合約金蜀,那么 C 合約的調(diào)用者將被修改成 A , C 合約的地址將被修改成 B 合約的地址的畴。所以在剛才用來轉(zhuǎn)賬的「庫合約」的例子中渊抄,「自己賬戶」指的不再是「庫合約」的賬戶了,而是調(diào)用「庫合約」的賬戶丧裁,轉(zhuǎn)賬也就可以按我們想要的方式進(jìn)行了护桦。
EVM.CallCode
與 EVM.DelegateCall
類似,不同的是 EVM.CallCode
不改變「庫合約」的調(diào)用者煎娇,只是改變「庫合約」的合約地址二庵。也就是說贪染,如果 A 通過 CallCode
的方式調(diào)用 B,那么 B 的調(diào)用者是 A催享,而 B 的賬戶地址也被改成了 A杭隙。
總結(jié)一下就是,EVM.CallCode
和 EVM.DelegateCall
修改了被調(diào)用合約的上下文環(huán)境睡陪,可以讓被調(diào)用的合約代碼就像自己寫的代碼一樣寺渗,從而達(dá)到「庫合約」的目的。具體來說兰迫,EVM.DelegateCall
會(huì)修改被調(diào)用合約的調(diào)用者和合約本身的地址信殊,而 EVM.CallCode
只會(huì)修改被調(diào)用合約的本身的地址。
了解了這兩個(gè)方法的目的和功能汁果,我們來看看代碼中它們是如何實(shí)現(xiàn)各自的功能的涡拘。對(duì)于 EvM.CallCode
來說,它通過下面展示的幾行代碼來修改被調(diào)用合約的地址:
func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
......
var (
to = AccountRef(caller.Address())
)
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
......
}
可以看到据德,在利用現(xiàn)有數(shù)據(jù)生成一個(gè)合約對(duì)象時(shí)鳄乏,將合約對(duì)象的地址 to
變量設(shè)置成調(diào)用者,也就是 caller
的地址棘利。(但在調(diào)用 Contract.SetCallCode
時(shí)橱野, code 的地址還是 addr
)
對(duì)于 EVM.DelegateCall
來說,它是通過下面幾行代碼修改被調(diào)用合約的調(diào)用者和自身地址的:
func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) {
......
var (
to = AccountRef(caller.Address())
)
contract := NewContract(caller, to, nil, gas).AsDelegate()
......
}
func (c *Contract) AsDelegate() *Contract {
parent := c.caller.(*Contract)
c.CallerAddress = parent.CallerAddress
c.value = parent.value
return c
}
這里首先也是通過將 to
變量設(shè)置成調(diào)用者善玫,也就是 caller
的地址水援,達(dá)到修改被調(diào)用合約的自身地址的目的。被調(diào)用合約的調(diào)用者是通過 Contract.AsDelegate
修改的茅郎。這個(gè)方法里蜗元,將合約的調(diào)用者地址 CallerAddress
設(shè)置成目前調(diào)用者的 CallerAddress
,也即當(dāng)前調(diào)用者的調(diào)用者的地址(有些繞系冗,仔細(xì)看一下就能明白)奕扣。
EVM.StaticCall
EVM.StaticCall
與 EVM.Call
類似,唯一不同的是 EVM.StaticCall
不允許執(zhí)行會(huì)修改永久存儲(chǔ)的數(shù)據(jù)的指令掌敬。如果執(zhí)行過程中遇到這樣的指令惯豆,就會(huì)失敗。比如對(duì)于以下合約:
contract test {
uint256 x;
function multiplyState(uint256 a) public returns(uint256){
x = a * 7;
return x;
}
}
就不能使用 EVM.StaticCall
調(diào)用 multiplyState
方法涝开,因?yàn)樗鼤?huì)修改合約的狀態(tài) x
變量循帐,而 x
變量是需要在以太坊狀態(tài)數(shù)據(jù)庫永久存儲(chǔ)的。
EVM.StaticCall
是如何實(shí)現(xiàn)拒絕執(zhí)行會(huì)修改永久存儲(chǔ)數(shù)據(jù)的指令的呢舀武?在解釋器的 EVMInterpreter.Run
方法以及 EVMInterpreter.enforceRestrictions
方法中拄养,有幾行這樣的代碼:
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
......
if readOnly && !in.readOnly {
in.readOnly = true
defer func() { in.readOnly = false }()
}
......
for atomic.LoadInt32(&in.evm.abort) == 0 {
......
operation := in.cfg.JumpTable[op]
......
if err := in.enforceRestrictions(op, operation, stack); err != nil {
return nil, err
}
......
}
}
func (in *EVMInterpreter) enforceRestrictions(op OpCode, operation operation, stack *Stack) error {
if in.evm.chainRules.IsByzantium {
if in.readOnly {
if operation.writes || (op == CALL && stack.Back(2).BitLen() > 0) {
return errWriteProtection
}
}
}
return nil
}
可以看到, EVMInterpreter.Run
有一個(gè) readOnly
參數(shù),如果為 true瘪匿,就會(huì)設(shè)置 EVMInterpreter.readOnly
為 true跛梗;而在循環(huán)解釋執(zhí)行指令時(shí),會(huì)在 EVMInterpreter.enforceRestrictions
方法中對(duì)其進(jìn)行檢查棋弥,如果是只讀核偿,但將要執(zhí)行的指令有寫操作,或者使用 CALL 指令向其它賬戶轉(zhuǎn)賬時(shí)顽染,都會(huì)直接返回錯(cuò)誤漾岳。
從上面的代碼可以看出,EVM.StaticCall
的實(shí)現(xiàn)需要存儲(chǔ)指令信息的 operation
對(duì)象配合粉寞,在其中記錄指令是否有寫操作尼荆。而 readOnly
參數(shù)正是來自于 EVM.StaticCall
:
func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) {
......
// run 函數(shù)的最后一個(gè)參數(shù) readOnly 為 true
ret, err = run(evm, contract, input, true)
......
}
func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, error) {
......
for _, interpreter := range evm.interpreters {
if interpreter.CanRun(contract.Code) {
......
// 調(diào)用 EVMInterrepter.Run,最后一個(gè)參數(shù) readOnly 正是 EVM.StaticCall 傳入的 true
return interpreter.Run(contract, input, readOnly)
}
}
......
}
EVM.Call 的實(shí)現(xiàn)
現(xiàn)在我們來看一下最基本的 EVM.Call
的實(shí)現(xiàn)唧垦,它是如何調(diào)用合約的捅儒。代碼較長(zhǎng),我們?nèi)匀环侄蝸砜矗?/p>
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 {
return nil, gas, nil
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
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) {
return nil, gas, ErrInsufficientBalance
}
......
}
方法開始的一部分代碼比較簡(jiǎn)單振亮,與 EVM.create
類似巧还,也是判斷遞歸層次和合約調(diào)用者是否有足夠的交易中約定的以太幣。需要注意的是 input
參數(shù)坊秸,這是調(diào)用合約的 public 方法的參數(shù)麸祷。
我們繼續(xù)看下一部分代碼:
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
......
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 EVM.
// 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))
......
}
這部分的代碼做了兩件事件。一是判斷合約地址是否存在褒搔;二是使用當(dāng)前的信息創(chuàng)建一個(gè)合約對(duì)象摇锋,其中 StateDB.GetCode
從狀態(tài)數(shù)據(jù)庫中獲取合約的代碼,填充到合約對(duì)象中站超。
一般情況下,被調(diào)用的合約地址應(yīng)該存在于以太坊狀態(tài)數(shù)據(jù)庫中乖酬,也就是說合約已經(jīng)創(chuàng)建成功了死相。否則就返回失敗。但有一種例外咬像,就是被調(diào)用的合約地址是預(yù)先定義的情況算撮,此時(shí)即使地址不在狀態(tài)數(shù)據(jù)庫中,也要立即創(chuàng)建一個(gè)县昂。(關(guān)于預(yù)定義的合約地址的詳細(xì)信息肮柜,請(qǐng)參看后面的「預(yù)定義合約」小節(jié))。
我們繼續(xù)看后面的代碼:
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
......
ret, err = run(evm, contract, input, false)
// 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
}
最后這部分代碼主要是對(duì) run
函數(shù)的調(diào)用倒彰,然后處理其返回值并返回审洞,方法結(jié)束。這里的 run
函數(shù)與創(chuàng)建合約時(shí)調(diào)用的是同一個(gè)函數(shù)待讳,因此不再過多介紹這個(gè)函數(shù)芒澜,想要詳細(xì)了解可以回看前面「創(chuàng)建合約」小節(jié)中對(duì)于 run
函數(shù)的介紹仰剿。
到這里你可能會(huì)有疑問:沒看到哪里調(diào)用合約代碼呀?只不過是使用 StateDB.GetCode
獲取合約代碼痴晦,然后使用 input
中的數(shù)據(jù)作為參數(shù)南吮,調(diào)用解釋器運(yùn)行合約代碼而已,哪里有調(diào)用合約 public 方法的影子誊酌?
這里確實(shí)與創(chuàng)建合約時(shí)類似部凑,沒有「調(diào)用」的影子。還記得前面我們介紹合約創(chuàng)建時(shí)碧浊,提到過合約編譯器在編譯時(shí)涂邀,會(huì)插入一些代碼嗎?在介紹合約創(chuàng)建時(shí)辉词,我們只介紹了編譯器插入的創(chuàng)建合約的代碼必孤,解釋器執(zhí)行這些代碼,就可以將合約的真正代碼返回瑞躺。類似的敷搪,編譯器還會(huì)插入一些調(diào)用合約的代碼,只要使用正確的參數(shù)執(zhí)行這些代碼幢哨,就可以「調(diào)用」到我們想調(diào)用的合約的 public 方法赡勘。
想要了解這整個(gè)機(jī)制,我們需要先介紹一下「函數(shù)選擇子」這個(gè)概念捞镰。在 solidity 的這篇官方文檔中闸与,介紹了什么是「函數(shù)選擇子」。簡(jiǎn)單來說岸售,就是合約的 public 方法的聲明字符串的 Keccak-256 哈希的前 4 個(gè)字節(jié)践樱。(關(guān)于「函數(shù)選擇子」的詳細(xì)說明和計(jì)算方法,請(qǐng)參看這篇文章)凸丸。
「函數(shù)選擇子」告訴了以太坊虛擬機(jī)我們想要調(diào)用合約的哪個(gè)方法拷邢,它和參數(shù)數(shù)據(jù)一起,被編碼到了交易的 data 數(shù)據(jù)中屎慢。但我們剛才通過對(duì)合約調(diào)用的分析瞭稼,并沒有發(fā)現(xiàn)有涉及到解析「函函數(shù)選擇子」的地方呀?這是因?yàn)椤负瘮?shù)選擇子」和參數(shù)的解析功能并不是由以太坊虛擬機(jī)的 go 代碼碼完成的腻惠,而是由合約編譯器在編譯時(shí)插入的代碼完成了环肘。
我們還以「合約創(chuàng)建」小節(jié)中合約為例。我們?cè)谀抢锾岬竭^集灌,實(shí)際的合約代碼是從 sub_0
這個(gè)標(biāo)簽處開始的悔雹,而當(dāng)時(shí)這個(gè)標(biāo)簽后的指令我們并沒有列出來。現(xiàn)在我們就把這些指令列出來。
合約的源代碼如下:
contract test {
uint256 x;
constructor() public {
x = 0xfafa;
}
function multiply(uint256 a) public view returns(uint256){
return x * a;
}
function multiplyState(uint256 a) public returns(uint256){
x = a * 7;
return x;
}
}
編譯成匯編代碼后荠商,sub_0
標(biāo)簽后面的部分代碼如下:
sub_0: assembly {
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1
jumpi
0x00
dup1
revert
tag_1:
pop
jumpi(tag_2, lt(calldatasize, 0x04))
shr(0xe0, calldataload(0x00))
dup1
0x1122db9a // 方法 multiplyState 的函數(shù)選擇子的值
eq
tag_3
jumpi
dup1
0xc6888fa1 // 方法 multiply 的函數(shù)選擇子的值
eq
tag_4
jumpi
tag_2:
......
tag_3:
......
tag_4:
......
......
我們不需要讀懂所有指令寂恬,只需注意 0x1122db9a
和 0xc6888fa1
前后的一些指令就可以了。這兩個(gè)值,正好分別是 test.multiplyState
和 test.multiply
的函數(shù)選擇子的值,其實(shí)不用看其它指令专酗,我們也能猜到這里是在比較參數(shù)中的函數(shù)選擇子的值,如果找到了(eq
指令牙咏,表示相等),就跳到相應(yīng)的代碼(tag_3
或 tag_4
)去執(zhí)行嘹裂。這樣通過參數(shù)中的函數(shù)選擇子妄壶,就達(dá)到了調(diào)用合約指定 public 方法的目的。(在這個(gè)例子中寄狼,tag_3
對(duì)應(yīng) test.multiplyState
丁寄,tag_4
對(duì)應(yīng) test.multiply
)
所以總得來看,調(diào)用合約與創(chuàng)建合約的 go 源碼沒有太大的差別泊愧,基本都是創(chuàng)建解釋器對(duì)象伊磺,然后解釋執(zhí)行合約指令。通過執(zhí)行不同的合約指令删咱,達(dá)到創(chuàng)建或調(diào)用的目的屑埋。
解釋器對(duì)象:EVMInterpreter
解釋器對(duì)象 EVMInterpreter
用來解釋執(zhí)行指定的合約指令。不過需要說明一點(diǎn)的是痰滋,實(shí)際的指令解釋執(zhí)行并不真正由解釋器對(duì)象完成的摘能,而是由 vm.Config.JumpTable
中的 operation
對(duì)象完成的,解釋器對(duì)象只是負(fù)責(zé)逐條解析指令碼敲街,然后獲取相應(yīng)的 operation
對(duì)象团搞,并在調(diào)用真正解釋指令的 operation.execute
函數(shù)之前檢查堆棧等對(duì)象。也可以說多艇,解釋器對(duì)象只是負(fù)責(zé)解釋的調(diào)度工作莺丑。
EVMInterpreter
關(guān)鍵方法是 Run
方法,我們?cè)谶@一小節(jié)里主要介紹一下這個(gè)方法的實(shí)現(xiàn)墩蔓。這個(gè)方法的代碼比較長(zhǎng),我們分開來一部分一部分的來看萧豆。下面是第一部分的代碼:
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
if in.intPool == nil {
in.intPool = poolOfIntPools.get()
defer func() {
poolOfIntPools.put(in.intPool)
in.intPool = nil
}()
}
// Increment the call depth which is restricted to 1024
in.evm.depth++
defer func() { in.evm.depth-- }()
// Make sure the readOnly is only set if we aren't in readOnly yet.
// This makes also sure that the readOnly flag isn't removed for child calls.
if readOnly && !in.readOnly {
in.readOnly = true
defer func() { in.readOnly = false }()
}
// Reset the previous call's return data. It's unimportant to preserve the old buffer
// as every returning call will return new data anyway.
in.returnData = nil
// Don't bother with the execution if there's no code.
if len(contract.Code) == 0 {
return nil, nil
}
......
}
這一部分的代碼比較簡(jiǎn)單奸披,只是初始化 EVMInterpreter
對(duì)象的一些字段。intPool
代表的是 big.Int
對(duì)象的一個(gè)池子涮雷,這樣可以節(jié)省頻繁創(chuàng)建和銷毀 big.Int
對(duì)象的開銷阵面。這里還增加了 EVM.depth
的值,這個(gè)值在「創(chuàng)建合約」小節(jié)提到過。 在一個(gè)合約中可以使用 new
創(chuàng)建另外一個(gè)合約样刷,這種情況屬于合約的遞歸創(chuàng)建仑扑,EVM.depth
記錄了遞歸的層數(shù)。
我們繼續(xù)看下面的代碼:
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
......
var (
......
mem = NewMemory() // bound memory
stack = newstack() // local stack
// For optimisation reason we're using uint64 as the program counter.
// It's theoretically possible to go above 2^64. The YP defines the PC
// to be uint256. Practically much less so feasible.
pc = uint64(0) // program counter
......
)
......
}
這一段是一些變量的聲明置鼻。之所以把這一段單獨(dú)拿出來镇饮,是想要強(qiáng)調(diào) mem
和 stack
變量。在合約的執(zhí)行過程中箕母,有三種存儲(chǔ)數(shù)據(jù)的方式储藐,其中兩種就是內(nèi)存塊和棧(詳情請(qǐng)參看「存儲(chǔ)」小節(jié)),它們分別由 mem
和 stack
變量代表嘶是。mem
是一個(gè) *Memory
類型的對(duì)象钙勃,它代表了一塊平坦的內(nèi)存空間;stack
是 *Stack
類型聂喇,它實(shí)現(xiàn)了一個(gè)有 push 和 pop 等典型棧操作的棧對(duì)象辖源。
我們繼續(xù)看后面的代碼。后面的代碼都在一個(gè) for 循環(huán)里希太,這個(gè) for 循環(huán)不斷的執(zhí)行合約的執(zhí)行克饶,直到執(zhí)行被中斷,或遇到錯(cuò)誤跛十,遇到停止執(zhí)行的指令彤路。這個(gè) for 循環(huán)比較大,我們依然分開來看:
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
......
for atomic.LoadInt32(&in.evm.abort) == 0 {
......
op = contract.GetOp(pc)
operation := in.cfg.JumpTable[op]
// 檢查指令的有效性
if !operation.valid {
return nil, fmt.Errorf("invalid opcode 0x%x", int(op))
}
// 檢查椊嬗常空間是否充足
if err := operation.validateStack(stack); err != nil {
return nil, err
}
// 檢查是否有寫指令限制
if err := in.enforceRestrictions(op, operation, stack); err != nil {
return nil, err
}
......
}
}
for 循環(huán)一開始洲尊,就從合約代碼中提取當(dāng)前將要執(zhí)行的指令的值(pc
跟蹤記錄了當(dāng)前將要執(zhí)行的指令的位置),然后從 vm.Config.JumpTable
中取出記錄指令詳細(xì)信息的 operation
對(duì)象奈偏。這個(gè)對(duì)象記錄了指令的很多詳細(xì)信息坞嘀,比如解釋指令的函數(shù)、指令消耗的 gas 的計(jì)算函數(shù)惊来、指令是否是寫操作等(詳細(xì)請(qǐng)參看下面的「跳轉(zhuǎn)表:vm.Config.JumpTable」小節(jié))丽涩。緊接著的還有檢查棧空間和是否有寫指令限制的代碼裁蚁。
我們接著看 for 循環(huán)的中間部分的代碼:
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
......
for atomic.LoadInt32(&in.evm.abort) == 0 {
......
// 如果將要執(zhí)行的指令需要用到內(nèi)存存儲(chǔ)空間矢渊,
// 則計(jì)算所需要的空間大小
var memorySize uint64
if operation.memorySize != nil {
memSize, overflow := bigUint64(operation.memorySize(stack))
if overflow {
return nil, errGasUintOverflow
}
if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
return nil, errGasUintOverflow
}
}
// 計(jì)算將要執(zhí)行的指令所需要的 gas 值并扣除。如果 gas 不足枉证,就直接失敗返回了
cost, err = operation.gasCost(in.gasTable, in.evm, contract, stack, mem, memorySize)
if err != nil || !contract.UseGas(cost) {
return nil, ErrOutOfGas
}
// 保證足夠的內(nèi)存存儲(chǔ)空間
if memorySize > 0 {
mem.Resize(memorySize)
}
......
}
}
在中間部分的代碼中矮男,主要做了兩件事,一是計(jì)算將要執(zhí)行的指令使用的內(nèi)存存儲(chǔ)空間室谚,并保證 mem
對(duì)象中有足夠的空間毡鉴;二是計(jì)算將要消耗的 gas崔泵,并立即消耗這些 gas,如果不夠用猪瞬,則返回失敗憎瘸。
我們接下來看最后一部分代碼:
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
......
for atomic.LoadInt32(&in.evm.abort) == 0 {
......
// 調(diào)用指令的解釋執(zhí)行函數(shù)
res, err := operation.execute(&pc, in, contract, mem, stack)
......
if operation.returns {
in.returnData = res
}
// 退出,或向前移動(dòng)指令指針
switch {
case err != nil:
return nil, err
case operation.reverts:
return res, errExecutionReverted
case operation.halts:
return res, nil
case !operation.jumps:
pc++
}
}
return nil, nil
}
這段代碼調(diào)用 operation.execute
函數(shù)陈瘦,用來真正的解釋指令做的事情幌甘。在其返回后,通過 switch 判斷錯(cuò)誤值甘晤,或向前移動(dòng) pc
含潘。
總得來說,解釋器對(duì)象 EVMInterpreter
并沒有什么復(fù)雜的東西线婚,無非解析合約代碼獲取指令碼遏弱,并從 vm.Config.JumpTable
中取得指令信息,然后檢查棧塞弊、內(nèi)存存儲(chǔ)漱逸、gas 等內(nèi)容,最后調(diào)用解釋指令的函數(shù)執(zhí)行指令的功能游沿,僅此而已饰抒。
預(yù)定義合約
解釋器對(duì)象的 Run
方法是在 run
函數(shù)中被調(diào)用的。在被調(diào)用之前诀黍,run
還處理了一種情況袋坑,我們稱之為「預(yù)定義合約」。如下代碼所示:
func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, error) {
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)
}
}
......
}
這段代碼查看合約的 CodeAddr
是否是預(yù)定義合約的地址眯勾,如果是就調(diào)用 RunPrecompiledContract
運(yùn)行這些預(yù)定義合約枣宫,而不再啟動(dòng)解釋器。(注意這里用的是 CodeAddr
吃环,因?yàn)?DelegateCall 等功能可能會(huì)改變合約地址也颤,但 CodeAddr
是不會(huì)被改變的)
根據(jù)以太坊版本的不同,預(yù)定義合約的集合也不同郁轻,PrecompiledContractsByzantium
這個(gè)變量里的數(shù)據(jù)比較新翅娶,所以我們就選這個(gè)變量看一下:
var PrecompiledContractsByzantium = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{1}): &ecrecover{},
common.BytesToAddress([]byte{2}): &sha256hash{},
common.BytesToAddress([]byte{3}): &ripemd160hash{},
common.BytesToAddress([]byte{4}): &dataCopy{},
common.BytesToAddress([]byte{5}): &bigModExp{},
common.BytesToAddress([]byte{6}): &bn256Add{},
common.BytesToAddress([]byte{7}): &bn256ScalarMul{},
common.BytesToAddress([]byte{8}): &bn256Pairing{},
}
可以看到這些所謂的「預(yù)定義合約」的地址也是預(yù)先定義好的,它們從 1 至 8好唯,類似于 0x0000000000000000000000000000000000000008
這樣竭沫。與地址對(duì)應(yīng)的對(duì)象也都有一個(gè) Run
方法,用來執(zhí)行各自合約的內(nèi)容骑篙,但它們直接是用 go 代碼實(shí)現(xiàn)的输吏,而不是解釋執(zhí)行某一份合約。從對(duì)象的名字就可以看出來替蛉,這些對(duì)象都有各自特定的功能贯溅。它們是用來實(shí)現(xiàn)一些 solidity 的內(nèi)置函數(shù)的。比如對(duì)于 solidity 的 ecrecover
這個(gè)內(nèi)置函數(shù)來說躲查,它通過編譯器編譯后它浅,最終是通過使用 STATICCALL
這個(gè)指令實(shí)現(xiàn)的。STATICCALL
的調(diào)用方式如下:
staticcall(gas, toAddr, inputOffset, inputSize, retOffset, retSize)
實(shí)現(xiàn) ecrecover
時(shí)镣煮,其第二個(gè)參數(shù) toAddr
就是 0x0000000000000000000000000000000000000001
姐霍。最終執(zhí)行 ecrecover.Run
方法實(shí)現(xiàn)此功能。
另外在使用 EVM.Call
調(diào)用合約時(shí)典唇,如果被調(diào)用的合約地址是預(yù)定義合約的地址镊折,且它不存在于狀態(tài)數(shù)據(jù)庫中,就使用預(yù)定義地址直接創(chuàng)建一個(gè)賬戶:
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
......
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)
}
}
gas 的消耗
我們知道介衔,合約中指令的執(zhí)行恨胚、數(shù)據(jù)的存儲(chǔ)都是需要消耗 gas 的。這一小節(jié)中我們集中看一下如何確定一個(gè)特定的動(dòng)作需要消耗的 gas 值炎咖。
gas 值只是一個(gè)數(shù)值赃泡,它有單價(jià),標(biāo)記在 Transaction.data.Price
字段中乘盼。在合約執(zhí)行開始之前升熊,以太坊會(huì)根據(jù)交易中的 gas 值和單價(jià),從交易發(fā)起人賬戶中扣除相應(yīng)的以太幣(所謂的「購買」gas)绸栅;合約執(zhí)行完成后级野,再將未使用的 gas 退還能交易發(fā)起人。如下代碼所示:
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
// 在 st.preCheck 中「購買」 gas
if err = st.preCheck(); err != nil {
return
}
// 執(zhí)行合約
......
// 退還沒用完的 gas粹胯,并把已使用的 gas 計(jì)入礦工賬戶中
st.refundGas()
st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
......
}
這里也可以看出蓖柔,在合約執(zhí)行過程中,gas 就是一個(gè)整數(shù)值矛双,不涉及到賬戶上以太幣的變化渊抽。初始化給合約的是可以使用的 gas 的最大值,隨著合約的執(zhí)行议忽, gas 逐漸被消耗懒闷,直到合約停止執(zhí)行或 gas 被耗盡。
我們知道栈幸,智能合約中每執(zhí)行一條指令都需要消耗 gas愤估。實(shí)際上在指令在被解釋執(zhí)行之前,就已經(jīng)計(jì)算好需要消耗的 gas 速址,并首先消耗掉這些 gas 后玩焰,才開始解釋執(zhí)行指令。如下代碼所示:
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
......
for atomic.LoadInt32(&in.evm.abort) == 0 {
......
operation := in.cfg.JumpTable[op]
......
cost, err = operation.gasCost(in.gasTable, in.evm, contract, stack, mem, memorySize)
if err != nil || !contract.UseGas(cost) {
return nil, ErrOutOfGas
}
......
res, err := operation.execute(&pc, in, contract, mem, stack)
......
}
}
可以看到芍锚,在每次執(zhí)行 operation.execute
解釋指令時(shí)昔园,都會(huì)先調(diào)用 operation.gasCost
計(jì)算需要的 gas 值蔓榄,并調(diào)用 Contract.useGas
消耗掉這些 gas。
你可能會(huì)注意到默刚,上面的代碼中調(diào)用 operation.gasCost
時(shí)第一個(gè)參數(shù)用到了 EVMInterpreter.gasTable
甥郑。這個(gè)字段中的值定義了一些特殊指令在計(jì)算所需 gas 時(shí)用到的數(shù)值,它在解釋器創(chuàng)建時(shí)被初始化:
func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter {
......
return &EVMInterpreter{
......
gasTable: evm.ChainConfig().GasTable(evm.BlockNumber),
}
}
與 vm.Config.JumpTable
類似荤西,根據(jù)以太坊版本的不同澜搅,這里初始化的對(duì)象也不同。我們挑一個(gè)比較高的版本邪锌,看一下 EVMInterpreter.gasTable
最終會(huì)被一些什么值填充:
GasTableConstantinople = GasTable{
ExtcodeSize: 700,
ExtcodeCopy: 700,
ExtcodeHash: 400,
Balance: 400,
SLoad: 200,
Calls: 700,
Suicide: 5000,
ExpByte: 50,
CreateBySuicide: 25000,
}
其實(shí) gasCost
是一個(gè)函數(shù)類型的字段勉躺,根據(jù)指令的不同,這個(gè)字段指向的函數(shù)也不同觅丰,因而計(jì)算方式也不同饵溅。這些信息都是在 JumpTable 中配置好的(參見「跳轉(zhuǎn)表:vm.Config.JumpTable」小節(jié))。比如對(duì)于 ADD 指令舶胀,它的定義如下(下面的代碼是函數(shù) newFrontierInstructionSet
的片段):
ADD: {
execute: opAdd,
gasCost: constGasFunc(GasFastestStep),
validateStack: makeStackFunc(2, 1),
valid: true,
},
而 constGasFunc
和 GasFastestStep
的定義如下:
func constGasFunc(gas uint64) gasFunc {
return
func(gt params.GasTable,
evm *EVM,
contract *Contract,
stack *Stack,
mem *Memory,
memorySize uint64) (uint64, error)
{
return gas, nil
}
}
const (
GasFastestStep uint64 = 3
)
可見對(duì)于 ADD 指令來說概说,它的 gas 消耗是固定的,就是 3嚣伐。但對(duì)于某些指令糖赔,gas 的計(jì)算就要復(fù)雜一些了,這里就不一一列舉了轩端,感興趣的可以直接去源碼中查看放典。
事實(shí)上,不僅指令的解釋執(zhí)行本身需要消耗 gas 基茵,當(dāng)使用內(nèi)存存儲(chǔ)(Memory
對(duì)象代表的存儲(chǔ)空間奋构,詳見下面「Memory」小節(jié))和 StateDB 永久存儲(chǔ)時(shí),都需要消耗 gas 拱层。因此在所有 operation.gasCost
所指向的函數(shù)中弥臼,對(duì) gas 的計(jì)算都需要考慮三方面的內(nèi)容:解釋指令本身需要的 gas 、使用內(nèi)存存儲(chǔ)需要的 gas 根灯、使用 StateDB 存儲(chǔ)需要的 gas 径缅。對(duì)于大多數(shù)指令,后兩項(xiàng)是用不到的烙肺,但對(duì)于某些指令(比如 CODECOPY 或 SSTORE)纳猪,它們的 gasCost
函數(shù)會(huì)考慮到內(nèi)存和 StateDB 使用的情況。
這里需要提一下內(nèi)存存儲(chǔ)的 gas 消耗桃笙。計(jì)算內(nèi)存存儲(chǔ) gas 值的函數(shù)為 memoryGasCost
氏堤。這個(gè)函數(shù)會(huì)根據(jù)所要求的空間大小,計(jì)算將要消耗的 gas 值搏明。但只有新需求的空間大小超過當(dāng)前空間大小時(shí)鼠锈,超過的部分才需要消耗 gas 闪檬。
最后需要說明一點(diǎn)的是,除上上面提到的每次執(zhí)行指令前消耗 gas 购笆,還有兩種情況情況會(huì)消耗 gas 谬以。一種情況是在執(zhí)行合約之前;另一種是在創(chuàng)建合約之后由桌。
在執(zhí)行合約之前消耗的 gas 被稱之為 「intrinsic gas」,如下代碼所示:
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
......
// Pay intrinsic gas
gas, err := IntrinsicGas(st.data, contractCreation, homestead)
if err != nil {
return nil, 0, false, err
}
if err = st.useGas(gas); err != nil {
return nil, 0, false, err
}
......
// 執(zhí)行合約
if contractCreation {
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
} else {
// Increment the nonce for the next transaction
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
......
}
從代碼中可以看出邮丰,在合約還沒有執(zhí)行之前行您,就要先 useGas
。此時(shí) gas 值是由 IntrinsicGas
計(jì)算出來的剪廉。這個(gè)函數(shù)根據(jù)合約代碼中非 0 字節(jié)的數(shù)量來計(jì)算消耗的 gas 值娃循。(感覺這里好霸道,啥也不干先交錢)
另一種情況在創(chuàng)建合約之后斗蒋。由于合約創(chuàng)建成功后要把合約代碼存儲(chǔ)到狀態(tài)數(shù)據(jù)庫 StateDB 中捌斧,所以當(dāng)然也需要消耗 gas。如下代碼所示:
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address) ([]byte, common.Address, uint64, error) {
......
// 創(chuàng)建合約泉沾。合約代碼在返回值 ret 中
ret, err := run(evm, contract, nil, false)
maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) &&
len(ret) > params.MaxCodeSize
if err == nil && !maxCodeSizeExceeded {
// 合約創(chuàng)建成功捞蚂,將合約保存到 StateDB 之前,先 useGas
createDataGas := uint64(len(ret)) * params.CreateDataGas
if contract.UseGas(createDataGas) {
evm.StateDB.SetCode(address, ret)
} else {
err = ErrCodeStoreOutOfGas
}
}
}
可以看到在合約創(chuàng)建成功后跷究,要調(diào)用 StateDB.SetCode
將合約代碼保存到狀態(tài)數(shù)據(jù)庫中姓迅。但在保存之前,要先消耗一定數(shù)據(jù)的 gas 俊马。如果此時(shí) gas 不夠用丁存,合約沒有被保存,也就創(chuàng)建失敗啦柴我。
跳轉(zhuǎn)表:vm.Config.JumpTable
我們前面已經(jīng)提到過解寝,解釋器不是真正解釋指令。指令的真正解釋函數(shù)艘儒,記錄在 vm.Config.JumpTable
字段中聋伦。在 創(chuàng)建解釋器的 NewEVMInterpreter
函數(shù)中,根據(jù)以太坊版本的不同彤悔,會(huì)使用不同的對(duì)象填充這個(gè)字段:
func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter {
// We use the STOP instruction whether to see
// the jump table was initialised. If it was not
// we'll set the default jump table.
if !cfg.JumpTable[STOP].valid {
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 = frontierInstructionSet
}
}
return &EVMInterpreter{
evm: evm,
cfg: cfg,
gasTable: evm.ChainConfig().GasTable(evm.BlockNumber),
}
}
這里面共有四個(gè)集合嘉抓,稍微看一下就能明白,frontierInstructionSet
這個(gè)對(duì)象包含了最基本的指令信息晕窑,其它三個(gè)是對(duì)這個(gè)集合的擴(kuò)充抑片,最全的一個(gè)是 constantinopleInstructionSet
(這跟以太坊的升級(jí)歷史是一樣的)。
這些對(duì)象的類型都是 [256]operation
杨赤,使用時(shí)以指令的 opcode 值為索引敞斋,從中取出存放指令詳細(xì)信息的 operation
對(duì)象截汪。所有 opcode 定義在 opcodes.go 文件中,這是就不一一列出了植捎。
operation
對(duì)象詳細(xì)記錄了指令的所有信息衙解,它的定義如下:
type operation struct {
execute executionFunc // 指令的解釋執(zhí)行函數(shù)
gasCost gasFunc // 計(jì)算指令將要消耗的 gas 值的函數(shù)
validateStack stackValidationFunc // 檢查棧空間是否足夠指令使用的函數(shù)
memorySize memorySizeFunc // 計(jì)算指令將要消耗的內(nèi)存存儲(chǔ)空間大小的函數(shù)
halts bool // 指令執(zhí)行完成后是否停止解釋器的執(zhí)行
jumps bool // 是否是跳轉(zhuǎn)指令(非跳轉(zhuǎn)指令執(zhí)行時(shí)不會(huì)修改 pc 變量的值)
writes bool // 是否是寫指令(會(huì)修改 StatDB 中的數(shù)據(jù))
valid bool // 是否是有效指令
reverts bool // 指令指行完后是否中斷執(zhí)行并恢復(fù)狀態(tài)數(shù)據(jù)庫
returns bool // 指令是否有返回值
}
這個(gè)結(jié)構(gòu)體的字段幾乎都比較好理解焰枢,這里就不多解釋了蚓峦。只有 reverts
字段需要多說一下。目前只有 REVERT
指令會(huì)設(shè)置這個(gè)字段為 true( REVERT
指令應(yīng)該對(duì)應(yīng)了 solidity 語言的 require
和 revert
函數(shù))济锄。如果這個(gè)字段為 true暑椰,則當(dāng)前指令執(zhí)行完成后,EVMInterpreter.Run
立即返回錯(cuò)誤 errExecutionReverted
荐绝。這個(gè)錯(cuò)誤值會(huì)一直傳回給調(diào)用 run
函數(shù)的代碼中一汽,比如 EVM.create
或 EVM.Call
。如果 run
返回的是這個(gè)錯(cuò)誤值低滩,那么將剩余的 gas 返還給調(diào)用者(出乎意料召夹,如果是其它錯(cuò)誤,所有 gas 將直接被耗盡)恕沫,如下代碼所示:
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
......
ret, err = run(evm, contract, input, false)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != errExecutionReverted {
contract.UseGas(contract.Gas)
}
}
}
跳轉(zhuǎn)指令
跳轉(zhuǎn)指令設(shè)計(jì)得稍微有點(diǎn)特殊监憎,因此這里單獨(dú)介紹一下。在合約的指令中昏兆,有兩個(gè)跳轉(zhuǎn)指令(不算 CALL 這類):JUMP 和 JUMPI枫虏。它們的特殊的地方在于,跳轉(zhuǎn)后的目標(biāo)地址的第一條指令必須是 JUMPDEST爬虱。
我們以 JUMP 指令為例隶债,看一下它的解釋函數(shù)的代碼:
func opJump(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
pos := stack.pop()
if !contract.validJumpdest(pos) {
nop := contract.GetOp(pos.Uint64())
return nil, fmt.Errorf("invalid jump destination (%v) %v", nop, pos)
}
*pc = pos.Uint64()
interpreter.intPool.put(pos)
return nil, nil
}
這個(gè)函數(shù)解釋執(zhí)行了 JUMP 指令。代碼首先從棧中取出一個(gè)值跑筝,作為跳轉(zhuǎn)目的地死讹。這個(gè)值其實(shí)就是相對(duì)于合約代碼第 0 字段的偏移。然后代碼會(huì)調(diào)用 Contract.validJumpdest
判斷這個(gè)目的地的第一條指令是否是 JUMPDEST曲梗,如果不是這里就出錯(cuò)了赞警。
現(xiàn)在的關(guān)鍵問題是如何判斷目的地的第一條指令是 JUMPDEST。你可能會(huì)說很簡(jiǎn)單虏两,看看目的地的第 0 個(gè)字節(jié)的值是否是 JUMPDEST 這個(gè) opcode 碼不是完了嘛愧旦。如果這個(gè)字節(jié)是一條指令,那確實(shí)是這樣定罢;但如果這個(gè)字節(jié)是數(shù)據(jù)笤虫,那就不對(duì)了。比如 JUMPDEST 這個(gè) opcode 的值為 0x5b, 如果有下面一段指令:
60 5b 60 80 ......
把它翻譯成指令的形式為:
PUSH1 0x5b
PUSH1 0x80
......
可以看到,這里的 0x5b 是作為數(shù)據(jù)出現(xiàn)的琼蚯,而非指令酬凳。但如果某條 jump 指令跳到了第 1 個(gè)字節(jié)(從 0 開始計(jì)數(shù)),并把這個(gè) 0x5b 當(dāng)成指令數(shù)據(jù)遭庶,得出跳轉(zhuǎn)后第 0 條指令為 JUMPDEST宁仔,顯然是不對(duì)的。
因此判斷目的地第一條指令是否是 JUMPDEST峦睡,要保證兩點(diǎn):一是它的值為 JUMPDEST 指令的 opcode 的值翎苫;二是這是一條指令,而非普通數(shù)據(jù)榨了。
下面我們介紹一下 Contract.validJumpdest
是如何做的拉队。除了比對(duì) opcode 外(這個(gè)很簡(jiǎn)單),Contract
還會(huì)創(chuàng)建一個(gè)位向量對(duì)象(即 bitvec
阻逮,bit vector)。這個(gè)對(duì)象會(huì)從頭至尾分析一遍合約指令秩彤,如果合約某一偏移處的字節(jié)屬于普通數(shù)據(jù)叔扼,就將 bitvec
中這個(gè)偏移值對(duì)應(yīng)的「位」設(shè)置為 1,如果是指令則設(shè)置為 0漫雷。在 Contract.validJumpdest
中瓜富,通過檢查跳轉(zhuǎn)目的地的偏移值在這個(gè)位向量對(duì)象中的「位」是否為 0,來判斷此處是否是正常指令降盹。
我們?cè)敿?xì)看一下 Contract.validJumpdest
的代碼:
func (c *Contract) validJumpdest(dest *big.Int) bool {
udest := dest.Uint64()
if dest.BitLen() >= 63 || udest >= uint64(len(c.Code)) {
return false
}
// 檢查目的地指令值是否為 JUMPDEST
if OpCode(c.Code[udest]) != JUMPDEST {
return false
}
// 下面的代碼都是檢查目的地的值是否是指令与柑,而非普通數(shù)據(jù)
// CodeHash 不為空的情況。一般調(diào)用合約時(shí)會(huì)進(jìn)入這個(gè)分支
if c.CodeHash != (common.Hash{}) {
// Does parent context have the analysis?
analysis, exist := c.jumpdests[c.CodeHash]
if !exist {
// Do the analysis and save in parent context
// We do not need to store it in c.analysis
analysis = codeBitmap(c.Code)
c.jumpdests[c.CodeHash] = analysis
}
return analysis.codeSegment(udest)
}
// CodeHash 為空的情況蓄坏,
// 一般是因?yàn)樾潞霞s創(chuàng)建价捧、還未將合約寫入狀態(tài)數(shù)據(jù)庫
if c.analysis == nil {
c.analysis = codeBitmap(c.Code)
}
return c.analysis.codeSegment(udest)
}
代碼的一開始檢查目的地的偏移不能太大,因?yàn)楹霞s代碼不可能那么長(zhǎng)涡戳。然后就是最簡(jiǎn)單的檢查目的地第 0 個(gè)字節(jié)是否是 JUMPDEST 這個(gè) opcode 值结蟋。當(dāng)這些都符合要求時(shí),就會(huì)繼續(xù)檢查目的地的值是否是指令而非數(shù)據(jù)渔彰。
檢查是否為指令的代碼有兩個(gè)分支嵌屎,但基本上它們做的事情是一樣的。只不過 Contract.jumpdests
中包含了合約調(diào)用者的數(shù)據(jù)恍涂,而 Contract.analysis
只包含了當(dāng)前合約的數(shù)據(jù)宝惰。這里的重點(diǎn)是 codeBitmap
函數(shù)生成的對(duì)象,以及這個(gè)對(duì)象的 codeSegment
方法再沧。
我們先看一下 codeBitmap
函數(shù):
func codeBitmap(code []byte) bitvec {
bits := make(bitvec, len(code)/8+1+4)
for pc := uint64(0); pc < uint64(len(code)); {
op := OpCode(code[pc])
if op >= PUSH1 && op <= PUSH32 {
// PUSH1 代表 push 指令后的 1 個(gè)字節(jié)都是用來入棧的數(shù)據(jù)
// PUSH2 代表 push 指令后的 2 個(gè)字節(jié)都是用來入棧的數(shù)據(jù)
// 以及類推尼夺,所以 numbits 為 op - PUSH1 + 1
numbits := op - PUSH1 + 1
pc++
for ; numbits >= 8; numbits -= 8 {
bits.set8(pc) // 8
pc += 8
}
for ; numbits > 0; numbits-- {
bits.set(pc)
pc++
}
} else {
pc++
}
}
return bits
}
這個(gè)函數(shù)用來生成位向量。位向量的類型為 bitvec
,它的真正類型是 []byte
汞斧。在這個(gè)函數(shù)中夜郁,code
參數(shù)代表了某個(gè)合約的完整的合約代碼,函數(shù)從頭到尾掃描每一條指令粘勒,把普通數(shù)據(jù)的偏移值在位向量中設(shè)置成 1竞端。bits.set
表示將指定偏移位設(shè)置成 1;bits.set8
表示將指定偏移位及其后面 7 個(gè)位都設(shè)置成 1庙睡。
(從這個(gè)函數(shù)上看事富,貌似只有 push 指令會(huì)在代碼中夾雜數(shù)據(jù),其它指令都不會(huì)如此乘陪。另外统台,我覺得這種從頭到尾掃描的方式無法保證嚴(yán)格正確,即可能有些是數(shù)據(jù)但沒有在位向量中置 1啡邑。也可能是合約編譯器做了處理贱勃,保證這里可以得到正確結(jié)果)
有了 bitvec
對(duì)象,就可以通過 bitvec.codeSegment
判斷某一個(gè)偏移位是否是數(shù)據(jù)了:
func (bits *bitvec) codeSegment(pos uint64) bool {
return ((*bits)[pos/8] & (0x80 >> (pos % 8))) == 0
}
這個(gè)方法只是查看指定偏移位的值是否為0谤逼,如果是贵扰,則代表這個(gè)偏移處的值是一條指令,而非數(shù)據(jù)流部。
總得來說戚绕,以太坊虛擬機(jī)中對(duì)跳轉(zhuǎn)指令限制得比較嚴(yán)格,跳轉(zhuǎn)目的地的第 0 條指令必須是 JUMPDEST枝冀,并且不光得 opcode 值相同舞丛,還得保證這確實(shí)是一條指令,而非普通數(shù)據(jù)果漾。這種嚴(yán)格的限制可以保證虛擬機(jī)執(zhí)行的正確性球切,也可能可以避免一些惡意攻擊。
存儲(chǔ)
以太坊虛擬機(jī)中有三個(gè)地方可以存儲(chǔ)合約的數(shù)據(jù):棧绒障、內(nèi)存塊存儲(chǔ)欧聘、狀態(tài)數(shù)據(jù)庫。其中只有狀態(tài)數(shù)據(jù)庫是永久性的端盆,其它兩個(gè)在合約運(yùn)行結(jié)束后怀骤,就銷毀了。
棧和內(nèi)存塊存儲(chǔ)在解釋器準(zhǔn)備啟動(dòng)時(shí)被創(chuàng)建焕妙,如下所示:
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
......
var (
mem = NewMemory() // bound memory
stack = newstack() // local stack
)
......
}
下面我們分別詳細(xì)介紹一下這幾個(gè)存儲(chǔ)對(duì)象蒋伦。
Stack
Stack
對(duì)象為合約的運(yùn)行提供了棧功能,其中的數(shù)據(jù)先進(jìn)后出焚鹊。它的定義很簡(jiǎn)單:
type Stack struct {
data []*big.Int
}
它只有一個(gè) data
字段痕届,這是一個(gè)可變長(zhǎng)數(shù)據(jù)韧献,用來充當(dāng)棧存儲(chǔ)空間⊙薪校可以看到它的元素的類型是 big.Int
锤窑。
newstack
函數(shù)用來創(chuàng)建一個(gè)新的棧對(duì)象:
func newstack() *Stack {
return &Stack{data: make([]*big.Int, 0, 1024)}
}
這個(gè)函數(shù)也很簡(jiǎn)單,生成了一個(gè)預(yù)留 1024 個(gè)存儲(chǔ)空間的數(shù)組填充 data
字段嚷炉。
Stack
對(duì)象有幾個(gè)操作方法渊啰,完全是按照「先進(jìn)后出」的棧結(jié)構(gòu)語義來實(shí)現(xiàn)的,比如 push 和 pop申屹,再比如 swap 方法绘证,其參數(shù) n
也是指從棧頂(即數(shù)組最后一個(gè)元素)向前 n 個(gè)元素,而非從數(shù)組第 0 個(gè)元素開始算起哗讥。Stack
的方法實(shí)現(xiàn)都很簡(jiǎn)單嚷那,幾乎都是一行代碼搞定,因此這里就不再我說了杆煞。
Memory
Memory
對(duì)象為合約的運(yùn)行提供了一整塊的平坦的存儲(chǔ)空間魏宽。它的定義如下:
type Memory struct {
store []byte
lastGasCost uint64
}
可以看到這個(gè)對(duì)象的定義也很簡(jiǎn)單,store
字段用來提供一塊平坦的內(nèi)存作為存儲(chǔ)空間决乎。lastGasCost
用來在每次使用存儲(chǔ)空間時(shí)湖员,參與計(jì)算消耗的 gas。
Memory
對(duì)象提供了一些操作方法瑞驱,如果 set 和 set32,用來將指定的數(shù)據(jù)存儲(chǔ)到指定偏移中窄坦。Memory.Resize
方法接收一個(gè)數(shù)值唤反,保證當(dāng)前擁有的空間大小不小于這個(gè)數(shù)值。其它方法比較簡(jiǎn)單鸭津,就不一一介紹了彤侍。
需要注意的是,在合約中使用內(nèi)存存儲(chǔ)有可能是需要消耗 gas 的逆趋。消耗的大小由 memoryGasCost
計(jì)算確定盏阶。從這個(gè)函數(shù)的實(shí)現(xiàn)來看,只要所需空間超過當(dāng)前空間的大小闻书,超過部分就需要消耗 gas名斟。
永久存儲(chǔ):StateDB
永久存儲(chǔ)對(duì)象 StateDB
即是以太坊的狀態(tài)數(shù)據(jù)庫,這個(gè)數(shù)據(jù)庫中包含了所有賬戶的信息魄眉,包括賬戶相關(guān)的合約代碼和合約的存儲(chǔ)數(shù)據(jù)砰盐。這個(gè)對(duì)象不是 evm 模塊中的對(duì)象。想要詳細(xì)了解狀態(tài)數(shù)據(jù)庫坑律,可以參看這篇文章岩梳。
其它輔助對(duì)象
為了文章的完整性,這里稍微提一下 evm 模塊中的其它輔助對(duì)象。但其實(shí)這些對(duì)象不是 evm 的核心功能冀值,無需花太多精力去了解也物。
intPool
intPool
是 big.Int
類型的變量的一個(gè)緩存池。在 evm 模塊的代碼中列疗,尤其是指令的解釋函數(shù)中滑蚯,當(dāng)需要新申請(qǐng)或銷毀一個(gè) big.Int
變量時(shí),都可以通過 intPool
來進(jìn)行作彤。
在 intPool
內(nèi)部膘魄,這個(gè)對(duì)象使用 Stack
對(duì)象存儲(chǔ)一些暫時(shí)不被使用的 big.Int
變量。當(dāng)有人調(diào)用 intPool.get
申請(qǐng)一個(gè) big.Int
變量時(shí)竭讳,它會(huì)從棧中取出一個(gè)创葡,或棧為空時(shí)新創(chuàng)建一個(gè);當(dāng)有人調(diào)用 intPool.put
銷毀一個(gè) big.Int
變量時(shí)绢慢,它會(huì)將這個(gè)變量放到內(nèi)部的棧中緩存起來(除非棧中緩存的數(shù)據(jù)已達(dá)到上限)灿渴,供后續(xù)有人調(diào)用 intPool.get
時(shí)重復(fù)利用榜贴。
logger
在 evm 模塊中争群,log 對(duì)象有 JSONLogger
和 StructLogger
兩種栏豺,它們都以一定的格式輸出一些 log 數(shù)據(jù)板惑,這些功能沒有什么可詳細(xì)說的揽惹,了解一下就行番电。
如果設(shè)置 vm.Config.Debug
字段為 true 撒强,那么在虛擬機(jī)運(yùn)行時(shí)挽荡,StructLogger
會(huì)有一些詳細(xì)的 log 輸出倦零,方便進(jìn)行調(diào)試误续。但我沒有找到有哪個(gè)選項(xiàng)或配置文件可以設(shè)置這個(gè)字段,或許只是在調(diào)試代碼時(shí)手動(dòng)寫一下這個(gè)值吧扫茅。
總結(jié)
evm 模塊實(shí)現(xiàn)了運(yùn)行以太坊智能合約的虛擬機(jī)蹋嵌,是一個(gè)非常重要的模塊。在這篇文章里葫隙,我們?cè)敿?xì)了解了 evm 模塊的實(shí)現(xiàn)栽烂,包括合約是如何創(chuàng)建和調(diào)用的;合約指令是如何被解釋執(zhí)行的等等恋脚。
我們?cè)谖恼麻_始已經(jīng)說過腺办,以太坊智能合約是很重要也很復(fù)雜的實(shí)現(xiàn),不能只靠 evm 模塊了解智能合約的全部糟描,evm 只提供了智能合約執(zhí)行的虛擬機(jī)菇晃,是比較小的一部分。如果想要完整了解以太坊智能合約的實(shí)現(xiàn)蚓挤,還需要學(xué)習(xí)智能合約的編譯器 solidity 項(xiàng)目磺送。
水平有限驻子,如果文章中有錯(cuò)誤,還請(qǐng)不吝指正估灿。
本文最先發(fā)表于我的博客崇呵,歡迎評(píng)論和轉(zhuǎn)載。