Skip to content

Commit e01415c

Browse files
zsfelfoldifjls1na
authored andcommitted
cmd/workload: RPC workload tests for filters and history (ethereum#31189)
Co-authored-by: Felix Lange <[email protected]> Co-authored-by: Sina Mahmoodi <[email protected]>
1 parent dc49ffa commit e01415c

15 files changed

+1504
-48
lines changed

cmd/abigen/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func init() {
9393
}
9494

9595
func abigen(c *cli.Context) error {
96-
utils.CheckExclusive(c, abiFlag, jsonFlag) // Only one source can be selected.
96+
flags.CheckExclusive(c, abiFlag, jsonFlag) // Only one source can be selected.
9797

9898
if c.String(pkgFlag.Name) == "" {
9999
utils.Fatalf("No destination package specified (--pkg)")

cmd/utils/flags.go

Lines changed: 6 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,7 +1198,7 @@ func setWS(ctx *cli.Context, cfg *node.Config) {
11981198
// setIPC creates an IPC path configuration from the set command line flags,
11991199
// returning an empty string if IPC was explicitly disabled, or the set path.
12001200
func setIPC(ctx *cli.Context, cfg *node.Config) {
1201-
CheckExclusive(ctx, IPCDisabledFlag, IPCPathFlag)
1201+
flags.CheckExclusive(ctx, IPCDisabledFlag, IPCPathFlag)
12021202
switch {
12031203
case ctx.Bool(IPCDisabledFlag.Name):
12041204
cfg.IPCPath = ""
@@ -1296,8 +1296,8 @@ func SetP2PConfig(ctx *cli.Context, cfg *p2p.Config) {
12961296
cfg.NoDiscovery = true
12971297
}
12981298

1299-
CheckExclusive(ctx, DiscoveryV4Flag, NoDiscoverFlag)
1300-
CheckExclusive(ctx, DiscoveryV5Flag, NoDiscoverFlag)
1299+
flags.CheckExclusive(ctx, DiscoveryV4Flag, NoDiscoverFlag)
1300+
flags.CheckExclusive(ctx, DiscoveryV5Flag, NoDiscoverFlag)
13011301
cfg.DiscoveryV4 = ctx.Bool(DiscoveryV4Flag.Name)
13021302
cfg.DiscoveryV5 = ctx.Bool(DiscoveryV5Flag.Name)
13031303

@@ -1529,52 +1529,11 @@ func setRequiredBlocks(ctx *cli.Context, cfg *ethconfig.Config) {
15291529
}
15301530
}
15311531

1532-
// CheckExclusive verifies that only a single instance of the provided flags was
1533-
// set by the user. Each flag might optionally be followed by a string type to
1534-
// specialize it further.
1535-
func CheckExclusive(ctx *cli.Context, args ...interface{}) {
1536-
set := make([]string, 0, 1)
1537-
for i := 0; i < len(args); i++ {
1538-
// Make sure the next argument is a flag and skip if not set
1539-
flag, ok := args[i].(cli.Flag)
1540-
if !ok {
1541-
panic(fmt.Sprintf("invalid argument, not cli.Flag type: %T", args[i]))
1542-
}
1543-
// Check if next arg extends current and expand its name if so
1544-
name := flag.Names()[0]
1545-
1546-
if i+1 < len(args) {
1547-
switch option := args[i+1].(type) {
1548-
case string:
1549-
// Extended flag check, make sure value set doesn't conflict with passed in option
1550-
if ctx.String(flag.Names()[0]) == option {
1551-
name += "=" + option
1552-
set = append(set, "--"+name)
1553-
}
1554-
// shift arguments and continue
1555-
i++
1556-
continue
1557-
1558-
case cli.Flag:
1559-
default:
1560-
panic(fmt.Sprintf("invalid argument, not cli.Flag or string extension: %T", args[i+1]))
1561-
}
1562-
}
1563-
// Mark the flag if it's set
1564-
if ctx.IsSet(flag.Names()[0]) {
1565-
set = append(set, "--"+name)
1566-
}
1567-
}
1568-
if len(set) > 1 {
1569-
Fatalf("Flags %v can't be used at the same time", strings.Join(set, ", "))
1570-
}
1571-
}
1572-
15731532
// SetEthConfig applies eth-related command line flags to the config.
15741533
func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
15751534
// Avoid conflicting network flags
1576-
CheckExclusive(ctx, MainnetFlag, DeveloperFlag, SepoliaFlag, HoleskyFlag)
1577-
CheckExclusive(ctx, DeveloperFlag, ExternalSignerFlag) // Can't use both ephemeral unlocked and external signer
1535+
flags.CheckExclusive(ctx, MainnetFlag, DeveloperFlag, SepoliaFlag, HoleskyFlag)
1536+
flags.CheckExclusive(ctx, DeveloperFlag, ExternalSignerFlag) // Can't use both ephemeral unlocked and external signer
15781537

15791538
// Set configurations from CLI flags
15801539
setEtherbase(ctx, cfg)
@@ -1840,7 +1799,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
18401799
func MakeBeaconLightConfig(ctx *cli.Context) bparams.ClientConfig {
18411800
var config bparams.ClientConfig
18421801
customConfig := ctx.IsSet(BeaconConfigFlag.Name)
1843-
CheckExclusive(ctx, MainnetFlag, SepoliaFlag, HoleskyFlag, BeaconConfigFlag)
1802+
flags.CheckExclusive(ctx, MainnetFlag, SepoliaFlag, HoleskyFlag, BeaconConfigFlag)
18441803
switch {
18451804
case ctx.Bool(MainnetFlag.Name):
18461805
config.ChainConfig = *bparams.MainnetLightConfig

cmd/workload/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## Workload Testing Tool
2+
3+
This tool performs RPC calls against a live node. It has tests for the Sepolia testnet and
4+
Mainnet. Note the tests require a fully synced node.
5+
6+
To run the tests against a Sepolia node, use:
7+
8+
```shell
9+
> ./workload test --sepolia http://host:8545
10+
```
11+
12+
To run a specific test, use the `--run` flag to filter the test cases. Filtering works
13+
similar to the `go test` command. For example, to run only tests for `eth_getBlockByHash`
14+
and `eth_getBlockByNumber`, use this command:
15+
16+
```
17+
> ./workload test --sepolia --run History/getBlockBy http://host:8545
18+
```
19+
20+
### Regenerating tests
21+
22+
There is a facility for updating the tests from the chain. This can also be used to
23+
generate the tests for a new network. As an example, to recreate tests for mainnet, run
24+
the following commands (in this directory) against a synced mainnet node:
25+
26+
```shell
27+
> go run . filtergen --queries queries/filter_queries_mainnet.json http://host:8545
28+
> go run . historygen --history-tests queries/history_mainnet.json http://host:8545
29+
```

cmd/workload/filtertest.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright 2025 The go-ethereum Authors
2+
// This file is part of go-ethereum.
3+
//
4+
// go-ethereum is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// go-ethereum is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package main
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"math/big"
24+
"time"
25+
26+
"github.com/ethereum/go-ethereum"
27+
"github.com/ethereum/go-ethereum/common"
28+
"github.com/ethereum/go-ethereum/core/types"
29+
"github.com/ethereum/go-ethereum/crypto"
30+
"github.com/ethereum/go-ethereum/internal/utesting"
31+
"github.com/ethereum/go-ethereum/rlp"
32+
"github.com/ethereum/go-ethereum/rpc"
33+
)
34+
35+
type filterTestSuite struct {
36+
cfg testConfig
37+
queries [][]*filterQuery
38+
}
39+
40+
func newFilterTestSuite(cfg testConfig) *filterTestSuite {
41+
s := &filterTestSuite{cfg: cfg}
42+
if err := s.loadQueries(); err != nil {
43+
exit(err)
44+
}
45+
return s
46+
}
47+
48+
func (s *filterTestSuite) allTests() []utesting.Test {
49+
return []utesting.Test{
50+
{Name: "Filter/ShortRange", Fn: s.filterShortRange},
51+
{Name: "Filter/LongRange", Fn: s.filterLongRange, Slow: true},
52+
{Name: "Filter/FullRange", Fn: s.filterFullRange, Slow: true},
53+
}
54+
}
55+
56+
func (s *filterTestSuite) filterRange(t *utesting.T, test func(query *filterQuery) bool, do func(t *utesting.T, query *filterQuery)) {
57+
var count, total int
58+
for _, bucket := range s.queries {
59+
for _, query := range bucket {
60+
if test(query) {
61+
total++
62+
}
63+
}
64+
}
65+
if total == 0 {
66+
t.Fatalf("No suitable queries available")
67+
}
68+
start := time.Now()
69+
last := start
70+
for _, bucket := range s.queries {
71+
for _, query := range bucket {
72+
if test(query) {
73+
do(t, query)
74+
count++
75+
if time.Since(last) > time.Second*5 {
76+
t.Logf("Making filter query %d/%d (elapsed: %v)", count, total, time.Since(start))
77+
last = time.Now()
78+
}
79+
}
80+
}
81+
}
82+
t.Logf("Made %d filter queries (elapsed: %v)", count, time.Since(start))
83+
}
84+
85+
const filterRangeThreshold = 10000
86+
87+
// filterShortRange runs all short-range filter tests.
88+
func (s *filterTestSuite) filterShortRange(t *utesting.T) {
89+
s.filterRange(t, func(query *filterQuery) bool {
90+
return query.ToBlock+1-query.FromBlock <= filterRangeThreshold
91+
}, s.queryAndCheck)
92+
}
93+
94+
// filterShortRange runs all long-range filter tests.
95+
func (s *filterTestSuite) filterLongRange(t *utesting.T) {
96+
s.filterRange(t, func(query *filterQuery) bool {
97+
return query.ToBlock+1-query.FromBlock > filterRangeThreshold
98+
}, s.queryAndCheck)
99+
}
100+
101+
// filterFullRange runs all filter tests, extending their range from genesis up
102+
// to the latest block. Note that results are only partially verified in this mode.
103+
func (s *filterTestSuite) filterFullRange(t *utesting.T) {
104+
finalized := mustGetFinalizedBlock(s.cfg.client)
105+
s.filterRange(t, func(query *filterQuery) bool {
106+
return query.ToBlock+1-query.FromBlock > finalized/2
107+
}, s.fullRangeQueryAndCheck)
108+
}
109+
110+
func (s *filterTestSuite) queryAndCheck(t *utesting.T, query *filterQuery) {
111+
query.run(s.cfg.client)
112+
if query.Err != nil {
113+
t.Errorf("Filter query failed (fromBlock: %d toBlock: %d addresses: %v topics: %v error: %v)", query.FromBlock, query.ToBlock, query.Address, query.Topics, query.Err)
114+
return
115+
}
116+
if *query.ResultHash != query.calculateHash() {
117+
t.Fatalf("Filter query result mismatch (fromBlock: %d toBlock: %d addresses: %v topics: %v)", query.FromBlock, query.ToBlock, query.Address, query.Topics)
118+
}
119+
}
120+
121+
func (s *filterTestSuite) fullRangeQueryAndCheck(t *utesting.T, query *filterQuery) {
122+
frQuery := &filterQuery{ // create full range query
123+
FromBlock: 0,
124+
ToBlock: int64(rpc.LatestBlockNumber),
125+
Address: query.Address,
126+
Topics: query.Topics,
127+
}
128+
frQuery.run(s.cfg.client)
129+
if frQuery.Err != nil {
130+
t.Errorf("Full range filter query failed (addresses: %v topics: %v error: %v)", frQuery.Address, frQuery.Topics, frQuery.Err)
131+
return
132+
}
133+
// filter out results outside the original query range
134+
j := 0
135+
for _, log := range frQuery.results {
136+
if int64(log.BlockNumber) >= query.FromBlock && int64(log.BlockNumber) <= query.ToBlock {
137+
frQuery.results[j] = log
138+
j++
139+
}
140+
}
141+
frQuery.results = frQuery.results[:j]
142+
if *query.ResultHash != frQuery.calculateHash() {
143+
t.Fatalf("Full range filter query result mismatch (fromBlock: %d toBlock: %d addresses: %v topics: %v)", query.FromBlock, query.ToBlock, query.Address, query.Topics)
144+
}
145+
}
146+
147+
func (s *filterTestSuite) loadQueries() error {
148+
file, err := s.cfg.fsys.Open(s.cfg.filterQueryFile)
149+
if err != nil {
150+
return fmt.Errorf("can't open filterQueryFile: %v", err)
151+
}
152+
defer file.Close()
153+
154+
var queries [][]*filterQuery
155+
if err := json.NewDecoder(file).Decode(&queries); err != nil {
156+
return fmt.Errorf("invalid JSON in %s: %v", s.cfg.filterQueryFile, err)
157+
}
158+
var count int
159+
for _, bucket := range queries {
160+
count += len(bucket)
161+
}
162+
if count == 0 {
163+
return fmt.Errorf("filterQueryFile %s is empty", s.cfg.filterQueryFile)
164+
}
165+
s.queries = queries
166+
return nil
167+
}
168+
169+
// filterQuery is a single query for testing.
170+
type filterQuery struct {
171+
FromBlock int64 `json:"fromBlock"`
172+
ToBlock int64 `json:"toBlock"`
173+
Address []common.Address `json:"address"`
174+
Topics [][]common.Hash `json:"topics"`
175+
ResultHash *common.Hash `json:"resultHash,omitempty"`
176+
results []types.Log
177+
Err error `json:"error,omitempty"`
178+
}
179+
180+
func (fq *filterQuery) isWildcard() bool {
181+
if len(fq.Address) != 0 {
182+
return false
183+
}
184+
for _, topics := range fq.Topics {
185+
if len(topics) != 0 {
186+
return false
187+
}
188+
}
189+
return true
190+
}
191+
192+
func (fq *filterQuery) calculateHash() common.Hash {
193+
enc, err := rlp.EncodeToBytes(&fq.results)
194+
if err != nil {
195+
exit(fmt.Errorf("Error encoding logs: %v", err))
196+
}
197+
return crypto.Keccak256Hash(enc)
198+
}
199+
200+
func (fq *filterQuery) run(client *client) {
201+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
202+
defer cancel()
203+
logs, err := client.Eth.FilterLogs(ctx, ethereum.FilterQuery{
204+
FromBlock: big.NewInt(fq.FromBlock),
205+
ToBlock: big.NewInt(fq.ToBlock),
206+
Addresses: fq.Address,
207+
Topics: fq.Topics,
208+
})
209+
if err != nil {
210+
fq.Err = err
211+
fmt.Printf("Filter query failed: fromBlock: %d toBlock: %d addresses: %v topics: %v error: %v\n",
212+
fq.FromBlock, fq.ToBlock, fq.Address, fq.Topics, err)
213+
return
214+
}
215+
fq.results = logs
216+
}

0 commit comments

Comments
 (0)