cmd/workload: add --sepolia and --mainnet flags

This commit is contained in:
Felix Lange 2025-02-25 13:38:52 +01:00
parent 8ddf889152
commit 2a2a902aab
5 changed files with 129 additions and 78 deletions

View file

@ -21,7 +21,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/big" "math/big"
"os"
"time" "time"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
@ -32,17 +31,18 @@ import (
"github.com/ethereum/go-ethereum/internal/utesting" "github.com/ethereum/go-ethereum/internal/utesting"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
) )
type filterTestSuite struct { type filterTestSuite struct {
ec *ethclient.Client cfg testConfig
filterTest queries [][]*filterQuery
} }
func newFilterTestSuite(ctx *cli.Context) *filterTestSuite { func newFilterTestSuite(cfg testConfig) *filterTestSuite {
s := &filterTestSuite{ec: makeEthClient(ctx)} s := &filterTestSuite{cfg: cfg}
s.filterTest.initFilterTest(ctx) if err := s.loadQueries(); err != nil {
exit(err)
}
return s return s
} }
@ -54,24 +54,9 @@ func (s *filterTestSuite) allTests() []utesting.Test {
} }
} }
type filterTest struct {
filterQueryFile, filterErrorFile string
filterQueries [filterBuckets][]*filterQuery
filterQueriesLoaded bool
filterErrors []*filterQuery
}
func (f *filterTest) initFilterTest(ctx *cli.Context) {
f.filterQueryFile = ctx.String(filterQueryFileFlag.Name)
f.filterErrorFile = ctx.String(filterErrorFileFlag.Name)
}
func (s *filterTestSuite) filterRange(t *utesting.T, test func(query *filterQuery) bool, do func(t *utesting.T, query *filterQuery)) { func (s *filterTestSuite) filterRange(t *utesting.T, test func(query *filterQuery) bool, do func(t *utesting.T, query *filterQuery)) {
if !s.filterQueriesLoaded {
s.loadQueries()
}
var count, total int var count, total int
for _, bucket := range s.filterQueries { for _, bucket := range s.queries {
for _, query := range bucket { for _, query := range bucket {
if test(query) { if test(query) {
total++ total++
@ -83,7 +68,7 @@ func (s *filterTestSuite) filterRange(t *utesting.T, test func(query *filterQuer
} }
start := time.Now() start := time.Now()
last := start last := start
for _, bucket := range s.filterQueries { for _, bucket := range s.queries {
for _, query := range bucket { for _, query := range bucket {
if test(query) { if test(query) {
do(t, query) do(t, query)
@ -117,14 +102,14 @@ func (s *filterTestSuite) filterLongRange(t *utesting.T) {
// filterFullRange runs all filter tests, extending their range from genesis up // filterFullRange runs all filter tests, extending their range from genesis up
// to the latest block. Note that results are only partially verified in this mode. // to the latest block. Note that results are only partially verified in this mode.
func (s *filterTestSuite) filterFullRange(t *utesting.T) { func (s *filterTestSuite) filterFullRange(t *utesting.T) {
finalized := mustGetFinalizedBlock(s.ec) finalized := mustGetFinalizedBlock(s.cfg.client)
s.filterRange(t, func(query *filterQuery) bool { s.filterRange(t, func(query *filterQuery) bool {
return query.ToBlock+1-query.FromBlock > finalized/2 return query.ToBlock+1-query.FromBlock > finalized/2
}, s.fullRangeQueryAndCheck) }, s.fullRangeQueryAndCheck)
} }
func (s *filterTestSuite) queryAndCheck(t *utesting.T, query *filterQuery) { func (s *filterTestSuite) queryAndCheck(t *utesting.T, query *filterQuery) {
query.run(s.ec) query.run(s.cfg.client)
if query.Err != nil { if query.Err != nil {
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) 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)
return return
@ -141,7 +126,7 @@ func (s *filterTestSuite) fullRangeQueryAndCheck(t *utesting.T, query *filterQue
Address: query.Address, Address: query.Address,
Topics: query.Topics, Topics: query.Topics,
} }
frQuery.run(s.ec) frQuery.run(s.cfg.client)
if frQuery.Err != nil { if frQuery.Err != nil {
t.Errorf("Full range filter query failed (addresses: %v topics: %v error: %v)", frQuery.Address, frQuery.Topics, frQuery.Err) t.Errorf("Full range filter query failed (addresses: %v topics: %v error: %v)", frQuery.Address, frQuery.Topics, frQuery.Err)
return return
@ -160,21 +145,27 @@ func (s *filterTestSuite) fullRangeQueryAndCheck(t *utesting.T, query *filterQue
} }
} }
func (s *filterTestSuite) loadQueries() int { func (s *filterTestSuite) loadQueries() error {
file, err := os.Open(s.filterQueryFile) file, err := s.cfg.fsys.Open(s.cfg.filterQueryFile)
if err != nil { if err != nil {
fmt.Println("Error opening filter test query file:", err) return fmt.Errorf("can't open filterQueryFile: %v", err)
return 0 }
defer file.Close()
var queries [][]*filterQuery
if err := json.NewDecoder(file).Decode(&queries); err != nil {
return fmt.Errorf("invalid JSON in %s: %v", s.cfg.filterQueryFile, err)
} }
json.NewDecoder(file).Decode(&s.filterQueries)
file.Close()
var count int var count int
for _, bucket := range s.filterQueries { for _, bucket := range queries {
count += len(bucket) count += len(bucket)
} }
if count == 0 {
return fmt.Errorf("filterQueryFile %s is empty", s.cfg.filterQueryFile)
}
fmt.Println("Loaded", count, "filter test queries") fmt.Println("Loaded", count, "filter test queries")
s.filterQueriesLoaded = true s.queries = queries
return count return nil
} }
// filterQuery is a single query for testing. // filterQuery is a single query for testing.

View file

@ -52,16 +52,6 @@ var (
filterErrorFileFlag, filterErrorFileFlag,
}, },
} }
filterPerfCommand = &cli.Command{
Name: "performance",
Usage: "Runs log filter performance test against an RPC endpoint",
ArgsUsage: "<RPC endpoint URL>",
Action: filterPerfCmd,
Flags: []cli.Flag{
filterQueryFileFlag,
filterErrorFileFlag,
},
}
filterQueryFileFlag = &cli.StringFlag{ filterQueryFileFlag = &cli.StringFlag{
Name: "queries", Name: "queries",
Usage: "JSON file containing filter test queries", Usage: "JSON file containing filter test queries",
@ -91,7 +81,7 @@ func filterGenCmd(ctx *cli.Context) error {
query := f.newQuery() query := f.newQuery()
query.run(f.ec) query.run(f.ec)
if query.Err != nil { if query.Err != nil {
f.filterErrors = append(f.filterErrors, query) f.errors = append(f.errors, query)
continue continue
} }
if len(query.results) > 0 && len(query.results) <= maxFilterResultSize { if len(query.results) > 0 && len(query.results) <= maxFilterResultSize {
@ -109,7 +99,7 @@ func filterGenCmd(ctx *cli.Context) error {
) )
} }
if extQuery.Err != nil { if extQuery.Err != nil {
f.filterErrors = append(f.filterErrors, extQuery) f.errors = append(f.errors, extQuery)
break break
} }
if len(extQuery.results) > maxFilterResultSize { if len(extQuery.results) > maxFilterResultSize {
@ -127,16 +117,23 @@ func filterGenCmd(ctx *cli.Context) error {
} }
} }
// filterTestGen is the filter query test generator.
type filterTestGen struct { type filterTestGen struct {
ec *ethclient.Client ec *ethclient.Client
queryFile string
errorFile string
finalizedBlock int64 finalizedBlock int64
filterTest queries [filterBuckets][]*filterQuery
errors []*filterQuery
} }
func newFilterTestGen(ctx *cli.Context) *filterTestGen { func newFilterTestGen(ctx *cli.Context) *filterTestGen {
s := &filterTestGen{ec: makeEthClient(ctx)} return &filterTestGen{
s.filterTest.initFilterTest(ctx) ec: makeEthClient(ctx),
return s queryFile: ctx.String(filterQueryFileFlag.Name),
errorFile: ctx.String(filterErrorFileFlag.Name),
}
} }
func (s *filterTestGen) updateFinalizedBlock() { func (s *filterTestGen) updateFinalizedBlock() {
@ -162,13 +159,13 @@ func (s *filterTestGen) storeQuery(query *filterQuery) {
if bucket >= filterBuckets { if bucket >= filterBuckets {
bucket = filterBuckets - 1 bucket = filterBuckets - 1
} }
if len(s.filterQueries[bucket]) < maxFilterBucketSize { if len(s.queries[bucket]) < maxFilterBucketSize {
s.filterQueries[bucket] = append(s.filterQueries[bucket], query) s.queries[bucket] = append(s.queries[bucket], query)
} else { } else {
s.filterQueries[bucket][rand.Intn(len(s.filterQueries[bucket]))] = query s.queries[bucket][rand.Intn(len(s.queries[bucket]))] = query
} }
fmt.Print("Generated queries per bucket:") fmt.Print("Generated queries per bucket:")
for _, list := range s.filterQueries { for _, list := range s.queries {
fmt.Print(" ", len(list)) fmt.Print(" ", len(list))
} }
fmt.Println() fmt.Println()
@ -340,7 +337,7 @@ func (s *filterTestGen) newNarrowedQuery() *filterQuery {
// randomQuery returns a random query from the ones that were already generated. // randomQuery returns a random query from the ones that were already generated.
func (s *filterTestGen) randomQuery() *filterQuery { func (s *filterTestGen) randomQuery() *filterQuery {
var bucket, bucketCount int var bucket, bucketCount int
for _, list := range s.filterQueries { for _, list := range s.queries {
if len(list) > 0 { if len(list) > 0 {
bucketCount++ bucketCount++
} }
@ -349,7 +346,7 @@ func (s *filterTestGen) randomQuery() *filterQuery {
return nil return nil
} }
pick := rand.Intn(bucketCount) pick := rand.Intn(bucketCount)
for i, list := range s.filterQueries { for i, list := range s.queries {
if len(list) > 0 { if len(list) > 0 {
if pick == 0 { if pick == 0 {
bucket = i bucket = i
@ -358,28 +355,28 @@ func (s *filterTestGen) randomQuery() *filterQuery {
pick-- pick--
} }
} }
return s.filterQueries[bucket][rand.Intn(len(s.filterQueries[bucket]))] return s.queries[bucket][rand.Intn(len(s.queries[bucket]))]
} }
// writeQueries serializes the generated queries to the output file. // writeQueries serializes the generated queries to the output file.
func (s *filterTestGen) writeQueries() { func (s *filterTestGen) writeQueries() {
file, err := os.Create(s.filterQueryFile) file, err := os.Create(s.queryFile)
if err != nil { if err != nil {
exit(fmt.Errorf("Error creating filter test query file %s: %v", s.filterQueryFile, err)) exit(fmt.Errorf("Error creating filter test query file %s: %v", s.queryFile, err))
return return
} }
json.NewEncoder(file).Encode(&s.filterQueries) json.NewEncoder(file).Encode(&s.queries)
file.Close() file.Close()
} }
// writeQueries serializes the generated errors to the error file. // writeQueries serializes the generated errors to the error file.
func (f *filterTestGen) writeErrors() { func (f *filterTestGen) writeErrors() {
file, err := os.Create(f.filterErrorFile) file, err := os.Create(f.errorFile)
if err != nil { if err != nil {
exit(fmt.Errorf("Error creating filter error file %s: %v", f.filterErrorFile, err)) exit(fmt.Errorf("Error creating filter error file %s: %v", f.errorFile, err))
return return
} }
json.NewEncoder(file).Encode(f.filterErrors) json.NewEncoder(file).Encode(f.errors)
file.Close() file.Close()
} }

View file

@ -19,19 +19,33 @@ package main
import ( import (
"fmt" "fmt"
"math/rand" "math/rand"
"slices"
"sort" "sort"
"time" "time"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var (
filterPerfCommand = &cli.Command{
Name: "performance",
Usage: "Runs log filter performance test against an RPC endpoint",
ArgsUsage: "<RPC endpoint URL>",
Action: filterPerfCmd,
Flags: []cli.Flag{
testSepoliaFlag,
testMainnetFlag,
filterQueryFileFlag,
filterErrorFileFlag,
},
}
)
const passCount = 1 const passCount = 1
func filterPerfCmd(ctx *cli.Context) error { func filterPerfCmd(ctx *cli.Context) error {
f := newFilterTestSuite(ctx) cfg := testConfigFromCLI(ctx)
if f.loadQueries() == 0 { f := newFilterTestSuite(cfg)
exit("No test requests loaded")
}
type queryTest struct { type queryTest struct {
query *filterQuery query *filterQuery
@ -40,7 +54,7 @@ func filterPerfCmd(ctx *cli.Context) error {
medianTime time.Duration medianTime time.Duration
} }
var queries, processed []queryTest var queries, processed []queryTest
for i, bucket := range f.filterQueries[:] { for i, bucket := range f.queries[:] {
for j, query := range bucket { for j, query := range bucket {
queries = append(queries, queryTest{query: query, bucket: i, index: j}) queries = append(queries, queryTest{query: query, bucket: i, index: j})
} }
@ -56,9 +70,9 @@ func filterPerfCmd(ctx *cli.Context) error {
queries[pick] = queries[len(queries)-1] queries[pick] = queries[len(queries)-1]
queries = queries[:len(queries)-1] queries = queries[:len(queries)-1]
start := time.Now() start := time.Now()
qt.query.run(f.ec) qt.query.run(cfg.client)
qt.runtime = append(qt.runtime, time.Since(start)) qt.runtime = append(qt.runtime, time.Since(start))
sort.Slice(qt.runtime, func(i, j int) bool { return qt.runtime[i] < qt.runtime[j] }) slices.Sort(qt.runtime)
qt.medianTime = qt.runtime[len(qt.runtime)/2] qt.medianTime = qt.runtime[len(qt.runtime)/2]
if qt.query.Err != nil { if qt.query.Err != nil {
failed++ failed++
@ -78,7 +92,7 @@ func filterPerfCmd(ctx *cli.Context) error {
// Show results and stats. // Show results and stats.
fmt.Println("Performance test finished; processed:", len(queries), "failed:", failed, "result mismatch:", mismatch) fmt.Println("Performance test finished; processed:", len(queries), "failed:", failed, "result mismatch:", mismatch)
stats := make([]bucketStats, len(f.filterQueries)) stats := make([]bucketStats, len(f.queries))
var wildcardStats bucketStats var wildcardStats bucketStats
for _, qt := range queries { for _, qt := range queries {
bs := &stats[qt.bucket] bs := &stats[qt.bucket]
@ -120,5 +134,4 @@ func (st *bucketStats) print(name string) {
} }
fmt.Printf("%-20s queries: %4d average block length: %12.2f average log count: %7.2f average runtime: %13v\n", fmt.Printf("%-20s queries: %4d average block length: %12.2f average log count: %7.2f average runtime: %13v\n",
name, st.count, float64(st.blocks)/float64(st.count), float64(st.logs)/float64(st.count), st.runtime/time.Duration(st.count)) name, st.count, float64(st.blocks)/float64(st.count), float64(st.logs)/float64(st.count), st.runtime/time.Duration(st.count))
} }

View file

@ -66,7 +66,7 @@ func makeEthClient(ctx *cli.Context) *ethclient.Client {
return cl return cl
} }
func exit(err interface{}) { func exit(err any) {
if err == nil { if err == nil {
os.Exit(0) os.Exit(0)
} }

View file

@ -17,14 +17,20 @@
package main package main
import ( import (
"embed"
"io/fs"
"os" "os"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/internal/flags" "github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/internal/utesting" "github.com/ethereum/go-ethereum/internal/utesting"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
//go:embed queries
var builtinTestFiles embed.FS
var ( var (
runTestCommand = &cli.Command{ runTestCommand = &cli.Command{
Name: "test", Name: "test",
@ -34,8 +40,9 @@ var (
Flags: []cli.Flag{ Flags: []cli.Flag{
testPatternFlag, testPatternFlag,
testTAPFlag, testTAPFlag,
testSepoliaFlag,
testMainnetFlag,
filterQueryFileFlag, filterQueryFileFlag,
filterErrorFileFlag,
}, },
} }
testPatternFlag = &cli.StringFlag{ testPatternFlag = &cli.StringFlag{
@ -48,10 +55,53 @@ var (
Usage: "Output test results in TAP format", Usage: "Output test results in TAP format",
Category: flags.TestingCategory, Category: flags.TestingCategory,
} }
testSepoliaFlag = &cli.BoolFlag{
Name: "sepolia",
Usage: "Use test cases for sepolia network",
Category: flags.TestingCategory,
}
testMainnetFlag = &cli.BoolFlag{
Name: "mainnet",
Usage: "Use test cases for mainnet network",
Category: flags.TestingCategory,
}
) )
// testConfig holds the parameters for testing.
type testConfig struct {
client *ethclient.Client
fsys fs.FS
filterQueryFile string
}
func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
flags.CheckExclusive(ctx, testMainnetFlag, testSepoliaFlag)
if (ctx.IsSet(testMainnetFlag.Name) || ctx.IsSet(testSepoliaFlag.Name)) && ctx.IsSet(filterQueryFileFlag.Name) {
exit(filterQueryFileFlag.Name + " cannot be used with " + testMainnetFlag.Name + " or " + testSepoliaFlag.Name)
}
// configure ethclient
cfg.client = makeEthClient(ctx)
// configure test files
switch {
case ctx.Bool(testMainnetFlag.Name):
cfg.fsys = builtinTestFiles
cfg.filterQueryFile = "queries/filter_queries_mainnet.json"
case ctx.Bool(testSepoliaFlag.Name):
cfg.fsys = builtinTestFiles
cfg.filterQueryFile = "queries/filter_queries_sepolia.json"
default:
cfg.fsys = os.DirFS(".")
cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name)
}
return cfg
}
func runTestCmd(ctx *cli.Context) error { func runTestCmd(ctx *cli.Context) error {
s := newFilterTestSuite(ctx) cfg := testConfigFromCLI(ctx)
s := newFilterTestSuite(cfg)
// Filter test cases. // Filter test cases.
tests := s.allTests() tests := s.allTests()