github.com/MetalBlockchain/subnet-evm@v0.4.9/core/bloombits/matcher_test.go (about) 1 // (c) 2019-2020, Ava Labs, Inc. 2 // 3 // This file is a derived work, based on the go-ethereum library whose original 4 // notices appear below. 5 // 6 // It is distributed under a license compatible with the licensing terms of the 7 // original code from which it is derived. 8 // 9 // Much love to the original authors for their work. 10 // ********** 11 // Copyright 2017 The go-ethereum Authors 12 // This file is part of the go-ethereum library. 13 // 14 // The go-ethereum library is free software: you can redistribute it and/or modify 15 // it under the terms of the GNU Lesser General Public License as published by 16 // the Free Software Foundation, either version 3 of the License, or 17 // (at your option) any later version. 18 // 19 // The go-ethereum library is distributed in the hope that it will be useful, 20 // but WITHOUT ANY WARRANTY; without even the implied warranty of 21 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 // GNU Lesser General Public License for more details. 23 // 24 // You should have received a copy of the GNU Lesser General Public License 25 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 26 27 package bloombits 28 29 import ( 30 "context" 31 "math/rand" 32 "sync/atomic" 33 "testing" 34 "time" 35 36 "github.com/ethereum/go-ethereum/common" 37 ) 38 39 const testSectionSize = 4096 40 41 // Tests that wildcard filter rules (nil) can be specified and are handled well. 42 func TestMatcherWildcards(t *testing.T) { 43 t.Parallel() 44 matcher := NewMatcher(testSectionSize, [][][]byte{ 45 {common.Address{}.Bytes(), common.Address{0x01}.Bytes()}, // Default address is not a wildcard 46 {common.Hash{}.Bytes(), common.Hash{0x01}.Bytes()}, // Default hash is not a wildcard 47 {common.Hash{0x01}.Bytes()}, // Plain rule, sanity check 48 {common.Hash{0x01}.Bytes(), nil}, // Wildcard suffix, drop rule 49 {nil, common.Hash{0x01}.Bytes()}, // Wildcard prefix, drop rule 50 {nil, nil}, // Wildcard combo, drop rule 51 {}, // Inited wildcard rule, drop rule 52 nil, // Proper wildcard rule, drop rule 53 }) 54 if len(matcher.filters) != 3 { 55 t.Fatalf("filter system size mismatch: have %d, want %d", len(matcher.filters), 3) 56 } 57 if len(matcher.filters[0]) != 2 { 58 t.Fatalf("address clause size mismatch: have %d, want %d", len(matcher.filters[0]), 2) 59 } 60 if len(matcher.filters[1]) != 2 { 61 t.Fatalf("combo topic clause size mismatch: have %d, want %d", len(matcher.filters[1]), 2) 62 } 63 if len(matcher.filters[2]) != 1 { 64 t.Fatalf("singletone topic clause size mismatch: have %d, want %d", len(matcher.filters[2]), 1) 65 } 66 } 67 68 // Tests the matcher pipeline on a single continuous workflow without interrupts. 69 func TestMatcherContinuous(t *testing.T) { 70 t.Parallel() 71 testMatcherDiffBatches(t, [][]bloomIndexes{{{10, 20, 30}}}, 0, 100000, false, 75) 72 testMatcherDiffBatches(t, [][]bloomIndexes{{{32, 3125, 100}}, {{40, 50, 10}}}, 0, 100000, false, 81) 73 testMatcherDiffBatches(t, [][]bloomIndexes{{{4, 8, 11}, {7, 8, 17}}, {{9, 9, 12}, {15, 20, 13}}, {{18, 15, 15}, {12, 10, 4}}}, 0, 10000, false, 36) 74 } 75 76 // Tests the matcher pipeline on a constantly interrupted and resumed work pattern 77 // with the aim of ensuring data items are requested only once. 78 func TestMatcherIntermittent(t *testing.T) { 79 t.Parallel() 80 testMatcherDiffBatches(t, [][]bloomIndexes{{{10, 20, 30}}}, 0, 100000, true, 75) 81 testMatcherDiffBatches(t, [][]bloomIndexes{{{32, 3125, 100}}, {{40, 50, 10}}}, 0, 100000, true, 81) 82 testMatcherDiffBatches(t, [][]bloomIndexes{{{4, 8, 11}, {7, 8, 17}}, {{9, 9, 12}, {15, 20, 13}}, {{18, 15, 15}, {12, 10, 4}}}, 0, 10000, true, 36) 83 } 84 85 // Tests the matcher pipeline on random input to hopefully catch anomalies. 86 func TestMatcherRandom(t *testing.T) { 87 t.Parallel() 88 for i := 0; i < 10; i++ { 89 testMatcherBothModes(t, makeRandomIndexes([]int{1}, 50), 0, 10000, 0) 90 testMatcherBothModes(t, makeRandomIndexes([]int{3}, 50), 0, 10000, 0) 91 testMatcherBothModes(t, makeRandomIndexes([]int{2, 2, 2}, 20), 0, 10000, 0) 92 testMatcherBothModes(t, makeRandomIndexes([]int{5, 5, 5}, 50), 0, 10000, 0) 93 testMatcherBothModes(t, makeRandomIndexes([]int{4, 4, 4}, 20), 0, 10000, 0) 94 } 95 } 96 97 // Tests that the matcher can properly find matches if the starting block is 98 // shifter from a multiple of 8. This is needed to cover an optimisation with 99 // bitset matching https://github.com/ethereum/go-ethereum/issues/15309. 100 func TestMatcherShifted(t *testing.T) { 101 t.Parallel() 102 // Block 0 always matches in the tests, skip ahead of first 8 blocks with the 103 // start to get a potential zero byte in the matcher bitset. 104 105 // To keep the second bitset byte zero, the filter must only match for the first 106 // time in block 16, so doing an all-16 bit filter should suffice. 107 108 // To keep the starting block non divisible by 8, block number 9 is the first 109 // that would introduce a shift and not match block 0. 110 testMatcherBothModes(t, [][]bloomIndexes{{{16, 16, 16}}}, 9, 64, 0) 111 } 112 113 // Tests that matching on everything doesn't crash (special case internally). 114 func TestWildcardMatcher(t *testing.T) { 115 t.Parallel() 116 testMatcherBothModes(t, nil, 0, 10000, 0) 117 } 118 119 // makeRandomIndexes generates a random filter system, composed on multiple filter 120 // criteria, each having one bloom list component for the address and arbitrarily 121 // many topic bloom list components. 122 func makeRandomIndexes(lengths []int, max int) [][]bloomIndexes { 123 res := make([][]bloomIndexes, len(lengths)) 124 for i, topics := range lengths { 125 res[i] = make([]bloomIndexes, topics) 126 for j := 0; j < topics; j++ { 127 for k := 0; k < len(res[i][j]); k++ { 128 res[i][j][k] = uint(rand.Intn(max-1) + 2) 129 } 130 } 131 } 132 return res 133 } 134 135 // testMatcherDiffBatches runs the given matches test in single-delivery and also 136 // in batches delivery mode, verifying that all kinds of deliveries are handled 137 // correctly within. 138 func testMatcherDiffBatches(t *testing.T, filter [][]bloomIndexes, start, blocks uint64, intermittent bool, retrievals uint32) { 139 singleton := testMatcher(t, filter, start, blocks, intermittent, retrievals, 1) 140 batched := testMatcher(t, filter, start, blocks, intermittent, retrievals, 16) 141 142 if singleton != batched { 143 t.Errorf("filter = %v blocks = %v intermittent = %v: request count mismatch, %v in singleton vs. %v in batched mode", filter, blocks, intermittent, singleton, batched) 144 } 145 } 146 147 // testMatcherBothModes runs the given matcher test in both continuous as well as 148 // in intermittent mode, verifying that the request counts match each other. 149 func testMatcherBothModes(t *testing.T, filter [][]bloomIndexes, start, blocks uint64, retrievals uint32) { 150 continuous := testMatcher(t, filter, start, blocks, false, retrievals, 16) 151 intermittent := testMatcher(t, filter, start, blocks, true, retrievals, 16) 152 153 if continuous != intermittent { 154 t.Errorf("filter = %v blocks = %v: request count mismatch, %v in continuous vs. %v in intermittent mode", filter, blocks, continuous, intermittent) 155 } 156 } 157 158 // testMatcher is a generic tester to run the given matcher test and return the 159 // number of requests made for cross validation between different modes. 160 func testMatcher(t *testing.T, filter [][]bloomIndexes, start, blocks uint64, intermittent bool, retrievals uint32, maxReqCount int) uint32 { 161 // Create a new matcher an simulate our explicit random bitsets 162 matcher := NewMatcher(testSectionSize, nil) 163 matcher.filters = filter 164 165 for _, rule := range filter { 166 for _, topic := range rule { 167 for _, bit := range topic { 168 matcher.addScheduler(bit) 169 } 170 } 171 } 172 // Track the number of retrieval requests made 173 var requested uint32 174 175 // Start the matching session for the filter and the retriever goroutines 176 quit := make(chan struct{}) 177 matches := make(chan uint64, 16) 178 179 session, err := matcher.Start(context.Background(), start, blocks-1, matches) 180 if err != nil { 181 t.Fatalf("failed to stat matcher session: %v", err) 182 } 183 startRetrievers(session, quit, &requested, maxReqCount) 184 185 // Iterate over all the blocks and verify that the pipeline produces the correct matches 186 for i := start; i < blocks; i++ { 187 if expMatch3(filter, i) { 188 match, ok := <-matches 189 if !ok { 190 t.Errorf("filter = %v blocks = %v intermittent = %v: expected #%v, results channel closed", filter, blocks, intermittent, i) 191 return 0 192 } 193 if match != i { 194 t.Errorf("filter = %v blocks = %v intermittent = %v: expected #%v, got #%v", filter, blocks, intermittent, i, match) 195 } 196 // If we're testing intermittent mode, abort and restart the pipeline 197 if intermittent { 198 session.Close() 199 close(quit) 200 201 quit = make(chan struct{}) 202 matches = make(chan uint64, 16) 203 204 session, err = matcher.Start(context.Background(), i+1, blocks-1, matches) 205 if err != nil { 206 t.Fatalf("failed to stat matcher session: %v", err) 207 } 208 startRetrievers(session, quit, &requested, maxReqCount) 209 } 210 } 211 } 212 // Ensure the result channel is torn down after the last block 213 match, ok := <-matches 214 if ok { 215 t.Errorf("filter = %v blocks = %v intermittent = %v: expected closed channel, got #%v", filter, blocks, intermittent, match) 216 } 217 // Clean up the session and ensure we match the expected retrieval count 218 session.Close() 219 close(quit) 220 221 if retrievals != 0 && requested != retrievals { 222 t.Errorf("filter = %v blocks = %v intermittent = %v: request count mismatch, have #%v, want #%v", filter, blocks, intermittent, requested, retrievals) 223 } 224 return requested 225 } 226 227 // startRetrievers starts a batch of goroutines listening for section requests 228 // and serving them. 229 func startRetrievers(session *MatcherSession, quit chan struct{}, retrievals *uint32, batch int) { 230 requests := make(chan chan *Retrieval) 231 232 for i := 0; i < 10; i++ { 233 // Start a multiplexer to test multiple threaded execution 234 go session.Multiplex(batch, 100*time.Microsecond, requests) 235 236 // Start a services to match the above multiplexer 237 go func() { 238 for { 239 // Wait for a service request or a shutdown 240 select { 241 case <-quit: 242 return 243 244 case request := <-requests: 245 task := <-request 246 247 task.Bitsets = make([][]byte, len(task.Sections)) 248 for i, section := range task.Sections { 249 if rand.Int()%4 != 0 { // Handle occasional missing deliveries 250 task.Bitsets[i] = generateBitset(task.Bit, section) 251 atomic.AddUint32(retrievals, 1) 252 } 253 } 254 request <- task 255 } 256 } 257 }() 258 } 259 } 260 261 // generateBitset generates the rotated bitset for the given bloom bit and section 262 // numbers. 263 func generateBitset(bit uint, section uint64) []byte { 264 bitset := make([]byte, testSectionSize/8) 265 for i := 0; i < len(bitset); i++ { 266 for b := 0; b < 8; b++ { 267 blockIdx := section*testSectionSize + uint64(i*8+b) 268 bitset[i] += bitset[i] 269 if (blockIdx % uint64(bit)) == 0 { 270 bitset[i]++ 271 } 272 } 273 } 274 return bitset 275 } 276 277 func expMatch1(filter bloomIndexes, i uint64) bool { 278 for _, ii := range filter { 279 if (i % uint64(ii)) != 0 { 280 return false 281 } 282 } 283 return true 284 } 285 286 func expMatch2(filter []bloomIndexes, i uint64) bool { 287 for _, ii := range filter { 288 if expMatch1(ii, i) { 289 return true 290 } 291 } 292 return false 293 } 294 295 func expMatch3(filter [][]bloomIndexes, i uint64) bool { 296 for _, ii := range filter { 297 if !expMatch2(ii, i) { 298 return false 299 } 300 } 301 return true 302 }