// Copyright 2021 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 ( "bufio" "encoding/json" "fmt" "io" "os" "path/filepath" "reflect" "strings" "testing" "github.com/ethereum/go-ethereum/cmd/evm/internal/t8ntool" "github.com/ethereum/go-ethereum/internal/cmdtest" "github.com/ethereum/go-ethereum/internal/reexec" ) func TestMain(m *testing.M) { // Run the app if we've been exec'd as "ethkey-test" in runEthkey. reexec.Register("evm-test", func() { if err := app.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } os.Exit(0) }) // check if we have been reexec'd if reexec.Init() { return } os.Exit(m.Run()) } type testT8n struct { *cmdtest.TestCmd } type t8nInput struct { inAlloc string inTxs string inEnv string stFork string stReward string } func (args *t8nInput) get(base string) []string { var out []string if opt := args.inAlloc; opt != "" { out = append(out, "--input.alloc") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } if opt := args.inTxs; opt != "" { out = append(out, "--input.txs") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } if opt := args.inEnv; opt != "" { out = append(out, "--input.env") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } if opt := args.stFork; opt != "" { out = append(out, "--state.fork", opt) } if opt := args.stReward; opt != "" { out = append(out, "--state.reward", opt) } return out } type t8nOutput struct { alloc bool result bool body bool } func (args *t8nOutput) get() (out []string) { if args.body { out = append(out, "--output.body", "stdout") } else { out = append(out, "--output.body", "") // empty means ignore } if args.result { out = append(out, "--output.result", "stdout") } else { out = append(out, "--output.result", "") } if args.alloc { out = append(out, "--output.alloc", "stdout") } else { out = append(out, "--output.alloc", "") } return out } func TestT8n(t *testing.T) { t.Parallel() tt := new(testT8n) tt.TestCmd = cmdtest.NewTestCmd(t, tt) for i, tc := range []struct { base string input t8nInput output t8nOutput expExitCode int expOut string }{ { // Test exit (3) on bad config base: "./testdata/1", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Frontier+1346", "", }, output: t8nOutput{alloc: true, result: true}, expExitCode: 3, }, { base: "./testdata/1", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Byzantium", "", }, output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, { // blockhash test base: "./testdata/3", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Berlin", "", }, output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, { // missing blockhash test base: "./testdata/4", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Berlin", "", }, output: t8nOutput{alloc: true, result: true}, expExitCode: 4, }, { // Uncle test base: "./testdata/5", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Byzantium", "0x80", }, output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, { // Sign json transactions base: "./testdata/13", input: t8nInput{ "alloc.json", "txs.json", "env.json", "London", "", }, output: t8nOutput{body: true}, expOut: "exp.json", }, { // Already signed transactions base: "./testdata/13", input: t8nInput{ "alloc.json", "signed_txs.rlp", "env.json", "London", "", }, output: t8nOutput{result: true}, expOut: "exp2.json", }, { // Difficulty calculation - no uncles base: "./testdata/14", input: t8nInput{ "alloc.json", "txs.json", "env.json", "London", "", }, output: t8nOutput{result: true}, expOut: "exp.json", }, { // Difficulty calculation - with uncles base: "./testdata/14", input: t8nInput{ "alloc.json", "txs.json", "env.uncles.json", "London", "", }, output: t8nOutput{result: true}, expOut: "exp2.json", }, { // Difficulty calculation - with ommers + Berlin base: "./testdata/14", input: t8nInput{ "alloc.json", "txs.json", "env.uncles.json", "Berlin", "", }, output: t8nOutput{result: true}, expOut: "exp_berlin.json", }, { // Difficulty calculation on arrow glacier base: "./testdata/19", input: t8nInput{ "alloc.json", "txs.json", "env.json", "London", "", }, output: t8nOutput{result: true}, expOut: "exp_london.json", }, { // Difficulty calculation on arrow glacier base: "./testdata/19", input: t8nInput{ "alloc.json", "txs.json", "env.json", "ArrowGlacier", "", }, output: t8nOutput{result: true}, expOut: "exp_arrowglacier.json", }, { // Difficulty calculation on gray glacier base: "./testdata/19", input: t8nInput{ "alloc.json", "txs.json", "env.json", "GrayGlacier", "", }, output: t8nOutput{result: true}, expOut: "exp_grayglacier.json", }, { // Sign unprotected (pre-EIP155) transaction base: "./testdata/23", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Berlin", "", }, output: t8nOutput{result: true}, expOut: "exp.json", }, { // Test post-merge transition base: "./testdata/24", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Paris", "", }, output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, { // Test post-merge transition where input is missing random base: "./testdata/24", input: t8nInput{ "alloc.json", "txs.json", "env-missingrandom.json", "Paris", "", }, output: t8nOutput{alloc: false, result: false}, expExitCode: 3, }, { // Test base fee calculation base: "./testdata/25", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Paris", "", }, output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, { // Test withdrawals transition base: "./testdata/26", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Shanghai", "", }, output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, { // Cancun tests base: "./testdata/28", input: t8nInput{ "alloc.json", "txs.rlp", "env.json", "Cancun", "", }, output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, { // More cancun tests base: "./testdata/29", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Cancun", "", }, output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, { // More cancun test, plus example of rlp-transaction that cannot be decoded properly base: "./testdata/30", input: t8nInput{ "alloc.json", "txs_more.rlp", "env.json", "Cancun", "", }, output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, { // Prague test, EIP-7702 transaction base: "./testdata/33", input: t8nInput{ "alloc.json", "txs.json", "env.json", "Prague", "", }, output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, } { args := []string{"t8n"} args = append(args, tc.output.get()...) args = append(args, tc.input.get(tc.base)...) var qArgs []string // quoted args for debugging purposes for _, arg := range args { if len(arg) == 0 { qArgs = append(qArgs, `""`) } else { qArgs = append(qArgs, arg) } } tt.Logf("args: %v\n", strings.Join(qArgs, " ")) tt.Run("evm-test", args...) // Compare the expected output, if provided if tc.expOut != "" { file := fmt.Sprintf("%v/%v", tc.base, tc.expOut) want, err := os.ReadFile(file) if err != nil { t.Fatalf("test %d: could not read expected output: %v", i, err) } have := tt.Output() ok, err := cmpJson(have, want) switch { case err != nil: t.Fatalf("test %d, file %v: json parsing failed: %v", i, file, err) case !ok: t.Fatalf("test %d, file %v: output wrong, have \n%v\nwant\n%v\n", i, file, string(have), string(want)) } } tt.WaitExit() if have, want := tt.ExitStatus(), tc.expExitCode; have != want { t.Fatalf("test %d: wrong exit code, have %d, want %d", i, have, want) } } } func lineIterator(path string) func() (string, error) { data, err := os.ReadFile(path) if err != nil { return func() (string, error) { return err.Error(), err } } scanner := bufio.NewScanner(strings.NewReader(string(data))) return func() (string, error) { if scanner.Scan() { return scanner.Text(), nil } if err := scanner.Err(); err != nil { return "", err } return "", io.EOF // scanner gobbles io.EOF, but we want it } } type t9nInput struct { inTxs string stFork string } func (args *t9nInput) get(base string) []string { var out []string if opt := args.inTxs; opt != "" { out = append(out, "--input.txs") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } if opt := args.stFork; opt != "" { out = append(out, "--state.fork", opt) } return out } func TestT9n(t *testing.T) { t.Parallel() tt := new(testT8n) tt.TestCmd = cmdtest.NewTestCmd(t, tt) for i, tc := range []struct { base string input t9nInput expExitCode int expOut string }{ { // London txs on homestead base: "./testdata/15", input: t9nInput{ inTxs: "signed_txs.rlp", stFork: "Homestead", }, expOut: "exp.json", }, { // London txs on London base: "./testdata/15", input: t9nInput{ inTxs: "signed_txs.rlp", stFork: "London", }, expOut: "exp2.json", }, { // An RLP list (a blockheader really) base: "./testdata/15", input: t9nInput{ inTxs: "blockheader.rlp", stFork: "London", }, expOut: "exp3.json", }, { // Transactions with too low gas base: "./testdata/16", input: t9nInput{ inTxs: "signed_txs.rlp", stFork: "London", }, expOut: "exp.json", }, { // Transactions with value exceeding 256 bits base: "./testdata/17", input: t9nInput{ inTxs: "signed_txs.rlp", stFork: "London", }, expOut: "exp.json", }, { // Invalid RLP base: "./testdata/18", input: t9nInput{ inTxs: "invalid.rlp", stFork: "London", }, expExitCode: t8ntool.ErrorIO, }, } { args := []string{"t9n"} args = append(args, tc.input.get(tc.base)...) tt.Run("evm-test", args...) tt.Logf("args:\n go run . %v\n", strings.Join(args, " ")) // Compare the expected output, if provided if tc.expOut != "" { want, err := os.ReadFile(fmt.Sprintf("%v/%v", tc.base, tc.expOut)) if err != nil { t.Fatalf("test %d: could not read expected output: %v", i, err) } have := tt.Output() ok, err := cmpJson(have, want) switch { case err != nil: t.Log(string(have)) t.Fatalf("test %d, json parsing failed: %v", i, err) case !ok: t.Fatalf("test %d: output wrong, have \n%v\nwant\n%v\n", i, string(have), string(want)) } } tt.WaitExit() if have, want := tt.ExitStatus(), tc.expExitCode; have != want { t.Fatalf("test %d: wrong exit code, have %d, want %d", i, have, want) } } } type b11rInput struct { inEnv string inOmmersRlp string inWithdrawals string inTxsRlp string inClique string ethash bool ethashMode string ethashDir string } func (args *b11rInput) get(base string) []string { var out []string if opt := args.inEnv; opt != "" { out = append(out, "--input.header") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } if opt := args.inOmmersRlp; opt != "" { out = append(out, "--input.ommers") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } if opt := args.inWithdrawals; opt != "" { out = append(out, "--input.withdrawals") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } if opt := args.inTxsRlp; opt != "" { out = append(out, "--input.txs") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } if opt := args.inClique; opt != "" { out = append(out, "--seal.clique") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } if args.ethash { out = append(out, "--seal.ethash") } if opt := args.ethashMode; opt != "" { out = append(out, "--seal.ethash.mode") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } if opt := args.ethashDir; opt != "" { out = append(out, "--seal.ethash.dir") out = append(out, fmt.Sprintf("%v/%v", base, opt)) } out = append(out, "--output.block") out = append(out, "stdout") return out } func TestB11r(t *testing.T) { t.Parallel() tt := new(testT8n) tt.TestCmd = cmdtest.NewTestCmd(t, tt) for i, tc := range []struct { base string input b11rInput expExitCode int expOut string }{ { // unsealed block base: "./testdata/20", input: b11rInput{ inEnv: "header.json", inOmmersRlp: "ommers.json", inTxsRlp: "txs.rlp", }, expOut: "exp.json", }, { // ethash test seal base: "./testdata/21", input: b11rInput{ inEnv: "header.json", inOmmersRlp: "ommers.json", inTxsRlp: "txs.rlp", }, expOut: "exp.json", }, { // clique test seal base: "./testdata/21", input: b11rInput{ inEnv: "header.json", inOmmersRlp: "ommers.json", inTxsRlp: "txs.rlp", inClique: "clique.json", }, expOut: "exp-clique.json", }, { // block with ommers base: "./testdata/22", input: b11rInput{ inEnv: "header.json", inOmmersRlp: "ommers.json", inTxsRlp: "txs.rlp", }, expOut: "exp.json", }, { // block with withdrawals base: "./testdata/27", input: b11rInput{ inEnv: "header.json", inOmmersRlp: "ommers.json", inWithdrawals: "withdrawals.json", inTxsRlp: "txs.rlp", }, expOut: "exp.json", }, } { args := []string{"b11r"} args = append(args, tc.input.get(tc.base)...) tt.Run("evm-test", args...) tt.Logf("args:\n go run . %v\n", strings.Join(args, " ")) // Compare the expected output, if provided if tc.expOut != "" { want, err := os.ReadFile(fmt.Sprintf("%v/%v", tc.base, tc.expOut)) if err != nil { t.Fatalf("test %d: could not read expected output: %v", i, err) } have := tt.Output() ok, err := cmpJson(have, want) switch { case err != nil: t.Log(string(have)) t.Fatalf("test %d, json parsing failed: %v", i, err) case !ok: t.Fatalf("test %d: output wrong, have \n%v\nwant\n%v\n", i, string(have), string(want)) } } tt.WaitExit() if have, want := tt.ExitStatus(), tc.expExitCode; have != want { t.Fatalf("test %d: wrong exit code, have %d, want %d", i, have, want) } } } func TestEvmRun(t *testing.T) { t.Parallel() tt := cmdtest.NewTestCmd(t, nil) for i, tc := range []struct { input []string wantStdout string wantStderr string }{ { // json tracing input: []string{"run", "--trace", "--trace.format=json", "6040"}, wantStdout: "./testdata/evmrun/1.out.1.txt", wantStderr: "./testdata/evmrun/1.out.2.txt", }, { // Same as above, using the deprecated --json input: []string{"run", "--json", "6040"}, wantStdout: "./testdata/evmrun/1.out.1.txt", wantStderr: "./testdata/evmrun/1.out.2.txt", }, { // default tracing (struct) input: []string{"run", "--trace", "0x6040"}, wantStdout: "./testdata/evmrun/2.out.1.txt", wantStderr: "./testdata/evmrun/2.out.2.txt", }, { // default tracing (struct), plus alloc-dump input: []string{"run", "--trace", "--dump", "0x6040"}, wantStdout: "./testdata/evmrun/3.out.1.txt", //wantStderr: "./testdata/evmrun/3.out.2.txt", }, { // json-tracing, plus alloc-dump input: []string{"run", "--trace", "--trace.format=json", "--dump", "0x6040"}, wantStdout: "./testdata/evmrun/4.out.1.txt", //wantStderr: "./testdata/evmrun/4.out.2.txt", }, { // md-tracing input: []string{"run", "--trace", "--trace.format=md", "0x6040"}, wantStdout: "./testdata/evmrun/5.out.1.txt", wantStderr: "./testdata/evmrun/5.out.2.txt", }, { // statetest subcommand input: []string{"statetest", "./testdata/statetest.json"}, wantStdout: "./testdata/evmrun/6.out.1.txt", wantStderr: "./testdata/evmrun/6.out.2.txt", }, { // statetest subcommand with output input: []string{"statetest", "--trace", "--trace.format=md", "./testdata/statetest.json"}, wantStdout: "./testdata/evmrun/7.out.1.txt", wantStderr: "./testdata/evmrun/7.out.2.txt", }, { // statetest subcommand with output input: []string{"statetest", "--trace", "--trace.format=json", "./testdata/statetest.json"}, wantStdout: "./testdata/evmrun/8.out.1.txt", wantStderr: "./testdata/evmrun/8.out.2.txt", }, } { tt.Logf("args: go run ./cmd/evm %v\n", strings.Join(tc.input, " ")) tt.Run("evm-test", tc.input...) haveStdOut := tt.Output() tt.WaitExit() haveStdErr := tt.StderrText() if have, wantFile := haveStdOut, tc.wantStdout; wantFile != "" { want, err := os.ReadFile(wantFile) if err != nil { t.Fatalf("test %d: could not read expected output: %v", i, err) } if string(haveStdOut) != string(want) { t.Fatalf("test %d, output wrong, have \n%v\nwant\n%v\n", i, string(have), string(want)) } } if have, wantFile := haveStdErr, tc.wantStderr; wantFile != "" { want, err := os.ReadFile(wantFile) if err != nil { t.Fatalf("test %d: could not read expected output: %v", i, err) } if have != string(want) { t.Fatalf("test %d, output wrong\nhave %q\nwant %q\n", i, have, string(want)) } } } } // cmpJson compares the JSON in two byte slices. func cmpJson(a, b []byte) (bool, error) { var j, j2 interface{} if err := json.Unmarshal(a, &j); err != nil { return false, err } if err := json.Unmarshal(b, &j2); err != nil { return false, err } return reflect.DeepEqual(j2, j), nil } // TestEVMTracing is a test that checks the tracing-output from evm. func TestEVMTracing(t *testing.T) { t.Parallel() tt := cmdtest.NewTestCmd(t, nil) for i, tc := range []struct { base string input []string expectedTraces []string }{ { base: "./testdata/31", input: []string{"t8n", "--input.alloc=./testdata/31/alloc.json", "--input.txs=./testdata/31/txs.json", "--input.env=./testdata/31/env.json", "--state.fork=Cancun", "--trace", }, expectedTraces: []string{"trace-0-0x88f5fbd1524731a81e49f637aa847543268a5aaf2a6b32a69d2c6d978c45dcfb.jsonl"}, }, { base: "./testdata/31", input: []string{"t8n", "--input.alloc=./testdata/31/alloc.json", "--input.txs=./testdata/31/txs.json", "--input.env=./testdata/31/env.json", "--state.fork=Cancun", "--trace.tracer", ` { result: function(){ return "hello world" }, fault: function(){} }`, }, expectedTraces: []string{"trace-0-0x88f5fbd1524731a81e49f637aa847543268a5aaf2a6b32a69d2c6d978c45dcfb.json"}, }, { base: "./testdata/32", input: []string{"t8n", "--input.alloc=./testdata/32/alloc.json", "--input.txs=./testdata/32/txs.json", "--input.env=./testdata/32/env.json", "--state.fork=Paris", "--trace", "--trace.callframes", }, expectedTraces: []string{"trace-0-0x47806361c0fa084be3caa18afe8c48156747c01dbdfc1ee11b5aecdbe4fcf23e.jsonl"}, }, // TODO, make it possible to run tracers on statetests, e.g: //{ // base: "./testdata/31", // input: []string{"statetest", "--trace", "--trace.tracer", `{ // result: function(){ // return "hello world" // }, // fault: function(){} //}`, "./testdata/statetest.json"}, // expectedTraces: []string{"trace-0-0x88f5fbd1524731a81e49f637aa847543268a5aaf2a6b32a69d2c6d978c45dcfb.json"}, // }, } { // Place the output somewhere we can find it outdir := t.TempDir() args := append(tc.input, "--output.basedir", outdir) tt.Run("evm-test", args...) tt.Logf("args: go run ./cmd/evm %v\n", args) tt.WaitExit() //t.Log(string(tt.Output())) // Compare the expected traces for _, traceFile := range tc.expectedTraces { haveFn := lineIterator(filepath.Join(outdir, traceFile)) wantFn := lineIterator(filepath.Join(tc.base, traceFile)) for line := 0; ; line++ { want, wErr := wantFn() have, hErr := haveFn() if want != have { t.Fatalf("test %d, trace %v, line %d\nwant: %v\nhave: %v\n", i, traceFile, line, want, have) } if wErr != nil && hErr != nil { break } if wErr != nil { t.Fatal(wErr) } if hErr != nil { t.Fatal(hErr) } //t.Logf("%v\n", want) } } } }