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] 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 }