以太坊源碼解析:evm

本篇文章分析的源碼地址為: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.ConfigJumpTable 字段中飒泻。

vm.Config 為虛擬機(jī)和解釋器提供了配置信息鞭光,其中最重要的就是 JumpTableJumpTablevm.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结序、homesteadInstructionSetfrontierInstructionSet纵潦。這四套指令集多數(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.CallCodeEVM.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.CallCodeEVM.DelegateCall 類似,不同的是 EVM.CallCode 不改變「庫合約」的調(diào)用者煎娇,只是改變「庫合約」的合約地址二庵。也就是說贪染,如果 A 通過 CallCode 的方式調(diào)用 B,那么 B 的調(diào)用者是 A催享,而 B 的賬戶地址也被改成了 A杭隙。

總結(jié)一下就是,EVM.CallCodeEVM.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.StaticCallEVM.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:
    ......

    ......

我們不需要讀懂所有指令寂恬,只需注意 0x1122db9a0xc6888fa1 前后的一些指令就可以了。這兩個(gè)值,正好分別是 test.multiplyStatetest.multiply 的函數(shù)選擇子的值,其實(shí)不用看其它指令专酗,我們也能猜到這里是在比較參數(shù)中的函數(shù)選擇子的值,如果找到了(eq 指令牙咏,表示相等),就跳到相應(yīng)的代碼(tag_3tag_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) memstack 變量。在合約的執(zhí)行過程中箕母,有三種存儲(chǔ)數(shù)據(jù)的方式储藐,其中兩種就是內(nèi)存塊和棧(詳情請(qǐng)參看「存儲(chǔ)」小節(jié)),它們分別由 memstack 變量代表嘶是。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,
    },

constGasFuncGasFastestStep 的定義如下:

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 語言的 requirerevert 函數(shù))济锄。如果這個(gè)字段為 true暑椰,則當(dāng)前指令執(zhí)行完成后,EVMInterpreter.Run 立即返回錯(cuò)誤 errExecutionReverted荐绝。這個(gè)錯(cuò)誤值會(huì)一直傳回給調(diào)用 run 函數(shù)的代碼中一汽,比如 EVM.createEVM.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

intPoolbig.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ì)象有 JSONLoggerStructLogger 兩種栏豺,它們都以一定的格式輸出一些 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)載。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末馅袁,一起剝皮案震驚了整個(gè)濱河市域慷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌汗销,老刑警劉巖犹褒,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異弛针,居然都是意外死亡叠骑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門削茁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宙枷,“玉大人,你說我怎么就攤上這事茧跋∥看裕” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵瘾杭,是天一觀的道長(zhǎng)诅病。 經(jīng)常有香客問我,道長(zhǎng)粥烁,這世上最難降的妖魔是什么贤笆? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮页徐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘银萍。我一直安慰自己变勇,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布贴唇。 她就那樣靜靜地躺著搀绣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪戳气。 梳的紋絲不亂的頭發(fā)上链患,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音瓶您,去河邊找鬼麻捻。 笑死纲仍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的贸毕。 我是一名探鬼主播郑叠,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼明棍!你這毒婦竟也來了乡革?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤摊腋,失蹤者是張志新(化名)和其女友劉穎沸版,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兴蒸,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡视粮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了类咧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片馒铃。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖痕惋,靈堂內(nèi)的尸體忽然破棺而出区宇,到底是詐尸還是另有隱情,我是刑警寧澤值戳,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布议谷,位于F島的核電站,受9級(jí)特大地震影響堕虹,放射性物質(zhì)發(fā)生泄漏卧晓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一赴捞、第九天 我趴在偏房一處隱蔽的房頂上張望逼裆。 院中可真熱鬧,春花似錦赦政、人聲如沸胜宇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桐愉。三九已至,卻和暖如春掰派,著一層夾襖步出監(jiān)牢的瞬間从诲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工靡羡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留系洛,地道東北人俊性。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像碎罚,于是被迫代替她去往敵國(guó)和親磅废。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容