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