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