diff --git a/cmd/workload/README.md b/cmd/workload/README.md
index c85e58a041..bf1978eebd 100644
--- a/cmd/workload/README.md
+++ b/cmd/workload/README.md
@@ -26,4 +26,5 @@ the following commands (in this directory) against a synced mainnet node:
```shell
> go run . filtergen --queries queries/filter_queries_mainnet.json http://host:8545
> go run . historygen --history-tests queries/history_mainnet.json http://host:8545
+> go run . tracegen --trace-tests queries/trace_mainnet.json http://host:8545
```
diff --git a/cmd/workload/client.go b/cmd/workload/client.go
new file mode 100644
index 0000000000..5a6a005ced
--- /dev/null
+++ b/cmd/workload/client.go
@@ -0,0 +1,104 @@
+// Copyright 2025 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 (
+ "context"
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethclient"
+ "github.com/ethereum/go-ethereum/ethclient/gethclient"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/urfave/cli/v2"
+)
+
+type client struct {
+ Eth *ethclient.Client
+ Geth *gethclient.Client
+ RPC *rpc.Client
+}
+
+func makeClient(ctx *cli.Context) *client {
+ if ctx.NArg() < 1 {
+ exit("missing RPC endpoint URL as command-line argument")
+ }
+ url := ctx.Args().First()
+ cl, err := rpc.Dial(url)
+ if err != nil {
+ exit(fmt.Errorf("could not create RPC client at %s: %v", url, err))
+ }
+ return &client{
+ RPC: cl,
+ Eth: ethclient.NewClient(cl),
+ Geth: gethclient.New(cl),
+ }
+}
+
+type simpleBlock struct {
+ Number hexutil.Uint64 `json:"number"`
+ Hash common.Hash `json:"hash"`
+}
+
+type simpleTransaction struct {
+ Hash common.Hash `json:"hash"`
+ TransactionIndex hexutil.Uint64 `json:"transactionIndex"`
+}
+
+func (c *client) getBlockByHash(ctx context.Context, arg common.Hash, fullTx bool) (*simpleBlock, error) {
+ var r *simpleBlock
+ err := c.RPC.CallContext(ctx, &r, "eth_getBlockByHash", arg, fullTx)
+ return r, err
+}
+
+func (c *client) getBlockByNumber(ctx context.Context, arg uint64, fullTx bool) (*simpleBlock, error) {
+ var r *simpleBlock
+ err := c.RPC.CallContext(ctx, &r, "eth_getBlockByNumber", hexutil.Uint64(arg), fullTx)
+ return r, err
+}
+
+func (c *client) getTransactionByBlockHashAndIndex(ctx context.Context, block common.Hash, index uint64) (*simpleTransaction, error) {
+ var r *simpleTransaction
+ err := c.RPC.CallContext(ctx, &r, "eth_getTransactionByBlockHashAndIndex", block, hexutil.Uint64(index))
+ return r, err
+}
+
+func (c *client) getTransactionByBlockNumberAndIndex(ctx context.Context, block uint64, index uint64) (*simpleTransaction, error) {
+ var r *simpleTransaction
+ err := c.RPC.CallContext(ctx, &r, "eth_getTransactionByBlockNumberAndIndex", hexutil.Uint64(block), hexutil.Uint64(index))
+ return r, err
+}
+
+func (c *client) getBlockTransactionCountByHash(ctx context.Context, block common.Hash) (uint64, error) {
+ var r hexutil.Uint64
+ err := c.RPC.CallContext(ctx, &r, "eth_getBlockTransactionCountByHash", block)
+ return uint64(r), err
+}
+
+func (c *client) getBlockTransactionCountByNumber(ctx context.Context, block uint64) (uint64, error) {
+ var r hexutil.Uint64
+ err := c.RPC.CallContext(ctx, &r, "eth_getBlockTransactionCountByNumber", hexutil.Uint64(block))
+ return uint64(r), err
+}
+
+func (c *client) getBlockReceipts(ctx context.Context, arg any) ([]*types.Receipt, error) {
+ var result []*types.Receipt
+ err := c.RPC.CallContext(ctx, &result, "eth_getBlockReceipts", arg)
+ return result, err
+}
diff --git a/cmd/workload/historytest.go b/cmd/workload/historytest.go
index 29fe3f5098..fc4abe46c1 100644
--- a/cmd/workload/historytest.go
+++ b/cmd/workload/historytest.go
@@ -23,7 +23,6 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
- "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/internal/utesting"
)
@@ -279,55 +278,3 @@ func (s *historyTestSuite) testGetTransactionByBlockNumberAndIndex(t *utesting.T
}
}
}
-
-type simpleBlock struct {
- Number hexutil.Uint64 `json:"number"`
- Hash common.Hash `json:"hash"`
-}
-
-type simpleTransaction struct {
- Hash common.Hash `json:"hash"`
- TransactionIndex hexutil.Uint64 `json:"transactionIndex"`
-}
-
-func (c *client) getBlockByHash(ctx context.Context, arg common.Hash, fullTx bool) (*simpleBlock, error) {
- var r *simpleBlock
- err := c.RPC.CallContext(ctx, &r, "eth_getBlockByHash", arg, fullTx)
- return r, err
-}
-
-func (c *client) getBlockByNumber(ctx context.Context, arg uint64, fullTx bool) (*simpleBlock, error) {
- var r *simpleBlock
- err := c.RPC.CallContext(ctx, &r, "eth_getBlockByNumber", hexutil.Uint64(arg), fullTx)
- return r, err
-}
-
-func (c *client) getTransactionByBlockHashAndIndex(ctx context.Context, block common.Hash, index uint64) (*simpleTransaction, error) {
- var r *simpleTransaction
- err := c.RPC.CallContext(ctx, &r, "eth_getTransactionByBlockHashAndIndex", block, hexutil.Uint64(index))
- return r, err
-}
-
-func (c *client) getTransactionByBlockNumberAndIndex(ctx context.Context, block uint64, index uint64) (*simpleTransaction, error) {
- var r *simpleTransaction
- err := c.RPC.CallContext(ctx, &r, "eth_getTransactionByBlockNumberAndIndex", hexutil.Uint64(block), hexutil.Uint64(index))
- return r, err
-}
-
-func (c *client) getBlockTransactionCountByHash(ctx context.Context, block common.Hash) (uint64, error) {
- var r hexutil.Uint64
- err := c.RPC.CallContext(ctx, &r, "eth_getBlockTransactionCountByHash", block)
- return uint64(r), err
-}
-
-func (c *client) getBlockTransactionCountByNumber(ctx context.Context, block uint64) (uint64, error) {
- var r hexutil.Uint64
- err := c.RPC.CallContext(ctx, &r, "eth_getBlockTransactionCountByNumber", hexutil.Uint64(block))
- return uint64(r), err
-}
-
-func (c *client) getBlockReceipts(ctx context.Context, arg any) ([]*types.Receipt, error) {
- var result []*types.Receipt
- err := c.RPC.CallContext(ctx, &result, "eth_getBlockReceipts", arg)
- return result, err
-}
diff --git a/cmd/workload/historytestgen.go b/cmd/workload/historytestgen.go
index 9d98bb28e6..002945f2a6 100644
--- a/cmd/workload/historytestgen.go
+++ b/cmd/workload/historytestgen.go
@@ -51,7 +51,7 @@ var (
}
historyTestEarliestFlag = &cli.IntFlag{
Name: "earliest",
- Usage: "JSON file containing filter test queries",
+ Usage: "The earliest block to test queries",
Value: 0,
Category: flags.TestingCategory,
}
@@ -139,7 +139,7 @@ func calcReceiptsHash(rcpt []*types.Receipt) common.Hash {
func writeJSON(fileName string, value any) {
file, err := os.Create(fileName)
if err != nil {
- exit(fmt.Errorf("Error creating %s: %v", fileName, err))
+ exit(fmt.Errorf("error creating %s: %v", fileName, err))
return
}
defer file.Close()
diff --git a/cmd/workload/main.go b/cmd/workload/main.go
index 6ade426c16..32618d6a79 100644
--- a/cmd/workload/main.go
+++ b/cmd/workload/main.go
@@ -20,10 +20,8 @@ import (
"fmt"
"os"
- "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/internal/flags"
- "github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
)
@@ -49,6 +47,7 @@ func init() {
runTestCommand,
historyGenerateCommand,
filterGenerateCommand,
+ traceGenerateCommand,
filterPerfCommand,
}
}
@@ -57,26 +56,6 @@ func main() {
exit(app.Run(os.Args))
}
-type client struct {
- Eth *ethclient.Client
- RPC *rpc.Client
-}
-
-func makeClient(ctx *cli.Context) *client {
- if ctx.NArg() < 1 {
- exit("missing RPC endpoint URL as command-line argument")
- }
- url := ctx.Args().First()
- cl, err := rpc.Dial(url)
- if err != nil {
- exit(fmt.Errorf("Could not create RPC client at %s: %v", url, err))
- }
- return &client{
- RPC: cl,
- Eth: ethclient.NewClient(cl),
- }
-}
-
func exit(err any) {
if err == nil {
os.Exit(0)
diff --git a/cmd/workload/testsuite.go b/cmd/workload/testsuite.go
index e8e25e7731..f66f68fabd 100644
--- a/cmd/workload/testsuite.go
+++ b/cmd/workload/testsuite.go
@@ -49,6 +49,8 @@ var (
testMainnetFlag,
filterQueryFileFlag,
historyTestFileFlag,
+ traceTestFileFlag,
+ traceTestInvalidOutputFlag,
},
}
testPatternFlag = &cli.StringFlag{
@@ -86,6 +88,7 @@ type testConfig struct {
filterQueryFile string
historyTestFile string
historyPruneBlock *uint64
+ traceTestFile string
}
var errPrunedHistory = fmt.Errorf("attempt to access pruned history")
@@ -125,16 +128,19 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
cfg.historyTestFile = "queries/history_mainnet.json"
cfg.historyPruneBlock = new(uint64)
*cfg.historyPruneBlock = history.PrunePoints[params.MainnetGenesisHash].BlockNumber
+ cfg.traceTestFile = "queries/trace_mainnet.json"
case ctx.Bool(testSepoliaFlag.Name):
cfg.fsys = builtinTestFiles
cfg.filterQueryFile = "queries/filter_queries_sepolia.json"
cfg.historyTestFile = "queries/history_sepolia.json"
cfg.historyPruneBlock = new(uint64)
*cfg.historyPruneBlock = history.PrunePoints[params.SepoliaGenesisHash].BlockNumber
+ cfg.traceTestFile = "queries/trace_sepolia.json"
default:
cfg.fsys = os.DirFS(".")
cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name)
cfg.historyTestFile = ctx.String(historyTestFileFlag.Name)
+ cfg.traceTestFile = ctx.String(traceTestFileFlag.Name)
}
return cfg
}
@@ -143,10 +149,12 @@ func runTestCmd(ctx *cli.Context) error {
cfg := testConfigFromCLI(ctx)
filterSuite := newFilterTestSuite(cfg)
historySuite := newHistoryTestSuite(cfg)
+ traceSuite := newTraceTestSuite(cfg, ctx)
// Filter test cases.
tests := filterSuite.allTests()
tests = append(tests, historySuite.allTests()...)
+ tests = append(tests, traceSuite.allTests()...)
if ctx.IsSet(testPatternFlag.Name) {
tests = utesting.MatchTests(tests, ctx.String(testPatternFlag.Name))
}
diff --git a/cmd/workload/tracetest.go b/cmd/workload/tracetest.go
new file mode 100644
index 0000000000..be85e6c4f2
--- /dev/null
+++ b/cmd/workload/tracetest.go
@@ -0,0 +1,127 @@
+// Copyright 2025 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 (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/eth/tracers"
+ "github.com/ethereum/go-ethereum/internal/utesting"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/urfave/cli/v2"
+)
+
+// traceTest is the content of a history test.
+type traceTest struct {
+ TxHashes []common.Hash `json:"txHashes"`
+ TraceConfigs []tracers.TraceConfig `json:"traceConfigs"`
+ ResultHashes []common.Hash `json:"resultHashes"`
+}
+
+type traceTestSuite struct {
+ cfg testConfig
+ tests traceTest
+
+ invalidDir string
+}
+
+func newTraceTestSuite(cfg testConfig, ctx *cli.Context) *traceTestSuite {
+ s := &traceTestSuite{
+ cfg: cfg,
+ invalidDir: ctx.String(traceTestInvalidOutputFlag.Name),
+ }
+ if err := s.loadTests(); err != nil {
+ exit(err)
+ }
+ return s
+}
+
+func (s *traceTestSuite) loadTests() error {
+ file, err := s.cfg.fsys.Open(s.cfg.traceTestFile)
+ if err != nil {
+ return fmt.Errorf("can't open traceTestFile: %v", err)
+ }
+ defer file.Close()
+
+ if err := json.NewDecoder(file).Decode(&s.tests); err != nil {
+ return fmt.Errorf("invalid JSON in %s: %v", s.cfg.traceTestFile, err)
+ }
+ if len(s.tests.TxHashes) == 0 {
+ return fmt.Errorf("traceTestFile %s has no test data", s.cfg.traceTestFile)
+ }
+ return nil
+}
+
+func (s *traceTestSuite) allTests() []utesting.Test {
+ return []utesting.Test{
+ {Name: "Trace/Transaction", Fn: s.traceTransaction},
+ }
+}
+
+// traceTransaction runs all transaction tracing tests
+func (s *traceTestSuite) traceTransaction(t *utesting.T) {
+ ctx := context.Background()
+
+ for i, hash := range s.tests.TxHashes {
+ config := s.tests.TraceConfigs[i]
+ result, err := s.cfg.client.Geth.TraceTransaction(ctx, hash, &config)
+ if err != nil {
+ t.Fatalf("Transaction %d (hash %v): error %v", i, hash, err)
+ }
+ blob, err := json.Marshal(result)
+ if err != nil {
+ t.Fatalf("Transaction %d (hash %v): error %v", i, hash, err)
+ continue
+ }
+ if crypto.Keccak256Hash(blob) != s.tests.ResultHashes[i] {
+ t.Errorf("Transaction %d (hash %v): invalid result", i, hash)
+
+ writeInvalidTraceResult(s.invalidDir, hash, result)
+ }
+ }
+}
+
+func writeInvalidTraceResult(dir string, hash common.Hash, result any) {
+ if dir == "" {
+ return
+ }
+ err := os.MkdirAll(dir, os.ModePerm)
+ if err != nil {
+ log.Info("Failed to make output directory", "err", err)
+ return
+ }
+ name := filepath.Join(dir, "invalid"+"_"+hash.String())
+ file, err := os.Create(name)
+ if err != nil {
+ exit(fmt.Errorf("error creating %s: %v", name, err))
+ return
+ }
+ defer file.Close()
+
+ data, _ := json.MarshalIndent(result, "", " ")
+ _, err = file.Write(data)
+ if err != nil {
+ exit(fmt.Errorf("error writing %s: %v", name, err))
+ return
+ }
+}
diff --git a/cmd/workload/tracetestgen.go b/cmd/workload/tracetestgen.go
new file mode 100644
index 0000000000..171963503c
--- /dev/null
+++ b/cmd/workload/tracetestgen.go
@@ -0,0 +1,198 @@
+// Copyright 2025 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 (
+ "context"
+ "encoding/json"
+ "fmt"
+ "math/big"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/eth/tracers"
+ "github.com/ethereum/go-ethereum/eth/tracers/logger"
+ "github.com/ethereum/go-ethereum/internal/flags"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/urfave/cli/v2"
+)
+
+var (
+ defaultBlocksToTrace = 64 // the number of states assumed to be available
+
+ traceGenerateCommand = &cli.Command{
+ Name: "tracegen",
+ Usage: "Generates tests for state tracing",
+ ArgsUsage: "",
+ Action: generateTraceTests,
+ Flags: []cli.Flag{
+ traceTestFileFlag,
+ traceTestResultOutputFlag,
+ traceTestBlockFlag,
+ },
+ }
+
+ traceTestFileFlag = &cli.StringFlag{
+ Name: "trace-tests",
+ Usage: "JSON file containing trace test queries",
+ Value: "trace_tests.json",
+ Category: flags.TestingCategory,
+ }
+ traceTestResultOutputFlag = &cli.StringFlag{
+ Name: "trace-output",
+ Usage: "Folder containing the trace output files",
+ Value: "",
+ Category: flags.TestingCategory,
+ }
+ traceTestBlockFlag = &cli.IntFlag{
+ Name: "trace-blocks",
+ Usage: "The number of blocks for tracing",
+ Value: defaultBlocksToTrace,
+ Category: flags.TestingCategory,
+ }
+ traceTestInvalidOutputFlag = &cli.StringFlag{
+ Name: "trace-invalid",
+ Usage: "Folder containing the mismatched trace output files",
+ Value: "",
+ Category: flags.TestingCategory,
+ }
+)
+
+func generateTraceTests(clictx *cli.Context) error {
+ var (
+ client = makeClient(clictx)
+ outputFile = clictx.String(traceTestFileFlag.Name)
+ outputDir = clictx.String(traceTestResultOutputFlag.Name)
+ blocks = clictx.Int(traceTestBlockFlag.Name)
+ ctx = context.Background()
+ test = new(traceTest)
+ )
+ if outputDir != "" {
+ err := os.MkdirAll(outputDir, os.ModePerm)
+ if err != nil {
+ return err
+ }
+ }
+ latest, err := client.Eth.BlockNumber(ctx)
+ if err != nil {
+ exit(err)
+ }
+ if latest < uint64(blocks) {
+ exit(fmt.Errorf("node seems not synced, latest block is %d", latest))
+ }
+ // Get blocks and assign block info into the test
+ var (
+ start = time.Now()
+ logged = time.Now()
+ failed int
+ )
+ log.Info("Trace transactions around the chain tip", "head", latest, "blocks", blocks)
+
+ for i := 0; i < blocks; i++ {
+ number := latest - uint64(i)
+ block, err := client.Eth.BlockByNumber(ctx, big.NewInt(int64(number)))
+ if err != nil {
+ exit(err)
+ }
+ for _, tx := range block.Transactions() {
+ config, configName := randomTraceOption()
+ result, err := client.Geth.TraceTransaction(ctx, tx.Hash(), config)
+ if err != nil {
+ failed += 1
+ continue
+ }
+ blob, err := json.Marshal(result)
+ if err != nil {
+ failed += 1
+ continue
+ }
+ test.TxHashes = append(test.TxHashes, tx.Hash())
+ test.TraceConfigs = append(test.TraceConfigs, *config)
+ test.ResultHashes = append(test.ResultHashes, crypto.Keccak256Hash(blob))
+
+ writeTraceResult(outputDir, tx.Hash(), result, configName)
+
+ if time.Since(logged) > time.Second*8 {
+ logged = time.Now()
+ log.Info("Tracing transactions", "executed", len(test.TxHashes), "failed", failed, "elapsed", common.PrettyDuration(time.Since(start)))
+ }
+ }
+ }
+ log.Info("Traced transactions", "executed", len(test.TxHashes), "failed", failed, "elapsed", common.PrettyDuration(time.Since(start)))
+
+ // Write output file.
+ writeJSON(outputFile, test)
+ return nil
+}
+
+func randomTraceOption() (*tracers.TraceConfig, string) {
+ x := rand.Intn(11)
+ if x == 0 {
+ // struct-logger, with all fields enabled, very heavy
+ return &tracers.TraceConfig{
+ Config: &logger.Config{
+ // TODO(rjl493456442) fix it, the empty memory is not properly omitted
+ //EnableMemory: true,
+ EnableReturnData: true,
+ },
+ }, "structAll"
+ }
+ if x == 1 {
+ // default options for struct-logger, with stack and storage capture
+ // enabled
+ return &tracers.TraceConfig{
+ Config: &logger.Config{},
+ }, "structDefault"
+ }
+ if x == 2 || x == 3 || x == 4 {
+ // struct-logger with storage capture enabled
+ return &tracers.TraceConfig{
+ Config: &logger.Config{
+ DisableStack: true,
+ },
+ }, "structStorage"
+ }
+ // Native tracer
+ loggers := []string{"callTracer", "4byteTracer", "flatCallTracer", "muxTracer", "noopTracer", "prestateTracer"}
+ return &tracers.TraceConfig{
+ Tracer: &loggers[x-5],
+ }, loggers[x-5]
+}
+
+func writeTraceResult(dir string, hash common.Hash, result any, configName string) {
+ if dir == "" {
+ return
+ }
+ name := filepath.Join(dir, configName+"_"+hash.String())
+ file, err := os.Create(name)
+ if err != nil {
+ exit(fmt.Errorf("error creating %s: %v", name, err))
+ return
+ }
+ defer file.Close()
+
+ data, _ := json.MarshalIndent(result, "", " ")
+ _, err = file.Write(data)
+ if err != nil {
+ exit(fmt.Errorf("error writing %s: %v", name, err))
+ return
+ }
+}
diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go
index 39ac4ff20f..19263f4411 100644
--- a/ethclient/gethclient/gethclient.go
+++ b/ethclient/gethclient/gethclient.go
@@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
@@ -204,6 +205,24 @@ func (ec *Client) SubscribePendingTransactions(ctx context.Context, ch chan<- co
return ec.c.EthSubscribe(ctx, ch, "newPendingTransactions")
}
+// TraceTransaction returns the structured logs created during the execution of EVM
+// and returns them as a JSON object.
+func (ec *Client) TraceTransaction(ctx context.Context, hash common.Hash, config *tracers.TraceConfig) (any, error) {
+ var (
+ err error
+ result any
+ )
+ if config != nil {
+ err = ec.c.CallContext(ctx, &result, "debug_traceTransaction", hash.Hex(), config)
+ } else {
+ err = ec.c.CallContext(ctx, &result, "debug_traceTransaction", hash.Hex())
+ }
+ if err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
func toBlockNumArg(number *big.Int) string {
if number == nil {
return "latest"
diff --git a/ethclient/gethclient/gethclient_test.go b/ethclient/gethclient/gethclient_test.go
index 65d006d1e6..02635faabc 100644
--- a/ethclient/gethclient/gethclient_test.go
+++ b/ethclient/gethclient/gethclient_test.go
@@ -33,6 +33,7 @@ import (
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/filters"
+ "github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
@@ -47,13 +48,16 @@ var (
testSlot = common.HexToHash("0xdeadbeef")
testValue = crypto.Keccak256Hash(testSlot[:])
testBalance = big.NewInt(2e15)
+ testTxHashes []common.Hash
)
func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
// Generate test chain.
genesis, blocks := generateTestChain()
// Create node
- n, err := node.New(&node.Config{})
+ n, err := node.New(&node.Config{
+ HTTPModules: []string{"debug", "eth", "admin"},
+ })
if err != nil {
t.Fatalf("can't create new node: %v", err)
}
@@ -63,6 +67,8 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
if err != nil {
t.Fatalf("can't create new ethereum service: %v", err)
}
+ n.RegisterAPIs(tracers.APIs(ethservice.APIBackend))
+
filterSystem := filters.NewFilterSystem(ethservice.APIBackend, filters.Config{})
n.RegisterAPIs([]rpc.API{{
Namespace: "eth",
@@ -93,6 +99,19 @@ func generateTestChain() (*core.Genesis, []*types.Block) {
generate := func(i int, g *core.BlockGen) {
g.OffsetTime(5)
g.SetExtra([]byte("test"))
+
+ to := common.BytesToAddress([]byte{byte(i + 1)})
+ tx := types.NewTx(&types.LegacyTx{
+ Nonce: uint64(i),
+ To: &to,
+ Value: big.NewInt(int64(2*i + 1)),
+ Gas: params.TxGas,
+ GasPrice: big.NewInt(params.InitialBaseFee),
+ Data: nil,
+ })
+ tx, _ = types.SignTx(tx, types.LatestSignerForChainID(genesis.Config.ChainID), testKey)
+ g.AddTx(tx)
+ testTxHashes = append(testTxHashes, tx.Hash())
}
_, blocks, _ := core.GenerateChainWithGenesis(genesis, ethash.NewFaker(), 1, generate)
blocks = append([]*types.Block{genesis.ToBlock()}, blocks...)
@@ -136,9 +155,6 @@ func TestGethClient(t *testing.T) {
}, {
"TestSubscribePendingTxHashes",
func(t *testing.T) { testSubscribePendingTransactions(t, client) },
- }, {
- "TestSubscribePendingTxs",
- func(t *testing.T) { testSubscribeFullPendingTransactions(t, client) },
}, {
"TestCallContract",
func(t *testing.T) { testCallContract(t, client) },
@@ -153,7 +169,12 @@ func TestGethClient(t *testing.T) {
{
"TestAccessList",
func(t *testing.T) { testAccessList(t, client) },
- }, {
+ },
+ {
+ "TestTraceTransaction",
+ func(t *testing.T) { testTraceTransactions(t, client) },
+ },
+ {
"TestSetHead",
func(t *testing.T) { testSetHead(t, client) },
},
@@ -197,7 +218,7 @@ func testAccessList(t *testing.T, client *rpc.Client) {
wantVMErr: "execution reverted",
wantAL: `[
{
- "address": "0x3a220f351252089d385b29beca14e27f204c296a",
+ "address": "0xdb7d6ab1f17c6b31909ae466702703daef9269cf",
"storageKeys": [
"0x0000000000000000000000000000000000000000000000000000000000000081"
]
@@ -389,16 +410,26 @@ func testSetHead(t *testing.T, client *rpc.Client) {
func testSubscribePendingTransactions(t *testing.T, client *rpc.Client) {
ec := New(client)
ethcl := ethclient.NewClient(client)
+
// Subscribe to Transactions
- ch := make(chan common.Hash)
- ec.SubscribePendingTransactions(context.Background(), ch)
+ ch1 := make(chan common.Hash)
+ ec.SubscribePendingTransactions(context.Background(), ch1)
+
+ // Subscribe to Transactions
+ ch2 := make(chan *types.Transaction)
+ ec.SubscribeFullPendingTransactions(context.Background(), ch2)
+
// Send a transaction
chainID, err := ethcl.ChainID(context.Background())
if err != nil {
t.Fatal(err)
}
+ nonce, err := ethcl.NonceAt(context.Background(), testAddr, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
// Create transaction
- tx := types.NewTransaction(0, common.Address{1}, big.NewInt(1), 22000, big.NewInt(1), nil)
+ tx := types.NewTransaction(nonce, common.Address{1}, big.NewInt(1), 22000, big.NewInt(1), nil)
signer := types.LatestSignerForChainID(chainID)
signature, err := crypto.Sign(signer.Hash(tx).Bytes(), testKey)
if err != nil {
@@ -414,41 +445,12 @@ func testSubscribePendingTransactions(t *testing.T, client *rpc.Client) {
t.Fatal(err)
}
// Check that the transaction was sent over the channel
- hash := <-ch
+ hash := <-ch1
if hash != signedTx.Hash() {
t.Fatalf("Invalid tx hash received, got %v, want %v", hash, signedTx.Hash())
}
-}
-
-func testSubscribeFullPendingTransactions(t *testing.T, client *rpc.Client) {
- ec := New(client)
- ethcl := ethclient.NewClient(client)
- // Subscribe to Transactions
- ch := make(chan *types.Transaction)
- ec.SubscribeFullPendingTransactions(context.Background(), ch)
- // Send a transaction
- chainID, err := ethcl.ChainID(context.Background())
- if err != nil {
- t.Fatal(err)
- }
- // Create transaction
- tx := types.NewTransaction(1, common.Address{1}, big.NewInt(1), 22000, big.NewInt(1), nil)
- signer := types.LatestSignerForChainID(chainID)
- signature, err := crypto.Sign(signer.Hash(tx).Bytes(), testKey)
- if err != nil {
- t.Fatal(err)
- }
- signedTx, err := tx.WithSignature(signer, signature)
- if err != nil {
- t.Fatal(err)
- }
- // Send transaction
- err = ethcl.SendTransaction(context.Background(), signedTx)
- if err != nil {
- t.Fatal(err)
- }
// Check that the transaction was sent over the channel
- tx = <-ch
+ tx = <-ch2
if tx.Hash() != signedTx.Hash() {
t.Fatalf("Invalid tx hash received, got %v, want %v", tx.Hash(), signedTx.Hash())
}
@@ -478,6 +480,25 @@ func testCallContract(t *testing.T, client *rpc.Client) {
}
}
+func testTraceTransactions(t *testing.T, client *rpc.Client) {
+ ec := New(client)
+ for _, txHash := range testTxHashes {
+ // Struct logger
+ _, err := ec.TraceTransaction(context.Background(), txHash, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Struct logger
+ _, err = ec.TraceTransaction(context.Background(), txHash,
+ &tracers.TraceConfig{},
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
func TestOverrideAccountMarshal(t *testing.T) {
om := map[common.Address]OverrideAccount{
{0x11}: {