From 419e5e1438a604b912e9e6a27708e32dfaf1ec44 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Wed, 10 Jun 2026 14:34:46 -0500
Subject: [PATCH 01/23] core/vm: replace interpreter dispatch with a generated
switch
---
core/vm/evm.go | 5 +
core/vm/gen/main.go | 497 ++++
core/vm/genspec.go | 106 +
core/vm/interp_diff_test.go | 365 +++
core/vm/interp_gen.go | 1995 +++++++++++++++++
core/vm/interp_gen_test.go | 51 +
core/vm/interpreter.go | 57 +-
core/vm/runtime/evm_bench_test.go | 106 +
core/vm/runtime/testdata/erc20approval.hex | 1 +
core/vm/runtime/testdata/erc20mint.hex | 1 +
core/vm/runtime/testdata/erc20transfer.hex | 1 +
core/vm/runtime/testdata/evm-bench/README.md | 56 +
core/vm/runtime/testdata/evm-bench/compare.sh | 61 +
core/vm/runtime/testdata/evm-bench/gen.sh | 43 +
core/vm/runtime/testdata/snailtracer.hex | 1 +
.../vm/runtime/testdata/tenthousandhashes.hex | 1 +
16 files changed, 3331 insertions(+), 16 deletions(-)
create mode 100644 core/vm/gen/main.go
create mode 100644 core/vm/genspec.go
create mode 100644 core/vm/interp_diff_test.go
create mode 100644 core/vm/interp_gen.go
create mode 100644 core/vm/interp_gen_test.go
create mode 100644 core/vm/runtime/evm_bench_test.go
create mode 100644 core/vm/runtime/testdata/erc20approval.hex
create mode 100644 core/vm/runtime/testdata/erc20mint.hex
create mode 100644 core/vm/runtime/testdata/erc20transfer.hex
create mode 100644 core/vm/runtime/testdata/evm-bench/README.md
create mode 100755 core/vm/runtime/testdata/evm-bench/compare.sh
create mode 100755 core/vm/runtime/testdata/evm-bench/gen.sh
create mode 100644 core/vm/runtime/testdata/snailtracer.hex
create mode 100644 core/vm/runtime/testdata/tenthousandhashes.hex
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 18aa50a8c8..b47adc8ed5 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -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
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
new file mode 100644
index 0000000000..2e7c0b45a3
--- /dev/null
+++ b/core/vm/gen/main.go
@@ -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 .
+
+// 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 = 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))
+}
diff --git a/core/vm/genspec.go b/core/vm/genspec.go
new file mode 100644
index 0000000000..c1e64cdc2a
--- /dev/null
+++ b/core/vm/genspec.go
@@ -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 .
+
+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
+}
diff --git a/core/vm/interp_diff_test.go b/core/vm/interp_diff_test.go
new file mode 100644
index 0000000000..0b32959d86
--- /dev/null
+++ b/core/vm/interp_diff_test.go
@@ -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 .
+
+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))
+ }
+ }
+ })
+}
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
new file mode 100644
index 0000000000..a2b3c6ed71
--- /dev/null
+++ b/core/vm/interp_gen.go
@@ -0,0 +1,1995 @@
+// Code generated by core/vm/gen; DO NOT EDIT.
+
+package vm
+
+import (
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/core/tracing"
+)
+
+// 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 {
+ case ADD:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ y.Add(&x, y)
+ pc++
+ continue mainLoop
+
+ case MUL:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 5 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 5
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ y.Mul(&x, y)
+ pc++
+ continue mainLoop
+
+ case SUB:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ y.Sub(&x, y)
+ pc++
+ continue mainLoop
+
+ case DIV:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 5 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 5
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ y.Div(&x, y)
+ pc++
+ continue mainLoop
+
+ case SDIV:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 5 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 5
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ y.SDiv(&x, y)
+ pc++
+ continue mainLoop
+
+ case MOD:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 5 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 5
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ y.Mod(&x, y)
+ pc++
+ continue mainLoop
+
+ case SMOD:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 5 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 5
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ y.SMod(&x, y)
+ pc++
+ continue mainLoop
+
+ case ADDMOD:
+ if sLen := stack.len(); sLen < 3 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 3}
+ }
+ if contract.Gas.RegularGas < 8 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 8
+ x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek()
+ z.AddMod(&x, &y, z)
+ pc++
+ continue mainLoop
+
+ case MULMOD:
+ if sLen := stack.len(); sLen < 3 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 3}
+ }
+ if contract.Gas.RegularGas < 8 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 8
+ x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek()
+ z.MulMod(&x, &y, z)
+ pc++
+ continue mainLoop
+
+ case SIGNEXTEND:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 5 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 5
+ back, num := scope.Stack.pop(), scope.Stack.peek()
+ num.ExtendSign(num, &back)
+ pc++
+ continue mainLoop
+
+ case LT:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ if x.Lt(y) {
+ y.SetOne()
+ } else {
+ y.Clear()
+ }
+ pc++
+ continue mainLoop
+
+ case GT:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ if x.Gt(y) {
+ y.SetOne()
+ } else {
+ y.Clear()
+ }
+ pc++
+ continue mainLoop
+
+ case SLT:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ if x.Slt(y) {
+ y.SetOne()
+ } else {
+ y.Clear()
+ }
+ pc++
+ continue mainLoop
+
+ case SGT:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ if x.Sgt(y) {
+ y.SetOne()
+ } else {
+ y.Clear()
+ }
+ pc++
+ continue mainLoop
+
+ case EQ:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ if x.Eq(y) {
+ y.SetOne()
+ } else {
+ y.Clear()
+ }
+ pc++
+ continue mainLoop
+
+ case ISZERO:
+ if sLen := stack.len(); sLen < 1 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x := scope.Stack.peek()
+ if x.IsZero() {
+ x.SetOne()
+ } else {
+ x.Clear()
+ }
+ pc++
+ continue mainLoop
+
+ case AND:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ y.And(&x, y)
+ pc++
+ continue mainLoop
+
+ case OR:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ y.Or(&x, y)
+ pc++
+ continue mainLoop
+
+ case XOR:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x, y := scope.Stack.pop(), scope.Stack.peek()
+ y.Xor(&x, y)
+ pc++
+ continue mainLoop
+
+ case NOT:
+ if sLen := stack.len(); sLen < 1 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ x := scope.Stack.peek()
+ x.Not(x)
+ pc++
+ continue mainLoop
+
+ case BYTE:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ th, val := scope.Stack.pop(), scope.Stack.peek()
+ val.Byte(&th)
+ pc++
+ continue mainLoop
+
+ case SHL:
+ if rules.IsConstantinople {
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ shift, value := scope.Stack.pop(), scope.Stack.peek()
+ if shift.LtUint64(256) {
+ value.Lsh(value, uint(shift.Uint64()))
+ } else {
+ value.Clear()
+ }
+ pc++
+ continue mainLoop
+
+ }
+ res, err = opUndefined(&pc, evm, scope)
+ break mainLoop
+ case SHR:
+ if rules.IsConstantinople {
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ shift, value := scope.Stack.pop(), scope.Stack.peek()
+ if shift.LtUint64(256) {
+ value.Rsh(value, uint(shift.Uint64()))
+ } else {
+ value.Clear()
+ }
+ pc++
+ continue mainLoop
+
+ }
+ res, err = opUndefined(&pc, evm, scope)
+ break mainLoop
+ case SAR:
+ if rules.IsConstantinople {
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ shift, value := scope.Stack.pop(), scope.Stack.peek()
+ if shift.GtUint64(256) {
+ if value.Sign() >= 0 {
+ value.Clear()
+ } else {
+
+ value.SetAllOne()
+ }
+ pc++
+ continue mainLoop
+ }
+ n := uint(shift.Uint64())
+ value.SRsh(value, n)
+ pc++
+ continue mainLoop
+
+ }
+ res, err = opUndefined(&pc, evm, scope)
+ break mainLoop
+ case CLZ:
+ if rules.IsOsaka {
+ if sLen := stack.len(); sLen < 1 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
+ }
+ if contract.Gas.RegularGas < 5 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 5
+ x := scope.Stack.peek()
+ x.SetUint64(256 - uint64(x.BitLen()))
+ pc++
+ continue mainLoop
+
+ }
+ res, err = opUndefined(&pc, evm, scope)
+ break mainLoop
+ case KECCAK256:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 30 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 30
+ var memorySize uint64
+ {
+ memSize, overflow := memoryKeccak256(stack)
+ if overflow {
+ return nil, ErrGasUintOverflow
+ }
+ if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
+ return nil, ErrGasUintOverflow
+ }
+ }
+ var dynamicCost GasCosts
+ dynamicCost, err = gasKeccak256(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 = opKeccak256(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ case POP:
+ if sLen := stack.len(); sLen < 1 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
+ }
+ if contract.Gas.RegularGas < 2 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 2
+ scope.Stack.pop()
+ pc++
+ continue mainLoop
+
+ case MLOAD:
+ if sLen := stack.len(); sLen < 1 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ var memorySize uint64
+ {
+ memSize, overflow := memoryMLoad(stack)
+ if overflow {
+ return nil, ErrGasUintOverflow
+ }
+ if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
+ return nil, ErrGasUintOverflow
+ }
+ }
+ var dynamicCost GasCosts
+ dynamicCost, err = gasMLoad(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 = opMload(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ case MSTORE:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ var memorySize uint64
+ {
+ memSize, overflow := memoryMStore(stack)
+ if overflow {
+ return nil, ErrGasUintOverflow
+ }
+ if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
+ return nil, ErrGasUintOverflow
+ }
+ }
+ var dynamicCost GasCosts
+ dynamicCost, err = gasMStore(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 = opMstore(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ case MSTORE8:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ var memorySize uint64
+ {
+ memSize, overflow := memoryMStore8(stack)
+ if overflow {
+ return nil, ErrGasUintOverflow
+ }
+ if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
+ return nil, ErrGasUintOverflow
+ }
+ }
+ var dynamicCost GasCosts
+ dynamicCost, err = gasMStore8(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 = opMstore8(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ case JUMP:
+ if sLen := stack.len(); sLen < 1 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
+ }
+ if contract.Gas.RegularGas < 8 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 8
+ if evm.abort.Load() {
+ res, err = nil, errStopToken
+ break mainLoop
+ }
+ pos := scope.Stack.pop()
+ if !scope.Contract.validJumpdest(&pos) {
+ res, err = nil, ErrInvalidJump
+ break mainLoop
+ }
+ pc = pos.Uint64() - 1
+ pc++
+ continue mainLoop
+
+ case JUMPI:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 10 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 10
+ if evm.abort.Load() {
+ res, err = nil, errStopToken
+ break mainLoop
+ }
+ pos, cond := scope.Stack.pop(), scope.Stack.pop()
+ if !cond.IsZero() {
+ if !scope.Contract.validJumpdest(&pos) {
+ res, err = nil, ErrInvalidJump
+ break mainLoop
+ }
+ pc = pos.Uint64() - 1
+ }
+ pc++
+ continue mainLoop
+
+ case PC:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 2 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 2
+ scope.Stack.get().SetUint64(pc)
+ pc++
+ continue mainLoop
+
+ case MSIZE:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 2 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 2
+ scope.Stack.get().SetUint64(uint64(scope.Memory.Len()))
+ pc++
+ continue mainLoop
+
+ case JUMPDEST:
+ if contract.Gas.RegularGas < 1 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 1
+ pc++
+ continue mainLoop
+
+ case PUSH0:
+ if rules.IsShanghai {
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 2 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 2
+ scope.Stack.get().Clear()
+ pc++
+ continue mainLoop
+
+ }
+ res, err = opUndefined(&pc, evm, scope)
+ break mainLoop
+ case PUSH1:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ var (
+ codeLen = uint64(len(scope.Contract.Code))
+ elem = scope.Stack.get()
+ )
+ pc += 1
+ if pc < codeLen {
+ elem.SetUint64(uint64(scope.Contract.Code[pc]))
+ } else {
+ elem.Clear()
+ }
+ pc++
+ continue mainLoop
+
+ case PUSH2:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ var (
+ codeLen = uint64(len(scope.Contract.Code))
+ elem = scope.Stack.get()
+ )
+ if pc+2 < codeLen {
+ elem.SetBytes2(scope.Contract.Code[pc+1 : pc+3])
+ } else if pc+1 < codeLen {
+ elem.SetUint64(uint64(scope.Contract.Code[pc+1]) << 8)
+ } else {
+ elem.Clear()
+ }
+ pc += 2
+ pc++
+ continue mainLoop
+
+ case PUSH3:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+3)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 3 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 3
+ pc++
+ continue mainLoop
+ case PUSH4:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+4)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 4 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 4
+ pc++
+ continue mainLoop
+ case PUSH5:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+5)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 5 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 5
+ pc++
+ continue mainLoop
+ case PUSH6:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+6)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 6 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 6
+ pc++
+ continue mainLoop
+ case PUSH7:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+7)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 7 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 7
+ pc++
+ continue mainLoop
+ case PUSH8:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+8)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 8 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 8
+ pc++
+ continue mainLoop
+ case PUSH9:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+9)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 9 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 9
+ pc++
+ continue mainLoop
+ case PUSH10:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+10)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 10 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 10
+ pc++
+ continue mainLoop
+ case PUSH11:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+11)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 11 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 11
+ pc++
+ continue mainLoop
+ case PUSH12:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+12)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 12 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 12
+ pc++
+ continue mainLoop
+ case PUSH13:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+13)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 13 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 13
+ pc++
+ continue mainLoop
+ case PUSH14:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+14)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 14 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 14
+ pc++
+ continue mainLoop
+ case PUSH15:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+15)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 15 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 15
+ pc++
+ continue mainLoop
+ case PUSH16:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+16)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 16 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 16
+ pc++
+ continue mainLoop
+ case PUSH17:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+17)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 17 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 17
+ pc++
+ continue mainLoop
+ case PUSH18:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+18)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 18 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 18
+ pc++
+ continue mainLoop
+ case PUSH19:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+19)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 19 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 19
+ pc++
+ continue mainLoop
+ case PUSH20:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+20)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 20 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 20
+ pc++
+ continue mainLoop
+ case PUSH21:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+21)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 21 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 21
+ pc++
+ continue mainLoop
+ case PUSH22:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+22)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 22 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 22
+ pc++
+ continue mainLoop
+ case PUSH23:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+23)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 23 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 23
+ pc++
+ continue mainLoop
+ case PUSH24:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+24)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 24 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 24
+ pc++
+ continue mainLoop
+ case PUSH25:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+25)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 25 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 25
+ pc++
+ continue mainLoop
+ case PUSH26:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+26)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 26 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 26
+ pc++
+ continue mainLoop
+ case PUSH27:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+27)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 27 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 27
+ pc++
+ continue mainLoop
+ case PUSH28:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+28)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 28 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 28
+ pc++
+ continue mainLoop
+ case PUSH29:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+29)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 29 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 29
+ pc++
+ continue mainLoop
+ case PUSH30:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+30)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 30 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 30
+ pc++
+ continue mainLoop
+ case PUSH31:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+31)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 31 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 31
+ pc++
+ continue mainLoop
+ case PUSH32:
+ if sLen := stack.len(); sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+32)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := 32 - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += 32
+ pc++
+ continue mainLoop
+ case DUP1:
+ if sLen := stack.len(); sLen < 1 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(1)
+ pc++
+ continue mainLoop
+ case DUP2:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(2)
+ pc++
+ continue mainLoop
+ case DUP3:
+ if sLen := stack.len(); sLen < 3 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 3}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(3)
+ pc++
+ continue mainLoop
+ case DUP4:
+ if sLen := stack.len(); sLen < 4 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 4}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(4)
+ pc++
+ continue mainLoop
+ case DUP5:
+ if sLen := stack.len(); sLen < 5 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 5}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(5)
+ pc++
+ continue mainLoop
+ case DUP6:
+ if sLen := stack.len(); sLen < 6 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 6}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(6)
+ pc++
+ continue mainLoop
+ case DUP7:
+ if sLen := stack.len(); sLen < 7 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 7}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(7)
+ pc++
+ continue mainLoop
+ case DUP8:
+ if sLen := stack.len(); sLen < 8 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 8}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(8)
+ pc++
+ continue mainLoop
+ case DUP9:
+ if sLen := stack.len(); sLen < 9 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 9}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(9)
+ pc++
+ continue mainLoop
+ case DUP10:
+ if sLen := stack.len(); sLen < 10 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 10}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(10)
+ pc++
+ continue mainLoop
+ case DUP11:
+ if sLen := stack.len(); sLen < 11 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 11}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(11)
+ pc++
+ continue mainLoop
+ case DUP12:
+ if sLen := stack.len(); sLen < 12 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 12}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(12)
+ pc++
+ continue mainLoop
+ case DUP13:
+ if sLen := stack.len(); sLen < 13 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 13}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(13)
+ pc++
+ continue mainLoop
+ case DUP14:
+ if sLen := stack.len(); sLen < 14 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 14}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(14)
+ pc++
+ continue mainLoop
+ case DUP15:
+ if sLen := stack.len(); sLen < 15 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 15}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(15)
+ pc++
+ continue mainLoop
+ case DUP16:
+ if sLen := stack.len(); sLen < 16 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 16}
+ } else if sLen > 1023 {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.dup(16)
+ pc++
+ continue mainLoop
+ case SWAP1:
+ if sLen := stack.len(); sLen < 2 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap1()
+ pc++
+ continue mainLoop
+
+ case SWAP2:
+ if sLen := stack.len(); sLen < 3 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 3}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap2()
+ pc++
+ continue mainLoop
+
+ case SWAP3:
+ if sLen := stack.len(); sLen < 4 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 4}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap3()
+ pc++
+ continue mainLoop
+
+ case SWAP4:
+ if sLen := stack.len(); sLen < 5 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 5}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap4()
+ pc++
+ continue mainLoop
+
+ case SWAP5:
+ if sLen := stack.len(); sLen < 6 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 6}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap5()
+ pc++
+ continue mainLoop
+
+ case SWAP6:
+ if sLen := stack.len(); sLen < 7 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 7}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap6()
+ pc++
+ continue mainLoop
+
+ case SWAP7:
+ if sLen := stack.len(); sLen < 8 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 8}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap7()
+ pc++
+ continue mainLoop
+
+ case SWAP8:
+ if sLen := stack.len(); sLen < 9 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 9}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap8()
+ pc++
+ continue mainLoop
+
+ case SWAP9:
+ if sLen := stack.len(); sLen < 10 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 10}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap9()
+ pc++
+ continue mainLoop
+
+ case SWAP10:
+ if sLen := stack.len(); sLen < 11 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 11}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap10()
+ pc++
+ continue mainLoop
+
+ case SWAP11:
+ if sLen := stack.len(); sLen < 12 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 12}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap11()
+ pc++
+ continue mainLoop
+
+ case SWAP12:
+ if sLen := stack.len(); sLen < 13 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 13}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap12()
+ pc++
+ continue mainLoop
+
+ case SWAP13:
+ if sLen := stack.len(); sLen < 14 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 14}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap13()
+ pc++
+ continue mainLoop
+
+ case SWAP14:
+ if sLen := stack.len(); sLen < 15 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 15}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap14()
+ pc++
+ continue mainLoop
+
+ case SWAP15:
+ if sLen := stack.len(); sLen < 16 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 16}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap15()
+ pc++
+ continue mainLoop
+
+ case SWAP16:
+ if sLen := stack.len(); sLen < 17 {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: 17}
+ }
+ if contract.Gas.RegularGas < 3 {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= 3
+ scope.Stack.swap16()
+ pc++
+ continue mainLoop
+
+ 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
+ }
+ }
+ if err == errStopToken {
+ err = nil
+ }
+ return res, err
+}
diff --git a/core/vm/interp_gen_test.go b/core/vm/interp_gen_test.go
new file mode 100644
index 0000000000..83b4bb1ed9
--- /dev/null
+++ b/core/vm/interp_gen_test.go
@@ -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 .
+
+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")
+ }
+}
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index c2dfe3769c..4c0dd799dc 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -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 {
diff --git a/core/vm/runtime/evm_bench_test.go b/core/vm/runtime/evm_bench_test.go
new file mode 100644
index 0000000000..31a265c1f6
--- /dev/null
+++ b/core/vm/runtime/evm_bench_test.go
@@ -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 .
+
+// 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) }
diff --git a/core/vm/runtime/testdata/erc20approval.hex b/core/vm/runtime/testdata/erc20approval.hex
new file mode 100644
index 0000000000..0700413573
--- /dev/null
+++ b/core/vm/runtime/testdata/erc20approval.hex
@@ -0,0 +1 @@
+608060405234801561001057600080fd5b50600436106100b45760003560e01c80633950935111610071578063395093511461013857806370a082311461014b57806395d89b4114610174578063a457c2d71461017c578063a9059cbb1461018f578063dd62ed3e146101a257600080fd5b806306fdde03146100b9578063095ea7b3146100d757806318160ddd146100fa57806323b872dd1461010c57806330627b7c1461011f578063313ce56714610129575b600080fd5b6100c16101b5565b6040516100ce91906108ca565b60405180910390f35b6100ea6100e5366004610934565b610247565b60405190151581526020016100ce565b6002545b6040519081526020016100ce565b6100ea61011a36600461095e565b610261565b610127610285565b005b604051601281526020016100ce565b6100ea610146366004610934565b6103e4565b6100fe61015936600461099a565b6001600160a01b031660009081526020819052604090205490565b6100c1610406565b6100ea61018a366004610934565b610415565b6100ea61019d366004610934565b610490565b6100fe6101b03660046109bc565b61049e565b6060600380546101c4906109ef565b80601f01602080910402602001604051908101604052809291908181526020018280546101f0906109ef565b801561023d5780601f106102125761010080835404028352916020019161023d565b820191906000526020600020905b81548152906001019060200180831161022057829003601f168201915b5050505050905090565b6000336102558185856104c9565b60019150505b92915050565b60003361026f8582856105ed565b61027a858585610667565b506001949350505050565b6102a8336102956012600a610b23565b6102a390633b9aca00610b32565b61080b565b60015b6103e88110156103e1576102bf333361049e565b156103115760405162461bcd60e51b815260206004820152601b60248201527f6e6f6e2d7a65726f20616c6c6f77616e636520746f207374617274000000000060448201526064015b60405180910390fd5b61031b3382610247565b5080610327333361049e565b1461036c5760405162461bcd60e51b81526020600482015260156024820152746469646e2774206769766520616c6c6f77616e636560581b6044820152606401610308565b610377333383610261565b50610382333361049e565b156103cf5760405162461bcd60e51b815260206004820152601960248201527f6e6f6e2d7a65726f20616c6c6f77616e636520746f20656e64000000000000006044820152606401610308565b806103d981610b49565b9150506102ab565b50565b6000336102558185856103f7838361049e565b6104019190610b62565b6104c9565b6060600480546101c4906109ef565b60003381610423828661049e565b9050838110156104835760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b6064820152608401610308565b61027a82868684036104c9565b600033610255818585610667565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6001600160a01b03831661052b5760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608401610308565b6001600160a01b03821661058c5760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608401610308565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b60006105f9848461049e565b9050600019811461066157818110156106545760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606401610308565b61066184848484036104c9565b50505050565b6001600160a01b0383166106cb5760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608401610308565b6001600160a01b03821661072d5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608401610308565b6001600160a01b038316600090815260208190526040902054818110156107a55760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608401610308565b6001600160a01b03848116600081815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a3610661565b6001600160a01b0382166108615760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152606401610308565b80600260008282546108739190610b62565b90915550506001600160a01b038216600081815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b600060208083528351808285015260005b818110156108f7578581018301518582016040015282016108db565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b038116811461092f57600080fd5b919050565b6000806040838503121561094757600080fd5b61095083610918565b946020939093013593505050565b60008060006060848603121561097357600080fd5b61097c84610918565b925061098a60208501610918565b9150604084013590509250925092565b6000602082840312156109ac57600080fd5b6109b582610918565b9392505050565b600080604083850312156109cf57600080fd5b6109d883610918565b91506109e660208401610918565b90509250929050565b600181811c90821680610a0357607f821691505b602082108103610a2357634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b600181815b80851115610a7a578160001904821115610a6057610a60610a29565b80851615610a6d57918102915b93841c9390800290610a44565b509250929050565b600082610a915750600161025b565b81610a9e5750600061025b565b8160018114610ab45760028114610abe57610ada565b600191505061025b565b60ff841115610acf57610acf610a29565b50506001821b61025b565b5060208310610133831016604e8410600b8410161715610afd575081810a61025b565b610b078383610a3f565b8060001904821115610b1b57610b1b610a29565b029392505050565b60006109b560ff841683610a82565b808202811582820484141761025b5761025b610a29565b600060018201610b5b57610b5b610a29565b5060010190565b8082018082111561025b5761025b610a2956fea2646970667358221220c33117c9d08b3ca92e5865f957cca670ed19c2990ba76736dd40b693ae62098064736f6c63430008110033
\ No newline at end of file
diff --git a/core/vm/runtime/testdata/erc20mint.hex b/core/vm/runtime/testdata/erc20mint.hex
new file mode 100644
index 0000000000..ed06ae6b14
--- /dev/null
+++ b/core/vm/runtime/testdata/erc20mint.hex
@@ -0,0 +1 @@
+608060405234801561001057600080fd5b50600436106100b45760003560e01c80633950935111610071578063395093511461013857806370a082311461014b57806395d89b4114610174578063a457c2d71461017c578063a9059cbb1461018f578063dd62ed3e146101a257600080fd5b806306fdde03146100b9578063095ea7b3146100d757806318160ddd146100fa57806323b872dd1461010c57806330627b7c1461011f578063313ce56714610129575b600080fd5b6100c16101b5565b6040516100ce919061079c565b60405180910390f35b6100ea6100e5366004610806565b610247565b60405190151581526020016100ce565b6002545b6040519081526020016100ce565b6100ea61011a366004610830565b610261565b610127610285565b005b604051601281526020016100ce565b6100ea610146366004610806565b6102b1565b6100fe61015936600461086c565b6001600160a01b031660009081526020819052604090205490565b6100c16102d3565b6100ea61018a366004610806565b6102e2565b6100ea61019d366004610806565b610362565b6100fe6101b036600461088e565b610370565b6060600380546101c4906108c1565b80601f01602080910402602001604051908101604052809291908181526020018280546101f0906108c1565b801561023d5780601f106102125761010080835404028352916020019161023d565b820191906000526020600020905b81548152906001019060200180831161022057829003601f168201915b5050505050905090565b60003361025581858561039b565b60019150505b92915050565b60003361026f8582856104bf565b61027a858585610539565b506001949350505050565b60005b6113888110156102ae5761029c33826106dd565b806102a681610911565b915050610288565b50565b6000336102558185856102c48383610370565b6102ce919061092a565b61039b565b6060600480546101c4906108c1565b600033816102f08286610370565b9050838110156103555760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b60648201526084015b60405180910390fd5b61027a828686840361039b565b600033610255818585610539565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6001600160a01b0383166103fd5760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b606482015260840161034c565b6001600160a01b03821661045e5760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b606482015260840161034c565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b60006104cb8484610370565b9050600019811461053357818110156105265760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000604482015260640161034c565b610533848484840361039b565b50505050565b6001600160a01b03831661059d5760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b606482015260840161034c565b6001600160a01b0382166105ff5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b606482015260840161034c565b6001600160a01b038316600090815260208190526040902054818110156106775760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b606482015260840161034c565b6001600160a01b03848116600081815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a3610533565b6001600160a01b0382166107335760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f206164647265737300604482015260640161034c565b8060026000828254610745919061092a565b90915550506001600160a01b038216600081815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b600060208083528351808285015260005b818110156107c9578581018301518582016040015282016107ad565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b038116811461080157600080fd5b919050565b6000806040838503121561081957600080fd5b610822836107ea565b946020939093013593505050565b60008060006060848603121561084557600080fd5b61084e846107ea565b925061085c602085016107ea565b9150604084013590509250925092565b60006020828403121561087e57600080fd5b610887826107ea565b9392505050565b600080604083850312156108a157600080fd5b6108aa836107ea565b91506108b8602084016107ea565b90509250929050565b600181811c908216806108d557607f821691505b6020821081036108f557634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b600060018201610923576109236108fb565b5060010190565b8082018082111561025b5761025b6108fb56fea2646970667358221220fa5dc8b94ca8266a9f37b573a3ac8993380ce8da74caa7732abd6e7ed31cd37e64736f6c63430008110033
\ No newline at end of file
diff --git a/core/vm/runtime/testdata/erc20transfer.hex b/core/vm/runtime/testdata/erc20transfer.hex
new file mode 100644
index 0000000000..cc01470901
--- /dev/null
+++ b/core/vm/runtime/testdata/erc20transfer.hex
@@ -0,0 +1 @@
+608060405234801561001057600080fd5b50600436106100b45760003560e01c80633950935111610071578063395093511461013857806370a082311461014b57806395d89b4114610174578063a457c2d71461017c578063a9059cbb1461018f578063dd62ed3e146101a257600080fd5b806306fdde03146100b9578063095ea7b3146100d757806318160ddd146100fa57806323b872dd1461010c57806330627b7c1461011f578063313ce56714610129575b600080fd5b6100c16101b5565b6040516100ce91906107bf565b60405180910390f35b6100ea6100e5366004610829565b610247565b60405190151581526020016100ce565b6002545b6040519081526020016100ce565b6100ea61011a366004610853565b610261565b610127610285565b005b604051601281526020016100ce565b6100ea610146366004610829565b6102d4565b6100fe61015936600461088f565b6001600160a01b031660009081526020819052604090205490565b6100c16102f6565b6100ea61018a366004610829565b610305565b6100ea61019d366004610829565b610385565b6100fe6101b03660046108b1565b610393565b6060600380546101c4906108e4565b80601f01602080910402602001604051908101604052809291908181526020018280546101f0906108e4565b801561023d5780601f106102125761010080835404028352916020019161023d565b820191906000526020600020905b81548152906001019060200180831161022057829003601f168201915b5050505050905090565b6000336102558185856103be565b60019150505b92915050565b60003361026f8582856104e2565b61027a85858561055c565b506001949350505050565b6102a6336102956012600a610a18565b6102a190612710610a27565b610700565b60005b6113888110156102d1576102be600182610385565b50806102c981610a3e565b9150506102a9565b50565b6000336102558185856102e78383610393565b6102f19190610a57565b6103be565b6060600480546101c4906108e4565b600033816103138286610393565b9050838110156103785760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b60648201526084015b60405180910390fd5b61027a82868684036103be565b60003361025581858561055c565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6001600160a01b0383166104205760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b606482015260840161036f565b6001600160a01b0382166104815760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b606482015260840161036f565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b60006104ee8484610393565b9050600019811461055657818110156105495760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000604482015260640161036f565b61055684848484036103be565b50505050565b6001600160a01b0383166105c05760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b606482015260840161036f565b6001600160a01b0382166106225760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b606482015260840161036f565b6001600160a01b0383166000908152602081905260409020548181101561069a5760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b606482015260840161036f565b6001600160a01b03848116600081815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a3610556565b6001600160a01b0382166107565760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f206164647265737300604482015260640161036f565b80600260008282546107689190610a57565b90915550506001600160a01b038216600081815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b600060208083528351808285015260005b818110156107ec578581018301518582016040015282016107d0565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b038116811461082457600080fd5b919050565b6000806040838503121561083c57600080fd5b6108458361080d565b946020939093013593505050565b60008060006060848603121561086857600080fd5b6108718461080d565b925061087f6020850161080d565b9150604084013590509250925092565b6000602082840312156108a157600080fd5b6108aa8261080d565b9392505050565b600080604083850312156108c457600080fd5b6108cd8361080d565b91506108db6020840161080d565b90509250929050565b600181811c908216806108f857607f821691505b60208210810361091857634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b600181815b8085111561096f5781600019048211156109555761095561091e565b8085161561096257918102915b93841c9390800290610939565b509250929050565b6000826109865750600161025b565b816109935750600061025b565b81600181146109a957600281146109b3576109cf565b600191505061025b565b60ff8411156109c4576109c461091e565b50506001821b61025b565b5060208310610133831016604e8410600b84101617156109f2575081810a61025b565b6109fc8383610934565b8060001904821115610a1057610a1061091e565b029392505050565b60006108aa60ff841683610977565b808202811582820484141761025b5761025b61091e565b600060018201610a5057610a5061091e565b5060010190565b8082018082111561025b5761025b61091e56fea26469706673582212200111a8455f1c6aba553f7e7a1fe413d6a44eececa7e001df4c190aef76f39b5864736f6c63430008110033
\ No newline at end of file
diff --git a/core/vm/runtime/testdata/evm-bench/README.md b/core/vm/runtime/testdata/evm-bench/README.md
new file mode 100644
index 0000000000..cc5ec51620
--- /dev/null
+++ b/core/vm/runtime/testdata/evm-bench/README.md
@@ -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.
diff --git a/core/vm/runtime/testdata/evm-bench/compare.sh b/core/vm/runtime/testdata/evm-bench/compare.sh
new file mode 100755
index 0000000000..7ae00603a1
--- /dev/null
+++ b/core/vm/runtime/testdata/evm-bench/compare.sh
@@ -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
+ 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"
diff --git a/core/vm/runtime/testdata/evm-bench/gen.sh b/core/vm/runtime/testdata/evm-bench/gen.sh
new file mode 100755
index 0000000000..6b69134823
--- /dev/null
+++ b/core/vm/runtime/testdata/evm-bench/gen.sh
@@ -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
diff --git a/core/vm/runtime/testdata/snailtracer.hex b/core/vm/runtime/testdata/snailtracer.hex
new file mode 100644
index 0000000000..9546187294
--- /dev/null
+++ b/core/vm/runtime/testdata/snailtracer.hex
@@ -0,0 +1 @@
+608060405234801561001057600080fd5b506004361061004c5760003560e01c806330627b7c1461005157806375ac892a14610085578063784f13661461011d578063c294360114610146575b600080fd5b610059610163565b604080516001600160f81b03199485168152928416602084015292168183015290519081900360600190f35b6100a86004803603604081101561009b57600080fd5b50803590602001356102d1565b6040805160208082528351818301528351919283929083019185019080838360005b838110156100e25781810151838201526020016100ca565b50505050905090810190601f16801561010f5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100596004803603606081101561013357600080fd5b508035906020810135906040013561055b565b6100a86004803603602081101561015c57600080fd5b5035610590565b6000806000610176610400610300610834565b60405180606001604052806001546000546207d5dc028161019357fe5b058152600060208083018290526040928301919091528251600b81905583820151600c81905593830151600d819055835160608082018652928152808401959095528484015282519081018352600654815260075491810191909152600854918101919091526102259161021c916102139161020e91612ef7565b612f64565b6207d5dc612feb565b620f424061301e565b8051600e556020810151600f55604001516010556102416142dd565b61025a816102556102006101806008613064565b613212565b90506102708161025561014561021c6008613064565b905061028481610255610258806008613064565b905061029a8161025561020a61020c6008613064565b90506102a781600461301e565b90506102b1613250565b8051602082015160409092015160f891821b9692821b9550901b92509050565b606060005b6000548112156104c95760006102ed828686613064565b90506002816000015160f81b90808054603f811680603e811461032a576002830184556001831661031c578192505b600160028404019350610342565b600084815260209081902060ff198516905560419094555b505050600190038154600116156103685790600052602060002090602091828204019190065b909190919091601f036101000a81548160ff02191690600160f81b840402179055506002816020015160f81b90808054603f811680603e81146103c557600283018455600183166103b7578192505b6001600284040193506103dd565b600084815260209081902060ff198516905560419094555b505050600190038154600116156104035790600052602060002090602091828204019190065b909190919091601f036101000a81548160ff02191690600160f81b840402179055506002816040015160f81b90808054603f811680603e81146104605760028301845560018316610452578192505b600160028404019350610478565b600084815260209081902060ff198516905560419094555b5050506001900381546001161561049e5790600052602060002090602091828204019190065b815460ff601f929092036101000a9182021916600160f81b90930402919091179055506001016102d6565b506002805460408051602060018416156101000260001901909316849004601f8101849004840282018401909252818152929183018282801561054d5780601f106105225761010080835404028352916020019161054d565b820191906000526020600020905b81548152906001019060200180831161053057829003601f168201915b505050505090505b92915050565b60008060008061056c878787613064565b8051602082015160409092015160f891821b9a92821b9950901b9650945050505050565b600154606090600019015b600081126107a35760005b6000548112156107995760006105bd828487613064565b90506002816000015160f81b90808054603f811680603e81146105fa57600283018455600183166105ec578192505b600160028404019350610612565b600084815260209081902060ff198516905560419094555b505050600190038154600116156106385790600052602060002090602091828204019190065b909190919091601f036101000a81548160ff02191690600160f81b840402179055506002816020015160f81b90808054603f811680603e81146106955760028301845560018316610687578192505b6001600284040193506106ad565b600084815260209081902060ff198516905560419094555b505050600190038154600116156106d35790600052602060002090602091828204019190065b909190919091601f036101000a81548160ff02191690600160f81b840402179055506002816040015160f81b90808054603f811680603e81146107305760028301845560018316610722578192505b600160028404019350610748565b600084815260209081902060ff198516905560419094555b5050506001900381546001161561076e5790600052602060002090602091828204019190065b815460ff601f929092036101000a9182021916600160f81b90930402919091179055506001016105a6565b506000190161059b565b506002805460408051602060018416156101000260001901909316849004601f810184900484028201840190925281815292918301828280156108275780601f106107fc57610100808354040283529160200191610827565b820191906000526020600020905b81548152906001019060200180831161080a57829003601f168201915b505050505090505b919050565b8160008190555080600181905550604051806080016040528060405180606001604052806302faf08081526020016303197500815260200163119e7f8081525081526020016108a460405180606001604052806000815260200161a673198152602001620f423f19815250612f64565b815260006020808301829052604092830182905283518051600355808201516004558301516005558381015180516006559081015160075582015160085582820151600955606092830151600a805460ff1916911515919091179055815192830190915260015490548291906207d5dc028161091c57fe5b058152600060208083018290526040928301919091528251600b81905583820151600c81905593830151600d819055835160608082018652928152808401959095528484015282519081018352600654815260075491810191909152600854918101919091526109979161021c916102139161020e91612ef7565b8051600e55602080820151600f55604091820151601055815160a08101835264174876e8008152825160608082018552641748862a40825263026e8f00828501526304dd1e008286015282840191825284518082018652600080825281860181905281870181905284870191825286518084018852620b71b081526203d09081880181905281890152928501928352608085018181526011805460018082018355919093528651600b9093027f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c688101938455955180517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c69880155808901517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6a8801558901517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6b870155925180517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6c870155808801517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6d8701558801517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6e860155925180517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6f860155958601517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c7085015594909501517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c71830155517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c72909101805492949192909160ff1990911690836002811115610c1057fe5b0217905550505060116040518060a0016040528064174876e8008152602001604051806060016040528064174290493f19815260200163026e8f0081526020016304dd1e008152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806203d09081526020016203d0908152602001620b71b0815250815260200160006002811115610cb657fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff1990921691908490811115610d5857fe5b0217905550505060116040518060a0016040528064174876e800815260200160405180606001604052806302faf080815260200163026e8f00815260200164174876e800815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620b71b08152602001620b71b08152602001620b71b0815250815260200160006002811115610dfd57fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff1990921691908490811115610e9f57fe5b0217905550505060116040518060a0016040528064174876e800815260200160405180606001604052806302faf080815260200163026e8f00815260200164173e54e97f1981525081526020016040518060600160405280600081526020016000815260200160008152508152602001604051806060016040528060008152602001600081526020016000815250815260200160006002811115610f3f57fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff1990921691908490811115610fe157fe5b0217905550505060116040518060a0016040528064174876e800815260200160405180606001604052806302faf080815260200164174876e80081526020016304dd1e00815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620b71b08152602001620b71b08152602001620b71b081525081526020016000600281111561108657fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff199092169190849081111561112857fe5b0217905550505060116040518060a0016040528064174876e800815260200160405180606001604052806302faf080815260200164174399c9ff1981526020016304dd1e00815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620b71b08152602001620b71b08152602001620b71b08152508152602001600060028111156111ce57fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff199092169190849081111561127057fe5b0217905550505060116040518060a0016040528062fbc5208152602001604051806060016040528063019bfcc0815260200162fbc52081526020016302cd29c0815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561131157fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff19909216919084908111156113b357fe5b0217905550505060116040518060a001604052806323c34600815260200160405180606001604052806302faf080815260200163289c455081526020016304dd1e008152508152602001604051806060016040528062b71b00815260200162b71b00815260200162b71b00815250815260200160405180606001604052806000815260200160008152602001600081525081526020016000600281111561145657fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff19909216919084908111156114f857fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f208152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016305a1f4a081525081526020016040518060600160405280630459e44081526020016302f34f6081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561160c57fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff19909216919084908111156116fd57fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f20815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001600081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016305a1f4a08152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561180e57fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff19909216919084908111156118ff57fe5b0217905550505060126040518060e001604052806040518060600160405280630555a9608152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e44081526020016302f34f6081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016305a1f4a08152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e58815250815260200160016002811115611a1357fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff1990921691908490811115611b0457fe5b0217905550505060126040518060e001604052806040518060600160405280630555a960815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016305a1f4a081525081526020016040518060600160405280630459e4408152602001600081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e58815250815260200160016002811115611c1557fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff1990921691908490811115611d0657fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f208152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e44081526020016302f34f6081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016303aa6a608152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e58815250815260200160016002811115611e1a57fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff1990921691908490811115611f0b57fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f20815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016303aa6a6081525081526020016040518060600160405280630459e4408152602001600081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561201c57fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff199092169190849081111561210d57fe5b0217905550505060126040518060e001604052806040518060600160405280630555a9608152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016303aa6a6081525081526020016040518060600160405280630459e44081526020016302f34f6081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561222157fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff199092169190849081111561231257fe5b0217905550505060126040518060e001604052806040518060600160405280630555a960815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001600081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016303aa6a608152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561242357fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff199092169190849081111561251457fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f208152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016303aa6a6081525081526020016040518060600160405280630555a9608152602001630188c2e081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561262857fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff199092169190849081111561271957fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f208152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630555a9608152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016305a1f4a08152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561282d57fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff199092169190849081111561291e57fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f20815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630555a960815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016303aa6a608152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e58815250815260200160016002811115612a3257fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff1990921691908490811115612b2357fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f20815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016305a1f4a081525081526020016040518060600160405280630555a960815260200163016a8c8081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e58815250815260200160016002811115612c3757fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff1990921691908490811115612d2857fe5b0217905550505060005b601254811015612ef257600060128281548110612d4b57fe5b600091825260209182902060408051610140810182526013909302909101805460e08401908152600182015461010085015260028083015461012086015290845282516060818101855260038401548252600484015482880152600584015482860152858701919091528351808201855260068401548152600784015481880152600884015481860152858501528351808201855260098401548152600a84015481880152600b840154818601528186015283518082018552600c8401548152600d84015481880152600e84015481860152608086015283519081018452600f830154815260108301549581019590955260118201549285019290925260a0830193909352601283015491929160c084019160ff90911690811115612e6c57fe5b6002811115612e7757fe5b815250509050612eac61020e612e95836020015184600001516132cd565b612ea7846040015185600001516132cd565b612ef7565b60128381548110612eb957fe5b60009182526020918290208351600960139093029091019182015590820151600a820155604090910151600b9091015550600101612d32565b505050565b612eff6142dd565b604051806060016040528083602001518560400151028460400151866020015102038152602001836040015185600001510284600001518660400151020381526020018360000151856020015102846020015186600001510203815250905092915050565b612f6c6142dd565b604082015160208301518351600092612f9292918002918002919091019080020161330c565b90506040518060600160405280828560000151620f42400281612fb157fe5b058152602001828560200151620f42400281612fc957fe5b058152602001828560400151620f42400281612fe157fe5b0590529392505050565b612ff36142dd565b5060408051606081018252835183028152602080850151840290820152928101519091029082015290565b6130266142dd565b60405180606001604052808385600001518161303e57fe5b0581526020018385602001518161305157fe5b05815260200183856040015181612fe157fe5b61306c6142dd565b6000546013805463ffffffff1916918502860163ffffffff169190911790556130936142dd565b905060005b828112156131f157600061317261314c61021c613115600b60405180606001604052908160008201548152602001600182015481526020016002820154815250506207a1206000546207a1206130ec613343565b63ffffffff16816130f957fe5b0663ffffffff168d620f424002018161310e57fe5b0503612feb565b60408051606081018252600e548152600f5460208201526010549181019190915260015461025591906207a12090816130ec613343565b604080516060810182526006548152600754602082015260085491810191909152613212565b6040805160e081019091526003546080820190815260045460a083015260055460c083015291925060009181906131ae9061025586608c612feb565b81526020016131bc84612f64565b815260006020820181905260409091015290506131e5846102556131df8461336c565b8861301e565b93505050600101613098565b5061320861021c61320183613753565b60ff612feb565b90505b9392505050565b61321a6142dd565b50604080516060810182528251845101815260208084015181860151019082015291810151928101519092019181019190915290565b60008080556001819055613266906002906142fe565b60006003819055600481905560058190556006819055600781905560088190556009819055600a805460ff19169055600b819055600c819055600d819055600e819055600f81905560108190556132bf90601190614345565b6132cb60126000614366565b565b6132d56142dd565b5060408051606081018252825184510381526020808401518186015103908201528282015184830151039181019190915292915050565b80600260018201055b8181121561333d5780915060028182858161332c57fe5b05018161333557fe5b059050613315565b50919050565b6013805463ffffffff19811663ffffffff9182166341c64e6d0261303901821617918290551690565b6133746142dd565b600a826040015113156133a657604051806060016040528060008152602001600081526020016000815250905061082f565b60008060006133b48561379f565b91945092509050826133e857604051806060016040528060008152602001600081526020016000815250935050505061082f565b6133f0614387565b6133f86143c7565b6134006142dd565b6134086142dd565b600086600181111561341657fe5b1415613505576011858154811061342957fe5b60009182526020918290206040805160a081018252600b90930290910180548352815160608082018452600183015482526002808401548388015260038401548386015285870192909252835180820185526004840154815260058401548188015260068401548186015285850152835180820185526007840154815260088401549681019690965260098301549386019390935291830193909352600a830154919291608084019160ff909116908111156134e157fe5b60028111156134ec57fe5b8152505093508360600151915083604001519050613653565b6012858154811061351257fe5b600091825260209182902060408051610140810182526013909302909101805460e08401908152600182015461010085015260028083015461012086015290845282516060818101855260038401548252600484015482880152600584015482860152858701919091528351808201855260068401548152600784015481880152600884015481860152858501528351808201855260098401548152600a84015481880152600b840154818601528186015283518082018552600c8401548152600d84015481880152600e84015481860152608086015283519081018452600f830154815260108301549581019590955260118201549285019290925260a0830193909352601283015491929160c084019160ff9091169081111561363357fe5b600281111561363e57fe5b8152505092508260a001519150826080015190505b6040820151600190811215613669575060408201515b808360200151131561367c575060208201515b808360400151131561368f575060408201515b60408a01805160010190819052600512156136f75780620f42406136b1613343565b63ffffffff16816136be57fe5b0663ffffffff1612156136e8576136e16136db84620f4240612feb565b8261301e565b92506136f7565b50965061082f95505050505050565b6136ff6142dd565b600088600181111561370d57fe5b14156137255761371e8b878b613a57565b9050613733565b6137308b868b613aec565b90505b6137448361025561021c8785613baa565b9b9a5050505050505050505050565b61375b6142dd565b60405180606001604052806137738460000151613be8565b81526020016137858460200151613be8565b81526020016137978460400151613be8565b905292915050565b60008080808080805b6011548110156138c2576000613890601183815481106137c457fe5b60009182526020918290206040805160a081018252600b90930290910180548352815160608082018452600183015482526002808401548388015260038401548386015285870192909252835180820185526004840154815260058401548188015260068401548186015285850152835180820185526007840154815260088401549681019690965260098301549386019390935291830193909352600a830154919291608084019160ff9091169081111561387c57fe5b600281111561388757fe5b9052508a613c13565b90506000811380156138a957508415806138a957508481125b156138b957809450600093508192505b506001016137a8565b5060005b601254811015613a49576000613a17601283815481106138e257fe5b600091825260209182902060408051610140810182526013909302909101805460e08401908152600182015461010085015260028083015461012086015290845282516060818101855260038401548252600484015482880152600584015482860152858701919091528351808201855260068401548152600784015481880152600884015481860152858501528351808201855260098401548152600a84015481880152600b840154818601528186015283518082018552600c8401548152600d84015481880152600e84015481860152608086015283519081018452600f830154815260108301549581019590955260118201549285019290925260a0830193909352601283015491929160c084019160ff90911690811115613a0357fe5b6002811115613a0e57fe5b9052508a613cbb565b9050600081138015613a305750841580613a3057508481125b15613a4057809450600193508192505b506001016138c6565b509196909550909350915050565b613a5f6142dd565b6000613a7a856000015161025561021c886020015187612feb565b90506000613a8f61020e8387602001516132cd565b9050600085608001516002811115613aa357fe5b1415613ae1576000613ab9828860200151613e0c565b12613acd57613aca81600019612feb565b90505b613ad8868383613e31565b9250505061320b565b613ad8868383613fc1565b613af46142dd565b6000613b0f856000015161025561021c886020015187612feb565b6060860151909150620a2c2a9015613b2757506216e3605b6000620f4240613b3f87606001518960200151613e0c565b81613b4657fe5b05905060008112613b55576000035b64e8d4a5100081800281038380020281900590036000811215613b8c57613b8188858960600151613fc1565b94505050505061320b565b613b9e88858960600151868686614039565b98975050505050505050565b613bb26142dd565b50604080516060810182528251845102815260208084015181860151029082015291810151928101519092029181019190915290565b600080821215613bfa5750600061082f565b620f4240821315613c0f5750620f424061082f565b5090565b600080613c28846020015184600001516132cd565b90506000620f4240613c3e838660200151613e0c565b81613c4557fe5b865191900591506000908002613c5b8480613e0c565b838402030190506000811215613c775760009350505050610555565b613c808161330c565b90506103e88183031315613c9957900391506105559050565b6103e88183011315613caf570191506105559050565b50600095945050505050565b600080613cd0846020015185600001516132cd565b90506000613ce6856040015186600001516132cd565b90506000613cf8856020015183612ef7565b90506000620f4240613d0a8584613e0c565b81613d1157fe5b0590506103e71981138015613d2757506103e881125b15613d39576000945050505050610555565b85518751600091613d49916132cd565b9050600082613d588386613e0c565b81613d5f57fe5b0590506000811280613d735750620f424081135b15613d875760009650505050505050610555565b6000613d938388612ef7565b9050600084613da68b6020015184613e0c565b81613dad57fe5b0590506000811280613dc35750620f4240818401135b15613dd957600098505050505050505050610555565b600085613de68985613e0c565b81613ded57fe5b0590506103e88112156137445760009950505050505050505050610555565b6040808201519083015160208084015190850151845186510291020191020192915050565b613e396142dd565b6000620f424080613e48613343565b63ffffffff1681613e5557fe5b0663ffffffff16625fdfb00281613e6857fe5b0590506000620f4240613e79613343565b63ffffffff1681613e8657fe5b0663ffffffff1690506000613e9a8261330c565b6103e8029050613ea86142dd565b620186a0613eb98760000151614216565b1315613ee657604051806060016040528060008152602001620f4240815260200160008152509050613f09565b6040518060600160405280620f4240815260200160008152602001600081525090505b613f1661020e8288612ef7565b90506000613f2761020e8884612ef7565b9050613f7f61020e613f64613f5285620f424088613f448c61422e565b0281613f4c57fe5b05612feb565b61025585620f424089613f448d61424e565b6102558a613f7689620f42400361330c565b6103e802612feb565b9150613fb460405180608001604052808a81526020018481526020018b6040015181526020018b60600151151581525061336c565b9998505050505050505050565b613fc96142dd565b6000613ffb61020e8660200151613ff686620f4240613fec898c60200151613e0c565b60020281613f4c57fe5b6132cd565b90506140306040518060800160405280868152602001838152602001876040015181526020018760600151151581525061336c565b95945050505050565b6140416142dd565b60608701516000199015614053575060015b600061408961020e61021c61406c8c602001518a612feb565b613ff68b6140798a61330c565b620f42408c8e0205018802612feb565b60608a0151909150620f42408601906140ba57620f42406140aa838a613e0c565b816140b157fe5b05620f42400390505b60408a0151619c406c0c9f2c9cd04674edea40000000620ea6008480028502850285020205019060021261415e5761412a61411f60405180608001604052808d81526020018681526020018e6040015181526020018e6060015115151581525061336c565b82620f424003612feb565b92506141448361025561413e8e8e8e613fc1565b84612feb565b925061415383620f424061301e565b94505050505061420c565b600281056203d09001620f4240614173613343565b63ffffffff168161418057fe5b0663ffffffff1612156141b2576141536141a461419e8d8d8d613fc1565b83612feb565b600283056203d0900161301e565b6142056141f76141ec60405180608001604052808e81526020018781526020018f6040015181526020018f6060015115151581525061336c565b83620f424003612feb565b60028305620b71b00361301e565b9450505050505b9695505050505050565b60008082131561422757508061082f565b5060000390565b60008061423a8361424e565b905061320b81820264e8d4a510000361330c565b60005b600082121561426757625fdfb082019150614251565b5b625fdfb0821261427f57625fdfb082039150614268565b6001828160025b818313156142d457818385028161429957fe5b0585019450620f4240808788860202816142af57fe5b05816142b757fe5b600095909503940592506001810181029190910290600201614286565b50505050919050565b60405180606001604052806000815260200160008152602001600081525090565b50805460018160011615610100020316600290046000825580601f106143245750614342565b601f0160209004906000526020600020908101906143429190614401565b50565b50805460008255600b02906000526020600020908101906143429190614416565b50805460008255601302906000526020600020908101906143429190614475565b6040518060a00160405280600081526020016143a16142dd565b81526020016143ae6142dd565b81526020016143bb6142dd565b81526020016000905290565b6040518060e001604052806143da6142dd565b81526020016143e76142dd565b81526020016143f46142dd565b81526020016143a16142dd565b5b80821115613c0f5760008155600101614402565b5b80821115613c0f57600080825560018201819055600282018190556003820181905560048201819055600582018190556006820181905560078201819055600882018190556009820155600a8101805460ff19169055600b01614417565b5b80821115613c0f576000808255600182018190556002820181905560038201819055600482018190556005820181905560068201819055600782018190556008820181905560098201819055600a8201819055600b8201819055600c8201819055600d8201819055600e8201819055600f820181905560108201819055601182015560128101805460ff1916905560130161447656fea2646970667358221220037024f5647853879c58fbcc61ac3616455f6f731cc6e84f91eb5a3b4e06c00464736f6c63430007060033
\ No newline at end of file
diff --git a/core/vm/runtime/testdata/tenthousandhashes.hex b/core/vm/runtime/testdata/tenthousandhashes.hex
new file mode 100644
index 0000000000..2892c1d984
--- /dev/null
+++ b/core/vm/runtime/testdata/tenthousandhashes.hex
@@ -0,0 +1 @@
+6080604052348015600f57600080fd5b506004361060285760003560e01c806330627b7c14602d575b600080fd5b60336035565b005b60005b614e20811015606a5760408051602081018390520160408051601f198184030190525280606381606d565b9150506038565b50565b600060018201608c57634e487b7160e01b600052601160045260246000fd5b506001019056fea2646970667358221220e499ea6d1ccd5fce28e8cd2507f6fef33f0d005543e04580f290bbd01df0655464736f6c63430008110033
\ No newline at end of file
From ed658c8f3c2e5b31cb4b7019d7164b844f6805d2 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Wed, 10 Jun 2026 17:34:40 -0500
Subject: [PATCH 02/23] core/vm/gen: fix lint
---
core/vm/gen/main.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index 2e7c0b45a3..c89ff2459b 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -217,7 +217,7 @@ func (g *generator) deriveMeta(forks []vm.GenFork) {
// 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)
+ g.checkStable(code, handler, forks)
}
for code := 0x62; code <= 0x7f; code++ { // PUSH3-32
g.checkStable(byte(code), "makePush", forks)
From fd9be88ed8e12a2d6d6f3ecfb32e81a7d4fd8416 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Thu, 11 Jun 2026 00:37:28 -0500
Subject: [PATCH 03/23] core, core/vm/runtime: fix hashes benchmark, add block
import benchmark
---
core/bench_test.go | 115 ++++++++++++++++++
core/vm/runtime/testdata/evm-bench/README.md | 15 ++-
.../testdata/evm-bench/TenThousandHashes.sol | 17 +++
core/vm/runtime/testdata/evm-bench/compare.sh | 2 +-
core/vm/runtime/testdata/evm-bench/gen.sh | 9 ++
.../vm/runtime/testdata/tenthousandhashes.hex | 2 +-
6 files changed, 156 insertions(+), 4 deletions(-)
create mode 100644 core/vm/runtime/testdata/evm-bench/TenThousandHashes.sol
diff --git a/core/bench_test.go b/core/bench_test.go
index 47b991e5a3..4e3923e4fe 100644
--- a/core/bench_test.go
+++ b/core/bench_test.go
@@ -18,7 +18,12 @@ package core
import (
"crypto/ecdsa"
+ "encoding/binary"
+ "encoding/hex"
"math/big"
+ "os"
+ "path/filepath"
+ "strings"
"testing"
"github.com/ethereum/go-ethereum/common"
@@ -68,6 +73,12 @@ func BenchmarkInsertChain_ring1000_memdb(b *testing.B) {
func BenchmarkInsertChain_ring1000_diskdb(b *testing.B) {
benchInsertChain(b, true, genTxRing(1000))
}
+func BenchmarkInsertChain_evmWorkload_memdb(b *testing.B) {
+ benchInsertChainEVM(b, false)
+}
+func BenchmarkInsertChain_evmWorkload_diskdb(b *testing.B) {
+ benchInsertChainEVM(b, true)
+}
var (
// This is the content of the genesis block used by the benchmarks.
@@ -208,6 +219,110 @@ func benchInsertChain(b *testing.B, disk bool, gen func(int, *BlockGen)) {
}
}
+// The evmWorkload addresses hold the evm-bench contracts (see
+// core/vm/runtime/testdata), installed through the genesis alloc. Their
+// runtime bytecode sets up all state inside Benchmark(), so no constructor
+// runs.
+var benchWorkloadAddrs = []common.Address{
+ common.HexToAddress("0x00000000000000000000000000000000000e0001"), // ERC20Transfer
+ common.HexToAddress("0x00000000000000000000000000000000000e0002"), // ERC20Mint
+ common.HexToAddress("0x00000000000000000000000000000000000e0003"), // ERC20ApprovalTransfer
+ common.HexToAddress("0x00000000000000000000000000000000000e0004"), // TenThousandHashes
+}
+
+func benchWorkloadCode(tb testing.TB) [][]byte {
+ files := []string{"erc20transfer.hex", "erc20mint.hex", "erc20approval.hex", "tenthousandhashes.hex"}
+ codes := make([][]byte, len(files))
+ for i, name := range files {
+ raw, err := os.ReadFile(filepath.Join("vm", "runtime", "testdata", name))
+ if err != nil {
+ tb.Fatal(err)
+ }
+ code, err := hex.DecodeString(strings.TrimSpace(string(raw)))
+ if err != nil {
+ tb.Fatal(err)
+ }
+ codes[i] = code
+ }
+ return codes
+}
+
+// benchInsertChainEVM measures InsertChain over blocks dominated by real
+// contract execution, as a coarse local surrogate for sync throughput: the
+// timed path is full block processing, including sender recovery, EVM
+// execution, state updates, receipts, bloom filters and the state root.
+// Every block invokes the four evm-bench workloads (ERC-20 transfer, mint
+// and approval loops plus a keccak loop) and sends ten value transfers to
+// fresh accounts so the state grows as it would during sync. Reports Mgas/s
+// alongside the standard timings.
+func benchInsertChainEVM(b *testing.B, disk bool) {
+ var db ethdb.Database
+ if !disk {
+ db = rawdb.NewMemoryDatabase()
+ } else {
+ pdb, err := pebble.New(b.TempDir(), 128, 128, "", false)
+ if err != nil {
+ b.Fatalf("cannot create temporary database: %v", err)
+ }
+ db = rawdb.NewDatabase(pdb)
+ defer db.Close()
+ }
+ alloc := types.GenesisAlloc{benchRootAddr: {Balance: benchRootFunds}}
+ for i, code := range benchWorkloadCode(b) {
+ alloc[benchWorkloadAddrs[i]] = types.Account{Code: code, Balance: new(big.Int)}
+ }
+ gspec := &Genesis{
+ Config: params.TestChainConfig,
+ Alloc: alloc,
+ GasLimit: 150_000_000,
+ }
+ selector := []byte{0x30, 0x62, 0x7b, 0x7c} // Benchmark()
+ _, chain, _ := GenerateChainWithGenesis(gspec, ethash.NewFaker(), b.N, func(i int, gen *BlockGen) {
+ signer := gen.Signer()
+ gasPrice := big.NewInt(0)
+ if gen.header.BaseFee != nil {
+ gasPrice = gen.header.BaseFee
+ }
+ for _, addr := range benchWorkloadAddrs {
+ to := addr
+ tx, _ := types.SignNewTx(benchRootKey, signer, &types.LegacyTx{
+ Nonce: gen.TxNonce(benchRootAddr),
+ To: &to,
+ Gas: 30_000_000,
+ Data: selector,
+ GasPrice: gasPrice,
+ })
+ gen.AddTx(tx)
+ }
+ for j := 0; j < 10; j++ {
+ var to common.Address
+ binary.BigEndian.PutUint64(to[4:12], uint64(i+1))
+ binary.BigEndian.PutUint64(to[12:20], uint64(j+1))
+ tx, _ := types.SignNewTx(benchRootKey, signer, &types.LegacyTx{
+ Nonce: gen.TxNonce(benchRootAddr),
+ To: &to,
+ Value: big.NewInt(1),
+ Gas: params.TxGas,
+ GasPrice: gasPrice,
+ })
+ gen.AddTx(tx)
+ }
+ })
+ var totalGas uint64
+ for _, block := range chain {
+ totalGas += block.GasUsed()
+ }
+ chainman, _ := NewBlockChain(db, gspec, ethash.NewFaker(), nil)
+ defer chainman.Stop()
+ b.ReportAllocs()
+ b.ResetTimer()
+ if i, err := chainman.InsertChain(chain); err != nil {
+ b.Fatalf("insert error (block %d): %v\n", i, err)
+ }
+ b.StopTimer()
+ b.ReportMetric(float64(totalGas)/1e6/b.Elapsed().Seconds(), "Mgas/s")
+}
+
func BenchmarkChainRead_header_10k(b *testing.B) {
benchReadChain(b, false, 10000)
}
diff --git a/core/vm/runtime/testdata/evm-bench/README.md b/core/vm/runtime/testdata/evm-bench/README.md
index cc5ec51620..fccea2ce87 100644
--- a/core/vm/runtime/testdata/evm-bench/README.md
+++ b/core/vm/runtime/testdata/evm-bench/README.md
@@ -17,7 +17,7 @@ workload:
| `ERC20Transfer` | ERC-20 transfers in a loop |
| `ERC20Mint` | 5000 `_mint`s (SSTORE/keccak/LOG-heavy) |
| `ERC20ApprovalTransfer` | 1000 approve+transferFrom cycles |
-| `TenThousandHashes` | a keccak loop |
+| `TenThousandHashes` | 20000 chained keccak256 calls |
## Running
@@ -40,6 +40,16 @@ interpreter change is committed) and runs `benchstat`.
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.
+`TenThousandHashes` is compiled from the vendored
+[`TenThousandHashes.sol`](TenThousandHashes.sol) in this directory, not the
+upstream evm-bench file. Upstream discards the hash result, so solc's optimizer
+deletes the keccak256 and the compiled benchmark degenerates into a bare
+counter loop performing a single static hash. The vendored copy chains the hash
+through a returned accumulator so the optimized bytecode performs all 20000
+hashes. Numbers for this benchmark are therefore not comparable with evm-bench
+or gevm published tables, which use degenerate bytecode (gevm's local
+replacement is broken differently, an inverted loop bound that hashes once).
+
`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
@@ -53,4 +63,5 @@ The `.hex` files are committed, so running the benchmarks needs no toolchain.
- 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.
+- Names match evm-bench / gevm for cross-comparison, except `TenThousandHashes`
+ (see the regenerating section).
diff --git a/core/vm/runtime/testdata/evm-bench/TenThousandHashes.sol b/core/vm/runtime/testdata/evm-bench/TenThousandHashes.sol
new file mode 100644
index 0000000000..004215e96f
--- /dev/null
+++ b/core/vm/runtime/testdata/evm-bench/TenThousandHashes.sol
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-3.0
+pragma solidity ^0.8.17;
+
+// Vendored from evm-bench (github.com/ziyadedher/evm-bench) with one fix.
+// The upstream loop discards the keccak256 result, so solc's optimizer
+// removes the pure call entirely and the compiled benchmark degenerates
+// into a bare counter loop that performs a single static hash. Chaining
+// the hash through an accumulator that the function returns keeps all
+// 20000 hashes in the optimized bytecode. The Benchmark() selector is
+// unchanged because return types are not part of the signature.
+contract TenThousandHashes {
+ function Benchmark() external pure returns (bytes32 acc) {
+ for (uint256 i = 0; i < 20000; i++) {
+ acc = keccak256(abi.encodePacked(acc, i));
+ }
+ }
+}
diff --git a/core/vm/runtime/testdata/evm-bench/compare.sh b/core/vm/runtime/testdata/evm-bench/compare.sh
index 7ae00603a1..624120bf6e 100755
--- a/core/vm/runtime/testdata/evm-bench/compare.sh
+++ b/core/vm/runtime/testdata/evm-bench/compare.sh
@@ -28,7 +28,7 @@ COUNT="${2:-10}"
# pattern, so the loops run as their own invocation anyway).
BENCHES=(
'^BenchmarkSnailtracer$:20x'
- '^BenchmarkTenThousandHashes$:200x'
+ '^BenchmarkTenThousandHashes$:100x'
'^BenchmarkERC20Transfer$:100x'
'^BenchmarkERC20Mint$:150x'
'^BenchmarkERC20ApprovalTransfer$:120x'
diff --git a/core/vm/runtime/testdata/evm-bench/gen.sh b/core/vm/runtime/testdata/evm-bench/gen.sh
index 6b69134823..26dbb6b73d 100755
--- a/core/vm/runtime/testdata/evm-bench/gen.sh
+++ b/core/vm/runtime/testdata/evm-bench/gen.sh
@@ -8,15 +8,24 @@
# contract's pragma: 0.8.17 for the ERC-20/hashing contracts, 0.4.26 for the
# legacy SnailTracer.
#
+# TenThousandHashes is compiled from the vendored TenThousandHashes.sol in
+# this directory rather than the upstream file. Upstream discards the hash
+# result, so the optimizer deletes the keccak256 and the benchmark degenerates
+# into a bare counter loop. The vendored copy chains the hash through a
+# returned accumulator, which keeps all 20000 hashes in the bytecode. See the
+# comment in that file.
+#
# Usage: core/vm/runtime/testdata/evm-bench/gen.sh
set -euo pipefail
TD="$(cd "$(dirname "$0")/.." && pwd)" # .../core/vm/runtime/testdata
+HERE="$(cd "$(dirname "$0")" && pwd)" # .../testdata/evm-bench
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"
+cp "$HERE/TenThousandHashes.sol" "$B/ten-thousand-hashes/TenThousandHashes.sol"
# 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
diff --git a/core/vm/runtime/testdata/tenthousandhashes.hex b/core/vm/runtime/testdata/tenthousandhashes.hex
index 2892c1d984..f2903b13d9 100644
--- a/core/vm/runtime/testdata/tenthousandhashes.hex
+++ b/core/vm/runtime/testdata/tenthousandhashes.hex
@@ -1 +1 @@
-6080604052348015600f57600080fd5b506004361060285760003560e01c806330627b7c14602d575b600080fd5b60336035565b005b60005b614e20811015606a5760408051602081018390520160408051601f198184030190525280606381606d565b9150506038565b50565b600060018201608c57634e487b7160e01b600052601160045260246000fd5b506001019056fea2646970667358221220e499ea6d1ccd5fce28e8cd2507f6fef33f0d005543e04580f290bbd01df0655464736f6c63430008110033
\ No newline at end of file
+6080604052348015600f57600080fd5b506004361060285760003560e01c806330627b7c14602d575b600080fd5b60336045565b60405190815260200160405180910390f35b6000805b614e20811015608e57604080516020810184905290810182905260600160405160208183030381529060405280519060200120915080806087906092565b9150506049565b5090565b60006001820160b157634e487b7160e01b600052601160045260246000fd5b506001019056fea26469706673582212207f65e4c71aef311b81ff8b065cdd78f5be33f5dc80d3b2318e118075e33697f164736f6c63430008110033
\ No newline at end of file
From 76b49515e070b7c78426cb2cb7fad52438799633 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Thu, 11 Jun 2026 15:49:34 -0500
Subject: [PATCH 04/23] core/vm/runtime/testdata: swapped out loop benchmarks
for block sync benchmarks
---
core/vm/runtime/testdata/evm-bench/README.md | 8 ++++-
core/vm/runtime/testdata/evm-bench/compare.sh | 33 ++++++++++---------
2 files changed, 24 insertions(+), 17 deletions(-)
diff --git a/core/vm/runtime/testdata/evm-bench/README.md b/core/vm/runtime/testdata/evm-bench/README.md
index fccea2ce87..128d1faccf 100644
--- a/core/vm/runtime/testdata/evm-bench/README.md
+++ b/core/vm/runtime/testdata/evm-bench/README.md
@@ -32,7 +32,13 @@ core/vm/runtime/testdata/evm-bench/compare.sh HEAD~1 8 # vs another ref, count
`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`.
+interpreter change is committed) and runs `benchstat`. Besides the contract
+workloads it runs both `BenchmarkInsertChain_evmWorkload` variants from
+`core/bench_test.go`, which push blocks carrying these contracts through the
+full block import path (state in memory and on disk) and report Mgas/s, as a
+local stand-in for sync throughput. The synthetic dispatch loops (`BenchmarkSimpleLoop/loop*` in
+`core/vm/runtime/runtime_test.go`) are not part of the A/B suite, but can
+still be run manually to isolate dispatch overhead.
## Regenerating the bytecode
diff --git a/core/vm/runtime/testdata/evm-bench/compare.sh b/core/vm/runtime/testdata/evm-bench/compare.sh
index 624120bf6e..aaf4eb9282 100755
--- a/core/vm/runtime/testdata/evm-bench/compare.sh
+++ b/core/vm/runtime/testdata/evm-bench/compare.sh
@@ -1,8 +1,9 @@
#!/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.
+# contract workloads (core/vm/runtime/evm_bench_test.go) plus the block import
+# benchmark (core/bench_test.go, BenchmarkInsertChain_evmWorkload, a local
+# stand-in for sync throughput) 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
@@ -22,26 +23,25 @@ COUNT="${2:-10}"
# 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).
+# per count on a fast box. Each entry is package:pattern:iterations.
BENCHES=(
- '^BenchmarkSnailtracer$:20x'
- '^BenchmarkTenThousandHashes$:100x'
- '^BenchmarkERC20Transfer$:100x'
- '^BenchmarkERC20Mint$:150x'
- '^BenchmarkERC20ApprovalTransfer$:120x'
- '^BenchmarkSimpleLoop$/^loop:7x'
+ './core/vm/runtime/:^BenchmarkSnailtracer$:20x'
+ './core/vm/runtime/:^BenchmarkTenThousandHashes$:100x'
+ './core/vm/runtime/:^BenchmarkERC20Transfer$:100x'
+ './core/vm/runtime/:^BenchmarkERC20Mint$:150x'
+ './core/vm/runtime/:^BenchmarkERC20ApprovalTransfer$:120x'
+ './core/:^BenchmarkInsertChain_evmWorkload_memdb$:10x'
+ './core/:^BenchmarkInsertChain_evmWorkload_diskdb$:10x'
)
NEW="$(mktemp)"; OLD="$(mktemp)"
run_suite() { # run_suite
- local dir="$1" out="$2" entry pat n
+ local dir="$1" out="$2" entry pkg pat n
for entry in "${BENCHES[@]}"; do
- pat="${entry%:*}"
+ pkg="${entry%%:*}"
+ pat="${entry#*:}"; pat="${pat%:*}"
n="${entry##*:}"
- ( cd "$dir" && go test ./core/vm/runtime/ -run '^$' -bench "$pat" -benchmem -benchtime "$n" -count="$COUNT" ) | tee -a "$out"
+ ( cd "$dir" && go test "$pkg" -run '^$' -bench "$pat" -benchmem -benchtime "$n" -count="$COUNT" ) | tee -a "$out"
done
}
@@ -52,6 +52,7 @@ 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/"
+cp core/bench_test.go "$WT/core/bench_test.go"
mkdir -p "$WT/core/vm/runtime/testdata"
cp core/vm/runtime/testdata/*.hex "$WT/core/vm/runtime/testdata/"
run_suite "$WT" "$OLD"
From 5a2400ddd1097ba67b8af1d0f81bfe9017a2e19e Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Mon, 15 Jun 2026 18:34:13 -0500
Subject: [PATCH 05/23] core/vm/gen: make code gen more readable
---
core/vm/gen/main.go | 307 ++++++++++++++++++++++++++++----------------
1 file changed, 193 insertions(+), 114 deletions(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index c89ff2459b..3a7f121b3f 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -115,7 +115,15 @@ type generator struct {
buf *bytes.Buffer
}
-func (g *generator) p(format string, args ...any) { fmt.Fprintf(g.buf, format, args...) }
+// p formats and appends generated Go, like fmt.Fprintf. A template can start on
+// the line after p( and be indented to match its structure. p drops the leading
+// newline and the indent before the closing backtick, and gofmt tidies the rest.
+// Double any percent meant for the generated code (%%w, %%v) so Fprintf emits it
+// literally.
+func (g *generator) p(format string, args ...any) {
+ format = strings.TrimRight(strings.TrimPrefix(format, "\n"), " \t")
+ fmt.Fprintf(g.buf, format, args...)
+}
// ---------------------------------------------------------------------------
// Handler parsing + body splicing
@@ -254,11 +262,25 @@ func (g *generator) emitStackChecks(m opMeta) {
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)
+ g.p(`
+ if sLen := stack.len(); sLen < %d {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: %d}
+ } else if sLen > %d {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: %d}
+ }
+ `, 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)
+ g.p(`
+ if sLen := stack.len(); sLen < %d {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: %d}
+ }
+ `, 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)
+ g.p(`
+ if sLen := stack.len(); sLen > %d {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: %d}
+ }
+ `, m.maxStack, m.maxStack)
}
}
@@ -266,7 +288,12 @@ 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)
+ g.p(`
+ if contract.Gas.RegularGas < %d {
+ return nil, ErrOutOfGas
+ }
+ contract.Gas.RegularGas -= %d
+ `, m.constGas, m.constGas)
}
// emitWork emits the stack/gas guards and the opcode body (the portion that runs
@@ -280,14 +307,27 @@ func (g *generator) emitWork(code byte) {
// 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")
+ g.p(`
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ `)
}
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)
+ g.p(`
+ scope.Stack.dup(%d)
+ pc++
+ continue mainLoop
+ `, int(code)-0x7f)
default:
g.p("%s", g.inlineBody(inlineHandler[code]))
}
@@ -295,13 +335,19 @@ func (g *generator) emitWork(code byte) {
// emitPushFixed inlines makePush(n, n) for PUSH (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)
+ g.p(`
+ codeLen := len(scope.Contract.Code)
+ start := min(codeLen, int(pc+1))
+ end := min(codeLen, start+%d)
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
+ if missing := %d - (end - start); missing > 0 {
+ a.Lsh(a, uint(8*missing))
+ }
+ pc += %d
+ pc++
+ continue mainLoop
+ `, n, n, n)
}
func (g *generator) emitInlineCase(code byte) {
@@ -316,7 +362,10 @@ func (g *generator) emitInlineCase(code byte) {
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")
+ g.p(`
+ res, err = opUndefined(&pc, evm, scope)
+ break mainLoop
+ `)
}
// emitDirectCold emits a cold opcode case identical to the default case, except
@@ -329,67 +378,90 @@ func (g *generator) emitDirectCold(code byte) {
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")
+ // The three %s are the memory-size, dynamic-gas and handler names, in the
+ // order fns[2], fns[1], fns[0]. The doubled %%w and %%v are not generator
+ // verbs: Fprintf collapses each %% to a single %, so the generated code ends
+ // up with a literal fmt.Errorf("%w: %v", ...).
+ g.p(`
+ var memorySize uint64
+ {
+ memSize, overflow := %s(stack)
+ if overflow {
+ return nil, ErrGasUintOverflow
+ }
+ if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
+ return nil, ErrGasUintOverflow
+ }
+ }
+ var dynamicCost GasCosts
+ dynamicCost, err = %s(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 = %s(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ `, fns[2], fns[1], fns[0])
}
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
-`)
+ // The doubled %%w and %%v below are not generator verbs: Fprintf collapses
+ // each %% to a single %, leaving a literal fmt.Errorf("%w: %v", ...) in the
+ // generated default case.
+ g.p(`
+ 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
+ `)
}
// ---------------------------------------------------------------------------
@@ -397,48 +469,55 @@ continue mainLoop
// ---------------------------------------------------------------------------
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.p(`
+ // Code generated by core/vm/gen; DO NOT EDIT.
- 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 {
-`)
+ package vm
+
+ import (
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ )
+
+ `)
+
+ g.p(`
+ // 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)
From 12eb1f1e02c7db6c7569303df56c2689bfc95e6a Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Thu, 18 Jun 2026 13:11:56 -0500
Subject: [PATCH 06/23] core/vm: adapt generated switch to merged in-place
stack and gas API
---
core/vm/gen/main.go | 2 +-
core/vm/interp_diff_test.go | 4 +-
core/vm/interp_gen.go | 84 ++++++++++++++++++-------------------
3 files changed, 45 insertions(+), 45 deletions(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index 3a7f121b3f..7cdf4b6eb7 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -510,7 +510,7 @@ func (g *generator) emitFile() {
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)
+ contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if consumed < wanted {
return nil, ErrOutOfGas
}
diff --git a/core/vm/interp_diff_test.go b/core/vm/interp_diff_test.go
index 0b32959d86..f5dc55b55b 100644
--- a/core/vm/interp_diff_test.go
+++ b/core/vm/interp_diff_test.go
@@ -279,7 +279,7 @@ func TestExtraEIPs(t *testing.T) {
Origin: diffCaller,
GasPrice: uint256.NewInt(1),
})
- _, _, err := evm.Call(diffCaller, diffContractAddr, nil, NewGasBudget(100000), new(uint256.Int))
+ _, _, err := evm.Call(diffCaller, diffContractAddr, nil, NewGasBudget(100000, 0), new(uint256.Int))
if err != nil {
t.Fatalf("PUSH0 enabled via ExtraEips failed: %v", err)
}
@@ -301,7 +301,7 @@ func runOne(t testing.TB, cfg *params.ChainConfig, merged, useTableLoop bool, co
})
evm.forceTableLoop = useTableLoop
- ret, leftOver, err := evm.Call(diffCaller, diffContractAddr, input, NewGasBudget(gas), new(uint256.Int))
+ ret, leftOver, err := evm.Call(diffCaller, diffContractAddr, input, NewGasBudget(gas, 0), new(uint256.Int))
errStr := ""
if err != nil {
errStr = err.Error()
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
index a2b3c6ed71..799545f1f5 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interp_gen.go
@@ -35,7 +35,7 @@ mainLoop:
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)
+ contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -50,8 +50,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Add(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Add(x, y)
pc++
continue mainLoop
@@ -63,8 +63,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Mul(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Mul(x, y)
pc++
continue mainLoop
@@ -76,8 +76,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Sub(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Sub(x, y)
pc++
continue mainLoop
@@ -89,8 +89,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Div(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Div(x, y)
pc++
continue mainLoop
@@ -102,8 +102,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.SDiv(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.SDiv(x, y)
pc++
continue mainLoop
@@ -115,8 +115,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Mod(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Mod(x, y)
pc++
continue mainLoop
@@ -128,8 +128,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.SMod(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.SMod(x, y)
pc++
continue mainLoop
@@ -141,8 +141,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 8
- x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek()
- z.AddMod(&x, &y, z)
+ x, y, z := scope.Stack.pop2Peek1()
+ z.AddMod(x, y, z)
pc++
continue mainLoop
@@ -154,8 +154,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 8
- x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek()
- z.MulMod(&x, &y, z)
+ x, y, z := scope.Stack.pop2Peek1()
+ z.MulMod(x, y, z)
pc++
continue mainLoop
@@ -167,8 +167,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- back, num := scope.Stack.pop(), scope.Stack.peek()
- num.ExtendSign(num, &back)
+ back, num := scope.Stack.pop1Peek1()
+ num.ExtendSign(num, back)
pc++
continue mainLoop
@@ -180,7 +180,7 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop(), scope.Stack.peek()
+ x, y := scope.Stack.pop1Peek1()
if x.Lt(y) {
y.SetOne()
} else {
@@ -197,7 +197,7 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop(), scope.Stack.peek()
+ x, y := scope.Stack.pop1Peek1()
if x.Gt(y) {
y.SetOne()
} else {
@@ -214,7 +214,7 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop(), scope.Stack.peek()
+ x, y := scope.Stack.pop1Peek1()
if x.Slt(y) {
y.SetOne()
} else {
@@ -231,7 +231,7 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop(), scope.Stack.peek()
+ x, y := scope.Stack.pop1Peek1()
if x.Sgt(y) {
y.SetOne()
} else {
@@ -248,7 +248,7 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop(), scope.Stack.peek()
+ x, y := scope.Stack.pop1Peek1()
if x.Eq(y) {
y.SetOne()
} else {
@@ -282,8 +282,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.And(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.And(x, y)
pc++
continue mainLoop
@@ -295,8 +295,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Or(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Or(x, y)
pc++
continue mainLoop
@@ -308,8 +308,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Xor(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Xor(x, y)
pc++
continue mainLoop
@@ -334,8 +334,8 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- th, val := scope.Stack.pop(), scope.Stack.peek()
- val.Byte(&th)
+ th, val := scope.Stack.pop1Peek1()
+ val.Byte(th)
pc++
continue mainLoop
@@ -348,7 +348,7 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- shift, value := scope.Stack.pop(), scope.Stack.peek()
+ shift, value := scope.Stack.pop1Peek1()
if shift.LtUint64(256) {
value.Lsh(value, uint(shift.Uint64()))
} else {
@@ -369,7 +369,7 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- shift, value := scope.Stack.pop(), scope.Stack.peek()
+ shift, value := scope.Stack.pop1Peek1()
if shift.LtUint64(256) {
value.Rsh(value, uint(shift.Uint64()))
} else {
@@ -390,7 +390,7 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- shift, value := scope.Stack.pop(), scope.Stack.peek()
+ shift, value := scope.Stack.pop1Peek1()
if shift.GtUint64(256) {
if value.Sign() >= 0 {
value.Clear()
@@ -470,7 +470,7 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 2
- scope.Stack.pop()
+ scope.Stack.drop()
pc++
continue mainLoop
@@ -594,8 +594,8 @@ mainLoop:
res, err = nil, errStopToken
break mainLoop
}
- pos := scope.Stack.pop()
- if !scope.Contract.validJumpdest(&pos) {
+ pos := scope.Stack.pop1()
+ if !scope.Contract.validJumpdest(pos) {
res, err = nil, ErrInvalidJump
break mainLoop
}
@@ -615,9 +615,9 @@ mainLoop:
res, err = nil, errStopToken
break mainLoop
}
- pos, cond := scope.Stack.pop(), scope.Stack.pop()
+ pos, cond := scope.Stack.pop2()
if !cond.IsZero() {
- if !scope.Contract.validJumpdest(&pos) {
+ if !scope.Contract.validJumpdest(pos) {
res, err = nil, ErrInvalidJump
break mainLoop
}
From c52874d6547e0bf3e4e7ae9f44dd57dd19f509a2 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Thu, 18 Jun 2026 14:11:04 -0500
Subject: [PATCH 07/23] core/vm: splice stack helper bodies into the generated
switch
---
core/vm/gen/main.go | 48 +++++-
core/vm/interp_gen.go | 361 ++++++++++++++++++++++++++++++------------
2 files changed, 301 insertions(+), 108 deletions(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index 7cdf4b6eb7..a555cd7300 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -196,7 +196,36 @@ func (g *generator) inlineBody(handler string) string {
}
out.WriteString(line + "\n")
}
- return out.String()
+ return expandStackHelpers(out.String())
+}
+
+// The compiler shrinks its inlining budget into very large functions, and
+// execUntraced is far past the size threshold. The cheap stack helpers
+// (pop1, len, peek, drop, back) still inline there, but get and the
+// pop1Peek1 family turn into real calls, which a snailtracer profile put at
+// over a tenth of the run. The generator splices those helper bodies in
+// textually wherever they appear in statement position, verbatim from
+// stack.go.
+var (
+ pop1Peek1Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+) := scope\.Stack\.pop1Peek1\(\)$`)
+ pop2Peek1Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+), (\w+) := scope\.Stack\.pop2Peek1\(\)$`)
+ pop2Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+) := scope\.Stack\.pop2\(\)$`)
+ getAssignRe = regexp.MustCompile(`(?m)^(\s*)(\w+) := scope\.Stack\.get\(\)$`)
+ getCallRe = regexp.MustCompile(`(?m)^(\s*)scope\.Stack\.get\(\)\.(\w+)\((.*)\)$`)
+)
+
+func expandStackHelpers(src string) string {
+ src = pop1Peek1Re.ReplaceAllString(src,
+ "${1}stack.inner.top--\n${1}stack.size--\n${1}$2 := &stack.inner.data[stack.inner.top]\n${1}$3 := &stack.inner.data[stack.inner.top-1]")
+ src = pop2Peek1Re.ReplaceAllString(src,
+ "${1}stack.inner.top -= 2\n${1}stack.size -= 2\n${1}$2 := &stack.inner.data[stack.inner.top+1]\n${1}$3 := &stack.inner.data[stack.inner.top]\n${1}$4 := &stack.inner.data[stack.inner.top-1]")
+ src = pop2Re.ReplaceAllString(src,
+ "${1}stack.inner.top -= 2\n${1}stack.size -= 2\n${1}$2 := &stack.inner.data[stack.inner.top+1]\n${1}$3 := &stack.inner.data[stack.inner.top]")
+ src = getAssignRe.ReplaceAllString(src,
+ "${1}$2 := &stack.inner.data[stack.inner.top]\n${1}stack.inner.top++\n${1}stack.size++")
+ src = getCallRe.ReplaceAllString(src,
+ "${1}stack.inner.data[stack.inner.top].$2($3)\n${1}stack.inner.top++\n${1}stack.size++")
+ return src
}
// ---------------------------------------------------------------------------
@@ -322,12 +351,21 @@ func (g *generator) emitWork(code byte) {
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)
+ case code >= 0x80 && code <= 0x8f: // DUP1-DUP16: the dup body, emitted raw
g.p(`
- scope.Stack.dup(%d)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-%d]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
`, int(code)-0x7f)
+ case code >= 0x90 && code <= 0x9f: // SWAP1-SWAP16: the swap body, emitted raw
+ n := int(code) - 0x8f
+ g.p(`
+ stack.inner.data[stack.bottom+stack.size-%d], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-%d]
+ pc++
+ continue mainLoop
+ `, n+1, n+1)
default:
g.p("%s", g.inlineBody(inlineHandler[code]))
}
@@ -339,7 +377,9 @@ func (g *generator) emitPushFixed(n int) {
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+%d)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := %d - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
index 799545f1f5..4dae39b7ef 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interp_gen.go
@@ -50,7 +50,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
y.Add(x, y)
pc++
continue mainLoop
@@ -63,7 +66,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
y.Mul(x, y)
pc++
continue mainLoop
@@ -76,7 +82,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
y.Sub(x, y)
pc++
continue mainLoop
@@ -89,7 +98,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
y.Div(x, y)
pc++
continue mainLoop
@@ -102,7 +114,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
y.SDiv(x, y)
pc++
continue mainLoop
@@ -115,7 +130,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
y.Mod(x, y)
pc++
continue mainLoop
@@ -128,7 +146,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
y.SMod(x, y)
pc++
continue mainLoop
@@ -141,7 +162,11 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 8
- x, y, z := scope.Stack.pop2Peek1()
+ stack.inner.top -= 2
+ stack.size -= 2
+ x := &stack.inner.data[stack.inner.top+1]
+ y := &stack.inner.data[stack.inner.top]
+ z := &stack.inner.data[stack.inner.top-1]
z.AddMod(x, y, z)
pc++
continue mainLoop
@@ -154,7 +179,11 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 8
- x, y, z := scope.Stack.pop2Peek1()
+ stack.inner.top -= 2
+ stack.size -= 2
+ x := &stack.inner.data[stack.inner.top+1]
+ y := &stack.inner.data[stack.inner.top]
+ z := &stack.inner.data[stack.inner.top-1]
z.MulMod(x, y, z)
pc++
continue mainLoop
@@ -167,7 +196,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 5
- back, num := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ back := &stack.inner.data[stack.inner.top]
+ num := &stack.inner.data[stack.inner.top-1]
num.ExtendSign(num, back)
pc++
continue mainLoop
@@ -180,7 +212,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
if x.Lt(y) {
y.SetOne()
} else {
@@ -197,7 +232,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
if x.Gt(y) {
y.SetOne()
} else {
@@ -214,7 +252,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
if x.Slt(y) {
y.SetOne()
} else {
@@ -231,7 +272,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
if x.Sgt(y) {
y.SetOne()
} else {
@@ -248,7 +292,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
if x.Eq(y) {
y.SetOne()
} else {
@@ -282,7 +329,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
y.And(x, y)
pc++
continue mainLoop
@@ -295,7 +345,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
y.Or(x, y)
pc++
continue mainLoop
@@ -308,7 +361,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- x, y := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ x := &stack.inner.data[stack.inner.top]
+ y := &stack.inner.data[stack.inner.top-1]
y.Xor(x, y)
pc++
continue mainLoop
@@ -334,7 +390,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- th, val := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ th := &stack.inner.data[stack.inner.top]
+ val := &stack.inner.data[stack.inner.top-1]
val.Byte(th)
pc++
continue mainLoop
@@ -348,7 +407,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- shift, value := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ shift := &stack.inner.data[stack.inner.top]
+ value := &stack.inner.data[stack.inner.top-1]
if shift.LtUint64(256) {
value.Lsh(value, uint(shift.Uint64()))
} else {
@@ -369,7 +431,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- shift, value := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ shift := &stack.inner.data[stack.inner.top]
+ value := &stack.inner.data[stack.inner.top-1]
if shift.LtUint64(256) {
value.Rsh(value, uint(shift.Uint64()))
} else {
@@ -390,7 +455,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- shift, value := scope.Stack.pop1Peek1()
+ stack.inner.top--
+ stack.size--
+ shift := &stack.inner.data[stack.inner.top]
+ value := &stack.inner.data[stack.inner.top-1]
if shift.GtUint64(256) {
if value.Sign() >= 0 {
value.Clear()
@@ -615,7 +683,10 @@ mainLoop:
res, err = nil, errStopToken
break mainLoop
}
- pos, cond := scope.Stack.pop2()
+ stack.inner.top -= 2
+ stack.size -= 2
+ pos := &stack.inner.data[stack.inner.top+1]
+ cond := &stack.inner.data[stack.inner.top]
if !cond.IsZero() {
if !scope.Contract.validJumpdest(pos) {
res, err = nil, ErrInvalidJump
@@ -634,7 +705,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 2
- scope.Stack.get().SetUint64(pc)
+ stack.inner.data[stack.inner.top].SetUint64(pc)
+ stack.inner.top++
+ stack.size++
pc++
continue mainLoop
@@ -646,7 +719,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 2
- scope.Stack.get().SetUint64(uint64(scope.Memory.Len()))
+ stack.inner.data[stack.inner.top].SetUint64(uint64(scope.Memory.Len()))
+ stack.inner.top++
+ stack.size++
pc++
continue mainLoop
@@ -667,7 +742,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 2
- scope.Stack.get().Clear()
+ stack.inner.data[stack.inner.top].Clear()
+ stack.inner.top++
+ stack.size++
pc++
continue mainLoop
@@ -753,7 +830,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+3)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 3 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -780,7 +859,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+4)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 4 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -807,7 +888,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+5)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 5 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -834,7 +917,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+6)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 6 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -861,7 +946,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+7)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 7 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -888,7 +975,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+8)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 8 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -915,7 +1004,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+9)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 9 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -942,7 +1033,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+10)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 10 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -969,7 +1062,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+11)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 11 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -996,7 +1091,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+12)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 12 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1023,7 +1120,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+13)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 13 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1050,7 +1149,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+14)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 14 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1077,7 +1178,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+15)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 15 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1104,7 +1207,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+16)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 16 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1131,7 +1236,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+17)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 17 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1158,7 +1265,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+18)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 18 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1185,7 +1294,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+19)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 19 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1212,7 +1323,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+20)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 20 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1239,7 +1352,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+21)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 21 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1266,7 +1381,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+22)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 22 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1293,7 +1410,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+23)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 23 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1320,7 +1439,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+24)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 24 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1347,7 +1468,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+25)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 25 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1374,7 +1497,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+26)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 26 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1401,7 +1526,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+27)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 27 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1428,7 +1555,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+28)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 28 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1455,7 +1584,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+29)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 29 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1482,7 +1613,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+30)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 30 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1509,7 +1642,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+31)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 31 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1536,7 +1671,9 @@ mainLoop:
codeLen := len(scope.Contract.Code)
start := min(codeLen, int(pc+1))
end := min(codeLen, start+32)
- a := scope.Stack.get()
+ a := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
a.SetBytes(scope.Contract.Code[start:end])
if missing := 32 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1554,7 +1691,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(1)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-1]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP2:
@@ -1567,7 +1706,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(2)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-2]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP3:
@@ -1580,7 +1721,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(3)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-3]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP4:
@@ -1593,7 +1736,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(4)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-4]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP5:
@@ -1606,7 +1751,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(5)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-5]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP6:
@@ -1619,7 +1766,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(6)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-6]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP7:
@@ -1632,7 +1781,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(7)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-7]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP8:
@@ -1645,7 +1796,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(8)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-8]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP9:
@@ -1658,7 +1811,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(9)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-9]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP10:
@@ -1671,7 +1826,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(10)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-10]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP11:
@@ -1684,7 +1841,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(11)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-11]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP12:
@@ -1697,7 +1856,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(12)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-12]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP13:
@@ -1710,7 +1871,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(13)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-13]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP14:
@@ -1723,7 +1886,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(14)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-14]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP15:
@@ -1736,7 +1901,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(15)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-15]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case DUP16:
@@ -1749,7 +1916,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.dup(16)
+ stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-16]
+ stack.size++
+ stack.inner.top++
pc++
continue mainLoop
case SWAP1:
@@ -1760,10 +1929,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap1()
+ stack.inner.data[stack.bottom+stack.size-2], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-2]
pc++
continue mainLoop
-
case SWAP2:
if sLen := stack.len(); sLen < 3 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 3}
@@ -1772,10 +1940,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap2()
+ stack.inner.data[stack.bottom+stack.size-3], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-3]
pc++
continue mainLoop
-
case SWAP3:
if sLen := stack.len(); sLen < 4 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 4}
@@ -1784,10 +1951,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap3()
+ stack.inner.data[stack.bottom+stack.size-4], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-4]
pc++
continue mainLoop
-
case SWAP4:
if sLen := stack.len(); sLen < 5 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 5}
@@ -1796,10 +1962,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap4()
+ stack.inner.data[stack.bottom+stack.size-5], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-5]
pc++
continue mainLoop
-
case SWAP5:
if sLen := stack.len(); sLen < 6 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 6}
@@ -1808,10 +1973,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap5()
+ stack.inner.data[stack.bottom+stack.size-6], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-6]
pc++
continue mainLoop
-
case SWAP6:
if sLen := stack.len(); sLen < 7 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 7}
@@ -1820,10 +1984,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap6()
+ stack.inner.data[stack.bottom+stack.size-7], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-7]
pc++
continue mainLoop
-
case SWAP7:
if sLen := stack.len(); sLen < 8 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 8}
@@ -1832,10 +1995,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap7()
+ stack.inner.data[stack.bottom+stack.size-8], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-8]
pc++
continue mainLoop
-
case SWAP8:
if sLen := stack.len(); sLen < 9 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 9}
@@ -1844,10 +2006,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap8()
+ stack.inner.data[stack.bottom+stack.size-9], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-9]
pc++
continue mainLoop
-
case SWAP9:
if sLen := stack.len(); sLen < 10 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 10}
@@ -1856,10 +2017,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap9()
+ stack.inner.data[stack.bottom+stack.size-10], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-10]
pc++
continue mainLoop
-
case SWAP10:
if sLen := stack.len(); sLen < 11 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 11}
@@ -1868,10 +2028,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap10()
+ stack.inner.data[stack.bottom+stack.size-11], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-11]
pc++
continue mainLoop
-
case SWAP11:
if sLen := stack.len(); sLen < 12 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 12}
@@ -1880,10 +2039,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap11()
+ stack.inner.data[stack.bottom+stack.size-12], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-12]
pc++
continue mainLoop
-
case SWAP12:
if sLen := stack.len(); sLen < 13 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 13}
@@ -1892,10 +2050,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap12()
+ stack.inner.data[stack.bottom+stack.size-13], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-13]
pc++
continue mainLoop
-
case SWAP13:
if sLen := stack.len(); sLen < 14 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 14}
@@ -1904,10 +2061,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap13()
+ stack.inner.data[stack.bottom+stack.size-14], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-14]
pc++
continue mainLoop
-
case SWAP14:
if sLen := stack.len(); sLen < 15 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 15}
@@ -1916,10 +2072,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap14()
+ stack.inner.data[stack.bottom+stack.size-15], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-15]
pc++
continue mainLoop
-
case SWAP15:
if sLen := stack.len(); sLen < 16 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 16}
@@ -1928,10 +2083,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap15()
+ stack.inner.data[stack.bottom+stack.size-16], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-16]
pc++
continue mainLoop
-
case SWAP16:
if sLen := stack.len(); sLen < 17 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 17}
@@ -1940,10 +2094,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 3
- scope.Stack.swap16()
+ stack.inner.data[stack.bottom+stack.size-17], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-17]
pc++
continue mainLoop
-
default:
operation := table[op]
if sLen := stack.len(); sLen < operation.minStack {
From ccbc468d4cf4db8a98d6b2a46519074d02d9559c Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Thu, 18 Jun 2026 15:58:41 -0500
Subject: [PATCH 08/23] core/vm: expand stack helpers declared in a var block
---
core/vm/gen/main.go | 16 +++++++++++++++-
core/vm/interp_gen.go | 16 ++++++++--------
2 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index a555cd7300..c34b326c99 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -205,16 +205,30 @@ func (g *generator) inlineBody(handler string) string {
// pop1Peek1 family turn into real calls, which a snailtracer profile put at
// over a tenth of the run. The generator splices those helper bodies in
// textually wherever they appear in statement position, verbatim from
-// stack.go.
+// stack.go. A helper declared inside a var ( ... ) group (the push handlers
+// write elem = scope.Stack.get() there) is first lifted out into := statements,
+// since its replacement is statements rather than an expression.
var (
pop1Peek1Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+) := scope\.Stack\.pop1Peek1\(\)$`)
pop2Peek1Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+), (\w+) := scope\.Stack\.pop2Peek1\(\)$`)
pop2Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+) := scope\.Stack\.pop2\(\)$`)
getAssignRe = regexp.MustCompile(`(?m)^(\s*)(\w+) := scope\.Stack\.get\(\)$`)
getCallRe = regexp.MustCompile(`(?m)^(\s*)scope\.Stack\.get\(\)\.(\w+)\((.*)\)$`)
+ varBlockRe = regexp.MustCompile(`(?ms)^(\s*)var \(\n(.*?)\n\s*\)$`)
+ varMemberRe = regexp.MustCompile(`(?m)^\s*([\w, ]+?)\s*= (.*)$`)
)
func expandStackHelpers(src string) string {
+ // Lift any var ( ... ) group holding a stack helper into := statements:
+ // a helper expansion is statements, which cannot sit inside a var group.
+ src = varBlockRe.ReplaceAllStringFunc(src, func(block string) string {
+ m := varBlockRe.FindStringSubmatch(block)
+ indent, members := m[1], m[2]
+ if !strings.Contains(members, "scope.Stack.") {
+ return block
+ }
+ return varMemberRe.ReplaceAllString(members, indent+"$1 := $2")
+ })
src = pop1Peek1Re.ReplaceAllString(src,
"${1}stack.inner.top--\n${1}stack.size--\n${1}$2 := &stack.inner.data[stack.inner.top]\n${1}$3 := &stack.inner.data[stack.inner.top-1]")
src = pop2Peek1Re.ReplaceAllString(src,
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
index 4dae39b7ef..ab440cb453 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interp_gen.go
@@ -767,10 +767,10 @@ mainLoop:
pc++
continue mainLoop
}
- var (
- codeLen = uint64(len(scope.Contract.Code))
- elem = scope.Stack.get()
- )
+ codeLen := uint64(len(scope.Contract.Code))
+ elem := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
pc += 1
if pc < codeLen {
elem.SetUint64(uint64(scope.Contract.Code[pc]))
@@ -796,10 +796,10 @@ mainLoop:
pc++
continue mainLoop
}
- var (
- codeLen = uint64(len(scope.Contract.Code))
- elem = scope.Stack.get()
- )
+ codeLen := uint64(len(scope.Contract.Code))
+ elem := &stack.inner.data[stack.inner.top]
+ stack.inner.top++
+ stack.size++
if pc+2 < codeLen {
elem.SetBytes2(scope.Contract.Code[pc+1 : pc+3])
} else if pc+1 < codeLen {
From 840ae6006afe0c000fd096d9d2a0fcacef5fe600 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Fri, 19 Jun 2026 08:39:13 -0500
Subject: [PATCH 09/23] core/vm: splice make* factory closures into the
generated switch
---
core/vm/gen/main.go | 115 +++++++++++------
core/vm/interp_gen.go | 286 +++++++++++++++++++++++++++++-------------
2 files changed, 274 insertions(+), 127 deletions(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index c34b326c99..f5fc034ade 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -115,6 +115,10 @@ type generator struct {
buf *bytes.Buffer
}
+// p is the sole writer of the generated file. Every line of output is appended
+// to g.buf through it. The splice helpers (inlineBody, inlineFactoryBody) only
+// build text, which their callers then hand to p.
+//
// p formats and appends generated Go, like fmt.Fprintf. A template can start on
// the line after p( and be indented to match its structure. p drops the leading
// newline and the indent before the closing backtick, and gofmt tidies the rest.
@@ -153,26 +157,84 @@ func parseHandlers(vmDir string) (*token.FileSet, map[string]*ast.FuncDecl) {
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.
+// inlineBody returns a named handler's body, rewritten so it can be spliced
+// into the dispatch loop (see rewriteSplicedBody). The caller emits it with p.
func (g *generator) inlineBody(handler string) string {
fn := g.handlers[handler]
if fn == nil {
fatalf("no handler %q to inline", handler)
}
+ return g.rewriteSplicedBody(g.renderAst(fn.Body.List))
+}
+
+// inlineFactoryBody splices the body of the executionFunc closure that a make*
+// factory returns, substituting the factory's parameters with the per-opcode
+// constants in args (positional, matching the factory signature). This lets
+// closure-built handlers (makePush, makeDup) be derived from their single
+// definition rather than restated in the generator. The caller emits the
+// result with p.
+func (g *generator) inlineFactoryBody(factory string, args ...int) string {
+ fn := g.handlers[factory]
+ if fn == nil {
+ fatalf("no factory %q to inline", factory)
+ }
+ lit := factoryClosure(factory, fn)
+ src := g.renderAst(lit.Body.List)
+ var names []string
+ for _, f := range fn.Type.Params.List {
+ for _, nm := range f.Names {
+ names = append(names, nm.Name)
+ }
+ }
+ if len(names) != len(args) {
+ fatalf("factory %q takes %d params, got %d args", factory, len(names), len(args))
+ }
+ for i, nm := range names {
+ src = regexp.MustCompile(`\b`+nm+`\b`).ReplaceAllString(src, fmt.Sprint(args[i]))
+ }
+ return g.rewriteSplicedBody(src)
+}
+
+// factoryClosure returns the executionFunc literal that a make* factory's body
+// is a single `return func(...) {...}` of.
+func factoryClosure(name string, fn *ast.FuncDecl) *ast.FuncLit {
+ if len(fn.Body.List) != 1 {
+ fatalf("factory %q body is not a single return", name)
+ }
+ ret, ok := fn.Body.List[0].(*ast.ReturnStmt)
+ if !ok || len(ret.Results) != 1 {
+ fatalf("factory %q does not return a single value", name)
+ }
+ lit, ok := ret.Results[0].(*ast.FuncLit)
+ if !ok {
+ fatalf("factory %q does not return a func literal", name)
+ }
+ return lit
+}
+
+// renderAst converts AST statements back to formatted Go source text, the
+// inverse of parsing. It uses the generator's fileset and emits nothing itself
+// (the caller passes the result to p).
+func (g *generator) renderAst(stmts []ast.Stmt) string {
var raw bytes.Buffer
cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8}
- for _, stmt := range fn.Body.List {
+ for _, stmt := range stmts {
if err := cfg.Fprint(&raw, g.fset, stmt); err != nil {
- fatalf("print %s body: %v", handler, err)
+ fatalf("print stmt: %v", err)
}
raw.WriteByte('\n')
}
- src := strings.ReplaceAll(raw.String(), "*pc", "pc")
+ return raw.String()
+}
- var out strings.Builder
+// rewriteSplicedBody rewrites a printed handler body so it runs inside 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. Stack helpers are then expanded.
+func (g *generator) rewriteSplicedBody(src string) string {
+ src = strings.ReplaceAll(src, "*pc", "pc")
+
+ var out bytes.Buffer
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])
@@ -214,6 +276,7 @@ var (
pop2Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+) := scope\.Stack\.pop2\(\)$`)
getAssignRe = regexp.MustCompile(`(?m)^(\s*)(\w+) := scope\.Stack\.get\(\)$`)
getCallRe = regexp.MustCompile(`(?m)^(\s*)scope\.Stack\.get\(\)\.(\w+)\((.*)\)$`)
+ dupRe = regexp.MustCompile(`(?m)^(\s*)scope\.Stack\.dup\((\d+)\)$`)
varBlockRe = regexp.MustCompile(`(?ms)^(\s*)var \(\n(.*?)\n\s*\)$`)
varMemberRe = regexp.MustCompile(`(?m)^\s*([\w, ]+?)\s*= (.*)$`)
)
@@ -239,6 +302,8 @@ func expandStackHelpers(src string) string {
"${1}$2 := &stack.inner.data[stack.inner.top]\n${1}stack.inner.top++\n${1}stack.size++")
src = getCallRe.ReplaceAllString(src,
"${1}stack.inner.data[stack.inner.top].$2($3)\n${1}stack.inner.top++\n${1}stack.size++")
+ src = dupRe.ReplaceAllString(src,
+ "${1}stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-$2]\n${1}stack.size++\n${1}stack.inner.top++")
return src
}
@@ -363,16 +428,11 @@ func (g *generator) emitWork(code byte) {
}
switch {
- case code >= 0x62 && code <= 0x7f: // PUSH3-PUSH32: inline makePush(n,n)
- g.emitPushFixed(int(code) - 0x5f)
- case code >= 0x80 && code <= 0x8f: // DUP1-DUP16: the dup body, emitted raw
- g.p(`
- stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-%d]
- stack.size++
- stack.inner.top++
- pc++
- continue mainLoop
- `, int(code)-0x7f)
+ case code >= 0x62 && code <= 0x7f: // PUSH3-PUSH32: splice makePush(n, n)
+ n := int(code) - 0x5f
+ g.p("%s", g.inlineFactoryBody("makePush", n, n))
+ case code >= 0x80 && code <= 0x8f: // DUP1-DUP16: splice makeDup(n)
+ g.p("%s", g.inlineFactoryBody("makeDup", int(code)-0x7f))
case code >= 0x90 && code <= 0x9f: // SWAP1-SWAP16: the swap body, emitted raw
n := int(code) - 0x8f
g.p(`
@@ -385,25 +445,6 @@ func (g *generator) emitWork(code byte) {
}
}
-// emitPushFixed inlines makePush(n, n) for PUSH (n = 3..32).
-func (g *generator) emitPushFixed(n int) {
- g.p(`
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+%d)
- a := &stack.inner.data[stack.inner.top]
- stack.inner.top++
- stack.size++
- a.SetBytes(scope.Contract.Code[start:end])
- if missing := %d - (end - start); missing > 0 {
- a.Lsh(a, uint(8*missing))
- }
- pc += %d
- pc++
- continue mainLoop
- `, n, n, n)
-}
-
func (g *generator) emitInlineCase(code byte) {
m := g.meta[code]
g.p("case %s:\n", m.name)
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
index ab440cb453..08edb5a04e 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interp_gen.go
@@ -827,9 +827,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+3)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+3)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -840,6 +842,7 @@ mainLoop:
pc += 3
pc++
continue mainLoop
+
case PUSH4:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -856,9 +859,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+4)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+4)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -869,6 +874,7 @@ mainLoop:
pc += 4
pc++
continue mainLoop
+
case PUSH5:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -885,9 +891,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+5)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+5)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -898,6 +906,7 @@ mainLoop:
pc += 5
pc++
continue mainLoop
+
case PUSH6:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -914,9 +923,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+6)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+6)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -927,6 +938,7 @@ mainLoop:
pc += 6
pc++
continue mainLoop
+
case PUSH7:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -943,9 +955,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+7)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+7)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -956,6 +970,7 @@ mainLoop:
pc += 7
pc++
continue mainLoop
+
case PUSH8:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -972,9 +987,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+8)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+8)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -985,6 +1002,7 @@ mainLoop:
pc += 8
pc++
continue mainLoop
+
case PUSH9:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1001,9 +1019,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+9)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+9)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1014,6 +1034,7 @@ mainLoop:
pc += 9
pc++
continue mainLoop
+
case PUSH10:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1030,9 +1051,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+10)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+10)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1043,6 +1066,7 @@ mainLoop:
pc += 10
pc++
continue mainLoop
+
case PUSH11:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1059,9 +1083,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+11)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+11)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1072,6 +1098,7 @@ mainLoop:
pc += 11
pc++
continue mainLoop
+
case PUSH12:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1088,9 +1115,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+12)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+12)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1101,6 +1130,7 @@ mainLoop:
pc += 12
pc++
continue mainLoop
+
case PUSH13:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1117,9 +1147,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+13)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+13)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1130,6 +1162,7 @@ mainLoop:
pc += 13
pc++
continue mainLoop
+
case PUSH14:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1146,9 +1179,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+14)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+14)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1159,6 +1194,7 @@ mainLoop:
pc += 14
pc++
continue mainLoop
+
case PUSH15:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1175,9 +1211,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+15)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+15)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1188,6 +1226,7 @@ mainLoop:
pc += 15
pc++
continue mainLoop
+
case PUSH16:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1204,9 +1243,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+16)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+16)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1217,6 +1258,7 @@ mainLoop:
pc += 16
pc++
continue mainLoop
+
case PUSH17:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1233,9 +1275,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+17)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+17)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1246,6 +1290,7 @@ mainLoop:
pc += 17
pc++
continue mainLoop
+
case PUSH18:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1262,9 +1307,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+18)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+18)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1275,6 +1322,7 @@ mainLoop:
pc += 18
pc++
continue mainLoop
+
case PUSH19:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1291,9 +1339,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+19)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+19)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1304,6 +1354,7 @@ mainLoop:
pc += 19
pc++
continue mainLoop
+
case PUSH20:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1320,9 +1371,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+20)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+20)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1333,6 +1386,7 @@ mainLoop:
pc += 20
pc++
continue mainLoop
+
case PUSH21:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1349,9 +1403,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+21)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+21)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1362,6 +1418,7 @@ mainLoop:
pc += 21
pc++
continue mainLoop
+
case PUSH22:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1378,9 +1435,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+22)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+22)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1391,6 +1450,7 @@ mainLoop:
pc += 22
pc++
continue mainLoop
+
case PUSH23:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1407,9 +1467,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+23)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+23)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1420,6 +1482,7 @@ mainLoop:
pc += 23
pc++
continue mainLoop
+
case PUSH24:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1436,9 +1499,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+24)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+24)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1449,6 +1514,7 @@ mainLoop:
pc += 24
pc++
continue mainLoop
+
case PUSH25:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1465,9 +1531,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+25)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+25)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1478,6 +1546,7 @@ mainLoop:
pc += 25
pc++
continue mainLoop
+
case PUSH26:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1494,9 +1563,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+26)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+26)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1507,6 +1578,7 @@ mainLoop:
pc += 26
pc++
continue mainLoop
+
case PUSH27:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1523,9 +1595,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+27)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+27)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1536,6 +1610,7 @@ mainLoop:
pc += 27
pc++
continue mainLoop
+
case PUSH28:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1552,9 +1627,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+28)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+28)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1565,6 +1642,7 @@ mainLoop:
pc += 28
pc++
continue mainLoop
+
case PUSH29:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1581,9 +1659,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+29)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+29)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1594,6 +1674,7 @@ mainLoop:
pc += 29
pc++
continue mainLoop
+
case PUSH30:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1610,9 +1691,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+30)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+30)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1623,6 +1706,7 @@ mainLoop:
pc += 30
pc++
continue mainLoop
+
case PUSH31:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1639,9 +1723,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+31)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+31)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1652,6 +1738,7 @@ mainLoop:
pc += 31
pc++
continue mainLoop
+
case PUSH32:
if sLen := stack.len(); sLen > 1023 {
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
@@ -1668,9 +1755,11 @@ mainLoop:
pc++
continue mainLoop
}
- codeLen := len(scope.Contract.Code)
- start := min(codeLen, int(pc+1))
- end := min(codeLen, start+32)
+ var (
+ codeLen = len(scope.Contract.Code)
+ start = min(codeLen, int(pc+1))
+ end = min(codeLen, start+32)
+ )
a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
@@ -1681,6 +1770,7 @@ mainLoop:
pc += 32
pc++
continue mainLoop
+
case DUP1:
if sLen := stack.len(); sLen < 1 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
@@ -1696,6 +1786,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP2:
if sLen := stack.len(); sLen < 2 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
@@ -1711,6 +1802,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP3:
if sLen := stack.len(); sLen < 3 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 3}
@@ -1726,6 +1818,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP4:
if sLen := stack.len(); sLen < 4 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 4}
@@ -1741,6 +1834,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP5:
if sLen := stack.len(); sLen < 5 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 5}
@@ -1756,6 +1850,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP6:
if sLen := stack.len(); sLen < 6 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 6}
@@ -1771,6 +1866,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP7:
if sLen := stack.len(); sLen < 7 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 7}
@@ -1786,6 +1882,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP8:
if sLen := stack.len(); sLen < 8 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 8}
@@ -1801,6 +1898,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP9:
if sLen := stack.len(); sLen < 9 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 9}
@@ -1816,6 +1914,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP10:
if sLen := stack.len(); sLen < 10 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 10}
@@ -1831,6 +1930,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP11:
if sLen := stack.len(); sLen < 11 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 11}
@@ -1846,6 +1946,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP12:
if sLen := stack.len(); sLen < 12 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 12}
@@ -1861,6 +1962,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP13:
if sLen := stack.len(); sLen < 13 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 13}
@@ -1876,6 +1978,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP14:
if sLen := stack.len(); sLen < 14 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 14}
@@ -1891,6 +1994,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP15:
if sLen := stack.len(); sLen < 15 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 15}
@@ -1906,6 +2010,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case DUP16:
if sLen := stack.len(); sLen < 16 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 16}
@@ -1921,6 +2026,7 @@ mainLoop:
stack.inner.top++
pc++
continue mainLoop
+
case SWAP1:
if sLen := stack.len(); sLen < 2 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
From ed8c379f42b58ce3d90825bea378e2c489688e0a Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Fri, 19 Jun 2026 12:52:34 -0500
Subject: [PATCH 10/23] core/vm: test the generated dispatch makes no real
stack-helper calls. consolidated tests into one file
---
core/vm/interp_diff_test.go | 365 ----------------------------
core/vm/interp_gen_test.go | 465 ++++++++++++++++++++++++++++++++++++
2 files changed, 465 insertions(+), 365 deletions(-)
delete mode 100644 core/vm/interp_diff_test.go
diff --git a/core/vm/interp_diff_test.go b/core/vm/interp_diff_test.go
deleted file mode 100644
index f5dc55b55b..0000000000
--- a/core/vm/interp_diff_test.go
+++ /dev/null
@@ -1,365 +0,0 @@
-// 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 .
-
-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, 0), 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, 0), 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))
- }
- }
- })
-}
diff --git a/core/vm/interp_gen_test.go b/core/vm/interp_gen_test.go
index 83b4bb1ed9..26807ed84e 100644
--- a/core/vm/interp_gen_test.go
+++ b/core/vm/interp_gen_test.go
@@ -16,11 +16,29 @@
package vm
+// Tests for the generated interpreter dispatch (interp_gen.go): that the
+// committed file is up to date, that it behaves identically to the table loop,
+// and that the fast path keeps its cheap stack helpers inlined.
+
import (
+ "bytes"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "math/big"
"os"
"os/exec"
"path/filepath"
+ "regexp"
+ "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/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
)
// TestGeneratedDispatchUpToDate asserts that the committed interp_gen.go matches
@@ -49,3 +67,450 @@ func TestGeneratedDispatchUpToDate(t *testing.T) {
t.Fatalf("interp_gen.go is out of date; run `go generate ./core/vm/...` and commit the result")
}
}
+
+// Differential test comparing the table loop against the generated dispatch.
+//
+// These tests prove 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.
+
+// 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, 0), 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, 0), 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))
+ }
+ }
+ })
+}
+
+// expandedHelpers are the costly *Stack helpers the generator splices inline
+// (see expandStackHelpers in core/vm/gen) instead of leaving as calls, because
+// they exceed the inline budget for a function as large as execUntraced. After
+// generation none should remain as a call in interp_gen.go. If a change to the
+// handler source or to stack.go alters how one is written, the expansion regex
+// silently stops matching and the helper survives as a real call: correct, but
+// it drops the inlining the fast path exists for. This is the generator's old
+// post-condition, moved here so both halves of the invariant live together.
+var expandedHelpers = []string{"get", "dup", "pop2", "pop1Peek1", "pop2Peek1"}
+
+// TestGeneratedFastPathHelpersExpanded asserts the generator spliced every
+// costly stack helper inline, so none survives as a call in interp_gen.go. It
+// is the expand-side counterpart to TestGeneratedFastPathHelpersInlined: together
+// they hold the one invariant that the fast path makes no real stack-helper
+// call, the costly ones by splicing, the cheap ones by compiler inlining.
+func TestGeneratedFastPathHelpersExpanded(t *testing.T) {
+ calls := countStackCalls(t, "interp_gen.go")
+ for _, h := range expandedHelpers {
+ if n := calls[h]; n != 0 {
+ t.Errorf("(*Stack).%s: %d residual call(s) in interp_gen.go, expected 0.\n"+
+ "The generator no longer splices it inline, so it leaked through as a real call.\n"+
+ "Fix the matching regex in expandStackHelpers (core/vm/gen).", h, n)
+ }
+ }
+}
+
+// mustInlineHelpers are the cheap *Stack helpers the generated fast path calls
+// directly and trusts the compiler to inline into execUntraced. Unlike get,
+// dup, pop2, pop1Peek1 and pop2Peek1, which are too costly to inline into a
+// function that large and so are spliced in textually by the generator (see
+// expandStackHelpers in core/vm/gen, checked by TestGeneratedFastPathHelpersExpanded),
+// these stay as plain calls. They inline today, most with comfortable margin, but pop1 sits
+// at cost 18 against Go's big-function budget of 20. A toolchain that re-scores
+// inline cost, or an extra branch in one of these bodies, could silently stop
+// the inlining and quietly slow the interpreter. This is the set we refuse to
+// let regress in silence. back is absent because it has no call site here.
+var mustInlineHelpers = []string{"len", "pop1", "peek", "drop"}
+
+// TestGeneratedFastPathHelpersInlined recompiles this package with the
+// compiler's inlining diagnostics on and fails if any call to a mustInlineHelper
+// in interp_gen.go was not inlined. It is the inlining-side counterpart to
+// TestGeneratedFastPathHelpersExpanded, which checks the expensive helpers were
+// spliced away. Together they keep both halves of the fast path honest: the
+// costly helpers stay spliced, the cheap ones stay inlined.
+func TestGeneratedFastPathHelpersInlined(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping inlining check (recompiles the package) in -short mode")
+ }
+
+ // go build -gcflags=-m prints every inlining decision. The build cache
+ // replays the diagnostics on a hit, so repeated runs are deterministic. The
+ // flag applies only to this package, cached dependencies stay quiet.
+ out, err := exec.Command("go", "build", "-gcflags=-m", ".").CombinedOutput()
+ if err != nil {
+ t.Fatalf("compiling with inlining diagnostics: %v\n%s", err, out)
+ }
+ diag := string(out)
+ if !strings.Contains(diag, "interp_gen.go") {
+ t.Fatalf("captured no interp_gen.go diagnostics, the -m build produced nothing to check:\n%s", diag)
+ }
+
+ calls := countStackCalls(t, "interp_gen.go")
+ for _, h := range mustInlineHelpers {
+ inlinedRe := regexp.MustCompile(`interp_gen\.go.*inlining call to \(\*Stack\)\.` + regexp.QuoteMeta(h) + `\b`)
+ inlined := len(inlinedRe.FindAllString(diag, -1))
+ if calls[h] != inlined {
+ t.Errorf("(*Stack).%s: %d call site(s) in interp_gen.go, %d inlined into execUntraced.\n"+
+ "The compiler stopped inlining it, so the fast path now pays a real call. Shrink the\n"+
+ "body to fit the inline budget, or promote it to a spliced helper (add an expansion to\n"+
+ "expandStackHelpers in core/vm/gen and move it to expandedHelpers here).", h, calls[h], inlined)
+ continue
+ }
+ t.Logf("(*Stack).%s: %d/%d call sites inlined", h, inlined, calls[h])
+ }
+}
+
+// countStackCalls parses a generated source file and counts calls to each
+// *Stack helper method, keyed by method name. It matches the fast path's stack
+// local and scope.Stack receivers. Parsing rather than grepping keeps comments
+// and strings from inflating the count.
+func countStackCalls(t *testing.T, file string) map[string]int {
+ t.Helper()
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, file, nil, 0)
+ if err != nil {
+ t.Fatalf("parsing %s: %v", file, err)
+ }
+ counts := map[string]int{}
+ ast.Inspect(f, func(n ast.Node) bool {
+ call, ok := n.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+ if sel, ok := call.Fun.(*ast.SelectorExpr); ok && isStackReceiver(sel.X) {
+ counts[sel.Sel.Name]++
+ }
+ return true
+ })
+ return counts
+}
+
+// isStackReceiver reports whether x is the fast path's stack local or scope.Stack.
+func isStackReceiver(x ast.Expr) bool {
+ switch r := x.(type) {
+ case *ast.Ident:
+ return r.Name == "stack"
+ case *ast.SelectorExpr:
+ return r.Sel.Name == "Stack"
+ }
+ return false
+}
From 87bf249b711c5d4f079b2f9e585c4008d932f04f Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Fri, 19 Jun 2026 14:29:02 -0500
Subject: [PATCH 11/23] core/vm: normalize stack-helper call sites to one form
---
core/vm/eips.go | 3 ++-
core/vm/instructions.go | 18 ++++++++----------
core/vm/interp_gen.go | 9 ++++++---
core/vm/stack.go | 3 +--
4 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/core/vm/eips.go b/core/vm/eips.go
index 8a09856029..a7785fb019 100644
--- a/core/vm/eips.go
+++ b/core/vm/eips.go
@@ -237,7 +237,8 @@ func enable3855(jt *JumpTable) {
// opPush0 implements the PUSH0 opcode
func opPush0(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.get().Clear()
+ elem := scope.Stack.get()
+ elem.Clear()
return nil, nil
}
diff --git a/core/vm/instructions.go b/core/vm/instructions.go
index 2cc8defb84..7b71a06a1d 100644
--- a/core/vm/instructions.go
+++ b/core/vm/instructions.go
@@ -536,12 +536,14 @@ func opJumpdest(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opPc(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.get().SetUint64(*pc)
+ elem := scope.Stack.get()
+ elem.SetUint64(*pc)
return nil, nil
}
func opMsize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.get().SetUint64(uint64(scope.Memory.Len()))
+ elem := scope.Stack.get()
+ elem.SetUint64(uint64(scope.Memory.Len()))
return nil, nil
}
@@ -1131,10 +1133,8 @@ func makeLog(size int) executionFunc {
// opPush1 is a specialized version of pushN
func opPush1(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- var (
- codeLen = uint64(len(scope.Contract.Code))
- elem = scope.Stack.get()
- )
+ codeLen := uint64(len(scope.Contract.Code))
+ elem := scope.Stack.get()
*pc += 1
if *pc < codeLen {
elem.SetUint64(uint64(scope.Contract.Code[*pc]))
@@ -1146,10 +1146,8 @@ func opPush1(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// opPush2 is a specialized version of pushN
func opPush2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- var (
- codeLen = uint64(len(scope.Contract.Code))
- elem = scope.Stack.get()
- )
+ codeLen := uint64(len(scope.Contract.Code))
+ elem := scope.Stack.get()
if *pc+2 < codeLen {
elem.SetBytes2(scope.Contract.Code[*pc+1 : *pc+3])
} else if *pc+1 < codeLen {
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
index 08edb5a04e..94fe339df5 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interp_gen.go
@@ -705,9 +705,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 2
- stack.inner.data[stack.inner.top].SetUint64(pc)
+ elem := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ elem.SetUint64(pc)
pc++
continue mainLoop
@@ -719,9 +720,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 2
- stack.inner.data[stack.inner.top].SetUint64(uint64(scope.Memory.Len()))
+ elem := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ elem.SetUint64(uint64(scope.Memory.Len()))
pc++
continue mainLoop
@@ -742,9 +744,10 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 2
- stack.inner.data[stack.inner.top].Clear()
+ elem := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ elem.Clear()
pc++
continue mainLoop
diff --git a/core/vm/stack.go b/core/vm/stack.go
index 564345ccd8..9b208ff07a 100644
--- a/core/vm/stack.go
+++ b/core/vm/stack.go
@@ -105,10 +105,9 @@ func (s *Stack) push(d *uint256.Int) {
// get returns a pointer to a newly created element
// on top of the stack
func (s *Stack) get() *uint256.Int {
- elem := &s.inner.data[s.inner.top]
s.inner.top++
s.size++
- return elem
+ return &s.inner.data[s.inner.top-1]
}
func (s *Stack) pop() uint256.Int {
From c7a36d75f6f1186f4176c19af10412346f456074 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Fri, 19 Jun 2026 14:38:43 -0500
Subject: [PATCH 12/23] core/vm/gen: drop the dead stack-helper expansion cases
for regex
---
core/vm/gen/main.go | 21 +++------------------
1 file changed, 3 insertions(+), 18 deletions(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index f5fc034ade..6617fb8220 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -267,31 +267,18 @@ func (g *generator) rewriteSplicedBody(src string) string {
// pop1Peek1 family turn into real calls, which a snailtracer profile put at
// over a tenth of the run. The generator splices those helper bodies in
// textually wherever they appear in statement position, verbatim from
-// stack.go. A helper declared inside a var ( ... ) group (the push handlers
-// write elem = scope.Stack.get() there) is first lifted out into := statements,
-// since its replacement is statements rather than an expression.
+// stack.go. The handler sources are kept to that statement form (no var blocks,
+// no method chaining on a helper call) so each helper is one statement-anchored
+// regex.
var (
pop1Peek1Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+) := scope\.Stack\.pop1Peek1\(\)$`)
pop2Peek1Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+), (\w+) := scope\.Stack\.pop2Peek1\(\)$`)
pop2Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+) := scope\.Stack\.pop2\(\)$`)
getAssignRe = regexp.MustCompile(`(?m)^(\s*)(\w+) := scope\.Stack\.get\(\)$`)
- getCallRe = regexp.MustCompile(`(?m)^(\s*)scope\.Stack\.get\(\)\.(\w+)\((.*)\)$`)
dupRe = regexp.MustCompile(`(?m)^(\s*)scope\.Stack\.dup\((\d+)\)$`)
- varBlockRe = regexp.MustCompile(`(?ms)^(\s*)var \(\n(.*?)\n\s*\)$`)
- varMemberRe = regexp.MustCompile(`(?m)^\s*([\w, ]+?)\s*= (.*)$`)
)
func expandStackHelpers(src string) string {
- // Lift any var ( ... ) group holding a stack helper into := statements:
- // a helper expansion is statements, which cannot sit inside a var group.
- src = varBlockRe.ReplaceAllStringFunc(src, func(block string) string {
- m := varBlockRe.FindStringSubmatch(block)
- indent, members := m[1], m[2]
- if !strings.Contains(members, "scope.Stack.") {
- return block
- }
- return varMemberRe.ReplaceAllString(members, indent+"$1 := $2")
- })
src = pop1Peek1Re.ReplaceAllString(src,
"${1}stack.inner.top--\n${1}stack.size--\n${1}$2 := &stack.inner.data[stack.inner.top]\n${1}$3 := &stack.inner.data[stack.inner.top-1]")
src = pop2Peek1Re.ReplaceAllString(src,
@@ -300,8 +287,6 @@ func expandStackHelpers(src string) string {
"${1}stack.inner.top -= 2\n${1}stack.size -= 2\n${1}$2 := &stack.inner.data[stack.inner.top+1]\n${1}$3 := &stack.inner.data[stack.inner.top]")
src = getAssignRe.ReplaceAllString(src,
"${1}$2 := &stack.inner.data[stack.inner.top]\n${1}stack.inner.top++\n${1}stack.size++")
- src = getCallRe.ReplaceAllString(src,
- "${1}stack.inner.data[stack.inner.top].$2($3)\n${1}stack.inner.top++\n${1}stack.size++")
src = dupRe.ReplaceAllString(src,
"${1}stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-$2]\n${1}stack.size++\n${1}stack.inner.top++")
return src
From 72d0907790fa174f33a90548eb97dafedf4e0384 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Fri, 19 Jun 2026 15:25:01 -0500
Subject: [PATCH 13/23] core/vm: replace the stack-helper expansion regexes
with an AST inliner
---
core/vm/gen/main.go | 300 ++++++++++++++++++++++++++++++-------
core/vm/interp_gen.go | 70 ++++-----
core/vm/interp_gen_test.go | 95 +++++++-----
core/vm/stack.go | 9 ++
4 files changed, 345 insertions(+), 129 deletions(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index 6617fb8220..ebb9c12a4b 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -109,10 +109,11 @@ type opMeta struct {
}
type generator struct {
- fset *token.FileSet
- handlers map[string]*ast.FuncDecl
- meta [256]opMeta
- buf *bytes.Buffer
+ fset *token.FileSet
+ handlers map[string]*ast.FuncDecl // opXxx handlers from instructions.go and eips.go
+ stackHelpers map[string]*ast.FuncDecl // (s *Stack) helpers from stack.go, spliced inline
+ meta [256]opMeta
+ buf *bytes.Buffer
}
// p is the sole writer of the generated file. Every line of output is appended
@@ -133,12 +134,14 @@ func (g *generator) p(format string, args ...any) {
// 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"} {
+// parseHandlers parses instructions.go, eips.go and stack.go. It returns the
+// top-level opXxx handlers by name, and separately the *Stack helper methods by
+// name (the inliner splices the latter into the former).
+func parseHandlers(vmDir string) (fset *token.FileSet, handlers, stackHelpers map[string]*ast.FuncDecl) {
+ fset = token.NewFileSet()
+ handlers = map[string]*ast.FuncDecl{}
+ stackHelpers = map[string]*ast.FuncDecl{}
+ for _, name := range []string{"instructions.go", "eips.go", "stack.go"} {
path := filepath.Join(vmDir, name)
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
@@ -146,13 +149,45 @@ func parseHandlers(vmDir string) (*token.FileSet, map[string]*ast.FuncDecl) {
}
for _, decl := range f.Decls {
fn, ok := decl.(*ast.FuncDecl)
- if !ok || fn.Recv != nil || fn.Body == nil {
+ if !ok || fn.Body == nil {
continue
}
- handlers[fn.Name.Name] = fn
+ switch {
+ case fn.Recv == nil: // top-level opXxx handler
+ handlers[fn.Name.Name] = fn
+ case isStackMethod(fn) && hasInlineMarker(fn): // (s *Stack) helper tagged //gen:inline
+ stackHelpers[fn.Name.Name] = fn
+ }
}
}
- return fset, handlers
+ return fset, handlers, stackHelpers
+}
+
+// isStackMethod reports whether fn is a method on *Stack.
+func isStackMethod(fn *ast.FuncDecl) bool {
+ if fn.Recv == nil || len(fn.Recv.List) != 1 {
+ return false
+ }
+ star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
+ if !ok {
+ return false
+ }
+ id, ok := star.X.(*ast.Ident)
+ return ok && id.Name == "Stack"
+}
+
+// hasInlineMarker reports whether fn is tagged //gen:inline, which marks a stack
+// helper for splicing into the generated dispatch.
+func hasInlineMarker(fn *ast.FuncDecl) bool {
+ if fn.Doc == nil {
+ return false
+ }
+ for _, c := range fn.Doc.List {
+ if c.Text == "//gen:inline" {
+ return true
+ }
+ }
+ return false
}
var returnRe = regexp.MustCompile(`^(\s*)return\s+([^,]+),\s*(.+)$`)
@@ -164,7 +199,7 @@ func (g *generator) inlineBody(handler string) string {
if fn == nil {
fatalf("no handler %q to inline", handler)
}
- return g.rewriteSplicedBody(g.renderAst(fn.Body.List))
+ return g.rewriteSplicedBody(g.expandStackHelpers(fn.Body.List, nil))
}
// inlineFactoryBody splices the body of the executionFunc closure that a make*
@@ -179,20 +214,16 @@ func (g *generator) inlineFactoryBody(factory string, args ...int) string {
fatalf("no factory %q to inline", factory)
}
lit := factoryClosure(factory, fn)
- src := g.renderAst(lit.Body.List)
- var names []string
- for _, f := range fn.Type.Params.List {
- for _, nm := range f.Names {
- names = append(names, nm.Name)
- }
- }
+ // Bind the factory parameters to the per-opcode constants, then inline.
+ names := paramNames(fn)
if len(names) != len(args) {
fatalf("factory %q takes %d params, got %d args", factory, len(names), len(args))
}
+ params := map[string]int{}
for i, nm := range names {
- src = regexp.MustCompile(`\b`+nm+`\b`).ReplaceAllString(src, fmt.Sprint(args[i]))
+ params[nm] = args[i]
}
- return g.rewriteSplicedBody(src)
+ return g.rewriteSplicedBody(g.expandStackHelpers(lit.Body.List, params))
}
// factoryClosure returns the executionFunc literal that a make* factory's body
@@ -230,7 +261,8 @@ func (g *generator) renderAst(stmts []ast.Stmt) string {
// rewriteSplicedBody rewrites a printed handler body so it runs inside 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. Stack helpers are then expanded.
+// and continues, an error sets err and breaks. (Stack helpers were already
+// inlined by expandStackHelpers before the body was printed.)
func (g *generator) rewriteSplicedBody(src string) string {
src = strings.ReplaceAll(src, "*pc", "pc")
@@ -258,40 +290,200 @@ func (g *generator) rewriteSplicedBody(src string) string {
}
out.WriteString(line + "\n")
}
- return expandStackHelpers(out.String())
+ return out.String()
}
-// The compiler shrinks its inlining budget into very large functions, and
-// execUntraced is far past the size threshold. The cheap stack helpers
-// (pop1, len, peek, drop, back) still inline there, but get and the
-// pop1Peek1 family turn into real calls, which a snailtracer profile put at
-// over a tenth of the run. The generator splices those helper bodies in
-// textually wherever they appear in statement position, verbatim from
-// stack.go. The handler sources are kept to that statement form (no var blocks,
-// no method chaining on a helper call) so each helper is one statement-anchored
-// regex.
-var (
- pop1Peek1Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+) := scope\.Stack\.pop1Peek1\(\)$`)
- pop2Peek1Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+), (\w+) := scope\.Stack\.pop2Peek1\(\)$`)
- pop2Re = regexp.MustCompile(`(?m)^(\s*)(\w+), (\w+) := scope\.Stack\.pop2\(\)$`)
- getAssignRe = regexp.MustCompile(`(?m)^(\s*)(\w+) := scope\.Stack\.get\(\)$`)
- dupRe = regexp.MustCompile(`(?m)^(\s*)scope\.Stack\.dup\((\d+)\)$`)
-)
+// The helpers spliced inline are tagged //gen:inline in stack.go and collected
+// into g.stackHelpers. They cost more than the compiler will inline into a
+// function the size of execUntraced, where a snailtracer profile put the calls
+// at over a tenth of the run. The cheap helpers (pop1, len, peek, drop, back)
+// inline on their own and stay as calls. Each tagged helper is inlined from its
+// stack.go body (expandStackHelpers), so the inlined code follows the one definition.
-func expandStackHelpers(src string) string {
- src = pop1Peek1Re.ReplaceAllString(src,
- "${1}stack.inner.top--\n${1}stack.size--\n${1}$2 := &stack.inner.data[stack.inner.top]\n${1}$3 := &stack.inner.data[stack.inner.top-1]")
- src = pop2Peek1Re.ReplaceAllString(src,
- "${1}stack.inner.top -= 2\n${1}stack.size -= 2\n${1}$2 := &stack.inner.data[stack.inner.top+1]\n${1}$3 := &stack.inner.data[stack.inner.top]\n${1}$4 := &stack.inner.data[stack.inner.top-1]")
- src = pop2Re.ReplaceAllString(src,
- "${1}stack.inner.top -= 2\n${1}stack.size -= 2\n${1}$2 := &stack.inner.data[stack.inner.top+1]\n${1}$3 := &stack.inner.data[stack.inner.top]")
- src = getAssignRe.ReplaceAllString(src,
- "${1}$2 := &stack.inner.data[stack.inner.top]\n${1}stack.inner.top++\n${1}stack.size++")
- src = dupRe.ReplaceAllString(src,
- "${1}stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-$2]\n${1}stack.size++\n${1}stack.inner.top++")
+// stackCall is a matched call to a tagged helper.
+type stackCall struct {
+ helper string // helper method name
+ lhs []ast.Expr // assignment targets, nil for a void call like dup
+ tok token.Token // the assignment token, := or =
+ args []ast.Expr // call arguments (only dup has one)
+}
+
+// matchStackHelper matches a statement that is a single must-expand helper call,
+// in one of the two normalized forms: an assignment `lhs... := scope.Stack.H(args)`
+// or a bare `scope.Stack.H(args)`.
+func (g *generator) matchStackHelper(stmt ast.Stmt) (stackCall, bool) {
+ switch s := stmt.(type) {
+ case *ast.AssignStmt:
+ if len(s.Rhs) == 1 {
+ if h, args, ok := g.stackHelperCall(s.Rhs[0]); ok {
+ return stackCall{helper: h, lhs: s.Lhs, tok: s.Tok, args: args}, true
+ }
+ }
+ case *ast.ExprStmt:
+ if h, args, ok := g.stackHelperCall(s.X); ok {
+ return stackCall{helper: h, args: args}, true
+ }
+ }
+ return stackCall{}, false
+}
+
+// stackHelperCall unwraps scope.Stack.H(args) where H is a must-expand helper.
+func (g *generator) stackHelperCall(e ast.Expr) (helper string, args []ast.Expr, ok bool) {
+ call, isCall := e.(*ast.CallExpr)
+ if !isCall {
+ return "", nil, false
+ }
+ sel, isSel := call.Fun.(*ast.SelectorExpr) // .H
+ if !isSel || g.stackHelpers[sel.Sel.Name] == nil || !isStackExpr(sel.X) {
+ return "", nil, false
+ }
+ return sel.Sel.Name, call.Args, true
+}
+
+// isStackExpr reports whether e is the stack receiver: the `stack` local or
+// scope.Stack.
+func isStackExpr(e ast.Expr) bool {
+ switch x := e.(type) {
+ case *ast.Ident:
+ return x.Name == "stack"
+ case *ast.SelectorExpr:
+ return x.Sel.Name == "Stack"
+ }
+ return false
+}
+
+// expandStackHelpers renders a handler body to source, inlining every must-expand
+// helper call and printing other statements unchanged. params maps the factory
+// parameters (makePush/makeDup) to their per-opcode constants. Helper calls
+// appear only at statement top-level here; a nested one would be left as a real
+// call and caught by the inlining guard test.
+func (g *generator) expandStackHelpers(stmts []ast.Stmt, params map[string]int) string {
+ var out strings.Builder
+ for _, stmt := range stmts {
+ if call, ok := g.matchStackHelper(stmt); ok {
+ out.WriteString(g.inlineStackHelper(call, params))
+ } else {
+ out.WriteString(substParams(g.renderAst([]ast.Stmt{stmt}), params))
+ }
+ }
+ return out.String()
+}
+
+// substParams replaces each factory parameter with its constant. It runs only
+// on printed non-helper statements and on helper arguments, never on a helper
+// expansion, so it cannot touch a field like stack.size. The parameter names do
+// not textually overlap, so map order does not affect the result.
+func substParams(src string, params map[string]int) string {
+ for name, val := range params {
+ src = regexp.MustCompile(`\b`+name+`\b`).ReplaceAllString(src, fmt.Sprint(val))
+ }
return src
}
+// inlineStackHelper expands one helper call to its stack.go body. The single
+// rule: the helper is straight-line statements then an optional final return
+// whose result count matches the call's targets. Anything else is not in
+// inlinable form and is a hard error (the shape post-condition). The receiver
+// maps to the loop's `stack` local and each parameter to its call argument.
+func (g *generator) inlineStackHelper(call stackCall, params map[string]int) string {
+ fn := g.stackHelpers[call.helper]
+ if fn == nil {
+ fatalf("no stack helper %q to inline", call.helper)
+ }
+ // Peel an optional trailing return off the body.
+ body := fn.Body.List
+ var ret *ast.ReturnStmt
+ if n := len(body); n > 0 {
+ if r, isRet := body[n-1].(*ast.ReturnStmt); isRet {
+ ret, body = r, body[:n-1]
+ }
+ }
+ results := 0
+ if ret != nil {
+ results = len(ret.Results)
+ }
+ if len(call.lhs) != results {
+ fatalf("stack helper %q returns %d values, call assigns %d", call.helper, results, len(call.lhs))
+ }
+ // Map the receiver to the loop local and each parameter to its argument.
+ names := paramNames(fn)
+ if len(names) != len(call.args) {
+ fatalf("stack helper %q takes %d params, call passes %d", call.helper, len(names), len(call.args))
+ }
+ subst := map[string]string{recvName(fn): "stack"}
+ for i, name := range names {
+ subst[name] = substParams(renderInlineExpr(call.args[i], nil), params)
+ }
+ // The leading bookkeeping statements, then bind each return expression to
+ // its assignment target.
+ var out strings.Builder
+ for _, stmt := range body {
+ out.WriteString(renderInlineStmt(stmt, subst) + "\n")
+ }
+ for i, lhs := range call.lhs {
+ out.WriteString(renderInlineExpr(lhs, nil) + " " + call.tok.String() + " " + renderInlineExpr(ret.Results[i], subst) + "\n")
+ }
+ return out.String()
+}
+
+// recvName returns a method's receiver name (e.g. "s").
+func recvName(fn *ast.FuncDecl) string {
+ if names := fn.Recv.List[0].Names; len(names) > 0 {
+ return names[0].Name
+ }
+ return ""
+}
+
+// paramNames returns a function's parameter names, in order.
+func paramNames(fn *ast.FuncDecl) []string {
+ var names []string
+ for _, f := range fn.Type.Params.List {
+ for _, nm := range f.Names {
+ names = append(names, nm.Name)
+ }
+ }
+ return names
+}
+
+// renderInlineStmt prints one helper-body statement with subst applied. Only the
+// statement shapes the helpers use are handled; any other is not inlinable.
+func renderInlineStmt(stmt ast.Stmt, subst map[string]string) string {
+ switch s := stmt.(type) {
+ case *ast.IncDecStmt: // s.inner.top++
+ return renderInlineExpr(s.X, subst) + s.Tok.String()
+ case *ast.AssignStmt: // s.size -= 2 or data[x] = data[y]
+ if len(s.Lhs) == 1 && len(s.Rhs) == 1 {
+ return renderInlineExpr(s.Lhs[0], subst) + " " + s.Tok.String() + " " + renderInlineExpr(s.Rhs[0], subst)
+ }
+ }
+ fatalf("inline: unsupported statement %T in stack helper", stmt)
+ return ""
+}
+
+// renderInlineExpr prints one helper-body expression, substituting any
+// identifier found in subst. Only the shapes the helpers use are handled.
+func renderInlineExpr(expr ast.Expr, subst map[string]string) string {
+ switch e := expr.(type) {
+ case *ast.Ident:
+ if r, ok := subst[e.Name]; ok {
+ return r
+ }
+ return e.Name
+ case *ast.BasicLit:
+ return e.Value
+ case *ast.SelectorExpr: // x.field
+ return renderInlineExpr(e.X, subst) + "." + e.Sel.Name
+ case *ast.IndexExpr: // x[i]
+ return renderInlineExpr(e.X, subst) + "[" + renderInlineExpr(e.Index, subst) + "]"
+ case *ast.BinaryExpr: // x op y
+ return renderInlineExpr(e.X, subst) + " " + e.Op.String() + " " + renderInlineExpr(e.Y, subst)
+ case *ast.UnaryExpr: // &x
+ return e.Op.String() + renderInlineExpr(e.X, subst)
+ }
+ fatalf("inline: unsupported expression %T in stack helper", expr)
+ return ""
+}
+
// ---------------------------------------------------------------------------
// Metadata derivation (from the per-fork tables, via vm.GenForks)
// ---------------------------------------------------------------------------
@@ -632,8 +824,8 @@ func main() {
}
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)}
+ fset, handlers, stackHelpers := parseHandlers(vmDir)
+ g := &generator{fset: fset, handlers: handlers, stackHelpers: stackHelpers, buf: new(bytes.Buffer)}
g.deriveMeta(vm.GenForks())
g.emitFile()
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
index 94fe339df5..0e7e324dec 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interp_gen.go
@@ -705,9 +705,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 2
- elem := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ elem := &stack.inner.data[stack.inner.top-1]
elem.SetUint64(pc)
pc++
continue mainLoop
@@ -720,9 +720,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 2
- elem := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ elem := &stack.inner.data[stack.inner.top-1]
elem.SetUint64(uint64(scope.Memory.Len()))
pc++
continue mainLoop
@@ -744,9 +744,9 @@ mainLoop:
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= 2
- elem := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ elem := &stack.inner.data[stack.inner.top-1]
elem.Clear()
pc++
continue mainLoop
@@ -771,9 +771,9 @@ mainLoop:
continue mainLoop
}
codeLen := uint64(len(scope.Contract.Code))
- elem := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ elem := &stack.inner.data[stack.inner.top-1]
pc += 1
if pc < codeLen {
elem.SetUint64(uint64(scope.Contract.Code[pc]))
@@ -800,9 +800,9 @@ mainLoop:
continue mainLoop
}
codeLen := uint64(len(scope.Contract.Code))
- elem := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ elem := &stack.inner.data[stack.inner.top-1]
if pc+2 < codeLen {
elem.SetBytes2(scope.Contract.Code[pc+1 : pc+3])
} else if pc+1 < codeLen {
@@ -835,9 +835,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+3)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 3 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -867,9 +867,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+4)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 4 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -899,9 +899,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+5)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 5 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -931,9 +931,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+6)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 6 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -963,9 +963,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+7)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 7 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -995,9 +995,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+8)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 8 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1027,9 +1027,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+9)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 9 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1059,9 +1059,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+10)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 10 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1091,9 +1091,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+11)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 11 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1123,9 +1123,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+12)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 12 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1155,9 +1155,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+13)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 13 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1187,9 +1187,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+14)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 14 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1219,9 +1219,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+15)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 15 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1251,9 +1251,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+16)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 16 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1283,9 +1283,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+17)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 17 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1315,9 +1315,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+18)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 18 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1347,9 +1347,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+19)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 19 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1379,9 +1379,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+20)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 20 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1411,9 +1411,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+21)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 21 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1443,9 +1443,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+22)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 22 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1475,9 +1475,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+23)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 23 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1507,9 +1507,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+24)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 24 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1539,9 +1539,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+25)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 25 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1571,9 +1571,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+26)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 26 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1603,9 +1603,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+27)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 27 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1635,9 +1635,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+28)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 28 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1667,9 +1667,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+29)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 29 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1699,9 +1699,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+30)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 30 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1731,9 +1731,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+31)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 31 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
@@ -1763,9 +1763,9 @@ mainLoop:
start = min(codeLen, int(pc+1))
end = min(codeLen, start+32)
)
- a := &stack.inner.data[stack.inner.top]
stack.inner.top++
stack.size++
+ a := &stack.inner.data[stack.inner.top-1]
a.SetBytes(scope.Contract.Code[start:end])
if missing := 32 - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
diff --git a/core/vm/interp_gen_test.go b/core/vm/interp_gen_test.go
index 26807ed84e..119faba910 100644
--- a/core/vm/interp_gen_test.go
+++ b/core/vm/interp_gen_test.go
@@ -403,50 +403,61 @@ func FuzzInterpreterDiff(f *testing.F) {
})
}
-// expandedHelpers are the costly *Stack helpers the generator splices inline
-// (see expandStackHelpers in core/vm/gen) instead of leaving as calls, because
-// they exceed the inline budget for a function as large as execUntraced. After
-// generation none should remain as a call in interp_gen.go. If a change to the
-// handler source or to stack.go alters how one is written, the expansion regex
-// silently stops matching and the helper survives as a real call: correct, but
-// it drops the inlining the fast path exists for. This is the generator's old
-// post-condition, moved here so both halves of the invariant live together.
-var expandedHelpers = []string{"get", "dup", "pop2", "pop1Peek1", "pop2Peek1"}
+// markedHelpers parses stack.go and returns the *Stack helpers tagged
+// //gen:inline. That tag is the single source of truth, shared with the
+// generator (core/vm/gen), for which helpers are spliced into the dispatch.
+func markedHelpers(t *testing.T) map[string]bool {
+ t.Helper()
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, "stack.go", nil, parser.ParseComments)
+ if err != nil {
+ t.Fatalf("parsing stack.go: %v", err)
+ }
+ marked := map[string]bool{}
+ for _, decl := range f.Decls {
+ fn, ok := decl.(*ast.FuncDecl)
+ if !ok || fn.Doc == nil {
+ continue
+ }
+ for _, c := range fn.Doc.List {
+ if c.Text == "//gen:inline" {
+ marked[fn.Name.Name] = true
+ }
+ }
+ }
+ if len(marked) == 0 {
+ t.Fatal("found no //gen:inline helpers in stack.go")
+ }
+ return marked
+}
// TestGeneratedFastPathHelpersExpanded asserts the generator spliced every
-// costly stack helper inline, so none survives as a call in interp_gen.go. It
-// is the expand-side counterpart to TestGeneratedFastPathHelpersInlined: together
-// they hold the one invariant that the fast path makes no real stack-helper
-// call, the costly ones by splicing, the cheap ones by compiler inlining.
+// //gen:inline helper inline, so none survives as a real call in interp_gen.go.
+// Those helpers exceed the compiler's inline budget for a function as large as
+// execUntraced, so a missed splice would silently drop the inlining the fast
+// path exists for. It is the expand-side counterpart to
+// TestGeneratedFastPathHelpersInlined: together they hold the one invariant that
+// the fast path makes no real stack-helper call, the costly ones by splicing,
+// the cheap ones by compiler inlining.
func TestGeneratedFastPathHelpersExpanded(t *testing.T) {
calls := countStackCalls(t, "interp_gen.go")
- for _, h := range expandedHelpers {
+ for h := range markedHelpers(t) {
if n := calls[h]; n != 0 {
- t.Errorf("(*Stack).%s: %d residual call(s) in interp_gen.go, expected 0.\n"+
- "The generator no longer splices it inline, so it leaked through as a real call.\n"+
- "Fix the matching regex in expandStackHelpers (core/vm/gen).", h, n)
+ t.Errorf("(*Stack).%s is //gen:inline but has %d residual call(s) in interp_gen.go, expected 0.\n"+
+ "The generator did not splice it. Check it is still in inlinable shape (core/vm/gen).", h, n)
}
}
}
-// mustInlineHelpers are the cheap *Stack helpers the generated fast path calls
-// directly and trusts the compiler to inline into execUntraced. Unlike get,
-// dup, pop2, pop1Peek1 and pop2Peek1, which are too costly to inline into a
-// function that large and so are spliced in textually by the generator (see
-// expandStackHelpers in core/vm/gen, checked by TestGeneratedFastPathHelpersExpanded),
-// these stay as plain calls. They inline today, most with comfortable margin, but pop1 sits
-// at cost 18 against Go's big-function budget of 20. A toolchain that re-scores
-// inline cost, or an extra branch in one of these bodies, could silently stop
-// the inlining and quietly slow the interpreter. This is the set we refuse to
-// let regress in silence. back is absent because it has no call site here.
-var mustInlineHelpers = []string{"len", "pop1", "peek", "drop"}
-
// TestGeneratedFastPathHelpersInlined recompiles this package with the
-// compiler's inlining diagnostics on and fails if any call to a mustInlineHelper
-// in interp_gen.go was not inlined. It is the inlining-side counterpart to
-// TestGeneratedFastPathHelpersExpanded, which checks the expensive helpers were
-// spliced away. Together they keep both halves of the fast path honest: the
-// costly helpers stay spliced, the cheap ones stay inlined.
+// compiler's inlining diagnostics on and fails if any *Stack helper call that
+// survives into interp_gen.go was not inlined. Every survivor must be a cheap
+// helper (len, pop1, peek, drop) the compiler inlines into execUntraced; the
+// //gen:inline helpers are spliced away and owned by the Expanded test. The
+// cheap ones inline today with margin except pop1, at cost 18 against Go's
+// big-function budget of 20. A toolchain that re-scores inline cost, or an extra
+// branch in one of these bodies, could silently stop the inlining and slow the
+// interpreter, so this turns that into a red build.
func TestGeneratedFastPathHelpersInlined(t *testing.T) {
if testing.Short() {
t.Skip("skipping inlining check (recompiles the package) in -short mode")
@@ -464,18 +475,22 @@ func TestGeneratedFastPathHelpersInlined(t *testing.T) {
t.Fatalf("captured no interp_gen.go diagnostics, the -m build produced nothing to check:\n%s", diag)
}
- calls := countStackCalls(t, "interp_gen.go")
- for _, h := range mustInlineHelpers {
+ // Every surviving stack-helper call (i.e. not a //gen:inline target) must be
+ // inlined by the compiler.
+ marked := markedHelpers(t)
+ for h, n := range countStackCalls(t, "interp_gen.go") {
+ if marked[h] {
+ continue // spliced away, owned by TestGeneratedFastPathHelpersExpanded
+ }
inlinedRe := regexp.MustCompile(`interp_gen\.go.*inlining call to \(\*Stack\)\.` + regexp.QuoteMeta(h) + `\b`)
inlined := len(inlinedRe.FindAllString(diag, -1))
- if calls[h] != inlined {
+ if inlined != n {
t.Errorf("(*Stack).%s: %d call site(s) in interp_gen.go, %d inlined into execUntraced.\n"+
"The compiler stopped inlining it, so the fast path now pays a real call. Shrink the\n"+
- "body to fit the inline budget, or promote it to a spliced helper (add an expansion to\n"+
- "expandStackHelpers in core/vm/gen and move it to expandedHelpers here).", h, calls[h], inlined)
+ "body to fit the inline budget, or tag it //gen:inline in stack.go to splice it instead.", h, n, inlined)
continue
}
- t.Logf("(*Stack).%s: %d/%d call sites inlined", h, inlined, calls[h])
+ t.Logf("(*Stack).%s: %d/%d call sites inlined", h, inlined, n)
}
}
diff --git a/core/vm/stack.go b/core/vm/stack.go
index 9b208ff07a..342526c397 100644
--- a/core/vm/stack.go
+++ b/core/vm/stack.go
@@ -104,6 +104,8 @@ func (s *Stack) push(d *uint256.Int) {
// get returns a pointer to a newly created element
// on top of the stack
+//
+//gen:inline
func (s *Stack) get() *uint256.Int {
s.inner.top++
s.size++
@@ -136,6 +138,8 @@ func (s *Stack) pop1() *uint256.Int {
// pop2 removes the top two elements and returns pointers to them. The
// pointers stay valid only until the next push or sub call.
+//
+//gen:inline
func (s *Stack) pop2() (top, second *uint256.Int) {
s.inner.top -= 2
s.size -= 2
@@ -161,6 +165,8 @@ func (s *Stack) pop4() (top, second, third, fourth *uint256.Int) {
// pop1Peek1 removes the top element and returns pointers to it and to the new
// top, the usual operand and write target of a binary operation. The first
// pointer stays valid only until the next push or sub call.
+//
+//gen:inline
func (s *Stack) pop1Peek1() (top, rest *uint256.Int) {
s.inner.top--
s.size--
@@ -170,6 +176,8 @@ func (s *Stack) pop1Peek1() (top, rest *uint256.Int) {
// pop2Peek1 removes the top two elements and returns pointers to them and to
// the new top, for three operand operations. The first two pointers stay
// valid only until the next push or sub call.
+//
+//gen:inline
func (s *Stack) pop2Peek1() (top, second, rest *uint256.Int) {
s.inner.top -= 2
s.size -= 2
@@ -225,6 +233,7 @@ func (s *Stack) swap16() {
s.inner.data[s.bottom+s.size-17], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-17]
}
+//gen:inline
func (s *Stack) dup(n int) {
s.inner.data[s.bottom+s.size] = s.inner.data[s.bottom+s.size-n]
s.size++
From 7e2d97ce5f5ce72249d0d253fde7dd5c0f4e68f3 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Sun, 21 Jun 2026 22:36:25 -0400
Subject: [PATCH 14/23] core/vm, core/vm/gen: verify directCold opcodes are
fork-stable
---
core/vm/gen/main.go | 43 ++++++++++++++++++++++++++++-
core/vm/genspec.go | 67 +++++++++++++++++++++++++++++++--------------
2 files changed, 88 insertions(+), 22 deletions(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index ebb9c12a4b..c39fc75d58 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -518,6 +518,12 @@ func (g *generator) deriveMeta(forks []vm.GenFork) {
for code := 0x80; code <= 0x8f; code++ { // DUP1-16
g.checkStable(byte(code), "makeDup", forks)
}
+ // directCold opcodes bake their static gas and stack bounds the same way, so
+ // they must be fork-stable too. Dynamic gas is allowed (it is charged through
+ // the named gas function, not baked).
+ for code := range directCold {
+ g.checkColdStable(code, forks)
+ }
}
func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
@@ -530,12 +536,47 @@ func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
if !o.Defined {
continue
}
- if o.ConstantGas != m.constGas || o.MinStack != m.minStack || o.MaxStack != m.maxStack || o.HasDynamicGas {
+ if o.ConstantGas != m.constGas || o.MinStack != m.minStack || o.MaxStack != m.maxStack || o.DynamicGasFn != "" {
fatalf("opcode %#x (%s) is not fork-stable (fork %s): cannot inline", code, what, fork.Name)
}
}
}
+// checkColdStable verifies a directCold opcode is safe to direct-call. Its static
+// gas and stack bounds must be the same across every fork it appears in (they are
+// baked as constants), and its handler, gas and memory functions must be the same
+// across those forks too (they are called by name, so a fork that swapped one
+// would otherwise be missed). Unlike checkStable it allows dynamic gas, which
+// directCold ops carry by definition. It does not check the directCold map's names
+// against the table, which the differential test covers.
+func (g *generator) checkColdStable(code byte, forks []vm.GenFork) {
+ m := g.meta[code]
+ if !m.defined {
+ fatalf("opcode %#x (directCold) is never defined", code)
+ }
+ var exec, dyn, mem string
+ seen := false
+ 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 {
+ fatalf("opcode %#x (%s) is in directCold but not fork-stable (fork %s): static gas or stack bounds vary, cannot bake", code, m.name, fork.Name)
+ }
+ // Handler, gas and memory functions must match across forks too, or
+ // direct-calling them by name would skip a fork that swapped one. Names
+ // come from FuncForPC via vm.GenForks (aliases resolve to the underlying
+ // func, still stable across forks).
+ if !seen {
+ exec, dyn, mem, seen = o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, true
+ } else if o.ExecuteFn != exec || o.DynamicGasFn != dyn || o.MemorySizeFn != mem {
+ fatalf("opcode %#x (%s) is in directCold but its functions vary by fork (fork %s): got %s/%s/%s, want %s/%s/%s, cannot direct-call",
+ code, m.name, fork.Name, o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, exec, dyn, mem)
+ }
+ }
+}
+
// ---------------------------------------------------------------------------
// Case emission
// ---------------------------------------------------------------------------
diff --git a/core/vm/genspec.go b/core/vm/genspec.go
index c1e64cdc2a..21fb1e72e5 100644
--- a/core/vm/genspec.go
+++ b/core/vm/genspec.go
@@ -18,27 +18,35 @@ package vm
//go:generate go run ./gen
+import (
+ "reflect"
+ "runtime"
+ "strings"
+)
+
// 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.
+// appears in, and the FuncForPC names of its handler/gas/memory functions) 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.
+// The function names let the generator confirm the directCold ops are
+// fork-invariant. The fork-varying gas/execute functions themselves are still
+// reached through the active per-fork JumpTable at runtime (see interp_gen.go),
+// not emitted by name: several are closures (gasCall, the memoryCopierGas
+// family, makeGasLog) that FuncForPC reports only as anonymous labels, so they
+// could not be called by name in any case.
// 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
+ 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
+ ExecuteFn string // FuncForPC name of op.execute
+ DynamicGasFn string // FuncForPC name of op.dynamicGas, "" if nil
+ MemorySizeFn string // FuncForPC name of op.memorySize, "" if nil
}
// GenFork bundles a fork's name, the params.Rules bool field that activates it
@@ -79,6 +87,22 @@ var genForkOrder = []struct {
{"Amsterdam", "IsAmsterdam", &amsterdamInstructionSet},
}
+// genFnName returns the short FuncForPC name of a jump-table function value
+// (e.g. "gasKeccak256"), or "" if nil. An aliased var resolves to the underlying
+// function (gasMLoad reports "pureMemoryGascost"), which is still stable across
+// forks and so serves the directCold fork-invariance check.
+func genFnName(fn any) string {
+ v := reflect.ValueOf(fn)
+ if !v.IsValid() || v.IsNil() {
+ return ""
+ }
+ full := runtime.FuncForPC(v.Pointer()).Name()
+ if i := strings.LastIndex(full, "."); i >= 0 {
+ return full[i+1:]
+ }
+ return full
+}
+
// GenForks returns per-fork opcode metadata for the interpreter code generator
// (core/vm/gen). It is exported solely for that purpose.
func GenForks() []GenFork {
@@ -91,13 +115,14 @@ func GenForks() []GenFork {
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,
+ Name: OpCode(code).String(),
+ Defined: true,
+ ConstantGas: op.constantGas,
+ MinStack: op.minStack,
+ MaxStack: op.maxStack,
+ ExecuteFn: genFnName(op.execute),
+ DynamicGasFn: genFnName(op.dynamicGas),
+ MemorySizeFn: genFnName(op.memorySize),
}
}
out[i] = gf
From 5d1ff24f5478120d43e93fc592497fe6dd3d33e1 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Fri, 26 Jun 2026 09:59:14 -0500
Subject: [PATCH 15/23] core/vm, core/vm/gen: generator code cleanup and
elimination of handcoded bodies for SWAP
---
core/vm/gen/main.go | 250 ++++++++++++++++++++----------------------
core/vm/interp_gen.go | 18 ++-
core/vm/stack.go | 31 ++++++
3 files changed, 169 insertions(+), 130 deletions(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index c39fc75d58..ddc9223387 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -23,8 +23,8 @@
// 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
+// - calls the fork-invariant ops (KECCAK256 / MLOAD / MSTORE / MSTORE8,
+// see directCall) directly by name, skipping the table's function
// pointers, which Go cannot inline through.
//
// - dispatches everything fork-varying (CALL / CREATE / SSTORE / SLOAD / LOG /
@@ -57,32 +57,43 @@ import (
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",
-}
+// inlineHandler maps an opcode byte to the handler whose body is spliced inline
+// for that opcode. These are the hot, fork-stable opcodes with no dynamic gas.
+// The value is usually an opXxx handler, but PUSH3-PUSH32 and DUP1-DUP16 are
+// factory-built (one shared makePush / makeDup each), so their value is the
+// factory name and emitOpBody splices the factory body with the per-opcode size.
+// Opcodes not listed here (or in directCall) fall through to the default case,
+// which dispatches via the per-fork table.
+var inlineHandler = func() map[byte]string {
+ m := 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",
+ }
+ for code := 0x62; code <= 0x7f; code++ { // PUSH3-PUSH32
+ m[byte(code)] = "makePush"
+ }
+ for code := 0x80; code <= 0x8f; code++ { // DUP1-DUP16
+ m[byte(code)] = "makeDup"
+ }
+ return m
+}()
-// directCold lists cold opcodes (dynamic gas, not inlined) whose handler,
+// directCall lists the 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
+// table load. Measured at ~3.4% on snailtracer (p=0.000). Fork-varying ops
// (CALL/SSTORE/SLOAD and friends) do not qualify and stay on the per-fork
// table in the default case.
//
@@ -91,15 +102,15 @@ var inlineHandler = map[byte]string{
// 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{
+var directCall = 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 {
+// opSpec holds the per-opcode constants the generator bakes (gas, stack bounds, intro fork), derived from the per-fork tables.
+type opSpec struct {
defined bool
name string // opcode mnemonic, e.g. "ADD"
introF string // params.Rules field activating it, empty for Frontier (always on)
@@ -112,19 +123,12 @@ type generator struct {
fset *token.FileSet
handlers map[string]*ast.FuncDecl // opXxx handlers from instructions.go and eips.go
stackHelpers map[string]*ast.FuncDecl // (s *Stack) helpers from stack.go, spliced inline
- meta [256]opMeta
+ specs [256]opSpec
buf *bytes.Buffer
}
-// p is the sole writer of the generated file. Every line of output is appended
-// to g.buf through it. The splice helpers (inlineBody, inlineFactoryBody) only
-// build text, which their callers then hand to p.
-//
-// p formats and appends generated Go, like fmt.Fprintf. A template can start on
-// the line after p( and be indented to match its structure. p drops the leading
-// newline and the indent before the closing backtick, and gofmt tidies the rest.
-// Double any percent meant for the generated code (%%w, %%v) so Fprintf emits it
-// literally.
+// p is the writer of the generated file. Every line of output is appended
+// to g.buf through it.
func (g *generator) p(format string, args ...any) {
format = strings.TrimRight(strings.TrimPrefix(format, "\n"), " \t")
fmt.Fprintf(g.buf, format, args...)
@@ -199,7 +203,7 @@ func (g *generator) inlineBody(handler string) string {
if fn == nil {
fatalf("no handler %q to inline", handler)
}
- return g.rewriteSplicedBody(g.expandStackHelpers(fn.Body.List, nil))
+ return g.rewriteSplicedBody(g.inlineStackHelpers(fn.Body.List, nil))
}
// inlineFactoryBody splices the body of the executionFunc closure that a make*
@@ -223,7 +227,7 @@ func (g *generator) inlineFactoryBody(factory string, args ...int) string {
for i, nm := range names {
params[nm] = args[i]
}
- return g.rewriteSplicedBody(g.expandStackHelpers(lit.Body.List, params))
+ return g.rewriteSplicedBody(g.inlineStackHelpers(lit.Body.List, params))
}
// factoryClosure returns the executionFunc literal that a make* factory's body
@@ -262,7 +266,7 @@ func (g *generator) renderAst(stmts []ast.Stmt) string {
// 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. (Stack helpers were already
-// inlined by expandStackHelpers before the body was printed.)
+// inlined by inlineStackHelpers before the body was printed.)
func (g *generator) rewriteSplicedBody(src string) string {
src = strings.ReplaceAll(src, "*pc", "pc")
@@ -293,13 +297,6 @@ func (g *generator) rewriteSplicedBody(src string) string {
return out.String()
}
-// The helpers spliced inline are tagged //gen:inline in stack.go and collected
-// into g.stackHelpers. They cost more than the compiler will inline into a
-// function the size of execUntraced, where a snailtracer profile put the calls
-// at over a tenth of the run. The cheap helpers (pop1, len, peek, drop, back)
-// inline on their own and stay as calls. Each tagged helper is inlined from its
-// stack.go body (expandStackHelpers), so the inlined code follows the one definition.
-
// stackCall is a matched call to a tagged helper.
type stackCall struct {
helper string // helper method name
@@ -352,17 +349,22 @@ func isStackExpr(e ast.Expr) bool {
return false
}
-// expandStackHelpers renders a handler body to source, inlining every must-expand
+// inlineStackHelpers renders a handler body to source, inlining every must-expand
// helper call and printing other statements unchanged. params maps the factory
-// parameters (makePush/makeDup) to their per-opcode constants. Helper calls
-// appear only at statement top-level here; a nested one would be left as a real
-// call and caught by the inlining guard test.
-func (g *generator) expandStackHelpers(stmts []ast.Stmt, params map[string]int) string {
+// parameters (makePush/makeDup) to their per-opcode constants.
+func (g *generator) inlineStackHelpers(stmts []ast.Stmt, params map[string]int) string {
var out strings.Builder
+ // Walk the handler body one statement at a time. A statement that is a
+ // tagged stack-helper call gets the helper's body spliced in: the generated
+ // dispatch is past Go's big-function inline budget, so the call would not be
+ // inlined otherwise. Every other statement is printed as written.
for _, stmt := range stmts {
if call, ok := g.matchStackHelper(stmt); ok {
+ // e.g. `x, y := scope.Stack.pop1Peek1()` becomes the body of pop1Peek1.
out.WriteString(g.inlineStackHelper(call, params))
} else {
+ // A plain statement: print it verbatim, then fill in any makePush or
+ // makeDup factory params with this opcode's constants.
out.WriteString(substParams(g.renderAst([]ast.Stmt{stmt}), params))
}
}
@@ -451,9 +453,15 @@ func renderInlineStmt(stmt ast.Stmt, subst map[string]string) string {
switch s := stmt.(type) {
case *ast.IncDecStmt: // s.inner.top++
return renderInlineExpr(s.X, subst) + s.Tok.String()
- case *ast.AssignStmt: // s.size -= 2 or data[x] = data[y]
- if len(s.Lhs) == 1 && len(s.Rhs) == 1 {
- return renderInlineExpr(s.Lhs[0], subst) + " " + s.Tok.String() + " " + renderInlineExpr(s.Rhs[0], subst)
+ case *ast.AssignStmt: // s.size -= 2, data[x] = data[y], or the swap tuple a, b = b, a
+ if len(s.Lhs) == len(s.Rhs) && len(s.Lhs) >= 1 {
+ lhs := make([]string, len(s.Lhs))
+ rhs := make([]string, len(s.Rhs))
+ for i := range s.Lhs {
+ lhs[i] = renderInlineExpr(s.Lhs[i], subst)
+ rhs[i] = renderInlineExpr(s.Rhs[i], subst)
+ }
+ return strings.Join(lhs, ", ") + " " + s.Tok.String() + " " + strings.Join(rhs, ", ")
}
}
fatalf("inline: unsupported statement %T in stack helper", stmt)
@@ -485,17 +493,17 @@ func renderInlineExpr(expr ast.Expr, subst map[string]string) string {
}
// ---------------------------------------------------------------------------
-// Metadata derivation (from the per-fork tables, via vm.GenForks)
+// Spec derivation (from the per-fork tables, via vm.GenForks)
// ---------------------------------------------------------------------------
-func (g *generator) deriveMeta(forks []vm.GenFork) {
+func (g *generator) deriveSpecs(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{
+ g.specs[code] = opSpec{
defined: true,
name: o.Name,
introF: fork.RuleField,
@@ -512,23 +520,17 @@ func (g *generator) deriveMeta(forks []vm.GenFork) {
for code, handler := range inlineHandler {
g.checkStable(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)
- }
- // directCold opcodes bake their static gas and stack bounds the same way, so
+ // directCall opcodes bake their static gas and stack bounds the same way, so
// they must be fork-stable too. Dynamic gas is allowed (it is charged through
// the named gas function, not baked).
- for code := range directCold {
- g.checkColdStable(code, forks)
+ for code := range directCall {
+ g.checkDirectCallStable(code, forks)
}
}
func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
- m := g.meta[code]
- if !m.defined {
+ spec := g.specs[code]
+ if !spec.defined {
fatalf("opcode %#x (%s) selected for inlining but never defined", code, what)
}
for _, fork := range forks {
@@ -536,23 +538,23 @@ func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
if !o.Defined {
continue
}
- if o.ConstantGas != m.constGas || o.MinStack != m.minStack || o.MaxStack != m.maxStack || o.DynamicGasFn != "" {
+ if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack || o.DynamicGasFn != "" {
fatalf("opcode %#x (%s) is not fork-stable (fork %s): cannot inline", code, what, fork.Name)
}
}
}
-// checkColdStable verifies a directCold opcode is safe to direct-call. Its static
+// checkDirectCallStable verifies a directCall opcode is safe to direct-call. Its static
// gas and stack bounds must be the same across every fork it appears in (they are
// baked as constants), and its handler, gas and memory functions must be the same
// across those forks too (they are called by name, so a fork that swapped one
// would otherwise be missed). Unlike checkStable it allows dynamic gas, which
-// directCold ops carry by definition. It does not check the directCold map's names
+// directCall ops carry by definition. It does not check the directCall map's names
// against the table, which the differential test covers.
-func (g *generator) checkColdStable(code byte, forks []vm.GenFork) {
- m := g.meta[code]
- if !m.defined {
- fatalf("opcode %#x (directCold) is never defined", code)
+func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
+ spec := g.specs[code]
+ if !spec.defined {
+ fatalf("opcode %#x (directCall) is never defined", code)
}
var exec, dyn, mem string
seen := false
@@ -561,8 +563,8 @@ func (g *generator) checkColdStable(code byte, forks []vm.GenFork) {
if !o.Defined {
continue
}
- if o.ConstantGas != m.constGas || o.MinStack != m.minStack || o.MaxStack != m.maxStack {
- fatalf("opcode %#x (%s) is in directCold but not fork-stable (fork %s): static gas or stack bounds vary, cannot bake", code, m.name, fork.Name)
+ if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack {
+ fatalf("opcode %#x (%s) is in directCall but not fork-stable (fork %s): static gas or stack bounds vary, cannot bake", code, spec.name, fork.Name)
}
// Handler, gas and memory functions must match across forks too, or
// direct-calling them by name would skip a fork that swapped one. Names
@@ -571,8 +573,8 @@ func (g *generator) checkColdStable(code byte, forks []vm.GenFork) {
if !seen {
exec, dyn, mem, seen = o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, true
} else if o.ExecuteFn != exec || o.DynamicGasFn != dyn || o.MemorySizeFn != mem {
- fatalf("opcode %#x (%s) is in directCold but its functions vary by fork (fork %s): got %s/%s/%s, want %s/%s/%s, cannot direct-call",
- code, m.name, fork.Name, o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, exec, dyn, mem)
+ fatalf("opcode %#x (%s) is in directCall but its functions vary by fork (fork %s): got %s/%s/%s, want %s/%s/%s, cannot direct-call",
+ code, spec.name, fork.Name, o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, exec, dyn, mem)
}
}
}
@@ -583,9 +585,9 @@ func (g *generator) checkColdStable(code byte, forks []vm.GenFork) {
// 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
+func (g *generator) emitStackChecks(spec opSpec) {
+ under := spec.minStack > 0
+ over := spec.maxStack < stackLimit
switch {
case under && over:
g.p(`
@@ -594,24 +596,24 @@ func (g *generator) emitStackChecks(m opMeta) {
} else if sLen > %d {
return nil, &ErrStackOverflow{stackLen: sLen, limit: %d}
}
- `, m.minStack, m.minStack, m.maxStack, m.maxStack)
+ `, spec.minStack, spec.minStack, spec.maxStack, spec.maxStack)
case under:
g.p(`
if sLen := stack.len(); sLen < %d {
return nil, &ErrStackUnderflow{stackLen: sLen, required: %d}
}
- `, m.minStack, m.minStack)
+ `, spec.minStack, spec.minStack)
case over:
g.p(`
if sLen := stack.len(); sLen > %d {
return nil, &ErrStackOverflow{stackLen: sLen, limit: %d}
}
- `, m.maxStack, m.maxStack)
+ `, spec.maxStack, spec.maxStack)
}
}
-func (g *generator) emitGasCheck(m opMeta) {
- if m.constGas == 0 {
+func (g *generator) emitGasCheck(spec opSpec) {
+ if spec.constGas == 0 {
return
}
g.p(`
@@ -619,15 +621,15 @@ func (g *generator) emitGasCheck(m opMeta) {
return nil, ErrOutOfGas
}
contract.Gas.RegularGas -= %d
- `, m.constGas, m.constGas)
+ `, spec.constGas, spec.constGas)
}
-// emitWork emits the stack/gas guards and the opcode body (the portion that runs
+// emitOpBody 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)
+func (g *generator) emitOpBody(code byte) {
+ spec := g.specs[code]
+ g.emitStackChecks(spec)
+ g.emitGasCheck(spec)
// 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.
@@ -645,35 +647,28 @@ func (g *generator) emitWork(code byte) {
`)
}
- switch {
- case code >= 0x62 && code <= 0x7f: // PUSH3-PUSH32: splice makePush(n, n)
+ switch h := inlineHandler[code]; h {
+ case "makePush": // PUSH3-PUSH32: splice makePush(size, size)
n := int(code) - 0x5f
g.p("%s", g.inlineFactoryBody("makePush", n, n))
- case code >= 0x80 && code <= 0x8f: // DUP1-DUP16: splice makeDup(n)
+ case "makeDup": // DUP1-DUP16: splice makeDup(n)
g.p("%s", g.inlineFactoryBody("makeDup", int(code)-0x7f))
- case code >= 0x90 && code <= 0x9f: // SWAP1-SWAP16: the swap body, emitted raw
- n := int(code) - 0x8f
- g.p(`
- stack.inner.data[stack.bottom+stack.size-%d], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-%d]
- pc++
- continue mainLoop
- `, n+1, n+1)
- default:
- g.p("%s", g.inlineBody(inlineHandler[code]))
+ default: // the rest: splice the opXxx handler body
+ g.p("%s", g.inlineBody(h))
}
}
-func (g *generator) emitInlineCase(code byte) {
- m := g.meta[code]
- g.p("case %s:\n", m.name)
- if m.introF == "" {
- g.emitWork(code)
+func (g *generator) emitInlineOp(code byte) {
+ spec := g.specs[code]
+ g.p("case %s:\n", spec.name)
+ if spec.introF == "" {
+ g.emitOpBody(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("if rules.%s {\n", spec.introF)
+ g.emitOpBody(code)
g.p("}\n")
g.p(`
res, err = opUndefined(&pc, evm, scope)
@@ -681,16 +676,16 @@ func (g *generator) emitInlineCase(code byte) {
`)
}
-// emitDirectCold emits a cold opcode case identical to the default case, except
+// emitDirectCallOp emits an 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)
+// fork-invariant ops (see directCall).
+func (g *generator) emitDirectCallOp(code byte) {
+ spec := g.specs[code]
+ fns := directCall[code]
+ g.p("case %s:\n", spec.name)
+ g.emitStackChecks(spec)
+ g.emitGasCheck(spec)
// The three %s are the memory-size, dynamic-gas and handler names, in the
// order fns[2], fns[1], fns[0]. The doubled %%w and %%v are not generator
// verbs: Fprintf collapses each %% to a single %, so the generated code ends
@@ -799,7 +794,7 @@ func (g *generator) emitFile() {
g.p(`
// 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
+ // in. Fork-invariant 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.
@@ -831,16 +826,13 @@ func (g *generator) emitFile() {
op := contract.GetOp(pc)
switch op {
`)
- // Inlined hot cases, in opcode order for readability.
+ // Inlined 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)
+ if _, named := inlineHandler[b]; named {
+ g.emitInlineOp(b)
+ } else if _, dc := directCall[b]; dc {
+ g.emitDirectCallOp(b)
}
}
g.emitDefault()
@@ -867,7 +859,7 @@ func main() {
fset, handlers, stackHelpers := parseHandlers(vmDir)
g := &generator{fset: fset, handlers: handlers, stackHelpers: stackHelpers, buf: new(bytes.Buffer)}
- g.deriveMeta(vm.GenForks())
+ g.deriveSpecs(vm.GenForks())
g.emitFile()
formatted, err := format.Source(g.buf.Bytes())
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
index 0e7e324dec..2161ad67bc 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interp_gen.go
@@ -11,7 +11,7 @@ import (
// 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
+// in. Fork-invariant 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.
@@ -2041,6 +2041,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-2], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-2]
pc++
continue mainLoop
+
case SWAP2:
if sLen := stack.len(); sLen < 3 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 3}
@@ -2052,6 +2053,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-3], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-3]
pc++
continue mainLoop
+
case SWAP3:
if sLen := stack.len(); sLen < 4 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 4}
@@ -2063,6 +2065,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-4], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-4]
pc++
continue mainLoop
+
case SWAP4:
if sLen := stack.len(); sLen < 5 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 5}
@@ -2074,6 +2077,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-5], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-5]
pc++
continue mainLoop
+
case SWAP5:
if sLen := stack.len(); sLen < 6 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 6}
@@ -2085,6 +2089,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-6], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-6]
pc++
continue mainLoop
+
case SWAP6:
if sLen := stack.len(); sLen < 7 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 7}
@@ -2096,6 +2101,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-7], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-7]
pc++
continue mainLoop
+
case SWAP7:
if sLen := stack.len(); sLen < 8 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 8}
@@ -2107,6 +2113,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-8], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-8]
pc++
continue mainLoop
+
case SWAP8:
if sLen := stack.len(); sLen < 9 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 9}
@@ -2118,6 +2125,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-9], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-9]
pc++
continue mainLoop
+
case SWAP9:
if sLen := stack.len(); sLen < 10 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 10}
@@ -2129,6 +2137,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-10], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-10]
pc++
continue mainLoop
+
case SWAP10:
if sLen := stack.len(); sLen < 11 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 11}
@@ -2140,6 +2149,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-11], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-11]
pc++
continue mainLoop
+
case SWAP11:
if sLen := stack.len(); sLen < 12 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 12}
@@ -2151,6 +2161,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-12], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-12]
pc++
continue mainLoop
+
case SWAP12:
if sLen := stack.len(); sLen < 13 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 13}
@@ -2162,6 +2173,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-13], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-13]
pc++
continue mainLoop
+
case SWAP13:
if sLen := stack.len(); sLen < 14 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 14}
@@ -2173,6 +2185,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-14], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-14]
pc++
continue mainLoop
+
case SWAP14:
if sLen := stack.len(); sLen < 15 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 15}
@@ -2184,6 +2197,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-15], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-15]
pc++
continue mainLoop
+
case SWAP15:
if sLen := stack.len(); sLen < 16 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 16}
@@ -2195,6 +2209,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-16], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-16]
pc++
continue mainLoop
+
case SWAP16:
if sLen := stack.len(); sLen < 17 {
return nil, &ErrStackUnderflow{stackLen: sLen, required: 17}
@@ -2206,6 +2221,7 @@ mainLoop:
stack.inner.data[stack.bottom+stack.size-17], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-17]
pc++
continue mainLoop
+
default:
operation := table[op]
if sLen := stack.len(); sLen < operation.minStack {
diff --git a/core/vm/stack.go b/core/vm/stack.go
index 342526c397..f76aa9713f 100644
--- a/core/vm/stack.go
+++ b/core/vm/stack.go
@@ -184,51 +184,82 @@ func (s *Stack) pop2Peek1() (top, second, rest *uint256.Int) {
return &s.inner.data[s.inner.top+1], &s.inner.data[s.inner.top], &s.inner.data[s.inner.top-1]
}
+//gen:inline
func (s *Stack) swap1() {
s.inner.data[s.bottom+s.size-2], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-2]
}
+
+//gen:inline
func (s *Stack) swap2() {
s.inner.data[s.bottom+s.size-3], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-3]
}
+
+//gen:inline
func (s *Stack) swap3() {
s.inner.data[s.bottom+s.size-4], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-4]
}
+
+//gen:inline
func (s *Stack) swap4() {
s.inner.data[s.bottom+s.size-5], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-5]
}
+
+//gen:inline
func (s *Stack) swap5() {
s.inner.data[s.bottom+s.size-6], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-6]
}
+
+//gen:inline
func (s *Stack) swap6() {
s.inner.data[s.bottom+s.size-7], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-7]
}
+
+//gen:inline
func (s *Stack) swap7() {
s.inner.data[s.bottom+s.size-8], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-8]
}
+
+//gen:inline
func (s *Stack) swap8() {
s.inner.data[s.bottom+s.size-9], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-9]
}
+
+//gen:inline
func (s *Stack) swap9() {
s.inner.data[s.bottom+s.size-10], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-10]
}
+
+//gen:inline
func (s *Stack) swap10() {
s.inner.data[s.bottom+s.size-11], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-11]
}
+
+//gen:inline
func (s *Stack) swap11() {
s.inner.data[s.bottom+s.size-12], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-12]
}
+
+//gen:inline
func (s *Stack) swap12() {
s.inner.data[s.bottom+s.size-13], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-13]
}
+
+//gen:inline
func (s *Stack) swap13() {
s.inner.data[s.bottom+s.size-14], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-14]
}
+
+//gen:inline
func (s *Stack) swap14() {
s.inner.data[s.bottom+s.size-15], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-15]
}
+
+//gen:inline
func (s *Stack) swap15() {
s.inner.data[s.bottom+s.size-16], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-16]
}
+
+//gen:inline
func (s *Stack) swap16() {
s.inner.data[s.bottom+s.size-17], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-17]
}
From 2e0035a63a28209da19a0edca79b1d99cc560138 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Mon, 29 Jun 2026 16:49:57 -0500
Subject: [PATCH 16/23] core/vm, core/vm/gen: fix generated EIP-8037 state-gas
charging
---
core/vm/gascosts.go | 9 +-
core/vm/gen/main.go | 181 ++++++++-----
core/vm/interp_gen.go | 540 +++++++++++++++++++++++++++++--------
core/vm/interp_gen_test.go | 12 +
core/vm/interpreter.go | 8 +-
5 files changed, 564 insertions(+), 186 deletions(-)
diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go
index 220e7d650f..b73732daa3 100644
--- a/core/vm/gascosts.go
+++ b/core/vm/gascosts.go
@@ -84,15 +84,14 @@ func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) {
return prior, ok
}
-// ChargeRegularOnly deducts a regular-only cost. It's always preferred for
-// performance consideration if the opcode doesn't have any state cost.
-func (g *GasBudget) ChargeRegularOnly(r uint64) bool {
+// chargeRegularOnly deducts a regular-only cost.
+func (g *GasBudget) chargeRegularOnly(r uint64) error {
if g.RegularGas < r {
- return false
+ return ErrOutOfGas
}
g.RegularGas -= r
g.UsedRegularGas += r
- return true
+ return nil
}
// CanAfford reports whether the running budget can cover the given cost vector
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index ddc9223387..0c8f805e17 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -24,7 +24,7 @@
// derived from the per-fork instruction tables via vm.GenForks.
//
// - calls the fork-invariant ops (KECCAK256 / MLOAD / MSTORE / MSTORE8,
-// see directCall) directly by name, skipping the table's function
+// see directCallOps) directly by name, skipping the table's function
// pointers, which Go cannot inline through.
//
// - dispatches everything fork-varying (CALL / CREATE / SSTORE / SLOAD / LOG /
@@ -57,14 +57,14 @@ import (
const stackLimit = 1024 // params.StackLimit
-// inlineHandler maps an opcode byte to the handler whose body is spliced inline
+// inlineOps maps an opcode byte to the handler whose body is spliced inline
// for that opcode. These are the hot, fork-stable opcodes with no dynamic gas.
// The value is usually an opXxx handler, but PUSH3-PUSH32 and DUP1-DUP16 are
// factory-built (one shared makePush / makeDup each), so their value is the
// factory name and emitOpBody splices the factory body with the per-opcode size.
-// Opcodes not listed here (or in directCall) fall through to the default case,
+// Opcodes not listed here (or in directCallOps) fall through to the default case,
// which dispatches via the per-fork table.
-var inlineHandler = func() map[byte]string {
+var inlineOps = func() map[byte]string {
m := map[byte]string{
0x01: "opAdd", 0x02: "opMul", 0x03: "opSub", 0x04: "opDiv", 0x05: "opSdiv",
0x06: "opMod", 0x07: "opSmod", 0x08: "opAddmod", 0x09: "opMulmod", 0x0b: "opSignExtend",
@@ -87,7 +87,7 @@ var inlineHandler = func() map[byte]string {
return m
}()
-// directCall lists the opcodes (dynamic gas, not inlined) whose handler,
+// directCallOps lists the 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
@@ -102,7 +102,7 @@ var inlineHandler = func() map[byte]string {
// 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 directCall = map[byte][3]string{
+var directCallOps = map[byte][3]string{
0x20: {"opKeccak256", "gasKeccak256", "memoryKeccak256"}, // KECCAK256
0x51: {"opMload", "gasMLoad", "memoryMLoad"}, // MLOAD
0x52: {"opMstore", "gasMStore", "memoryMStore"}, // MSTORE
@@ -113,7 +113,7 @@ var directCall = map[byte][3]string{
type opSpec struct {
defined bool
name string // opcode mnemonic, e.g. "ADD"
- introF string // params.Rules field activating it, empty for Frontier (always on)
+ fork string // params.Rules field activating it, empty for Frontier (always on)
constGas uint64
minStack int
maxStack int
@@ -123,6 +123,7 @@ type generator struct {
fset *token.FileSet
handlers map[string]*ast.FuncDecl // opXxx handlers from instructions.go and eips.go
stackHelpers map[string]*ast.FuncDecl // (s *Stack) helpers from stack.go, spliced inline
+ gasHelpers map[string]*ast.FuncDecl // (g *GasBudget) charge methods from gascosts.go, spliced by name
specs [256]opSpec
buf *bytes.Buffer
}
@@ -138,14 +139,16 @@ func (g *generator) p(format string, args ...any) {
// Handler parsing + body splicing
// ---------------------------------------------------------------------------
-// parseHandlers parses instructions.go, eips.go and stack.go. It returns the
-// top-level opXxx handlers by name, and separately the *Stack helper methods by
-// name (the inliner splices the latter into the former).
-func parseHandlers(vmDir string) (fset *token.FileSet, handlers, stackHelpers map[string]*ast.FuncDecl) {
+// parseHandlers parses instructions.go, eips.go, stack.go and gascosts.go. It
+// returns the top-level opXxx handlers by name, the //gen:inline *Stack helper
+// methods by name (spliced into handler bodies), and the *GasBudget charge
+// methods by name (spliced directly at gas steps).
+func parseHandlers(vmDir string) (fset *token.FileSet, handlers, stackHelpers, gasHelpers map[string]*ast.FuncDecl) {
fset = token.NewFileSet()
handlers = map[string]*ast.FuncDecl{}
stackHelpers = map[string]*ast.FuncDecl{}
- for _, name := range []string{"instructions.go", "eips.go", "stack.go"} {
+ gasHelpers = map[string]*ast.FuncDecl{}
+ for _, name := range []string{"instructions.go", "eips.go", "stack.go", "gascosts.go"} {
path := filepath.Join(vmDir, name)
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
@@ -159,25 +162,31 @@ func parseHandlers(vmDir string) (fset *token.FileSet, handlers, stackHelpers ma
switch {
case fn.Recv == nil: // top-level opXxx handler
handlers[fn.Name.Name] = fn
- case isStackMethod(fn) && hasInlineMarker(fn): // (s *Stack) helper tagged //gen:inline
+ case methodReceiver(fn) == "Stack" && hasInlineMarker(fn): // (s *Stack) helper tagged //gen:inline
stackHelpers[fn.Name.Name] = fn
+ case methodReceiver(fn) == "GasBudget": // (g *GasBudget) charge method, spliced by name at gas steps
+ gasHelpers[fn.Name.Name] = fn
}
}
}
- return fset, handlers, stackHelpers
+ return fset, handlers, stackHelpers, gasHelpers
}
-// isStackMethod reports whether fn is a method on *Stack.
-func isStackMethod(fn *ast.FuncDecl) bool {
+// methodReceiver returns the receiver type name of a pointer-receiver method
+// (e.g. "Stack" for (s *Stack)), or "" if fn is not such a method.
+func methodReceiver(fn *ast.FuncDecl) string {
if fn.Recv == nil || len(fn.Recv.List) != 1 {
- return false
+ return ""
}
star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
if !ok {
- return false
+ return ""
}
id, ok := star.X.(*ast.Ident)
- return ok && id.Name == "Stack"
+ if !ok {
+ return ""
+ }
+ return id.Name
}
// hasInlineMarker reports whether fn is tagged //gen:inline, which marks a stack
@@ -194,25 +203,25 @@ func hasInlineMarker(fn *ast.FuncDecl) bool {
return false
}
-var returnRe = regexp.MustCompile(`^(\s*)return\s+([^,]+),\s*(.+)$`)
+var opcodeReturnRe = regexp.MustCompile(`^(\s*)return\s+([^,]+),\s*(.+)$`)
-// inlineBody returns a named handler's body, rewritten so it can be spliced
-// into the dispatch loop (see rewriteSplicedBody). The caller emits it with p.
-func (g *generator) inlineBody(handler string) string {
+// inlineOpcodeBody returns a named handler's body, rewritten so it can be spliced
+// into the dispatch loop (see rewriteOpcodeReturns). The caller emits it with p.
+func (g *generator) inlineOpcodeBody(handler string) string {
fn := g.handlers[handler]
if fn == nil {
fatalf("no handler %q to inline", handler)
}
- return g.rewriteSplicedBody(g.inlineStackHelpers(fn.Body.List, nil))
+ return g.rewriteOpcodeReturns(g.inlineStackHelpers(fn.Body.List, nil))
}
-// inlineFactoryBody splices the body of the executionFunc closure that a make*
+// inlineOpcodeFactoryBody splices the body of the executionFunc closure that a make*
// factory returns, substituting the factory's parameters with the per-opcode
// constants in args (positional, matching the factory signature). This lets
// closure-built handlers (makePush, makeDup) be derived from their single
// definition rather than restated in the generator. The caller emits the
// result with p.
-func (g *generator) inlineFactoryBody(factory string, args ...int) string {
+func (g *generator) inlineOpcodeFactoryBody(factory string, args ...int) string {
fn := g.handlers[factory]
if fn == nil {
fatalf("no factory %q to inline", factory)
@@ -227,7 +236,7 @@ func (g *generator) inlineFactoryBody(factory string, args ...int) string {
for i, nm := range names {
params[nm] = args[i]
}
- return g.rewriteSplicedBody(g.inlineStackHelpers(lit.Body.List, params))
+ return g.rewriteOpcodeReturns(g.inlineStackHelpers(lit.Body.List, params))
}
// factoryClosure returns the executionFunc literal that a make* factory's body
@@ -262,17 +271,17 @@ func (g *generator) renderAst(stmts []ast.Stmt) string {
return raw.String()
}
-// rewriteSplicedBody rewrites a printed handler body so it runs inside the
+// rewriteOpcodeReturns rewrites a printed handler body so it runs inside 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. (Stack helpers were already
// inlined by inlineStackHelpers before the body was printed.)
-func (g *generator) rewriteSplicedBody(src string) string {
+func (g *generator) rewriteOpcodeReturns(src string) string {
src = strings.ReplaceAll(src, "*pc", "pc")
var out bytes.Buffer
for _, line := range strings.Split(src, "\n") {
- if m := returnRe.FindStringSubmatch(line); m != nil {
+ if m := opcodeReturnRe.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
@@ -297,6 +306,50 @@ func (g *generator) rewriteSplicedBody(src string) string {
return out.String()
}
+var gasReturnRe = regexp.MustCompile(`^(\s*)return\s+(\S.*)$`)
+
+// inlineGasBody splices a (g *GasBudget) charge method's body at a gas step,
+// mapping the receiver to contract.Gas and the method's single uint64 parameter
+// to the per-opcode gas constant. It is the gas-step analog of inlineOpcodeBody: the
+// method's `return ` becomes the loop's out-of-gas exit and its trailing
+// `return nil` is dropped so the opcode falls through to its remaining steps (see
+// rewriteGasReturns). The receiver and parameter are substituted textually on
+// word boundaries, which cannot touch fields like RegularGas.
+func (g *generator) inlineGasBody(name string, arg int) string {
+ fn := g.gasHelpers[name]
+ if fn == nil {
+ fatalf("no gas helper %q to inline", name)
+ }
+ names := paramNames(fn)
+ if len(names) != 1 {
+ fatalf("gas helper %q takes %d params, want 1", name, len(names))
+ }
+ src := g.renderAst(fn.Body.List)
+ src = regexp.MustCompile(`\b`+recvName(fn)+`\b`).ReplaceAllString(src, "contract.Gas")
+ src = substParams(src, map[string]int{names[0]: arg})
+ return g.rewriteGasReturns(src)
+}
+
+// rewriteGasReturns rewrites a spliced charge body so it runs as a gas step in
+// the dispatch loop: a `return ` becomes the out-of-gas break, and the
+// trailing `return nil` (success) is dropped so the opcode continues.
+func (g *generator) rewriteGasReturns(src string) string {
+ var out bytes.Buffer
+ for _, line := range strings.Split(src, "\n") {
+ if m := gasReturnRe.FindStringSubmatch(line); m != nil {
+ indent, val := m[1], strings.TrimSpace(m[2])
+ if val == "nil" {
+ continue // success: fall through to the rest of the op
+ }
+ out.WriteString(indent + "res, err = nil, " + val + "\n")
+ out.WriteString(indent + "break mainLoop\n")
+ continue
+ }
+ out.WriteString(line + "\n")
+ }
+ return out.String()
+}
+
// stackCall is a matched call to a tagged helper.
type stackCall struct {
helper string // helper method name
@@ -506,7 +559,7 @@ func (g *generator) deriveSpecs(forks []vm.GenFork) {
g.specs[code] = opSpec{
defined: true,
name: o.Name,
- introF: fork.RuleField,
+ fork: fork.RuleField,
constGas: o.ConstantGas,
minStack: o.MinStack,
maxStack: o.MaxStack,
@@ -517,13 +570,13 @@ func (g *generator) deriveSpecs(forks []vm.GenFork) {
// 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 {
+ for code, handler := range inlineOps {
g.checkStable(code, handler, forks)
}
- // directCall opcodes bake their static gas and stack bounds the same way, so
+ // directCallOps opcodes bake their static gas and stack bounds the same way, so
// they must be fork-stable too. Dynamic gas is allowed (it is charged through
// the named gas function, not baked).
- for code := range directCall {
+ for code := range directCallOps {
g.checkDirectCallStable(code, forks)
}
}
@@ -544,17 +597,17 @@ func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
}
}
-// checkDirectCallStable verifies a directCall opcode is safe to direct-call. Its static
+// checkDirectCallStable verifies a directCallOps opcode is safe to direct-call. Its static
// gas and stack bounds must be the same across every fork it appears in (they are
// baked as constants), and its handler, gas and memory functions must be the same
// across those forks too (they are called by name, so a fork that swapped one
// would otherwise be missed). Unlike checkStable it allows dynamic gas, which
-// directCall ops carry by definition. It does not check the directCall map's names
+// directCallOps ops carry by definition. It does not check the directCallOps map's names
// against the table, which the differential test covers.
func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
spec := g.specs[code]
if !spec.defined {
- fatalf("opcode %#x (directCall) is never defined", code)
+ fatalf("opcode %#x (directCallOps) is never defined", code)
}
var exec, dyn, mem string
seen := false
@@ -564,7 +617,7 @@ func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
continue
}
if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack {
- fatalf("opcode %#x (%s) is in directCall but not fork-stable (fork %s): static gas or stack bounds vary, cannot bake", code, spec.name, fork.Name)
+ fatalf("opcode %#x (%s) is in directCallOps but not fork-stable (fork %s): static gas or stack bounds vary, cannot bake", code, spec.name, fork.Name)
}
// Handler, gas and memory functions must match across forks too, or
// direct-calling them by name would skip a fork that swapped one. Names
@@ -573,7 +626,7 @@ func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
if !seen {
exec, dyn, mem, seen = o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, true
} else if o.ExecuteFn != exec || o.DynamicGasFn != dyn || o.MemorySizeFn != mem {
- fatalf("opcode %#x (%s) is in directCall but its functions vary by fork (fork %s): got %s/%s/%s, want %s/%s/%s, cannot direct-call",
+ fatalf("opcode %#x (%s) is in directCallOps but its functions vary by fork (fork %s): got %s/%s/%s, want %s/%s/%s, cannot direct-call",
code, spec.name, fork.Name, o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, exec, dyn, mem)
}
}
@@ -616,12 +669,7 @@ func (g *generator) emitGasCheck(spec opSpec) {
if spec.constGas == 0 {
return
}
- g.p(`
- if contract.Gas.RegularGas < %d {
- return nil, ErrOutOfGas
- }
- contract.Gas.RegularGas -= %d
- `, spec.constGas, spec.constGas)
+ g.p("%s", g.inlineGasBody("chargeRegularOnly", int(spec.constGas)))
}
// emitOpBody emits the stack/gas guards and the opcode body (the portion that runs
@@ -647,27 +695,27 @@ func (g *generator) emitOpBody(code byte) {
`)
}
- switch h := inlineHandler[code]; h {
+ switch h := inlineOps[code]; h {
case "makePush": // PUSH3-PUSH32: splice makePush(size, size)
n := int(code) - 0x5f
- g.p("%s", g.inlineFactoryBody("makePush", n, n))
+ g.p("%s", g.inlineOpcodeFactoryBody("makePush", n, n))
case "makeDup": // DUP1-DUP16: splice makeDup(n)
- g.p("%s", g.inlineFactoryBody("makeDup", int(code)-0x7f))
+ g.p("%s", g.inlineOpcodeFactoryBody("makeDup", int(code)-0x7f))
default: // the rest: splice the opXxx handler body
- g.p("%s", g.inlineBody(h))
+ g.p("%s", g.inlineOpcodeBody(h))
}
}
func (g *generator) emitInlineOp(code byte) {
spec := g.specs[code]
g.p("case %s:\n", spec.name)
- if spec.introF == "" {
+ if spec.fork == "" {
g.emitOpBody(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", spec.introF)
+ g.p("if rules.%s {\n", spec.fork)
g.emitOpBody(code)
g.p("}\n")
g.p(`
@@ -679,10 +727,10 @@ func (g *generator) emitInlineOp(code byte) {
// emitDirectCallOp emits an 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 directCall).
+// fork-invariant ops (see directCallOps).
func (g *generator) emitDirectCallOp(code byte) {
spec := g.specs[code]
- fns := directCall[code]
+ fns := directCallOps[code]
g.p("case %s:\n", spec.name)
g.emitStackChecks(spec)
g.emitGasCheck(spec)
@@ -706,10 +754,13 @@ func (g *generator) emitDirectCallOp(code byte) {
if err != nil {
return nil, fmt.Errorf("%%w: %%v", ErrOutOfGas, err)
}
- if contract.Gas.RegularGas < dynamicCost.RegularGas {
+ if dynamicCost.StateGas == 0 {
+ if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
+ return nil, err
+ }
+ } else if !contract.Gas.charge(dynamicCost) {
return nil, ErrOutOfGas
}
- contract.Gas.RegularGas -= dynamicCost.RegularGas
if memorySize > 0 {
mem.Resize(memorySize)
}
@@ -735,10 +786,9 @@ func (g *generator) emitDefault() {
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
cost := operation.constantGas
- if contract.Gas.RegularGas < cost {
- return nil, ErrOutOfGas
+ if err := contract.Gas.chargeRegularOnly(cost); err != nil {
+ return nil, err
}
- contract.Gas.RegularGas -= cost
var memorySize uint64
if operation.dynamicGas != nil {
if operation.memorySize != nil {
@@ -755,10 +805,13 @@ func (g *generator) emitDefault() {
if err != nil {
return nil, fmt.Errorf("%%w: %%v", ErrOutOfGas, err)
}
- if contract.Gas.RegularGas < dynamicCost.RegularGas {
+ if dynamicCost.StateGas == 0 {
+ if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
+ return nil, err
+ }
+ } else if !contract.Gas.charge(dynamicCost) {
return nil, ErrOutOfGas
}
- contract.Gas.RegularGas -= dynamicCost.RegularGas
}
if memorySize > 0 {
mem.Resize(memorySize)
@@ -829,9 +882,9 @@ func (g *generator) emitFile() {
// Inlined cases, in opcode order for readability.
for code := 0; code < 256; code++ {
b := byte(code)
- if _, named := inlineHandler[b]; named {
+ if _, named := inlineOps[b]; named {
g.emitInlineOp(b)
- } else if _, dc := directCall[b]; dc {
+ } else if _, dc := directCallOps[b]; dc {
g.emitDirectCallOp(b)
}
}
@@ -857,8 +910,8 @@ func main() {
}
vmDir := filepath.Dir(filepath.Dir(self)) // .../core/vm/gen -> .../core/vm
- fset, handlers, stackHelpers := parseHandlers(vmDir)
- g := &generator{fset: fset, handlers: handlers, stackHelpers: stackHelpers, buf: new(bytes.Buffer)}
+ fset, handlers, stackHelpers, gasHelpers := parseHandlers(vmDir)
+ g := &generator{fset: fset, handlers: handlers, stackHelpers: stackHelpers, gasHelpers: gasHelpers, buf: new(bytes.Buffer)}
g.deriveSpecs(vm.GenForks())
g.emitFile()
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
index 2161ad67bc..3a8cd501cc 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interp_gen.go
@@ -47,9 +47,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -63,9 +66,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 5 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 5
+ contract.Gas.UsedRegularGas += 5
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -79,9 +85,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -95,9 +104,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 5 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 5
+ contract.Gas.UsedRegularGas += 5
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -111,9 +123,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 5 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 5
+ contract.Gas.UsedRegularGas += 5
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -127,9 +142,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 5 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 5
+ contract.Gas.UsedRegularGas += 5
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -143,9 +161,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 5 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 5
+ contract.Gas.UsedRegularGas += 5
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -159,9 +180,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 3}
}
if contract.Gas.RegularGas < 8 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 8
+ contract.Gas.UsedRegularGas += 8
+
stack.inner.top -= 2
stack.size -= 2
x := &stack.inner.data[stack.inner.top+1]
@@ -176,9 +200,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 3}
}
if contract.Gas.RegularGas < 8 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 8
+ contract.Gas.UsedRegularGas += 8
+
stack.inner.top -= 2
stack.size -= 2
x := &stack.inner.data[stack.inner.top+1]
@@ -193,9 +220,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 5 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 5
+ contract.Gas.UsedRegularGas += 5
+
stack.inner.top--
stack.size--
back := &stack.inner.data[stack.inner.top]
@@ -209,9 +239,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -229,9 +262,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -249,9 +285,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -269,9 +308,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -289,9 +331,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -309,9 +354,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
x := scope.Stack.peek()
if x.IsZero() {
x.SetOne()
@@ -326,9 +374,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -342,9 +393,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -358,9 +412,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
x := &stack.inner.data[stack.inner.top]
@@ -374,9 +431,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
x := scope.Stack.peek()
x.Not(x)
pc++
@@ -387,9 +447,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
th := &stack.inner.data[stack.inner.top]
@@ -404,9 +467,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
shift := &stack.inner.data[stack.inner.top]
@@ -428,9 +494,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
shift := &stack.inner.data[stack.inner.top]
@@ -452,9 +521,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.top--
stack.size--
shift := &stack.inner.data[stack.inner.top]
@@ -483,9 +555,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
}
if contract.Gas.RegularGas < 5 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 5
+ contract.Gas.UsedRegularGas += 5
+
x := scope.Stack.peek()
x.SetUint64(256 - uint64(x.BitLen()))
pc++
@@ -499,9 +574,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 30 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 30
+ contract.Gas.UsedRegularGas += 30
+
var memorySize uint64
{
memSize, overflow := memoryKeccak256(stack)
@@ -517,10 +595,13 @@ mainLoop:
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if contract.Gas.RegularGas < dynamicCost.RegularGas {
+ if dynamicCost.StateGas == 0 {
+ if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
+ return nil, err
+ }
+ } else if !contract.Gas.charge(dynamicCost) {
return nil, ErrOutOfGas
}
- contract.Gas.RegularGas -= dynamicCost.RegularGas
if memorySize > 0 {
mem.Resize(memorySize)
}
@@ -535,9 +616,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
}
if contract.Gas.RegularGas < 2 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 2
+ contract.Gas.UsedRegularGas += 2
+
scope.Stack.drop()
pc++
continue mainLoop
@@ -547,9 +631,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
var memorySize uint64
{
memSize, overflow := memoryMLoad(stack)
@@ -565,10 +652,13 @@ mainLoop:
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if contract.Gas.RegularGas < dynamicCost.RegularGas {
+ if dynamicCost.StateGas == 0 {
+ if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
+ return nil, err
+ }
+ } else if !contract.Gas.charge(dynamicCost) {
return nil, ErrOutOfGas
}
- contract.Gas.RegularGas -= dynamicCost.RegularGas
if memorySize > 0 {
mem.Resize(memorySize)
}
@@ -583,9 +673,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
var memorySize uint64
{
memSize, overflow := memoryMStore(stack)
@@ -601,10 +694,13 @@ mainLoop:
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if contract.Gas.RegularGas < dynamicCost.RegularGas {
+ if dynamicCost.StateGas == 0 {
+ if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
+ return nil, err
+ }
+ } else if !contract.Gas.charge(dynamicCost) {
return nil, ErrOutOfGas
}
- contract.Gas.RegularGas -= dynamicCost.RegularGas
if memorySize > 0 {
mem.Resize(memorySize)
}
@@ -619,9 +715,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
var memorySize uint64
{
memSize, overflow := memoryMStore8(stack)
@@ -637,10 +736,13 @@ mainLoop:
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if contract.Gas.RegularGas < dynamicCost.RegularGas {
+ if dynamicCost.StateGas == 0 {
+ if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
+ return nil, err
+ }
+ } else if !contract.Gas.charge(dynamicCost) {
return nil, ErrOutOfGas
}
- contract.Gas.RegularGas -= dynamicCost.RegularGas
if memorySize > 0 {
mem.Resize(memorySize)
}
@@ -655,9 +757,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 1}
}
if contract.Gas.RegularGas < 8 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 8
+ contract.Gas.UsedRegularGas += 8
+
if evm.abort.Load() {
res, err = nil, errStopToken
break mainLoop
@@ -676,9 +781,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 10 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 10
+ contract.Gas.UsedRegularGas += 10
+
if evm.abort.Load() {
res, err = nil, errStopToken
break mainLoop
@@ -702,9 +810,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 2 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 2
+ contract.Gas.UsedRegularGas += 2
+
stack.inner.top++
stack.size++
elem := &stack.inner.data[stack.inner.top-1]
@@ -717,9 +828,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 2 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 2
+ contract.Gas.UsedRegularGas += 2
+
stack.inner.top++
stack.size++
elem := &stack.inner.data[stack.inner.top-1]
@@ -729,9 +843,12 @@ mainLoop:
case JUMPDEST:
if contract.Gas.RegularGas < 1 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 1
+ contract.Gas.UsedRegularGas += 1
+
pc++
continue mainLoop
@@ -741,9 +858,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 2 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 2
+ contract.Gas.UsedRegularGas += 2
+
stack.inner.top++
stack.size++
elem := &stack.inner.data[stack.inner.top-1]
@@ -759,9 +879,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -788,9 +911,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -819,9 +945,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -851,9 +980,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -883,9 +1015,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -915,9 +1050,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -947,9 +1085,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -979,9 +1120,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1011,9 +1155,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1043,9 +1190,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1075,9 +1225,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1107,9 +1260,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1139,9 +1295,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1171,9 +1330,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1203,9 +1365,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1235,9 +1400,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1267,9 +1435,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1299,9 +1470,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1331,9 +1505,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1363,9 +1540,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1395,9 +1575,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1427,9 +1610,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1459,9 +1645,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1491,9 +1680,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1523,9 +1715,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1555,9 +1750,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1587,9 +1785,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1619,9 +1820,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1651,9 +1855,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1683,9 +1890,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1715,9 +1925,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1747,9 +1960,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
if isEIP4762 {
res, err = table[op].execute(&pc, evm, scope)
if err != nil {
@@ -1781,9 +1997,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-1]
stack.size++
stack.inner.top++
@@ -1797,9 +2016,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-2]
stack.size++
stack.inner.top++
@@ -1813,9 +2035,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-3]
stack.size++
stack.inner.top++
@@ -1829,9 +2054,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-4]
stack.size++
stack.inner.top++
@@ -1845,9 +2073,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-5]
stack.size++
stack.inner.top++
@@ -1861,9 +2092,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-6]
stack.size++
stack.inner.top++
@@ -1877,9 +2111,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-7]
stack.size++
stack.inner.top++
@@ -1893,9 +2130,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-8]
stack.size++
stack.inner.top++
@@ -1909,9 +2149,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-9]
stack.size++
stack.inner.top++
@@ -1925,9 +2168,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-10]
stack.size++
stack.inner.top++
@@ -1941,9 +2187,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-11]
stack.size++
stack.inner.top++
@@ -1957,9 +2206,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-12]
stack.size++
stack.inner.top++
@@ -1973,9 +2225,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-13]
stack.size++
stack.inner.top++
@@ -1989,9 +2244,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-14]
stack.size++
stack.inner.top++
@@ -2005,9 +2263,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-15]
stack.size++
stack.inner.top++
@@ -2021,9 +2282,12 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: 1023}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size] = stack.inner.data[stack.bottom+stack.size-16]
stack.size++
stack.inner.top++
@@ -2035,9 +2299,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 2}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-2], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-2]
pc++
continue mainLoop
@@ -2047,9 +2314,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 3}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-3], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-3]
pc++
continue mainLoop
@@ -2059,9 +2329,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 4}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-4], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-4]
pc++
continue mainLoop
@@ -2071,9 +2344,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 5}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-5], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-5]
pc++
continue mainLoop
@@ -2083,9 +2359,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 6}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-6], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-6]
pc++
continue mainLoop
@@ -2095,9 +2374,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 7}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-7], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-7]
pc++
continue mainLoop
@@ -2107,9 +2389,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 8}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-8], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-8]
pc++
continue mainLoop
@@ -2119,9 +2404,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 9}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-9], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-9]
pc++
continue mainLoop
@@ -2131,9 +2419,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 10}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-10], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-10]
pc++
continue mainLoop
@@ -2143,9 +2434,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 11}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-11], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-11]
pc++
continue mainLoop
@@ -2155,9 +2449,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 12}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-12], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-12]
pc++
continue mainLoop
@@ -2167,9 +2464,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 13}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-13], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-13]
pc++
continue mainLoop
@@ -2179,9 +2479,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 14}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-14], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-14]
pc++
continue mainLoop
@@ -2191,9 +2494,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 15}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-15], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-15]
pc++
continue mainLoop
@@ -2203,9 +2509,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 16}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-16], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-16]
pc++
continue mainLoop
@@ -2215,9 +2524,12 @@ mainLoop:
return nil, &ErrStackUnderflow{stackLen: sLen, required: 17}
}
if contract.Gas.RegularGas < 3 {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
contract.Gas.RegularGas -= 3
+ contract.Gas.UsedRegularGas += 3
+
stack.inner.data[stack.bottom+stack.size-17], stack.inner.data[stack.bottom+stack.size-1] = stack.inner.data[stack.bottom+stack.size-1], stack.inner.data[stack.bottom+stack.size-17]
pc++
continue mainLoop
@@ -2230,10 +2542,9 @@ mainLoop:
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
cost := operation.constantGas
- if contract.Gas.RegularGas < cost {
- return nil, ErrOutOfGas
+ if err := contract.Gas.chargeRegularOnly(cost); err != nil {
+ return nil, err
}
- contract.Gas.RegularGas -= cost
var memorySize uint64
if operation.dynamicGas != nil {
if operation.memorySize != nil {
@@ -2250,10 +2561,13 @@ mainLoop:
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if contract.Gas.RegularGas < dynamicCost.RegularGas {
+ if dynamicCost.StateGas == 0 {
+ if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
+ return nil, err
+ }
+ } else if !contract.Gas.charge(dynamicCost) {
return nil, ErrOutOfGas
}
- contract.Gas.RegularGas -= dynamicCost.RegularGas
}
if memorySize > 0 {
mem.Resize(memorySize)
diff --git a/core/vm/interp_gen_test.go b/core/vm/interp_gen_test.go
index 119faba910..5ca2039e31 100644
--- a/core/vm/interp_gen_test.go
+++ b/core/vm/interp_gen_test.go
@@ -103,6 +103,14 @@ var diffForks = func() []struct {
preCon.ArrowGlacierBlock = nil
preCon.GrayGlacierBlock = nil
+ // amsterdam: Merged plus the Amsterdam (EIP-8037) timestamp, so the diff test
+ // exercises the multidimensional gas accounting (regular + state gas). Without
+ // this lane a state-gas charging divergence between the two interpreters would
+ // go unnoticed.
+ ams := *params.MergedTestChainConfig
+ amsTime := uint64(0)
+ ams.AmsterdamTime = &amsTime
+
return []struct {
name string
cfg *params.ChainConfig
@@ -112,6 +120,7 @@ var diffForks = func() []struct {
{"Byzantium", &preCon, false},
{"London", params.TestChainConfig, false},
{"Merged", params.MergedTestChainConfig, true},
+ {"Amsterdam", &ams, true},
}
}()
@@ -295,6 +304,9 @@ func diffBlockCtx(merged bool) BlockContext {
GasLimit: 30_000_000,
BaseFee: big.NewInt(7),
BlobBaseFee: big.NewInt(3),
+ // Price state gas as mainnet does (see core.NewEVMBlockContext), so the
+ // Amsterdam diff lane exercises the EIP-8037 state-gas charging path.
+ CostPerStateByte: params.CostPerStateByte,
}
if merged {
h := common.HexToHash("0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20")
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 4c0dd799dc..60d0888bd6 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -217,8 +217,8 @@ func (evm *EVM) execTraced(scope *ScopeContext) (ret []byte, err error) {
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
// for tracing: this gas consumption event is emitted below in the debug section.
- if !contract.Gas.ChargeRegularOnly(cost) {
- return nil, ErrOutOfGas
+ if err := contract.Gas.chargeRegularOnly(cost); err != nil {
+ return nil, err
}
// All ops with a dynamic memory usage also has a dynamic gas cost.
@@ -248,8 +248,8 @@ func (evm *EVM) execTraced(scope *ScopeContext) (ret []byte, err error) {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
if dynamicCost.StateGas == 0 {
- if !contract.Gas.ChargeRegularOnly(dynamicCost.RegularGas) {
- return nil, ErrOutOfGas
+ if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
+ return nil, err
}
} else if !contract.Gas.charge(dynamicCost) {
return nil, ErrOutOfGas
From bfa0bfdc7c2b2827e34079d5de2b4df32296f43b Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Mon, 29 Jun 2026 17:45:01 -0500
Subject: [PATCH 17/23] core/vm, core/vm/gen: charge dynamic gas through a
shared chargeDynamic helper
---
core/vm/gascosts.go | 13 +++++++++
core/vm/gen/main.go | 66 ++++++++++++++++++++++--------------------
core/vm/interp_gen.go | 40 +++++++------------------
core/vm/interpreter.go | 8 ++---
4 files changed, 59 insertions(+), 68 deletions(-)
diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go
index b73732daa3..9111bdf856 100644
--- a/core/vm/gascosts.go
+++ b/core/vm/gascosts.go
@@ -94,6 +94,19 @@ func (g *GasBudget) chargeRegularOnly(r uint64) error {
return nil
}
+// chargeDynamic charges a dynamic gas cost: a regular-only deduction when there
+// is no state gas, otherwise the full multidimensional charge through the
+// reservoir.
+func (g *GasBudget) chargeDynamic(cost GasCosts) error {
+ if cost.StateGas == 0 {
+ return g.chargeRegularOnly(cost.RegularGas)
+ }
+ if !g.charge(cost) {
+ return ErrOutOfGas
+ }
+ return nil
+}
+
// CanAfford reports whether the running budget can cover the given cost vector
// without going out of gas.
func (g GasBudget) CanAfford(cost GasCosts) bool {
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index 0c8f805e17..43cecd07a9 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -665,7 +665,10 @@ func (g *generator) emitStackChecks(spec opSpec) {
}
}
-func (g *generator) emitGasCheck(spec opSpec) {
+// emitStaticGas charges an opcode's constant gas by splicing the chargeRegularOnly
+// body inline (call-free, for the hot path); a zero constant emits nothing. It is
+// the static-gas counterpart to emitDynamicGas.
+func (g *generator) emitStaticGas(spec opSpec) {
if spec.constGas == 0 {
return
}
@@ -677,7 +680,7 @@ func (g *generator) emitGasCheck(spec opSpec) {
func (g *generator) emitOpBody(code byte) {
spec := g.specs[code]
g.emitStackChecks(spec)
- g.emitGasCheck(spec)
+ g.emitStaticGas(spec)
// 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.
@@ -724,6 +727,26 @@ func (g *generator) emitInlineOp(code byte) {
`)
}
+// emitDynamicGas emits the dynamic-gas computation and charge shared by the
+// direct-call and default cases. gasFn is the dynamic-gas function to invoke:
+// a name like "gasKeccak256" for a direct call, or "operation.dynamicGas" for
+// the table case. A computation error is wrapped as ErrOutOfGas, then the cost
+// is charged through GasBudget.chargeDynamic. The doubled %%w/%%v are not
+// generator verbs: Fprintf collapses each %% to one %, leaving a literal
+// fmt.Errorf("%w: %v", ...) in the generated code.
+func (g *generator) emitDynamicGas(gasFn string) {
+ g.p(`
+ var dynamicCost GasCosts
+ dynamicCost, err = %s(evm, contract, stack, mem, memorySize)
+ if err != nil {
+ return nil, fmt.Errorf("%%w: %%v", ErrOutOfGas, err)
+ }
+ if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
+ return nil, err
+ }
+ `, gasFn)
+}
+
// emitDirectCallOp emits an 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
@@ -733,11 +756,8 @@ func (g *generator) emitDirectCallOp(code byte) {
fns := directCallOps[code]
g.p("case %s:\n", spec.name)
g.emitStackChecks(spec)
- g.emitGasCheck(spec)
- // The three %s are the memory-size, dynamic-gas and handler names, in the
- // order fns[2], fns[1], fns[0]. The doubled %%w and %%v are not generator
- // verbs: Fprintf collapses each %% to a single %, so the generated code ends
- // up with a literal fmt.Errorf("%w: %v", ...).
+ g.emitStaticGas(spec)
+ // fns[2], fns[1], fns[0] are the memory-size, dynamic-gas and handler names.
g.p(`
var memorySize uint64
{
@@ -749,18 +769,9 @@ func (g *generator) emitDirectCallOp(code byte) {
return nil, ErrGasUintOverflow
}
}
- var dynamicCost GasCosts
- dynamicCost, err = %s(evm, contract, stack, mem, memorySize)
- if err != nil {
- return nil, fmt.Errorf("%%w: %%v", ErrOutOfGas, err)
- }
- if dynamicCost.StateGas == 0 {
- if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
- return nil, err
- }
- } else if !contract.Gas.charge(dynamicCost) {
- return nil, ErrOutOfGas
- }
+ `, fns[2])
+ g.emitDynamicGas(fns[1])
+ g.p(`
if memorySize > 0 {
mem.Resize(memorySize)
}
@@ -770,7 +781,7 @@ func (g *generator) emitDirectCallOp(code byte) {
}
pc++
continue mainLoop
- `, fns[2], fns[1], fns[0])
+ `, fns[0])
}
func (g *generator) emitDefault() {
@@ -800,18 +811,9 @@ func (g *generator) emitDefault() {
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 dynamicCost.StateGas == 0 {
- if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
- return nil, err
- }
- } else if !contract.Gas.charge(dynamicCost) {
- return nil, ErrOutOfGas
- }
+ `)
+ g.emitDynamicGas("operation.dynamicGas")
+ g.p(`
}
if memorySize > 0 {
mem.Resize(memorySize)
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
index 3a8cd501cc..79c01a5a38 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interp_gen.go
@@ -595,12 +595,8 @@ mainLoop:
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if dynamicCost.StateGas == 0 {
- if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
- return nil, err
- }
- } else if !contract.Gas.charge(dynamicCost) {
- return nil, ErrOutOfGas
+ if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
+ return nil, err
}
if memorySize > 0 {
mem.Resize(memorySize)
@@ -652,12 +648,8 @@ mainLoop:
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if dynamicCost.StateGas == 0 {
- if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
- return nil, err
- }
- } else if !contract.Gas.charge(dynamicCost) {
- return nil, ErrOutOfGas
+ if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
+ return nil, err
}
if memorySize > 0 {
mem.Resize(memorySize)
@@ -694,12 +686,8 @@ mainLoop:
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if dynamicCost.StateGas == 0 {
- if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
- return nil, err
- }
- } else if !contract.Gas.charge(dynamicCost) {
- return nil, ErrOutOfGas
+ if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
+ return nil, err
}
if memorySize > 0 {
mem.Resize(memorySize)
@@ -736,12 +724,8 @@ mainLoop:
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if dynamicCost.StateGas == 0 {
- if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
- return nil, err
- }
- } else if !contract.Gas.charge(dynamicCost) {
- return nil, ErrOutOfGas
+ if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
+ return nil, err
}
if memorySize > 0 {
mem.Resize(memorySize)
@@ -2561,12 +2545,8 @@ mainLoop:
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if dynamicCost.StateGas == 0 {
- if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
- return nil, err
- }
- } else if !contract.Gas.charge(dynamicCost) {
- return nil, ErrOutOfGas
+ if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
+ return nil, err
}
}
if memorySize > 0 {
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 60d0888bd6..56d38c0bbd 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -247,12 +247,8 @@ func (evm *EVM) execTraced(scope *ScopeContext) (ret []byte, err error) {
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- if dynamicCost.StateGas == 0 {
- if err := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); err != nil {
- return nil, err
- }
- } else if !contract.Gas.charge(dynamicCost) {
- return nil, ErrOutOfGas
+ if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
+ return nil, err
}
}
From 834f7017aa67344a9f042afd228c49b7b76ffa30 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Mon, 29 Jun 2026 18:35:41 -0500
Subject: [PATCH 18/23] core/vm, core/vm/gen: build the default case from the
shared stack and gas emitters
---
core/vm/gen/main.go | 109 ++++++++++++++++--------------------------
core/vm/interp_gen.go | 9 ++--
2 files changed, 48 insertions(+), 70 deletions(-)
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index 43cecd07a9..56d9d18c38 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -73,10 +73,6 @@ var inlineOps = func() map[byte]string {
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",
}
for code := 0x62; code <= 0x7f; code++ { // PUSH3-PUSH32
m[byte(code)] = "makePush"
@@ -84,6 +80,9 @@ var inlineOps = func() map[byte]string {
for code := 0x80; code <= 0x8f; code++ { // DUP1-DUP16
m[byte(code)] = "makeDup"
}
+ for code := 0x90; code <= 0x9f; code++ { // SWAP1-SWAP16
+ m[byte(code)] = fmt.Sprintf("opSwap%d", code-0x8f)
+ }
return m
}()
@@ -91,17 +90,7 @@ var inlineOps = func() map[byte]string {
// 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 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.
+// in the default case.
var directCallOps = map[byte][3]string{
0x20: {"opKeccak256", "gasKeccak256", "memoryKeccak256"}, // KECCAK256
0x51: {"opMload", "gasMLoad", "memoryMLoad"}, // MLOAD
@@ -113,7 +102,7 @@ var directCallOps = map[byte][3]string{
type opSpec struct {
defined bool
name string // opcode mnemonic, e.g. "ADD"
- fork string // params.Rules field activating it, empty for Frontier (always on)
+ fork string
constGas uint64
minStack int
maxStack int
@@ -283,15 +272,6 @@ func (g *generator) rewriteOpcodeReturns(src string) string {
for _, line := range strings.Split(src, "\n") {
if m := opcodeReturnRe.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")
@@ -315,7 +295,7 @@ var gasReturnRe = regexp.MustCompile(`^(\s*)return\s+(\S.*)$`)
// `return nil` is dropped so the opcode falls through to its remaining steps (see
// rewriteGasReturns). The receiver and parameter are substituted textually on
// word boundaries, which cannot touch fields like RegularGas.
-func (g *generator) inlineGasBody(name string, arg int) string {
+func (g *generator) inlineGasBody(name, amount string) string {
fn := g.gasHelpers[name]
if fn == nil {
fatalf("no gas helper %q to inline", name)
@@ -326,7 +306,7 @@ func (g *generator) inlineGasBody(name string, arg int) string {
}
src := g.renderAst(fn.Body.List)
src = regexp.MustCompile(`\b`+recvName(fn)+`\b`).ReplaceAllString(src, "contract.Gas")
- src = substParams(src, map[string]int{names[0]: arg})
+ src = regexp.MustCompile(`\b`+names[0]+`\b`).ReplaceAllString(src, amount)
return g.rewriteGasReturns(src)
}
@@ -636,51 +616,52 @@ func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
// 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(spec opSpec) {
- under := spec.minStack > 0
- over := spec.maxStack < stackLimit
+// emitStackChecks emits the underflow/overflow guards, mirroring the legacy
+// loop's order (stack validated before gas). minExpr/maxExpr are the stack-bound
+// expressions (baked constants on the inlined/direct paths, operation.minStack/
+// operation.maxStack in the table path) and under/over select which guards to
+// emit; the baked paths omit a guard whose bound is trivial.
+func (g *generator) emitStackChecks(minExpr, maxExpr string, under, over bool) {
switch {
case under && over:
g.p(`
- if sLen := stack.len(); sLen < %d {
- return nil, &ErrStackUnderflow{stackLen: sLen, required: %d}
- } else if sLen > %d {
- return nil, &ErrStackOverflow{stackLen: sLen, limit: %d}
+ if sLen := stack.len(); sLen < %s {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: %s}
+ } else if sLen > %s {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: %s}
}
- `, spec.minStack, spec.minStack, spec.maxStack, spec.maxStack)
+ `, minExpr, minExpr, maxExpr, maxExpr)
case under:
g.p(`
- if sLen := stack.len(); sLen < %d {
- return nil, &ErrStackUnderflow{stackLen: sLen, required: %d}
+ if sLen := stack.len(); sLen < %s {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: %s}
}
- `, spec.minStack, spec.minStack)
+ `, minExpr, minExpr)
case over:
g.p(`
- if sLen := stack.len(); sLen > %d {
- return nil, &ErrStackOverflow{stackLen: sLen, limit: %d}
+ if sLen := stack.len(); sLen > %s {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: %s}
}
- `, spec.maxStack, spec.maxStack)
+ `, maxExpr, maxExpr)
}
}
-// emitStaticGas charges an opcode's constant gas by splicing the chargeRegularOnly
-// body inline (call-free, for the hot path); a zero constant emits nothing. It is
-// the static-gas counterpart to emitDynamicGas.
-func (g *generator) emitStaticGas(spec opSpec) {
- if spec.constGas == 0 {
- return
- }
- g.p("%s", g.inlineGasBody("chargeRegularOnly", int(spec.constGas)))
+// emitStaticGas charges static gas by splicing the chargeRegularOnly body inline
+// (call-free) for amount: a baked constant on the inlined and direct-call paths,
+// operation.constantGas in the table path. It is the static-gas counterpart to
+// emitDynamicGas.
+func (g *generator) emitStaticGas(amount string) {
+ g.p("%s", g.inlineGasBody("chargeRegularOnly", amount))
}
// emitOpBody 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) emitOpBody(code byte) {
spec := g.specs[code]
- g.emitStackChecks(spec)
- g.emitStaticGas(spec)
+ g.emitStackChecks(fmt.Sprint(spec.minStack), fmt.Sprint(spec.maxStack), spec.minStack > 0, spec.maxStack < stackLimit)
+ if spec.constGas != 0 {
+ g.emitStaticGas(fmt.Sprint(spec.constGas))
+ }
// 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.
@@ -755,8 +736,10 @@ func (g *generator) emitDirectCallOp(code byte) {
spec := g.specs[code]
fns := directCallOps[code]
g.p("case %s:\n", spec.name)
- g.emitStackChecks(spec)
- g.emitStaticGas(spec)
+ g.emitStackChecks(fmt.Sprint(spec.minStack), fmt.Sprint(spec.maxStack), spec.minStack > 0, spec.maxStack < stackLimit)
+ if spec.constGas != 0 {
+ g.emitStaticGas(fmt.Sprint(spec.constGas))
+ }
// fns[2], fns[1], fns[0] are the memory-size, dynamic-gas and handler names.
g.p(`
var memorySize uint64
@@ -785,21 +768,13 @@ func (g *generator) emitDirectCallOp(code byte) {
}
func (g *generator) emitDefault() {
- // The doubled %%w and %%v below are not generator verbs: Fprintf collapses
- // each %% to a single %, leaving a literal fmt.Errorf("%w: %v", ...) in the
- // generated default case.
g.p(`
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 err := contract.Gas.chargeRegularOnly(cost); err != nil {
- return nil, err
- }
+ `)
+ g.emitStackChecks("operation.minStack", "operation.maxStack", true, true)
+ g.emitStaticGas("operation.constantGas")
+ g.p(`
var memorySize uint64
if operation.dynamicGas != nil {
if operation.memorySize != nil {
diff --git a/core/vm/interp_gen.go b/core/vm/interp_gen.go
index 79c01a5a38..fa7679f471 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interp_gen.go
@@ -2525,10 +2525,13 @@ mainLoop:
} else if sLen > operation.maxStack {
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
- cost := operation.constantGas
- if err := contract.Gas.chargeRegularOnly(cost); err != nil {
- return nil, err
+ if contract.Gas.RegularGas < operation.constantGas {
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
+ contract.Gas.RegularGas -= operation.constantGas
+ contract.Gas.UsedRegularGas += operation.constantGas
+
var memorySize uint64
if operation.dynamicGas != nil {
if operation.memorySize != nil {
From f0d9dc1c8b511f571bf23964aa1d07a98e87cf6f Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Wed, 1 Jul 2026 18:37:29 -0500
Subject: [PATCH 19/23] core/vm, core/vm/gen: share the traced loop's gas and
memory steps with the generated dispatch and other cleanup
---
core/vm/gascosts.go | 13 -
core/vm/gen/main.go | 447 ++++++++++--------
core/vm/genspec.go | 2 +-
core/vm/interpreter.go | 124 +++--
core/vm/{interp_gen.go => interpreter_gen.go} | 184 +++----
...rp_gen_test.go => interpreter_gen_test.go} | 32 +-
6 files changed, 453 insertions(+), 349 deletions(-)
rename core/vm/{interp_gen.go => interpreter_gen.go} (95%)
rename core/vm/{interp_gen_test.go => interpreter_gen_test.go} (95%)
diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go
index 9111bdf856..b73732daa3 100644
--- a/core/vm/gascosts.go
+++ b/core/vm/gascosts.go
@@ -94,19 +94,6 @@ func (g *GasBudget) chargeRegularOnly(r uint64) error {
return nil
}
-// chargeDynamic charges a dynamic gas cost: a regular-only deduction when there
-// is no state gas, otherwise the full multidimensional charge through the
-// reservoir.
-func (g *GasBudget) chargeDynamic(cost GasCosts) error {
- if cost.StateGas == 0 {
- return g.chargeRegularOnly(cost.RegularGas)
- }
- if !g.charge(cost) {
- return ErrOutOfGas
- }
- return nil
-}
-
// CanAfford reports whether the running budget can cover the given cost vector
// without going out of gas.
func (g GasBudget) CanAfford(cost GasCosts) bool {
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index 56d9d18c38..3f59e6f151 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
-// Command gen generates core/vm/interp_gen.go, the EVM interpreter's untraced
+// Command gen generates core/vm/interpreter_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 /
@@ -33,7 +33,7 @@
// 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.
+// output. Do not hand-edit interpreter_gen.go.
//
// Usage: go generate ./core/vm/...
package main
@@ -61,7 +61,7 @@ const stackLimit = 1024 // params.StackLimit
// for that opcode. These are the hot, fork-stable opcodes with no dynamic gas.
// The value is usually an opXxx handler, but PUSH3-PUSH32 and DUP1-DUP16 are
// factory-built (one shared makePush / makeDup each), so their value is the
-// factory name and emitOpBody splices the factory body with the per-opcode size.
+// factory name and emitInlineOp splices the factory body with the per-opcode size.
// Opcodes not listed here (or in directCallOps) fall through to the default case,
// which dispatches via the per-fork table.
var inlineOps = func() map[byte]string {
@@ -92,16 +92,16 @@ var inlineOps = func() map[byte]string {
// those functions by name instead of the indirect operation.* pointer calls
// in the default case.
var directCallOps = map[byte][3]string{
- 0x20: {"opKeccak256", "gasKeccak256", "memoryKeccak256"}, // KECCAK256
- 0x51: {"opMload", "gasMLoad", "memoryMLoad"}, // MLOAD
- 0x52: {"opMstore", "gasMStore", "memoryMStore"}, // MSTORE
- 0x53: {"opMstore8", "gasMStore8", "memoryMStore8"}, // MSTORE8
+ 0x20: {"opKeccak256", "gasKeccak256", "memoryKeccak256"},
+ 0x51: {"opMload", "gasMLoad", "memoryMLoad"},
+ 0x52: {"opMstore", "gasMStore", "memoryMStore"},
+ 0x53: {"opMstore8", "gasMStore8", "memoryMStore8"},
}
// opSpec holds the per-opcode constants the generator bakes (gas, stack bounds, intro fork), derived from the per-fork tables.
type opSpec struct {
defined bool
- name string // opcode mnemonic, e.g. "ADD"
+ name string
fork string
constGas uint64
minStack int
@@ -109,12 +109,12 @@ type opSpec struct {
}
type generator struct {
- fset *token.FileSet
- handlers map[string]*ast.FuncDecl // opXxx handlers from instructions.go and eips.go
- stackHelpers map[string]*ast.FuncDecl // (s *Stack) helpers from stack.go, spliced inline
- gasHelpers map[string]*ast.FuncDecl // (g *GasBudget) charge methods from gascosts.go, spliced by name
- specs [256]opSpec
- buf *bytes.Buffer
+ fset *token.FileSet
+ opcodeHandlers map[string]*ast.FuncDecl
+ stackHelpers map[string]*ast.FuncDecl
+ gasHelpers map[string]*ast.FuncDecl
+ specs [256]opSpec
+ buf *bytes.Buffer
}
// p is the writer of the generated file. Every line of output is appended
@@ -124,20 +124,17 @@ func (g *generator) p(format string, args ...any) {
fmt.Fprintf(g.buf, format, args...)
}
-// ---------------------------------------------------------------------------
-// Handler parsing + body splicing
-// ---------------------------------------------------------------------------
-
-// parseHandlers parses instructions.go, eips.go, stack.go and gascosts.go. It
-// returns the top-level opXxx handlers by name, the //gen:inline *Stack helper
-// methods by name (spliced into handler bodies), and the *GasBudget charge
-// methods by name (spliced directly at gas steps).
-func parseHandlers(vmDir string) (fset *token.FileSet, handlers, stackHelpers, gasHelpers map[string]*ast.FuncDecl) {
+// parseHandlers parses instructions.go, eips.go, stack.go, gascosts.go and
+// interpreter.go. It returns the top-level opXxx handlers by name, the
+// //gen:inline *Stack helper methods, and the gas/memory helper functions
+// (chargeRegularOnly, computeMemorySize, chargeDynamicGas) whose bodies are
+// spliced into the generated dispatch (all by name).
+func parseHandlers(vmDir string) (fset *token.FileSet, opcodeHandlers, stackHelpers, gasHelpers map[string]*ast.FuncDecl) {
fset = token.NewFileSet()
- handlers = map[string]*ast.FuncDecl{}
+ opcodeHandlers = map[string]*ast.FuncDecl{}
stackHelpers = map[string]*ast.FuncDecl{}
gasHelpers = map[string]*ast.FuncDecl{}
- for _, name := range []string{"instructions.go", "eips.go", "stack.go", "gascosts.go"} {
+ for _, name := range []string{"instructions.go", "eips.go", "stack.go", "gascosts.go", "interpreter.go"} {
path := filepath.Join(vmDir, name)
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
@@ -149,16 +146,16 @@ func parseHandlers(vmDir string) (fset *token.FileSet, handlers, stackHelpers, g
continue
}
switch {
+ case fn.Name.Name == "chargeRegularOnly" || fn.Name.Name == "computeMemorySize" || fn.Name.Name == "chargeDynamicGas" || fn.Name.Name == "chargeVerkleCodeChunkGas": // spliced gas/memory helpers
+ gasHelpers[fn.Name.Name] = fn
case fn.Recv == nil: // top-level opXxx handler
- handlers[fn.Name.Name] = fn
+ opcodeHandlers[fn.Name.Name] = fn
case methodReceiver(fn) == "Stack" && hasInlineMarker(fn): // (s *Stack) helper tagged //gen:inline
stackHelpers[fn.Name.Name] = fn
- case methodReceiver(fn) == "GasBudget": // (g *GasBudget) charge method, spliced by name at gas steps
- gasHelpers[fn.Name.Name] = fn
}
}
}
- return fset, handlers, stackHelpers, gasHelpers
+ return fset, opcodeHandlers, stackHelpers, gasHelpers
}
// methodReceiver returns the receiver type name of a pointer-receiver method
@@ -194,24 +191,24 @@ func hasInlineMarker(fn *ast.FuncDecl) bool {
var opcodeReturnRe = regexp.MustCompile(`^(\s*)return\s+([^,]+),\s*(.+)$`)
-// inlineOpcodeBody returns a named handler's body, rewritten so it can be spliced
+// spliceOpcodeBody returns a named handler's body, rewritten so it can be spliced
// into the dispatch loop (see rewriteOpcodeReturns). The caller emits it with p.
-func (g *generator) inlineOpcodeBody(handler string) string {
- fn := g.handlers[handler]
+func (g *generator) spliceOpcodeBody(handler string) string {
+ fn := g.opcodeHandlers[handler]
if fn == nil {
fatalf("no handler %q to inline", handler)
}
return g.rewriteOpcodeReturns(g.inlineStackHelpers(fn.Body.List, nil))
}
-// inlineOpcodeFactoryBody splices the body of the executionFunc closure that a make*
+// spliceOpcodeFactoryBody splices the body of the executionFunc closure that a make*
// factory returns, substituting the factory's parameters with the per-opcode
// constants in args (positional, matching the factory signature). This lets
// closure-built handlers (makePush, makeDup) be derived from their single
// definition rather than restated in the generator. The caller emits the
// result with p.
-func (g *generator) inlineOpcodeFactoryBody(factory string, args ...int) string {
- fn := g.handlers[factory]
+func (g *generator) spliceOpcodeFactoryBody(factory string, args ...int) string {
+ fn := g.opcodeHandlers[factory]
if fn == nil {
fatalf("no factory %q to inline", factory)
}
@@ -288,28 +285,6 @@ func (g *generator) rewriteOpcodeReturns(src string) string {
var gasReturnRe = regexp.MustCompile(`^(\s*)return\s+(\S.*)$`)
-// inlineGasBody splices a (g *GasBudget) charge method's body at a gas step,
-// mapping the receiver to contract.Gas and the method's single uint64 parameter
-// to the per-opcode gas constant. It is the gas-step analog of inlineOpcodeBody: the
-// method's `return ` becomes the loop's out-of-gas exit and its trailing
-// `return nil` is dropped so the opcode falls through to its remaining steps (see
-// rewriteGasReturns). The receiver and parameter are substituted textually on
-// word boundaries, which cannot touch fields like RegularGas.
-func (g *generator) inlineGasBody(name, amount string) string {
- fn := g.gasHelpers[name]
- if fn == nil {
- fatalf("no gas helper %q to inline", name)
- }
- names := paramNames(fn)
- if len(names) != 1 {
- fatalf("gas helper %q takes %d params, want 1", name, len(names))
- }
- src := g.renderAst(fn.Body.List)
- src = regexp.MustCompile(`\b`+recvName(fn)+`\b`).ReplaceAllString(src, "contract.Gas")
- src = regexp.MustCompile(`\b`+names[0]+`\b`).ReplaceAllString(src, amount)
- return g.rewriteGasReturns(src)
-}
-
// rewriteGasReturns rewrites a spliced charge body so it runs as a gas step in
// the dispatch loop: a `return ` becomes the out-of-gas break, and the
// trailing `return nil` (success) is dropped so the opcode continues.
@@ -330,6 +305,30 @@ func (g *generator) rewriteGasReturns(src string) string {
return out.String()
}
+// rewriteStepReturns rewrites a spliced gas-step body's (value, error) returns so
+// it runs inline in the dispatch loop: a non-nil error becomes the out-of-gas
+// break; on success the value is assigned to target (or dropped when target is
+// empty) and the op falls through.
+func (g *generator) rewriteStepReturns(src, target string) string {
+ var out bytes.Buffer
+ for _, line := range strings.Split(src, "\n") {
+ if m := opcodeReturnRe.FindStringSubmatch(line); m != nil {
+ indent, val, errVal := m[1], strings.TrimSpace(m[2]), strings.TrimSpace(m[3])
+ if errVal == "nil" {
+ if target != "" {
+ out.WriteString(indent + target + " = " + val + "\n")
+ }
+ continue
+ }
+ out.WriteString(indent + "res, err = nil, " + errVal + "\n")
+ out.WriteString(indent + "break mainLoop\n")
+ continue
+ }
+ out.WriteString(line + "\n")
+ }
+ return out.String()
+}
+
// stackCall is a matched call to a tagged helper.
type stackCall struct {
helper string // helper method name
@@ -525,10 +524,10 @@ func renderInlineExpr(expr ast.Expr, subst map[string]string) string {
return ""
}
-// ---------------------------------------------------------------------------
-// Spec derivation (from the per-fork tables, via vm.GenForks)
-// ---------------------------------------------------------------------------
-
+// deriveSpecs records each opcode's bakeable constants (name, intro fork, static
+// gas, stack bounds) from the first fork that defines it, then checks that the
+// opcodes chosen for inlining and direct-calling are safe to bake by verifying
+// their constants are fork-stable (see checkStable and checkDirectCallStable).
func (g *generator) deriveSpecs(forks []vm.GenFork) {
for code := 0; code < 256; code++ {
for _, fork := range forks {
@@ -547,12 +546,13 @@ func (g *generator) deriveSpecs(forks []vm.GenFork) {
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.
+
+ // Every inlined opcode must be defined and have fork-stable static
+ // gas / stack bounds across all forks where it appears. Bail loudly otherwise.
for code, handler := range inlineOps {
g.checkStable(code, handler, forks)
}
+
// directCallOps opcodes bake their static gas and stack bounds the same way, so
// they must be fork-stable too. Dynamic gas is allowed (it is charged through
// the named gas function, not baked).
@@ -561,6 +561,11 @@ func (g *generator) deriveSpecs(forks []vm.GenFork) {
}
}
+// checkStable verifies an opcode selected for inlining is safe to inline: it must
+// be defined, its static gas and stack bounds must be the same across every fork
+// it appears in (they are baked as constants), and it must have no dynamic gas,
+// since an inlined op charges only the baked static gas. It bails loudly
+// otherwise. `what` names the handler for the error message.
func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
spec := g.specs[code]
if !spec.defined {
@@ -612,55 +617,74 @@ func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
}
}
-// ---------------------------------------------------------------------------
-// Case emission
-// ---------------------------------------------------------------------------
-
-// emitStackChecks emits the underflow/overflow guards, mirroring the legacy
-// loop's order (stack validated before gas). minExpr/maxExpr are the stack-bound
-// expressions (baked constants on the inlined/direct paths, operation.minStack/
-// operation.maxStack in the table path) and under/over select which guards to
-// emit; the baked paths omit a guard whose bound is trivial.
-func (g *generator) emitStackChecks(minExpr, maxExpr string, under, over bool) {
+// generateStackChecks returns the underflow/overflow guards, mirroring the legacy
+// loop's order (stack validated before gas). minExpr and maxExpr are the
+// stack-bound expressions (baked constants on the inlined/direct paths,
+// operation.minStack/maxStack in the table path). under and over select which
+// guards to emit, so the baked paths can omit a guard whose bound is trivial.
+func (g *generator) generateStackChecks(minExpr, maxExpr any, under, over bool) string {
switch {
case under && over:
- g.p(`
- if sLen := stack.len(); sLen < %s {
- return nil, &ErrStackUnderflow{stackLen: sLen, required: %s}
- } else if sLen > %s {
- return nil, &ErrStackOverflow{stackLen: sLen, limit: %s}
- }
- `, minExpr, minExpr, maxExpr, maxExpr)
+ return fmt.Sprintf(`if sLen := stack.len(); sLen < %v {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: %v}
+} else if sLen > %v {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: %v}
+}
+`, minExpr, minExpr, maxExpr, maxExpr)
case under:
- g.p(`
- if sLen := stack.len(); sLen < %s {
- return nil, &ErrStackUnderflow{stackLen: sLen, required: %s}
- }
- `, minExpr, minExpr)
+ return fmt.Sprintf(`if sLen := stack.len(); sLen < %v {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: %v}
+}
+`, minExpr, minExpr)
case over:
- g.p(`
- if sLen := stack.len(); sLen > %s {
- return nil, &ErrStackOverflow{stackLen: sLen, limit: %s}
- }
- `, maxExpr, maxExpr)
+ return fmt.Sprintf(`if sLen := stack.len(); sLen > %v {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: %v}
+}
+`, maxExpr, maxExpr)
}
+ return ""
}
-// emitStaticGas charges static gas by splicing the chargeRegularOnly body inline
-// (call-free) for amount: a baked constant on the inlined and direct-call paths,
-// operation.constantGas in the table path. It is the static-gas counterpart to
-// emitDynamicGas.
-func (g *generator) emitStaticGas(amount string) {
- g.p("%s", g.inlineGasBody("chargeRegularOnly", amount))
+// generateStaticGas returns the static-gas charge, spliced call-free from the
+// chargeRegularOnly body for amount: a baked constant on the inlined and
+// direct-call paths, operation.constantGas in the table path. The receiver maps
+// to contract.Gas and the method's single uint64 parameter to amount, substituted
+// textually on word boundaries (which cannot touch fields like RegularGas). Its
+// `return ` becomes the loop's out-of-gas exit and its trailing `return nil`
+// is dropped so the opcode falls through to its remaining steps (see
+// rewriteGasReturns).
+func (g *generator) generateStaticGas(amount any) string {
+ fn := g.gasHelpers["chargeRegularOnly"]
+ if fn == nil {
+ fatalf("no chargeRegularOnly gas helper to inline")
+ }
+ names := paramNames(fn)
+ if len(names) != 1 {
+ fatalf("chargeRegularOnly takes %d params, want 1", len(names))
+ }
+ src := g.renderAst(fn.Body.List)
+ src = regexp.MustCompile(`\b`+recvName(fn)+`\b`).ReplaceAllString(src, "contract.Gas")
+ src = regexp.MustCompile(`\b`+names[0]+`\b`).ReplaceAllString(src, fmt.Sprint(amount))
+ return g.rewriteGasReturns(src)
}
-// emitOpBody 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) emitOpBody(code byte) {
+// emitInlineOp emits an inlined opcode case: the stack and gas guards followed by
+// the spliced opcode body. A fork-introduced opcode wraps that body in a fork gate
+// so it runs only when the opcode is active for the current fork, otherwise the
+// case mirrors the legacy loop's undefined-opcode handling.
+func (g *generator) emitInlineOp(code byte) {
spec := g.specs[code]
- g.emitStackChecks(fmt.Sprint(spec.minStack), fmt.Sprint(spec.maxStack), spec.minStack > 0, spec.maxStack < stackLimit)
+ g.p("case %s:\n", spec.name)
+ if spec.fork != "" {
+ g.p("if rules.%s {\n", spec.fork)
+ }
+
+ // stack bounds check
+ g.p("%s", g.generateStackChecks(spec.minStack, spec.maxStack, spec.minStack > 0, spec.maxStack < stackLimit))
+
+ // static gas
if spec.constGas != 0 {
- g.emitStaticGas(fmt.Sprint(spec.constGas))
+ g.p("%s", g.generateStaticGas(spec.constGas))
}
// PUSH1-PUSH32 swap their execute function under EIP-4762 (verkle) to charge
@@ -679,53 +703,26 @@ func (g *generator) emitOpBody(code byte) {
`)
}
+ // opcode body
switch h := inlineOps[code]; h {
case "makePush": // PUSH3-PUSH32: splice makePush(size, size)
n := int(code) - 0x5f
- g.p("%s", g.inlineOpcodeFactoryBody("makePush", n, n))
+ g.p("%s", g.spliceOpcodeFactoryBody("makePush", n, n))
case "makeDup": // DUP1-DUP16: splice makeDup(n)
- g.p("%s", g.inlineOpcodeFactoryBody("makeDup", int(code)-0x7f))
+ g.p("%s", g.spliceOpcodeFactoryBody("makeDup", int(code)-0x7f))
default: // the rest: splice the opXxx handler body
- g.p("%s", g.inlineOpcodeBody(h))
+ g.p("%s", g.spliceOpcodeBody(h))
}
-}
-func (g *generator) emitInlineOp(code byte) {
- spec := g.specs[code]
- g.p("case %s:\n", spec.name)
- if spec.fork == "" {
- g.emitOpBody(code)
- return
+ // If opcode is inactive for this fork, then close the gate
+ // and fall back to the legacy loop's undefined-opcode handling.
+ if spec.fork != "" {
+ g.p(`
+ }
+ res, err = opUndefined(&pc, evm, scope)
+ break mainLoop
+ `)
}
- // 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", spec.fork)
- g.emitOpBody(code)
- g.p("}\n")
- g.p(`
- res, err = opUndefined(&pc, evm, scope)
- break mainLoop
- `)
-}
-
-// emitDynamicGas emits the dynamic-gas computation and charge shared by the
-// direct-call and default cases. gasFn is the dynamic-gas function to invoke:
-// a name like "gasKeccak256" for a direct call, or "operation.dynamicGas" for
-// the table case. A computation error is wrapped as ErrOutOfGas, then the cost
-// is charged through GasBudget.chargeDynamic. The doubled %%w/%%v are not
-// generator verbs: Fprintf collapses each %% to one %, leaving a literal
-// fmt.Errorf("%w: %v", ...) in the generated code.
-func (g *generator) emitDynamicGas(gasFn string) {
- g.p(`
- var dynamicCost GasCosts
- dynamicCost, err = %s(evm, contract, stack, mem, memorySize)
- if err != nil {
- return nil, fmt.Errorf("%%w: %%v", ErrOutOfGas, err)
- }
- if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
- return nil, err
- }
- `, gasFn)
}
// emitDirectCallOp emits an opcode case identical to the default case, except
@@ -736,77 +733,112 @@ func (g *generator) emitDirectCallOp(code byte) {
spec := g.specs[code]
fns := directCallOps[code]
g.p("case %s:\n", spec.name)
- g.emitStackChecks(fmt.Sprint(spec.minStack), fmt.Sprint(spec.maxStack), spec.minStack > 0, spec.maxStack < stackLimit)
+
+ // stack bounds check
+ g.p("%s", g.generateStackChecks(spec.minStack, spec.maxStack, spec.minStack > 0, spec.maxStack < stackLimit))
+
+ // static gas
if spec.constGas != 0 {
- g.emitStaticGas(fmt.Sprint(spec.constGas))
+ g.p("%s", g.generateStaticGas(spec.constGas))
}
- // fns[2], fns[1], fns[0] are the memory-size, dynamic-gas and handler names.
- g.p(`
- var memorySize uint64
- {
- memSize, overflow := %s(stack)
- if overflow {
- return nil, ErrGasUintOverflow
- }
- if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
- return nil, ErrGasUintOverflow
- }
- }
- `, fns[2])
- g.emitDynamicGas(fns[1])
+
+ // dynamic gas
+ g.p("\nvar memorySize uint64\n")
+
+ // Splice computeMemorySize's body, rewriting its operation.memorySize lookup to
+ // the opcode's memory-size function and its returns for the dispatch loop.
+ memSizeFn := g.gasHelpers["computeMemorySize"]
+ if memSizeFn == nil {
+ fatalf("no computeMemorySize gas helper to inline")
+ }
+ memSizeSrc := g.renderAst(memSizeFn.Body.List)
+ memSizeSrc = strings.ReplaceAll(memSizeSrc, "operation.memorySize", fns[2])
+ g.p("%s", g.rewriteStepReturns(memSizeSrc, "memorySize"))
+
+ // Splice chargeDynamicGas's body the same way, rewriting operation.dynamicGas to
+ // the opcode's gas function.
+ dynGasFn := g.gasHelpers["chargeDynamicGas"]
+ if dynGasFn == nil {
+ fatalf("no chargeDynamicGas gas helper to inline")
+ }
+ dynGasSrc := g.renderAst(dynGasFn.Body.List)
+ dynGasSrc = strings.ReplaceAll(dynGasSrc, "operation.dynamicGas", fns[1])
+ g.p("%s", g.rewriteStepReturns(dynGasSrc, ""))
+
+ // resize memory
g.p(`
if memorySize > 0 {
mem.Resize(memorySize)
}
+ `)
+
+ // call the opcode handler
+ g.p(`
res, err = %s(&pc, evm, scope)
if err != nil {
break mainLoop
}
+ `, fns[0])
+
+ // advance to the next opcode
+ g.p(`
pc++
continue mainLoop
- `, fns[0])
+ `)
}
+// emitDefault emits the switch's default case: every opcode not inlined or
+// direct-called (the fork-varying ops such as CALL, CREATE, SSTORE, SLOAD, LOG
+// and the COPY family) is dispatched through the active per-fork table, exactly
+// as the legacy loop did, so their volatile gas and opcode logic stays shared
+// rather than restated here.
func (g *generator) emitDefault() {
g.p(`
default:
operation := table[op]
`)
- g.emitStackChecks("operation.minStack", "operation.maxStack", true, true)
- g.emitStaticGas("operation.constantGas")
+ // stack bounds check
+ g.p("%s", g.generateStackChecks("operation.minStack", "operation.maxStack", true, true))
+
+ // static gas
+ g.p("%s", g.generateStaticGas("operation.constantGas"))
+
+ // dynamic gas
g.p(`
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
- }
- }
- `)
- g.emitDynamicGas("operation.dynamicGas")
- g.p(`
+ if memorySize, _, err = contract.meterDynamicGas(operation, evm, stack, mem); err != nil {
+ return nil, err
}
+ `)
+
+ // resize memory
+ g.p(`
if memorySize > 0 {
mem.Resize(memorySize)
}
+ `)
+
+ // call the opcode handler
+ g.p(`
res, err = operation.execute(&pc, evm, scope)
if err != nil {
break mainLoop
}
+ `)
+
+ // advance to the next opcode
+ g.p(`
pc++
continue mainLoop
`)
}
-// ---------------------------------------------------------------------------
-// File emission
-// ---------------------------------------------------------------------------
-
-func (g *generator) emitFile() {
+// createFile emits the whole generated file into g.buf: the header, package and
+// imports, then the execUntraced function (its locals and dispatch loop, the
+// verkle code-chunk gas, and a switch with one case per opcode built by the
+// emit* helpers). main formats the buffer and writes it to interpreter_gen.go.
+func (g *generator) createFile() {
+ // file header, package clause, and imports
g.p(`
// Code generated by core/vm/gen; DO NOT EDIT.
@@ -821,6 +853,7 @@ func (g *generator) emitFile() {
`)
+ // execUntraced: doc comment, loop-local declarations, and the dispatch loop
g.p(`
// execUntraced is the generated, tracing-free interpreter fast path. Hot,
// fork-stable opcodes are inlined with their static gas and stack bounds baked
@@ -845,18 +878,22 @@ func (g *generator) emitFile() {
_ = 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.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
- if consumed < wanted {
- return nil, ErrOutOfGas
- }
- }
+ `)
+
+ // verkle code-chunk gas, spliced from chargeVerkleCodeChunkGas
+ ccgFn := g.gasHelpers["chargeVerkleCodeChunkGas"]
+ if ccgFn == nil {
+ fatalf("no chargeVerkleCodeChunkGas gas helper to inline")
+ }
+ g.p("%s", g.rewriteGasReturns(g.renderAst(ccgFn.Body.List)))
+
+ // fetch the opcode and open the dispatch switch
+ g.p(`
op := contract.GetOp(pc)
switch op {
`)
- // Inlined cases, in opcode order for readability.
+
+ // one case per inlined or direct-call opcode, in opcode order
for code := 0; code < 256; code++ {
b := byte(code)
if _, named := inlineOps[b]; named {
@@ -865,15 +902,21 @@ func (g *generator) emitFile() {
g.emitDirectCallOp(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
-}
-// ---------------------------------------------------------------------------
+ // the default case: fork-varying ops via the per-fork table
+ g.emitDefault()
+
+ // close the switch and loop, clear the stop token, and return
+ g.p(`
+ }
+ }
+ if err == errStopToken {
+ err = nil
+ }
+ return res, err
+ }
+ `)
+}
func fatalf(format string, args ...any) {
fmt.Fprintf(os.Stderr, "gen: "+format+"\n", args...)
@@ -887,21 +930,21 @@ func main() {
}
vmDir := filepath.Dir(filepath.Dir(self)) // .../core/vm/gen -> .../core/vm
- fset, handlers, stackHelpers, gasHelpers := parseHandlers(vmDir)
- g := &generator{fset: fset, handlers: handlers, stackHelpers: stackHelpers, gasHelpers: gasHelpers, buf: new(bytes.Buffer)}
+ fset, opcodeHandlers, stackHelpers, gasHelpers := parseHandlers(vmDir)
+ g := &generator{fset: fset, opcodeHandlers: opcodeHandlers, stackHelpers: stackHelpers, gasHelpers: gasHelpers, buf: new(bytes.Buffer)}
g.deriveSpecs(vm.GenForks())
- g.emitFile()
+ g.createFile()
formatted, err := format.Source(g.buf.Bytes())
if err != nil {
- dbg := filepath.Join(vmDir, "interp_gen.go.broken")
+ dbg := filepath.Join(vmDir, "interpreter_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
+ // INTERPRETER_GEN_OUT lets the CI-match test (interpreter_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 := filepath.Join(vmDir, "interpreter_gen.go")
+ if env := os.Getenv("INTERPRETER_GEN_OUT"); env != "" {
out = env
}
if err := os.WriteFile(out, formatted, 0644); err != nil {
diff --git a/core/vm/genspec.go b/core/vm/genspec.go
index 21fb1e72e5..89729554db 100644
--- a/core/vm/genspec.go
+++ b/core/vm/genspec.go
@@ -32,7 +32,7 @@ import (
//
// The function names let the generator confirm the directCold ops are
// fork-invariant. The fork-varying gas/execute functions themselves are still
-// reached through the active per-fork JumpTable at runtime (see interp_gen.go),
+// reached through the active per-fork JumpTable at runtime (see interpreter_gen.go),
// not emitted by name: several are closures (gasCall, the memoryCopierGas
// family, makeGasLog) that FuncForPC reports only as anonymous labels, so they
// could not be called by name in any case.
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 56d38c0bbd..86589428a6 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -193,16 +193,8 @@ func (evm *EVM) execTraced(scope *ScopeContext) (ret []byte, err error) {
// Capture pre-execution values for tracing.
logged, pcCopy, gasCopy = false, pc, contract.Gas.RegularGas
}
-
- 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.RegularGas)
- contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
- if consumed < wanted {
- return nil, ErrOutOfGas
- }
+ if err = contract.chargeVerkleCodeChunkGas(evm, pc, isEIP4762); err != nil {
+ return nil, err
}
// Get the operation from the jump table and validate the stack to ensure there are
@@ -221,35 +213,15 @@ func (evm *EVM) execTraced(scope *ScopeContext) (ret []byte, err error) {
return nil, err
}
- // All ops with a dynamic memory usage also has a dynamic gas cost.
+ // All ops with a dynamic memory usage also has a dynamic gas cost. Size the
+ // memory and charge dynamic gas here.
var memorySize uint64
- if 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 overflows
- if 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 cost
- var dynamicCost GasCosts
- dynamicCost, err = operation.dynamicGas(evm, contract, stack, mem, memorySize)
- cost += dynamicCost.RegularGas // for tracing
- if err != nil {
- return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
- }
- if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
- return nil, err
- }
+ var dynamicCost GasCosts
+ memorySize, dynamicCost, err = contract.meterDynamicGas(operation, evm, stack, mem)
+ // cost is explicitly set so that the capture state defer method can get the proper cost
+ cost += dynamicCost.RegularGas // for tracing
+ if err != nil {
+ return nil, err
}
// Do tracing before potential memory expansion
@@ -284,3 +256,79 @@ func (evm *EVM) execTraced(scope *ScopeContext) (ret []byte, err error) {
return res, err
}
+
+// computeMemorySize runs an operation's memorySize function and word-aligns the
+// result, guarding overflow. The traced loop and the default case call it, and the
+// direct-call ops splice it, substituting the opcode's memory-size function for
+// operation.memorySize.
+func computeMemorySize(operation *operation, stack *Stack) (uint64, error) {
+ memSize, overflow := operation.memorySize(stack)
+ if overflow {
+ return 0, ErrGasUintOverflow
+ }
+ // memory is expanded in words of 32 bytes. Gas is also calculated in words.
+ size, overflow := math.SafeMul(toWordSize(memSize), 32)
+ if overflow {
+ return 0, ErrGasUintOverflow
+ }
+ return size, nil
+}
+
+// chargeDynamicGas runs an operation's dynamicGas function, treats a computation
+// failure as out of gas, and charges the cost. It returns the cost so the traced
+// loop can report it. The traced loop and the default case call it, and the
+// direct-call ops splice it, substituting the opcode's gas function for
+// operation.dynamicGas.
+func (contract *Contract) chargeDynamicGas(operation *operation, evm *EVM, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ dynamicCost, gerr := operation.dynamicGas(evm, contract, stack, mem, memorySize)
+ if gerr != nil {
+ return dynamicCost, fmt.Errorf("%w: %v", ErrOutOfGas, gerr)
+ }
+ // A regular-only deduction when there is no state gas, otherwise the full
+ // multidimensional charge through the reservoir.
+ if dynamicCost.StateGas == 0 {
+ if cerr := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
+ return dynamicCost, cerr
+ }
+ } else if !contract.Gas.charge(dynamicCost) {
+ return dynamicCost, ErrOutOfGas
+ }
+ return dynamicCost, nil
+}
+
+// meterDynamicGas sizes the memory an operation needs and charges its dynamic
+// gas, returning the size to expand to and the charged cost (which the traced
+// loop reports). The caller expands the memory afterward. computeMemorySize and
+// chargeDynamicGas do the work, guarded here for the generic table paths. The
+// direct-call ops splice those two by name and skip the guards.
+func (contract *Contract) meterDynamicGas(operation *operation, evm *EVM, stack *Stack, mem *Memory) (memorySize uint64, dynamicCost GasCosts, err error) {
+ if operation.dynamicGas != nil {
+ if operation.memorySize != nil {
+ if memorySize, err = computeMemorySize(operation, stack); err != nil {
+ return memorySize, dynamicCost, err
+ }
+ }
+ if dynamicCost, err = contract.chargeDynamicGas(operation, evm, stack, mem, memorySize); err != nil {
+ return memorySize, dynamicCost, err
+ }
+ }
+ return memorySize, dynamicCost, nil
+}
+
+// chargeVerkleCodeChunkGas charges EIP-4762 (verkle) witness gas for the code chunk the
+// pc sits in. It is a no-op outside verkle and for deployment or system calls.
+// isEIP4762 is passed in so the caller can hoist evm.chainRules.IsEIP4762 out of
+// the loop. The traced loop calls it, the generated loop splices its body.
+func (contract *Contract) chargeVerkleCodeChunkGas(evm *EVM, pc uint64, isEIP4762 bool) error {
+ // if the PC ends up in a new "chunk" of verkleized code, charge the
+ // associated costs.
+ 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.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
+ if consumed < wanted {
+ return ErrOutOfGas
+ }
+ }
+ return nil
+}
diff --git a/core/vm/interp_gen.go b/core/vm/interpreter_gen.go
similarity index 95%
rename from core/vm/interp_gen.go
rename to core/vm/interpreter_gen.go
index fa7679f471..252b69784f 100644
--- a/core/vm/interp_gen.go
+++ b/core/vm/interpreter_gen.go
@@ -37,9 +37,11 @@ mainLoop:
consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas.RegularGas)
contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if consumed < wanted {
- return nil, ErrOutOfGas
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
}
+
op := contract.GetOp(pc)
switch op {
case ADD:
@@ -581,23 +583,33 @@ mainLoop:
contract.Gas.UsedRegularGas += 30
var memorySize uint64
- {
- memSize, overflow := memoryKeccak256(stack)
- if overflow {
- return nil, ErrGasUintOverflow
- }
- if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
- return nil, ErrGasUintOverflow
+ memSize, overflow := memoryKeccak256(stack)
+ if overflow {
+ res, err = nil, ErrGasUintOverflow
+ break mainLoop
+ }
+ size, overflow := math.SafeMul(toWordSize(memSize), 32)
+ if overflow {
+ res, err = nil, ErrGasUintOverflow
+ break mainLoop
+ }
+ memorySize = size
+
+ dynamicCost, gerr := gasKeccak256(evm, contract, stack, mem, memorySize)
+ if gerr != nil {
+ res, err = nil, fmt.Errorf("%w: %v", ErrOutOfGas, gerr)
+ break mainLoop
+ }
+ if dynamicCost.StateGas == 0 {
+ if cerr := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
+ res, err = nil, cerr
+ break mainLoop
}
+ } else if !contract.Gas.charge(dynamicCost) {
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
- var dynamicCost GasCosts
- dynamicCost, err = gasKeccak256(evm, contract, stack, mem, memorySize)
- if err != nil {
- return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
- }
- if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
- return nil, err
- }
+
if memorySize > 0 {
mem.Resize(memorySize)
}
@@ -634,23 +646,33 @@ mainLoop:
contract.Gas.UsedRegularGas += 3
var memorySize uint64
- {
- memSize, overflow := memoryMLoad(stack)
- if overflow {
- return nil, ErrGasUintOverflow
- }
- if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
- return nil, ErrGasUintOverflow
+ memSize, overflow := memoryMLoad(stack)
+ if overflow {
+ res, err = nil, ErrGasUintOverflow
+ break mainLoop
+ }
+ size, overflow := math.SafeMul(toWordSize(memSize), 32)
+ if overflow {
+ res, err = nil, ErrGasUintOverflow
+ break mainLoop
+ }
+ memorySize = size
+
+ dynamicCost, gerr := gasMLoad(evm, contract, stack, mem, memorySize)
+ if gerr != nil {
+ res, err = nil, fmt.Errorf("%w: %v", ErrOutOfGas, gerr)
+ break mainLoop
+ }
+ if dynamicCost.StateGas == 0 {
+ if cerr := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
+ res, err = nil, cerr
+ break mainLoop
}
+ } else if !contract.Gas.charge(dynamicCost) {
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
- var dynamicCost GasCosts
- dynamicCost, err = gasMLoad(evm, contract, stack, mem, memorySize)
- if err != nil {
- return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
- }
- if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
- return nil, err
- }
+
if memorySize > 0 {
mem.Resize(memorySize)
}
@@ -672,23 +694,33 @@ mainLoop:
contract.Gas.UsedRegularGas += 3
var memorySize uint64
- {
- memSize, overflow := memoryMStore(stack)
- if overflow {
- return nil, ErrGasUintOverflow
- }
- if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
- return nil, ErrGasUintOverflow
+ memSize, overflow := memoryMStore(stack)
+ if overflow {
+ res, err = nil, ErrGasUintOverflow
+ break mainLoop
+ }
+ size, overflow := math.SafeMul(toWordSize(memSize), 32)
+ if overflow {
+ res, err = nil, ErrGasUintOverflow
+ break mainLoop
+ }
+ memorySize = size
+
+ dynamicCost, gerr := gasMStore(evm, contract, stack, mem, memorySize)
+ if gerr != nil {
+ res, err = nil, fmt.Errorf("%w: %v", ErrOutOfGas, gerr)
+ break mainLoop
+ }
+ if dynamicCost.StateGas == 0 {
+ if cerr := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
+ res, err = nil, cerr
+ break mainLoop
}
+ } else if !contract.Gas.charge(dynamicCost) {
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
- var dynamicCost GasCosts
- dynamicCost, err = gasMStore(evm, contract, stack, mem, memorySize)
- if err != nil {
- return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
- }
- if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
- return nil, err
- }
+
if memorySize > 0 {
mem.Resize(memorySize)
}
@@ -710,23 +742,33 @@ mainLoop:
contract.Gas.UsedRegularGas += 3
var memorySize uint64
- {
- memSize, overflow := memoryMStore8(stack)
- if overflow {
- return nil, ErrGasUintOverflow
- }
- if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
- return nil, ErrGasUintOverflow
+ memSize, overflow := memoryMStore8(stack)
+ if overflow {
+ res, err = nil, ErrGasUintOverflow
+ break mainLoop
+ }
+ size, overflow := math.SafeMul(toWordSize(memSize), 32)
+ if overflow {
+ res, err = nil, ErrGasUintOverflow
+ break mainLoop
+ }
+ memorySize = size
+
+ dynamicCost, gerr := gasMStore8(evm, contract, stack, mem, memorySize)
+ if gerr != nil {
+ res, err = nil, fmt.Errorf("%w: %v", ErrOutOfGas, gerr)
+ break mainLoop
+ }
+ if dynamicCost.StateGas == 0 {
+ if cerr := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
+ res, err = nil, cerr
+ break mainLoop
}
+ } else if !contract.Gas.charge(dynamicCost) {
+ res, err = nil, ErrOutOfGas
+ break mainLoop
}
- var dynamicCost GasCosts
- dynamicCost, err = gasMStore8(evm, contract, stack, mem, memorySize)
- if err != nil {
- return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
- }
- if err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
- return nil, err
- }
+
if memorySize > 0 {
mem.Resize(memorySize)
}
@@ -2533,24 +2575,8 @@ mainLoop:
contract.Gas.UsedRegularGas += operation.constantGas
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 err := contract.Gas.chargeDynamic(dynamicCost); err != nil {
- return nil, err
- }
+ if memorySize, _, err = contract.meterDynamicGas(operation, evm, stack, mem); err != nil {
+ return nil, err
}
if memorySize > 0 {
mem.Resize(memorySize)
diff --git a/core/vm/interp_gen_test.go b/core/vm/interpreter_gen_test.go
similarity index 95%
rename from core/vm/interp_gen_test.go
rename to core/vm/interpreter_gen_test.go
index 5ca2039e31..786dd14338 100644
--- a/core/vm/interp_gen_test.go
+++ b/core/vm/interpreter_gen_test.go
@@ -16,7 +16,7 @@
package vm
-// Tests for the generated interpreter dispatch (interp_gen.go): that the
+// Tests for the generated interpreter dispatch (interpreter_gen.go): that the
// committed file is up to date, that it behaves identically to the table loop,
// and that the fast path keeps its cheap stack helpers inlined.
@@ -41,7 +41,7 @@ import (
"github.com/holiman/uint256"
)
-// TestGeneratedDispatchUpToDate asserts that the committed interp_gen.go matches
+// TestGeneratedDispatchUpToDate asserts that the committed interpreter_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.
@@ -49,9 +49,9 @@ 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")
+ tmp := filepath.Join(t.TempDir(), "interpreter_gen.go")
cmd := exec.Command("go", "run", "./gen")
- cmd.Env = append(os.Environ(), "INTERP_GEN_OUT="+tmp)
+ cmd.Env = append(os.Environ(), "INTERPRETER_GEN_OUT="+tmp)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("running generator: %v\n%s", err, out)
}
@@ -59,12 +59,12 @@ func TestGeneratedDispatchUpToDate(t *testing.T) {
if err != nil {
t.Fatalf("reading regenerated output: %v", err)
}
- want, err := os.ReadFile("interp_gen.go")
+ want, err := os.ReadFile("interpreter_gen.go")
if err != nil {
- t.Fatalf("reading committed interp_gen.go: %v", err)
+ t.Fatalf("reading committed interpreter_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")
+ t.Fatalf("interpreter_gen.go is out of date; run `go generate ./core/vm/...` and commit the result")
}
}
@@ -444,7 +444,7 @@ func markedHelpers(t *testing.T) map[string]bool {
}
// TestGeneratedFastPathHelpersExpanded asserts the generator spliced every
-// //gen:inline helper inline, so none survives as a real call in interp_gen.go.
+// //gen:inline helper inline, so none survives as a real call in interpreter_gen.go.
// Those helpers exceed the compiler's inline budget for a function as large as
// execUntraced, so a missed splice would silently drop the inlining the fast
// path exists for. It is the expand-side counterpart to
@@ -452,10 +452,10 @@ func markedHelpers(t *testing.T) map[string]bool {
// the fast path makes no real stack-helper call, the costly ones by splicing,
// the cheap ones by compiler inlining.
func TestGeneratedFastPathHelpersExpanded(t *testing.T) {
- calls := countStackCalls(t, "interp_gen.go")
+ calls := countStackCalls(t, "interpreter_gen.go")
for h := range markedHelpers(t) {
if n := calls[h]; n != 0 {
- t.Errorf("(*Stack).%s is //gen:inline but has %d residual call(s) in interp_gen.go, expected 0.\n"+
+ t.Errorf("(*Stack).%s is //gen:inline but has %d residual call(s) in interpreter_gen.go, expected 0.\n"+
"The generator did not splice it. Check it is still in inlinable shape (core/vm/gen).", h, n)
}
}
@@ -463,7 +463,7 @@ func TestGeneratedFastPathHelpersExpanded(t *testing.T) {
// TestGeneratedFastPathHelpersInlined recompiles this package with the
// compiler's inlining diagnostics on and fails if any *Stack helper call that
-// survives into interp_gen.go was not inlined. Every survivor must be a cheap
+// survives into interpreter_gen.go was not inlined. Every survivor must be a cheap
// helper (len, pop1, peek, drop) the compiler inlines into execUntraced; the
// //gen:inline helpers are spliced away and owned by the Expanded test. The
// cheap ones inline today with margin except pop1, at cost 18 against Go's
@@ -483,21 +483,21 @@ func TestGeneratedFastPathHelpersInlined(t *testing.T) {
t.Fatalf("compiling with inlining diagnostics: %v\n%s", err, out)
}
diag := string(out)
- if !strings.Contains(diag, "interp_gen.go") {
- t.Fatalf("captured no interp_gen.go diagnostics, the -m build produced nothing to check:\n%s", diag)
+ if !strings.Contains(diag, "interpreter_gen.go") {
+ t.Fatalf("captured no interpreter_gen.go diagnostics, the -m build produced nothing to check:\n%s", diag)
}
// Every surviving stack-helper call (i.e. not a //gen:inline target) must be
// inlined by the compiler.
marked := markedHelpers(t)
- for h, n := range countStackCalls(t, "interp_gen.go") {
+ for h, n := range countStackCalls(t, "interpreter_gen.go") {
if marked[h] {
continue // spliced away, owned by TestGeneratedFastPathHelpersExpanded
}
- inlinedRe := regexp.MustCompile(`interp_gen\.go.*inlining call to \(\*Stack\)\.` + regexp.QuoteMeta(h) + `\b`)
+ inlinedRe := regexp.MustCompile(`interpreter_gen\.go.*inlining call to \(\*Stack\)\.` + regexp.QuoteMeta(h) + `\b`)
inlined := len(inlinedRe.FindAllString(diag, -1))
if inlined != n {
- t.Errorf("(*Stack).%s: %d call site(s) in interp_gen.go, %d inlined into execUntraced.\n"+
+ t.Errorf("(*Stack).%s: %d call site(s) in interpreter_gen.go, %d inlined into execUntraced.\n"+
"The compiler stopped inlining it, so the fast path now pays a real call. Shrink the\n"+
"body to fit the inline budget, or tag it //gen:inline in stack.go to splice it instead.", h, n, inlined)
continue
From 2756eedd27094cd1f7c7c2fdf64f359df15baaf6 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Mon, 6 Jul 2026 16:36:39 -0500
Subject: [PATCH 20/23] remove benchmarks
---
core/bench_test.go | 115 ------------------
core/vm/runtime/evm_bench_test.go | 106 ----------------
core/vm/runtime/testdata/erc20approval.hex | 1 -
core/vm/runtime/testdata/erc20mint.hex | 1 -
core/vm/runtime/testdata/erc20transfer.hex | 1 -
core/vm/runtime/testdata/evm-bench/README.md | 73 -----------
.../testdata/evm-bench/TenThousandHashes.sol | 17 ---
core/vm/runtime/testdata/evm-bench/compare.sh | 62 ----------
core/vm/runtime/testdata/evm-bench/gen.sh | 52 --------
core/vm/runtime/testdata/snailtracer.hex | 1 -
.../vm/runtime/testdata/tenthousandhashes.hex | 1 -
11 files changed, 430 deletions(-)
delete mode 100644 core/vm/runtime/evm_bench_test.go
delete mode 100644 core/vm/runtime/testdata/erc20approval.hex
delete mode 100644 core/vm/runtime/testdata/erc20mint.hex
delete mode 100644 core/vm/runtime/testdata/erc20transfer.hex
delete mode 100644 core/vm/runtime/testdata/evm-bench/README.md
delete mode 100644 core/vm/runtime/testdata/evm-bench/TenThousandHashes.sol
delete mode 100755 core/vm/runtime/testdata/evm-bench/compare.sh
delete mode 100755 core/vm/runtime/testdata/evm-bench/gen.sh
delete mode 100644 core/vm/runtime/testdata/snailtracer.hex
delete mode 100644 core/vm/runtime/testdata/tenthousandhashes.hex
diff --git a/core/bench_test.go b/core/bench_test.go
index 4e3923e4fe..47b991e5a3 100644
--- a/core/bench_test.go
+++ b/core/bench_test.go
@@ -18,12 +18,7 @@ package core
import (
"crypto/ecdsa"
- "encoding/binary"
- "encoding/hex"
"math/big"
- "os"
- "path/filepath"
- "strings"
"testing"
"github.com/ethereum/go-ethereum/common"
@@ -73,12 +68,6 @@ func BenchmarkInsertChain_ring1000_memdb(b *testing.B) {
func BenchmarkInsertChain_ring1000_diskdb(b *testing.B) {
benchInsertChain(b, true, genTxRing(1000))
}
-func BenchmarkInsertChain_evmWorkload_memdb(b *testing.B) {
- benchInsertChainEVM(b, false)
-}
-func BenchmarkInsertChain_evmWorkload_diskdb(b *testing.B) {
- benchInsertChainEVM(b, true)
-}
var (
// This is the content of the genesis block used by the benchmarks.
@@ -219,110 +208,6 @@ func benchInsertChain(b *testing.B, disk bool, gen func(int, *BlockGen)) {
}
}
-// The evmWorkload addresses hold the evm-bench contracts (see
-// core/vm/runtime/testdata), installed through the genesis alloc. Their
-// runtime bytecode sets up all state inside Benchmark(), so no constructor
-// runs.
-var benchWorkloadAddrs = []common.Address{
- common.HexToAddress("0x00000000000000000000000000000000000e0001"), // ERC20Transfer
- common.HexToAddress("0x00000000000000000000000000000000000e0002"), // ERC20Mint
- common.HexToAddress("0x00000000000000000000000000000000000e0003"), // ERC20ApprovalTransfer
- common.HexToAddress("0x00000000000000000000000000000000000e0004"), // TenThousandHashes
-}
-
-func benchWorkloadCode(tb testing.TB) [][]byte {
- files := []string{"erc20transfer.hex", "erc20mint.hex", "erc20approval.hex", "tenthousandhashes.hex"}
- codes := make([][]byte, len(files))
- for i, name := range files {
- raw, err := os.ReadFile(filepath.Join("vm", "runtime", "testdata", name))
- if err != nil {
- tb.Fatal(err)
- }
- code, err := hex.DecodeString(strings.TrimSpace(string(raw)))
- if err != nil {
- tb.Fatal(err)
- }
- codes[i] = code
- }
- return codes
-}
-
-// benchInsertChainEVM measures InsertChain over blocks dominated by real
-// contract execution, as a coarse local surrogate for sync throughput: the
-// timed path is full block processing, including sender recovery, EVM
-// execution, state updates, receipts, bloom filters and the state root.
-// Every block invokes the four evm-bench workloads (ERC-20 transfer, mint
-// and approval loops plus a keccak loop) and sends ten value transfers to
-// fresh accounts so the state grows as it would during sync. Reports Mgas/s
-// alongside the standard timings.
-func benchInsertChainEVM(b *testing.B, disk bool) {
- var db ethdb.Database
- if !disk {
- db = rawdb.NewMemoryDatabase()
- } else {
- pdb, err := pebble.New(b.TempDir(), 128, 128, "", false)
- if err != nil {
- b.Fatalf("cannot create temporary database: %v", err)
- }
- db = rawdb.NewDatabase(pdb)
- defer db.Close()
- }
- alloc := types.GenesisAlloc{benchRootAddr: {Balance: benchRootFunds}}
- for i, code := range benchWorkloadCode(b) {
- alloc[benchWorkloadAddrs[i]] = types.Account{Code: code, Balance: new(big.Int)}
- }
- gspec := &Genesis{
- Config: params.TestChainConfig,
- Alloc: alloc,
- GasLimit: 150_000_000,
- }
- selector := []byte{0x30, 0x62, 0x7b, 0x7c} // Benchmark()
- _, chain, _ := GenerateChainWithGenesis(gspec, ethash.NewFaker(), b.N, func(i int, gen *BlockGen) {
- signer := gen.Signer()
- gasPrice := big.NewInt(0)
- if gen.header.BaseFee != nil {
- gasPrice = gen.header.BaseFee
- }
- for _, addr := range benchWorkloadAddrs {
- to := addr
- tx, _ := types.SignNewTx(benchRootKey, signer, &types.LegacyTx{
- Nonce: gen.TxNonce(benchRootAddr),
- To: &to,
- Gas: 30_000_000,
- Data: selector,
- GasPrice: gasPrice,
- })
- gen.AddTx(tx)
- }
- for j := 0; j < 10; j++ {
- var to common.Address
- binary.BigEndian.PutUint64(to[4:12], uint64(i+1))
- binary.BigEndian.PutUint64(to[12:20], uint64(j+1))
- tx, _ := types.SignNewTx(benchRootKey, signer, &types.LegacyTx{
- Nonce: gen.TxNonce(benchRootAddr),
- To: &to,
- Value: big.NewInt(1),
- Gas: params.TxGas,
- GasPrice: gasPrice,
- })
- gen.AddTx(tx)
- }
- })
- var totalGas uint64
- for _, block := range chain {
- totalGas += block.GasUsed()
- }
- chainman, _ := NewBlockChain(db, gspec, ethash.NewFaker(), nil)
- defer chainman.Stop()
- b.ReportAllocs()
- b.ResetTimer()
- if i, err := chainman.InsertChain(chain); err != nil {
- b.Fatalf("insert error (block %d): %v\n", i, err)
- }
- b.StopTimer()
- b.ReportMetric(float64(totalGas)/1e6/b.Elapsed().Seconds(), "Mgas/s")
-}
-
func BenchmarkChainRead_header_10k(b *testing.B) {
benchReadChain(b, false, 10000)
}
diff --git a/core/vm/runtime/evm_bench_test.go b/core/vm/runtime/evm_bench_test.go
deleted file mode 100644
index 31a265c1f6..0000000000
--- a/core/vm/runtime/evm_bench_test.go
+++ /dev/null
@@ -1,106 +0,0 @@
-// 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 .
-
-// 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) }
diff --git a/core/vm/runtime/testdata/erc20approval.hex b/core/vm/runtime/testdata/erc20approval.hex
deleted file mode 100644
index 0700413573..0000000000
--- a/core/vm/runtime/testdata/erc20approval.hex
+++ /dev/null
@@ -1 +0,0 @@
-608060405234801561001057600080fd5b50600436106100b45760003560e01c80633950935111610071578063395093511461013857806370a082311461014b57806395d89b4114610174578063a457c2d71461017c578063a9059cbb1461018f578063dd62ed3e146101a257600080fd5b806306fdde03146100b9578063095ea7b3146100d757806318160ddd146100fa57806323b872dd1461010c57806330627b7c1461011f578063313ce56714610129575b600080fd5b6100c16101b5565b6040516100ce91906108ca565b60405180910390f35b6100ea6100e5366004610934565b610247565b60405190151581526020016100ce565b6002545b6040519081526020016100ce565b6100ea61011a36600461095e565b610261565b610127610285565b005b604051601281526020016100ce565b6100ea610146366004610934565b6103e4565b6100fe61015936600461099a565b6001600160a01b031660009081526020819052604090205490565b6100c1610406565b6100ea61018a366004610934565b610415565b6100ea61019d366004610934565b610490565b6100fe6101b03660046109bc565b61049e565b6060600380546101c4906109ef565b80601f01602080910402602001604051908101604052809291908181526020018280546101f0906109ef565b801561023d5780601f106102125761010080835404028352916020019161023d565b820191906000526020600020905b81548152906001019060200180831161022057829003601f168201915b5050505050905090565b6000336102558185856104c9565b60019150505b92915050565b60003361026f8582856105ed565b61027a858585610667565b506001949350505050565b6102a8336102956012600a610b23565b6102a390633b9aca00610b32565b61080b565b60015b6103e88110156103e1576102bf333361049e565b156103115760405162461bcd60e51b815260206004820152601b60248201527f6e6f6e2d7a65726f20616c6c6f77616e636520746f207374617274000000000060448201526064015b60405180910390fd5b61031b3382610247565b5080610327333361049e565b1461036c5760405162461bcd60e51b81526020600482015260156024820152746469646e2774206769766520616c6c6f77616e636560581b6044820152606401610308565b610377333383610261565b50610382333361049e565b156103cf5760405162461bcd60e51b815260206004820152601960248201527f6e6f6e2d7a65726f20616c6c6f77616e636520746f20656e64000000000000006044820152606401610308565b806103d981610b49565b9150506102ab565b50565b6000336102558185856103f7838361049e565b6104019190610b62565b6104c9565b6060600480546101c4906109ef565b60003381610423828661049e565b9050838110156104835760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b6064820152608401610308565b61027a82868684036104c9565b600033610255818585610667565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6001600160a01b03831661052b5760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608401610308565b6001600160a01b03821661058c5760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608401610308565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b60006105f9848461049e565b9050600019811461066157818110156106545760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606401610308565b61066184848484036104c9565b50505050565b6001600160a01b0383166106cb5760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608401610308565b6001600160a01b03821661072d5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608401610308565b6001600160a01b038316600090815260208190526040902054818110156107a55760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608401610308565b6001600160a01b03848116600081815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a3610661565b6001600160a01b0382166108615760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152606401610308565b80600260008282546108739190610b62565b90915550506001600160a01b038216600081815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b600060208083528351808285015260005b818110156108f7578581018301518582016040015282016108db565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b038116811461092f57600080fd5b919050565b6000806040838503121561094757600080fd5b61095083610918565b946020939093013593505050565b60008060006060848603121561097357600080fd5b61097c84610918565b925061098a60208501610918565b9150604084013590509250925092565b6000602082840312156109ac57600080fd5b6109b582610918565b9392505050565b600080604083850312156109cf57600080fd5b6109d883610918565b91506109e660208401610918565b90509250929050565b600181811c90821680610a0357607f821691505b602082108103610a2357634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b600181815b80851115610a7a578160001904821115610a6057610a60610a29565b80851615610a6d57918102915b93841c9390800290610a44565b509250929050565b600082610a915750600161025b565b81610a9e5750600061025b565b8160018114610ab45760028114610abe57610ada565b600191505061025b565b60ff841115610acf57610acf610a29565b50506001821b61025b565b5060208310610133831016604e8410600b8410161715610afd575081810a61025b565b610b078383610a3f565b8060001904821115610b1b57610b1b610a29565b029392505050565b60006109b560ff841683610a82565b808202811582820484141761025b5761025b610a29565b600060018201610b5b57610b5b610a29565b5060010190565b8082018082111561025b5761025b610a2956fea2646970667358221220c33117c9d08b3ca92e5865f957cca670ed19c2990ba76736dd40b693ae62098064736f6c63430008110033
\ No newline at end of file
diff --git a/core/vm/runtime/testdata/erc20mint.hex b/core/vm/runtime/testdata/erc20mint.hex
deleted file mode 100644
index ed06ae6b14..0000000000
--- a/core/vm/runtime/testdata/erc20mint.hex
+++ /dev/null
@@ -1 +0,0 @@
-608060405234801561001057600080fd5b50600436106100b45760003560e01c80633950935111610071578063395093511461013857806370a082311461014b57806395d89b4114610174578063a457c2d71461017c578063a9059cbb1461018f578063dd62ed3e146101a257600080fd5b806306fdde03146100b9578063095ea7b3146100d757806318160ddd146100fa57806323b872dd1461010c57806330627b7c1461011f578063313ce56714610129575b600080fd5b6100c16101b5565b6040516100ce919061079c565b60405180910390f35b6100ea6100e5366004610806565b610247565b60405190151581526020016100ce565b6002545b6040519081526020016100ce565b6100ea61011a366004610830565b610261565b610127610285565b005b604051601281526020016100ce565b6100ea610146366004610806565b6102b1565b6100fe61015936600461086c565b6001600160a01b031660009081526020819052604090205490565b6100c16102d3565b6100ea61018a366004610806565b6102e2565b6100ea61019d366004610806565b610362565b6100fe6101b036600461088e565b610370565b6060600380546101c4906108c1565b80601f01602080910402602001604051908101604052809291908181526020018280546101f0906108c1565b801561023d5780601f106102125761010080835404028352916020019161023d565b820191906000526020600020905b81548152906001019060200180831161022057829003601f168201915b5050505050905090565b60003361025581858561039b565b60019150505b92915050565b60003361026f8582856104bf565b61027a858585610539565b506001949350505050565b60005b6113888110156102ae5761029c33826106dd565b806102a681610911565b915050610288565b50565b6000336102558185856102c48383610370565b6102ce919061092a565b61039b565b6060600480546101c4906108c1565b600033816102f08286610370565b9050838110156103555760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b60648201526084015b60405180910390fd5b61027a828686840361039b565b600033610255818585610539565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6001600160a01b0383166103fd5760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b606482015260840161034c565b6001600160a01b03821661045e5760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b606482015260840161034c565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b60006104cb8484610370565b9050600019811461053357818110156105265760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000604482015260640161034c565b610533848484840361039b565b50505050565b6001600160a01b03831661059d5760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b606482015260840161034c565b6001600160a01b0382166105ff5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b606482015260840161034c565b6001600160a01b038316600090815260208190526040902054818110156106775760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b606482015260840161034c565b6001600160a01b03848116600081815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a3610533565b6001600160a01b0382166107335760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f206164647265737300604482015260640161034c565b8060026000828254610745919061092a565b90915550506001600160a01b038216600081815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b600060208083528351808285015260005b818110156107c9578581018301518582016040015282016107ad565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b038116811461080157600080fd5b919050565b6000806040838503121561081957600080fd5b610822836107ea565b946020939093013593505050565b60008060006060848603121561084557600080fd5b61084e846107ea565b925061085c602085016107ea565b9150604084013590509250925092565b60006020828403121561087e57600080fd5b610887826107ea565b9392505050565b600080604083850312156108a157600080fd5b6108aa836107ea565b91506108b8602084016107ea565b90509250929050565b600181811c908216806108d557607f821691505b6020821081036108f557634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b600060018201610923576109236108fb565b5060010190565b8082018082111561025b5761025b6108fb56fea2646970667358221220fa5dc8b94ca8266a9f37b573a3ac8993380ce8da74caa7732abd6e7ed31cd37e64736f6c63430008110033
\ No newline at end of file
diff --git a/core/vm/runtime/testdata/erc20transfer.hex b/core/vm/runtime/testdata/erc20transfer.hex
deleted file mode 100644
index cc01470901..0000000000
--- a/core/vm/runtime/testdata/erc20transfer.hex
+++ /dev/null
@@ -1 +0,0 @@
-608060405234801561001057600080fd5b50600436106100b45760003560e01c80633950935111610071578063395093511461013857806370a082311461014b57806395d89b4114610174578063a457c2d71461017c578063a9059cbb1461018f578063dd62ed3e146101a257600080fd5b806306fdde03146100b9578063095ea7b3146100d757806318160ddd146100fa57806323b872dd1461010c57806330627b7c1461011f578063313ce56714610129575b600080fd5b6100c16101b5565b6040516100ce91906107bf565b60405180910390f35b6100ea6100e5366004610829565b610247565b60405190151581526020016100ce565b6002545b6040519081526020016100ce565b6100ea61011a366004610853565b610261565b610127610285565b005b604051601281526020016100ce565b6100ea610146366004610829565b6102d4565b6100fe61015936600461088f565b6001600160a01b031660009081526020819052604090205490565b6100c16102f6565b6100ea61018a366004610829565b610305565b6100ea61019d366004610829565b610385565b6100fe6101b03660046108b1565b610393565b6060600380546101c4906108e4565b80601f01602080910402602001604051908101604052809291908181526020018280546101f0906108e4565b801561023d5780601f106102125761010080835404028352916020019161023d565b820191906000526020600020905b81548152906001019060200180831161022057829003601f168201915b5050505050905090565b6000336102558185856103be565b60019150505b92915050565b60003361026f8582856104e2565b61027a85858561055c565b506001949350505050565b6102a6336102956012600a610a18565b6102a190612710610a27565b610700565b60005b6113888110156102d1576102be600182610385565b50806102c981610a3e565b9150506102a9565b50565b6000336102558185856102e78383610393565b6102f19190610a57565b6103be565b6060600480546101c4906108e4565b600033816103138286610393565b9050838110156103785760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b60648201526084015b60405180910390fd5b61027a82868684036103be565b60003361025581858561055c565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6001600160a01b0383166104205760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b606482015260840161036f565b6001600160a01b0382166104815760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b606482015260840161036f565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b60006104ee8484610393565b9050600019811461055657818110156105495760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000604482015260640161036f565b61055684848484036103be565b50505050565b6001600160a01b0383166105c05760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b606482015260840161036f565b6001600160a01b0382166106225760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b606482015260840161036f565b6001600160a01b0383166000908152602081905260409020548181101561069a5760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b606482015260840161036f565b6001600160a01b03848116600081815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a3610556565b6001600160a01b0382166107565760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f206164647265737300604482015260640161036f565b80600260008282546107689190610a57565b90915550506001600160a01b038216600081815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b600060208083528351808285015260005b818110156107ec578581018301518582016040015282016107d0565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b038116811461082457600080fd5b919050565b6000806040838503121561083c57600080fd5b6108458361080d565b946020939093013593505050565b60008060006060848603121561086857600080fd5b6108718461080d565b925061087f6020850161080d565b9150604084013590509250925092565b6000602082840312156108a157600080fd5b6108aa8261080d565b9392505050565b600080604083850312156108c457600080fd5b6108cd8361080d565b91506108db6020840161080d565b90509250929050565b600181811c908216806108f857607f821691505b60208210810361091857634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b600181815b8085111561096f5781600019048211156109555761095561091e565b8085161561096257918102915b93841c9390800290610939565b509250929050565b6000826109865750600161025b565b816109935750600061025b565b81600181146109a957600281146109b3576109cf565b600191505061025b565b60ff8411156109c4576109c461091e565b50506001821b61025b565b5060208310610133831016604e8410600b84101617156109f2575081810a61025b565b6109fc8383610934565b8060001904821115610a1057610a1061091e565b029392505050565b60006108aa60ff841683610977565b808202811582820484141761025b5761025b61091e565b600060018201610a5057610a5061091e565b5060010190565b8082018082111561025b5761025b61091e56fea26469706673582212200111a8455f1c6aba553f7e7a1fe413d6a44eececa7e001df4c190aef76f39b5864736f6c63430008110033
\ No newline at end of file
diff --git a/core/vm/runtime/testdata/evm-bench/README.md b/core/vm/runtime/testdata/evm-bench/README.md
deleted file mode 100644
index 128d1faccf..0000000000
--- a/core/vm/runtime/testdata/evm-bench/README.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# 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` | 20000 chained keccak256 calls |
-
-## 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`. Besides the contract
-workloads it runs both `BenchmarkInsertChain_evmWorkload` variants from
-`core/bench_test.go`, which push blocks carrying these contracts through the
-full block import path (state in memory and on disk) and report Mgas/s, as a
-local stand-in for sync throughput. The synthetic dispatch loops (`BenchmarkSimpleLoop/loop*` in
-`core/vm/runtime/runtime_test.go`) are not part of the A/B suite, but can
-still be run manually to isolate dispatch overhead.
-
-## 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.
-
-`TenThousandHashes` is compiled from the vendored
-[`TenThousandHashes.sol`](TenThousandHashes.sol) in this directory, not the
-upstream evm-bench file. Upstream discards the hash result, so solc's optimizer
-deletes the keccak256 and the compiled benchmark degenerates into a bare
-counter loop performing a single static hash. The vendored copy chains the hash
-through a returned accumulator so the optimized bytecode performs all 20000
-hashes. Numbers for this benchmark are therefore not comparable with evm-bench
-or gevm published tables, which use degenerate bytecode (gevm's local
-replacement is broken differently, an inverted loop bound that hashes once).
-
-`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, except `TenThousandHashes`
- (see the regenerating section).
diff --git a/core/vm/runtime/testdata/evm-bench/TenThousandHashes.sol b/core/vm/runtime/testdata/evm-bench/TenThousandHashes.sol
deleted file mode 100644
index 004215e96f..0000000000
--- a/core/vm/runtime/testdata/evm-bench/TenThousandHashes.sol
+++ /dev/null
@@ -1,17 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0
-pragma solidity ^0.8.17;
-
-// Vendored from evm-bench (github.com/ziyadedher/evm-bench) with one fix.
-// The upstream loop discards the keccak256 result, so solc's optimizer
-// removes the pure call entirely and the compiled benchmark degenerates
-// into a bare counter loop that performs a single static hash. Chaining
-// the hash through an accumulator that the function returns keeps all
-// 20000 hashes in the optimized bytecode. The Benchmark() selector is
-// unchanged because return types are not part of the signature.
-contract TenThousandHashes {
- function Benchmark() external pure returns (bytes32 acc) {
- for (uint256 i = 0; i < 20000; i++) {
- acc = keccak256(abi.encodePacked(acc, i));
- }
- }
-}
diff --git a/core/vm/runtime/testdata/evm-bench/compare.sh b/core/vm/runtime/testdata/evm-bench/compare.sh
deleted file mode 100755
index aaf4eb9282..0000000000
--- a/core/vm/runtime/testdata/evm-bench/compare.sh
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/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 block import
-# benchmark (core/bench_test.go, BenchmarkInsertChain_evmWorkload, a local
-# stand-in for sync throughput) 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 package:pattern:iterations.
-BENCHES=(
- './core/vm/runtime/:^BenchmarkSnailtracer$:20x'
- './core/vm/runtime/:^BenchmarkTenThousandHashes$:100x'
- './core/vm/runtime/:^BenchmarkERC20Transfer$:100x'
- './core/vm/runtime/:^BenchmarkERC20Mint$:150x'
- './core/vm/runtime/:^BenchmarkERC20ApprovalTransfer$:120x'
- './core/:^BenchmarkInsertChain_evmWorkload_memdb$:10x'
- './core/:^BenchmarkInsertChain_evmWorkload_diskdb$:10x'
-)
-NEW="$(mktemp)"; OLD="$(mktemp)"
-
-run_suite() { # run_suite
- local dir="$1" out="$2" entry pkg pat n
- for entry in "${BENCHES[@]}"; do
- pkg="${entry%%:*}"
- pat="${entry#*:}"; pat="${pat%:*}"
- n="${entry##*:}"
- ( cd "$dir" && go test "$pkg" -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/"
-cp core/bench_test.go "$WT/core/bench_test.go"
-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"
diff --git a/core/vm/runtime/testdata/evm-bench/gen.sh b/core/vm/runtime/testdata/evm-bench/gen.sh
deleted file mode 100755
index 26dbb6b73d..0000000000
--- a/core/vm/runtime/testdata/evm-bench/gen.sh
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/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.
-#
-# TenThousandHashes is compiled from the vendored TenThousandHashes.sol in
-# this directory rather than the upstream file. Upstream discards the hash
-# result, so the optimizer deletes the keccak256 and the benchmark degenerates
-# into a bare counter loop. The vendored copy chains the hash through a
-# returned accumulator, which keeps all 20000 hashes in the bytecode. See the
-# comment in that file.
-#
-# Usage: core/vm/runtime/testdata/evm-bench/gen.sh
-set -euo pipefail
-
-TD="$(cd "$(dirname "$0")/.." && pwd)" # .../core/vm/runtime/testdata
-HERE="$(cd "$(dirname "$0")" && pwd)" # .../testdata/evm-bench
-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"
-cp "$HERE/TenThousandHashes.sol" "$B/ten-thousand-hashes/TenThousandHashes.sol"
-
-# 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
diff --git a/core/vm/runtime/testdata/snailtracer.hex b/core/vm/runtime/testdata/snailtracer.hex
deleted file mode 100644
index 9546187294..0000000000
--- a/core/vm/runtime/testdata/snailtracer.hex
+++ /dev/null
@@ -1 +0,0 @@
-608060405234801561001057600080fd5b506004361061004c5760003560e01c806330627b7c1461005157806375ac892a14610085578063784f13661461011d578063c294360114610146575b600080fd5b610059610163565b604080516001600160f81b03199485168152928416602084015292168183015290519081900360600190f35b6100a86004803603604081101561009b57600080fd5b50803590602001356102d1565b6040805160208082528351818301528351919283929083019185019080838360005b838110156100e25781810151838201526020016100ca565b50505050905090810190601f16801561010f5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100596004803603606081101561013357600080fd5b508035906020810135906040013561055b565b6100a86004803603602081101561015c57600080fd5b5035610590565b6000806000610176610400610300610834565b60405180606001604052806001546000546207d5dc028161019357fe5b058152600060208083018290526040928301919091528251600b81905583820151600c81905593830151600d819055835160608082018652928152808401959095528484015282519081018352600654815260075491810191909152600854918101919091526102259161021c916102139161020e91612ef7565b612f64565b6207d5dc612feb565b620f424061301e565b8051600e556020810151600f55604001516010556102416142dd565b61025a816102556102006101806008613064565b613212565b90506102708161025561014561021c6008613064565b905061028481610255610258806008613064565b905061029a8161025561020a61020c6008613064565b90506102a781600461301e565b90506102b1613250565b8051602082015160409092015160f891821b9692821b9550901b92509050565b606060005b6000548112156104c95760006102ed828686613064565b90506002816000015160f81b90808054603f811680603e811461032a576002830184556001831661031c578192505b600160028404019350610342565b600084815260209081902060ff198516905560419094555b505050600190038154600116156103685790600052602060002090602091828204019190065b909190919091601f036101000a81548160ff02191690600160f81b840402179055506002816020015160f81b90808054603f811680603e81146103c557600283018455600183166103b7578192505b6001600284040193506103dd565b600084815260209081902060ff198516905560419094555b505050600190038154600116156104035790600052602060002090602091828204019190065b909190919091601f036101000a81548160ff02191690600160f81b840402179055506002816040015160f81b90808054603f811680603e81146104605760028301845560018316610452578192505b600160028404019350610478565b600084815260209081902060ff198516905560419094555b5050506001900381546001161561049e5790600052602060002090602091828204019190065b815460ff601f929092036101000a9182021916600160f81b90930402919091179055506001016102d6565b506002805460408051602060018416156101000260001901909316849004601f8101849004840282018401909252818152929183018282801561054d5780601f106105225761010080835404028352916020019161054d565b820191906000526020600020905b81548152906001019060200180831161053057829003601f168201915b505050505090505b92915050565b60008060008061056c878787613064565b8051602082015160409092015160f891821b9a92821b9950901b9650945050505050565b600154606090600019015b600081126107a35760005b6000548112156107995760006105bd828487613064565b90506002816000015160f81b90808054603f811680603e81146105fa57600283018455600183166105ec578192505b600160028404019350610612565b600084815260209081902060ff198516905560419094555b505050600190038154600116156106385790600052602060002090602091828204019190065b909190919091601f036101000a81548160ff02191690600160f81b840402179055506002816020015160f81b90808054603f811680603e81146106955760028301845560018316610687578192505b6001600284040193506106ad565b600084815260209081902060ff198516905560419094555b505050600190038154600116156106d35790600052602060002090602091828204019190065b909190919091601f036101000a81548160ff02191690600160f81b840402179055506002816040015160f81b90808054603f811680603e81146107305760028301845560018316610722578192505b600160028404019350610748565b600084815260209081902060ff198516905560419094555b5050506001900381546001161561076e5790600052602060002090602091828204019190065b815460ff601f929092036101000a9182021916600160f81b90930402919091179055506001016105a6565b506000190161059b565b506002805460408051602060018416156101000260001901909316849004601f810184900484028201840190925281815292918301828280156108275780601f106107fc57610100808354040283529160200191610827565b820191906000526020600020905b81548152906001019060200180831161080a57829003601f168201915b505050505090505b919050565b8160008190555080600181905550604051806080016040528060405180606001604052806302faf08081526020016303197500815260200163119e7f8081525081526020016108a460405180606001604052806000815260200161a673198152602001620f423f19815250612f64565b815260006020808301829052604092830182905283518051600355808201516004558301516005558381015180516006559081015160075582015160085582820151600955606092830151600a805460ff1916911515919091179055815192830190915260015490548291906207d5dc028161091c57fe5b058152600060208083018290526040928301919091528251600b81905583820151600c81905593830151600d819055835160608082018652928152808401959095528484015282519081018352600654815260075491810191909152600854918101919091526109979161021c916102139161020e91612ef7565b8051600e55602080820151600f55604091820151601055815160a08101835264174876e8008152825160608082018552641748862a40825263026e8f00828501526304dd1e008286015282840191825284518082018652600080825281860181905281870181905284870191825286518084018852620b71b081526203d09081880181905281890152928501928352608085018181526011805460018082018355919093528651600b9093027f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c688101938455955180517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c69880155808901517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6a8801558901517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6b870155925180517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6c870155808801517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6d8701558801517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6e860155925180517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6f860155958601517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c7085015594909501517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c71830155517f31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c72909101805492949192909160ff1990911690836002811115610c1057fe5b0217905550505060116040518060a0016040528064174876e8008152602001604051806060016040528064174290493f19815260200163026e8f0081526020016304dd1e008152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806203d09081526020016203d0908152602001620b71b0815250815260200160006002811115610cb657fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff1990921691908490811115610d5857fe5b0217905550505060116040518060a0016040528064174876e800815260200160405180606001604052806302faf080815260200163026e8f00815260200164174876e800815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620b71b08152602001620b71b08152602001620b71b0815250815260200160006002811115610dfd57fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff1990921691908490811115610e9f57fe5b0217905550505060116040518060a0016040528064174876e800815260200160405180606001604052806302faf080815260200163026e8f00815260200164173e54e97f1981525081526020016040518060600160405280600081526020016000815260200160008152508152602001604051806060016040528060008152602001600081526020016000815250815260200160006002811115610f3f57fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff1990921691908490811115610fe157fe5b0217905550505060116040518060a0016040528064174876e800815260200160405180606001604052806302faf080815260200164174876e80081526020016304dd1e00815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620b71b08152602001620b71b08152602001620b71b081525081526020016000600281111561108657fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff199092169190849081111561112857fe5b0217905550505060116040518060a0016040528064174876e800815260200160405180606001604052806302faf080815260200164174399c9ff1981526020016304dd1e00815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620b71b08152602001620b71b08152602001620b71b08152508152602001600060028111156111ce57fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff199092169190849081111561127057fe5b0217905550505060116040518060a0016040528062fbc5208152602001604051806060016040528063019bfcc0815260200162fbc52081526020016302cd29c0815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561131157fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff19909216919084908111156113b357fe5b0217905550505060116040518060a001604052806323c34600815260200160405180606001604052806302faf080815260200163289c455081526020016304dd1e008152508152602001604051806060016040528062b71b00815260200162b71b00815260200162b71b00815250815260200160405180606001604052806000815260200160008152602001600081525081526020016000600281111561145657fe5b905281546001818101845560009384526020938490208351600b90930201918255838301518051838301558085015160028085019190915560409182015160038501558185015180516004860155808701516005860155820151600685015560608501518051600786015595860151600885015594015160098301556080830151600a83018054949593949193909260ff19909216919084908111156114f857fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f208152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016305a1f4a081525081526020016040518060600160405280630459e44081526020016302f34f6081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561160c57fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff19909216919084908111156116fd57fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f20815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001600081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016305a1f4a08152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561180e57fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff19909216919084908111156118ff57fe5b0217905550505060126040518060e001604052806040518060600160405280630555a9608152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e44081526020016302f34f6081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016305a1f4a08152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e58815250815260200160016002811115611a1357fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff1990921691908490811115611b0457fe5b0217905550505060126040518060e001604052806040518060600160405280630555a960815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016305a1f4a081525081526020016040518060600160405280630459e4408152602001600081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e58815250815260200160016002811115611c1557fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff1990921691908490811115611d0657fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f208152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e44081526020016302f34f6081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016303aa6a608152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e58815250815260200160016002811115611e1a57fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff1990921691908490811115611f0b57fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f20815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016303aa6a6081525081526020016040518060600160405280630459e4408152602001600081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561201c57fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff199092169190849081111561210d57fe5b0217905550505060126040518060e001604052806040518060600160405280630555a9608152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016303aa6a6081525081526020016040518060600160405280630459e44081526020016302f34f6081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561222157fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff199092169190849081111561231257fe5b0217905550505060126040518060e001604052806040518060600160405280630555a960815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001600081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016303aa6a608152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561242357fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff199092169190849081111561251457fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f208152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016303aa6a6081525081526020016040518060600160405280630555a9608152602001630188c2e081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561262857fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff199092169190849081111561271957fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f208152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630555a9608152602001630188c2e081526020016304a62f8081525081526020016040518060600160405280630459e4408152602001630188c2e081526020016305a1f4a08152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e5881525081526020016001600281111561282d57fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff199092169190849081111561291e57fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f20815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630555a960815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016303aa6a608152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e58815250815260200160016002811115612a3257fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff1990921691908490811115612b2357fe5b0217905550505060126040518060e00160405280604051806060016040528063035e1f20815260200163016a8c8081526020016304a62f8081525081526020016040518060600160405280630459e440815260200163016a8c8081526020016305a1f4a081525081526020016040518060600160405280630555a960815260200163016a8c8081526020016304a62f808152508152602001604051806060016040528060008152602001600081526020016000815250815260200160405180606001604052806000815260200160008152602001600081525081526020016040518060600160405280620f3e588152602001620f3e588152602001620f3e58815250815260200160016002811115612c3757fe5b905281546001818101845560009384526020938490208351805160139094029091019283558085015183830155604090810151600280850191909155858501518051600386015580870151600486015582015160058501558185015180516006860155808701516007860155820151600885015560608501518051600986015580870151600a860155820151600b85015560808501518051600c86015580870151600d860155820151600e85015560a08501518051600f860155958601516010850155940151601183015560c0830151601283018054949593949193909260ff1990921691908490811115612d2857fe5b0217905550505060005b601254811015612ef257600060128281548110612d4b57fe5b600091825260209182902060408051610140810182526013909302909101805460e08401908152600182015461010085015260028083015461012086015290845282516060818101855260038401548252600484015482880152600584015482860152858701919091528351808201855260068401548152600784015481880152600884015481860152858501528351808201855260098401548152600a84015481880152600b840154818601528186015283518082018552600c8401548152600d84015481880152600e84015481860152608086015283519081018452600f830154815260108301549581019590955260118201549285019290925260a0830193909352601283015491929160c084019160ff90911690811115612e6c57fe5b6002811115612e7757fe5b815250509050612eac61020e612e95836020015184600001516132cd565b612ea7846040015185600001516132cd565b612ef7565b60128381548110612eb957fe5b60009182526020918290208351600960139093029091019182015590820151600a820155604090910151600b9091015550600101612d32565b505050565b612eff6142dd565b604051806060016040528083602001518560400151028460400151866020015102038152602001836040015185600001510284600001518660400151020381526020018360000151856020015102846020015186600001510203815250905092915050565b612f6c6142dd565b604082015160208301518351600092612f9292918002918002919091019080020161330c565b90506040518060600160405280828560000151620f42400281612fb157fe5b058152602001828560200151620f42400281612fc957fe5b058152602001828560400151620f42400281612fe157fe5b0590529392505050565b612ff36142dd565b5060408051606081018252835183028152602080850151840290820152928101519091029082015290565b6130266142dd565b60405180606001604052808385600001518161303e57fe5b0581526020018385602001518161305157fe5b05815260200183856040015181612fe157fe5b61306c6142dd565b6000546013805463ffffffff1916918502860163ffffffff169190911790556130936142dd565b905060005b828112156131f157600061317261314c61021c613115600b60405180606001604052908160008201548152602001600182015481526020016002820154815250506207a1206000546207a1206130ec613343565b63ffffffff16816130f957fe5b0663ffffffff168d620f424002018161310e57fe5b0503612feb565b60408051606081018252600e548152600f5460208201526010549181019190915260015461025591906207a12090816130ec613343565b604080516060810182526006548152600754602082015260085491810191909152613212565b6040805160e081019091526003546080820190815260045460a083015260055460c083015291925060009181906131ae9061025586608c612feb565b81526020016131bc84612f64565b815260006020820181905260409091015290506131e5846102556131df8461336c565b8861301e565b93505050600101613098565b5061320861021c61320183613753565b60ff612feb565b90505b9392505050565b61321a6142dd565b50604080516060810182528251845101815260208084015181860151019082015291810151928101519092019181019190915290565b60008080556001819055613266906002906142fe565b60006003819055600481905560058190556006819055600781905560088190556009819055600a805460ff19169055600b819055600c819055600d819055600e819055600f81905560108190556132bf90601190614345565b6132cb60126000614366565b565b6132d56142dd565b5060408051606081018252825184510381526020808401518186015103908201528282015184830151039181019190915292915050565b80600260018201055b8181121561333d5780915060028182858161332c57fe5b05018161333557fe5b059050613315565b50919050565b6013805463ffffffff19811663ffffffff9182166341c64e6d0261303901821617918290551690565b6133746142dd565b600a826040015113156133a657604051806060016040528060008152602001600081526020016000815250905061082f565b60008060006133b48561379f565b91945092509050826133e857604051806060016040528060008152602001600081526020016000815250935050505061082f565b6133f0614387565b6133f86143c7565b6134006142dd565b6134086142dd565b600086600181111561341657fe5b1415613505576011858154811061342957fe5b60009182526020918290206040805160a081018252600b90930290910180548352815160608082018452600183015482526002808401548388015260038401548386015285870192909252835180820185526004840154815260058401548188015260068401548186015285850152835180820185526007840154815260088401549681019690965260098301549386019390935291830193909352600a830154919291608084019160ff909116908111156134e157fe5b60028111156134ec57fe5b8152505093508360600151915083604001519050613653565b6012858154811061351257fe5b600091825260209182902060408051610140810182526013909302909101805460e08401908152600182015461010085015260028083015461012086015290845282516060818101855260038401548252600484015482880152600584015482860152858701919091528351808201855260068401548152600784015481880152600884015481860152858501528351808201855260098401548152600a84015481880152600b840154818601528186015283518082018552600c8401548152600d84015481880152600e84015481860152608086015283519081018452600f830154815260108301549581019590955260118201549285019290925260a0830193909352601283015491929160c084019160ff9091169081111561363357fe5b600281111561363e57fe5b8152505092508260a001519150826080015190505b6040820151600190811215613669575060408201515b808360200151131561367c575060208201515b808360400151131561368f575060408201515b60408a01805160010190819052600512156136f75780620f42406136b1613343565b63ffffffff16816136be57fe5b0663ffffffff1612156136e8576136e16136db84620f4240612feb565b8261301e565b92506136f7565b50965061082f95505050505050565b6136ff6142dd565b600088600181111561370d57fe5b14156137255761371e8b878b613a57565b9050613733565b6137308b868b613aec565b90505b6137448361025561021c8785613baa565b9b9a5050505050505050505050565b61375b6142dd565b60405180606001604052806137738460000151613be8565b81526020016137858460200151613be8565b81526020016137978460400151613be8565b905292915050565b60008080808080805b6011548110156138c2576000613890601183815481106137c457fe5b60009182526020918290206040805160a081018252600b90930290910180548352815160608082018452600183015482526002808401548388015260038401548386015285870192909252835180820185526004840154815260058401548188015260068401548186015285850152835180820185526007840154815260088401549681019690965260098301549386019390935291830193909352600a830154919291608084019160ff9091169081111561387c57fe5b600281111561388757fe5b9052508a613c13565b90506000811380156138a957508415806138a957508481125b156138b957809450600093508192505b506001016137a8565b5060005b601254811015613a49576000613a17601283815481106138e257fe5b600091825260209182902060408051610140810182526013909302909101805460e08401908152600182015461010085015260028083015461012086015290845282516060818101855260038401548252600484015482880152600584015482860152858701919091528351808201855260068401548152600784015481880152600884015481860152858501528351808201855260098401548152600a84015481880152600b840154818601528186015283518082018552600c8401548152600d84015481880152600e84015481860152608086015283519081018452600f830154815260108301549581019590955260118201549285019290925260a0830193909352601283015491929160c084019160ff90911690811115613a0357fe5b6002811115613a0e57fe5b9052508a613cbb565b9050600081138015613a305750841580613a3057508481125b15613a4057809450600193508192505b506001016138c6565b509196909550909350915050565b613a5f6142dd565b6000613a7a856000015161025561021c886020015187612feb565b90506000613a8f61020e8387602001516132cd565b9050600085608001516002811115613aa357fe5b1415613ae1576000613ab9828860200151613e0c565b12613acd57613aca81600019612feb565b90505b613ad8868383613e31565b9250505061320b565b613ad8868383613fc1565b613af46142dd565b6000613b0f856000015161025561021c886020015187612feb565b6060860151909150620a2c2a9015613b2757506216e3605b6000620f4240613b3f87606001518960200151613e0c565b81613b4657fe5b05905060008112613b55576000035b64e8d4a5100081800281038380020281900590036000811215613b8c57613b8188858960600151613fc1565b94505050505061320b565b613b9e88858960600151868686614039565b98975050505050505050565b613bb26142dd565b50604080516060810182528251845102815260208084015181860151029082015291810151928101519092029181019190915290565b600080821215613bfa5750600061082f565b620f4240821315613c0f5750620f424061082f565b5090565b600080613c28846020015184600001516132cd565b90506000620f4240613c3e838660200151613e0c565b81613c4557fe5b865191900591506000908002613c5b8480613e0c565b838402030190506000811215613c775760009350505050610555565b613c808161330c565b90506103e88183031315613c9957900391506105559050565b6103e88183011315613caf570191506105559050565b50600095945050505050565b600080613cd0846020015185600001516132cd565b90506000613ce6856040015186600001516132cd565b90506000613cf8856020015183612ef7565b90506000620f4240613d0a8584613e0c565b81613d1157fe5b0590506103e71981138015613d2757506103e881125b15613d39576000945050505050610555565b85518751600091613d49916132cd565b9050600082613d588386613e0c565b81613d5f57fe5b0590506000811280613d735750620f424081135b15613d875760009650505050505050610555565b6000613d938388612ef7565b9050600084613da68b6020015184613e0c565b81613dad57fe5b0590506000811280613dc35750620f4240818401135b15613dd957600098505050505050505050610555565b600085613de68985613e0c565b81613ded57fe5b0590506103e88112156137445760009950505050505050505050610555565b6040808201519083015160208084015190850151845186510291020191020192915050565b613e396142dd565b6000620f424080613e48613343565b63ffffffff1681613e5557fe5b0663ffffffff16625fdfb00281613e6857fe5b0590506000620f4240613e79613343565b63ffffffff1681613e8657fe5b0663ffffffff1690506000613e9a8261330c565b6103e8029050613ea86142dd565b620186a0613eb98760000151614216565b1315613ee657604051806060016040528060008152602001620f4240815260200160008152509050613f09565b6040518060600160405280620f4240815260200160008152602001600081525090505b613f1661020e8288612ef7565b90506000613f2761020e8884612ef7565b9050613f7f61020e613f64613f5285620f424088613f448c61422e565b0281613f4c57fe5b05612feb565b61025585620f424089613f448d61424e565b6102558a613f7689620f42400361330c565b6103e802612feb565b9150613fb460405180608001604052808a81526020018481526020018b6040015181526020018b60600151151581525061336c565b9998505050505050505050565b613fc96142dd565b6000613ffb61020e8660200151613ff686620f4240613fec898c60200151613e0c565b60020281613f4c57fe5b6132cd565b90506140306040518060800160405280868152602001838152602001876040015181526020018760600151151581525061336c565b95945050505050565b6140416142dd565b60608701516000199015614053575060015b600061408961020e61021c61406c8c602001518a612feb565b613ff68b6140798a61330c565b620f42408c8e0205018802612feb565b60608a0151909150620f42408601906140ba57620f42406140aa838a613e0c565b816140b157fe5b05620f42400390505b60408a0151619c406c0c9f2c9cd04674edea40000000620ea6008480028502850285020205019060021261415e5761412a61411f60405180608001604052808d81526020018681526020018e6040015181526020018e6060015115151581525061336c565b82620f424003612feb565b92506141448361025561413e8e8e8e613fc1565b84612feb565b925061415383620f424061301e565b94505050505061420c565b600281056203d09001620f4240614173613343565b63ffffffff168161418057fe5b0663ffffffff1612156141b2576141536141a461419e8d8d8d613fc1565b83612feb565b600283056203d0900161301e565b6142056141f76141ec60405180608001604052808e81526020018781526020018f6040015181526020018f6060015115151581525061336c565b83620f424003612feb565b60028305620b71b00361301e565b9450505050505b9695505050505050565b60008082131561422757508061082f565b5060000390565b60008061423a8361424e565b905061320b81820264e8d4a510000361330c565b60005b600082121561426757625fdfb082019150614251565b5b625fdfb0821261427f57625fdfb082039150614268565b6001828160025b818313156142d457818385028161429957fe5b0585019450620f4240808788860202816142af57fe5b05816142b757fe5b600095909503940592506001810181029190910290600201614286565b50505050919050565b60405180606001604052806000815260200160008152602001600081525090565b50805460018160011615610100020316600290046000825580601f106143245750614342565b601f0160209004906000526020600020908101906143429190614401565b50565b50805460008255600b02906000526020600020908101906143429190614416565b50805460008255601302906000526020600020908101906143429190614475565b6040518060a00160405280600081526020016143a16142dd565b81526020016143ae6142dd565b81526020016143bb6142dd565b81526020016000905290565b6040518060e001604052806143da6142dd565b81526020016143e76142dd565b81526020016143f46142dd565b81526020016143a16142dd565b5b80821115613c0f5760008155600101614402565b5b80821115613c0f57600080825560018201819055600282018190556003820181905560048201819055600582018190556006820181905560078201819055600882018190556009820155600a8101805460ff19169055600b01614417565b5b80821115613c0f576000808255600182018190556002820181905560038201819055600482018190556005820181905560068201819055600782018190556008820181905560098201819055600a8201819055600b8201819055600c8201819055600d8201819055600e8201819055600f820181905560108201819055601182015560128101805460ff1916905560130161447656fea2646970667358221220037024f5647853879c58fbcc61ac3616455f6f731cc6e84f91eb5a3b4e06c00464736f6c63430007060033
\ No newline at end of file
diff --git a/core/vm/runtime/testdata/tenthousandhashes.hex b/core/vm/runtime/testdata/tenthousandhashes.hex
deleted file mode 100644
index f2903b13d9..0000000000
--- a/core/vm/runtime/testdata/tenthousandhashes.hex
+++ /dev/null
@@ -1 +0,0 @@
-6080604052348015600f57600080fd5b506004361060285760003560e01c806330627b7c14602d575b600080fd5b60336045565b60405190815260200160405180910390f35b6000805b614e20811015608e57604080516020810184905290810182905260600160405160208183030381529060405280519060200120915080806087906092565b9150506049565b5090565b60006001820160b157634e487b7160e01b600052601160045260246000fd5b506001019056fea26469706673582212207f65e4c71aef311b81ff8b065cdd78f5be33f5dc80d3b2318e118075e33697f164736f6c63430008110033
\ No newline at end of file
From af039e9a1696e37c5915f5d2d08d64ec256a9e1f Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Mon, 6 Jul 2026 18:04:43 -0500
Subject: [PATCH 21/23] core/vm, core/vm/gen: mimic rlpgen's pattern and apply
more cleanup
---
core/vm/gen/gen.go | 948 ++++++++++++++++++++++++++++++++
core/vm/gen/gen_test.go | 42 ++
core/vm/gen/main.go | 925 +------------------------------
core/vm/interpreter.go | 2 +-
core/vm/interpreter_gen.go | 4 +-
core/vm/interpreter_gen_test.go | 40 +-
6 files changed, 1005 insertions(+), 956 deletions(-)
create mode 100644 core/vm/gen/gen.go
create mode 100644 core/vm/gen/gen_test.go
diff --git a/core/vm/gen/gen.go b/core/vm/gen/gen.go
new file mode 100644
index 0000000000..297656669d
--- /dev/null
+++ b/core/vm/gen/gen.go
@@ -0,0 +1,948 @@
+// 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 .
+
+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"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+// inlineOps maps an opcode byte to the handler whose body is spliced inline
+// for that opcode. These are the hot, fork-stable opcodes with no dynamic gas.
+// The value is usually an opXxx handler, but PUSH3-PUSH32 and DUP1-DUP16 are
+// factory-built (one shared makePush / makeDup each), so their value is the
+// factory name and emitInlineOp splices the factory body with the per-opcode size.
+// Opcodes not listed here (or in directCallOps) fall through to the default case,
+// which dispatches via the per-fork table.
+var inlineOps = func() map[byte]string {
+ m := 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",
+ }
+ for code := 0x62; code <= 0x7f; code++ { // PUSH3-PUSH32
+ m[byte(code)] = "makePush"
+ }
+ for code := 0x80; code <= 0x8f; code++ { // DUP1-DUP16
+ m[byte(code)] = "makeDup"
+ }
+ for code := 0x90; code <= 0x9f; code++ { // SWAP1-SWAP16
+ m[byte(code)] = fmt.Sprintf("opSwap%d", code-0x8f)
+ }
+ return m
+}()
+
+// directCallOps lists the 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.
+var directCallOps = map[byte][3]string{
+ 0x20: {"opKeccak256", "gasKeccak256", "memoryKeccak256"},
+ 0x51: {"opMload", "gasMLoad", "memoryMLoad"},
+ 0x52: {"opMstore", "gasMStore", "memoryMStore"},
+ 0x53: {"opMstore8", "gasMStore8", "memoryMStore8"},
+}
+
+// opSpec holds the per-opcode constants the generator emits (gas, stack bounds, intro fork), derived from the per-fork tables.
+type opSpec struct {
+ defined bool
+ name string
+ fork string
+ constGas uint64
+ minStack int
+ maxStack int
+}
+
+type generator struct {
+ fset *token.FileSet
+ opcodeHandlers map[string]*ast.FuncDecl
+ stackHelpers map[string]*ast.FuncDecl
+ gasHelpers map[string]*ast.FuncDecl
+ specs [256]opSpec
+ buf *bytes.Buffer
+}
+
+// p is the writer of the generated file. Every line of output is appended
+// to g.buf through it.
+func (g *generator) p(format string, args ...any) {
+ format = strings.TrimRight(strings.TrimPrefix(format, "\n"), " \t")
+ fmt.Fprintf(g.buf, format, args...)
+}
+
+// parseHandlers parses instructions.go, eips.go, stack.go, gascosts.go and
+// interpreter.go. It returns the top-level opXxx handlers by name, the
+// //gen:inline *Stack helper methods, and the gas/memory helper functions
+// (chargeRegularOnly, computeMemorySize, chargeDynamicGas) whose bodies are
+// spliced into the generated dispatch (all by name).
+func parseHandlers(vmDir string) (fset *token.FileSet, opcodeHandlers, stackHelpers, gasHelpers map[string]*ast.FuncDecl) {
+ fset = token.NewFileSet()
+ opcodeHandlers = map[string]*ast.FuncDecl{}
+ stackHelpers = map[string]*ast.FuncDecl{}
+ gasHelpers = map[string]*ast.FuncDecl{}
+ for _, name := range []string{"instructions.go", "eips.go", "stack.go", "gascosts.go", "interpreter.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.Body == nil {
+ continue
+ }
+ switch {
+ case fn.Name.Name == "chargeRegularOnly" || fn.Name.Name == "computeMemorySize" || fn.Name.Name == "chargeDynamicGas" || fn.Name.Name == "chargeVerkleCodeChunkGas": // spliced gas/memory helpers
+ gasHelpers[fn.Name.Name] = fn
+ case fn.Recv == nil: // top-level opXxx handler
+ opcodeHandlers[fn.Name.Name] = fn
+ case methodReceiver(fn) == "Stack" && hasInlineMarker(fn): // (s *Stack) helper tagged //gen:inline
+ stackHelpers[fn.Name.Name] = fn
+ }
+ }
+ }
+ return fset, opcodeHandlers, stackHelpers, gasHelpers
+}
+
+// methodReceiver returns the receiver type name of a pointer-receiver method
+// (e.g. "Stack" for (s *Stack)), or "" if fn is not such a method.
+func methodReceiver(fn *ast.FuncDecl) string {
+ if fn.Recv == nil || len(fn.Recv.List) != 1 {
+ return ""
+ }
+ star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
+ if !ok {
+ return ""
+ }
+ id, ok := star.X.(*ast.Ident)
+ if !ok {
+ return ""
+ }
+ return id.Name
+}
+
+// hasInlineMarker reports whether fn is tagged //gen:inline, which marks a stack
+// helper for splicing into the generated dispatch.
+func hasInlineMarker(fn *ast.FuncDecl) bool {
+ if fn.Doc == nil {
+ return false
+ }
+ for _, c := range fn.Doc.List {
+ if c.Text == "//gen:inline" {
+ return true
+ }
+ }
+ return false
+}
+
+var opcodeReturnRe = regexp.MustCompile(`^(\s*)return\s+([^,]+),\s*(.+)$`)
+
+// spliceOpcodeBody returns a named handler's body, rewritten so it can be spliced
+// into the dispatch loop (see rewriteOpcodeReturns). The caller emits it with p.
+func (g *generator) spliceOpcodeBody(handler string) string {
+ fn := g.opcodeHandlers[handler]
+ if fn == nil {
+ fatalf("no handler %q to inline", handler)
+ }
+ return g.rewriteOpcodeReturns(g.inlineStackHelpers(fn.Body.List, nil))
+}
+
+// spliceOpcodeFactoryBody splices the body of the executionFunc closure that a make*
+// factory returns, substituting the factory's parameters with the per-opcode
+// constants in args (positional, matching the factory signature). This lets
+// closure-built handlers (makePush, makeDup) be derived from their single
+// definition rather than restated in the generator. The caller emits the
+// result with p.
+func (g *generator) spliceOpcodeFactoryBody(factory string, args ...int) string {
+ fn := g.opcodeHandlers[factory]
+ if fn == nil {
+ fatalf("no factory %q to inline", factory)
+ }
+ lit := factoryClosure(factory, fn)
+ // Bind the factory parameters to the per-opcode constants, then inline.
+ names := paramNames(fn)
+ if len(names) != len(args) {
+ fatalf("factory %q takes %d params, got %d args", factory, len(names), len(args))
+ }
+ params := map[string]int{}
+ for i, nm := range names {
+ params[nm] = args[i]
+ }
+ return g.rewriteOpcodeReturns(g.inlineStackHelpers(lit.Body.List, params))
+}
+
+// factoryClosure returns the executionFunc literal that a make* factory's body
+// is a single `return func(...) {...}` of.
+func factoryClosure(name string, fn *ast.FuncDecl) *ast.FuncLit {
+ if len(fn.Body.List) != 1 {
+ fatalf("factory %q body is not a single return", name)
+ }
+ ret, ok := fn.Body.List[0].(*ast.ReturnStmt)
+ if !ok || len(ret.Results) != 1 {
+ fatalf("factory %q does not return a single value", name)
+ }
+ lit, ok := ret.Results[0].(*ast.FuncLit)
+ if !ok {
+ fatalf("factory %q does not return a func literal", name)
+ }
+ return lit
+}
+
+// renderAst converts AST statements back to formatted Go source text, the
+// inverse of parsing. It uses the generator's fileset and emits nothing itself
+// (the caller passes the result to p).
+func (g *generator) renderAst(stmts []ast.Stmt) string {
+ var raw bytes.Buffer
+ cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8}
+ for _, stmt := range stmts {
+ if err := cfg.Fprint(&raw, g.fset, stmt); err != nil {
+ fatalf("print stmt: %v", err)
+ }
+ raw.WriteByte('\n')
+ }
+ return raw.String()
+}
+
+// rewriteOpcodeReturns rewrites a printed handler body so it runs inside 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. (Stack helpers were already
+// inlined by inlineStackHelpers before the body was printed.)
+func (g *generator) rewriteOpcodeReturns(src string) string {
+ src = strings.ReplaceAll(src, "*pc", "pc")
+
+ var out bytes.Buffer
+ for _, line := range strings.Split(src, "\n") {
+ if m := opcodeReturnRe.FindStringSubmatch(line); m != nil {
+ indent, r0, r1 := m[1], strings.TrimSpace(m[2]), strings.TrimSpace(m[3])
+ 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()
+}
+
+var gasReturnRe = regexp.MustCompile(`^(\s*)return\s+(\S.*)$`)
+
+// rewriteGasReturns rewrites a spliced charge body so it runs as a gas step in
+// the dispatch loop: a `return ` becomes the out-of-gas break, and the
+// trailing `return nil` (success) is dropped so the opcode continues.
+func (g *generator) rewriteGasReturns(src string) string {
+ var out bytes.Buffer
+ for _, line := range strings.Split(src, "\n") {
+ if m := gasReturnRe.FindStringSubmatch(line); m != nil {
+ indent, val := m[1], strings.TrimSpace(m[2])
+ if val == "nil" {
+ continue // success: fall through to the rest of the op
+ }
+ out.WriteString(indent + "res, err = nil, " + val + "\n")
+ out.WriteString(indent + "break mainLoop\n")
+ continue
+ }
+ out.WriteString(line + "\n")
+ }
+ return out.String()
+}
+
+// rewriteStepReturns rewrites a spliced gas-step body's (value, error) returns so
+// it runs inline in the dispatch loop: a non-nil error becomes the out-of-gas
+// break; on success the value is assigned to target (or dropped when target is
+// empty) and the op falls through.
+func (g *generator) rewriteStepReturns(src, target string) string {
+ var out bytes.Buffer
+ for _, line := range strings.Split(src, "\n") {
+ if m := opcodeReturnRe.FindStringSubmatch(line); m != nil {
+ indent, val, errVal := m[1], strings.TrimSpace(m[2]), strings.TrimSpace(m[3])
+ if errVal == "nil" {
+ if target != "" {
+ out.WriteString(indent + target + " = " + val + "\n")
+ }
+ continue
+ }
+ out.WriteString(indent + "res, err = nil, " + errVal + "\n")
+ out.WriteString(indent + "break mainLoop\n")
+ continue
+ }
+ out.WriteString(line + "\n")
+ }
+ return out.String()
+}
+
+// stackCall is a matched call to a tagged helper.
+type stackCall struct {
+ helper string // helper method name
+ lhs []ast.Expr // assignment targets, nil for a void call like dup
+ tok token.Token // the assignment token, := or =
+ args []ast.Expr // call arguments (only dup has one)
+}
+
+// matchStackHelper matches a statement that is a single must-expand helper call,
+// in one of the two normalized forms: an assignment `lhs... := scope.Stack.H(args)`
+// or a bare `scope.Stack.H(args)`.
+func (g *generator) matchStackHelper(stmt ast.Stmt) (stackCall, bool) {
+ switch s := stmt.(type) {
+ case *ast.AssignStmt:
+ if len(s.Rhs) == 1 {
+ if h, args, ok := g.stackHelperCall(s.Rhs[0]); ok {
+ return stackCall{helper: h, lhs: s.Lhs, tok: s.Tok, args: args}, true
+ }
+ }
+ case *ast.ExprStmt:
+ if h, args, ok := g.stackHelperCall(s.X); ok {
+ return stackCall{helper: h, args: args}, true
+ }
+ }
+ return stackCall{}, false
+}
+
+// stackHelperCall unwraps scope.Stack.H(args) where H is a must-expand helper.
+func (g *generator) stackHelperCall(e ast.Expr) (helper string, args []ast.Expr, ok bool) {
+ call, isCall := e.(*ast.CallExpr)
+ if !isCall {
+ return "", nil, false
+ }
+ sel, isSel := call.Fun.(*ast.SelectorExpr) // .H
+ if !isSel || g.stackHelpers[sel.Sel.Name] == nil || !isStackExpr(sel.X) {
+ return "", nil, false
+ }
+ return sel.Sel.Name, call.Args, true
+}
+
+// isStackExpr reports whether e is the stack receiver: the `stack` local or
+// scope.Stack.
+func isStackExpr(e ast.Expr) bool {
+ switch x := e.(type) {
+ case *ast.Ident:
+ return x.Name == "stack"
+ case *ast.SelectorExpr:
+ return x.Sel.Name == "Stack"
+ }
+ return false
+}
+
+// inlineStackHelpers renders a handler body to source, inlining every must-expand
+// helper call and printing other statements unchanged. params maps the factory
+// parameters (makePush/makeDup) to their per-opcode constants.
+func (g *generator) inlineStackHelpers(stmts []ast.Stmt, params map[string]int) string {
+ var out strings.Builder
+ // Walk the handler body one statement at a time. A statement that is a
+ // tagged stack-helper call gets the helper's body spliced in: the generated
+ // dispatch is past Go's big-function inline budget, so the call would not be
+ // inlined otherwise. Every other statement is printed as written.
+ for _, stmt := range stmts {
+ if call, ok := g.matchStackHelper(stmt); ok {
+ // e.g. `x, y := scope.Stack.pop1Peek1()` becomes the body of pop1Peek1.
+ out.WriteString(g.inlineStackHelper(call, params))
+ } else {
+ // A plain statement: print it verbatim, then fill in any makePush or
+ // makeDup factory params with this opcode's constants.
+ out.WriteString(substParams(g.renderAst([]ast.Stmt{stmt}), params))
+ }
+ }
+ return out.String()
+}
+
+// substParams replaces each factory parameter with its constant. It runs only
+// on printed non-helper statements and on helper arguments, never on a helper
+// expansion, so it cannot touch a field like stack.size. The parameter names do
+// not textually overlap, so map order does not affect the result.
+func substParams(src string, params map[string]int) string {
+ for name, val := range params {
+ src = regexp.MustCompile(`\b`+name+`\b`).ReplaceAllString(src, fmt.Sprint(val))
+ }
+ return src
+}
+
+// inlineStackHelper expands one helper call to its stack.go body. The single
+// rule: the helper is straight-line statements then an optional final return
+// whose result count matches the call's targets. Anything else is not in
+// inlinable form and is a hard error (the shape post-condition). The receiver
+// maps to the loop's `stack` local and each parameter to its call argument.
+func (g *generator) inlineStackHelper(call stackCall, params map[string]int) string {
+ fn := g.stackHelpers[call.helper]
+ if fn == nil {
+ fatalf("no stack helper %q to inline", call.helper)
+ }
+ // Peel an optional trailing return off the body.
+ body := fn.Body.List
+ var ret *ast.ReturnStmt
+ if n := len(body); n > 0 {
+ if r, isRet := body[n-1].(*ast.ReturnStmt); isRet {
+ ret, body = r, body[:n-1]
+ }
+ }
+ results := 0
+ if ret != nil {
+ results = len(ret.Results)
+ }
+ if len(call.lhs) != results {
+ fatalf("stack helper %q returns %d values, call assigns %d", call.helper, results, len(call.lhs))
+ }
+ // Map the receiver to the loop local and each parameter to its argument.
+ names := paramNames(fn)
+ if len(names) != len(call.args) {
+ fatalf("stack helper %q takes %d params, call passes %d", call.helper, len(names), len(call.args))
+ }
+ subst := map[string]string{recvName(fn): "stack"}
+ for i, name := range names {
+ subst[name] = substParams(renderInlineExpr(call.args[i], nil), params)
+ }
+ // The leading bookkeeping statements, then bind each return expression to
+ // its assignment target.
+ var out strings.Builder
+ for _, stmt := range body {
+ out.WriteString(renderInlineStmt(stmt, subst) + "\n")
+ }
+ for i, lhs := range call.lhs {
+ out.WriteString(renderInlineExpr(lhs, nil) + " " + call.tok.String() + " " + renderInlineExpr(ret.Results[i], subst) + "\n")
+ }
+ return out.String()
+}
+
+// recvName returns a method's receiver name (e.g. "s").
+func recvName(fn *ast.FuncDecl) string {
+ if names := fn.Recv.List[0].Names; len(names) > 0 {
+ return names[0].Name
+ }
+ return ""
+}
+
+// paramNames returns a function's parameter names, in order.
+func paramNames(fn *ast.FuncDecl) []string {
+ var names []string
+ for _, f := range fn.Type.Params.List {
+ for _, nm := range f.Names {
+ names = append(names, nm.Name)
+ }
+ }
+ return names
+}
+
+// renderInlineStmt prints one helper-body statement with subst applied. Only the
+// statement shapes the helpers use are handled; any other is not inlinable.
+func renderInlineStmt(stmt ast.Stmt, subst map[string]string) string {
+ switch s := stmt.(type) {
+ case *ast.IncDecStmt: // s.inner.top++
+ return renderInlineExpr(s.X, subst) + s.Tok.String()
+ case *ast.AssignStmt: // s.size -= 2, data[x] = data[y], or the swap tuple a, b = b, a
+ if len(s.Lhs) == len(s.Rhs) && len(s.Lhs) >= 1 {
+ lhs := make([]string, len(s.Lhs))
+ rhs := make([]string, len(s.Rhs))
+ for i := range s.Lhs {
+ lhs[i] = renderInlineExpr(s.Lhs[i], subst)
+ rhs[i] = renderInlineExpr(s.Rhs[i], subst)
+ }
+ return strings.Join(lhs, ", ") + " " + s.Tok.String() + " " + strings.Join(rhs, ", ")
+ }
+ }
+ fatalf("inline: unsupported statement %T in stack helper", stmt)
+ return ""
+}
+
+// renderInlineExpr prints one helper-body expression, substituting any
+// identifier found in subst. Only the shapes the helpers use are handled.
+func renderInlineExpr(expr ast.Expr, subst map[string]string) string {
+ switch e := expr.(type) {
+ case *ast.Ident:
+ if r, ok := subst[e.Name]; ok {
+ return r
+ }
+ return e.Name
+ case *ast.BasicLit:
+ return e.Value
+ case *ast.SelectorExpr: // x.field
+ return renderInlineExpr(e.X, subst) + "." + e.Sel.Name
+ case *ast.IndexExpr: // x[i]
+ return renderInlineExpr(e.X, subst) + "[" + renderInlineExpr(e.Index, subst) + "]"
+ case *ast.BinaryExpr: // x op y
+ return renderInlineExpr(e.X, subst) + " " + e.Op.String() + " " + renderInlineExpr(e.Y, subst)
+ case *ast.UnaryExpr: // &x
+ return e.Op.String() + renderInlineExpr(e.X, subst)
+ }
+ fatalf("inline: unsupported expression %T in stack helper", expr)
+ return ""
+}
+
+// deriveSpecs records each opcode's constant values (name, intro fork, static
+// gas, stack bounds) from the first fork that defines it, then checks that the
+// opcodes chosen for inlining and direct-calling are safe to emit as constants
+// by verifying they are fork-stable (see checkStable and checkDirectCallStable).
+func (g *generator) deriveSpecs(forks []vm.GenFork) {
+ for code := range 256 {
+ for _, fork := range forks {
+ o := fork.Ops[code]
+ if !o.Defined {
+ continue
+ }
+ g.specs[code] = opSpec{
+ defined: true,
+ name: o.Name,
+ fork: fork.RuleField,
+ constGas: o.ConstantGas,
+ minStack: o.MinStack,
+ maxStack: o.MaxStack,
+ }
+ break // first fork that defines it wins (its intro fork)
+ }
+ }
+
+ // Every inlined opcode must be defined and have fork-stable static
+ // gas / stack bounds across all forks where it appears. Bail loudly otherwise.
+ for code, handler := range inlineOps {
+ g.checkStable(code, handler, forks)
+ }
+
+ // directCallOps opcodes emit their static gas and stack bounds as constants the
+ // same way, so they must be fork-stable too. Dynamic gas is allowed (it is
+ // charged through the named gas function, not a constant).
+ for code := range directCallOps {
+ g.checkDirectCallStable(code, forks)
+ }
+}
+
+// checkStable verifies an opcode selected for inlining is safe to inline: it must
+// be defined, its static gas and stack bounds must be the same across every fork
+// it appears in (they are emitted as constants), and it must have no dynamic gas,
+// since an inlined op charges only its constant static gas. It bails loudly
+// otherwise. `what` names the handler for the error message.
+func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
+ spec := g.specs[code]
+ if !spec.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 != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack || o.DynamicGasFn != "" {
+ fatalf("opcode %#x (%s) is not fork-stable (fork %s): cannot inline", code, what, fork.Name)
+ }
+ }
+}
+
+// checkDirectCallStable verifies a directCallOps opcode is safe to direct-call. Its static
+// gas and stack bounds must be the same across every fork it appears in (they are
+// emitted as constants), and its handler, gas and memory functions must be the same
+// across those forks too (they are called by name, so a fork that swapped one
+// would otherwise be missed). Unlike checkStable it allows dynamic gas, which
+// directCallOps ops carry by definition. It does not check the directCallOps map's names
+// against the table, which the differential test covers.
+func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
+ spec := g.specs[code]
+ if !spec.defined {
+ fatalf("opcode %#x (directCallOps) is never defined", code)
+ }
+ var exec, dyn, mem string
+ seen := false
+ for _, fork := range forks {
+ o := fork.Ops[code]
+ if !o.Defined {
+ continue
+ }
+ if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack {
+ fatalf("opcode %#x (%s) is in directCallOps but not fork-stable (fork %s): static gas or stack bounds vary, cannot emit as constants", code, spec.name, fork.Name)
+ }
+ // Handler, gas and memory functions must match across forks too, or
+ // direct-calling them by name would skip a fork that swapped one. Names
+ // come from FuncForPC via vm.GenForks (aliases resolve to the underlying
+ // func, still stable across forks).
+ if !seen {
+ exec, dyn, mem, seen = o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, true
+ } else if o.ExecuteFn != exec || o.DynamicGasFn != dyn || o.MemorySizeFn != mem {
+ fatalf("opcode %#x (%s) is in directCallOps but its functions vary by fork (fork %s): got %s/%s/%s, want %s/%s/%s, cannot direct-call",
+ code, spec.name, fork.Name, o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, exec, dyn, mem)
+ }
+ }
+}
+
+// generateStackChecks returns the underflow/overflow guards, mirroring the legacy
+// loop's order (stack validated before gas). minExpr and maxExpr are the
+// stack-bound expressions (constants on the inlined/direct paths,
+// operation.minStack/maxStack in the table path). under and over select which
+// guards to emit, so those paths can omit a guard whose bound is trivial.
+func (g *generator) generateStackChecks(minExpr, maxExpr any, under, over bool) string {
+ switch {
+ case under && over:
+ return fmt.Sprintf(`if sLen := stack.len(); sLen < %v {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: %v}
+} else if sLen > %v {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: %v}
+}
+`, minExpr, minExpr, maxExpr, maxExpr)
+ case under:
+ return fmt.Sprintf(`if sLen := stack.len(); sLen < %v {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: %v}
+}
+`, minExpr, minExpr)
+ case over:
+ return fmt.Sprintf(`if sLen := stack.len(); sLen > %v {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: %v}
+}
+`, maxExpr, maxExpr)
+ }
+ return ""
+}
+
+// generateStaticGas returns the static-gas charge, spliced call-free from the
+// chargeRegularOnly body for amount: a constant on the inlined and
+// direct-call paths, operation.constantGas in the table path. The receiver maps
+// to contract.Gas and the method's single uint64 parameter to amount, substituted
+// textually on word boundaries (which cannot touch fields like RegularGas). Its
+// `return ` becomes the loop's out-of-gas exit and its trailing `return nil`
+// is dropped so the opcode falls through to its remaining steps (see
+// rewriteGasReturns).
+func (g *generator) generateStaticGas(amount any) string {
+ fn := g.gasHelpers["chargeRegularOnly"]
+ if fn == nil {
+ fatalf("no chargeRegularOnly gas helper to inline")
+ }
+ names := paramNames(fn)
+ if len(names) != 1 {
+ fatalf("chargeRegularOnly takes %d params, want 1", len(names))
+ }
+ src := g.renderAst(fn.Body.List)
+ src = regexp.MustCompile(`\b`+recvName(fn)+`\b`).ReplaceAllString(src, "contract.Gas")
+ src = regexp.MustCompile(`\b`+names[0]+`\b`).ReplaceAllString(src, fmt.Sprint(amount))
+ return g.rewriteGasReturns(src)
+}
+
+// emitInlineOp emits an inlined opcode case: the stack and gas guards followed by
+// the spliced opcode body. A fork-introduced opcode wraps that body in a fork gate
+// so it runs only when the opcode is active for the current fork, otherwise the
+// case mirrors the legacy loop's undefined-opcode handling.
+func (g *generator) emitInlineOp(code byte) {
+ spec := g.specs[code]
+ g.p("case %s:\n", spec.name)
+ if spec.fork != "" {
+ g.p("if rules.%s {\n", spec.fork)
+ }
+
+ // stack bounds check
+ g.p("%s", g.generateStackChecks(spec.minStack, spec.maxStack, spec.minStack > 0, spec.maxStack < int(params.StackLimit)))
+
+ // static gas
+ if spec.constGas != 0 {
+ g.p("%s", g.generateStaticGas(spec.constGas))
+ }
+
+ // 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 constant static gas and stack guard above already match.
+ if code >= 0x60 && code <= 0x7f {
+ g.p(`
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ `)
+ }
+
+ // opcode body
+ switch h := inlineOps[code]; h {
+ case "makePush": // PUSH3-PUSH32: splice makePush(size, size)
+ n := int(code) - 0x5f
+ g.p("%s", g.spliceOpcodeFactoryBody("makePush", n, n))
+ case "makeDup": // DUP1-DUP16: splice makeDup(n)
+ g.p("%s", g.spliceOpcodeFactoryBody("makeDup", int(code)-0x7f))
+ default: // the rest: splice the opXxx handler body
+ g.p("%s", g.spliceOpcodeBody(h))
+ }
+
+ // If opcode is inactive for this fork, then close the gate
+ // and fall back to the legacy loop's undefined-opcode handling.
+ if spec.fork != "" {
+ g.p(`
+ }
+ res, err = opUndefined(&pc, evm, scope)
+ break mainLoop
+ `)
+ }
+}
+
+// emitDirectCallOp emits an 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 directCallOps).
+func (g *generator) emitDirectCallOp(code byte) {
+ spec := g.specs[code]
+ fns := directCallOps[code]
+ g.p("case %s:\n", spec.name)
+
+ // stack bounds check
+ g.p("%s", g.generateStackChecks(spec.minStack, spec.maxStack, spec.minStack > 0, spec.maxStack < int(params.StackLimit)))
+
+ // static gas
+ if spec.constGas != 0 {
+ g.p("%s", g.generateStaticGas(spec.constGas))
+ }
+
+ // dynamic gas
+ g.p("\nvar memorySize uint64\n")
+
+ // Splice computeMemorySize's body, rewriting its operation.memorySize lookup to
+ // the opcode's memory-size function and its returns for the dispatch loop.
+ memSizeFn := g.gasHelpers["computeMemorySize"]
+ if memSizeFn == nil {
+ fatalf("no computeMemorySize gas helper to inline")
+ }
+ memSizeSrc := g.renderAst(memSizeFn.Body.List)
+ memSizeSrc = strings.ReplaceAll(memSizeSrc, "operation.memorySize", fns[2])
+ g.p("%s", g.rewriteStepReturns(memSizeSrc, "memorySize"))
+
+ // Splice chargeDynamicGas's body the same way, rewriting operation.dynamicGas to
+ // the opcode's gas function.
+ dynGasFn := g.gasHelpers["chargeDynamicGas"]
+ if dynGasFn == nil {
+ fatalf("no chargeDynamicGas gas helper to inline")
+ }
+ dynGasSrc := g.renderAst(dynGasFn.Body.List)
+ dynGasSrc = strings.ReplaceAll(dynGasSrc, "operation.dynamicGas", fns[1])
+ g.p("%s", g.rewriteStepReturns(dynGasSrc, ""))
+
+ // resize memory
+ g.p(`
+ if memorySize > 0 {
+ mem.Resize(memorySize)
+ }
+ `)
+
+ // call the opcode handler
+ g.p(`
+ res, err = %s(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ `, fns[0])
+
+ // advance to the next opcode
+ g.p(`
+ pc++
+ continue mainLoop
+ `)
+}
+
+// emitDefault emits the switch's default case: every opcode not inlined or
+// direct-called (the fork-varying ops such as CALL, CREATE, SSTORE, SLOAD, LOG
+// and the COPY family) is dispatched through the active per-fork table, exactly
+// as the legacy loop did, so their volatile gas and opcode logic stays shared
+// rather than restated here.
+func (g *generator) emitDefault() {
+ g.p(`
+ default:
+ operation := table[op]
+ `)
+ // stack bounds check
+ g.p("%s", g.generateStackChecks("operation.minStack", "operation.maxStack", true, true))
+
+ // static gas
+ g.p("%s", g.generateStaticGas("operation.constantGas"))
+
+ // dynamic gas
+ g.p(`
+ var memorySize uint64
+ if memorySize, _, err = contract.meterDynamicGas(operation, evm, stack, mem); err != nil {
+ return nil, err
+ }
+ `)
+
+ // resize memory
+ g.p(`
+ if memorySize > 0 {
+ mem.Resize(memorySize)
+ }
+ `)
+
+ // call the opcode handler
+ g.p(`
+ res, err = operation.execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ `)
+
+ // advance to the next opcode
+ g.p(`
+ pc++
+ continue mainLoop
+ `)
+}
+
+// createFile emits the whole generated file into g.buf: the header, package and
+// imports, then the execUntraced function (its locals and dispatch loop, the
+// verkle code-chunk gas, and a switch with one case per opcode built by the
+// emit* helpers). main formats the buffer and writes it to interpreter_gen.go.
+//
+// The switch has three tiers:
+//
+// - the hot, fork-stable opcodes (arithmetic / comparison / bitwise / PUSH /
+// DUP / SWAP / POP / JUMP / JUMPI / PC / MSIZE / JUMPDEST) are inlined by
+// splicing the existing opXxx handler bodies from instructions.go and
+// eips.go, with their static gas and stack bounds emitted as constants
+// derived from the per-fork instruction tables via vm.GenForks.
+//
+// - the fork-invariant ops (KECCAK256 / MLOAD / MSTORE / MSTORE8, see
+// directCallOps) are called directly by name, skipping the table's function
+// pointers, which Go cannot inline through.
+//
+// - everything fork-varying (CALL / CREATE / SSTORE / SLOAD / LOG / the COPY
+// family and so on) is dispatched 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.
+func (g *generator) createFile() {
+ // file header, package clause, and imports
+ g.p(`
+ // Code generated by core/vm/gen; DO NOT EDIT.
+
+ package vm
+
+ import (
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ )
+
+ `)
+
+ // execUntraced: doc comment, loop-local declarations, and the dispatch loop
+ g.p(`
+ // execUntraced is the generated, tracing-free interpreter fast path. Hot,
+ // fork-stable opcodes are inlined with their static gas and stack bounds emitted
+ // as constants. Fork-invariant 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 {
+ `)
+
+ // verkle code-chunk gas, spliced from chargeVerkleCodeChunkGas
+ ccgFn := g.gasHelpers["chargeVerkleCodeChunkGas"]
+ if ccgFn == nil {
+ fatalf("no chargeVerkleCodeChunkGas gas helper to inline")
+ }
+ g.p("%s", g.rewriteGasReturns(g.renderAst(ccgFn.Body.List)))
+
+ // fetch the opcode and open the dispatch switch
+ g.p(`
+ op := contract.GetOp(pc)
+ switch op {
+ `)
+
+ // one case per inlined or direct-call opcode, in opcode order
+ for code := range 256 {
+ b := byte(code)
+ if _, named := inlineOps[b]; named {
+ g.emitInlineOp(b)
+ } else if _, dc := directCallOps[b]; dc {
+ g.emitDirectCallOp(b)
+ }
+ }
+
+ // the default case: fork-varying ops via the per-fork table
+ g.emitDefault()
+
+ // close the switch and loop, clear the stop token, and return
+ g.p(`
+ }
+ }
+ if err == errStopToken {
+ err = nil
+ }
+ return res, err
+ }
+ `)
+}
+
+func fatalf(format string, args ...any) {
+ fmt.Fprintf(os.Stderr, "gen: "+format+"\n", args...)
+ os.Exit(1)
+}
+
+// vmDir returns the core/vm directory, the parent of this generator package. It
+// is resolved from this source file's own path so it does not depend on the
+// directory the generator or the test happens to run from.
+func vmDir() string {
+ _, self, _, ok := runtime.Caller(0)
+ if !ok {
+ fatalf("cannot resolve generator source path")
+ }
+ return filepath.Dir(filepath.Dir(self)) // .../core/vm/gen -> .../core/vm
+}
+
+// generate parses the opcode, gas and fork definitions under core/vm and returns
+// the formatted contents of interpreter_gen.go. It is the shared core of the
+// generator: main writes the result to disk, and the up-to-date test in
+// gen_test.go compares it against the committed file.
+func generate() ([]byte, error) {
+ fset, opcodeHandlers, stackHelpers, gasHelpers := parseHandlers(vmDir())
+ g := &generator{fset: fset, opcodeHandlers: opcodeHandlers, stackHelpers: stackHelpers, gasHelpers: gasHelpers, buf: new(bytes.Buffer)}
+ g.deriveSpecs(vm.GenForks())
+ g.createFile()
+
+ formatted, err := format.Source(g.buf.Bytes())
+ if err != nil {
+ dbg := filepath.Join(vmDir(), "interpreter_gen.go.broken")
+ os.WriteFile(dbg, g.buf.Bytes(), 0644)
+ return nil, fmt.Errorf("gofmt failed (%v); wrote unformatted output to %s", err, dbg)
+ }
+ return formatted, nil
+}
diff --git a/core/vm/gen/gen_test.go b/core/vm/gen/gen_test.go
new file mode 100644
index 0000000000..f22c3c9db0
--- /dev/null
+++ b/core/vm/gen/gen_test.go
@@ -0,0 +1,42 @@
+// 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 .
+
+package main
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// TestGeneratedDispatchUpToDate asserts that the committed interpreter_gen.go
+// matches what the generator produces from the current opcode, gas and 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) {
+ got, err := generate()
+ if err != nil {
+ t.Fatalf("running generator: %v", err)
+ }
+ want, err := os.ReadFile(filepath.Join(vmDir(), "interpreter_gen.go"))
+ if err != nil {
+ t.Fatalf("reading committed interpreter_gen.go: %v", err)
+ }
+ if !bytes.Equal(got, want) {
+ t.Fatal("interpreter_gen.go is out of date; run `go generate ./core/vm/...` and commit the result")
+ }
+}
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index 3f59e6f151..2c31a8c9d7 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -15,938 +15,25 @@
// along with the go-ethereum library. If not, see .
// Command gen generates core/vm/interpreter_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 ops (KECCAK256 / MLOAD / MSTORE / MSTORE8,
-// see directCallOps) 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 interpreter_gen.go.
+// fast-path dispatch, a switch over the opcode byte. The generated file is
+// committed and a CI test asserts it matches `go generate` output. Do not
+// hand-edit interpreter_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
-
-// inlineOps maps an opcode byte to the handler whose body is spliced inline
-// for that opcode. These are the hot, fork-stable opcodes with no dynamic gas.
-// The value is usually an opXxx handler, but PUSH3-PUSH32 and DUP1-DUP16 are
-// factory-built (one shared makePush / makeDup each), so their value is the
-// factory name and emitInlineOp splices the factory body with the per-opcode size.
-// Opcodes not listed here (or in directCallOps) fall through to the default case,
-// which dispatches via the per-fork table.
-var inlineOps = func() map[byte]string {
- m := 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",
- }
- for code := 0x62; code <= 0x7f; code++ { // PUSH3-PUSH32
- m[byte(code)] = "makePush"
- }
- for code := 0x80; code <= 0x8f; code++ { // DUP1-DUP16
- m[byte(code)] = "makeDup"
- }
- for code := 0x90; code <= 0x9f; code++ { // SWAP1-SWAP16
- m[byte(code)] = fmt.Sprintf("opSwap%d", code-0x8f)
- }
- return m
-}()
-
-// directCallOps lists the 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.
-var directCallOps = map[byte][3]string{
- 0x20: {"opKeccak256", "gasKeccak256", "memoryKeccak256"},
- 0x51: {"opMload", "gasMLoad", "memoryMLoad"},
- 0x52: {"opMstore", "gasMStore", "memoryMStore"},
- 0x53: {"opMstore8", "gasMStore8", "memoryMStore8"},
-}
-
-// opSpec holds the per-opcode constants the generator bakes (gas, stack bounds, intro fork), derived from the per-fork tables.
-type opSpec struct {
- defined bool
- name string
- fork string
- constGas uint64
- minStack int
- maxStack int
-}
-
-type generator struct {
- fset *token.FileSet
- opcodeHandlers map[string]*ast.FuncDecl
- stackHelpers map[string]*ast.FuncDecl
- gasHelpers map[string]*ast.FuncDecl
- specs [256]opSpec
- buf *bytes.Buffer
-}
-
-// p is the writer of the generated file. Every line of output is appended
-// to g.buf through it.
-func (g *generator) p(format string, args ...any) {
- format = strings.TrimRight(strings.TrimPrefix(format, "\n"), " \t")
- fmt.Fprintf(g.buf, format, args...)
-}
-
-// parseHandlers parses instructions.go, eips.go, stack.go, gascosts.go and
-// interpreter.go. It returns the top-level opXxx handlers by name, the
-// //gen:inline *Stack helper methods, and the gas/memory helper functions
-// (chargeRegularOnly, computeMemorySize, chargeDynamicGas) whose bodies are
-// spliced into the generated dispatch (all by name).
-func parseHandlers(vmDir string) (fset *token.FileSet, opcodeHandlers, stackHelpers, gasHelpers map[string]*ast.FuncDecl) {
- fset = token.NewFileSet()
- opcodeHandlers = map[string]*ast.FuncDecl{}
- stackHelpers = map[string]*ast.FuncDecl{}
- gasHelpers = map[string]*ast.FuncDecl{}
- for _, name := range []string{"instructions.go", "eips.go", "stack.go", "gascosts.go", "interpreter.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.Body == nil {
- continue
- }
- switch {
- case fn.Name.Name == "chargeRegularOnly" || fn.Name.Name == "computeMemorySize" || fn.Name.Name == "chargeDynamicGas" || fn.Name.Name == "chargeVerkleCodeChunkGas": // spliced gas/memory helpers
- gasHelpers[fn.Name.Name] = fn
- case fn.Recv == nil: // top-level opXxx handler
- opcodeHandlers[fn.Name.Name] = fn
- case methodReceiver(fn) == "Stack" && hasInlineMarker(fn): // (s *Stack) helper tagged //gen:inline
- stackHelpers[fn.Name.Name] = fn
- }
- }
- }
- return fset, opcodeHandlers, stackHelpers, gasHelpers
-}
-
-// methodReceiver returns the receiver type name of a pointer-receiver method
-// (e.g. "Stack" for (s *Stack)), or "" if fn is not such a method.
-func methodReceiver(fn *ast.FuncDecl) string {
- if fn.Recv == nil || len(fn.Recv.List) != 1 {
- return ""
- }
- star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
- if !ok {
- return ""
- }
- id, ok := star.X.(*ast.Ident)
- if !ok {
- return ""
- }
- return id.Name
-}
-
-// hasInlineMarker reports whether fn is tagged //gen:inline, which marks a stack
-// helper for splicing into the generated dispatch.
-func hasInlineMarker(fn *ast.FuncDecl) bool {
- if fn.Doc == nil {
- return false
- }
- for _, c := range fn.Doc.List {
- if c.Text == "//gen:inline" {
- return true
- }
- }
- return false
-}
-
-var opcodeReturnRe = regexp.MustCompile(`^(\s*)return\s+([^,]+),\s*(.+)$`)
-
-// spliceOpcodeBody returns a named handler's body, rewritten so it can be spliced
-// into the dispatch loop (see rewriteOpcodeReturns). The caller emits it with p.
-func (g *generator) spliceOpcodeBody(handler string) string {
- fn := g.opcodeHandlers[handler]
- if fn == nil {
- fatalf("no handler %q to inline", handler)
- }
- return g.rewriteOpcodeReturns(g.inlineStackHelpers(fn.Body.List, nil))
-}
-
-// spliceOpcodeFactoryBody splices the body of the executionFunc closure that a make*
-// factory returns, substituting the factory's parameters with the per-opcode
-// constants in args (positional, matching the factory signature). This lets
-// closure-built handlers (makePush, makeDup) be derived from their single
-// definition rather than restated in the generator. The caller emits the
-// result with p.
-func (g *generator) spliceOpcodeFactoryBody(factory string, args ...int) string {
- fn := g.opcodeHandlers[factory]
- if fn == nil {
- fatalf("no factory %q to inline", factory)
- }
- lit := factoryClosure(factory, fn)
- // Bind the factory parameters to the per-opcode constants, then inline.
- names := paramNames(fn)
- if len(names) != len(args) {
- fatalf("factory %q takes %d params, got %d args", factory, len(names), len(args))
- }
- params := map[string]int{}
- for i, nm := range names {
- params[nm] = args[i]
- }
- return g.rewriteOpcodeReturns(g.inlineStackHelpers(lit.Body.List, params))
-}
-
-// factoryClosure returns the executionFunc literal that a make* factory's body
-// is a single `return func(...) {...}` of.
-func factoryClosure(name string, fn *ast.FuncDecl) *ast.FuncLit {
- if len(fn.Body.List) != 1 {
- fatalf("factory %q body is not a single return", name)
- }
- ret, ok := fn.Body.List[0].(*ast.ReturnStmt)
- if !ok || len(ret.Results) != 1 {
- fatalf("factory %q does not return a single value", name)
- }
- lit, ok := ret.Results[0].(*ast.FuncLit)
- if !ok {
- fatalf("factory %q does not return a func literal", name)
- }
- return lit
-}
-
-// renderAst converts AST statements back to formatted Go source text, the
-// inverse of parsing. It uses the generator's fileset and emits nothing itself
-// (the caller passes the result to p).
-func (g *generator) renderAst(stmts []ast.Stmt) string {
- var raw bytes.Buffer
- cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8}
- for _, stmt := range stmts {
- if err := cfg.Fprint(&raw, g.fset, stmt); err != nil {
- fatalf("print stmt: %v", err)
- }
- raw.WriteByte('\n')
- }
- return raw.String()
-}
-
-// rewriteOpcodeReturns rewrites a printed handler body so it runs inside 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. (Stack helpers were already
-// inlined by inlineStackHelpers before the body was printed.)
-func (g *generator) rewriteOpcodeReturns(src string) string {
- src = strings.ReplaceAll(src, "*pc", "pc")
-
- var out bytes.Buffer
- for _, line := range strings.Split(src, "\n") {
- if m := opcodeReturnRe.FindStringSubmatch(line); m != nil {
- indent, r0, r1 := m[1], strings.TrimSpace(m[2]), strings.TrimSpace(m[3])
- 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()
-}
-
-var gasReturnRe = regexp.MustCompile(`^(\s*)return\s+(\S.*)$`)
-
-// rewriteGasReturns rewrites a spliced charge body so it runs as a gas step in
-// the dispatch loop: a `return ` becomes the out-of-gas break, and the
-// trailing `return nil` (success) is dropped so the opcode continues.
-func (g *generator) rewriteGasReturns(src string) string {
- var out bytes.Buffer
- for _, line := range strings.Split(src, "\n") {
- if m := gasReturnRe.FindStringSubmatch(line); m != nil {
- indent, val := m[1], strings.TrimSpace(m[2])
- if val == "nil" {
- continue // success: fall through to the rest of the op
- }
- out.WriteString(indent + "res, err = nil, " + val + "\n")
- out.WriteString(indent + "break mainLoop\n")
- continue
- }
- out.WriteString(line + "\n")
- }
- return out.String()
-}
-
-// rewriteStepReturns rewrites a spliced gas-step body's (value, error) returns so
-// it runs inline in the dispatch loop: a non-nil error becomes the out-of-gas
-// break; on success the value is assigned to target (or dropped when target is
-// empty) and the op falls through.
-func (g *generator) rewriteStepReturns(src, target string) string {
- var out bytes.Buffer
- for _, line := range strings.Split(src, "\n") {
- if m := opcodeReturnRe.FindStringSubmatch(line); m != nil {
- indent, val, errVal := m[1], strings.TrimSpace(m[2]), strings.TrimSpace(m[3])
- if errVal == "nil" {
- if target != "" {
- out.WriteString(indent + target + " = " + val + "\n")
- }
- continue
- }
- out.WriteString(indent + "res, err = nil, " + errVal + "\n")
- out.WriteString(indent + "break mainLoop\n")
- continue
- }
- out.WriteString(line + "\n")
- }
- return out.String()
-}
-
-// stackCall is a matched call to a tagged helper.
-type stackCall struct {
- helper string // helper method name
- lhs []ast.Expr // assignment targets, nil for a void call like dup
- tok token.Token // the assignment token, := or =
- args []ast.Expr // call arguments (only dup has one)
-}
-
-// matchStackHelper matches a statement that is a single must-expand helper call,
-// in one of the two normalized forms: an assignment `lhs... := scope.Stack.H(args)`
-// or a bare `scope.Stack.H(args)`.
-func (g *generator) matchStackHelper(stmt ast.Stmt) (stackCall, bool) {
- switch s := stmt.(type) {
- case *ast.AssignStmt:
- if len(s.Rhs) == 1 {
- if h, args, ok := g.stackHelperCall(s.Rhs[0]); ok {
- return stackCall{helper: h, lhs: s.Lhs, tok: s.Tok, args: args}, true
- }
- }
- case *ast.ExprStmt:
- if h, args, ok := g.stackHelperCall(s.X); ok {
- return stackCall{helper: h, args: args}, true
- }
- }
- return stackCall{}, false
-}
-
-// stackHelperCall unwraps scope.Stack.H(args) where H is a must-expand helper.
-func (g *generator) stackHelperCall(e ast.Expr) (helper string, args []ast.Expr, ok bool) {
- call, isCall := e.(*ast.CallExpr)
- if !isCall {
- return "", nil, false
- }
- sel, isSel := call.Fun.(*ast.SelectorExpr) // .H
- if !isSel || g.stackHelpers[sel.Sel.Name] == nil || !isStackExpr(sel.X) {
- return "", nil, false
- }
- return sel.Sel.Name, call.Args, true
-}
-
-// isStackExpr reports whether e is the stack receiver: the `stack` local or
-// scope.Stack.
-func isStackExpr(e ast.Expr) bool {
- switch x := e.(type) {
- case *ast.Ident:
- return x.Name == "stack"
- case *ast.SelectorExpr:
- return x.Sel.Name == "Stack"
- }
- return false
-}
-
-// inlineStackHelpers renders a handler body to source, inlining every must-expand
-// helper call and printing other statements unchanged. params maps the factory
-// parameters (makePush/makeDup) to their per-opcode constants.
-func (g *generator) inlineStackHelpers(stmts []ast.Stmt, params map[string]int) string {
- var out strings.Builder
- // Walk the handler body one statement at a time. A statement that is a
- // tagged stack-helper call gets the helper's body spliced in: the generated
- // dispatch is past Go's big-function inline budget, so the call would not be
- // inlined otherwise. Every other statement is printed as written.
- for _, stmt := range stmts {
- if call, ok := g.matchStackHelper(stmt); ok {
- // e.g. `x, y := scope.Stack.pop1Peek1()` becomes the body of pop1Peek1.
- out.WriteString(g.inlineStackHelper(call, params))
- } else {
- // A plain statement: print it verbatim, then fill in any makePush or
- // makeDup factory params with this opcode's constants.
- out.WriteString(substParams(g.renderAst([]ast.Stmt{stmt}), params))
- }
- }
- return out.String()
-}
-
-// substParams replaces each factory parameter with its constant. It runs only
-// on printed non-helper statements and on helper arguments, never on a helper
-// expansion, so it cannot touch a field like stack.size. The parameter names do
-// not textually overlap, so map order does not affect the result.
-func substParams(src string, params map[string]int) string {
- for name, val := range params {
- src = regexp.MustCompile(`\b`+name+`\b`).ReplaceAllString(src, fmt.Sprint(val))
- }
- return src
-}
-
-// inlineStackHelper expands one helper call to its stack.go body. The single
-// rule: the helper is straight-line statements then an optional final return
-// whose result count matches the call's targets. Anything else is not in
-// inlinable form and is a hard error (the shape post-condition). The receiver
-// maps to the loop's `stack` local and each parameter to its call argument.
-func (g *generator) inlineStackHelper(call stackCall, params map[string]int) string {
- fn := g.stackHelpers[call.helper]
- if fn == nil {
- fatalf("no stack helper %q to inline", call.helper)
- }
- // Peel an optional trailing return off the body.
- body := fn.Body.List
- var ret *ast.ReturnStmt
- if n := len(body); n > 0 {
- if r, isRet := body[n-1].(*ast.ReturnStmt); isRet {
- ret, body = r, body[:n-1]
- }
- }
- results := 0
- if ret != nil {
- results = len(ret.Results)
- }
- if len(call.lhs) != results {
- fatalf("stack helper %q returns %d values, call assigns %d", call.helper, results, len(call.lhs))
- }
- // Map the receiver to the loop local and each parameter to its argument.
- names := paramNames(fn)
- if len(names) != len(call.args) {
- fatalf("stack helper %q takes %d params, call passes %d", call.helper, len(names), len(call.args))
- }
- subst := map[string]string{recvName(fn): "stack"}
- for i, name := range names {
- subst[name] = substParams(renderInlineExpr(call.args[i], nil), params)
- }
- // The leading bookkeeping statements, then bind each return expression to
- // its assignment target.
- var out strings.Builder
- for _, stmt := range body {
- out.WriteString(renderInlineStmt(stmt, subst) + "\n")
- }
- for i, lhs := range call.lhs {
- out.WriteString(renderInlineExpr(lhs, nil) + " " + call.tok.String() + " " + renderInlineExpr(ret.Results[i], subst) + "\n")
- }
- return out.String()
-}
-
-// recvName returns a method's receiver name (e.g. "s").
-func recvName(fn *ast.FuncDecl) string {
- if names := fn.Recv.List[0].Names; len(names) > 0 {
- return names[0].Name
- }
- return ""
-}
-
-// paramNames returns a function's parameter names, in order.
-func paramNames(fn *ast.FuncDecl) []string {
- var names []string
- for _, f := range fn.Type.Params.List {
- for _, nm := range f.Names {
- names = append(names, nm.Name)
- }
- }
- return names
-}
-
-// renderInlineStmt prints one helper-body statement with subst applied. Only the
-// statement shapes the helpers use are handled; any other is not inlinable.
-func renderInlineStmt(stmt ast.Stmt, subst map[string]string) string {
- switch s := stmt.(type) {
- case *ast.IncDecStmt: // s.inner.top++
- return renderInlineExpr(s.X, subst) + s.Tok.String()
- case *ast.AssignStmt: // s.size -= 2, data[x] = data[y], or the swap tuple a, b = b, a
- if len(s.Lhs) == len(s.Rhs) && len(s.Lhs) >= 1 {
- lhs := make([]string, len(s.Lhs))
- rhs := make([]string, len(s.Rhs))
- for i := range s.Lhs {
- lhs[i] = renderInlineExpr(s.Lhs[i], subst)
- rhs[i] = renderInlineExpr(s.Rhs[i], subst)
- }
- return strings.Join(lhs, ", ") + " " + s.Tok.String() + " " + strings.Join(rhs, ", ")
- }
- }
- fatalf("inline: unsupported statement %T in stack helper", stmt)
- return ""
-}
-
-// renderInlineExpr prints one helper-body expression, substituting any
-// identifier found in subst. Only the shapes the helpers use are handled.
-func renderInlineExpr(expr ast.Expr, subst map[string]string) string {
- switch e := expr.(type) {
- case *ast.Ident:
- if r, ok := subst[e.Name]; ok {
- return r
- }
- return e.Name
- case *ast.BasicLit:
- return e.Value
- case *ast.SelectorExpr: // x.field
- return renderInlineExpr(e.X, subst) + "." + e.Sel.Name
- case *ast.IndexExpr: // x[i]
- return renderInlineExpr(e.X, subst) + "[" + renderInlineExpr(e.Index, subst) + "]"
- case *ast.BinaryExpr: // x op y
- return renderInlineExpr(e.X, subst) + " " + e.Op.String() + " " + renderInlineExpr(e.Y, subst)
- case *ast.UnaryExpr: // &x
- return e.Op.String() + renderInlineExpr(e.X, subst)
- }
- fatalf("inline: unsupported expression %T in stack helper", expr)
- return ""
-}
-
-// deriveSpecs records each opcode's bakeable constants (name, intro fork, static
-// gas, stack bounds) from the first fork that defines it, then checks that the
-// opcodes chosen for inlining and direct-calling are safe to bake by verifying
-// their constants are fork-stable (see checkStable and checkDirectCallStable).
-func (g *generator) deriveSpecs(forks []vm.GenFork) {
- for code := 0; code < 256; code++ {
- for _, fork := range forks {
- o := fork.Ops[code]
- if !o.Defined {
- continue
- }
- g.specs[code] = opSpec{
- defined: true,
- name: o.Name,
- fork: fork.RuleField,
- constGas: o.ConstantGas,
- minStack: o.MinStack,
- maxStack: o.MaxStack,
- }
- break // first fork that defines it wins (its intro fork)
- }
- }
-
- // Every inlined opcode must be defined and have fork-stable static
- // gas / stack bounds across all forks where it appears. Bail loudly otherwise.
- for code, handler := range inlineOps {
- g.checkStable(code, handler, forks)
- }
-
- // directCallOps opcodes bake their static gas and stack bounds the same way, so
- // they must be fork-stable too. Dynamic gas is allowed (it is charged through
- // the named gas function, not baked).
- for code := range directCallOps {
- g.checkDirectCallStable(code, forks)
- }
-}
-
-// checkStable verifies an opcode selected for inlining is safe to inline: it must
-// be defined, its static gas and stack bounds must be the same across every fork
-// it appears in (they are baked as constants), and it must have no dynamic gas,
-// since an inlined op charges only the baked static gas. It bails loudly
-// otherwise. `what` names the handler for the error message.
-func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
- spec := g.specs[code]
- if !spec.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 != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack || o.DynamicGasFn != "" {
- fatalf("opcode %#x (%s) is not fork-stable (fork %s): cannot inline", code, what, fork.Name)
- }
- }
-}
-
-// checkDirectCallStable verifies a directCallOps opcode is safe to direct-call. Its static
-// gas and stack bounds must be the same across every fork it appears in (they are
-// baked as constants), and its handler, gas and memory functions must be the same
-// across those forks too (they are called by name, so a fork that swapped one
-// would otherwise be missed). Unlike checkStable it allows dynamic gas, which
-// directCallOps ops carry by definition. It does not check the directCallOps map's names
-// against the table, which the differential test covers.
-func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
- spec := g.specs[code]
- if !spec.defined {
- fatalf("opcode %#x (directCallOps) is never defined", code)
- }
- var exec, dyn, mem string
- seen := false
- for _, fork := range forks {
- o := fork.Ops[code]
- if !o.Defined {
- continue
- }
- if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack {
- fatalf("opcode %#x (%s) is in directCallOps but not fork-stable (fork %s): static gas or stack bounds vary, cannot bake", code, spec.name, fork.Name)
- }
- // Handler, gas and memory functions must match across forks too, or
- // direct-calling them by name would skip a fork that swapped one. Names
- // come from FuncForPC via vm.GenForks (aliases resolve to the underlying
- // func, still stable across forks).
- if !seen {
- exec, dyn, mem, seen = o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, true
- } else if o.ExecuteFn != exec || o.DynamicGasFn != dyn || o.MemorySizeFn != mem {
- fatalf("opcode %#x (%s) is in directCallOps but its functions vary by fork (fork %s): got %s/%s/%s, want %s/%s/%s, cannot direct-call",
- code, spec.name, fork.Name, o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, exec, dyn, mem)
- }
- }
-}
-
-// generateStackChecks returns the underflow/overflow guards, mirroring the legacy
-// loop's order (stack validated before gas). minExpr and maxExpr are the
-// stack-bound expressions (baked constants on the inlined/direct paths,
-// operation.minStack/maxStack in the table path). under and over select which
-// guards to emit, so the baked paths can omit a guard whose bound is trivial.
-func (g *generator) generateStackChecks(minExpr, maxExpr any, under, over bool) string {
- switch {
- case under && over:
- return fmt.Sprintf(`if sLen := stack.len(); sLen < %v {
- return nil, &ErrStackUnderflow{stackLen: sLen, required: %v}
-} else if sLen > %v {
- return nil, &ErrStackOverflow{stackLen: sLen, limit: %v}
-}
-`, minExpr, minExpr, maxExpr, maxExpr)
- case under:
- return fmt.Sprintf(`if sLen := stack.len(); sLen < %v {
- return nil, &ErrStackUnderflow{stackLen: sLen, required: %v}
-}
-`, minExpr, minExpr)
- case over:
- return fmt.Sprintf(`if sLen := stack.len(); sLen > %v {
- return nil, &ErrStackOverflow{stackLen: sLen, limit: %v}
-}
-`, maxExpr, maxExpr)
- }
- return ""
-}
-
-// generateStaticGas returns the static-gas charge, spliced call-free from the
-// chargeRegularOnly body for amount: a baked constant on the inlined and
-// direct-call paths, operation.constantGas in the table path. The receiver maps
-// to contract.Gas and the method's single uint64 parameter to amount, substituted
-// textually on word boundaries (which cannot touch fields like RegularGas). Its
-// `return ` becomes the loop's out-of-gas exit and its trailing `return nil`
-// is dropped so the opcode falls through to its remaining steps (see
-// rewriteGasReturns).
-func (g *generator) generateStaticGas(amount any) string {
- fn := g.gasHelpers["chargeRegularOnly"]
- if fn == nil {
- fatalf("no chargeRegularOnly gas helper to inline")
- }
- names := paramNames(fn)
- if len(names) != 1 {
- fatalf("chargeRegularOnly takes %d params, want 1", len(names))
- }
- src := g.renderAst(fn.Body.List)
- src = regexp.MustCompile(`\b`+recvName(fn)+`\b`).ReplaceAllString(src, "contract.Gas")
- src = regexp.MustCompile(`\b`+names[0]+`\b`).ReplaceAllString(src, fmt.Sprint(amount))
- return g.rewriteGasReturns(src)
-}
-
-// emitInlineOp emits an inlined opcode case: the stack and gas guards followed by
-// the spliced opcode body. A fork-introduced opcode wraps that body in a fork gate
-// so it runs only when the opcode is active for the current fork, otherwise the
-// case mirrors the legacy loop's undefined-opcode handling.
-func (g *generator) emitInlineOp(code byte) {
- spec := g.specs[code]
- g.p("case %s:\n", spec.name)
- if spec.fork != "" {
- g.p("if rules.%s {\n", spec.fork)
- }
-
- // stack bounds check
- g.p("%s", g.generateStackChecks(spec.minStack, spec.maxStack, spec.minStack > 0, spec.maxStack < stackLimit))
-
- // static gas
- if spec.constGas != 0 {
- g.p("%s", g.generateStaticGas(spec.constGas))
- }
-
- // 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 {
- res, err = table[op].execute(&pc, evm, scope)
- if err != nil {
- break mainLoop
- }
- pc++
- continue mainLoop
- }
- `)
- }
-
- // opcode body
- switch h := inlineOps[code]; h {
- case "makePush": // PUSH3-PUSH32: splice makePush(size, size)
- n := int(code) - 0x5f
- g.p("%s", g.spliceOpcodeFactoryBody("makePush", n, n))
- case "makeDup": // DUP1-DUP16: splice makeDup(n)
- g.p("%s", g.spliceOpcodeFactoryBody("makeDup", int(code)-0x7f))
- default: // the rest: splice the opXxx handler body
- g.p("%s", g.spliceOpcodeBody(h))
- }
-
- // If opcode is inactive for this fork, then close the gate
- // and fall back to the legacy loop's undefined-opcode handling.
- if spec.fork != "" {
- g.p(`
- }
- res, err = opUndefined(&pc, evm, scope)
- break mainLoop
- `)
- }
-}
-
-// emitDirectCallOp emits an 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 directCallOps).
-func (g *generator) emitDirectCallOp(code byte) {
- spec := g.specs[code]
- fns := directCallOps[code]
- g.p("case %s:\n", spec.name)
-
- // stack bounds check
- g.p("%s", g.generateStackChecks(spec.minStack, spec.maxStack, spec.minStack > 0, spec.maxStack < stackLimit))
-
- // static gas
- if spec.constGas != 0 {
- g.p("%s", g.generateStaticGas(spec.constGas))
- }
-
- // dynamic gas
- g.p("\nvar memorySize uint64\n")
-
- // Splice computeMemorySize's body, rewriting its operation.memorySize lookup to
- // the opcode's memory-size function and its returns for the dispatch loop.
- memSizeFn := g.gasHelpers["computeMemorySize"]
- if memSizeFn == nil {
- fatalf("no computeMemorySize gas helper to inline")
- }
- memSizeSrc := g.renderAst(memSizeFn.Body.List)
- memSizeSrc = strings.ReplaceAll(memSizeSrc, "operation.memorySize", fns[2])
- g.p("%s", g.rewriteStepReturns(memSizeSrc, "memorySize"))
-
- // Splice chargeDynamicGas's body the same way, rewriting operation.dynamicGas to
- // the opcode's gas function.
- dynGasFn := g.gasHelpers["chargeDynamicGas"]
- if dynGasFn == nil {
- fatalf("no chargeDynamicGas gas helper to inline")
- }
- dynGasSrc := g.renderAst(dynGasFn.Body.List)
- dynGasSrc = strings.ReplaceAll(dynGasSrc, "operation.dynamicGas", fns[1])
- g.p("%s", g.rewriteStepReturns(dynGasSrc, ""))
-
- // resize memory
- g.p(`
- if memorySize > 0 {
- mem.Resize(memorySize)
- }
- `)
-
- // call the opcode handler
- g.p(`
- res, err = %s(&pc, evm, scope)
- if err != nil {
- break mainLoop
- }
- `, fns[0])
-
- // advance to the next opcode
- g.p(`
- pc++
- continue mainLoop
- `)
-}
-
-// emitDefault emits the switch's default case: every opcode not inlined or
-// direct-called (the fork-varying ops such as CALL, CREATE, SSTORE, SLOAD, LOG
-// and the COPY family) is dispatched through the active per-fork table, exactly
-// as the legacy loop did, so their volatile gas and opcode logic stays shared
-// rather than restated here.
-func (g *generator) emitDefault() {
- g.p(`
- default:
- operation := table[op]
- `)
- // stack bounds check
- g.p("%s", g.generateStackChecks("operation.minStack", "operation.maxStack", true, true))
-
- // static gas
- g.p("%s", g.generateStaticGas("operation.constantGas"))
-
- // dynamic gas
- g.p(`
- var memorySize uint64
- if memorySize, _, err = contract.meterDynamicGas(operation, evm, stack, mem); err != nil {
- return nil, err
- }
- `)
-
- // resize memory
- g.p(`
- if memorySize > 0 {
- mem.Resize(memorySize)
- }
- `)
-
- // call the opcode handler
- g.p(`
- res, err = operation.execute(&pc, evm, scope)
- if err != nil {
- break mainLoop
- }
- `)
-
- // advance to the next opcode
- g.p(`
- pc++
- continue mainLoop
- `)
-}
-
-// createFile emits the whole generated file into g.buf: the header, package and
-// imports, then the execUntraced function (its locals and dispatch loop, the
-// verkle code-chunk gas, and a switch with one case per opcode built by the
-// emit* helpers). main formats the buffer and writes it to interpreter_gen.go.
-func (g *generator) createFile() {
- // file header, package clause, and imports
- g.p(`
- // Code generated by core/vm/gen; DO NOT EDIT.
-
- package vm
-
- import (
- "fmt"
-
- "github.com/ethereum/go-ethereum/common/math"
- "github.com/ethereum/go-ethereum/core/tracing"
- )
-
- `)
-
- // execUntraced: doc comment, loop-local declarations, and the dispatch loop
- g.p(`
- // 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 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 {
- `)
-
- // verkle code-chunk gas, spliced from chargeVerkleCodeChunkGas
- ccgFn := g.gasHelpers["chargeVerkleCodeChunkGas"]
- if ccgFn == nil {
- fatalf("no chargeVerkleCodeChunkGas gas helper to inline")
- }
- g.p("%s", g.rewriteGasReturns(g.renderAst(ccgFn.Body.List)))
-
- // fetch the opcode and open the dispatch switch
- g.p(`
- op := contract.GetOp(pc)
- switch op {
- `)
-
- // one case per inlined or direct-call opcode, in opcode order
- for code := 0; code < 256; code++ {
- b := byte(code)
- if _, named := inlineOps[b]; named {
- g.emitInlineOp(b)
- } else if _, dc := directCallOps[b]; dc {
- g.emitDirectCallOp(b)
- }
- }
-
- // the default case: fork-varying ops via the per-fork table
- g.emitDefault()
-
- // close the switch and loop, clear the stop token, and return
- g.p(`
- }
- }
- if err == errStopToken {
- err = nil
- }
- return res, err
- }
- `)
-}
-
-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, opcodeHandlers, stackHelpers, gasHelpers := parseHandlers(vmDir)
- g := &generator{fset: fset, opcodeHandlers: opcodeHandlers, stackHelpers: stackHelpers, gasHelpers: gasHelpers, buf: new(bytes.Buffer)}
- g.deriveSpecs(vm.GenForks())
- g.createFile()
-
- formatted, err := format.Source(g.buf.Bytes())
+ formatted, err := generate()
if err != nil {
- dbg := filepath.Join(vmDir, "interpreter_gen.go.broken")
- os.WriteFile(dbg, g.buf.Bytes(), 0644)
- fatalf("gofmt failed (%v); wrote unformatted output to %s", err, dbg)
- }
- // INTERPRETER_GEN_OUT lets the CI-match test (interpreter_gen_test.go) regenerate to a
- // temporary file and diff it against the committed one, without clobbering it.
- out := filepath.Join(vmDir, "interpreter_gen.go")
- if env := os.Getenv("INTERPRETER_GEN_OUT"); env != "" {
- out = env
+ fatalf("%v", err)
}
+ out := filepath.Join(vmDir(), "interpreter_gen.go")
if err := os.WriteFile(out, formatted, 0644); err != nil {
fatalf("write %s: %v", out, err)
}
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 86589428a6..166641a183 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -133,7 +133,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
mem.Free()
}()
- // The generated fast path bakes its fork gates at generate time, so it
+ // The generated fast path resolves 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 {
diff --git a/core/vm/interpreter_gen.go b/core/vm/interpreter_gen.go
index 252b69784f..f0c14906f1 100644
--- a/core/vm/interpreter_gen.go
+++ b/core/vm/interpreter_gen.go
@@ -10,8 +10,8 @@ import (
)
// 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 ops (KECCAK256/MLOAD/MSTORE/MSTORE8) call their
+// fork-stable opcodes are inlined with their static gas and stack bounds emitted
+// as constants. Fork-invariant 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.
diff --git a/core/vm/interpreter_gen_test.go b/core/vm/interpreter_gen_test.go
index 786dd14338..b01bba1cd1 100644
--- a/core/vm/interpreter_gen_test.go
+++ b/core/vm/interpreter_gen_test.go
@@ -16,9 +16,10 @@
package vm
-// Tests for the generated interpreter dispatch (interpreter_gen.go): that the
-// committed file is up to date, that it behaves identically to the table loop,
-// and that the fast path keeps its cheap stack helpers inlined.
+// Tests for the generated interpreter dispatch (interpreter_gen.go): that it
+// behaves identically to the table loop, and that the fast path keeps its cheap
+// stack helpers inlined. The check that the committed file matches the generator
+// output lives with the generator, in core/vm/gen.
import (
"bytes"
@@ -26,9 +27,7 @@ import (
"go/parser"
"go/token"
"math/big"
- "os"
"os/exec"
- "path/filepath"
"regexp"
"strings"
"testing"
@@ -41,33 +40,6 @@ import (
"github.com/holiman/uint256"
)
-// TestGeneratedDispatchUpToDate asserts that the committed interpreter_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(), "interpreter_gen.go")
- cmd := exec.Command("go", "run", "./gen")
- cmd.Env = append(os.Environ(), "INTERPRETER_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("interpreter_gen.go")
- if err != nil {
- t.Fatalf("reading committed interpreter_gen.go: %v", err)
- }
- if string(got) != string(want) {
- t.Fatalf("interpreter_gen.go is out of date; run `go generate ./core/vm/...` and commit the result")
- }
-}
-
// Differential test comparing the table loop against the generated dispatch.
//
// These tests prove that the generated dispatch (execUntraced) is bit-identical
@@ -84,7 +56,7 @@ func TestGeneratedDispatchUpToDate(t *testing.T) {
// the tracer test suites instead.
// 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
+// under. Spanning forks exercises the generated 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 {
@@ -318,7 +290,7 @@ func diffBlockCtx(merged bool) BlockContext {
// 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.
+// the generated fork gate does not, so execution must route through the table loop.
func TestExtraEIPs(t *testing.T) {
code := asm(PUSH0, STOP)
statedb := newDiffState(t)
From efb58fc5260594e061f5b759321b2d9a6adf8479 Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Mon, 6 Jul 2026 22:51:14 -0500
Subject: [PATCH 22/23] core/vm, core/vm/gen: derive opcode handler names from
the jump tables instead of restating them
---
core/vm/gen/gen.go | 156 +++++++++++++++++++++----------------
core/vm/genspec.go | 30 +++----
core/vm/interpreter_gen.go | 6 +-
3 files changed, 110 insertions(+), 82 deletions(-)
diff --git a/core/vm/gen/gen.go b/core/vm/gen/gen.go
index 297656669d..054f8e30c8 100644
--- a/core/vm/gen/gen.go
+++ b/core/vm/gen/gen.go
@@ -34,48 +34,54 @@ import (
"github.com/ethereum/go-ethereum/params"
)
-// inlineOps maps an opcode byte to the handler whose body is spliced inline
-// for that opcode. These are the hot, fork-stable opcodes with no dynamic gas.
-// The value is usually an opXxx handler, but PUSH3-PUSH32 and DUP1-DUP16 are
-// factory-built (one shared makePush / makeDup each), so their value is the
-// factory name and emitInlineOp splices the factory body with the per-opcode size.
-// Opcodes not listed here (or in directCallOps) fall through to the default case,
-// which dispatches via the per-fork table.
-var inlineOps = func() map[byte]string {
- m := 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",
+// inlineOps selects the opcodes whose handler bodies are spliced inline: the
+// hot, fork-stable opcodes with no dynamic gas. Which handler that is comes
+// from the per-fork tables via vm.GenForks (see deriveSpecs), not from a
+// restated name. Most resolve to a top-level opXxx handler. PUSH3-PUSH32 and
+// DUP1-DUP16 resolve to makePush / makeDup closures, so emitInlineOp splices
+// the factory body with the per-opcode size instead. Opcodes not selected here
+// (or in directCallOps) fall through to the default case, which dispatches via
+// the per-fork table.
+var inlineOps = func() map[byte]bool {
+ m := map[byte]bool{
+ 0x01: true, 0x02: true, 0x03: true, 0x04: true, 0x05: true, // ADD MUL SUB DIV SDIV
+ 0x06: true, 0x07: true, 0x08: true, 0x09: true, 0x0b: true, // MOD SMOD ADDMOD MULMOD SIGNEXTEND
+ 0x10: true, 0x11: true, 0x12: true, 0x13: true, 0x14: true, 0x15: true, // LT GT SLT SGT EQ ISZERO
+ 0x16: true, 0x17: true, 0x18: true, 0x19: true, 0x1a: true, // AND OR XOR NOT BYTE
+ 0x1b: true, 0x1c: true, 0x1d: true, 0x1e: true, // SHL SHR SAR CLZ
+ 0x50: true, 0x56: true, 0x57: true, 0x58: true, 0x59: true, 0x5b: true, // POP JUMP JUMPI PC MSIZE JUMPDEST
+ 0x5f: true, 0x60: true, 0x61: true, // PUSH0 PUSH1 PUSH2
}
for code := 0x62; code <= 0x7f; code++ { // PUSH3-PUSH32
- m[byte(code)] = "makePush"
+ m[byte(code)] = true
}
for code := 0x80; code <= 0x8f; code++ { // DUP1-DUP16
- m[byte(code)] = "makeDup"
+ m[byte(code)] = true
}
for code := 0x90; code <= 0x9f; code++ { // SWAP1-SWAP16
- m[byte(code)] = fmt.Sprintf("opSwap%d", code-0x8f)
+ m[byte(code)] = true
}
return m
}()
-// directCallOps lists the opcodes (dynamic gas, not inlined) whose handler,
+// directCallOps selects the 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.
-var directCallOps = map[byte][3]string{
- 0x20: {"opKeccak256", "gasKeccak256", "memoryKeccak256"},
- 0x51: {"opMload", "gasMLoad", "memoryMLoad"},
- 0x52: {"opMstore", "gasMStore", "memoryMStore"},
- 0x53: {"opMstore8", "gasMStore8", "memoryMStore8"},
+// (verified by checkDirectCallStable). They are emitted as direct calls to
+// those functions by name, with the names derived from the per-fork tables,
+// instead of the indirect operation.* pointer calls in the default case. An
+// aliased gas var derives as its underlying function, so MLOAD's charge is
+// emitted as pureMemoryGascost rather than through the gasMLoad func var.
+var directCallOps = map[byte]bool{
+ 0x20: true, // KECCAK256
+ 0x51: true, // MLOAD
+ 0x52: true, // MSTORE
+ 0x53: true, // MSTORE8
}
-// opSpec holds the per-opcode constants the generator emits (gas, stack bounds, intro fork), derived from the per-fork tables.
+// opSpec holds the per-opcode facts the generator emits from: the constants
+// (gas, stack bounds, intro fork) and the FuncForPC names of the opcode's
+// handler, dynamic-gas and memory-size functions, all derived from the
+// per-fork tables.
type opSpec struct {
defined bool
name string
@@ -83,6 +89,9 @@ type opSpec struct {
constGas uint64
minStack int
maxStack int
+ execFn string
+ dynFn string
+ memFn string
}
type generator struct {
@@ -502,9 +511,10 @@ func renderInlineExpr(expr ast.Expr, subst map[string]string) string {
}
// deriveSpecs records each opcode's constant values (name, intro fork, static
-// gas, stack bounds) from the first fork that defines it, then checks that the
-// opcodes chosen for inlining and direct-calling are safe to emit as constants
-// by verifying they are fork-stable (see checkStable and checkDirectCallStable).
+// gas, stack bounds) and its handler, dynamic-gas and memory-size function
+// names from the first fork that defines it, then checks that the opcodes
+// chosen for inlining and direct-calling are safe to emit from those specs by
+// verifying they are fork-stable (see checkStable and checkDirectCallStable).
func (g *generator) deriveSpecs(forks []vm.GenFork) {
for code := range 256 {
for _, fork := range forks {
@@ -519,15 +529,18 @@ func (g *generator) deriveSpecs(forks []vm.GenFork) {
constGas: o.ConstantGas,
minStack: o.MinStack,
maxStack: o.MaxStack,
+ execFn: o.ExecuteFn,
+ dynFn: o.DynamicGasFn,
+ memFn: o.MemorySizeFn,
}
break // first fork that defines it wins (its intro fork)
}
}
- // Every inlined opcode must be defined and have fork-stable static
+ // Every inlined opcode must be defined and keep the same handler and static
// gas / stack bounds across all forks where it appears. Bail loudly otherwise.
- for code, handler := range inlineOps {
- g.checkStable(code, handler, forks)
+ for code := range inlineOps {
+ g.checkStable(code, forks)
}
// directCallOps opcodes emit their static gas and stack bounds as constants the
@@ -539,22 +552,22 @@ func (g *generator) deriveSpecs(forks []vm.GenFork) {
}
// checkStable verifies an opcode selected for inlining is safe to inline: it must
-// be defined, its static gas and stack bounds must be the same across every fork
-// it appears in (they are emitted as constants), and it must have no dynamic gas,
-// since an inlined op charges only its constant static gas. It bails loudly
-// otherwise. `what` names the handler for the error message.
-func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
+// be defined, its handler and its static gas and stack bounds must be the same
+// across every fork it appears in (the body and constants are emitted from the
+// first defining fork's spec), and it must have no dynamic gas, since an inlined
+// op charges only its constant static gas. It bails loudly otherwise.
+func (g *generator) checkStable(code byte, forks []vm.GenFork) {
spec := g.specs[code]
if !spec.defined {
- fatalf("opcode %#x (%s) selected for inlining but never defined", code, what)
+ fatalf("opcode %#x selected for inlining but never defined", code)
}
for _, fork := range forks {
o := fork.Ops[code]
if !o.Defined {
continue
}
- if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack || o.DynamicGasFn != "" {
- fatalf("opcode %#x (%s) is not fork-stable (fork %s): cannot inline", code, what, fork.Name)
+ if o.ExecuteFn != spec.execFn || o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack || o.DynamicGasFn != "" {
+ fatalf("opcode %#x (%s) is not fork-stable (fork %s): cannot inline", code, spec.name, fork.Name)
}
}
}
@@ -562,17 +575,14 @@ func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
// checkDirectCallStable verifies a directCallOps opcode is safe to direct-call. Its static
// gas and stack bounds must be the same across every fork it appears in (they are
// emitted as constants), and its handler, gas and memory functions must be the same
-// across those forks too (they are called by name, so a fork that swapped one
-// would otherwise be missed). Unlike checkStable it allows dynamic gas, which
-// directCallOps ops carry by definition. It does not check the directCallOps map's names
-// against the table, which the differential test covers.
+// across those forks too (they are called by the first defining fork's names, so a
+// fork that swapped one would otherwise be missed). Unlike checkStable it allows
+// dynamic gas, which directCallOps ops carry by definition.
func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
spec := g.specs[code]
if !spec.defined {
fatalf("opcode %#x (directCallOps) is never defined", code)
}
- var exec, dyn, mem string
- seen := false
for _, fork := range forks {
o := fork.Ops[code]
if !o.Defined {
@@ -581,15 +591,9 @@ func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack {
fatalf("opcode %#x (%s) is in directCallOps but not fork-stable (fork %s): static gas or stack bounds vary, cannot emit as constants", code, spec.name, fork.Name)
}
- // Handler, gas and memory functions must match across forks too, or
- // direct-calling them by name would skip a fork that swapped one. Names
- // come from FuncForPC via vm.GenForks (aliases resolve to the underlying
- // func, still stable across forks).
- if !seen {
- exec, dyn, mem, seen = o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, true
- } else if o.ExecuteFn != exec || o.DynamicGasFn != dyn || o.MemorySizeFn != mem {
+ if o.ExecuteFn != spec.execFn || o.DynamicGasFn != spec.dynFn || o.MemorySizeFn != spec.memFn {
fatalf("opcode %#x (%s) is in directCallOps but its functions vary by fork (fork %s): got %s/%s/%s, want %s/%s/%s, cannot direct-call",
- code, spec.name, fork.Name, o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, exec, dyn, mem)
+ code, spec.name, fork.Name, o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, spec.execFn, spec.dynFn, spec.memFn)
}
}
}
@@ -645,6 +649,25 @@ func (g *generator) generateStaticGas(amount any) string {
return g.rewriteGasReturns(src)
}
+// closureSegRe matches the anonymous trailing segments of a closure's
+// FuncForPC name, "func31" or a nested "2".
+var closureSegRe = regexp.MustCompile(`^(func\d+|\d+)$`)
+
+// factoryName returns the factory a closure-built handler was created by
+// (e.g. "makeDup" for "newFrontierInstructionSet.makeDup.func37"), or "" for
+// a plain top-level handler name.
+func factoryName(fn string) string {
+ segs := strings.Split(fn, ".")
+ n := len(segs)
+ for n > 0 && closureSegRe.MatchString(segs[n-1]) {
+ n--
+ }
+ if n == len(segs) || n == 0 {
+ return ""
+ }
+ return segs[n-1]
+}
+
// emitInlineOp emits an inlined opcode case: the stack and gas guards followed by
// the spliced opcode body. A fork-introduced opcode wraps that body in a fork gate
// so it runs only when the opcode is active for the current fork, otherwise the
@@ -681,14 +704,16 @@ func (g *generator) emitInlineOp(code byte) {
}
// opcode body
- switch h := inlineOps[code]; h {
+ switch factory := factoryName(spec.execFn); factory {
case "makePush": // PUSH3-PUSH32: splice makePush(size, size)
n := int(code) - 0x5f
g.p("%s", g.spliceOpcodeFactoryBody("makePush", n, n))
case "makeDup": // DUP1-DUP16: splice makeDup(n)
g.p("%s", g.spliceOpcodeFactoryBody("makeDup", int(code)-0x7f))
- default: // the rest: splice the opXxx handler body
- g.p("%s", g.spliceOpcodeBody(h))
+ case "": // the rest: splice the opXxx handler body
+ g.p("%s", g.spliceOpcodeBody(spec.execFn))
+ default:
+ fatalf("opcode %#x (%s) is built by factory %q, which the generator cannot inline", code, spec.name, factory)
}
// If opcode is inactive for this fork, then close the gate
@@ -708,7 +733,6 @@ func (g *generator) emitInlineOp(code byte) {
// fork-invariant ops (see directCallOps).
func (g *generator) emitDirectCallOp(code byte) {
spec := g.specs[code]
- fns := directCallOps[code]
g.p("case %s:\n", spec.name)
// stack bounds check
@@ -729,7 +753,7 @@ func (g *generator) emitDirectCallOp(code byte) {
fatalf("no computeMemorySize gas helper to inline")
}
memSizeSrc := g.renderAst(memSizeFn.Body.List)
- memSizeSrc = strings.ReplaceAll(memSizeSrc, "operation.memorySize", fns[2])
+ memSizeSrc = strings.ReplaceAll(memSizeSrc, "operation.memorySize", spec.memFn)
g.p("%s", g.rewriteStepReturns(memSizeSrc, "memorySize"))
// Splice chargeDynamicGas's body the same way, rewriting operation.dynamicGas to
@@ -739,7 +763,7 @@ func (g *generator) emitDirectCallOp(code byte) {
fatalf("no chargeDynamicGas gas helper to inline")
}
dynGasSrc := g.renderAst(dynGasFn.Body.List)
- dynGasSrc = strings.ReplaceAll(dynGasSrc, "operation.dynamicGas", fns[1])
+ dynGasSrc = strings.ReplaceAll(dynGasSrc, "operation.dynamicGas", spec.dynFn)
g.p("%s", g.rewriteStepReturns(dynGasSrc, ""))
// resize memory
@@ -755,7 +779,7 @@ func (g *generator) emitDirectCallOp(code byte) {
if err != nil {
break mainLoop
}
- `, fns[0])
+ `, spec.execFn)
// advance to the next opcode
g.p(`
@@ -890,9 +914,9 @@ func (g *generator) createFile() {
// one case per inlined or direct-call opcode, in opcode order
for code := range 256 {
b := byte(code)
- if _, named := inlineOps[b]; named {
+ if inlineOps[b] {
g.emitInlineOp(b)
- } else if _, dc := directCallOps[b]; dc {
+ } else if directCallOps[b] {
g.emitDirectCallOp(b)
}
}
diff --git a/core/vm/genspec.go b/core/vm/genspec.go
index 89729554db..e0ee57b289 100644
--- a/core/vm/genspec.go
+++ b/core/vm/genspec.go
@@ -30,12 +30,11 @@ import (
// appears in, and the FuncForPC names of its handler/gas/memory functions) from
// the existing per-fork instruction sets, rather than restating that metadata.
//
-// The function names let the generator confirm the directCold ops are
-// fork-invariant. The fork-varying gas/execute functions themselves are still
-// reached through the active per-fork JumpTable at runtime (see interpreter_gen.go),
-// not emitted by name: several are closures (gasCall, the memoryCopierGas
-// family, makeGasLog) that FuncForPC reports only as anonymous labels, so they
-// could not be called by name in any case.
+// The function names supply the generator's opcode-to-handler mapping and its
+// fork-invariance checks. The fork-varying gas/execute functions themselves are
+// still reached through the active per-fork JumpTable at runtime (see
+// interpreter_gen.go), not emitted by name: several are closures (gasCall, the
+// memoryCopierGas family, makeGasLog) that have no callable name.
// GenOp is the generator-facing scalar metadata for one opcode slot in one fork.
type GenOp struct {
@@ -87,18 +86,23 @@ var genForkOrder = []struct {
{"Amsterdam", "IsAmsterdam", &amsterdamInstructionSet},
}
-// genFnName returns the short FuncForPC name of a jump-table function value
-// (e.g. "gasKeccak256"), or "" if nil. An aliased var resolves to the underlying
-// function (gasMLoad reports "pureMemoryGascost"), which is still stable across
-// forks and so serves the directCold fork-invariance check.
+// genFnName returns the FuncForPC name of a jump-table function value with the
+// package path stripped (e.g. "gasKeccak256"), or "" if nil. An aliased var
+// resolves to the underlying function (gasMLoad reports "pureMemoryGascost").
+// A closure keeps its enclosing chain (DUP7's handler reports
+// "newFrontierInstructionSet.makeDup.func37"), so the generator can tell which
+// factory built it and unrelated closures cannot collide on a bare "funcN".
func genFnName(fn any) string {
v := reflect.ValueOf(fn)
if !v.IsValid() || v.IsNil() {
return ""
}
full := runtime.FuncForPC(v.Pointer()).Name()
- if i := strings.LastIndex(full, "."); i >= 0 {
- return full[i+1:]
+ if i := strings.LastIndex(full, "/"); i >= 0 {
+ full = full[i+1:] // strip the package path, leaving "vm."
+ }
+ if i := strings.Index(full, "."); i >= 0 {
+ full = full[i+1:] // strip the package name
}
return full
}
@@ -109,7 +113,7 @@ 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++ {
+ for code := range 256 {
op := f.set[code]
if op == nil || op.undefined {
continue
diff --git a/core/vm/interpreter_gen.go b/core/vm/interpreter_gen.go
index f0c14906f1..b017cad275 100644
--- a/core/vm/interpreter_gen.go
+++ b/core/vm/interpreter_gen.go
@@ -658,7 +658,7 @@ mainLoop:
}
memorySize = size
- dynamicCost, gerr := gasMLoad(evm, contract, stack, mem, memorySize)
+ dynamicCost, gerr := pureMemoryGascost(evm, contract, stack, mem, memorySize)
if gerr != nil {
res, err = nil, fmt.Errorf("%w: %v", ErrOutOfGas, gerr)
break mainLoop
@@ -706,7 +706,7 @@ mainLoop:
}
memorySize = size
- dynamicCost, gerr := gasMStore(evm, contract, stack, mem, memorySize)
+ dynamicCost, gerr := pureMemoryGascost(evm, contract, stack, mem, memorySize)
if gerr != nil {
res, err = nil, fmt.Errorf("%w: %v", ErrOutOfGas, gerr)
break mainLoop
@@ -754,7 +754,7 @@ mainLoop:
}
memorySize = size
- dynamicCost, gerr := gasMStore8(evm, contract, stack, mem, memorySize)
+ dynamicCost, gerr := pureMemoryGascost(evm, contract, stack, mem, memorySize)
if gerr != nil {
res, err = nil, fmt.Errorf("%w: %v", ErrOutOfGas, gerr)
break mainLoop
From 008e27ae8a437f18a945a7df0e3e6ff7c91a206f Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Fri, 17 Jul 2026 14:09:55 -0500
Subject: [PATCH 23/23] core/vm: fixes due to rebase and removed hardcoded fork
schedule in genspec
---
core/vm/gascosts.go | 4 +-
core/vm/gen/gen.go | 12 ++--
core/vm/genspec.go | 125 ++++++++++++++++++++++---------------
core/vm/interpreter.go | 4 +-
core/vm/interpreter_gen.go | 8 +--
5 files changed, 90 insertions(+), 63 deletions(-)
diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go
index b73732daa3..d1dbc1104f 100644
--- a/core/vm/gascosts.go
+++ b/core/vm/gascosts.go
@@ -84,8 +84,8 @@ func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) {
return prior, ok
}
-// chargeRegularOnly deducts a regular-only cost.
-func (g *GasBudget) chargeRegularOnly(r uint64) error {
+// ChargeRegularOnly deducts a regular-only cost.
+func (g *GasBudget) ChargeRegularOnly(r uint64) error {
if g.RegularGas < r {
return ErrOutOfGas
}
diff --git a/core/vm/gen/gen.go b/core/vm/gen/gen.go
index 054f8e30c8..f2d2691779 100644
--- a/core/vm/gen/gen.go
+++ b/core/vm/gen/gen.go
@@ -113,7 +113,7 @@ func (g *generator) p(format string, args ...any) {
// parseHandlers parses instructions.go, eips.go, stack.go, gascosts.go and
// interpreter.go. It returns the top-level opXxx handlers by name, the
// //gen:inline *Stack helper methods, and the gas/memory helper functions
-// (chargeRegularOnly, computeMemorySize, chargeDynamicGas) whose bodies are
+// (ChargeRegularOnly, computeMemorySize, chargeDynamicGas) whose bodies are
// spliced into the generated dispatch (all by name).
func parseHandlers(vmDir string) (fset *token.FileSet, opcodeHandlers, stackHelpers, gasHelpers map[string]*ast.FuncDecl) {
fset = token.NewFileSet()
@@ -132,7 +132,7 @@ func parseHandlers(vmDir string) (fset *token.FileSet, opcodeHandlers, stackHelp
continue
}
switch {
- case fn.Name.Name == "chargeRegularOnly" || fn.Name.Name == "computeMemorySize" || fn.Name.Name == "chargeDynamicGas" || fn.Name.Name == "chargeVerkleCodeChunkGas": // spliced gas/memory helpers
+ case fn.Name.Name == "ChargeRegularOnly" || fn.Name.Name == "computeMemorySize" || fn.Name.Name == "chargeDynamicGas" || fn.Name.Name == "chargeVerkleCodeChunkGas": // spliced gas/memory helpers
gasHelpers[fn.Name.Name] = fn
case fn.Recv == nil: // top-level opXxx handler
opcodeHandlers[fn.Name.Name] = fn
@@ -627,7 +627,7 @@ func (g *generator) generateStackChecks(minExpr, maxExpr any, under, over bool)
}
// generateStaticGas returns the static-gas charge, spliced call-free from the
-// chargeRegularOnly body for amount: a constant on the inlined and
+// ChargeRegularOnly body for amount: a constant on the inlined and
// direct-call paths, operation.constantGas in the table path. The receiver maps
// to contract.Gas and the method's single uint64 parameter to amount, substituted
// textually on word boundaries (which cannot touch fields like RegularGas). Its
@@ -635,13 +635,13 @@ func (g *generator) generateStackChecks(minExpr, maxExpr any, under, over bool)
// is dropped so the opcode falls through to its remaining steps (see
// rewriteGasReturns).
func (g *generator) generateStaticGas(amount any) string {
- fn := g.gasHelpers["chargeRegularOnly"]
+ fn := g.gasHelpers["ChargeRegularOnly"]
if fn == nil {
- fatalf("no chargeRegularOnly gas helper to inline")
+ fatalf("no ChargeRegularOnly gas helper to inline")
}
names := paramNames(fn)
if len(names) != 1 {
- fatalf("chargeRegularOnly takes %d params, want 1", len(names))
+ fatalf("ChargeRegularOnly takes %d params, want 1", len(names))
}
src := g.renderAst(fn.Body.List)
src = regexp.MustCompile(`\b`+recvName(fn)+`\b`).ReplaceAllString(src, "contract.Gas")
diff --git a/core/vm/genspec.go b/core/vm/genspec.go
index e0ee57b289..f4dba5569d 100644
--- a/core/vm/genspec.go
+++ b/core/vm/genspec.go
@@ -22,6 +22,8 @@ import (
"reflect"
"runtime"
"strings"
+
+ "github.com/ethereum/go-ethereum/params"
)
// This file exposes the interpreter's opcode metadata to the code generator in
@@ -56,35 +58,13 @@ type GenFork struct {
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},
-}
+// codegenSkippedForks are forks in geth's fork schedule that the generator does
+// not give a lane. Verkle/UBT is the only one: over its Shanghai base it adds no
+// opcodes, it only swaps gas and execute functions on existing ones (enable4762),
+// which the generated switch picks up from the active table at runtime. Emitting
+// a lane for it would trip the generator's fork-stability check (an inlined op's
+// execute function is not allowed to vary by fork).
+var codegenSkippedForks = map[string]bool{"IsUBT": true}
// genFnName returns the FuncForPC name of a jump-table function value with the
// package path stripped (e.g. "gasKeccak256"), or "" if nil. An aliased var
@@ -108,28 +88,75 @@ func genFnName(fn any) string {
}
// GenForks returns per-fork opcode metadata for the interpreter code generator
-// (core/vm/gen). It is exported solely for that purpose.
+// (core/vm/gen), one entry per fork that changes the opcode table, oldest to
+// newest. It derives the progression from params.Rules and LookupInstructionSet
+// (in params.Rules declaration order, which is chronological) so new forks are
+// picked up without restating a list here.
func GenForks() []GenFork {
- out := make([]GenFork, len(genForkOrder))
- for i, f := range genForkOrder {
- gf := GenFork{Name: f.name, RuleField: f.rule}
- for code := range 256 {
- 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,
- ExecuteFn: genFnName(op.execute),
- DynamicGasFn: genFnName(op.dynamicGas),
- MemorySizeFn: genFnName(op.memorySize),
- }
+ // Frontier is always active and carries no rule gate.
+ frontier, _ := LookupInstructionSet(params.Rules{})
+ out := []GenFork{genFork("Frontier", "", &frontier)}
+
+ rt := reflect.TypeOf(params.Rules{})
+ for i := range rt.NumField() {
+ field := rt.Field(i)
+ if field.Type.Kind() != reflect.Bool || codegenSkippedForks[field.Name] {
+ continue
}
- out[i] = gf
+ // Activate only this field so the fork resolves to the table it gates.
+ var rules params.Rules
+ reflect.ValueOf(&rules).Elem().Field(i).SetBool(true)
+ set, _ := LookupInstructionSet(rules)
+ if sameOps(&set, &frontier) {
+ continue // this rule does not change the opcode table
+ }
+ out = append(out, genFork(strings.TrimPrefix(field.Name, "Is"), field.Name, &set))
}
return out
}
+
+// genFork extracts the generator-facing per-opcode metadata from one fork's
+// instruction set.
+func genFork(name, rule string, set *JumpTable) GenFork {
+ gf := GenFork{Name: name, RuleField: rule}
+ for code := range 256 {
+ op := 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,
+ ExecuteFn: genFnName(op.execute),
+ DynamicGasFn: genFnName(op.dynamicGas),
+ MemorySizeFn: genFnName(op.memorySize),
+ }
+ }
+ return gf
+}
+
+// sameOps reports whether two instruction sets carry identical per-opcode static
+// metadata: which slots are defined, and their static gas and stack bounds. It
+// is used to tell whether a fork actually changes the opcode table. Handler,
+// dynamic-gas and memory-size functions are ignored (they cannot be compared for
+// equality and are reached through the table at runtime).
+func sameOps(a, b *JumpTable) bool {
+ for code := range 256 {
+ oa, ob := a[code], b[code]
+ undefA := oa == nil || oa.undefined
+ undefB := ob == nil || ob.undefined
+ if undefA != undefB {
+ return false
+ }
+ if undefA {
+ continue
+ }
+ if oa.constantGas != ob.constantGas || oa.minStack != ob.minStack || oa.maxStack != ob.maxStack {
+ return false
+ }
+ }
+ return true
+}
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 166641a183..c8039f605d 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -209,7 +209,7 @@ func (evm *EVM) execTraced(scope *ScopeContext) (ret []byte, err error) {
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
// for tracing: this gas consumption event is emitted below in the debug section.
- if err := contract.Gas.chargeRegularOnly(cost); err != nil {
+ if err := contract.Gas.ChargeRegularOnly(cost); err != nil {
return nil, err
}
@@ -287,7 +287,7 @@ func (contract *Contract) chargeDynamicGas(operation *operation, evm *EVM, stack
// A regular-only deduction when there is no state gas, otherwise the full
// multidimensional charge through the reservoir.
if dynamicCost.StateGas == 0 {
- if cerr := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
+ if cerr := contract.Gas.ChargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
return dynamicCost, cerr
}
} else if !contract.Gas.charge(dynamicCost) {
diff --git a/core/vm/interpreter_gen.go b/core/vm/interpreter_gen.go
index b017cad275..a8d075a5b8 100644
--- a/core/vm/interpreter_gen.go
+++ b/core/vm/interpreter_gen.go
@@ -601,7 +601,7 @@ mainLoop:
break mainLoop
}
if dynamicCost.StateGas == 0 {
- if cerr := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
+ if cerr := contract.Gas.ChargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
res, err = nil, cerr
break mainLoop
}
@@ -664,7 +664,7 @@ mainLoop:
break mainLoop
}
if dynamicCost.StateGas == 0 {
- if cerr := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
+ if cerr := contract.Gas.ChargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
res, err = nil, cerr
break mainLoop
}
@@ -712,7 +712,7 @@ mainLoop:
break mainLoop
}
if dynamicCost.StateGas == 0 {
- if cerr := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
+ if cerr := contract.Gas.ChargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
res, err = nil, cerr
break mainLoop
}
@@ -760,7 +760,7 @@ mainLoop:
break mainLoop
}
if dynamicCost.StateGas == 0 {
- if cerr := contract.Gas.chargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
+ if cerr := contract.Gas.ChargeRegularOnly(dynamicCost.RegularGas); cerr != nil {
res, err = nil, cerr
break mainLoop
}