1. 程式人生 > >以太坊智慧合約虛擬機器(EVM)原理與實現

以太坊智慧合約虛擬機器(EVM)原理與實現

以太坊底層通過EVM模組支援合約的執行與呼叫,呼叫時根據合約地址獲取到程式碼,生成環境後載入到EVM中執行。通常智慧合約的開發流程是用solidlity編寫邏輯程式碼,再通過編譯器編譯元資料,最後再發布到以太坊上。

輸入圖片說明

程式碼結構

.
├── analysis.go            //跳轉目標判定
├── common.go
├── contract.go            //合約資料結構
├── contracts.go           //預編譯好的合約
├── errors.go
├── evm.go                 //執行器 對外提供一些外部介面   
├── gas.go
//call gas花費計算 一級指令耗費gas級別 ├── gas_table.go //指令耗費計算函式表 ├── gen_structlog.go ├── instructions.go //指令操作 ├── interface.go ├── interpreter.go //直譯器 呼叫核心 ├── intpool.go //int值池 ├── int_pool_verifier_empty.go ├── int_pool_verifier.go ├── jump_table.go
//指令和指令操作(操作,花費,驗證)對應表 ├── logger.go //狀態日誌 ├── memory.go //EVM 記憶體 ├── memory_table.go //EVM 記憶體操作表 主要衡量操作所需記憶體大小 ├── noop.go ├── opcodes.go //Op指令 以及一些對應關係 ├── runtime │   ├── env.go //執行環境 │   ├── fuzz.go │   └── runtime.go //執行介面 測試使用
├── stack.go //棧 └── stack_table.go //棧驗證

指令

OpCode

檔案opcodes.go中定義了所有的OpCode,該值是一個byte,合約編譯出來的bytecode中,一個OpCode就是上面的一位。opcodes按功能分為9組(運算相關,塊操作,加密相關等)。

    //算數相關
    const (
        // 0x0 range - arithmetic ops
        STOP OpCode = iota
        ADD
        MUL
        SUB
        DIV
        SDIV
        MOD
        SMOD
        ADDMOD
        MULMOD
        EXP
        SIGNEXTEND
    )

Instruction

檔案jump.table.go定義了四種指令集合,每個集合實質上是個256長度的陣列,名字翻譯過來是(荒地,農莊,拜占庭,君士坦丁堡)估計是對應了EVM的四個發展階段。指令集向前相容。

	frontierInstructionSet       = NewFrontierInstructionSet()
	homesteadInstructionSet      = NewHomesteadInstructionSet()
	byzantiumInstructionSet      = NewByzantiumInstructionSet()
	constantinopleInstructionSet = NewConstantinopleInstructionSet()

具體每條指令結構如下,欄位意思見註釋。

typeoperation struct {
	//對應的操作函式
	execute executionFunc
	// 操作對應的gas消耗
	gasCost gasFunc
	// 棧深度驗證
	validateStack stackValidationFunc
	// 操作所需空間
	memorySize memorySizeFunc

	halts   bool // 運算中止
	jumps   bool // 跳轉(for)
	writes  bool // 是否寫入
	valid   bool // 操作是否有效
	reverts bool // 出錯回滾
	returns bool // 返回
}

按下面的ADD指令為例

定義

    ADD: {
        execute:       opAdd,
        gasCost:       constGasFunc(GasFastestStep),
        validateStack: makeStackFunc(2, 1),
        valid:         true,
    },

操作

不同的操作有所不同,操作物件根據指令不同可能影響棧,記憶體,statedb。

    funcopAdd(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack)([]byte, error) {
        //彈出一個值,取出一個值(這個值依舊儲存在棧上面,運算結束後這個值就改變成結果值)
        x, y := stack.pop(), stack.peek()
        //加運算
        math.U256(y.Add(x, y))
        //數值快取
        evm.interpreter.intPool.put(x)
        return nil, nil
    }

gas花費

不同的運算有不同的初始值和對應的運算方法,具體的方法都定義在gas_table裡面。 按加法的為例,一次加操作固定耗費為3。

    //固定耗費
    funcconstGasFunc(gas uint64)gasFunc {
        return func(gt params.GasTable, evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64)(uint64, error) {
            return gas, nil
        }
    }

除此之外還有兩個定義會影響gas的計算,通常作為量化的一個單位。

    //file go-ethereum/core/vm/gas.go
    const (
        GasQuickStep   uint64 = 2
        GasFastestStep uint64 = 3
        GasFastStep    uint64 = 5
        GasMidStep     uint64 = 8
        GasSlowStep    uint64 = 10
        GasExtStep     uint64 = 20

        GasReturn       uint64 = 0
        GasStop         uint64 = 0
        GasContractByte uint64 = 200
    )

    //file go-ethereum/params/gas_table.go
    type GasTable struct {
        ExtcodeSize uint64
        ExtcodeCopy uint64
        Balance     uint64
        SLoad       uint64
        Calls       uint64
        Suicide     uint64

        ExpByte uint64

        // CreateBySuicide occurs when the
        // refunded account is one that does
        // not exist. This logic is similar
        // to call. May be left nil. Nil means
        // not charged.
        CreateBySuicide uint64
    }

memorySize

因為加操作不需要申請記憶體因而memorySize為預設值0。

棧驗證

先驗證棧上的運算元夠不夠,再驗證棧是否超出最大限制,加法在這裡僅需驗證其引數夠不夠,運算之後棧是要減一的。

    funcmakeStackFunc(pop, push int)stackValidationFunc {
        return func(stack *Stack)error {
            //深度驗證
            if err := stack.require(pop); err != nil {
                return err
            }
            //最大值驗證
            //StackLimit       uint64 = 1024 
            if stack.len()+push-pop > int(params.StackLimit) {
                return fmt.Errorf("stack limit reached %d (%d)", stack.len(), params.StackLimit)
            }
            return nil
        }
    }

智慧合約

合約是EVM智慧合約的儲存單位也是直譯器執行的基本單位,包含了程式碼,呼叫人,所有人,gas相關的資訊.

    typeContract struct {
        // CallerAddressistheresultofthecallerwhichinitialisedthis
        // contract. Howeverwhenthe "callmethod" isdelegatedthisvalue
        // needstobeinitialisedtothatofthecaller'scaller.
        CallerAddresscommon.AddresscallerContractRefselfContractRefjumpdestsdestinations // resultofJUMPDESTanalysis.

        Code     []byteCodeHashcommon.HashCodeAddr *common.AddressInput    []byteGasuint64value *big.IntArgs []byteDelegateCallbool
    }

EVM原生預編譯了一批合約,定義在contracts.go裡面。主要用於加密操作。

// PrecompiledContractsByzantium contains the default set of pre-compiled Ethereum
// contracts used in the Byzantium release.
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{},
}

執行機

EVM中棧用於儲存運算元,每個運算元的型別是big.int,這就是網上很多人說EVM是256位虛擬機器的原因。執行opcode的時候,從上往下彈出運算元,作為操作的引數。

type Stack struct {
	data []*big.Int
}

func(st *Stack)push(d *big.Int) {
	// NOTE push limit (1024) is checked in baseCheck
	//stackItem := new(big.Int).Set(d)
	//st.data = append(st.data, stackItem)
	st.data = append(st.data, d)
}

func(st *Stack)peek() *big.Int {
	return st.data[st.len()-1]
}

func(st *Stack)pop()(ret *big.Int) {
	ret = st.data[len(st.data)-1]
	st.data = st.data[:len(st.data)-1]
	return
}

記憶體

記憶體用於一些記憶體操作(MLOAD,MSTORE,MSTORE8)及合約呼叫的引數拷貝(CALL,CALLCODE)。

記憶體資料結構,維護了一個byte陣列,MLOAD,MSTORE讀取存入的時候都要指定位置及長度才能準確的讀寫。

    type Memory struct {
        store       []byte
        lastGasCost uint64
    }

    // Set sets offset + size to value
    func(m *Memory)Set(offset, size uint64, value []byte) {
        // length of store may never be less than offset + size.
        // The store should be resized PRIOR to setting the memory
        if size > uint64(len(m.store)) {
            panic("INVALID memory: store empty")
        }

        // It's possible the offset is greater than 0 and size equals 0. This is because
        // the calcMemSize (common.go) could potentially return 0 when size is zero (NO-OP)
        if size > 0 {
            copy(m.store[offset:offset+size], value)
        }
    }

    func(self *Memory)Get(offset, size int64)(cpy []byte) {
        if size == 0 {
            return nil
        }

        if len(self.store) > int(offset) {
            cpy = make([]byte, size)
            copy(cpy, self.store[offset:offset+size])

            return
        }

        return
    }

記憶體操作

    funcopMload(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack)([]byte, error) {
        offset := stack.pop()
        val := evm.interpreter.intPool.get().SetBytes(memory.Get(offset.Int64(), 32))
        stack.push(val)

        evm.interpreter.intPool.put(offset)
        return nil, nil
    }

    funcopMstore(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack)([]byte, error) {
        // pop value of the stack
        mStart, val := stack.pop(), stack.pop()
        memory.Set(mStart.Uint64(), 32, math.PaddedBigBytes(val, 32))

        evm.interpreter.intPool.put(mStart, val)
        return nil, nil
    }

    funcopMstore8(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack)([]byte, error) {
        off, val := stack.pop().Int64(), stack.pop().Int64()
        memory.store[off] = byte(val & 0xff)

        return nil, nil
    }

stateDb

合約本身不儲存資料,那麼合約的資料是儲存在哪裡呢?合約及其呼叫類似於資料庫的日誌,儲存了合約定義以及對他的一系列操作,只要將這些操作執行一遍就能獲取當前的結果,但是如果每次都要去執行就太慢了,因而這部分資料是會持久化到stateDb裡面的。code中定義了兩條指令SSTORE SLOAD用於從db中讀寫合約當前的狀態。

    func opSload(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
        loc := common.BigToHash(stack.pop())
        val := evm.StateDB.GetState(contract.Address(), loc).Big()
        stack.push(val)
        return nil, nil
    }

    func opSstore(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
        loc := common.BigToHash(stack.pop())
        val := stack.pop()
        evm.StateDB.SetState(contract.Address(), loc, common.BigToHash(val))

        evm.interpreter.intPool.put(val)
        return nil, nil
    }

執行過程

執行入口定義在evm.go中,功能就是組裝執行環境(程式碼,執行人關係,引數等)。

    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
        }

        // 合約呼叫深度檢查
        if evm.depth > int(params.CallCreateDepth) {
            return nil, gas, ErrDepth
        }
        // balance 檢查
        if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
            return nil, gas, ErrInsufficientBalance
        }

        var (
            to       = AccountRef(addr)
            //儲存當前狀態,如果出錯,就回滾到這個狀態
            snapshot = evm.StateDB.Snapshot()
        )
        if !evm.StateDB.Exist(addr) {
            //建立呼叫物件的stateObject
            precompiles := PrecompiledContractsHomestead
            if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
                precompiles = PrecompiledContractsByzantium
            }
            if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
                return nil, gas, nil
            }
            evm.StateDB.CreateAccount(addr)
        }
        //呼叫別人合約可能需要花錢
        evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)

        //建立合約環境
        contract := NewContract(caller, to, value, gas)
        contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))

        start := time.Now()

        // Capture the tracer start/end events in debug mode
        if evm.vmConfig.Debug && evm.depth == 0 {
            evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)

            defer func()