go-ethereum中evm源码学习

首先关于EVM的基础知识情参考这里。另外推荐一个EVM操作码介绍网站

Stack

我们都知道EVM是基于栈结构的,我们这里先看一下他的栈实现。

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

首先结构体很简单,就是一个big.Int类型的数组。构造方法如下

func newstack() *Stack {
	return &Stack{data: make([]*big.Int, 0, 1024)}
}

构造方法就是初始化了一个长度为1024的数组,也就是栈最大深度是1024.

push & pop & peek & back

func (st *Stack) push(d *big.Int) {
	st.data = append(st.data, d)
}

func (st *Stack) pushN(ds ...*big.Int) {
	st.data = append(st.data, ds...)
}

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

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

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

push就向数组添加内容,pop就是取出数组最后一个数据,也就是栈顶元素。peek访问栈顶元素。Back是从栈顶开始取第n个元素

swap

func (st *Stack) swap(n int) {
	st.data[st.len()-n], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-n]
}

将栈顶元素和指定位置元素交换,对于虚拟机中的SWAP操作码

dup

func (st *Stack) dup(pool *intPool, n int) {
	st.push(pool.get().Set(st.data[st.len()-n]))
}

参数中intPool封装了一个stack对象,为的是实现big.Int复用。他的get方法就是获得栈顶元素,这里复用一个对象,将其设为栈中某个元素并放到栈顶,为的是复制元素到栈顶。

require

func (st *Stack) require(n int) error {
	if st.len() < n {
		return fmt.Errorf("stack underflow (%d <=> %d)", len(st.data), n)
	}
	return nil
}

这个方法作用是检查是否能取出要求个数的元素

stack_table

这里面放着检测栈的几个方法,这几个方法都是用在后面定义operation时的validateStack字段

makeStackFunc

func makeStackFunc(pop, push int) stackValidationFunc {
	return func(stack *Stack) error {
		if err := stack.require(pop); err != nil {
			return err
		}

		if stack.len()+push-pop > int(params.StackLimit) {
			return fmt.Errorf("stack limit reached %d (%d)", stack.len(), params.StackLimit)
		}
		return nil
	}
}

这个方法节后两个变量,第一个表示要执行几次pop操作,第二个表示要执行几次push操作,这个方法目的是检查一个栈是否能执行这样一组pop和push操作。首先调用require检测是否能pop对应数量的元素。然后在检查在pop与push操作之后栈是否超过最大深度。

makeDupStackFunc

func makeDupStackFunc(n int) stackValidationFunc {
	return makeStackFunc(n, n+1)
}

这个表示复制操作的检测,就是要pop出n个值,然后push进n+1个值

makeSwapStackFunc

func makeSwapStackFunc(n int) stackValidationFunc {
	return makeStackFunc(n, n)
}

这是检查交换操作的,就是看能否弹出以及压入n个值。

intPool

前面见过这种类型,它内部封装了一个栈对象,为的是存储复用bigInt对象

type intPool struct {
	pool *Stack
}

func newIntPool() *intPool {
	return &intPool{pool: newstack()}
}

get

func (p *intPool) get() *big.Int {
	if p.pool.len() > 0 {
		return p.pool.pop()
	}
	return new(big.Int)
}

这是取出一个bigInt对象供我们使用,不必每次都重新创建对象,但是前提是intPool中有数据,不然还是需要创建

func (p *intPool) getZero() *big.Int {
	if p.pool.len() > 0 {
		return p.pool.pop().SetUint64(0)
	}
	return new(big.Int)
}

特别的给我们提供了值为0的bigInt对象获取方法

put

func (p *intPool) put(is ...*big.Int) {
	if len(p.pool.data) > poolLimit {
		return
	}
	for _, i := range is {
		if verifyPool {
			i.Set(checkVal)
		}
		p.pool.push(i)
	}
}

首先不能超过pool的最大容量–256.其次根据需要在存入前将每个值设为默认值:-42

intPoolPool

这是intPool的缓存池,最大容量为25,

type intPoolPool struct {
	pools []*intPool
	lock  sync.Mutex
}

var poolOfIntPools = &intPoolPool{
	pools: make([]*intPool, 0, poolDefaultCap),
}

get与put方法都很简单,这里不再详述

memory

这个实现了EVM的内存模型

type Memory struct {
	store       []byte
	lastGasCost uint64
}

func NewMemory() *Memory {
	return &Memory{}
}

包含了一个字节数组,用于存储东西,一个整数存储gas的消耗。

Resize

func (m *Memory) Resize(size uint64) {
	if uint64(m.Len()) < size {
		m.store = append(m.store, make([]byte, size-uint64(m.Len()))...)
	}
}

func (m *Memory) Len() int {
	return len(m.store)
}

使用Resize空间,注意这个方法设计的可以在使用过程中动态调整内存空间,只有当要赋予的空间大于已有的时,在对store数组进行扩展,其中的数据保持不变。

Set

func (m *Memory) Set(offset, size uint64, value []byte) {

	if size > 0 {
		if offset+size > uint64(len(m.store)) {
			panic("invalid memory: store empty")
		}
		copy(m.store[offset:offset+size], value)
	}
}

由于是模仿内存模型,所以存储数据时要指定偏移量,数据长度以及数据本体。首先检查是否能存下,可以的话按照给定的偏移即长度存储数据。

func (m *Memory) Set32(offset uint64, val *big.Int) {

	if offset+32 > uint64(len(m.store)) {
		panic("invalid memory: store empty")
	}

	copy(m.store[offset:offset+32], []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})

	math.ReadBits(val, m.store[offset:offset+32])
}

由于EVM的栈宽度是32字节,所以特别给出了存储一个32字节数据的方法,第一步还是检查是否放的下,然后先存储32字节的灵数据,这是因为我们要存储的是bigInt类型,先用0占位后,然后再用math包中的ReadBits方法写入字节数据。

Get

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

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

		return
	}

	return
}

同样也是根据偏移量和大小取一组字节数据,但这个方法是获得数据副本

func (m *Memory) GetPtr(offset, size int64) []byte {
	if size == 0 {
		return nil
	}

	if len(m.store) > int(offset) {
		return m.store[offset : offset+size]
	}

	return nil
}

这个方法则是获取数据的引用,可以修改元数据

memory_table.go

这里面的方法是获取某个操作会占用的memory大小,如

func memorySha3(stack *Stack) *big.Int {
	return calcMemSize(stack.Back(0), stack.Back(1))
}

func calcMemSize(off, l *big.Int) *big.Int {
	if l.Sign() == 0 {
		return common.Big0
	}

	return new(big.Int).Add(off, l)
}

这个方法对于sha3操作,他需要从栈中弹出两个值,第一个值是在memory中的偏移量,第二个值是数据的长度,所以memorySha3调用了calcMemSize,第一个参数就是栈顶元素,第二个参数就是栈顶倒数第二个参数,该操作所需要的memory最小长度是offset+length。

jump_table.go

这里面定义了operation,表示一个个操作符

type operation struct {

	execute executionFunc
	gasCost gasFunc
	validateStack stackValidationFunc
	memorySize memorySizeFunc

	halts   bool 
	jumps   bool 
	writes  bool 
	valid   bool 
	reverts bool 
	returns bool 
}

解释一下每个成员的含义:

  • execute:表示要执行的函数
  • gasCost:表示gas消耗函数
  • validateStack:表示堆栈大小验证函数
  • memorySize:表示需要的内存大小
  • halts:表示是否停止
  • jumps:表示程序计数器是否不应该递增
  • writes:表示是否是一个状态修改操作
  • valid:表示检索到的操作是否有效且已知
  • reverts:确定操作是否已经恢复状态
  • returns:表示操作是否设置了返回内容

EVM最多有256个操作符,但许多都未定义。不同的版本有不同的操作符集合

var (
	frontierInstructionSet       = newFrontierInstructionSet()
	homesteadInstructionSet      = newHomesteadInstructionSet()
	byzantiumInstructionSet      = newByzantiumInstructionSet()
	constantinopleInstructionSet = newConstantinopleInstructionSet()
)

基本上都是新版本包含了旧版本的操作符,同时又添加了新的操作符。

instruction.go

jump_table中定义了全部的operation,每个operation的execute实现在这里定义,如:

opAdd

func opAdd(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
	x, y := stack.pop(), stack.peek()
	math.U256(y.Add(x, y))

	interpreter.intPool.put(x)
	return nil, nil
}

这是基于栈的加法,首先弹出栈顶元素x,然后获取新的栈顶元素y(该元素并不出栈),然后将x,并赋值到y,最后结果用补码表示。还有一点是,刚才出栈的数在用完后放入intPool中以便后面进行复用。

opLt

func opLt(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
	x, y := stack.pop(), stack.peek()
	if x.Cmp(y) < 0 {
		y.SetUint64(1)
	} else {
		y.SetUint64(0)
	}
	interpreter.intPool.put(x)
	return nil, nil
}

这是小于比较的方法,也是先出栈一个元素x,再获取栈顶元素,然后二者相减进行比较,结果写入y,最后x放入intPool复用。

opCallDataLoad

func opCallDataLoad(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
	stack.push(interpreter.intPool.get().SetBytes(getDataBig(contract.Input, stack.pop(), big32)))
	return nil, nil
}

func getDataBig(data []byte, start *big.Int, size *big.Int) []byte {
	dlen := big.NewInt(int64(len(data)))

	s := math.BigMin(start, dlen)
	e := math.BigMin(new(big.Int).Add(s, size), dlen)
	return common.RightPadBytes(data[s.Uint64():e.Uint64()], int(size.Uint64()))
}

func RightPadBytes(slice []byte, l int) []byte {
	if l <= len(slice) {
		return slice
	}

	padded := make([]byte, l)
	copy(padded, slice)

	return padded
}

这是一个常用的获取输入参数的操作符,注意看,第一步利用getDataBig从合约的输入中从某个偏移开始读取32字节输入数据,有时未必能读够32字节,这时还需要RightPadBytes方法在右侧补零。获取完数据后放入栈顶

opMstore

func opMstore(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
	mStart, val := stack.pop(), stack.pop()
	memory.Set32(mStart.Uint64(), val)

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

从栈顶获取代表偏移量和数据的两个值,然后放入内存模型中。

opSstore

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

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

这个也就是我们说的持久化存储操作符,可见他实际存储到状态树中,最后放入数据库。其存储位置就是键。

opJump

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
}

这是程序跳转操作,首先从栈顶获取跳转的位置,然后验证是否是有效的跳转目的地,是的话修改程序计数器实现跳转,否则返回错误。

gas_table.go

这里面定义了每种操作符的gas消耗。在jump_table中定义operation的gasCost字段时定义了gas消耗计算方法。对于一些基础操作都是constGasFunc方法

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
	}
}

这里指定的gas就是该操作消耗的gas,对于一些基本操作符如ADD,SUB等都是消耗固定的gas,一般分以下几种

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
)

可见基本是根据步骤执行快慢决定的,如调用ADDRESS、ORIGIN都消耗GasQuickStep也就是2Gas。ADD、SUB等都是GasFastestStep。

除此之外,还有许多操作符是单独定义了,如MSTORE

func gasMStore(gt params.GasTable, evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
	var overflow bool
	gas, err := memoryGasCost(mem, memorySize)
	if err != nil {
		return 0, errGasUintOverflow
	}
	if gas, overflow = math.SafeAdd(gas, GasFastestStep); overflow {
		return 0, errGasUintOverflow
	}
	return gas, nil
}

func memoryGasCost(mem *Memory, newMemSize uint64) (uint64, error) {

	if newMemSize == 0 {
		return 0, nil
	}

	if newMemSize > 0xffffffffe0 {
		return 0, errGasUintOverflow
	}

	newMemSizeWords := toWordSize(newMemSize)
	newMemSize = newMemSizeWords * 32

	if newMemSize > uint64(mem.Len()) {
		square := newMemSizeWords * newMemSizeWords
		linCoef := newMemSizeWords * params.MemoryGas
		quadCoef := square / params.QuadCoeffDiv
		newTotalFee := linCoef + quadCoef

		fee := newTotalFee - mem.lastGasCost
		mem.lastGasCost = newTotalFee

		return fee, nil
	}
	return 0, nil
}

对于从memory中读取数据的操作,主要是根据要取的数据大小计算gas,借助了memoryGasCost方法。

memoryGasCost中,首先如果要取的数据为0,则直接返回0。另外如果大于0xffffffffe0则报错。对于正常情况,首先按32字节一个字计算,看有多少个字,借助toWordSize方法(相当于除以32并向上取整),之后再乘以32,表示按32字节一个字取的话要多少字节。之后如果取值大于memory的长度,则首先按每个字3Gas计算出linCoef,然后对字数取平方在除以512计算出quadCoef。此时总费用newTotalFee就是linCoef + quadCoef,但是还要减去memory的lastGasCost字段,最后将lastGasCost字段更新为newTotalFee。如果计算出的实际字节数没有超过memory长度则直接返回零。

回到gasMStore中,如果前一步没有错,则实际费用就是memoryGasCost返回值加上基础费用3。这里我们也可以知道如果从memory取值时如果没有超出当前memory返回,费用只有基础费用3Gas

Contract

这就是表示一个合约的对象

type Contract struct {
	CallerAddress common.Address
	caller        ContractRef
	self          ContractRef

	jumpdests map[common.Hash]bitvec 
	analysis  bitvec                

	Code     []byte
	CodeHash common.Hash
	CodeAddr *common.Address
	Input    []byte

	Gas   uint64
	value *big.Int
}

包含了合约创建者地址,合约地址,合约代码以及输入等许多基本要素

NewContract

func NewContract(caller ContractRef, object ContractRef, value *big.Int, gas uint64) *Contract {
	c := &Contract{CallerAddress: caller.Address(), caller: caller, self: object}

	if parent, ok := caller.(*Contract); ok {
		c.jumpdests = parent.jumpdests
	} else {
		c.jumpdests = make(map[common.Hash]bitvec)
	}

	c.Gas = gas
	c.value = value

	return c
}

构造方法最后返回一个Contract对象,需要传入创建者的地址,如果创建者是一个合约的话,则继承其的jumpdests。

interpreter

前面在instruction中每个操作符函数参数中都有一个interpreter参数,这是一个接口表示解释器,解释器是用来执行智能合约的

type Interpreter interface {
	Run(contract *Contract, input []byte, static bool) ([]byte, error)

	CanRun([]byte) bool
}

实际使用的EVMInterpreter

type EVMInterpreter struct {
	evm      *EVM
	cfg      Config
	gasTable params.GasTable

	intPool *intPool

	hasher    keccakState 
	hasherBuf common.Hash 

	readOnly   bool  
	returnData []byte 
}

构造函数如下

func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter {
	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,CanRun
		gasTable: evm.ChainConfig().GasTable(evm.BlockNumber),
	}
}

首先根据JumpTable的STOP指令的有效性判断JumpTable是否初始化,没有的话根据具体版本加载不同的指令集。然后新建EVMInterpreter对象。

CanRun

func (in *EVMInterpreter) CanRun(code []byte) bool {
	return true
}

这个方法作用是检查代码是否可以运行,但是这里直接返回true,表示都可以执行。

Run

这是运行合约的核心方法

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
		}()
	}

	in.evm.depth++
	defer func() { in.evm.depth-- }()

	if readOnly && !in.readOnly {
		in.readOnly = true
		defer func() { in.readOnly = false }()
	}

	in.returnData = nil

	if len(contract.Code) == 0 {
		return nil, nil
	}

	var (
		op    OpCode        
		mem   = NewMemory() 
		stack = newstack()  

		pc   = uint64(0) 
		cost uint64
		pcCopy  uint64 
		gasCopy uint64 
		logged  bool   
		res     []byte 
	)
	contract.Input = input

	defer func() { in.intPool.put(stack.data...) }()

	if in.cfg.Debug {
		defer func() {
			if err != nil {
				if !logged {
					in.cfg.Tracer.CaptureState(in.evm, pcCopy, op, gasCopy, cost, mem, stack, contract, in.evm.depth, err)
				} else {
					in.cfg.Tracer.CaptureFault(in.evm, pcCopy, op, gasCopy, cost, mem, stack, contract, in.evm.depth, err)
				}
			}
		}()
	}

	for atomic.LoadInt32(&in.evm.abort) == 0 {
		if in.cfg.Debug {
			logged, pcCopy, gasCopy = false, pc, contract.Gas
		}

		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
		}

		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
			}
		}
		cost, err = operation.gasCost(in.gasTable, in.evm, contract, stack, mem, memorySize)
		if err != nil || !contract.UseGas(cost) {
			return nil, ErrOutOfGas
		}
		if memorySize > 0 {
			mem.Resize(memorySize)
		}

		if in.cfg.Debug {
			in.cfg.Tracer.CaptureState(in.evm, pc, op, gasCopy, cost, mem, stack, contract, in.evm.depth, err)
			logged = true
		}


		res, err = operation.execute(&pc, in, contract, mem, stack)
		if verifyPool {
			verifyIntegerPool(in.intPool)
		}
		if operation.returns {
			in.returnData = res
		}

		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
}

第一步如果intPool为空,则从poolOfIntPools中取一个,poolOfIntPools是intPoolPool类型,就是intPool的缓存池。这里还定义了一个defer,是在run执行完之后回收解释器的intPool

接着调用栈深度加一,也就是每执行一次Run方法调用栈深度加一。但是调用结束后会减一。接着初始化一些变量用于执行代码,如栈、内存模型、程序计数器等。然后将输入赋给合约的Input字段。中间有一步,如果传入的合约对象的代码为空的话则直接结束,对应evm的call方法中指定的合约地址不存在的情况。

再往下开始了一个循环,每次都要先判断evm的abort字段是否为0,为1的话要终止操作。进入循环,首先通过合约的GetOp方法获取pc所指的操作符,再根据操作符获取JumpTable对应的operation对象。获得operation后,先检查有效性,再检测当前栈的大小是否符合操作符要求,利用的validateStack方法。如果通过检查,则调用enforceRestrictions方法

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
}

这个主要是针对拜占庭版本下只读模式时,操作符是否进行写操作,在evm的StaticCall的方法会进行只读操作,是的话触发写保护错误。

一切正常的话,检查memory是否符合操作符要求,主要借助operation的memorySize获取期望的memory大小。接着又计算了gas的消耗,利用contract.UseGas检查gas是否够用。经过上面的检测,都通过的话,先对memory大小进行调整,然后执行操作符的execute方法,得到返回值和错误信息。之后,如果操作符需要返回值,则将刚才执行完后的返回值res赋给解释器的returnData字段。

最后还有一个switch字段,根据operation的一些条件返回不同的值。这样一次循环结束,接着下一次循环,根据pc执行下一个操作符。

evm

这个就是EVM的对象,他主要是运行智能合约的。

type EVM struct {

	Context

	StateDB StateDB

	depth int

	chainConfig *params.ChainConfig

	chainRules params.Rules
.
	vmConfig Config

	interpreters []Interpreter
	interpreter  Interpreter

	abort int32

	callGasTemp uint64
}

其中Context提供了区块链的相关上下文信息。StateDB是一个接口,实际上就是state包中stateDB,用于对合约账户操作。

type Context struct {
	CanTransfer CanTransferFunc
	Transfer TransferFunc
	GetHash GetHashFunc

	Origin   common.Address 
	GasPrice *big.Int      

	Coinbase    common.Address 
	GasLimit    uint64        
	BlockNumber *big.Int       
	Time        *big.Int       
	Difficulty  *big.Int      
}

NewEVM

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.")
	}

	evm.interpreters = append(evm.interpreters, NewEVMInterpreter(evm, vmConfig))
	evm.interpreter = evm.interpreters[0]

	return evm
}

主要是对evm的成员进行赋值。chainConfig.Rules方法主要是记录区块链的ID和以太坊版本。之后新建了一个解释器。

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)
}

func CreateAddress(b common.Address, nonce uint64) common.Address {
	data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})
	return common.BytesToAddress(Keccak256(data)[12:])
}

这是创建一个合约的方法,在StateTransition处理交易时,如果接收人地址为空则调用该方法创建合约。传入的参数中caller是交易发起人的地址(ContractRef类型)、code是交易中附带的数据、gas是交易最大gas量减去固有费用后剩余的gas、value是交易中需要交易的金额。

首先第一步构造合约地址,这里我们知道合约地址实际上就是发起者的地址拼接上发起者的nonce经过rlp编码后再做keccak256散列后取后20字节。接下来调用了create方法

func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address) ([]byte, common.Address, uint64, error) {
	if evm.depth > int(params.CallCreateDepth) {
		return nil, common.Address{}, gas, ErrDepth
	}
	if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
		return nil, common.Address{}, gas, ErrInsufficientBalance
	}
	nonce := evm.StateDB.GetNonce(caller.Address())
	evm.StateDB.SetNonce(caller.Address(), nonce+1)

	contractHash := evm.StateDB.GetCodeHash(address)
	if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
		return nil, common.Address{}, 0, ErrContractAddressCollision
	}

	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)


	contract := NewContract(caller, AccountRef(address), value, gas)
	contract.SetCodeOptionalHash(&address, codeAndHash)

	if evm.vmConfig.NoRecursion && evm.depth > 0 {
		return nil, address, gas, nil
	}

	if evm.vmConfig.Debug && evm.depth == 0 {
		evm.vmConfig.Tracer.CaptureStart(caller.Address(), address, true, codeAndHash.code, gas, value)
	}
	start := time.Now()

	ret, err := run(evm, contract, nil, false)

	maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) && len(ret) > params.MaxCodeSize
	if err == nil && !maxCodeSizeExceeded {
		createDataGas := uint64(len(ret)) * params.CreateDataGas
		if contract.UseGas(createDataGas) {
			evm.StateDB.SetCode(address, ret)
		} else {
			err = ErrCodeStoreOutOfGas
		}
	}

	if maxCodeSizeExceeded || (err != nil && (evm.ChainConfig().IsHomestead(evm.BlockNumber) || err != ErrCodeStoreOutOfGas)) {
		evm.StateDB.RevertToSnapshot(snapshot)
		if err != errExecutionReverted {
			contract.UseGas(contract.Gas)
		}
	}

	if maxCodeSizeExceeded && err == nil {
		err = errMaxCodeSizeExceeded
	}
	if evm.vmConfig.Debug && evm.depth == 0 {
		evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
	}
	return ret, address, contract.Gas, err

}

首先检测调用深度不得超过1024。之后利用CanTransfer检测账户余额是否足够

func CanTransfer(db vm.StateDB, addr common.Address, amount *big.Int) bool {
	return db.GetBalance(addr).Cmp(amount) >= 0
}

接着获取了发起者账户的Nonce,之后将其Nonce自增一。接下来检测该合约是否被创建过。一切检测完毕后,创建一个StateDB的快照。

接下来首先调用StateDB的CreateAccount,在statedb中的表现是创建了一个stateObject并记录相应事件,这个stateObject就代表新建的合约账户。接着如果处于EIP158版本,则将合约账户nonce初始化为1。再往下执行了Transfer方法。

func Transfer(db vm.StateDB, sender, recipient common.Address, amount *big.Int) {
	db.SubBalance(sender, amount)
	db.AddBalance(recipient, amount)
}

到此处表示合约账户创建成功了,此时要将创建人的账户减去他给合约发送的金额,而合约账户要相应的增加对应的金额。

接着调用NewContract创建一个新的合约对象,并利用SetCodeOptionalHash设置合约代码以代码hash值等数据。此时如果配置了NoRecursion(也就是不允许解释器执行call、callcode、delegate call和create)并且调用深度大于0,则直接返回。否则如果调用深度为0但是Debug模式则开始抓取信息。接下来运行run方法进行合约初始化,返回值ret是合约的代码。之后检查代码是否超过最大长度(24567)。如果没有的话存储代码,首先计算存储代码所需的gas,每字节200Gas。然后调用Contract的UseGas方法判断gas是否够用,够用的话使用statedb存储代码。如果前面代码过长或者Gas不足则回滚。最后方法结束,返回合约代码,合约地址以及合约剩余Gas。

除了Create方法可以创建合约外,还有一个Create2方法,唯一区别就是合约账户地址的生成不一样,

func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment *big.Int, salt *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
	codeAndHash := &codeAndHash{code: code}
	contractAddr = crypto.CreateAddress2(caller.Address(), common.BigToHash(salt), codeAndHash.Hash().Bytes())
	return evm.create(caller, codeAndHash, gas, endowment, contractAddr)
}

func CreateAddress2(b common.Address, salt [32]byte, inithash []byte) common.Address {
	return common.BytesToAddress(Keccak256([]byte{0xff}, b.Bytes(), salt[:], inithash)[12:])
}

Create2中地址为keccak256(0xff + 发起人地址 + salt + 代码的hash值),取最后20字节。

Call

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
	}

	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) {
		precompiles := PrecompiledContractsHomestead
		if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
			precompiles = PrecompiledContractsByzantium
		}
		if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
			if evm.vmConfig.Debug && evm.depth == 0 {
				evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
				evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil)
			}
			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()

	if evm.vmConfig.Debug && evm.depth == 0 {
		evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)

		defer func() { 
			evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
		}()
	}
	ret, err = run(evm, contract, input, false)

	if err != nil {
		evm.StateDB.RevertToSnapshot(snapshot)
		if err != errExecutionReverted {
			contract.UseGas(contract.Gas)
		}
	}
	return ret, contract.Gas, err
}

与Create方法类似,这个方法在一般交易中或者调用合约是会用到。在StateTransition中如果是一般交易的话会调用该方法。传入的参数依次是交易的发起人,交易的接受者,交易附带数据(这里解释为调用合约的输入值),交易剩余的可用gas,交易传输的金额。

开始时也是先进行检测,第一步如果配置了NoRecursion且调用深度大于0,则直接返回。第二如果调用深度大于1024报错返回。第三步检测调用账户余额是否足够。

之后记录交易的目标地址以及创建快照。接着使用stateDB的Exist方法判断该地址是否存在,如果不存在,也就说明该账户尚未创建,则首先加载一系列预编译的代码,然后检查所谓的目标地址是否和这些预编译的代码编号对应,如果对应不上且是EIP158版本且发送的value为0,则返回,本次没有实际的调用。

否则,则是前面的几种情况有满足的,调用StateDB的CreateAccount创建一个账户对应的stateObject。接下来调用Transfer转移金额。再往下创建一个新的合约对象并设置代码及代码hash。这里只是创建一个Contract对象,提供后面执行的上下文环境,并没有实际的创建合约账户,而且Contract的代码字段也仅仅是通过给定的合约地址获取的,如果刚才这个合约地址不存在的话,这个GetCode方法取到的只能为nil,当code为空时后面的run方法会自动退出,这时表示对一个账户单纯的转账。

接着如果是debug模式而且调用深度为0,开始捕获信息。然后调用run方法执行,此时有输入信息,如果前面创建的合约对象code不为空的话。这里就表示调用合约的代码。执行完之后,如果有错则进行回滚。

Callcode

func (evm *EVM) CallCode(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
	}

	if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
		return nil, gas, ErrInsufficientBalance
	}

	var (
		snapshot = evm.StateDB.Snapshot()
		to       = AccountRef(caller.Address())
	)

	contract := NewContract(caller, to, value, gas)
	contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))

	ret, err = run(evm, contract, input, false)
	if err != nil {
		evm.StateDB.RevertToSnapshot(snapshot)
		if err != errExecutionReverted {
			contract.UseGas(contract.Gas)
		}
	}
	return ret, contract.Gas, err
}

这个方法和call类似,最大的区别是使用的是调用者的上下文,体现在创建Contract对象时,第二个参数to是一个用AccountRef包装的调用者地址,这个参数最后影响Contract对象的self字段,表示的是合约的地址,这样设置为调用者的地址后也就相当于调用者自己。回顾call,那里面的to参数就是合约的地址。最后还有一点,call方法会给目标地址转移金额,而callcode不会。

DelegateCall

func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (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
	}

	var (
		snapshot = evm.StateDB.Snapshot()
		to       = AccountRef(caller.Address())
	)

	contract := NewContract(caller, to, nil, gas).AsDelegate()
	contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))

	ret, err = run(evm, contract, input, false)
	if err != nil {
		evm.StateDB.RevertToSnapshot(snapshot)
		if err != errExecutionReverted {
			contract.UseGas(contract.Gas)
		}
	}
	return ret, contract.Gas, err
}

func (c *Contract) AsDelegate() *Contract {
	parent := c.caller.(*Contract)
	c.CallerAddress = parent.CallerAddress
	c.value = parent.value

	return c
}

这个和callcode方法类似,唯一的区别还是创建合约的形式不同,这里和callcode一样将Contract对象的self字段设为调用者地址,另外还使用AsDelegate对合约进行转化。主要修改的是这个合约的调用者不再是DelegateCall传入的调用者,而是DelegateCall方法给的调用者的调用者。注意这里有两层关系,这样创建出的合约实际上调用者类似,自己的地址是caller,调用者则是caller.CallerAddress。这就相当于将两个合约合为一体,效果就类似将另一个合约的代码拿来放在自己合约内执行,当然上下文环境也是自己的。

实际上call、callcode、delegatecall这三个方法就相当于solidity中对应名称的三个低级调用。

StaticCall

func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (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
	}

	var (
		to       = AccountRef(addr)
		snapshot = evm.StateDB.Snapshot()
	)

	contract := NewContract(caller, to, new(big.Int), gas)
	contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))

	evm.StateDB.AddBalance(addr, bigZero)

	ret, err = run(evm, contract, input, true)
	if err != nil {
		evm.StateDB.RevertToSnapshot(snapshot)
		if err != errExecutionReverted {
			contract.UseGas(contract.Gas)
		}
	}
	return ret, contract.Gas, err
}

这类似于前面的call方法,但是他不允许修改状态,原因是调用run方法时最后一个readOnly变量为true,而前几个都为false。

run

evm中多次调用了该方法,这是运行给定合约

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 {
				defer func(i Interpreter) {
					evm.interpreter = i
				}(evm.interpreter)
				evm.interpreter = interpreter
			}
			return interpreter.Run(contract, input, readOnly)
		}
	}
	return nil, ErrNoCompatibleInterpreter
}

第一步如果合约中的CodeAddr不为空,则先加载预编译的代码,然后看合约地址是否是这些预编译代码的编号,是的话执行对应的预编译代码

func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contract) (ret []byte, err error) {
	gas := p.RequiredGas(input)
	if contract.UseGas(gas) {
		return p.Run(input)
	}
	return nil, ErrOutOfGas
}

基本就是调用了这些预编译合约的RequiredGas和Run方法,一个获取所需的gas,一个执行逻辑。我们以最简单的dataCopy为例

type dataCopy struct{}

func (c *dataCopy) RequiredGas(input []byte) uint64 {
	return uint64(len(input)+31)/32*params.IdentityPerWordGas + params.IdentityBaseGas
}
func (c *dataCopy) Run(in []byte) ([]byte, error) {
	return in, nil
}

它的RequiredGas就是根据输入数据的长度按每32字节一个字(向上取整)消耗3Gas在加上基础消耗15Gas计算。Run方法则是直接返回输入数据,实现copy功能。

类似的还有sha256hash,实现SHA256计算

type sha256hash struct{}

func (c *sha256hash) RequiredGas(input []byte) uint64 {
	return uint64(len(input)+31)/32*params.Sha256PerWordGas + params.Sha256BaseGas
}
func (c *sha256hash) Run(input []byte) ([]byte, error) {
	h := sha256.Sum256(input)
	return h[:], nil
}

还回到run中,如果不是预编译的代码,则遍历evm中的所有解释器,对每个解释器执行canRun方法,检查是否可以执行,可以的话则调用解释器的Run方法去执行,并返回结果。Run方法参见前面解释器的源码分析。

题图来自unsplash:https://unsplash.com/photos/X0OoHrPvgXE