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 }