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  }