github.com/ethereum/go-ethereum@v1.16.1/cmd/workload/filtertestgen.go (about)

     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"
    24  	"math/big"
    25  	"math/rand"
    26  	"os"
    27  	"time"
    28  
    29  	"github.com/ethereum/go-ethereum/common"
    30  	"github.com/ethereum/go-ethereum/internal/flags"
    31  	"github.com/ethereum/go-ethereum/rpc"
    32  	"github.com/urfave/cli/v2"
    33  )
    34  
    35  var (
    36  	filterGenerateCommand = &cli.Command{
    37  		Name:      "filtergen",
    38  		Usage:     "Generates query set for log filter workload test",
    39  		ArgsUsage: "<RPC endpoint URL>",
    40  		Action:    filterGenCmd,
    41  		Flags: []cli.Flag{
    42  			filterQueryFileFlag,
    43  		},
    44  	}
    45  	filterQueryFileFlag = &cli.StringFlag{
    46  		Name:     "queries",
    47  		Usage:    "JSON file containing filter test queries",
    48  		Value:    "filter_queries.json",
    49  		Category: flags.TestingCategory,
    50  	}
    51  	filterErrorFileFlag = &cli.StringFlag{
    52  		Name:     "errors",
    53  		Usage:    "JSON file containing failed filter queries",
    54  		Value:    "filter_errors.json",
    55  		Category: flags.TestingCategory,
    56  	}
    57  )
    58  
    59  // filterGenCmd is the main function of the filter tests generator.
    60  func filterGenCmd(ctx *cli.Context) error {
    61  	f := newFilterTestGen(ctx)
    62  	lastWrite := time.Now()
    63  	for {
    64  		select {
    65  		case <-ctx.Done():
    66  			return nil
    67  		default:
    68  		}
    69  
    70  		f.updateFinalizedBlock()
    71  		query := f.newQuery()
    72  		query.run(f.client, nil)
    73  		if query.Err != nil {
    74  			query.printError()
    75  			exit("filter query failed")
    76  		}
    77  		if len(query.results) > 0 && len(query.results) <= maxFilterResultSize {
    78  			for {
    79  				extQuery := f.extendRange(query)
    80  				if extQuery == nil {
    81  					break
    82  				}
    83  				extQuery.run(f.client, nil)
    84  				if extQuery.Err == nil && len(extQuery.results) < len(query.results) {
    85  					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",
    86  						query.FromBlock, query.ToBlock, len(query.results),
    87  						extQuery.FromBlock, extQuery.ToBlock, len(extQuery.results),
    88  						extQuery.Address, extQuery.Topics,
    89  					)
    90  				}
    91  				if extQuery.Err != nil {
    92  					extQuery.printError()
    93  					exit("filter query failed")
    94  				}
    95  				if len(extQuery.results) > maxFilterResultSize {
    96  					break
    97  				}
    98  				query = extQuery
    99  			}
   100  			f.storeQuery(query)
   101  			if time.Since(lastWrite) > time.Second*10 {
   102  				f.writeQueries()
   103  				lastWrite = time.Now()
   104  			}
   105  		}
   106  	}
   107  }
   108  
   109  // filterTestGen is the filter query test generator.
   110  type filterTestGen struct {
   111  	client    *client
   112  	queryFile string
   113  
   114  	finalizedBlock int64
   115  	queries        [filterBuckets][]*filterQuery
   116  }
   117  
   118  func newFilterTestGen(ctx *cli.Context) *filterTestGen {
   119  	return &filterTestGen{
   120  		client:    makeClient(ctx),
   121  		queryFile: ctx.String(filterQueryFileFlag.Name),
   122  	}
   123  }
   124  
   125  func (s *filterTestGen) updateFinalizedBlock() {
   126  	s.finalizedBlock = mustGetFinalizedBlock(s.client)
   127  }
   128  
   129  const (
   130  	// Parameter of the random filter query generator.
   131  	maxFilterRange      = 10000000
   132  	maxFilterResultSize = 300
   133  	filterBuckets       = 10
   134  	maxFilterBucketSize = 100
   135  	filterSeedChance    = 10
   136  	filterMergeChance   = 45
   137  )
   138  
   139  // storeQuery adds a filter query to the output file.
   140  func (s *filterTestGen) storeQuery(query *filterQuery) {
   141  	query.ResultHash = new(common.Hash)
   142  	*query.ResultHash = query.calculateHash()
   143  	logRatio := math.Log(float64(len(query.results))*float64(s.finalizedBlock)/float64(query.ToBlock+1-query.FromBlock)) / math.Log(float64(s.finalizedBlock)*maxFilterResultSize)
   144  	bucket := int(math.Floor(logRatio * filterBuckets))
   145  	if bucket >= filterBuckets {
   146  		bucket = filterBuckets - 1
   147  	}
   148  	if len(s.queries[bucket]) < maxFilterBucketSize {
   149  		s.queries[bucket] = append(s.queries[bucket], query)
   150  	} else {
   151  		s.queries[bucket][rand.Intn(len(s.queries[bucket]))] = query
   152  	}
   153  	fmt.Print("Generated queries per bucket:")
   154  	for _, list := range s.queries {
   155  		fmt.Print(" ", len(list))
   156  	}
   157  	fmt.Println()
   158  }
   159  
   160  func (s *filterTestGen) extendRange(q *filterQuery) *filterQuery {
   161  	rangeLen := q.ToBlock + 1 - q.FromBlock
   162  	extLen := rand.Int63n(rangeLen) + 1
   163  	if rangeLen+extLen > s.finalizedBlock {
   164  		return nil
   165  	}
   166  	extBefore := min(rand.Int63n(extLen+1), q.FromBlock)
   167  	extAfter := extLen - extBefore
   168  	if q.ToBlock+extAfter > s.finalizedBlock {
   169  		d := q.ToBlock + extAfter - s.finalizedBlock
   170  		extAfter -= d
   171  		if extBefore+d <= q.FromBlock {
   172  			extBefore += d
   173  		} else {
   174  			extBefore = q.FromBlock
   175  		}
   176  	}
   177  	return &filterQuery{
   178  		FromBlock: q.FromBlock - extBefore,
   179  		ToBlock:   q.ToBlock + extAfter,
   180  		Address:   q.Address,
   181  		Topics:    q.Topics,
   182  	}
   183  }
   184  
   185  // newQuery generates a new filter query.
   186  func (s *filterTestGen) newQuery() *filterQuery {
   187  	for {
   188  		t := rand.Intn(100)
   189  		if t < filterSeedChance {
   190  			return s.newSeedQuery()
   191  		}
   192  		if t < filterSeedChance+filterMergeChance {
   193  			if query := s.newMergedQuery(); query != nil {
   194  				return query
   195  			}
   196  			continue
   197  		}
   198  		if query := s.newNarrowedQuery(); query != nil {
   199  			return query
   200  		}
   201  	}
   202  }
   203  
   204  // newSeedQuery creates a query that gets all logs in a random non-finalized block.
   205  func (s *filterTestGen) newSeedQuery() *filterQuery {
   206  	block := rand.Int63n(s.finalizedBlock + 1)
   207  	return &filterQuery{
   208  		FromBlock: block,
   209  		ToBlock:   block,
   210  	}
   211  }
   212  
   213  // newMergedQuery creates a new query by combining (with OR) the filter criteria
   214  // of two existing queries (chosen at random).
   215  func (s *filterTestGen) newMergedQuery() *filterQuery {
   216  	q1 := s.randomQuery()
   217  	q2 := s.randomQuery()
   218  	if q1 == nil || q2 == nil || q1 == q2 {
   219  		return nil
   220  	}
   221  	var (
   222  		block      int64
   223  		topicCount int
   224  	)
   225  	if rand.Intn(2) == 0 {
   226  		block = q1.FromBlock + rand.Int63n(q1.ToBlock+1-q1.FromBlock)
   227  		topicCount = len(q1.Topics)
   228  	} else {
   229  		block = q2.FromBlock + rand.Int63n(q2.ToBlock+1-q2.FromBlock)
   230  		topicCount = len(q2.Topics)
   231  	}
   232  	m := &filterQuery{
   233  		FromBlock: block,
   234  		ToBlock:   block,
   235  		Topics:    make([][]common.Hash, topicCount),
   236  	}
   237  	for _, addr := range q1.Address {
   238  		if rand.Intn(2) == 0 {
   239  			m.Address = append(m.Address, addr)
   240  		}
   241  	}
   242  	for _, addr := range q2.Address {
   243  		if rand.Intn(2) == 0 {
   244  			m.Address = append(m.Address, addr)
   245  		}
   246  	}
   247  	for i := range m.Topics {
   248  		if len(q1.Topics) > i {
   249  			for _, topic := range q1.Topics[i] {
   250  				if rand.Intn(2) == 0 {
   251  					m.Topics[i] = append(m.Topics[i], topic)
   252  				}
   253  			}
   254  		}
   255  		if len(q2.Topics) > i {
   256  			for _, topic := range q2.Topics[i] {
   257  				if rand.Intn(2) == 0 {
   258  					m.Topics[i] = append(m.Topics[i], topic)
   259  				}
   260  			}
   261  		}
   262  	}
   263  	return m
   264  }
   265  
   266  // newNarrowedQuery creates a new query by 'narrowing' an existing (randomly chosen)
   267  // query. The new query is made more specific by analyzing the filter criteria and adding
   268  // topics/addresses from the known result set.
   269  func (s *filterTestGen) newNarrowedQuery() *filterQuery {
   270  	q := s.randomQuery()
   271  	if q == nil {
   272  		return nil
   273  	}
   274  	log := q.results[rand.Intn(len(q.results))]
   275  	var emptyCount int
   276  	if len(q.Address) == 0 {
   277  		emptyCount++
   278  	}
   279  	for i := range log.Topics {
   280  		if len(q.Topics) <= i || len(q.Topics[i]) == 0 {
   281  			emptyCount++
   282  		}
   283  	}
   284  	if emptyCount == 0 {
   285  		return nil
   286  	}
   287  	query := &filterQuery{
   288  		FromBlock: q.FromBlock,
   289  		ToBlock:   q.ToBlock,
   290  		Address:   make([]common.Address, len(q.Address)),
   291  		Topics:    make([][]common.Hash, len(q.Topics)),
   292  	}
   293  	copy(query.Address, q.Address)
   294  	for i, topics := range q.Topics {
   295  		if len(topics) > 0 {
   296  			query.Topics[i] = make([]common.Hash, len(topics))
   297  			copy(query.Topics[i], topics)
   298  		}
   299  	}
   300  	pick := rand.Intn(emptyCount)
   301  	if len(query.Address) == 0 {
   302  		if pick == 0 {
   303  			query.Address = []common.Address{log.Address}
   304  			return query
   305  		}
   306  		pick--
   307  	}
   308  	for i := range log.Topics {
   309  		if len(query.Topics) <= i || len(query.Topics[i]) == 0 {
   310  			if pick == 0 {
   311  				if len(query.Topics) <= i {
   312  					query.Topics = append(query.Topics, make([][]common.Hash, i+1-len(query.Topics))...)
   313  				}
   314  				query.Topics[i] = []common.Hash{log.Topics[i]}
   315  				return query
   316  			}
   317  			pick--
   318  		}
   319  	}
   320  	panic("unreachable")
   321  }
   322  
   323  // randomQuery returns a random query from the ones that were already generated.
   324  func (s *filterTestGen) randomQuery() *filterQuery {
   325  	var bucket, bucketCount int
   326  	for _, list := range s.queries {
   327  		if len(list) > 0 {
   328  			bucketCount++
   329  		}
   330  	}
   331  	if bucketCount == 0 {
   332  		return nil
   333  	}
   334  	pick := rand.Intn(bucketCount)
   335  	for i, list := range s.queries {
   336  		if len(list) > 0 {
   337  			if pick == 0 {
   338  				bucket = i
   339  				break
   340  			}
   341  			pick--
   342  		}
   343  	}
   344  	return s.queries[bucket][rand.Intn(len(s.queries[bucket]))]
   345  }
   346  
   347  // writeQueries serializes the generated queries to the output file.
   348  func (s *filterTestGen) writeQueries() {
   349  	file, err := os.Create(s.queryFile)
   350  	if err != nil {
   351  		exit(fmt.Errorf("Error creating filter test query file %s: %v", s.queryFile, err))
   352  		return
   353  	}
   354  	json.NewEncoder(file).Encode(&s.queries)
   355  	file.Close()
   356  }
   357  
   358  func mustGetFinalizedBlock(client *client) int64 {
   359  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   360  	defer cancel()
   361  	header, err := client.Eth.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber)))
   362  	if err != nil {
   363  		exit(fmt.Errorf("could not fetch finalized header (error: %v)", err))
   364  	}
   365  	return header.Number.Int64()
   366  }