github.com/ethereum/go-ethereum@v1.16.1/cmd/workload/filtertest.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/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() []workloadTest { 49 return []workloadTest{ 50 newWorkLoadTest("Filter/ShortRange", s.filterShortRange), 51 newSlowWorkloadTest("Filter/LongRange", s.filterLongRange), 52 newSlowWorkloadTest("Filter/FullRange", s.filterFullRange), 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, s.cfg.historyPruneBlock) 112 if query.Err == errPrunedHistory { 113 return 114 } 115 if query.Err != nil { 116 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) 117 return 118 } 119 if *query.ResultHash != query.calculateHash() { 120 t.Fatalf("Filter query result mismatch (fromBlock: %d toBlock: %d addresses: %v topics: %v)", query.FromBlock, query.ToBlock, query.Address, query.Topics) 121 } 122 } 123 124 func (s *filterTestSuite) fullRangeQueryAndCheck(t *utesting.T, query *filterQuery) { 125 frQuery := &filterQuery{ // create full range query 126 FromBlock: 0, 127 ToBlock: int64(rpc.LatestBlockNumber), 128 Address: query.Address, 129 Topics: query.Topics, 130 } 131 frQuery.run(s.cfg.client, s.cfg.historyPruneBlock) 132 if frQuery.Err == errPrunedHistory { 133 return 134 } 135 if frQuery.Err != nil { 136 t.Errorf("Full range filter query failed (addresses: %v topics: %v error: %v)", frQuery.Address, frQuery.Topics, frQuery.Err) 137 return 138 } 139 // filter out results outside the original query range 140 j := 0 141 for _, log := range frQuery.results { 142 if int64(log.BlockNumber) >= query.FromBlock && int64(log.BlockNumber) <= query.ToBlock { 143 frQuery.results[j] = log 144 j++ 145 } 146 } 147 frQuery.results = frQuery.results[:j] 148 if *query.ResultHash != frQuery.calculateHash() { 149 t.Fatalf("Full range filter query result mismatch (fromBlock: %d toBlock: %d addresses: %v topics: %v)", query.FromBlock, query.ToBlock, query.Address, query.Topics) 150 } 151 } 152 153 func (s *filterTestSuite) loadQueries() error { 154 file, err := s.cfg.fsys.Open(s.cfg.filterQueryFile) 155 if err != nil { 156 return fmt.Errorf("can't open filterQueryFile: %v", err) 157 } 158 defer file.Close() 159 160 var queries [][]*filterQuery 161 if err := json.NewDecoder(file).Decode(&queries); err != nil { 162 return fmt.Errorf("invalid JSON in %s: %v", s.cfg.filterQueryFile, err) 163 } 164 var count int 165 for _, bucket := range queries { 166 count += len(bucket) 167 } 168 if count == 0 { 169 return fmt.Errorf("filterQueryFile %s is empty", s.cfg.filterQueryFile) 170 } 171 s.queries = queries 172 return nil 173 } 174 175 // filterQuery is a single query for testing. 176 type filterQuery struct { 177 FromBlock int64 `json:"fromBlock"` 178 ToBlock int64 `json:"toBlock"` 179 Address []common.Address `json:"address"` 180 Topics [][]common.Hash `json:"topics"` 181 ResultHash *common.Hash `json:"resultHash,omitempty"` 182 results []types.Log 183 Err error `json:"error,omitempty"` 184 } 185 186 func (fq *filterQuery) isWildcard() bool { 187 if len(fq.Address) != 0 { 188 return false 189 } 190 for _, topics := range fq.Topics { 191 if len(topics) != 0 { 192 return false 193 } 194 } 195 return true 196 } 197 198 func (fq *filterQuery) calculateHash() common.Hash { 199 enc, err := rlp.EncodeToBytes(&fq.results) 200 if err != nil { 201 exit(fmt.Errorf("Error encoding logs: %v", err)) 202 } 203 return crypto.Keccak256Hash(enc) 204 } 205 206 func (fq *filterQuery) run(client *client, historyPruneBlock *uint64) { 207 ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) 208 defer cancel() 209 logs, err := client.Eth.FilterLogs(ctx, ethereum.FilterQuery{ 210 FromBlock: big.NewInt(fq.FromBlock), 211 ToBlock: big.NewInt(fq.ToBlock), 212 Addresses: fq.Address, 213 Topics: fq.Topics, 214 }) 215 fq.results = logs 216 fq.Err = validateHistoryPruneErr(err, uint64(fq.FromBlock), historyPruneBlock) 217 } 218 219 func (fq *filterQuery) printError() { 220 fmt.Printf("Filter query failed: fromBlock: %d toBlock: %d addresses: %v topics: %v error: %v\n", 221 fq.FromBlock, fq.ToBlock, fq.Address, fq.Topics, fq.Err) 222 }