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 }