github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/chainsync/core_rapid_test.go (about)

     1  package chainsync
     2  
     3  import (
     4  	"io"
     5  	"testing"
     6  
     7  	"github.com/rs/zerolog"
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"pgregory.net/rapid"
    12  
    13  	"github.com/onflow/flow-go/model/flow"
    14  	"github.com/onflow/flow-go/module/metrics"
    15  	"github.com/onflow/flow-go/utils/unittest"
    16  )
    17  
    18  // TODO: remove this restriction and write a proper parametrizable generator (à la SliceOfN) once we have
    19  // a FlatMap combinator in rapid
    20  const NUM_BLOCKS int = 100
    21  
    22  // This returns a forest of blocks, some of which are in a parent relationship
    23  // It should include forks
    24  func populatedBlockStore(t *rapid.T) []*flow.Header {
    25  	store := []*flow.Header{unittest.BlockHeaderFixture()}
    26  	for i := 1; i < NUM_BLOCKS; i++ {
    27  		// we sample from the store 2/3 times to get deeper trees
    28  		b := rapid.OneOf(rapid.Just(unittest.BlockHeaderFixture()), rapid.SampledFrom(store), rapid.SampledFrom(store)).Draw(t, "parent")
    29  		store = append(store, unittest.BlockHeaderWithParentFixture(b))
    30  	}
    31  	return store
    32  }
    33  
    34  type rapidSync struct {
    35  	store          []*flow.Header
    36  	core           *Core
    37  	idRequests     map[flow.Identifier]bool // depth 1 pushdown automaton to track ID requests
    38  	heightRequests map[uint64]bool          // depth 1 pushdown automaton to track height requests
    39  }
    40  
    41  // init is an action for initializing a rapidSync instance.
    42  func (r *rapidSync) init(t *rapid.T) {
    43  	var err error
    44  
    45  	r.core, err = New(zerolog.New(io.Discard), DefaultConfig(), metrics.NewNoopCollector(), flow.Localnet)
    46  	require.NoError(t, err)
    47  
    48  	r.store = populatedBlockStore(t)
    49  	r.idRequests = make(map[flow.Identifier]bool)
    50  	r.heightRequests = make(map[uint64]bool)
    51  }
    52  
    53  // RequestByID is an action that requests a block by its ID.
    54  func (r *rapidSync) RequestByID(t *rapid.T) {
    55  	b := rapid.SampledFrom(r.store).Draw(t, "id_request")
    56  	r.core.RequestBlock(b.ID(), b.Height)
    57  	// Re-queueing by ID should always succeed
    58  	r.idRequests[b.ID()] = true
    59  	// Re-qeueuing by ID "forgets" a past height request
    60  	r.heightRequests[b.Height] = false
    61  }
    62  
    63  // RequestByHeight is an action that requests a specific height
    64  func (r *rapidSync) RequestByHeight(t *rapid.T) {
    65  	b := rapid.SampledFrom(r.store).Draw(t, "height_request")
    66  	r.core.RequestHeight(b.Height)
    67  	// Re-queueing by height should always succeed
    68  	r.heightRequests[b.Height] = true
    69  }
    70  
    71  // HandleHeight is an action that requests a heights
    72  // upon receiving an argument beyond a certain tolerance
    73  func (r *rapidSync) HandleHeight(t *rapid.T) {
    74  	b := rapid.SampledFrom(r.store).Draw(t, "height_hint_request")
    75  	incr := rapid.IntRange(0, (int)(DefaultConfig().Tolerance)+1).Draw(t, "height increment")
    76  	requestHeight := b.Height + (uint64)(incr)
    77  	r.core.HandleHeight(b, requestHeight)
    78  	// Re-queueing by height should always succeed if beyond tolerance
    79  	if (uint)(incr) > DefaultConfig().Tolerance {
    80  		for h := b.Height + 1; h <= requestHeight; h++ {
    81  			r.heightRequests[h] = true
    82  		}
    83  	}
    84  }
    85  
    86  // HandleByID is an action that provides a block header to the sync engine
    87  func (r *rapidSync) HandleByID(t *rapid.T) {
    88  	b := rapid.SampledFrom(r.store).Draw(t, "id_handling")
    89  	success := r.core.HandleBlock(b)
    90  	assert.True(t, success || r.idRequests[b.ID()] == false)
    91  
    92  	// we decrease the pending requests iff we have already requested this block
    93  	// and we have not received it since
    94  	if r.idRequests[b.ID()] == true {
    95  		r.idRequests[b.ID()] = false
    96  	}
    97  	// we eagerly remove height requests for blocks we receive
    98  	r.heightRequests[b.Height] = false
    99  }
   100  
   101  // Check runs after every action and verifies that all required invariants hold.
   102  func (r *rapidSync) Check(t *rapid.T) {
   103  	// we collect the received blocks as determined above
   104  	var receivedBlocks []*flow.Header
   105  	// we also collect the pending blocks
   106  	var activeBlocks []*flow.Header
   107  
   108  	// we check the validity of our pushdown automaton for ID requests and populate activeBlocks / receivedBlocks
   109  	for id, requested := range r.idRequests {
   110  		s, foundID := r.core.blockIDs[id]
   111  
   112  		block, foundBlock := findHeader(r.store, func(h *flow.Header) bool {
   113  			return h.ID() == id
   114  		})
   115  		require.True(t, foundBlock, "incorrect management of idRequests in the tests: all added IDs are supposed to be from the store")
   116  
   117  		if requested {
   118  			require.True(t, foundID, "ID %v is supposed to be known, but isn't", id)
   119  
   120  			assert.True(t, s.WasQueued(), "ID %v was expected to be Queued and is %v", id, s.StatusString())
   121  			assert.False(t, s.WasReceived(), "ID %v was expected to be Queued and is %v", id, s.StatusString())
   122  			activeBlocks = append(activeBlocks, block)
   123  		} else {
   124  			if foundID {
   125  				// if a block is known with 0 pendings, it's because it was received
   126  				assert.True(t, s.WasReceived(), "ID %v was expected to be Received and is %v", id, s.StatusString())
   127  				receivedBlocks = append(receivedBlocks, block)
   128  			}
   129  		}
   130  	}
   131  
   132  	// we collect still-active heights
   133  	var activeHeights []uint64
   134  
   135  	for h, requested := range r.heightRequests {
   136  		s, ok := r.core.heights[h]
   137  		if requested {
   138  			require.True(t, ok, "Height %x is supposed to be known, but isn't", h)
   139  			assert.True(t, s.WasQueued(), "Height %x was expected to be Queued and is %v", h, s.StatusString())
   140  			assert.False(t, s.WasReceived(), "Height %x was expected to be Queued and is %v", h, s.StatusString())
   141  			activeHeights = append(activeHeights, h)
   142  
   143  		} else {
   144  			// if a height is known with 0 pendings, it's because:
   145  			// - it was received
   146  			// - or because a block at this height was (blockAtHeightWasReceived)
   147  			// - or because a request for a block at that height made us "forget" the prior height reception (clobberedByID)
   148  			if ok {
   149  				wasReceived := s.WasReceived()
   150  				_, blockAtHeightWasReceived := findHeader(receivedBlocks, func(header *flow.Header) bool {
   151  					return header.Height == h
   152  				})
   153  				_, clobberedByID := findHeader(activeBlocks, func(header *flow.Header) bool {
   154  					return header.Height == h
   155  				})
   156  				heightWasCanceled := wasReceived || blockAtHeightWasReceived || clobberedByID
   157  
   158  				assert.True(t, heightWasCanceled, "Height %x was expected to be Received (or filled through a same-height block) and is %v", h, s.StatusString())
   159  			}
   160  		}
   161  	}
   162  
   163  	heights, blockIDs := r.core.getRequestableItems()
   164  	// the queueing logic queues intervals, while our r.heightRequests only queues specific requests
   165  	assert.Subset(t, heights, activeHeights, "sync engine's height request tracking lost heights")
   166  
   167  	for _, bID := range blockIDs {
   168  		v, ok := r.idRequests[bID]
   169  		require.True(t, ok)
   170  		assert.Equal(t, true, v, "blockID %v is supposed to be pending but is not", bID)
   171  	}
   172  }
   173  
   174  func TestRapidSync(t *testing.T) {
   175  	unittest.SkipUnless(t, unittest.TEST_FLAKY, "flaky test")
   176  
   177  	rapid.Check(t, func(t *rapid.T) {
   178  		sm := new(rapidSync)
   179  		sm.init(t)
   180  		t.Repeat(rapid.StateMachineActions(sm))
   181  	})
   182  }
   183  
   184  // utility functions
   185  func findHeader(store []*flow.Header, predicate func(*flow.Header) bool) (*flow.Header, bool) {
   186  	for _, b := range store {
   187  		if predicate(b) {
   188  			return b, true
   189  		}
   190  	}
   191  	return nil, false
   192  }