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