diff --git a/cmd/workload/filtertest.go b/cmd/workload/filtertest.go
index ae7af44b26..287158fba9 100644
--- a/cmd/workload/filtertest.go
+++ b/cmd/workload/filtertest.go
@@ -27,7 +27,6 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
- "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/internal/utesting"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc"
@@ -199,10 +198,10 @@ func (fq *filterQuery) calculateHash() common.Hash {
return crypto.Keccak256Hash(enc)
}
-func (fq *filterQuery) run(ec *ethclient.Client) {
+func (fq *filterQuery) run(client *client) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
- logs, err := ec.FilterLogs(ctx, ethereum.FilterQuery{
+ logs, err := client.Eth.FilterLogs(ctx, ethereum.FilterQuery{
FromBlock: big.NewInt(fq.FromBlock),
ToBlock: big.NewInt(fq.ToBlock),
Addresses: fq.Address,
diff --git a/cmd/workload/filtertestgen.go b/cmd/workload/filtertestgen.go
index b2ac32e115..ff3f1d44c2 100644
--- a/cmd/workload/filtertestgen.go
+++ b/cmd/workload/filtertestgen.go
@@ -27,7 +27,6 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
@@ -79,7 +78,7 @@ func filterGenCmd(ctx *cli.Context) error {
f.updateFinalizedBlock()
query := f.newQuery()
- query.run(f.ec)
+ query.run(f.client)
if query.Err != nil {
f.errors = append(f.errors, query)
continue
@@ -90,7 +89,7 @@ func filterGenCmd(ctx *cli.Context) error {
if extQuery == nil {
break
}
- extQuery.run(f.ec)
+ extQuery.run(f.client)
if extQuery.Err == nil && len(extQuery.results) < len(query.results) {
extQuery.Err = fmt.Errorf("invalid result length; old range %d %d; old length %d; new range %d %d; new length %d; address %v; Topics %v",
query.FromBlock, query.ToBlock, len(query.results),
@@ -119,7 +118,7 @@ func filterGenCmd(ctx *cli.Context) error {
// filterTestGen is the filter query test generator.
type filterTestGen struct {
- ec *ethclient.Client
+ client *client
queryFile string
errorFile string
@@ -130,14 +129,14 @@ type filterTestGen struct {
func newFilterTestGen(ctx *cli.Context) *filterTestGen {
return &filterTestGen{
- ec: makeEthClient(ctx),
+ client: makeClient(ctx),
queryFile: ctx.String(filterQueryFileFlag.Name),
errorFile: ctx.String(filterErrorFileFlag.Name),
}
}
func (s *filterTestGen) updateFinalizedBlock() {
- s.finalizedBlock = mustGetFinalizedBlock(s.ec)
+ s.finalizedBlock = mustGetFinalizedBlock(s.client)
}
const (
@@ -380,10 +379,10 @@ func (s *filterTestGen) writeErrors() {
json.NewEncoder(file).Encode(s.errors)
}
-func mustGetFinalizedBlock(ec *ethclient.Client) int64 {
+func mustGetFinalizedBlock(client *client) int64 {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
- header, err := ec.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber)))
+ header, err := client.Eth.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber)))
if err != nil {
exit(fmt.Errorf("could not fetch finalized header (error: %v)", err))
}
diff --git a/cmd/workload/historytestgen.go b/cmd/workload/historytestgen.go
new file mode 100644
index 0000000000..2bcd283e15
--- /dev/null
+++ b/cmd/workload/historytestgen.go
@@ -0,0 +1,170 @@
+// 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"
+ "os"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/internal/flags"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/urfave/cli/v2"
+)
+
+// eth_getBlockByHash
+// eth_getBlockByNumber
+// eth_getBlockReceipts
+// eth_getBlockTransactionCountByHash
+// eth_getBlockTransactionCountByNumber
+// eth_getTransactionByBlockHashAndIndex
+// eth_getTransactionByBlockNumberAndIndex
+
+var (
+ historyGenerateCommand = &cli.Command{
+ Name: "historygen",
+ Usage: "Generates history retrieval tests",
+ ArgsUsage: "",
+ Action: generateHistoryTests,
+ Flags: []cli.Flag{
+ historyTestFileFlag,
+ },
+ }
+
+ historyTestFileFlag = &cli.StringFlag{
+ Name: "history-tests",
+ Usage: "JSON file containing filter test queries",
+ Value: "history_tests.json",
+ Category: flags.TestingCategory,
+ }
+ historyTestEarliesFlag = &cli.IntFlag{
+ Name: "earliest",
+ Usage: "JSON file containing filter test queries",
+ Value: 0,
+ Category: flags.TestingCategory,
+ }
+)
+
+// historyTest is the content of a history test.
+type historyTest struct {
+ BlockNumbers []uint64 `json:"blockNumbers"`
+ BlockHashes []common.Hash `json:"blockHashes"`
+ TxCounts []int `json:"txCounts"`
+ TxHashIndex []int `json:"txHashIndex"`
+ TxHashes []*common.Hash `json:"txHashes"`
+ ReceiptsHashes []common.Hash `json:"blockReceiptsHashes"`
+}
+
+const historyTestBlockCount = 2000
+
+func generateHistoryTests(clictx *cli.Context) error {
+ var (
+ client = makeClient(clictx)
+ earliest = uint64(clictx.Int(historyTestEarliesFlag.Name))
+ outputFile = clictx.String(historyTestFileFlag.Name)
+ ctx = context.Background()
+ )
+
+ test := new(historyTest)
+
+ // Create the block numbers. Here we choose 1k blocks between earliest and head.
+ latest, err := client.Eth.BlockNumber(ctx)
+ if err != nil {
+ exit(err)
+ }
+ if latest < historyTestBlockCount {
+ exit(fmt.Errorf("node seems not synced, latest block is %d", latest))
+ }
+ test.BlockNumbers = make([]uint64, 0, historyTestBlockCount)
+ stride := (latest - earliest) / historyTestBlockCount
+ for b := earliest; b < latest; b += stride {
+ test.BlockNumbers = append(test.BlockNumbers, b)
+ }
+
+ // Get blocks and assign block info into the test
+ fmt.Println("Fetching blocks")
+ blocks := make([]*types.Block, len(test.BlockNumbers))
+ for i, blocknum := range test.BlockNumbers {
+ b, err := client.Eth.BlockByNumber(ctx, new(big.Int).SetUint64(blocknum))
+ if err != nil {
+ exit(fmt.Errorf("error fetching block %d: %v", blocknum, err))
+ }
+ blocks[i] = b
+ }
+ test.BlockHashes = make([]common.Hash, len(blocks))
+ test.TxCounts = make([]int, len(blocks))
+ for i, block := range blocks {
+ test.BlockHashes[i] = block.Hash()
+ test.TxCounts[i] = len(block.Transactions())
+ }
+
+ // Fill tx index.
+ test.TxHashIndex = make([]int, len(blocks))
+ test.TxHashes = make([]*common.Hash, len(blocks))
+ for i, block := range blocks {
+ txs := block.Transactions()
+ if len(txs) == 0 {
+ continue
+ }
+ index := len(txs) / 2
+ txhash := txs[index].Hash()
+ test.TxHashIndex[i] = index
+ test.TxHashes[i] = &txhash
+ }
+
+ // Get receipts.
+ fmt.Println("Fetching receipts")
+ test.ReceiptsHashes = make([]common.Hash, len(blocks))
+ for i, blockHash := range test.BlockHashes {
+ receipts, err := client.getBlockReceipts(ctx, blockHash)
+ if err != nil {
+ exit(fmt.Errorf("error fetching block %v receipts: %v", blockHash, err))
+ }
+ test.ReceiptsHashes[i] = calcReceiptsHash(receipts)
+ }
+
+ // Write output file.
+ writeJSON(outputFile, test)
+ return nil
+}
+
+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
+}
+
+func calcReceiptsHash(rcpt []*types.Receipt) common.Hash {
+ h := crypto.NewKeccakState()
+ rlp.Encode(h, rcpt)
+ return common.Hash(h.Sum(nil))
+}
+
+func writeJSON(fileName string, value any) {
+ file, err := os.Create(fileName)
+ if err != nil {
+ exit(fmt.Errorf("Error creating %s: %v", fileName, err))
+ return
+ }
+ defer file.Close()
+ json.NewEncoder(file).Encode(value)
+}
diff --git a/cmd/workload/main.go b/cmd/workload/main.go
index a4646164a7..1c03e4bbed 100644
--- a/cmd/workload/main.go
+++ b/cmd/workload/main.go
@@ -23,6 +23,7 @@ import (
"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"
)
@@ -46,6 +47,7 @@ func init() {
// Add subcommands.
app.Commands = []*cli.Command{
runTestCommand,
+ historyGenerateCommand,
filterCommand,
}
}
@@ -54,16 +56,24 @@ func main() {
exit(app.Run(os.Args))
}
-func makeEthClient(ctx *cli.Context) *ethclient.Client {
+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 := ethclient.Dial(url)
+ cl, err := rpc.Dial(url)
if err != nil {
exit(fmt.Errorf("Could not create RPC client at %s: %v", url, err))
}
- return cl
+ return &client{
+ RPC: cl,
+ Eth: ethclient.NewClient(cl),
+ }
}
func exit(err any) {
diff --git a/cmd/workload/testsuite.go b/cmd/workload/testsuite.go
index 99c2d2bf73..83eb7a7add 100644
--- a/cmd/workload/testsuite.go
+++ b/cmd/workload/testsuite.go
@@ -21,7 +21,6 @@ import (
"io/fs"
"os"
- "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/internal/utesting"
"github.com/ethereum/go-ethereum/log"
@@ -69,9 +68,10 @@ var (
// testConfig holds the parameters for testing.
type testConfig struct {
- client *ethclient.Client
+ client *client
fsys fs.FS
filterQueryFile string
+ historyTestFile string
}
func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
@@ -81,21 +81,23 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
}
// configure ethclient
- cfg.client = makeEthClient(ctx)
+ cfg.client = makeClient(ctx)
// configure test files
switch {
case ctx.Bool(testMainnetFlag.Name):
cfg.fsys = builtinTestFiles
cfg.filterQueryFile = "queries/filter_queries_mainnet.json"
+ cfg.historyTestFile = "queries/history_mainnet.json"
case ctx.Bool(testSepoliaFlag.Name):
cfg.fsys = builtinTestFiles
cfg.filterQueryFile = "queries/filter_queries_sepolia.json"
+ cfg.historyTestFile = "queries/history_sepolia.json"
default:
cfg.fsys = os.DirFS(".")
cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name)
+ cfg.historyTestFile = ctx.String(historyTestFileFlag.Name)
}
-
return cfg
}