mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-19 03:10:48 +00:00
core/vm, core/vm/gen: share the traced loop's gas and memory steps with the generated dispatch and other cleanup
This commit is contained in:
parent
834f7017aa
commit
f0d9dc1c8b
6 changed files with 453 additions and 349 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// 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 <err>` 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 <err>` 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 <err>` 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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
Loading…
Reference in a new issue