From bcb008f33d8d46cce31ae352741d983d5c42bfe2 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Thu, 16 Apr 2026 08:24:01 +0800 Subject: [PATCH] refactor(cmd/evm): improve block/state test runner #30633 --- cmd/evm/disasm.go | 55 ---- cmd/evm/eest.go | 50 ++++ cmd/evm/internal/compiler/compiler.go | 39 --- cmd/evm/main.go | 282 +++++++++++-------- cmd/evm/reporter.go | 103 +++++++ cmd/evm/runner.go | 350 ++++++++++++++++-------- cmd/evm/{compiler.go => runner_test.go} | 49 ++-- cmd/evm/staterunner.go | 184 +++++++++---- tests/state_test_util.go | 31 ++- 9 files changed, 734 insertions(+), 409 deletions(-) delete mode 100644 cmd/evm/disasm.go create mode 100644 cmd/evm/eest.go delete mode 100644 cmd/evm/internal/compiler/compiler.go create mode 100644 cmd/evm/reporter.go rename cmd/evm/{compiler.go => runner_test.go} (51%) diff --git a/cmd/evm/disasm.go b/cmd/evm/disasm.go deleted file mode 100644 index 2e09f5aab693..000000000000 --- a/cmd/evm/disasm.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see . - -package main - -import ( - "errors" - "fmt" - "os" - "strings" - - "github.com/XinFinOrg/XDPoSChain/core/asm" - "github.com/urfave/cli/v2" -) - -var disasmCommand = &cli.Command{ - Action: disasmCmd, - Name: "disasm", - Usage: "disassembles evm binary", - ArgsUsage: "", -} - -func disasmCmd(ctx *cli.Context) error { - var in string - switch { - case len(ctx.Args().First()) > 0: - fn := ctx.Args().First() - input, err := os.ReadFile(fn) - if err != nil { - return err - } - in = string(input) - case ctx.IsSet(InputFlag.Name): - in = ctx.String(InputFlag.Name) - default: - return errors.New("missing filename or --input value") - } - - code := strings.TrimSpace(in) - fmt.Printf("%v\n", code) - return asm.PrintDisassembled(code) -} diff --git a/cmd/evm/eest.go b/cmd/evm/eest.go new file mode 100644 index 000000000000..91aca188c21c --- /dev/null +++ b/cmd/evm/eest.go @@ -0,0 +1,50 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package main + +import "regexp" + +var ( + eestTestMetadataPattern = `tests\/([^\/]+)\/([^\/]+)\/([^:]+)::([^[]+)\[fork_([^-\]]+)-[^-]+-(.+)\]` + eestTestMetadataRegexp = regexp.MustCompile(eestTestMetadataPattern) +) + +// testMetadata provides more granular access to the test information encoded +// within its filename by the execution spec test (EEST). +type testMetadata struct { + fork string + module string // which python module generated the test, e.g. EIP-7702 + file string // exact file the test came from, e.g. test_gas.py + function string // func that created the test, e.g. test_valid_mcopy_operations + parameters string // the name of the parameters which were used to fill the test, e.g. zero_inputs +} + +// parseTestMetadata reads a test name and parses out more specific information +// about the test. +func parseTestMetadata(s string) *testMetadata { + match := eestTestMetadataRegexp.FindStringSubmatch(s) + if len(match) == 0 { + return nil + } + return &testMetadata{ + fork: match[5], + module: match[2], + file: match[3], + function: match[4], + parameters: match[6], + } +} diff --git a/cmd/evm/internal/compiler/compiler.go b/cmd/evm/internal/compiler/compiler.go deleted file mode 100644 index 2d86df4a60a5..000000000000 --- a/cmd/evm/internal/compiler/compiler.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see . - -package compiler - -import ( - "errors" - "fmt" - - "github.com/XinFinOrg/XDPoSChain/core/asm" -) - -func Compile(fn string, src []byte, debug bool) (string, error) { - compiler := asm.NewCompiler(debug) - compiler.Feed(asm.Lex(fn, src, debug)) - - bin, compileErrors := compiler.Compile() - if len(compileErrors) > 0 { - // report errors - for _, err := range compileErrors { - fmt.Printf("%s:%v\n", fn, err) - } - return "", errors.New("compiling failed") - } - return bin, nil -} diff --git a/cmd/evm/main.go b/cmd/evm/main.go index ec9d3e42457f..4a007a7a361e 100644 --- a/cmd/evm/main.go +++ b/cmd/evm/main.go @@ -19,158 +19,140 @@ package main import ( "fmt" - "math/big" + "io/fs" "os" + "path/filepath" + "slices" + "github.com/XinFinOrg/XDPoSChain/core/state" + "github.com/XinFinOrg/XDPoSChain/core/tracing" + "github.com/XinFinOrg/XDPoSChain/eth/tracers/logger" + "github.com/XinFinOrg/XDPoSChain/internal/debug" "github.com/XinFinOrg/XDPoSChain/internal/flags" "github.com/urfave/cli/v2" ) -var ( - app = flags.NewApp("the evm command line interface") -) +// Some other nice-to-haves: +// * accumulate traces into an object to bundle with test +// * write tx identifier for trace before hand (blocktest only) +// * combine blocktest and statetest runner logic using unified test interface + +const traceCategory = "TRACING" var ( - DebugFlag = &cli.BoolFlag{ - Name: "debug", - Usage: "output full trace logs", + // Test running flags. + RunFlag = &cli.StringFlag{ + Name: "run", + Value: ".*", + Usage: "Run only those tests matching the regular expression.", Category: flags.VMCategory, } - MemProfileFlag = &cli.StringFlag{ - Name: "memprofile", - Usage: "creates a memory profile at the given path", - Category: flags.VMCategory, - } - CPUProfileFlag = &cli.StringFlag{ - Name: "cpuprofile", - Usage: "creates a CPU profile at the given path", - Category: flags.VMCategory, - } - StatDumpFlag = &cli.BoolFlag{ - Name: "statdump", - Usage: "displays stack and heap memory information", - Category: flags.VMCategory, - } - CodeFlag = &cli.StringFlag{ - Name: "code", - Usage: "EVM code", - Category: flags.VMCategory, - } - CodeFileFlag = &cli.StringFlag{ - Name: "codefile", - Usage: "File containing EVM code. If '-' is specified, code is read from stdin ", - Category: flags.VMCategory, - } - GasFlag = &cli.Uint64Flag{ - Name: "gas", - Usage: "gas limit for the evm", - Value: 10000000000, - Category: flags.VMCategory, - } - PriceFlag = &flags.BigFlag{ - Name: "price", - Usage: "price set for the evm", - Value: new(big.Int), - Category: flags.VMCategory, - } - ValueFlag = &flags.BigFlag{ - Name: "value", - Usage: "value set for the evm", - Value: new(big.Int), + BenchFlag = &cli.BoolFlag{ + Name: "bench", + Usage: "benchmark the execution", Category: flags.VMCategory, } + + // Debugging flags. DumpFlag = &cli.BoolFlag{ Name: "dump", Usage: "dumps the state after the run", Category: flags.VMCategory, } - InputFlag = &cli.StringFlag{ - Name: "input", - Usage: "input for the EVM", - Category: flags.VMCategory, - } - VerbosityFlag = &cli.IntFlag{ - Name: "verbosity", - Usage: "sets the verbosity level", - Category: flags.VMCategory, - } - CreateFlag = &cli.BoolFlag{ - Name: "create", - Usage: "indicates the action should be create rather than call", - Category: flags.VMCategory, - } - GenesisFlag = &cli.StringFlag{ - Name: "prestate", - Usage: "JSON file with prestate (genesis) config", - Category: flags.VMCategory, + HumanReadableFlag = &cli.BoolFlag{ + Name: "human", + Usage: "\"Human-readable\" output", } - MachineFlag = &cli.BoolFlag{ - Name: "json", - Usage: "output trace logs in machine readable format (json)", - Category: flags.VMCategory, - } - SenderFlag = &cli.StringFlag{ - Name: "sender", - Usage: "The transaction origin", - Category: flags.VMCategory, - } - ReceiverFlag = &cli.StringFlag{ - Name: "receiver", - Usage: "The transaction receiver (execution context)", + StatDumpFlag = &cli.BoolFlag{ + Name: "statdump", + Usage: "displays stack and heap memory information", Category: flags.VMCategory, } - DisableMemoryFlag = &cli.BoolFlag{ - Name: "nomemory", + + // Tracing flags. + TraceFlag = &cli.BoolFlag{ + Name: "trace", + Usage: "Enable tracing and output trace log.", + Category: traceCategory, + } + TraceFormatFlag = &cli.StringFlag{ + Name: "trace.format", + Usage: "Trace output format to use (json|struct|md)", + Value: "json", + Category: traceCategory, + } + TraceDisableMemoryFlag = &cli.BoolFlag{ + Name: "trace.nomemory", + Aliases: []string{"nomemory"}, Value: true, Usage: "disable memory output", - Category: flags.VMCategory, + Category: traceCategory, } - DisableStackFlag = &cli.BoolFlag{ - Name: "nostack", + TraceDisableStackFlag = &cli.BoolFlag{ + Name: "trace.nostack", + Aliases: []string{"nostack"}, Usage: "disable stack output", - Category: flags.VMCategory, + Category: traceCategory, } - DisableStorageFlag = &cli.BoolFlag{ - Name: "nostorage", + TraceDisableStorageFlag = &cli.BoolFlag{ + Name: "trace.nostorage", + Aliases: []string{"nostorage"}, Usage: "disable storage output", - Category: flags.VMCategory, + Category: traceCategory, } - DisableReturnDataFlag = &cli.BoolFlag{ - Name: "noreturndata", + TraceDisableReturnDataFlag = &cli.BoolFlag{ + Name: "trace.noreturndata", + Aliases: []string{"noreturndata"}, Value: true, - Usage: "enable return data output", - Category: flags.VMCategory, + Usage: "disable return data output", + Category: traceCategory, + } + + // Deprecated flags. + DebugFlag = &cli.BoolFlag{ + Name: "debug", + Usage: "output full trace logs (deprecated)", + Hidden: true, + Category: traceCategory, + } + MachineFlag = &cli.BoolFlag{ + Name: "json", + Usage: "output trace logs in machine readable format, json (deprecated)", + Hidden: true, + Category: traceCategory, } ) +// traceFlags contains flags that configure tracing output. +var traceFlags = []cli.Flag{ + TraceFlag, + TraceFormatFlag, + TraceDisableStackFlag, + TraceDisableMemoryFlag, + TraceDisableStorageFlag, + TraceDisableReturnDataFlag, + + // deprecated + DebugFlag, + MachineFlag, +} + +var app = flags.NewApp("the evm command line interface") + func init() { - app.Flags = []cli.Flag{ - CreateFlag, - DebugFlag, - VerbosityFlag, - CodeFlag, - CodeFileFlag, - GasFlag, - PriceFlag, - ValueFlag, - DumpFlag, - InputFlag, - MemProfileFlag, - CPUProfileFlag, - StatDumpFlag, - GenesisFlag, - MachineFlag, - SenderFlag, - ReceiverFlag, - DisableMemoryFlag, - DisableStackFlag, - } + app.Flags = debug.Flags app.Commands = []*cli.Command{ - compileCommand, - disasmCommand, runCommand, stateTestCommand, } + app.Before = func(ctx *cli.Context) error { + flags.MigrateGlobalFlags(ctx) + return debug.Setup(ctx) + } + app.After = func(ctx *cli.Context) error { + debug.Exit() + return nil + } } func main() { @@ -179,3 +161,71 @@ func main() { os.Exit(1) } } + +// tracerFromFlags parses the cli flags and returns the specified tracer, or an error for unknown formats. +func tracerFromFlags(ctx *cli.Context) (*tracing.Hooks, error) { + config := &logger.Config{ + EnableMemory: !ctx.Bool(TraceDisableMemoryFlag.Name), + DisableStack: ctx.Bool(TraceDisableStackFlag.Name), + DisableStorage: ctx.Bool(TraceDisableStorageFlag.Name), + EnableReturnData: !ctx.Bool(TraceDisableReturnDataFlag.Name), + } + switch { + case ctx.Bool(TraceFlag.Name): + switch format := ctx.String(TraceFormatFlag.Name); format { + case "struct": + return logger.NewStreamingStructLogger(config, os.Stderr).Hooks(), nil + case "json": + return logger.NewJSONLogger(config, os.Stderr), nil + case "md", "markdown": + return logger.NewMarkdownLogger(config, os.Stderr).Hooks(), nil + default: + return nil, cli.Exit(fmt.Sprintf("unknown trace format: %q", format), 1) + } + // Deprecated ways of configuring tracing. + case ctx.Bool(MachineFlag.Name): + return logger.NewJSONLogger(config, os.Stderr), nil + case ctx.Bool(DebugFlag.Name): + return logger.NewStreamingStructLogger(config, os.Stderr).Hooks(), nil + default: + return nil, nil + } +} + +// collectFiles walks the given path. If the path is a directory, it returns +// a list of all .json files found recursively under the directory. +// Otherwise, if path points to a file, it returns that path. +func collectFiles(path string) ([]string, error) { + var out []string + if info, err := os.Stat(path); err == nil && !info.IsDir() { + // User explicitly pointed out a file, ignore extension. + return []string{path}, nil + } + err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && filepath.Ext(info.Name()) == ".json" { + out = append(out, path) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to collect files from %q: %v", path, err) + } + if len(out) > 1 { + slices.Sort(out) + } + return out, nil +} + +// dump returns a state dump for the most current trie. +func dump(s *state.StateDB) *state.Dump { + root := s.IntermediateRoot(false) + cpy, err := state.New(root, s.Database()) + if err != nil { + return nil + } + dump := cpy.RawDump(nil) + return &dump +} diff --git a/cmd/evm/reporter.go b/cmd/evm/reporter.go new file mode 100644 index 000000000000..9f71c6f7f467 --- /dev/null +++ b/cmd/evm/reporter.go @@ -0,0 +1,103 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/core/state" + "github.com/urfave/cli/v2" + "golang.org/x/term" +) + +var ( + PASS string + FAIL string +) + +func init() { + if isTerminal(os.Stdout.Fd()) { + PASS = "\033[32mPASS\033[0m" + FAIL = "\033[31mFAIL\033[0m" + } else { + PASS = "PASS" + FAIL = "FAIL" + } +} + +func isTerminal(fd uintptr) bool { + return term.IsTerminal(int(fd)) +} + +// testResult contains the execution status after running a state test, any +// error that might have occurred and a dump of the final state if requested. +type testResult struct { + Name string `json:"name"` + Pass bool `json:"pass"` + Root *common.Hash `json:"stateRoot,omitempty"` + Fork string `json:"fork"` + Error string `json:"error,omitempty"` + State *state.Dump `json:"state,omitempty"` + Stats *execStats `json:"benchStats,omitempty"` +} + +func (r testResult) String() string { + var status string + if r.Pass { + status = fmt.Sprintf("[%s]", PASS) + } else { + status = fmt.Sprintf("[%s]", FAIL) + } + info := r.Name + m := parseTestMetadata(r.Name) + if m != nil { + info = fmt.Sprintf("%s %s, param=%s", m.module, m.function, m.parameters) + } + var extra string + if !r.Pass { + extra = fmt.Sprintf(", err=%v, fork=%s", r.Error, r.Fork) + } + out := fmt.Sprintf("%s %s%s", status, info, extra) + if r.State != nil { + state, _ := json.MarshalIndent(r.State, "", " ") + out += "\n" + string(state) + } + return out +} + +// report prints the after-test summary. +func report(ctx *cli.Context, results []testResult) { + if ctx.Bool(HumanReadableFlag.Name) { + pass := 0 + for _, r := range results { + if r.Pass { + pass++ + } + } + for _, r := range results { + fmt.Println(r) + } + fmt.Println("--") + fmt.Printf("%d tests passed, %d tests failed.\n", pass, len(results)-pass) + return + } + out, _ := json.MarshalIndent(results, "", " ") + fmt.Println(string(out)) +} diff --git a/cmd/evm/runner.go b/cmd/evm/runner.go index fe6f898c2f31..5603bdcd109a 100644 --- a/cmd/evm/runner.go +++ b/cmd/evm/runner.go @@ -20,15 +20,17 @@ import ( "bytes" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "math/big" "os" goruntime "runtime" - "runtime/pprof" + "slices" + "strings" + "testing" "time" - "github.com/XinFinOrg/XDPoSChain/cmd/evm/internal/compiler" "github.com/XinFinOrg/XDPoSChain/cmd/utils" "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/core" @@ -38,9 +40,9 @@ import ( "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/core/vm" "github.com/XinFinOrg/XDPoSChain/core/vm/runtime" - "github.com/XinFinOrg/XDPoSChain/eth/tracers/logger" "github.com/XinFinOrg/XDPoSChain/internal/flags" "github.com/XinFinOrg/XDPoSChain/params" + "github.com/XinFinOrg/XDPoSChain/trie" "github.com/urfave/cli/v2" ) @@ -50,13 +52,83 @@ var runCommand = &cli.Command{ Usage: "run arbitrary evm binary", ArgsUsage: "", Description: `The run command runs arbitrary EVM code.`, + Flags: slices.Concat([]cli.Flag{ + BenchFlag, + CodeFileFlag, + CreateFlag, + GasFlag, + GenesisFlag, + InputFlag, + InputFileFlag, + PriceFlag, + ReceiverFlag, + SenderFlag, + ValueFlag, + StatDumpFlag, + DumpFlag, + }, traceFlags), } +var ( + CodeFileFlag = &cli.StringFlag{ + Name: "codefile", + Usage: "File containing EVM code. If '-' is specified, code is read from stdin ", + Category: flags.VMCategory, + } + CreateFlag = &cli.BoolFlag{ + Name: "create", + Usage: "Indicates the action should be create rather than call", + Category: flags.VMCategory, + } + GasFlag = &cli.Uint64Flag{ + Name: "gas", + Usage: "Gas limit for the evm", + Value: 10000000000, + Category: flags.VMCategory, + } + GenesisFlag = &cli.StringFlag{ + Name: "prestate", + Usage: "JSON file with prestate (genesis) config", + Category: flags.VMCategory, + } + InputFlag = &cli.StringFlag{ + Name: "input", + Usage: "Input for the EVM", + Category: flags.VMCategory, + } + InputFileFlag = &cli.StringFlag{ + Name: "inputfile", + Usage: "File containing input for the EVM", + Category: flags.VMCategory, + } + PriceFlag = &flags.BigFlag{ + Name: "price", + Usage: "Price set for the evm", + Value: new(big.Int), + Category: flags.VMCategory, + } + ReceiverFlag = &cli.StringFlag{ + Name: "receiver", + Usage: "The transaction receiver (execution context)", + Category: flags.VMCategory, + } + SenderFlag = &cli.StringFlag{ + Name: "sender", + Usage: "The transaction origin", + Category: flags.VMCategory, + } + ValueFlag = &flags.BigFlag{ + Name: "value", + Usage: "Value set for the evm", + Value: new(big.Int), + Category: flags.VMCategory, + } +) + // readGenesis will read the given JSON format genesis file and return // the initialized Genesis structure func readGenesis(genesisPath string) *core.Genesis { // Make sure we have a valid genesis JSON - //genesisPath := ctx.Args().First() if len(genesisPath) == 0 { utils.Fatalf("Must supply path to genesis JSON file") } @@ -73,96 +145,143 @@ func readGenesis(genesisPath string) *core.Genesis { return genesis } -func runCmd(ctx *cli.Context) error { - logconfig := &logger.Config{ - EnableMemory: !ctx.Bool(DisableMemoryFlag.Name), - DisableStack: ctx.Bool(DisableStackFlag.Name), - DisableStorage: ctx.Bool(DisableStorageFlag.Name), - EnableReturnData: !ctx.Bool(DisableReturnDataFlag.Name), - Debug: ctx.Bool(DebugFlag.Name), +type execStats struct { + Time time.Duration `json:"time"` // The execution Time. + Allocs int64 `json:"allocs"` // The number of heap allocations during execution. + BytesAllocated int64 `json:"bytesAllocated"` // The cumulative number of bytes allocated during execution. + GasUsed uint64 `json:"gasUsed"` // the amount of gas used during execution +} + +var errInconsistentBenchmarkResult = errors.New("benchmark execution was nondeterministic") + +func newBenchmarkMismatchError(format string, args ...any) error { + return fmt.Errorf("%w: "+format, append([]any{errInconsistentBenchmarkResult}, args...)...) +} + +func timedExec(bench bool, execFunc func() ([]byte, uint64, error)) ([]byte, execStats, error) { + if bench { + testing.Init() + // Do one warm-up run + output, gasUsed, err := execFunc() + var benchErr error + result := testing.Benchmark(func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + haveOutput, haveGasUsed, haveErr := execFunc() + if !bytes.Equal(haveOutput, output) { + benchErr = newBenchmarkMismatchError("output differs\nhave %x\nwant %x", haveOutput, output) + b.StopTimer() + return + } + if haveGasUsed != gasUsed { + benchErr = newBenchmarkMismatchError("gas differs, have %v want %v", haveGasUsed, gasUsed) + b.StopTimer() + return + } + if (haveErr == nil) != (err == nil) || (haveErr != nil && err != nil && haveErr.Error() != err.Error()) { + benchErr = newBenchmarkMismatchError("err differs, have %v want %v", haveErr, err) + b.StopTimer() + return + } + } + }) + if benchErr != nil { + return output, execStats{GasUsed: gasUsed}, benchErr + } + // Get the average execution time from the benchmarking result. + // There are other useful stats here that could be reported. + stats := execStats{ + Time: time.Duration(result.NsPerOp()), + Allocs: result.AllocsPerOp(), + BytesAllocated: result.AllocedBytesPerOp(), + GasUsed: gasUsed, + } + return output, stats, err + } + var memStatsBefore, memStatsAfter goruntime.MemStats + goruntime.ReadMemStats(&memStatsBefore) + t0 := time.Now() + output, gasUsed, err := execFunc() + duration := time.Since(t0) + goruntime.ReadMemStats(&memStatsAfter) + stats := execStats{ + Time: duration, + Allocs: int64(memStatsAfter.Mallocs - memStatsBefore.Mallocs), + BytesAllocated: int64(memStatsAfter.TotalAlloc - memStatsBefore.TotalAlloc), + GasUsed: gasUsed, } + return output, stats, err +} +func runCmd(ctx *cli.Context) error { var ( - tracer *tracing.Hooks - statedb *state.StateDB - chainConfig *params.ChainConfig - sender = common.StringToAddress("sender") - receiver = common.StringToAddress("receiver") - genesisConfig *core.Genesis + tracer *tracing.Hooks + prestate *state.StateDB + chainConfig *params.ChainConfig + sender = common.BytesToAddress([]byte("sender")) + receiver = common.BytesToAddress([]byte("receiver")) + preimages = ctx.Bool(DumpFlag.Name) ) - if ctx.Bool(MachineFlag.Name) { - tracer = logger.NewJSONLogger(logconfig, os.Stdout) - } else if ctx.Bool(DebugFlag.Name) { - tracer = logger.NewStreamingStructLogger(logconfig, os.Stderr).Hooks() + var err error + tracer, err = tracerFromFlags(ctx) + if err != nil { + return err } - + initialGas := ctx.Uint64(GasFlag.Name) + genesisConfig := new(core.Genesis) + genesisConfig.GasLimit = initialGas if ctx.String(GenesisFlag.Name) != "" { - gen := readGenesis(ctx.String(GenesisFlag.Name)) - genesisConfig = gen - db := rawdb.NewMemoryDatabase() - genesis := gen.MustCommit(db) - statedb, _ = state.New(genesis.Root(), state.NewDatabase(db)) - chainConfig = gen.Config + genesisConfig = readGenesis(ctx.String(GenesisFlag.Name)) + if genesisConfig.GasLimit != 0 { + initialGas = genesisConfig.GasLimit + } } else { - db := rawdb.NewMemoryDatabase() - statedb, _ = state.New(types.EmptyRootHash, state.NewDatabase(db)) - genesisConfig = new(core.Genesis) + genesisConfig.Config = params.AllDevChainProtocolChanges } + + db := rawdb.NewMemoryDatabase() + triedb := trie.NewDatabaseWithConfig(db, &trie.Config{Preimages: preimages}) + defer triedb.Close() + genesis := genesisConfig.MustCommit(db) + sdb := state.NewDatabaseWithNodeDB(db, triedb) + prestate, _ = state.New(genesis.Root(), sdb) + chainConfig = genesisConfig.Config + if ctx.String(SenderFlag.Name) != "" { sender = common.HexToAddress(ctx.String(SenderFlag.Name)) } - statedb.CreateAccount(sender) if ctx.String(ReceiverFlag.Name) != "" { receiver = common.HexToAddress(ctx.String(ReceiverFlag.Name)) } - var ( - code []byte - ret []byte - err error - ) - // The '--code' or '--codefile' flag overrides code in state - if ctx.String(CodeFileFlag.Name) != "" { - var hexcode []byte - var err error + var code []byte + codeFileFlag := ctx.String(CodeFileFlag.Name) + hexcode := ctx.Args().First() + + // The '--codefile' flag overrides code in state + if codeFileFlag == "-" { // If - is specified, it means that code comes from stdin - if ctx.String(CodeFileFlag.Name) == "-" { - //Try reading from stdin - if hexcode, err = io.ReadAll(os.Stdin); err != nil { - fmt.Printf("Could not load code from stdin: %v\n", err) - os.Exit(1) - } - } else { - // Codefile with hex assembly - if hexcode, err = os.ReadFile(ctx.String(CodeFileFlag.Name)); err != nil { - fmt.Printf("Could not load code from file: %v\n", err) - os.Exit(1) - } - } - code = common.Hex2Bytes(string(bytes.TrimRight(hexcode, "\n"))) - } else if ctx.String(CodeFlag.Name) != "" { - code = common.Hex2Bytes(ctx.String(CodeFlag.Name)) - } else if fn := ctx.Args().First(); len(fn) > 0 { - // EASM-file to compile - src, err := os.ReadFile(fn) + input, err := io.ReadAll(os.Stdin) if err != nil { - return err + return cli.Exit(fmt.Sprintf("Could not load code from stdin: %v", err), 1) } - bin, err := compiler.Compile(fn, src, false) + hexcode = string(input) + } else if codeFileFlag != "" { + // Codefile with hex assembly + input, err := os.ReadFile(codeFileFlag) if err != nil { - return err + return cli.Exit(fmt.Sprintf("Could not load code from file: %v", err), 1) } - code = common.Hex2Bytes(bin) + hexcode = string(input) } - initialGas := ctx.Uint64(GasFlag.Name) - if genesisConfig.GasLimit != 0 { - initialGas = genesisConfig.GasLimit - } + hexcode = strings.TrimSpace(hexcode) + code = common.FromHex(hexcode) + runtimeConfig := runtime.Config{ Origin: sender, - State: statedb, + State: prestate, GasLimit: initialGas, GasPrice: flags.GlobalBig(ctx, PriceFlag.Name), Value: flags.GlobalBig(ctx, ValueFlag.Name), @@ -170,56 +289,66 @@ func runCmd(ctx *cli.Context) error { Time: genesisConfig.Timestamp, Coinbase: genesisConfig.Coinbase, BlockNumber: new(big.Int).SetUint64(genesisConfig.Number), + BaseFee: genesisConfig.BaseFee, EVMConfig: vm.Config{ Tracer: tracer, }, } - if cpuProfilePath := ctx.String(CPUProfileFlag.Name); cpuProfilePath != "" { - f, err := os.Create(cpuProfilePath) - if err != nil { - fmt.Println("could not create CPU profile: ", err) - os.Exit(1) - } - if err := pprof.StartCPUProfile(f); err != nil { - fmt.Println("could not start CPU profile: ", err) - os.Exit(1) - } - defer pprof.StopCPUProfile() - } - if chainConfig != nil { runtimeConfig.ChainConfig = chainConfig + } else { + runtimeConfig.ChainConfig = params.AllDevChainProtocolChanges } - tstart := time.Now() - var leftOverGas uint64 + + var hexInput []byte + if inputFileFlag := ctx.String(InputFileFlag.Name); inputFileFlag != "" { + var err error + if hexInput, err = os.ReadFile(inputFileFlag); err != nil { + return cli.Exit(fmt.Sprintf("could not load input from file: %v", err), 1) + } + } else { + hexInput = []byte(ctx.String(InputFlag.Name)) + } + hexInput = bytes.TrimSpace(hexInput) + input := common.FromHex(string(hexInput)) + + var execFunc func() ([]byte, uint64, error) if ctx.Bool(CreateFlag.Name) { - input := append(code, common.Hex2Bytes(ctx.String(InputFlag.Name))...) - ret, _, leftOverGas, err = runtime.Create(input, &runtimeConfig) + input = append(code, input...) + execFunc = func() ([]byte, uint64, error) { + // don't mutate the state! + runtimeConfig.State = prestate.Copy() + output, _, gasLeft, err := runtime.Create(input, &runtimeConfig) + return output, initialGas - gasLeft, err + } } else { if len(code) > 0 { - statedb.SetCode(receiver, code) + prestate.SetCode(receiver, code) + } + execFunc = func() ([]byte, uint64, error) { + // don't mutate the state! + runtimeConfig.State = prestate.Copy() + output, gasLeft, err := runtime.Call(receiver, input, &runtimeConfig) + return output, initialGas - gasLeft, err } - ret, leftOverGas, err = runtime.Call(receiver, common.Hex2Bytes(ctx.String(InputFlag.Name)), &runtimeConfig) } - execTime := time.Since(tstart) - if ctx.Bool(DumpFlag.Name) { - statedb.Commit(genesisConfig.Number, true) - fmt.Println(string(statedb.Dump(nil))) - } + bench := ctx.Bool(BenchFlag.Name) + output, stats, err := timedExec(bench, execFunc) - if memProfilePath := ctx.String(MemProfileFlag.Name); memProfilePath != "" { - f, err := os.Create(memProfilePath) + if ctx.Bool(DumpFlag.Name) { + root, err := runtimeConfig.State.Commit(genesisConfig.Number, true) if err != nil { - fmt.Println("could not create memory profile: ", err) - os.Exit(1) + fmt.Printf("Failed to commit changes %v\n", err) + return err } - if err := pprof.WriteHeapProfile(f); err != nil { - fmt.Println("could not write memory profile: ", err) - os.Exit(1) + dumpdb, err := state.New(root, sdb) + if err != nil { + fmt.Printf("Failed to open statedb %v\n", err) + return err } - f.Close() + fmt.Println(string(dumpdb.Dump(nil))) } if ctx.Bool(DebugFlag.Name) { @@ -229,20 +358,15 @@ func runCmd(ctx *cli.Context) error { } } - if ctx.Bool(StatDumpFlag.Name) { - var mem goruntime.MemStats - goruntime.ReadMemStats(&mem) - fmt.Fprintf(os.Stderr, `evm execution time: %v -heap objects: %d -allocations: %d -total allocations: %d -GC calls: %d -Gas used: %d - -`, execTime, mem.HeapObjects, mem.Alloc, mem.TotalAlloc, mem.NumGC, initialGas-leftOverGas) + if bench || ctx.Bool(StatDumpFlag.Name) { + fmt.Fprintf(os.Stderr, `EVM gas used: %d +execution time: %v +allocations: %d +allocated bytes: %d +`, stats.GasUsed, stats.Time, stats.Allocs, stats.BytesAllocated) } if tracer == nil { - fmt.Printf("%#x\n", ret) + fmt.Printf("%#x\n", output) if err != nil { fmt.Printf(" error: %v\n", err) } diff --git a/cmd/evm/compiler.go b/cmd/evm/runner_test.go similarity index 51% rename from cmd/evm/compiler.go rename to cmd/evm/runner_test.go index b2f45e2f911f..652cf850c0a3 100644 --- a/cmd/evm/compiler.go +++ b/cmd/evm/runner_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 The go-ethereum Authors +// Copyright 2024 The go-ethereum Authors // This file is part of go-ethereum. // // go-ethereum is free software: you can redistribute it and/or modify @@ -18,37 +18,32 @@ package main import ( "errors" - "fmt" - "os" - - "github.com/XinFinOrg/XDPoSChain/cmd/evm/internal/compiler" - "github.com/urfave/cli/v2" + "testing" ) -var compileCommand = &cli.Command{ - Action: compileCmd, - Name: "compile", - Usage: "compiles easm source to evm binary", - ArgsUsage: "", -} - -func compileCmd(ctx *cli.Context) error { - debug := ctx.Bool(DebugFlag.Name) +func TestTimedExecBenchNondeterministicReturnsError(t *testing.T) { + t.Parallel() - if len(ctx.Args().First()) == 0 { - return errors.New("filename required") + var calls int + execFunc := func() ([]byte, uint64, error) { + calls++ + if calls == 1 { + return []byte{0x01}, 7, nil + } + return []byte{0x02}, 7, nil } - fn := ctx.Args().First() - src, err := os.ReadFile(fn) - if err != nil { - return err - } + defer func() { + if recovered := recover(); recovered != nil { + t.Fatalf("timedExec panicked instead of returning an error: %v", recovered) + } + }() - bin, err := compiler.Compile(fn, src, debug) - if err != nil { - return err + _, _, err := timedExec(true, execFunc) + if err == nil { + t.Fatal("expected nondeterministic benchmark run to return an error") + } + if !errors.Is(err, errInconsistentBenchmarkResult) { + t.Fatalf("expected errInconsistentBenchmarkResult, got %v", err) } - fmt.Println(bin) - return nil } diff --git a/cmd/evm/staterunner.go b/cmd/evm/staterunner.go index ca86eb8cace8..b84740429f48 100644 --- a/cmd/evm/staterunner.go +++ b/cmd/evm/staterunner.go @@ -17,86 +17,170 @@ package main import ( + "bufio" "encoding/json" - "errors" "fmt" "os" + "regexp" + "slices" + "strings" - "github.com/XinFinOrg/XDPoSChain/core/state" "github.com/XinFinOrg/XDPoSChain/core/vm" - "github.com/XinFinOrg/XDPoSChain/eth/tracers/logger" + "github.com/XinFinOrg/XDPoSChain/internal/flags" "github.com/XinFinOrg/XDPoSChain/tests" "github.com/urfave/cli/v2" + "golang.org/x/term" +) + +var ( + forkFlag = &cli.StringFlag{ + Name: "statetest.fork", + Usage: "Only run tests for the specified fork.", + Category: flags.VMCategory, + } + idxFlag = &cli.IntFlag{ + Name: "statetest.index", + Usage: "The index of the subtest to run.", + Category: flags.VMCategory, + Value: -1, // default to select all subtest indices + } ) var stateTestCommand = &cli.Command{ Action: stateTestCmd, Name: "statetest", - Usage: "executes the given state tests", + Usage: "Executes the given state tests. Filenames can be fed via standard input (batch mode) or as an argument (one-off execution).", ArgsUsage: "", -} - -type StatetestResult struct { - Name string `json:"name"` - Pass bool `json:"pass"` - Fork string `json:"fork"` - Error string `json:"error,omitempty"` - State *state.Dump `json:"state,omitempty"` + Flags: slices.Concat([]cli.Flag{ + BenchFlag, + DumpFlag, + forkFlag, + HumanReadableFlag, + idxFlag, + RunFlag, + }, traceFlags), } func stateTestCmd(ctx *cli.Context) error { - if len(ctx.Args().First()) == 0 { - return errors.New("path-to-test argument required") - } + path := ctx.Args().First() - // Configure the EVM logger - config := &logger.Config{ - EnableMemory: !ctx.Bool(DisableMemoryFlag.Name), - DisableStack: ctx.Bool(DisableStackFlag.Name), - DisableStorage: ctx.Bool(DisableStorageFlag.Name), - EnableReturnData: !ctx.Bool(DisableReturnDataFlag.Name), + // If path is provided, run the tests at that path. + if len(path) != 0 { + collected, err := collectFiles(path) + if err != nil { + return cli.Exit(err.Error(), 1) + } + var results []testResult + for _, fname := range collected { + r, err := runStateTest(ctx, fname) + if err != nil { + return err + } + results = append(results, r...) + } + report(ctx, results) + return nil } + // Otherwise, read filenames from stdin and execute back-to-back. + // If stdin is a terminal, print error and exit instead of blocking. + if term.IsTerminal(int(os.Stdin.Fd())) { + return fmt.Errorf("no input file provided and stdin is a terminal; please provide a path argument or pipe filenames via stdin") + } + scanner := bufio.NewScanner(os.Stdin) + var results []testResult + for scanner.Scan() { + fname := scanner.Text() + fname = strings.TrimSpace(fname) + if len(fname) == 0 { + continue + } + r, err := runStateTest(ctx, fname) + if err != nil { + return err + } + results = append(results, r...) + } + if err := scanner.Err(); err != nil { + return err + } + report(ctx, results) + return nil +} - var cfg vm.Config - switch { - case ctx.Bool(MachineFlag.Name): - cfg.Tracer = logger.NewJSONLogger(config, os.Stderr) - - case ctx.Bool(DebugFlag.Name): - cfg.Tracer = logger.NewStructLogger(config).Hooks() +// runStateTest loads the state-test given by fname, and executes the test. +func runStateTest(ctx *cli.Context, fname string) ([]testResult, error) { + src, err := os.ReadFile(fname) + if err != nil { + return nil, err } - // Load the test content from the input file - src, err := os.ReadFile(ctx.Args().First()) + var testsByName map[string]tests.StateTest + if err := json.Unmarshal(src, &testsByName); err != nil { + return nil, fmt.Errorf("unable to read test file %s: %w", fname, err) + } + + tracer, err := tracerFromFlags(ctx) if err != nil { - return err + return nil, err } - var tests map[string]tests.StateTest - if err = json.Unmarshal(src, &tests); err != nil { - return err + cfg := vm.Config{Tracer: tracer} + re, err := regexp.Compile(ctx.String(RunFlag.Name)) + if err != nil { + return nil, fmt.Errorf("invalid regex --%s: %v", RunFlag.Name, err) } + // Iterate over all the tests, run them and aggregate the results - results := make([]StatetestResult, 0, len(tests)) - for key, test := range tests { + results := make([]testResult, 0, len(testsByName)) + for key, test := range testsByName { + if !re.MatchString(key) { + continue + } for _, st := range test.Subtests() { + if idx := ctx.Int(idxFlag.Name); idx != -1 && idx != st.Index { + // If specific index requested, skip all tests that do not match. + continue + } + if fork := ctx.String(forkFlag.Name); fork != "" && st.Fork != fork { + // If specific fork requested, skip all tests that do not match. + continue + } // Run the test and aggregate the result - result := &StatetestResult{Name: key, Fork: st.Fork, Pass: true} - s, err := test.Run(st, cfg) - if err != nil { - // Test failed, mark as so and dump any state to aid debugging - result.Pass, result.Error = false, err.Error() - if ctx.Bool(DumpFlag.Name) && s != nil { - dump := s.RawDump(nil) - result.State = &dump + result := &testResult{Name: key, Fork: st.Fork, Pass: true} + statedb, gasUsed, err := test.RunWithGas(st, cfg) + if statedb != nil { + root := statedb.IntermediateRoot(false) + result.Root = &root + if ctx.Bool(DumpFlag.Name) { + fmt.Fprintf(os.Stderr, "{\"stateRoot\": \"%#x\"}\n", root) + result.State = dump(statedb) } } - // print state root for evmlab tracing (already committed above, so no need to delete objects again - if ctx.Bool(MachineFlag.Name) && s != nil { - fmt.Fprintf(os.Stderr, "{\"stateRoot\": \"%x\"}\n", s.IntermediateRoot(false)) + if ctx.Bool(BenchFlag.Name) { + benchCfg := cfg + benchCfg.Tracer = nil + _, stats, benchErr := timedExec(true, func() ([]byte, uint64, error) { + _, gasUsed, err := test.RunWithGas(st, benchCfg) + return nil, gasUsed, err + }) + result.Stats = &stats + if benchErr != nil { + result.Pass = false + if result.Error != "" { + result.Error += "; " + } + result.Error += "bench error: " + benchErr.Error() + } + } + if err != nil { + result.Pass = false + if result.Error != "" { + result.Error += "; " + } + result.Error += err.Error() + } else if result.Stats == nil && ctx.Bool(BenchFlag.Name) { + result.Stats = &execStats{GasUsed: gasUsed} } results = append(results, *result) } } - out, _ := json.MarshalIndent(results, "", " ") - fmt.Println(string(out)) - return nil + return results, nil } diff --git a/tests/state_test_util.go b/tests/state_test_util.go index 27b6ad19fe48..ac5e6755ebd7 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -154,9 +154,16 @@ func (t *StateTest) Subtests() []StateSubtest { // Run executes a specific subtest. func (t *StateTest) Run(subtest StateSubtest, vmconfig vm.Config) (*state.StateDB, error) { + statedb, _, err := t.RunWithGas(subtest, vmconfig) + return statedb, err +} + +// RunWithGas executes a specific subtest and returns the resulting state along +// with the gas used by message execution. +func (t *StateTest) RunWithGas(subtest StateSubtest, vmconfig vm.Config) (*state.StateDB, uint64, error) { config, ok := Forks[subtest.Fork] if !ok { - return nil, UnsupportedForkError{subtest.Fork} + return nil, 0, UnsupportedForkError{subtest.Fork} } block := t.genesis(config).ToBlock() db := rawdb.NewMemoryDatabase() @@ -174,7 +181,7 @@ func (t *StateTest) Run(subtest StateSubtest, vmconfig vm.Config) (*state.StateD post := t.json.Post[subtest.Fork][subtest.Index] msg, err := t.json.Tx.toMessage(post, baseFee) if err != nil { - return nil, err + return nil, 0, err } // Prepare the EVM. @@ -191,24 +198,30 @@ func (t *StateTest) Run(subtest StateSubtest, vmconfig vm.Config) (*state.StateD gaspool.AddGas(block.GasLimit()) coinbase := &t.json.Env.Coinbase - if _, err := core.ApplyMessage(evm, msg, gaspool, *coinbase); err != nil { + result, err := core.ApplyMessage(evm, msg, gaspool, *coinbase) + if err != nil { statedb.RevertToSnapshot(snapshot) } if logs := rlpHash(statedb.Logs()); logs != common.Hash(post.Logs) { - return statedb, fmt.Errorf("post state logs hash mismatch: got %x, want %x", logs, post.Logs) + return statedb, 0, fmt.Errorf("post state logs hash mismatch: got %x, want %x", logs, post.Logs) } // Commit block root, _ := statedb.Commit(block.NumberU64(), config.IsEIP158(block.Number())) if root != common.Hash(post.Root) { - return statedb, fmt.Errorf("post state root mismatch: got %x, want %x", root, post.Root) + return statedb, 0, fmt.Errorf("post state root mismatch: got %x, want %x", root, post.Root) } // Re-init the post-state instance for further operation - statedb, err = state.New(root, statedb.Database()) - if err != nil { - return nil, err + statedb, err2 := state.New(root, statedb.Database()) + if err2 != nil { + return nil, 0, err2 + } + var gasUsed uint64 + if result != nil { + gasUsed = result.UsedGas } - return statedb, nil + // If we got an error from ApplyMessage, but post-state is correct, clear err (unless internal failure) + return statedb, gasUsed, nil } func (t *StateTest) gasLimit(subtest StateSubtest) uint64 {