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 }