mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
core/vm: replace interpreter dispatch with a generated switch
This commit is contained in:
parent
dddbaa4bf3
commit
419e5e1438
16 changed files with 3331 additions and 16 deletions
|
|
@ -131,6 +131,11 @@ type EVM struct {
|
|||
returnData []byte // Last CALL's return data for subsequent reuse
|
||||
|
||||
arena *stackArena
|
||||
|
||||
// forceTableLoop is a test-only switch. The differential test sets it so
|
||||
// that Run uses the table loop (execTraced) for the whole call tree, which
|
||||
// lets the test compare it against the generated dispatch.
|
||||
forceTableLoop bool
|
||||
}
|
||||
|
||||
// NewEVM constructs an EVM instance with the supplied block context, state
|
||||
|
|
|
|||
497
core/vm/gen/main.go
Normal file
497
core/vm/gen/main.go
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
// Copyright 2026 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Command gen generates core/vm/interp_gen.go, the EVM interpreter's untraced
|
||||
// fast-path dispatch. The output is a switch over the opcode byte that:
|
||||
//
|
||||
// - inlines the hot, fork-stable opcodes (arithmetic / comparison / bitwise /
|
||||
// PUSH / DUP / SWAP / POP / JUMP / JUMPI / PC / MSIZE / JUMPDEST) by
|
||||
// splicing the existing opXxx handler bodies from instructions.go and
|
||||
// eips.go, with their static gas and stack bounds baked in as constants
|
||||
// derived from the per-fork instruction tables via vm.GenForks.
|
||||
//
|
||||
// - calls the fork-invariant cold ops (KECCAK256 / MLOAD / MSTORE / MSTORE8,
|
||||
// see directCold) directly by name, skipping the table's function
|
||||
// pointers, which Go cannot inline through.
|
||||
//
|
||||
// - dispatches everything fork-varying (CALL / CREATE / SSTORE / SLOAD / LOG /
|
||||
// the COPY family and so on) through the active per-fork JumpTable in the
|
||||
// default case, exactly as the legacy loop did, so volatile gas and opcode
|
||||
// logic stays shared rather than restated.
|
||||
//
|
||||
// The generated file is committed and a CI test asserts it matches `go generate`
|
||||
// output. Do not hand-edit interp_gen.go.
|
||||
//
|
||||
// Usage: go generate ./core/vm/...
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
)
|
||||
|
||||
const stackLimit = 1024 // params.StackLimit
|
||||
|
||||
// inlineHandler maps an opcode byte to the opXxx handler whose body is spliced
|
||||
// inline for that opcode. These are the hot, fork-stable opcodes with no dynamic
|
||||
// gas. Opcodes not listed here (or in directCold, or PUSH3-32 / DUP1-16, which
|
||||
// are handled specially) fall through to the default case, which dispatches via
|
||||
// the per-fork table.
|
||||
var inlineHandler = map[byte]string{
|
||||
0x01: "opAdd", 0x02: "opMul", 0x03: "opSub", 0x04: "opDiv", 0x05: "opSdiv",
|
||||
0x06: "opMod", 0x07: "opSmod", 0x08: "opAddmod", 0x09: "opMulmod", 0x0b: "opSignExtend",
|
||||
0x10: "opLt", 0x11: "opGt", 0x12: "opSlt", 0x13: "opSgt", 0x14: "opEq", 0x15: "opIszero",
|
||||
0x16: "opAnd", 0x17: "opOr", 0x18: "opXor", 0x19: "opNot", 0x1a: "opByte",
|
||||
0x1b: "opSHL", 0x1c: "opSHR", 0x1d: "opSAR", 0x1e: "opCLZ",
|
||||
0x50: "opPop", 0x56: "opJump", 0x57: "opJumpi", 0x58: "opPc", 0x59: "opMsize", 0x5b: "opJumpdest",
|
||||
0x5f: "opPush0", 0x60: "opPush1", 0x61: "opPush2",
|
||||
0x90: "opSwap1", 0x91: "opSwap2", 0x92: "opSwap3", 0x93: "opSwap4",
|
||||
0x94: "opSwap5", 0x95: "opSwap6", 0x96: "opSwap7", 0x97: "opSwap8",
|
||||
0x98: "opSwap9", 0x99: "opSwap10", 0x9a: "opSwap11", 0x9b: "opSwap12",
|
||||
0x9c: "opSwap13", 0x9d: "opSwap14", 0x9e: "opSwap15", 0x9f: "opSwap16",
|
||||
}
|
||||
|
||||
// directCold lists cold opcodes (dynamic gas, not inlined) whose handler,
|
||||
// dynamic-gas, and memory-size functions are the same across every fork
|
||||
// (verified: untouched by any enableXxx). They are emitted as direct calls to
|
||||
// those functions by name instead of the indirect operation.* pointer calls
|
||||
// in the default case. Go inlines the plain functions, and the var-aliased gas
|
||||
// funcs (gasMLoad and friends alias pureMemoryGascost) at least skip the
|
||||
// table load. Measured at ~3.4% on snailtracer (p=0.000). Fork-varying cold ops
|
||||
// (CALL/SSTORE/SLOAD and friends) do not qualify and stay on the per-fork
|
||||
// table in the default case.
|
||||
//
|
||||
// opcode → {handler, dynamicGas, memorySize}
|
||||
//
|
||||
// Limited to the memory/hash ops that appear in hot loops. Adding more (e.g.
|
||||
// CALLDATACOPY/RETURN, typically once per call) grows the generated function
|
||||
// and regresses tiny benchmarks through code layout, for negligible gain.
|
||||
var directCold = map[byte][3]string{
|
||||
0x20: {"opKeccak256", "gasKeccak256", "memoryKeccak256"}, // KECCAK256
|
||||
0x51: {"opMload", "gasMLoad", "memoryMLoad"}, // MLOAD
|
||||
0x52: {"opMstore", "gasMStore", "memoryMStore"}, // MSTORE
|
||||
0x53: {"opMstore8", "gasMStore8", "memoryMStore8"}, // MSTORE8
|
||||
}
|
||||
|
||||
// opMeta is the per-opcode metadata derived from the per-fork tables.
|
||||
type opMeta struct {
|
||||
defined bool
|
||||
name string // opcode mnemonic, e.g. "ADD"
|
||||
introF string // params.Rules field activating it, empty for Frontier (always on)
|
||||
constGas uint64
|
||||
minStack int
|
||||
maxStack int
|
||||
}
|
||||
|
||||
type generator struct {
|
||||
fset *token.FileSet
|
||||
handlers map[string]*ast.FuncDecl
|
||||
meta [256]opMeta
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (g *generator) p(format string, args ...any) { fmt.Fprintf(g.buf, format, args...) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handler parsing + body splicing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// parseHandlers parses instructions.go and eips.go and returns every top-level
|
||||
// opXxx function declaration by name.
|
||||
func parseHandlers(vmDir string) (*token.FileSet, map[string]*ast.FuncDecl) {
|
||||
fset := token.NewFileSet()
|
||||
handlers := map[string]*ast.FuncDecl{}
|
||||
for _, name := range []string{"instructions.go", "eips.go"} {
|
||||
path := filepath.Join(vmDir, name)
|
||||
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
fatalf("parse %s: %v", path, err)
|
||||
}
|
||||
for _, decl := range f.Decls {
|
||||
fn, ok := decl.(*ast.FuncDecl)
|
||||
if !ok || fn.Recv != nil || fn.Body == nil {
|
||||
continue
|
||||
}
|
||||
handlers[fn.Name.Name] = fn
|
||||
}
|
||||
}
|
||||
return fset, handlers
|
||||
}
|
||||
|
||||
var returnRe = regexp.MustCompile(`^(\s*)return\s+([^,]+),\s*(.+)$`)
|
||||
|
||||
// inlineBody returns the source of handler's body, rewritten so it can be
|
||||
// spliced into the dispatch loop: the `*pc` dereference becomes the loop's `pc`
|
||||
// local, and each `return r0, r1` becomes loop control flow. Success
|
||||
// (r1 == nil) advances pc and continues, an error sets err and breaks.
|
||||
func (g *generator) inlineBody(handler string) string {
|
||||
fn := g.handlers[handler]
|
||||
if fn == nil {
|
||||
fatalf("no handler %q to inline", handler)
|
||||
}
|
||||
var raw bytes.Buffer
|
||||
cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8}
|
||||
for _, stmt := range fn.Body.List {
|
||||
if err := cfg.Fprint(&raw, g.fset, stmt); err != nil {
|
||||
fatalf("print %s body: %v", handler, err)
|
||||
}
|
||||
raw.WriteByte('\n')
|
||||
}
|
||||
src := strings.ReplaceAll(raw.String(), "*pc", "pc")
|
||||
|
||||
var out strings.Builder
|
||||
for _, line := range strings.Split(src, "\n") {
|
||||
if m := returnRe.FindStringSubmatch(line); m != nil {
|
||||
indent, r0, r1 := m[1], strings.TrimSpace(m[2]), strings.TrimSpace(m[3])
|
||||
// The error and halt path must overwrite res and err. Otherwise a
|
||||
// halting inlined op (JUMPI on an invalid jump, say) returns stale
|
||||
// res from an earlier res-setting op such as a DELEGATECALL. The
|
||||
// fuzzer caught exactly that bug. The success path advances pc and
|
||||
// continues without writing res or err: stale res on a continuing
|
||||
// op is always overwritten by whichever op ends the run, and err
|
||||
// stays nil through normal iteration. Keeping these stores off the
|
||||
// success path avoids growing every inlined case, which regresses
|
||||
// tiny, layout-sensitive benchmarks.
|
||||
if r1 == "nil" {
|
||||
out.WriteString(indent + "pc++\n")
|
||||
out.WriteString(indent + "continue mainLoop\n")
|
||||
} else {
|
||||
out.WriteString(indent + "res, err = " + r0 + ", " + r1 + "\n")
|
||||
out.WriteString(indent + "break mainLoop\n")
|
||||
}
|
||||
continue
|
||||
}
|
||||
out.WriteString(line + "\n")
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata derivation (from the per-fork tables, via vm.GenForks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (g *generator) deriveMeta(forks []vm.GenFork) {
|
||||
for code := 0; code < 256; code++ {
|
||||
for _, fork := range forks {
|
||||
o := fork.Ops[code]
|
||||
if !o.Defined {
|
||||
continue
|
||||
}
|
||||
g.meta[code] = opMeta{
|
||||
defined: true,
|
||||
name: o.Name,
|
||||
introF: fork.RuleField,
|
||||
constGas: o.ConstantGas,
|
||||
minStack: o.MinStack,
|
||||
maxStack: o.MaxStack,
|
||||
}
|
||||
break // first fork that defines it wins (its intro fork)
|
||||
}
|
||||
}
|
||||
// Sanity: every inlined opcode must be defined and have fork-stable static
|
||||
// gas / stack bounds across all forks where it appears (that is what makes
|
||||
// it safe to bake as a constant). Bail loudly otherwise.
|
||||
for code, handler := range inlineHandler {
|
||||
g.checkStable(byte(code), handler, forks)
|
||||
}
|
||||
for code := 0x62; code <= 0x7f; code++ { // PUSH3-32
|
||||
g.checkStable(byte(code), "makePush", forks)
|
||||
}
|
||||
for code := 0x80; code <= 0x8f; code++ { // DUP1-16
|
||||
g.checkStable(byte(code), "makeDup", forks)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
|
||||
m := g.meta[code]
|
||||
if !m.defined {
|
||||
fatalf("opcode %#x (%s) selected for inlining but never defined", code, what)
|
||||
}
|
||||
for _, fork := range forks {
|
||||
o := fork.Ops[code]
|
||||
if !o.Defined {
|
||||
continue
|
||||
}
|
||||
if o.ConstantGas != m.constGas || o.MinStack != m.minStack || o.MaxStack != m.maxStack || o.HasDynamicGas {
|
||||
fatalf("opcode %#x (%s) is not fork-stable (fork %s): cannot inline", code, what, fork.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case emission
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// emitStackChecks emits the underflow/overflow guards for a baked opcode,
|
||||
// mirroring the legacy loop's order (stack validated before gas).
|
||||
func (g *generator) emitStackChecks(m opMeta) {
|
||||
under := m.minStack > 0
|
||||
over := m.maxStack < stackLimit
|
||||
switch {
|
||||
case under && over:
|
||||
g.p("if sLen := stack.len(); sLen < %d {\nreturn nil, &ErrStackUnderflow{stackLen: sLen, required: %d}\n} else if sLen > %d {\nreturn nil, &ErrStackOverflow{stackLen: sLen, limit: %d}\n}\n", m.minStack, m.minStack, m.maxStack, m.maxStack)
|
||||
case under:
|
||||
g.p("if sLen := stack.len(); sLen < %d {\nreturn nil, &ErrStackUnderflow{stackLen: sLen, required: %d}\n}\n", m.minStack, m.minStack)
|
||||
case over:
|
||||
g.p("if sLen := stack.len(); sLen > %d {\nreturn nil, &ErrStackOverflow{stackLen: sLen, limit: %d}\n}\n", m.maxStack, m.maxStack)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) emitGasCheck(m opMeta) {
|
||||
if m.constGas == 0 {
|
||||
return
|
||||
}
|
||||
g.p("if contract.Gas.RegularGas < %d {\nreturn nil, ErrOutOfGas\n}\ncontract.Gas.RegularGas -= %d\n", m.constGas, m.constGas)
|
||||
}
|
||||
|
||||
// emitWork emits the stack/gas guards and the opcode body (the portion that runs
|
||||
// when the opcode is active for the current fork).
|
||||
func (g *generator) emitWork(code byte) {
|
||||
m := g.meta[code]
|
||||
g.emitStackChecks(m)
|
||||
g.emitGasCheck(m)
|
||||
|
||||
// PUSH1-PUSH32 swap their execute function under EIP-4762 (verkle) to charge
|
||||
// code-chunk gas on the immediate bytes. Defer to the table handler there.
|
||||
// The baked static gas and stack guard above already match.
|
||||
if code >= 0x60 && code <= 0x7f {
|
||||
g.p("if isEIP4762 {\nres, err = table[op].execute(&pc, evm, scope)\nif err != nil {\nbreak mainLoop\n}\npc++\ncontinue mainLoop\n}\n")
|
||||
}
|
||||
|
||||
switch {
|
||||
case code >= 0x62 && code <= 0x7f: // PUSH3-PUSH32: inline makePush(n,n)
|
||||
g.emitPushFixed(int(code) - 0x5f)
|
||||
case code >= 0x80 && code <= 0x8f: // DUP1-DUP16: inline makeDup(n)
|
||||
g.p("scope.Stack.dup(%d)\npc++\ncontinue mainLoop\n", int(code)-0x7f)
|
||||
default:
|
||||
g.p("%s", g.inlineBody(inlineHandler[code]))
|
||||
}
|
||||
}
|
||||
|
||||
// emitPushFixed inlines makePush(n, n) for PUSH<n> (n = 3..32).
|
||||
func (g *generator) emitPushFixed(n int) {
|
||||
g.p("codeLen := len(scope.Contract.Code)\n")
|
||||
g.p("start := min(codeLen, int(pc+1))\n")
|
||||
g.p("end := min(codeLen, start+%d)\n", n)
|
||||
g.p("a := scope.Stack.get()\n")
|
||||
g.p("a.SetBytes(scope.Contract.Code[start:end])\n")
|
||||
g.p("if missing := %d - (end - start); missing > 0 {\na.Lsh(a, uint(8*missing))\n}\n", n)
|
||||
g.p("pc += %d\npc++\ncontinue mainLoop\n", n)
|
||||
}
|
||||
|
||||
func (g *generator) emitInlineCase(code byte) {
|
||||
m := g.meta[code]
|
||||
g.p("case %s:\n", m.name)
|
||||
if m.introF == "" {
|
||||
g.emitWork(code)
|
||||
return
|
||||
}
|
||||
// Fork-gated: run the inlined body only when the opcode is active for the
|
||||
// current fork. Otherwise mirror the legacy loop's undefined-opcode handling.
|
||||
g.p("if rules.%s {\n", m.introF)
|
||||
g.emitWork(code)
|
||||
g.p("}\n")
|
||||
g.p("res, err = opUndefined(&pc, evm, scope)\nbreak mainLoop\n")
|
||||
}
|
||||
|
||||
// emitDirectCold emits a cold opcode case identical to the default case, except
|
||||
// the handler, dynamic-gas, and memory-size functions are called by name
|
||||
// rather than through the indirect operation.* table pointers. Valid only for
|
||||
// fork-invariant ops (see directCold).
|
||||
func (g *generator) emitDirectCold(code byte) {
|
||||
m := g.meta[code]
|
||||
fns := directCold[code]
|
||||
g.p("case %s:\n", m.name)
|
||||
g.emitStackChecks(m)
|
||||
g.emitGasCheck(m)
|
||||
g.p("var memorySize uint64\n{\n")
|
||||
g.p("memSize, overflow := %s(stack)\n", fns[2])
|
||||
g.p("if overflow {\nreturn nil, ErrGasUintOverflow\n}\n")
|
||||
g.p("if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {\nreturn nil, ErrGasUintOverflow\n}\n}\n")
|
||||
g.p("var dynamicCost GasCosts\n")
|
||||
g.p("dynamicCost, err = %s(evm, contract, stack, mem, memorySize)\n", fns[1])
|
||||
// WriteString: keep %w/%v literal (not generator format verbs).
|
||||
g.buf.WriteString("if err != nil {\nreturn nil, fmt.Errorf(\"%w: %v\", ErrOutOfGas, err)\n}\n")
|
||||
g.p("if contract.Gas.RegularGas < dynamicCost.RegularGas {\nreturn nil, ErrOutOfGas\n}\n")
|
||||
g.p("contract.Gas.RegularGas -= dynamicCost.RegularGas\n")
|
||||
g.p("if memorySize > 0 {\nmem.Resize(memorySize)\n}\n")
|
||||
g.p("res, err = %s(&pc, evm, scope)\n", fns[0])
|
||||
g.p("if err != nil {\nbreak mainLoop\n}\npc++\ncontinue mainLoop\n")
|
||||
}
|
||||
|
||||
func (g *generator) emitDefault() {
|
||||
// WriteString, not p(): this template contains %w/%v that must reach the
|
||||
// output verbatim (they are not generator format verbs).
|
||||
g.buf.WriteString(`default:
|
||||
operation := table[op]
|
||||
if 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}
|
||||
}
|
||||
cost := operation.constantGas
|
||||
if contract.Gas.RegularGas < cost {
|
||||
return nil, ErrOutOfGas
|
||||
}
|
||||
contract.Gas.RegularGas -= cost
|
||||
var memorySize uint64
|
||||
if operation.dynamicGas != nil {
|
||||
if operation.memorySize != nil {
|
||||
memSize, overflow := operation.memorySize(stack)
|
||||
if overflow {
|
||||
return nil, ErrGasUintOverflow
|
||||
}
|
||||
if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
|
||||
return nil, ErrGasUintOverflow
|
||||
}
|
||||
}
|
||||
var dynamicCost GasCosts
|
||||
dynamicCost, err = operation.dynamicGas(evm, contract, stack, mem, memorySize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
|
||||
}
|
||||
if contract.Gas.RegularGas < dynamicCost.RegularGas {
|
||||
return nil, ErrOutOfGas
|
||||
}
|
||||
contract.Gas.RegularGas -= dynamicCost.RegularGas
|
||||
}
|
||||
if memorySize > 0 {
|
||||
mem.Resize(memorySize)
|
||||
}
|
||||
res, err = operation.execute(&pc, evm, scope)
|
||||
if err != nil {
|
||||
break mainLoop
|
||||
}
|
||||
pc++
|
||||
continue mainLoop
|
||||
`)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File emission
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (g *generator) emitFile() {
|
||||
g.p("// Code generated by core/vm/gen; DO NOT EDIT.\n\n")
|
||||
g.p("package vm\n\n")
|
||||
g.p("import (\n")
|
||||
g.p("\t\"fmt\"\n\n")
|
||||
g.p("\t\"github.com/ethereum/go-ethereum/common/math\"\n")
|
||||
g.p("\t\"github.com/ethereum/go-ethereum/core/tracing\"\n")
|
||||
g.p(")\n\n")
|
||||
|
||||
g.buf.WriteString(`// execUntraced is the generated, tracing-free interpreter fast path. Hot,
|
||||
// fork-stable opcodes are inlined with their static gas and stack bounds baked
|
||||
// in. Fork-invariant cold ops (KECCAK256/MLOAD/MSTORE/MSTORE8) call their
|
||||
// handler and gas functions directly by name. Everything fork-varying is
|
||||
// dispatched through the active per-fork table in the default case. EVM.Run
|
||||
// selects this path when no tracer is configured.
|
||||
func (evm *EVM) execUntraced(scope *ScopeContext) (ret []byte, err error) {
|
||||
var (
|
||||
contract = scope.Contract
|
||||
mem = scope.Memory
|
||||
stack = scope.Stack
|
||||
table = evm.table
|
||||
rules = evm.chainRules
|
||||
isEIP4762 = rules.IsEIP4762
|
||||
pc = uint64(0)
|
||||
res []byte
|
||||
)
|
||||
_ = mem
|
||||
_ = rules
|
||||
_ = isEIP4762
|
||||
_ = table
|
||||
mainLoop:
|
||||
for {
|
||||
if isEIP4762 && !contract.IsDeployment && !contract.IsSystemCall {
|
||||
contractAddr := contract.Address()
|
||||
consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas.RegularGas)
|
||||
contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
|
||||
if consumed < wanted {
|
||||
return nil, ErrOutOfGas
|
||||
}
|
||||
}
|
||||
op := contract.GetOp(pc)
|
||||
switch op {
|
||||
`)
|
||||
// Inlined hot cases, in opcode order for readability.
|
||||
for code := 0; code < 256; code++ {
|
||||
b := byte(code)
|
||||
_, named := inlineHandler[b]
|
||||
isPushFixed := code >= 0x62 && code <= 0x7f
|
||||
isDup := code >= 0x80 && code <= 0x8f
|
||||
if named || isPushFixed || isDup {
|
||||
g.emitInlineCase(b)
|
||||
} else if _, dc := directCold[b]; dc {
|
||||
g.emitDirectCold(b)
|
||||
}
|
||||
}
|
||||
g.emitDefault()
|
||||
g.p("}\n") // switch
|
||||
g.p("}\n") // for
|
||||
g.p("if err == errStopToken {\nerr = nil\n}\n")
|
||||
g.p("return res, err\n")
|
||||
g.p("}\n") // func
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "gen: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func main() {
|
||||
_, self, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
fatalf("cannot resolve generator source path")
|
||||
}
|
||||
vmDir := filepath.Dir(filepath.Dir(self)) // .../core/vm/gen -> .../core/vm
|
||||
|
||||
fset, handlers := parseHandlers(vmDir)
|
||||
g := &generator{fset: fset, handlers: handlers, buf: new(bytes.Buffer)}
|
||||
g.deriveMeta(vm.GenForks())
|
||||
g.emitFile()
|
||||
|
||||
formatted, err := format.Source(g.buf.Bytes())
|
||||
if err != nil {
|
||||
dbg := filepath.Join(vmDir, "interp_gen.go.broken")
|
||||
os.WriteFile(dbg, g.buf.Bytes(), 0644)
|
||||
fatalf("gofmt failed (%v); wrote unformatted output to %s", err, dbg)
|
||||
}
|
||||
// INTERP_GEN_OUT lets the CI-match test (interp_gen_test.go) regenerate to a
|
||||
// temporary file and diff it against the committed one, without clobbering it.
|
||||
out := filepath.Join(vmDir, "interp_gen.go")
|
||||
if env := os.Getenv("INTERP_GEN_OUT"); env != "" {
|
||||
out = env
|
||||
}
|
||||
if err := os.WriteFile(out, formatted, 0644); err != nil {
|
||||
fatalf("write %s: %v", out, err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "gen: wrote %s (%d bytes)\n", out, len(formatted))
|
||||
}
|
||||
106
core/vm/genspec.go
Normal file
106
core/vm/genspec.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright 2026 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package vm
|
||||
|
||||
//go:generate go run ./gen
|
||||
|
||||
// This file exposes the interpreter's opcode metadata to the code generator in
|
||||
// core/vm/gen. It is not used at runtime. It exists so the generator can derive
|
||||
// the per-opcode spec (static gas, stack bounds, the fork an opcode first
|
||||
// appears in, and whether it carries dynamic gas or memory sizing) from the
|
||||
// existing per-fork instruction sets, rather than restating that metadata.
|
||||
//
|
||||
// The fork-varying dynamic-gas / memory-size / execute *functions* are not
|
||||
// surfaced here: several are closures (gasCall, the memoryCopierGas family,
|
||||
// makeGasLog) that cannot be recovered by name. The generated switch instead
|
||||
// reaches those volatile opcodes through the active per-fork JumpTable at
|
||||
// runtime (see interp_gen.go), so they need no generator-side restatement.
|
||||
|
||||
// GenOp is the generator-facing scalar metadata for one opcode slot in one fork.
|
||||
type GenOp struct {
|
||||
Name string // opcode mnemonic, e.g. "ADD" (valid only if Defined)
|
||||
Defined bool // false if the slot is undefined/invalid in this fork
|
||||
ConstantGas uint64
|
||||
MinStack int
|
||||
MaxStack int
|
||||
HasDynamicGas bool
|
||||
HasMemorySize bool
|
||||
}
|
||||
|
||||
// GenFork bundles a fork's name, the params.Rules bool field that activates it
|
||||
// (empty for Frontier, which is always active), and its per-opcode metadata.
|
||||
type GenFork struct {
|
||||
Name string
|
||||
RuleField string
|
||||
Ops [256]GenOp
|
||||
}
|
||||
|
||||
// genForkOrder is the canonical fork progression for code generation, oldest to
|
||||
// newest, each paired with the params.Rules field that activates it.
|
||||
//
|
||||
// Petersburg is omitted: it shares Constantinople's opcode set and only changes
|
||||
// SSTORE dynamic gas, which flows through the shared gas function. Verkle/UBT is
|
||||
// omitted: over its Shanghai base it adds no new opcodes (it only swaps gas and
|
||||
// execute on existing opcodes), which the generated switch picks up from the
|
||||
// active table at runtime.
|
||||
var genForkOrder = []struct {
|
||||
name string
|
||||
rule string
|
||||
set *JumpTable
|
||||
}{
|
||||
{"Frontier", "", &frontierInstructionSet},
|
||||
{"Homestead", "IsHomestead", &homesteadInstructionSet},
|
||||
{"TangerineWhistle", "IsEIP150", &tangerineWhistleInstructionSet},
|
||||
{"SpuriousDragon", "IsEIP158", &spuriousDragonInstructionSet},
|
||||
{"Byzantium", "IsByzantium", &byzantiumInstructionSet},
|
||||
{"Constantinople", "IsConstantinople", &constantinopleInstructionSet},
|
||||
{"Istanbul", "IsIstanbul", &istanbulInstructionSet},
|
||||
{"Berlin", "IsBerlin", &berlinInstructionSet},
|
||||
{"London", "IsLondon", &londonInstructionSet},
|
||||
{"Merge", "IsMerge", &mergeInstructionSet},
|
||||
{"Shanghai", "IsShanghai", &shanghaiInstructionSet},
|
||||
{"Cancun", "IsCancun", &cancunInstructionSet},
|
||||
{"Prague", "IsPrague", &pragueInstructionSet},
|
||||
{"Osaka", "IsOsaka", &osakaInstructionSet},
|
||||
{"Amsterdam", "IsAmsterdam", &amsterdamInstructionSet},
|
||||
}
|
||||
|
||||
// GenForks returns per-fork opcode metadata for the interpreter code generator
|
||||
// (core/vm/gen). It is exported solely for that purpose.
|
||||
func GenForks() []GenFork {
|
||||
out := make([]GenFork, len(genForkOrder))
|
||||
for i, f := range genForkOrder {
|
||||
gf := GenFork{Name: f.name, RuleField: f.rule}
|
||||
for code := 0; code < 256; code++ {
|
||||
op := f.set[code]
|
||||
if op == nil || op.undefined {
|
||||
continue
|
||||
}
|
||||
gf.Ops[code] = GenOp{
|
||||
Name: OpCode(code).String(),
|
||||
Defined: true,
|
||||
ConstantGas: op.constantGas,
|
||||
MinStack: op.minStack,
|
||||
MaxStack: op.maxStack,
|
||||
HasDynamicGas: op.dynamicGas != nil,
|
||||
HasMemorySize: op.memorySize != nil,
|
||||
}
|
||||
}
|
||||
out[i] = gf
|
||||
}
|
||||
return out
|
||||
}
|
||||
365
core/vm/interp_diff_test.go
Normal file
365
core/vm/interp_diff_test.go
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
// Copyright 2026 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package vm
|
||||
|
||||
// Differential test comparing the table loop against the generated dispatch.
|
||||
//
|
||||
// This file proves that the generated dispatch (execUntraced) is bit-identical
|
||||
// to the table-walking loop (execTraced, run here without a tracer via
|
||||
// EVM.forceTableLoop) for the observable surface of an EVM execution: return
|
||||
// data, gas left, error/halt, refund counter, emitted logs, and the resulting
|
||||
// state root. It runs the same program through both interpreters over freshly
|
||||
// built, identical state across several forks, plus a fuzz target over
|
||||
// arbitrary bytecode.
|
||||
//
|
||||
// execTraced is also the production tracing path, and
|
||||
// if it drifted from the generated dispatch then traced re-execution would
|
||||
// disagree with what consensus executed. Hook emission itself is covered by
|
||||
// the tracer test suites instead.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/tracing"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// diffForks is the set of fork configurations the diff test runs every program
|
||||
// under. Spanning forks exercises the baked runtime fork gates (e.g. SHL from
|
||||
// Constantinople, PUSH0 from Shanghai, CLZ from Osaka) in both the active and
|
||||
// the not-yet-activated states.
|
||||
var diffForks = func() []struct {
|
||||
name string
|
||||
cfg *params.ChainConfig
|
||||
merged bool
|
||||
} {
|
||||
// preConstantinople: Byzantium active, Constantinople and later not.
|
||||
preCon := *params.TestChainConfig
|
||||
preCon.ConstantinopleBlock = nil
|
||||
preCon.PetersburgBlock = nil
|
||||
preCon.IstanbulBlock = nil
|
||||
preCon.MuirGlacierBlock = nil
|
||||
preCon.BerlinBlock = nil
|
||||
preCon.LondonBlock = nil
|
||||
preCon.ArrowGlacierBlock = nil
|
||||
preCon.GrayGlacierBlock = nil
|
||||
|
||||
return []struct {
|
||||
name string
|
||||
cfg *params.ChainConfig
|
||||
merged bool
|
||||
}{
|
||||
{"Frontier", params.NonActivatedConfig, false},
|
||||
{"Byzantium", &preCon, false},
|
||||
{"London", params.TestChainConfig, false},
|
||||
{"Merged", params.MergedTestChainConfig, true},
|
||||
}
|
||||
}()
|
||||
|
||||
var (
|
||||
diffContractAddr = common.HexToAddress("0x000000000000000000000000000000000000c0de")
|
||||
diffCalleeAddr = common.HexToAddress("0x000000000000000000000000000000000000ca11")
|
||||
diffCaller = common.HexToAddress("0x000000000000000000000000000000000000face")
|
||||
)
|
||||
|
||||
// diffCalleeCode is deployed at diffCalleeAddr as a CALL/CREATE target: it
|
||||
// writes a storage slot, logs, and returns 32 bytes of memory.
|
||||
//
|
||||
// PUSH1 0x2a PUSH1 0x07 SSTORE // sstore(7, 42)
|
||||
// PUSH1 0xbb PUSH1 0x00 MSTORE // mem[0..32] = 0xbb
|
||||
// PUSH1 0x20 PUSH1 0x00 LOG0 // log0(mem[0:32])
|
||||
// PUSH1 0x20 PUSH1 0x00 RETURN // return mem[0:32]
|
||||
var diffCalleeCode = []byte{
|
||||
byte(PUSH1), 0x2a, byte(PUSH1), 0x07, byte(SSTORE),
|
||||
byte(PUSH1), 0xbb, byte(PUSH1), 0x00, byte(MSTORE),
|
||||
byte(PUSH1), 0x20, byte(PUSH1), 0x00, byte(LOG0),
|
||||
byte(PUSH1), 0x20, byte(PUSH1), 0x00, byte(RETURN),
|
||||
}
|
||||
|
||||
// asm is a tiny helper to build bytecode from opcodes/immediates.
|
||||
func asm(parts ...any) []byte {
|
||||
var b []byte
|
||||
for _, p := range parts {
|
||||
switch v := p.(type) {
|
||||
case OpCode:
|
||||
b = append(b, byte(v))
|
||||
case byte:
|
||||
b = append(b, v)
|
||||
case int:
|
||||
b = append(b, byte(v))
|
||||
case []byte:
|
||||
b = append(b, v...)
|
||||
default:
|
||||
panic("asm: bad part")
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// diffPrograms is a curated set of bytecode snippets covering the inlined hot
|
||||
// opcodes, the volatile call-through opcodes, fork-gated opcodes, control flow,
|
||||
// and the principal error paths.
|
||||
var diffPrograms = []struct {
|
||||
name string
|
||||
code []byte
|
||||
gas uint64
|
||||
}{
|
||||
{"arith", asm(PUSH1, 0x07, PUSH1, 0x03, ADD, PUSH1, 0x02, MUL, PUSH1, 0x04, SUB, PUSH1, 0x03, DIV, PUSH1, 0x05, MOD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"signed-arith", asm(PUSH1, 0x07, PUSH1, 0xfd, SDIV, PUSH1, 0x03, SMOD, PUSH1, 0x02, SIGNEXTEND, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"addmod-mulmod-exp", asm(PUSH1, 0x07, PUSH1, 0x05, PUSH1, 0x03, ADDMOD, PUSH1, 0x09, PUSH1, 0x04, PUSH1, 0x02, MULMOD, PUSH1, 0x03, PUSH1, 0x02, EXP, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"cmp", asm(PUSH1, 0x07, PUSH1, 0x03, LT, PUSH1, 0x01, GT, PUSH1, 0x01, SLT, PUSH1, 0x01, SGT, PUSH1, 0x01, EQ, ISZERO, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"bitwise", asm(PUSH1, 0xf0, PUSH1, 0x0f, AND, PUSH1, 0xaa, OR, PUSH1, 0x55, XOR, NOT, PUSH1, 0x01, BYTE, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"shifts-clz", asm(PUSH1, 0xff, PUSH1, 0x04, SHL, PUSH1, 0x02, SHR, PUSH1, 0x01, SAR, CLZ, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"dup-swap", asm(PUSH1, 0x01, PUSH1, 0x02, PUSH1, 0x03, DUP3, SWAP2, DUP1, SWAP1, POP, ADD, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"push0-push32", asm(PUSH0, PUSH3, 0x01, 0x02, 0x03, ADD, PUSH5, 0x01, 0x02, 0x03, 0x04, 0x05, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"keccak", asm(PUSH1, 0x20, PUSH1, 0x00, KECCAK256, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"memory", asm(PUSH1, 0xab, PUSH1, 0x00, MSTORE8, PUSH1, 0xcd, PUSH2, 0x00, 0x40, MSTORE, MSIZE, PUSH1, 0x60, MSTORE, PUSH1, 0x80, PUSH1, 0x00, RETURN), 100000},
|
||||
{"mcopy", asm(PUSH1, 0xff, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x20, MCOPY, PUSH1, 0x40, PUSH1, 0x00, RETURN), 100000},
|
||||
{"storage", asm(PUSH1, 0x63, PUSH1, 0x07, SSTORE, PUSH1, 0x07, SLOAD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"transient", asm(PUSH1, 0x63, PUSH1, 0x07, TSTORE, PUSH1, 0x07, TLOAD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"loop", asm(PUSH1, 0x00, JUMPDEST, PUSH1, 0x01, ADD, DUP1, PUSH1, 0x05, LT, PUSH1, 0x02, JUMPI, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"jump", asm(PUSH1, 0x06, JUMP, INVALID, INVALID, JUMPDEST, PUSH1, 0x2a, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"env", asm(ADDRESS, CALLER, CALLVALUE, ORIGIN, GASPRICE, CODESIZE, GAS, PC, ADD, ADD, ADD, ADD, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"block", asm(NUMBER, TIMESTAMP, COINBASE, GASLIMIT, CHAINID, SELFBALANCE, BASEFEE, DIFFICULTY, ADD, ADD, ADD, ADD, ADD, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"calldata", asm(PUSH1, 0x00, CALLDATALOAD, CALLDATASIZE, PUSH1, 0x00, PUSH1, 0x00, CALLDATACOPY, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"codecopy", asm(PUSH1, 0x10, PUSH1, 0x00, PUSH1, 0x00, CODECOPY, PUSH1, 0x10, PUSH1, 0x00, RETURN), 100000},
|
||||
{"log", asm(PUSH1, 0x11, PUSH1, 0x00, MSTORE, PUSH1, 0x22, PUSH1, 0x33, PUSH1, 0x20, PUSH1, 0x00, LOG2, STOP), 100000},
|
||||
// Fuzz-found regression (the stale-res bug): a res-setting DELEGATECALL
|
||||
// followed by a halting inlined op (JUMPI to an invalid destination). The
|
||||
// buggy build returned the DELEGATECALL output instead of nil.
|
||||
{"delegatecall-then-invalid-jumpi", asm(
|
||||
PUSH1, 0x30, PUSH1, 0x30, PUSH1, 0x30, PUSH1, 0x30,
|
||||
PUSH20, diffCalleeAddr.Bytes(),
|
||||
PUSH2, 0x30, 0x30, DELEGATECALL,
|
||||
PC, PC, JUMPI), 100000},
|
||||
{"extaccess", asm(PUSH20, diffCalleeAddr.Bytes(), EXTCODESIZE, PUSH20, diffCalleeAddr.Bytes(), BALANCE, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000},
|
||||
{"call", asm(
|
||||
PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00,
|
||||
PUSH20, diffCalleeAddr.Bytes(), PUSH2, 0xff, 0xff, CALL,
|
||||
PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000},
|
||||
{"staticcall", asm(
|
||||
PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00,
|
||||
PUSH20, diffCalleeAddr.Bytes(), PUSH2, 0xff, 0xff, STATICCALL,
|
||||
PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000},
|
||||
{"delegatecall", asm(
|
||||
PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00,
|
||||
PUSH20, diffCalleeAddr.Bytes(), PUSH2, 0xff, 0xff, DELEGATECALL,
|
||||
PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000},
|
||||
{"create", asm(
|
||||
// store init code that returns empty, then CREATE
|
||||
PUSH1, 0x00, PUSH1, 0x00, MSTORE,
|
||||
PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, CREATE,
|
||||
PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000},
|
||||
{"revert", asm(PUSH1, 0xaa, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, REVERT), 100000},
|
||||
{"selfdestruct", asm(PUSH20, diffCaller.Bytes(), SELFDESTRUCT), 100000},
|
||||
{"stop", asm(PUSH1, 0x01, STOP), 100000},
|
||||
{"invalid-opcode", asm(PUSH1, 0x01, INVALID), 100000},
|
||||
{"undefined-opcode", asm(PUSH1, 0x01, 0x0c), 100000},
|
||||
{"stack-underflow", asm(ADD), 100000},
|
||||
{"oog", asm(PUSH1, 0x07, PUSH1, 0x03, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 7},
|
||||
{"invalid-jump", asm(PUSH1, 0x03, JUMP, STOP), 100000},
|
||||
}
|
||||
|
||||
// diffResult captures the observable outcome of running a program.
|
||||
type diffResult struct {
|
||||
ret []byte
|
||||
gasLeft uint64
|
||||
errStr string // "" if no error
|
||||
refund uint64
|
||||
root common.Hash
|
||||
logs []*types.Log
|
||||
}
|
||||
|
||||
func (r diffResult) equal(o diffResult) (string, bool) {
|
||||
if !bytes.Equal(r.ret, o.ret) {
|
||||
return "return data", false
|
||||
}
|
||||
if r.gasLeft != o.gasLeft {
|
||||
return "gas left", false
|
||||
}
|
||||
if r.errStr != o.errStr {
|
||||
return "error", false
|
||||
}
|
||||
if r.refund != o.refund {
|
||||
return "refund", false
|
||||
}
|
||||
if r.root != o.root {
|
||||
return "state root", false
|
||||
}
|
||||
if len(r.logs) != len(o.logs) {
|
||||
return "log count", false
|
||||
}
|
||||
for i := range r.logs {
|
||||
a, b := r.logs[i], o.logs[i]
|
||||
if a.Address != b.Address || !bytes.Equal(a.Data, b.Data) || len(a.Topics) != len(b.Topics) {
|
||||
return "log content", false
|
||||
}
|
||||
for j := range a.Topics {
|
||||
if a.Topics[j] != b.Topics[j] {
|
||||
return "log topic", false
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", true
|
||||
}
|
||||
|
||||
func newDiffState(t testing.TB) *state.StateDB {
|
||||
t.Helper()
|
||||
statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
|
||||
if err != nil {
|
||||
t.Fatalf("state.New: %v", err)
|
||||
}
|
||||
// Main contract: balance + a pre-set storage slot.
|
||||
statedb.CreateAccount(diffContractAddr)
|
||||
statedb.SetBalance(diffContractAddr, uint256.NewInt(1000), tracing.BalanceChangeUnspecified)
|
||||
statedb.SetState(diffContractAddr, common.Hash{31: 0x07}, common.Hash{31: 0x07})
|
||||
// Callee target for CALL/STATICCALL/DELEGATECALL.
|
||||
statedb.CreateAccount(diffCalleeAddr)
|
||||
statedb.SetBalance(diffCalleeAddr, uint256.NewInt(500), tracing.BalanceChangeUnspecified)
|
||||
statedb.SetCode(diffCalleeAddr, diffCalleeCode, tracing.CodeChangeUnspecified)
|
||||
// Caller EOA with a balance.
|
||||
statedb.CreateAccount(diffCaller)
|
||||
statedb.SetBalance(diffCaller, uint256.NewInt(1<<62), tracing.BalanceChangeUnspecified)
|
||||
statedb.Finalise(true)
|
||||
return statedb
|
||||
}
|
||||
|
||||
func diffBlockCtx(merged bool) BlockContext {
|
||||
ctx := BlockContext{
|
||||
CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true },
|
||||
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
|
||||
GetHash: func(uint64) common.Hash { return common.Hash{0xde, 0xad} },
|
||||
Coinbase: common.HexToAddress("0xc01ba5e"),
|
||||
BlockNumber: big.NewInt(8),
|
||||
Time: 1234,
|
||||
Difficulty: big.NewInt(0x20000),
|
||||
GasLimit: 30_000_000,
|
||||
BaseFee: big.NewInt(7),
|
||||
BlobBaseFee: big.NewInt(3),
|
||||
}
|
||||
if merged {
|
||||
h := common.HexToHash("0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20")
|
||||
ctx.Random = &h
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// TestExtraEIPs checks that EIPs enabled via Config.ExtraEips take effect even
|
||||
// when they touch opcodes the generated dispatch inlines. PUSH0 (EIP-3855) on
|
||||
// a pre-Shanghai config is the canary: the runtime table has it enabled but
|
||||
// the baked fork gate does not, so execution must route through the table loop.
|
||||
func TestExtraEIPs(t *testing.T) {
|
||||
code := asm(PUSH0, STOP)
|
||||
statedb := newDiffState(t)
|
||||
statedb.SetCode(diffContractAddr, code, tracing.CodeChangeUnspecified)
|
||||
statedb.Finalise(true)
|
||||
|
||||
evm := NewEVM(diffBlockCtx(false), statedb, params.TestChainConfig, Config{ExtraEips: []int{3855}})
|
||||
evm.SetTxContext(TxContext{
|
||||
Origin: diffCaller,
|
||||
GasPrice: uint256.NewInt(1),
|
||||
})
|
||||
_, _, err := evm.Call(diffCaller, diffContractAddr, nil, NewGasBudget(100000), new(uint256.Int))
|
||||
if err != nil {
|
||||
t.Fatalf("PUSH0 enabled via ExtraEips failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// runOne executes code at diffContractAddr with the given interpreter selection
|
||||
// and returns the observable result.
|
||||
func runOne(t testing.TB, cfg *params.ChainConfig, merged, useTableLoop bool, code, input []byte, gas uint64) diffResult {
|
||||
t.Helper()
|
||||
statedb := newDiffState(t)
|
||||
statedb.SetCode(diffContractAddr, code, tracing.CodeChangeUnspecified)
|
||||
statedb.Finalise(true)
|
||||
|
||||
evm := NewEVM(diffBlockCtx(merged), statedb, cfg, Config{})
|
||||
evm.SetTxContext(TxContext{
|
||||
Origin: diffCaller,
|
||||
GasPrice: uint256.NewInt(1),
|
||||
BlobHashes: []common.Hash{{0xb1, 0x0b}},
|
||||
})
|
||||
evm.forceTableLoop = useTableLoop
|
||||
|
||||
ret, leftOver, err := evm.Call(diffCaller, diffContractAddr, input, NewGasBudget(gas), new(uint256.Int))
|
||||
errStr := ""
|
||||
if err != nil {
|
||||
errStr = err.Error()
|
||||
}
|
||||
return diffResult{
|
||||
ret: ret,
|
||||
gasLeft: leftOver.RegularGas,
|
||||
errStr: errStr,
|
||||
refund: statedb.GetRefund(),
|
||||
root: statedb.IntermediateRoot(true),
|
||||
logs: statedb.Logs(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpreterDiff(t *testing.T) {
|
||||
for _, fk := range diffForks {
|
||||
for _, prog := range diffPrograms {
|
||||
t.Run(fk.name+"/"+prog.name, func(t *testing.T) {
|
||||
input := common.FromHex("0xdeadbeef00000000000000000000000000000000000000000000000000000042")
|
||||
table := runOne(t, fk.cfg, fk.merged, true, prog.code, input, prog.gas)
|
||||
gen := runOne(t, fk.cfg, fk.merged, false, prog.code, input, prog.gas)
|
||||
if where, ok := gen.equal(table); !ok {
|
||||
t.Fatalf("divergence in %s:\n table: ret=%x gas=%d err=%q refund=%d root=%x logs=%d\n gen: ret=%x gas=%d err=%q refund=%d root=%x logs=%d",
|
||||
where,
|
||||
table.ret, table.gasLeft, table.errStr, table.refund, table.root, len(table.logs),
|
||||
gen.ret, gen.gasLeft, gen.errStr, gen.refund, gen.root, len(gen.logs))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FuzzInterpreterDiff fuzzes arbitrary bytecode + calldata + gas and asserts the
|
||||
// generated dispatch matches the table-walking loop on every observable axis.
|
||||
func FuzzInterpreterDiff(f *testing.F) {
|
||||
for _, prog := range diffPrograms {
|
||||
f.Add(prog.code, []byte{0x01, 0x02, 0x03, 0x04}, uint64(100000))
|
||||
}
|
||||
// A couple of structurally-interesting seeds.
|
||||
f.Add(asm(PUSH1, 0x00, JUMPDEST, PUSH1, 0x01, ADD, DUP1, PUSH1, 0xff, GT, PUSH1, 0x02, JUMPI, STOP), []byte{}, uint64(50000))
|
||||
f.Add(bytes.Repeat([]byte{byte(PUSH1), 0x01}, 64), []byte{}, uint64(100000))
|
||||
|
||||
f.Fuzz(func(t *testing.T, code, input []byte, gas uint64) {
|
||||
if len(code) > 24576 { // max contract code size, keep cases realistic
|
||||
return
|
||||
}
|
||||
if gas > 5_000_000 {
|
||||
gas = 5_000_000 // bound execution time
|
||||
}
|
||||
for _, fk := range diffForks {
|
||||
table := runOne(t, fk.cfg, fk.merged, true, code, input, gas)
|
||||
gen := runOne(t, fk.cfg, fk.merged, false, code, input, gas)
|
||||
if where, ok := gen.equal(table); !ok {
|
||||
t.Fatalf("divergence in %s (fork %s): code=%x input=%x gas=%d\n table: ret=%x gas=%d err=%q refund=%d root=%x logs=%d\n gen: ret=%x gas=%d err=%q refund=%d root=%x logs=%d",
|
||||
where, fk.name, code, input, gas,
|
||||
table.ret, table.gasLeft, table.errStr, table.refund, table.root, len(table.logs),
|
||||
gen.ret, gen.gasLeft, gen.errStr, gen.refund, gen.root, len(gen.logs))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
1995
core/vm/interp_gen.go
Normal file
1995
core/vm/interp_gen.go
Normal file
File diff suppressed because it is too large
Load diff
51
core/vm/interp_gen_test.go
Normal file
51
core/vm/interp_gen_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2026 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package vm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestGeneratedDispatchUpToDate asserts that the committed interp_gen.go matches
|
||||
// what `go generate` (core/vm/gen) produces from the current opcode/gas/fork
|
||||
// definitions. It is the CI guard against hand-edits to the generated file and
|
||||
// against the generator drifting from the committed output.
|
||||
func TestGeneratedDispatchUpToDate(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping generator round-trip in -short mode")
|
||||
}
|
||||
tmp := filepath.Join(t.TempDir(), "interp_gen.go")
|
||||
cmd := exec.Command("go", "run", "./gen")
|
||||
cmd.Env = append(os.Environ(), "INTERP_GEN_OUT="+tmp)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("running generator: %v\n%s", err, out)
|
||||
}
|
||||
got, err := os.ReadFile(tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("reading regenerated output: %v", err)
|
||||
}
|
||||
want, err := os.ReadFile("interp_gen.go")
|
||||
if err != nil {
|
||||
t.Fatalf("reading committed interp_gen.go: %v", err)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("interp_gen.go is out of date; run `go generate ./core/vm/...` and commit the result")
|
||||
}
|
||||
}
|
||||
|
|
@ -92,6 +92,9 @@ func (ctx *ScopeContext) ContractCode() []byte {
|
|||
// It's important to note that any errors returned by the interpreter should be
|
||||
// considered a revert-and-consume-all-gas operation except for
|
||||
// ErrExecutionReverted which means revert-and-keep-gas-left.
|
||||
//
|
||||
// Run sets up the per-call scope and picks the interpreter loop. Without a
|
||||
// tracer it runs the generated execUntraced, otherwise execTraced.
|
||||
func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
|
||||
// Increment the call depth which is restricted to 1024
|
||||
evm.depth++
|
||||
|
|
@ -112,17 +115,48 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
|
|||
if len(contract.Code) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
contract.Input = input
|
||||
|
||||
mem := NewMemory() // bound memory
|
||||
stack := evm.arena.stack() // local stack
|
||||
scope := &ScopeContext{
|
||||
Memory: mem,
|
||||
Stack: stack,
|
||||
Contract: contract,
|
||||
}
|
||||
|
||||
// The exit hooks in execTraced still need the stack and memory, so this
|
||||
// teardown must run after them. Defers in execTraced fire before this one,
|
||||
// so the ordering holds.
|
||||
defer func() {
|
||||
stack.release()
|
||||
mem.Free()
|
||||
}()
|
||||
|
||||
// The generated fast path bakes its fork gates at generate time, so it
|
||||
// cannot see table changes made by ExtraEips. Those configs run the table
|
||||
// loop, as do tracing and the differential test via forceTableLoop.
|
||||
if evm.forceTableLoop || len(evm.Config.ExtraEips) > 0 || evm.Config.Tracer != nil {
|
||||
return evm.execTraced(scope)
|
||||
}
|
||||
return evm.execUntraced(scope)
|
||||
}
|
||||
|
||||
// execTraced is the old table-walking interpreter loop. It dispatches every
|
||||
// opcode through the per-fork operation table and emits the tracing hooks
|
||||
// exactly as before. It stays hand-written so trace output does not change.
|
||||
//
|
||||
// The differential test also runs this loop without a tracer and compares it
|
||||
// against the generated execUntraced, which keeps the two from drifting
|
||||
// apart.
|
||||
func (evm *EVM) execTraced(scope *ScopeContext) (ret []byte, err error) {
|
||||
var (
|
||||
op OpCode // current opcode
|
||||
jumpTable *JumpTable = evm.table
|
||||
mem = NewMemory() // bound memory
|
||||
stack = evm.arena.stack() // local stack
|
||||
callContext = &ScopeContext{
|
||||
Memory: mem,
|
||||
Stack: stack,
|
||||
Contract: contract,
|
||||
}
|
||||
mem = scope.Memory
|
||||
stack = scope.Stack
|
||||
contract = scope.Contract
|
||||
callContext = scope
|
||||
// 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.
|
||||
|
|
@ -136,15 +170,6 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
|
|||
debug = evm.Config.Tracer != nil
|
||||
isEIP4762 = 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 pools
|
||||
defer func() {
|
||||
stack.release()
|
||||
mem.Free()
|
||||
}()
|
||||
contract.Input = input
|
||||
|
||||
if debug {
|
||||
defer func() { // this deferred method handles exit-with-error
|
||||
if err == nil {
|
||||
|
|
|
|||
106
core/vm/runtime/evm_bench_test.go
Normal file
106
core/vm/runtime/evm_bench_test.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright 2026 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Whole-contract interpreter benchmarks from the evm-bench suite
|
||||
// (github.com/ziyadedher/evm-bench). Each contract exposes a Benchmark()
|
||||
// entrypoint (selector 0x30627b7c) that performs the whole workload internally
|
||||
// (e.g. 5000 mints), so the benchmark is a single Call per iteration. These
|
||||
// complement the opcode/loop micro-benchmarks in runtime_test.go with realistic
|
||||
// workloads (a ray tracer, ERC-20 transfer/mint/approval, a hashing loop) and
|
||||
// use the same names as evm-bench / gevm for cross-comparison.
|
||||
//
|
||||
// Run: go test ./core/vm/runtime/ -run '^$' -bench 'Benchmark(Snailtracer|TenThousandHashes|ERC20)' -benchmem -count=10
|
||||
// Compare A/B: ./core/vm/runtime/testdata/evm-bench/compare.sh
|
||||
//
|
||||
// The committed runtime bytecode in testdata/*.hex is regenerated by
|
||||
// testdata/evm-bench/gen.sh (solc via docker). See that script for provenance
|
||||
// and exact compiler versions.
|
||||
|
||||
package runtime
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/tracing"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
//go:embed testdata/snailtracer.hex
|
||||
var benchSnailtracerHex string
|
||||
|
||||
//go:embed testdata/tenthousandhashes.hex
|
||||
var benchTenThousandHashesHex string
|
||||
|
||||
//go:embed testdata/erc20transfer.hex
|
||||
var benchERC20TransferHex string
|
||||
|
||||
//go:embed testdata/erc20mint.hex
|
||||
var benchERC20MintHex string
|
||||
|
||||
//go:embed testdata/erc20approval.hex
|
||||
var benchERC20ApprovalHex string
|
||||
|
||||
// benchmarkSelector is keccak256("Benchmark()")[:4], the evm-bench entrypoint.
|
||||
var benchmarkSelector = []byte{0x30, 0x62, 0x7b, 0x7c}
|
||||
|
||||
func benchHexToBytes(s string) []byte {
|
||||
b, err := hex.DecodeString(strings.TrimSpace(s))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// runEVMBench deploys the contract's runtime bytecode and repeatedly invokes its
|
||||
// Benchmark() entrypoint. The contract's constructor is not run, so storage
|
||||
// starts empty. The evm-bench contracts set up whatever state they need inside
|
||||
// Benchmark(), which matches the work the evm-bench harness measures.
|
||||
func runEVMBench(b *testing.B, codeHex string) {
|
||||
code := benchHexToBytes(codeHex)
|
||||
statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
contract := common.BytesToAddress([]byte{0x10})
|
||||
caller := common.BytesToAddress([]byte{0x01})
|
||||
statedb.CreateAccount(caller)
|
||||
statedb.AddBalance(caller, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
|
||||
statedb.SetCode(contract, code, tracing.CodeChangeUnspecified)
|
||||
|
||||
cfg := &Config{Origin: caller, State: statedb, GasLimit: 10_000_000_000}
|
||||
|
||||
// Sanity: the workload must run to completion, not revert/out-of-gas.
|
||||
if _, _, err := Call(contract, benchmarkSelector, cfg); err != nil {
|
||||
b.Fatalf("Benchmark() did not complete: %v", err)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
Call(contract, benchmarkSelector, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSnailtracer(b *testing.B) { runEVMBench(b, benchSnailtracerHex) }
|
||||
func BenchmarkTenThousandHashes(b *testing.B) { runEVMBench(b, benchTenThousandHashesHex) }
|
||||
func BenchmarkERC20Transfer(b *testing.B) { runEVMBench(b, benchERC20TransferHex) }
|
||||
func BenchmarkERC20Mint(b *testing.B) { runEVMBench(b, benchERC20MintHex) }
|
||||
func BenchmarkERC20ApprovalTransfer(b *testing.B) { runEVMBench(b, benchERC20ApprovalHex) }
|
||||
1
core/vm/runtime/testdata/erc20approval.hex
vendored
Normal file
1
core/vm/runtime/testdata/erc20approval.hex
vendored
Normal file
File diff suppressed because one or more lines are too long
1
core/vm/runtime/testdata/erc20mint.hex
vendored
Normal file
1
core/vm/runtime/testdata/erc20mint.hex
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
608060405234801561001057600080fd5b50600436106100b45760003560e01c80633950935111610071578063395093511461013857806370a082311461014b57806395d89b4114610174578063a457c2d71461017c578063a9059cbb1461018f578063dd62ed3e146101a257600080fd5b806306fdde03146100b9578063095ea7b3146100d757806318160ddd146100fa57806323b872dd1461010c57806330627b7c1461011f578063313ce56714610129575b600080fd5b6100c16101b5565b6040516100ce919061079c565b60405180910390f35b6100ea6100e5366004610806565b610247565b60405190151581526020016100ce565b6002545b6040519081526020016100ce565b6100ea61011a366004610830565b610261565b610127610285565b005b604051601281526020016100ce565b6100ea610146366004610806565b6102b1565b6100fe61015936600461086c565b6001600160a01b031660009081526020819052604090205490565b6100c16102d3565b6100ea61018a366004610806565b6102e2565b6100ea61019d366004610806565b610362565b6100fe6101b036600461088e565b610370565b6060600380546101c4906108c1565b80601f01602080910402602001604051908101604052809291908181526020018280546101f0906108c1565b801561023d5780601f106102125761010080835404028352916020019161023d565b820191906000526020600020905b81548152906001019060200180831161022057829003601f168201915b5050505050905090565b60003361025581858561039b565b60019150505b92915050565b60003361026f8582856104bf565b61027a858585610539565b506001949350505050565b60005b6113888110156102ae5761029c33826106dd565b806102a681610911565b915050610288565b50565b6000336102558185856102c48383610370565b6102ce919061092a565b61039b565b6060600480546101c4906108c1565b600033816102f08286610370565b9050838110156103555760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b60648201526084015b60405180910390fd5b61027a828686840361039b565b600033610255818585610539565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6001600160a01b0383166103fd5760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b606482015260840161034c565b6001600160a01b03821661045e5760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b606482015260840161034c565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b60006104cb8484610370565b9050600019811461053357818110156105265760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000604482015260640161034c565b610533848484840361039b565b50505050565b6001600160a01b03831661059d5760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b606482015260840161034c565b6001600160a01b0382166105ff5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b606482015260840161034c565b6001600160a01b038316600090815260208190526040902054818110156106775760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b606482015260840161034c565b6001600160a01b03848116600081815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a3610533565b6001600160a01b0382166107335760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f206164647265737300604482015260640161034c565b8060026000828254610745919061092a565b90915550506001600160a01b038216600081815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b600060208083528351808285015260005b818110156107c9578581018301518582016040015282016107ad565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b038116811461080157600080fd5b919050565b6000806040838503121561081957600080fd5b610822836107ea565b946020939093013593505050565b60008060006060848603121561084557600080fd5b61084e846107ea565b925061085c602085016107ea565b9150604084013590509250925092565b60006020828403121561087e57600080fd5b610887826107ea565b9392505050565b600080604083850312156108a157600080fd5b6108aa836107ea565b91506108b8602084016107ea565b90509250929050565b600181811c908216806108d557607f821691505b6020821081036108f557634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b600060018201610923576109236108fb565b5060010190565b8082018082111561025b5761025b6108fb56fea2646970667358221220fa5dc8b94ca8266a9f37b573a3ac8993380ce8da74caa7732abd6e7ed31cd37e64736f6c63430008110033
|
||||
1
core/vm/runtime/testdata/erc20transfer.hex
vendored
Normal file
1
core/vm/runtime/testdata/erc20transfer.hex
vendored
Normal file
File diff suppressed because one or more lines are too long
56
core/vm/runtime/testdata/evm-bench/README.md
vendored
Normal file
56
core/vm/runtime/testdata/evm-bench/README.md
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# evm-bench whole-contract interpreter benchmarks
|
||||
|
||||
Realistic-workload benchmarks for the `core/vm` interpreter, driven by contracts
|
||||
from the [evm-bench](https://github.com/ziyadedher/evm-bench) suite. They
|
||||
complement the opcode/loop micro-benchmarks in `runtime_test.go`.
|
||||
|
||||
- Benchmark code: [`core/vm/runtime/evm_bench_test.go`](../../evm_bench_test.go)
|
||||
- Runtime bytecode: `core/vm/runtime/testdata/*.hex` (committed, `//go:embed`-ed)
|
||||
|
||||
Each contract exposes a `Benchmark()` entrypoint (selector `0x30627b7c`) that
|
||||
performs the entire workload internally, so one `runtime.Call` = one full
|
||||
workload:
|
||||
|
||||
| benchmark | workload |
|
||||
|---|---|
|
||||
| `Snailtracer` | a ray tracer, compute and memory heavy |
|
||||
| `ERC20Transfer` | ERC-20 transfers in a loop |
|
||||
| `ERC20Mint` | 5000 `_mint`s (SSTORE/keccak/LOG-heavy) |
|
||||
| `ERC20ApprovalTransfer` | 1000 approve+transferFrom cycles |
|
||||
| `TenThousandHashes` | a keccak loop |
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
# all of them, current build
|
||||
go test ./core/vm/runtime/ -run '^$' -bench 'Benchmark(Snailtracer|TenThousandHashes|ERC20)' -benchmem -count=10
|
||||
|
||||
# A/B vs a baseline (default master), with benchstat
|
||||
core/vm/runtime/testdata/evm-bench/compare.sh # vs master
|
||||
core/vm/runtime/testdata/evm-bench/compare.sh HEAD~1 8 # vs another ref, count=8
|
||||
```
|
||||
|
||||
`compare.sh` benches the current working tree and a baseline ref (checked out in
|
||||
a throwaway worktree with this suite copied in, so it works whether or not the
|
||||
interpreter change is committed) and runs `benchstat`.
|
||||
|
||||
## Regenerating the bytecode
|
||||
|
||||
[`gen.sh`](gen.sh) regenerates the ERC-20 and ten-thousand-hashes `.hex` from
|
||||
the evm-bench Solidity sources (solc 0.8.17 via docker). These contracts set up
|
||||
their state inside `Benchmark()`, so the runtime bytecode is callable directly.
|
||||
|
||||
`snailtracer.hex` is **vendored, not regenerated by gen.sh**: evm-bench's
|
||||
`SnailTracer` initializes its scene in the constructor, so its runtime bytecode
|
||||
(no constructor) renders an empty scene. The committed file is a runtime-callable
|
||||
build (scene encoded in code) vendored from
|
||||
[Giulio2002/gevm](https://github.com/Giulio2002/gevm)'s gethbench testdata.
|
||||
|
||||
The `.hex` files are committed, so running the benchmarks needs no toolchain.
|
||||
|
||||
## Notes
|
||||
|
||||
- The contract constructor is not executed, so storage starts empty. The
|
||||
evm-bench contracts set up whatever state they need inside `Benchmark()`,
|
||||
matching the work the evm-bench harness measures.
|
||||
- Names match evm-bench / gevm for cross-comparison.
|
||||
61
core/vm/runtime/testdata/evm-bench/compare.sh
vendored
Executable file
61
core/vm/runtime/testdata/evm-bench/compare.sh
vendored
Executable file
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env bash
|
||||
# A/B benchmark of the codegen interpreter vs a baseline: runs the evm-bench
|
||||
# contract workloads (core/vm/runtime/evm_bench_test.go) plus the synthetic
|
||||
# dispatch loops (BenchmarkSimpleLoop/loop*) on the current working tree and on
|
||||
# a baseline ref, then benchstats them.
|
||||
#
|
||||
# The baseline is checked out in a throwaway git worktree and this suite is
|
||||
# copied into it, so the comparison works whether or not the interpreter changes
|
||||
# are committed yet. Requires: go, git, and benchstat
|
||||
# (go install golang.org/x/perf/cmd/benchstat@latest).
|
||||
#
|
||||
# Usage: core/vm/runtime/testdata/evm-bench/compare.sh [baseref] [count]
|
||||
# baseref defaults to "master", count to 10.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../../../../.." && pwd)" # repo root
|
||||
cd "$ROOT"
|
||||
BASEREF="${1:-master}"
|
||||
COUNT="${2:-10}"
|
||||
# Each benchmark runs a FIXED iteration count instead of the default 1s of
|
||||
# benchtime. With time-based benchtime the faster side runs more iterations,
|
||||
# so one-time costs (map growth, pool warmup) amortize over a different N and
|
||||
# B/op picks up phantom deltas, and GC pacing can do the same to sec/op. Fixed
|
||||
# N makes both sides do identical work. The counts target roughly one second
|
||||
# per count on a fast box. Each entry is pattern:iterations, and SimpleLoop
|
||||
# needs the /^loop element to select only its loop variants (go test splits
|
||||
# -bench on / and benchmarks without sub-benchmarks cannot match a two-element
|
||||
# pattern, so the loops run as their own invocation anyway).
|
||||
BENCHES=(
|
||||
'^BenchmarkSnailtracer$:20x'
|
||||
'^BenchmarkTenThousandHashes$:200x'
|
||||
'^BenchmarkERC20Transfer$:100x'
|
||||
'^BenchmarkERC20Mint$:150x'
|
||||
'^BenchmarkERC20ApprovalTransfer$:120x'
|
||||
'^BenchmarkSimpleLoop$/^loop:7x'
|
||||
)
|
||||
NEW="$(mktemp)"; OLD="$(mktemp)"
|
||||
|
||||
run_suite() { # run_suite <dir> <outfile>
|
||||
local dir="$1" out="$2" entry pat n
|
||||
for entry in "${BENCHES[@]}"; do
|
||||
pat="${entry%:*}"
|
||||
n="${entry##*:}"
|
||||
( cd "$dir" && go test ./core/vm/runtime/ -run '^$' -bench "$pat" -benchmem -benchtime "$n" -count="$COUNT" ) | tee -a "$out"
|
||||
done
|
||||
}
|
||||
|
||||
echo "==> current working tree"
|
||||
run_suite "$ROOT" "$NEW"
|
||||
|
||||
echo "==> baseline: $BASEREF (throwaway worktree, suite copied in)"
|
||||
WT="$(mktemp -d)"
|
||||
git worktree add --quiet --detach "$WT" "$BASEREF"
|
||||
cp core/vm/runtime/evm_bench_test.go "$WT/core/vm/runtime/"
|
||||
mkdir -p "$WT/core/vm/runtime/testdata"
|
||||
cp core/vm/runtime/testdata/*.hex "$WT/core/vm/runtime/testdata/"
|
||||
run_suite "$WT" "$OLD"
|
||||
git worktree remove --force "$WT"
|
||||
|
||||
echo "==> benchstat: $BASEREF (left) vs working tree (right)"
|
||||
benchstat "$OLD" "$NEW"
|
||||
43
core/vm/runtime/testdata/evm-bench/gen.sh
vendored
Executable file
43
core/vm/runtime/testdata/evm-bench/gen.sh
vendored
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env bash
|
||||
# Regenerates core/vm/runtime/testdata/*.hex, the runtime bytecode for the
|
||||
# whole-contract interpreter benchmarks (see core/vm/runtime/evm_bench_test.go).
|
||||
#
|
||||
# Source: the evm-bench suite (github.com/ziyadedher/evm-bench). Each contract
|
||||
# exposes Benchmark() (selector 0x30627b7c) which performs the whole workload.
|
||||
# Compiled with solc via docker (no local solc needed). solc versions match each
|
||||
# contract's pragma: 0.8.17 for the ERC-20/hashing contracts, 0.4.26 for the
|
||||
# legacy SnailTracer.
|
||||
#
|
||||
# Usage: core/vm/runtime/testdata/evm-bench/gen.sh
|
||||
set -euo pipefail
|
||||
|
||||
TD="$(cd "$(dirname "$0")/.." && pwd)" # .../core/vm/runtime/testdata
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
git clone --depth 1 https://github.com/ziyadedher/evm-bench "$WORK/evm-bench" >/dev/null 2>&1
|
||||
B="$WORK/evm-bench/benchmarks"
|
||||
|
||||
# ERC-20 (transfer/mint/approval) + ten-thousand-hashes: pragma ^0.8.17. These
|
||||
# set up all their state inside Benchmark(), so the runtime bytecode is callable
|
||||
# directly with no constructor, which is how the benchmark drives them.
|
||||
docker run --rm -v "$B":/src -w /src ethereum/solc:0.8.17 \
|
||||
--optimize --bin-runtime --overwrite -o /src/out \
|
||||
erc20/transfer/ERC20Transfer.sol \
|
||||
erc20/mint/ERC20Mint.sol \
|
||||
erc20/approval-transfer/ERC20ApprovalTransfer.sol \
|
||||
ten-thousand-hashes/TenThousandHashes.sol
|
||||
|
||||
cp "$B/out/TenThousandHashes.bin-runtime" "$TD/tenthousandhashes.hex"
|
||||
cp "$B/out/ERC20Transfer.bin-runtime" "$TD/erc20transfer.hex"
|
||||
cp "$B/out/ERC20Mint.bin-runtime" "$TD/erc20mint.hex"
|
||||
cp "$B/out/ERC20ApprovalTransfer.bin-runtime" "$TD/erc20approval.hex"
|
||||
|
||||
# NOTE: snailtracer.hex is not regenerated here. evm-bench's SnailTracer
|
||||
# initializes its scene in the constructor, so its --bin-runtime (no constructor)
|
||||
# renders an empty scene. The committed snailtracer.hex is a runtime-callable
|
||||
# build (scene encoded in code, Benchmark() self-contained) vendored from
|
||||
# Giulio2002/gevm's gethbench testdata. Leave it as-is.
|
||||
|
||||
echo "regenerated (snailtracer.hex left vendored):"
|
||||
ls -l "$TD"/*.hex
|
||||
1
core/vm/runtime/testdata/snailtracer.hex
vendored
Normal file
1
core/vm/runtime/testdata/snailtracer.hex
vendored
Normal file
File diff suppressed because one or more lines are too long
1
core/vm/runtime/testdata/tenthousandhashes.hex
vendored
Normal file
1
core/vm/runtime/testdata/tenthousandhashes.hex
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
6080604052348015600f57600080fd5b506004361060285760003560e01c806330627b7c14602d575b600080fd5b60336035565b005b60005b614e20811015606a5760408051602081018390520160408051601f198184030190525280606381606d565b9150506038565b50565b600060018201608c57634e487b7160e01b600052601160045260246000fd5b506001019056fea2646970667358221220e499ea6d1ccd5fce28e8cd2507f6fef33f0d005543e04580f290bbd01df0655464736f6c63430008110033
|
||||
Loading…
Reference in a new issue