go-ethereum core之以太网虚拟机EVM
简介
以太网虚拟机执行合约代码
结构
type EVM struct {// Context provides auxiliary blockchain related informationContext BlockContextTxContext// StateDB gives access to the underlying stateStateDB StateDB// table holds the opcode specific handlerstable *JumpTable// depth is the current call stackdepth int// chainConfig contains information about the current chainchainConfig *params.ChainConfig// chain rules contains the chain rules for the current epochchainRules params.Rules// virtual machine configuration options used to initialise the evmConfig Config// abort is used to abort the EVM calling operationsabort atomic.Bool// callGasTemp holds the gas available for the current call. This is needed because the// available gas is calculated in gasCall* according to the 63/64 rule and later// applied in opCall*.callGasTemp uint64// precompiles holds the precompiled contracts for the current epochprecompiles map[common.Address]PrecompiledContract// jumpDests stores results of JUMPDEST analysis.jumpDests JumpDestCachehasher crypto.KeccakState // Keccak256 hasher instance shared across opcodeshasherBuf common.Hash // Keccak256 hasher result array shared across opcodesreadOnly bool // Whether to throw on stateful modificationsreturnData []byte // Last CALL's return data for subsequent reuse
}
table *JumpTable:指令操作表,类型为JumpTable
type JumpTable [256]*operationtype operation struct {// execute is the operation functionexecute executionFuncconstantGas uint64dynamicGas gasFunc// minStack tells how many stack items are requiredminStack int// maxStack specifies the max length the stack can have for this operation// to not overflow the stack.maxStack int// memorySize returns the memory size required for the operationmemorySize memorySizeFunc// undefined denotes if the instruction is not officially defined in the jump tableundefined bool
}type executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error
虚拟机创建
通过NewEVM创建虚拟机,虚拟机的table指令集依赖chainRules
func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainConfig, config Config) *EVM {evm := &EVM{Context: blockCtx,StateDB: statedb,Config: config,chainConfig: chainConfig,chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time),jumpDests: newMapJumpDests(),hasher: crypto.NewKeccakState(),}evm.precompiles = activePrecompiledContracts(evm.chainRules)switch {case evm.chainRules.IsOsaka:evm.table = &osakaInstructionSetcase evm.chainRules.IsVerkle:// TODO replace with proper instruction set when fork is specifiedevm.table = &verkleInstructionSetcase evm.chainRules.IsPrague:evm.table = &pragueInstructionSetcase evm.chainRules.IsCancun:evm.table = &cancunInstructionSetcase evm.chainRules.IsShanghai:evm.table = &shanghaiInstructionSetcase evm.chainRules.IsMerge:evm.table = &mergeInstructionSetcase evm.chainRules.IsLondon:evm.table = &londonInstructionSetcase evm.chainRules.IsBerlin:evm.table = &berlinInstructionSetcase evm.chainRules.IsIstanbul:evm.table = &istanbulInstructionSetcase evm.chainRules.IsConstantinople:evm.table = &constantinopleInstructionSetcase evm.chainRules.IsByzantium:evm.table = &byzantiumInstructionSetcase evm.chainRules.IsEIP158:evm.table = &spuriousDragonInstructionSetcase evm.chainRules.IsEIP150:evm.table = &tangerineWhistleInstructionSetcase evm.chainRules.IsHomestead:evm.table = &homesteadInstructionSetdefault:evm.table = &frontierInstructionSet}var extraEips []intif len(evm.Config.ExtraEips) > 0 {// Deep-copy jumptable to prevent modification of opcodes in other tablesevm.table = copyJumpTable(evm.table)}for _, eip := range evm.Config.ExtraEips {if err := EnableEIP(eip, evm.table); err != nil {// Disable it, so caller can check if it's activated or notlog.Error("EIP activation failed", "eip", eip, "error", err)} else {extraEips = append(extraEips, eip)}}evm.Config.ExtraEips = extraEipsreturn evm
}
合约调用
调用函数有Call, CallCode,DelegateCall,StaticCall
Call步聚为
- 如果合约代码已经预编译,则调用RunPrecompiledContract
- 没有预编译,先解析地址对应的合约代码,创建合约,设置合约的code,调用evm.run执行合约
func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) {// Capture the tracer start/end events in debug modeif evm.Config.Tracer != nil {evm.captureBegin(evm.depth, CALL, caller, addr, input, gas, value.ToBig())defer func(startGas uint64) {evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err)}(gas)}// Fail if we're trying to execute above the call depth limitif evm.depth > int(params.CallCreateDepth) {return nil, gas, ErrDepth}// Fail if we're trying to transfer more than the available balanceif !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) {return nil, gas, ErrInsufficientBalance}snapshot := evm.StateDB.Snapshot()p, isPrecompile := evm.precompile(addr)if !evm.StateDB.Exist(addr) {if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) {// Add proof of absence to witness// At this point, the read costs have already been charged, either because this// is a direct tx call, in which case it's covered by the intrinsic gas, or because// of a CALL instruction, in which case BASIC_DATA has been added to the access// list in write mode. If there is enough gas paying for the addition of the code// hash leaf to the access list, then account creation will proceed unimpaired.// Thus, only pay for the creation of the code hash leaf here.wgas := evm.AccessEvents.CodeHashGas(addr, true, gas, false)if gas < wgas {evm.StateDB.RevertToSnapshot(snapshot)return nil, 0, ErrOutOfGas}gas -= wgas}if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() {// Calling a non-existing account, don't do anything.return nil, gas, nil}evm.StateDB.CreateAccount(addr)}evm.Context.Transfer(evm.StateDB, caller, addr, value)if isPrecompile {ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer)} else {// Initialise a new contract and set the code that is to be used by the EVM.code := evm.resolveCode(addr)if len(code) == 0 {ret, err = nil, nil // gas is unchanged} else {// The contract is a scoped environment for this execution context only.contract := NewContract(caller, addr, value, gas, evm.jumpDests)contract.IsSystemCall = isSystemCall(caller)contract.SetCallCode(evm.resolveCodeHash(addr), code)ret, err = evm.Run(contract, input, false)gas = contract.Gas}}// 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 {if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution)}gas = 0}// TODO: consider clearing up unused snapshots://} else {// evm.StateDB.DiscardSnapshot(snapshot)}return ret, gas, err
}
执行预编译合约RunPrecompiledContract
执行步骤为
- 先调用PrecompiledContract接口中的RequiredGas判断要求的gas
- 执行PrecompiledContract接口中的Run
func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64, logger *tracing.Hooks) (ret []byte, remainingGas uint64, err error) {gasCost := p.RequiredGas(input)if suppliedGas < gasCost {return nil, 0, ErrOutOfGas}if logger != nil && logger.OnGasChange != nil {logger.OnGasChange(suppliedGas, suppliedGas-gasCost, tracing.GasChangeCallPrecompiledContract)}suppliedGas -= gasCostoutput, err := p.Run(input)return output, suppliedGas, err
}
执行合约代码Run
执行步聚为
- 根据程序计数器(pc)从合约中的code得到操作码
- 根据操作码从JumpTable(evm.table)指令集中得到操作
- 执行操作operation.exeute,其依赖创建的本地栈ScopeContext{Memory, Stack, Contract}
func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {// Increment the call depth which is restricted to 1024evm.depth++defer func() { evm.depth-- }()// Make sure the readOnly is only set if we aren't in readOnly yet.// This also makes sure that the readOnly flag isn't removed for child calls.if readOnly && !evm.readOnly {evm.readOnly = truedefer func() { evm.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.evm.returnData = nil// Don't bother with the execution if there's no code.if len(contract.Code) == 0 {return nil, nil}var (op OpCode // current opcodejumpTable *JumpTable = evm.tablemem = NewMemory() // bound memorystack = newstack() // local stackcallContext = &ScopeContext{Memory: mem,Stack: stack,Contract: contract,}// 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 countercost uint64// copies used by tracerpcCopy uint64 // needed for the deferred EVMLoggergasCopy uint64 // for EVMLogger to log gas remaining before executionlogged bool // deferred EVMLogger should ignore already logged stepsres []byte // result of the opcode execution functiondebug = evm.Config.Tracer != nilisEIP4762 = evm.chainRules.IsEIP4762)// Don't move this deferred function, it's placed before the OnOpcode-deferred method,// so that it gets executed _after_: the OnOpcode needs the stacks before// they are returned to the poolsdefer func() {returnStack(stack)mem.Free()}()contract.Input = inputif debug {defer func() { // this deferred method handles exit-with-errorif err == nil {return}if !logged && evm.Config.Tracer.OnOpcode != nil {evm.Config.Tracer.OnOpcode(pcCopy, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err))}if logged && evm.Config.Tracer.OnFault != nil {evm.Config.Tracer.OnFault(pcCopy, byte(op), gasCopy, cost, callContext, evm.depth, VMErrorFromErr(err))}}()}// The Interpreter main run loop (contextual). This loop runs until either an// explicit STOP, RETURN or SELFDESTRUCT is executed, an error occurred during// the execution of one of the operations or until the done flag is set by the// parent context._ = jumpTable[0] // nil-check the jumpTable out of the loopfor {if debug {// Capture pre-execution values for tracing.logged, pcCopy, gasCopy = false, pc, contract.Gas}if isEIP4762 && !contract.IsDeployment && !contract.IsSystemCall {// if the PC ends up in a new "chunk" of verkleized code, charge the// associated costs.contractAddr := contract.Address()consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas)contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)if consumed < wanted {return nil, ErrOutOfGas}}// Get the operation from the jump table and validate the stack to ensure there are// enough stack items available to perform the operation.op = contract.GetOp(pc)operation := jumpTable[op]cost = operation.constantGas // For tracing// Validate stackif sLen := stack.len(); sLen < operation.minStack {return nil, &ErrStackUnderflow{stackLen: sLen, required: operation.minStack}} else if sLen > operation.maxStack {return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}}// for tracing: this gas consumption event is emitted below in the debug section.if contract.Gas < cost {return nil, ErrOutOfGas} else {contract.Gas -= cost}// All ops with a dynamic memory usage also has a dynamic gas cost.var memorySize uint64if operation.dynamicGas != nil {// calculate the new memory size and expand the memory to fit// the operation// Memory check needs to be done prior to evaluating the dynamic gas portion,// to detect calculation overflowsif operation.memorySize != nil {memSize, overflow := operation.memorySize(stack)if overflow {return nil, ErrGasUintOverflow}// memory is expanded in words of 32 bytes. Gas// is also calculated in words.if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {return nil, ErrGasUintOverflow}}// Consume the gas and return an error if not enough gas is available.// cost is explicitly set so that the capture state defer method can get the proper costvar dynamicCost uint64dynamicCost, err = operation.dynamicGas(evm, contract, stack, mem, memorySize)cost += dynamicCost // for tracingif err != nil {return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)}// for tracing: this gas consumption event is emitted below in the debug section.if contract.Gas < dynamicCost {return nil, ErrOutOfGas} else {contract.Gas -= dynamicCost}}// Do tracing before potential memory expansionif debug {if evm.Config.Tracer.OnGasChange != nil {evm.Config.Tracer.OnGasChange(gasCopy, gasCopy-cost, tracing.GasChangeCallOpCode)}if evm.Config.Tracer.OnOpcode != nil {evm.Config.Tracer.OnOpcode(pc, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err))logged = true}}if memorySize > 0 {mem.Resize(memorySize)}// execute the operationres, err = operation.execute(&pc, evm, callContext)if err != nil {break}pc++}if err == errStopToken {err = nil // clear stop token error}return res, err
}
