diff --git a/core/vm/interpreter_experimental.go b/core/vm/interpreter_experimental.go new file mode 100644 index 0000000000..6ec97782d5 --- /dev/null +++ b/core/vm/interpreter_experimental.go @@ -0,0 +1,495 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package vm + +import ( + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// runExperimental is an optimized interpreter loop using modern optimization +// techniques taken from reth and gevm. +// It is used when tracing is disabled and Verkle (EIP-4762) is not active, since +// the code does not deal with those cases yet. +// +// Currently there are two optimizations over the standard loop: +// +// 1. Switch dispatch: the standard interpreter calls operation.execute() via a function +// pointer table. This is an indirect call that the CPU can't predict and the compiler won't +// inline. +// runExperimental replaces this with a switch statement for common +// opcodes, with the opcode logic inlined in each case. The remaining opcodes +// fall through to the default case which uses the standard jump table. +// TODO: Eventualy, we can migrate over everything. +// +// 2. Gas accumulation: the standard interpreter checks and writes contract.Gas (on the heap) +// on every opcode. +// runExperimental instead accumulates gas in a local (register allocated) variable +// It flushes to contract.Gas only at: +// - Control flow boundaries (JUMP, JUMPI etc) +// - Halt points (STOP, end of code) +// - Error paths (stack underflow/overflow, OOG) +// - The default fallback (before dynamic-gas opcodes like SLOAD, CALL, etc.) +// +// This is safe because: +// - all inlined opcodes only touch the stack, so executing a few extra ops +// before detecting OOG has no observable side effects. +// - the EVM spec consumes all gas on OOG errors regardless. +// - gas is always flushed before any opcode with external effects (storage, calls, logs) +// since those go through the default path. +// - JUMP/JUMPI flush before jumping, so loops can't run indefinitely without a gas check. +func (evm *EVM) runExperimental(contract *Contract, stack *Stack, mem *Memory, callContext *ScopeContext, jumpTable *JumpTable, code []byte) (ret []byte, err error) { + var ( + pc uint64 + codeLen = uint64(len(code)) + gasUsed uint64 // accumulated gas for constant-gas opcodes + ) + + for { + if pc >= codeLen { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, errStopToken + } + op := OpCode(code[pc]) + + switch op { + + case STOP: + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, errStopToken + + case ADD: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + y.Add(&x, y) + + case SUB: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + y.Sub(&x, y) + + case MUL: + gasUsed += GasFastStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + y.Mul(&x, y) + + case DIV: + gasUsed += GasFastStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + y.Div(&x, y) + + case SDIV: + gasUsed += GasFastStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + y.SDiv(&x, y) + + case MOD: + gasUsed += GasFastStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + y.Mod(&x, y) + + case SMOD: + gasUsed += GasFastStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + y.SMod(&x, y) + + case LT: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + if x.Lt(y) { + y.SetOne() + } else { + y.Clear() + } + + case GT: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + if x.Gt(y) { + y.SetOne() + } else { + y.Clear() + } + + case SLT: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + if x.Slt(y) { + y.SetOne() + } else { + y.Clear() + } + + case SGT: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + if x.Sgt(y) { + y.SetOne() + } else { + y.Clear() + } + + case EQ: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + if x.Eq(y) { + y.SetOne() + } else { + y.Clear() + } + + case ISZERO: + gasUsed += GasFastestStep + if stack.len() < 1 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 1} + } + x := stack.peek() + if x.IsZero() { + x.SetOne() + } else { + x.Clear() + } + + case AND: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + y.And(&x, y) + + case OR: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + y.Or(&x, y) + + case XOR: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + x, y := stack.pop(), stack.peek() + y.Xor(&x, y) + + case NOT: + gasUsed += GasFastestStep + if stack.len() < 1 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 1} + } + x := stack.peek() + x.Not(x) + + case POP: + gasUsed += GasQuickStep + if stack.len() < 1 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: 0, required: 1} + } + stack.pop() + + case DUP1, DUP2, DUP3, DUP4, DUP5, DUP6, DUP7: + gasUsed += GasFastestStep + n := int(op - DUP1 + 1) + sLen := stack.len() + if sLen < n { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: sLen, required: n} + } + if sLen >= int(params.StackLimit) { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackOverflow{stackLen: sLen, limit: int(params.StackLimit)} + } + stack.dup(n) + + case SWAP1: + gasUsed += GasFastestStep + if stack.len() < 2 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + stack.swap1() + + case SWAP2: + gasUsed += GasFastestStep + if stack.len() < 3 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 3} + } + stack.swap2() + + case SWAP3: + gasUsed += GasFastestStep + if stack.len() < 4 { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 4} + } + stack.swap3() + + case PUSH1: + gasUsed += GasFastestStep + if stack.len() >= int(params.StackLimit) { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackOverflow{stackLen: stack.len(), limit: int(params.StackLimit)} + } + pc++ + var val uint256.Int + if pc < codeLen { + val.SetUint64(uint64(code[pc])) + } + stack.push(&val) + + case PUSH2: + gasUsed += GasFastestStep + if stack.len() >= int(params.StackLimit) { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackOverflow{stackLen: stack.len(), limit: int(params.StackLimit)} + } + var val uint256.Int + if pc+2 < codeLen { + val.SetUint64(uint64(code[pc+1])<<8 | uint64(code[pc+2])) + } else if pc+1 < codeLen { + val.SetUint64(uint64(code[pc+1]) << 8) + } + stack.push(&val) + pc += 2 + + case PUSH0: + if !evm.chainRules.IsShanghai { + return nil, &ErrInvalidOpCode{opcode: op} + } + gasUsed += GasQuickStep + if stack.len() >= int(params.StackLimit) { + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + return nil, &ErrStackOverflow{stackLen: stack.len(), limit: int(params.StackLimit)} + } + var zero uint256.Int + stack.push(&zero) + + case JUMPDEST: + gasUsed += params.JumpdestGas + + case JUMP: + gasUsed += GasMidStep + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + gasUsed = 0 + + if stack.len() < 1 { + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 1} + } + pos := stack.pop() + + if evm.abort.Load() { + return nil, errStopToken + } + if !contract.validJumpdest(&pos) { + return nil, ErrInvalidJump + } + pc = pos.Uint64() + continue // skip pc++ + + case JUMPI: + gasUsed += GasSlowStep + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + gasUsed = 0 + + if stack.len() < 2 { + return nil, &ErrStackUnderflow{stackLen: stack.len(), required: 2} + } + pos, cond := stack.pop(), stack.pop() + + if evm.abort.Load() { + return nil, errStopToken + } + if !cond.IsZero() { + if !contract.validJumpdest(&pos) { + return nil, ErrInvalidJump + } + pc = pos.Uint64() + continue // skip pc++ + } + // Flush gas and fall back to normal dispatch + default: + if contract.Gas < gasUsed { + return nil, ErrOutOfGas + } + contract.Gas -= gasUsed + gasUsed = 0 + + // TODO: second return (cost) is for tracing — use it when adding a tracing to runExperimental. + operation, _, memorySize, gasErr := chargeGasOp(op, evm, contract, stack, mem, jumpTable) + if gasErr != nil { + return nil, gasErr + } + if memorySize > 0 { + mem.Resize(memorySize) + } + var res []byte + res, err = operation.execute(&pc, evm, callContext) + if err != nil { + return res, err + } + } + + pc++ + } +}