github.com/onflow/flow-go@v0.33.17/engine/access/ingestion/engine_test.go (about)

     1  package ingestion
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"math/rand"
     7  	"os"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/rs/zerolog"
    13  	"github.com/stretchr/testify/mock"
    14  	"github.com/stretchr/testify/require"
    15  	"github.com/stretchr/testify/suite"
    16  
    17  	hotmodel "github.com/onflow/flow-go/consensus/hotstuff/model"
    18  	"github.com/onflow/flow-go/model/flow"
    19  	"github.com/onflow/flow-go/model/flow/filter"
    20  	"github.com/onflow/flow-go/module/component"
    21  	downloadermock "github.com/onflow/flow-go/module/executiondatasync/execution_data/mock"
    22  	"github.com/onflow/flow-go/module/irrecoverable"
    23  	"github.com/onflow/flow-go/module/mempool/stdmap"
    24  	"github.com/onflow/flow-go/module/metrics"
    25  	module "github.com/onflow/flow-go/module/mock"
    26  	"github.com/onflow/flow-go/module/signature"
    27  	"github.com/onflow/flow-go/module/state_synchronization/indexer"
    28  	"github.com/onflow/flow-go/network/channels"
    29  	"github.com/onflow/flow-go/network/mocknetwork"
    30  	protocol "github.com/onflow/flow-go/state/protocol/mock"
    31  	storerr "github.com/onflow/flow-go/storage"
    32  	storage "github.com/onflow/flow-go/storage/mock"
    33  	"github.com/onflow/flow-go/utils/unittest"
    34  )
    35  
    36  type Suite struct {
    37  	suite.Suite
    38  
    39  	// protocol state
    40  	proto struct {
    41  		state    *protocol.FollowerState
    42  		snapshot *protocol.Snapshot
    43  		params   *protocol.Params
    44  	}
    45  
    46  	me             *module.Local
    47  	request        *module.Requester
    48  	provider       *mocknetwork.Engine
    49  	blocks         *storage.Blocks
    50  	headers        *storage.Headers
    51  	collections    *storage.Collections
    52  	transactions   *storage.Transactions
    53  	receipts       *storage.ExecutionReceipts
    54  	results        *storage.ExecutionResults
    55  	seals          *storage.Seals
    56  	downloader     *downloadermock.Downloader
    57  	sealedBlock    *flow.Header
    58  	finalizedBlock *flow.Header
    59  	log            zerolog.Logger
    60  
    61  	collectionExecutedMetric *indexer.CollectionExecutedMetricImpl
    62  
    63  	eng    *Engine
    64  	cancel context.CancelFunc
    65  }
    66  
    67  func TestIngestEngine(t *testing.T) {
    68  	suite.Run(t, new(Suite))
    69  }
    70  
    71  func (s *Suite) TearDownTest() {
    72  	s.cancel()
    73  }
    74  
    75  func (s *Suite) SetupTest() {
    76  	s.log = zerolog.New(os.Stderr)
    77  	ctx, cancel := context.WithCancel(context.Background())
    78  	s.cancel = cancel
    79  
    80  	obsIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleAccess))
    81  
    82  	// mock out protocol state
    83  	s.proto.state = new(protocol.FollowerState)
    84  	s.proto.snapshot = new(protocol.Snapshot)
    85  	s.proto.params = new(protocol.Params)
    86  	s.finalizedBlock = unittest.BlockHeaderFixture(unittest.WithHeaderHeight(0))
    87  	s.proto.state.On("Identity").Return(obsIdentity, nil)
    88  	s.proto.state.On("Final").Return(s.proto.snapshot, nil)
    89  	s.proto.state.On("Params").Return(s.proto.params)
    90  	s.proto.snapshot.On("Head").Return(
    91  		func() *flow.Header {
    92  			return s.finalizedBlock
    93  		},
    94  		nil,
    95  	).Maybe()
    96  
    97  	s.me = new(module.Local)
    98  	s.me.On("NodeID").Return(obsIdentity.NodeID)
    99  
   100  	net := new(mocknetwork.Network)
   101  	conduit := new(mocknetwork.Conduit)
   102  	net.On("Register", channels.ReceiveReceipts, mock.Anything).
   103  		Return(conduit, nil).
   104  		Once()
   105  	s.request = new(module.Requester)
   106  
   107  	s.provider = new(mocknetwork.Engine)
   108  	s.blocks = new(storage.Blocks)
   109  	s.headers = new(storage.Headers)
   110  	s.collections = new(storage.Collections)
   111  	s.transactions = new(storage.Transactions)
   112  	s.receipts = new(storage.ExecutionReceipts)
   113  	s.results = new(storage.ExecutionResults)
   114  	collectionsToMarkFinalized, err := stdmap.NewTimes(100)
   115  	require.NoError(s.T(), err)
   116  	collectionsToMarkExecuted, err := stdmap.NewTimes(100)
   117  	require.NoError(s.T(), err)
   118  	blocksToMarkExecuted, err := stdmap.NewTimes(100)
   119  	require.NoError(s.T(), err)
   120  
   121  	s.collectionExecutedMetric, err = indexer.NewCollectionExecutedMetricImpl(
   122  		s.log,
   123  		metrics.NewNoopCollector(),
   124  		collectionsToMarkFinalized,
   125  		collectionsToMarkExecuted,
   126  		blocksToMarkExecuted,
   127  		s.collections,
   128  		s.blocks,
   129  	)
   130  	require.NoError(s.T(), err)
   131  
   132  	eng, err := New(s.log, net, s.proto.state, s.me, s.request, s.blocks, s.headers, s.collections,
   133  		s.transactions, s.results, s.receipts, s.collectionExecutedMetric)
   134  	require.NoError(s.T(), err)
   135  
   136  	s.blocks.On("GetLastFullBlockHeight").Once().Return(uint64(0), errors.New("do nothing"))
   137  
   138  	irrecoverableCtx, _ := irrecoverable.WithSignaler(ctx)
   139  	eng.ComponentManager.Start(irrecoverableCtx)
   140  	<-eng.Ready()
   141  
   142  	s.eng = eng
   143  }
   144  
   145  // TestOnFinalizedBlock checks that when a block is received, a request for each individual collection is made
   146  func (s *Suite) TestOnFinalizedBlock() {
   147  	s.blocks.On("GetLastFullBlockHeight").Return(uint64(0), nil).Once()
   148  
   149  	block := unittest.BlockFixture()
   150  	block.SetPayload(unittest.PayloadFixture(
   151  		unittest.WithGuarantees(unittest.CollectionGuaranteesFixture(4)...),
   152  		unittest.WithExecutionResults(unittest.ExecutionResultFixture()),
   153  	))
   154  
   155  	// prepare cluster committee members
   156  	clusterCommittee := unittest.IdentityListFixture(32 * 4).Filter(filter.HasRole(flow.RoleCollection))
   157  	refBlockID := unittest.IdentifierFixture()
   158  	for _, guarantee := range block.Payload.Guarantees {
   159  		guarantee.ReferenceBlockID = refBlockID
   160  		// guarantee signers must be cluster committee members, so that access will fetch collection from
   161  		// the signers that are specified by guarantee.SignerIndices
   162  		indices, err := signature.EncodeSignersToIndices(clusterCommittee.NodeIDs(), clusterCommittee.NodeIDs())
   163  		require.NoError(s.T(), err)
   164  		guarantee.SignerIndices = indices
   165  	}
   166  
   167  	hotstuffBlock := hotmodel.Block{
   168  		BlockID: block.ID(),
   169  	}
   170  
   171  	// we should query the block once and index the guarantee payload once
   172  	s.blocks.On("ByID", block.ID()).Return(&block, nil).Twice()
   173  	for _, g := range block.Payload.Guarantees {
   174  		collection := unittest.CollectionFixture(1)
   175  		light := collection.Light()
   176  		s.collections.On("LightByID", g.CollectionID).Return(&light, nil).Twice()
   177  	}
   178  
   179  	// expect that the block storage is indexed with each of the collection guarantee
   180  	s.blocks.On("IndexBlockForCollections", block.ID(), []flow.Identifier(flow.GetIDs(block.Payload.Guarantees))).Return(nil).Once()
   181  
   182  	cluster := new(protocol.Cluster)
   183  	cluster.On("Members").Return(clusterCommittee, nil)
   184  	epoch := new(protocol.Epoch)
   185  	epoch.On("ClusterByChainID", mock.Anything).Return(cluster, nil)
   186  	epochs := new(protocol.EpochQuery)
   187  	epochs.On("Current").Return(epoch)
   188  	snap := new(protocol.Snapshot)
   189  	snap.On("Epochs").Return(epochs)
   190  	s.proto.state.On("AtBlockID", refBlockID).Return(snap)
   191  	s.results.On("Index", mock.Anything, mock.Anything).Return(nil)
   192  
   193  	// for each of the guarantees, we should request the corresponding collection once
   194  	needed := make(map[flow.Identifier]struct{})
   195  	for _, guarantee := range block.Payload.Guarantees {
   196  		needed[guarantee.ID()] = struct{}{}
   197  	}
   198  
   199  	wg := sync.WaitGroup{}
   200  	wg.Add(4)
   201  
   202  	s.request.On("EntityByID", mock.Anything, mock.Anything).Run(
   203  		func(args mock.Arguments) {
   204  			collID := args.Get(0).(flow.Identifier)
   205  			_, pending := needed[collID]
   206  			s.Assert().True(pending, "collection should be pending (%x)", collID)
   207  			delete(needed, collID)
   208  			wg.Done()
   209  		},
   210  	)
   211  
   212  	// process the block through the finalized callback
   213  	s.eng.OnFinalizedBlock(&hotstuffBlock)
   214  	s.Assertions.Eventually(func() bool {
   215  		wg.Wait()
   216  		return true
   217  	}, time.Millisecond*20, time.Millisecond)
   218  
   219  	// assert that the block was retrieved and all collections were requested
   220  	s.headers.AssertExpectations(s.T())
   221  	s.request.AssertNumberOfCalls(s.T(), "EntityByID", len(block.Payload.Guarantees))
   222  	s.request.AssertNumberOfCalls(s.T(), "Index", len(block.Payload.Seals))
   223  }
   224  
   225  // TestOnCollection checks that when a Collection is received, it is persisted
   226  func (s *Suite) TestOnCollection() {
   227  	collection := unittest.CollectionFixture(5)
   228  	light := collection.Light()
   229  
   230  	// we should store the light collection and index its transactions
   231  	s.collections.On("StoreLightAndIndexByTransaction", &light).Return(nil).Once()
   232  
   233  	// for each transaction in the collection, we should store it
   234  	needed := make(map[flow.Identifier]struct{})
   235  	for _, txID := range light.Transactions {
   236  		needed[txID] = struct{}{}
   237  	}
   238  	s.transactions.On("Store", mock.Anything).Return(nil).Run(
   239  		func(args mock.Arguments) {
   240  			tx := args.Get(0).(*flow.TransactionBody)
   241  			_, pending := needed[tx.ID()]
   242  			s.Assert().True(pending, "tx not pending (%x)", tx.ID())
   243  		},
   244  	)
   245  
   246  	err := indexer.HandleCollection(&collection, s.collections, s.transactions, s.log, s.collectionExecutedMetric)
   247  	require.NoError(s.T(), err)
   248  
   249  	// check that the collection was stored and indexed, and we stored all transactions
   250  	s.collections.AssertExpectations(s.T())
   251  	s.transactions.AssertNumberOfCalls(s.T(), "Store", len(collection.Transactions))
   252  }
   253  
   254  // TestExecutionReceiptsAreIndexed checks that execution receipts are properly indexed
   255  func (s *Suite) TestExecutionReceiptsAreIndexed() {
   256  
   257  	originID := unittest.IdentifierFixture()
   258  	collection := unittest.CollectionFixture(5)
   259  	light := collection.Light()
   260  
   261  	// we should store the light collection and index its transactions
   262  	s.collections.On("StoreLightAndIndexByTransaction", &light).Return(nil).Once()
   263  	block := &flow.Block{
   264  		Header:  &flow.Header{Height: 0},
   265  		Payload: &flow.Payload{Guarantees: []*flow.CollectionGuarantee{}},
   266  	}
   267  	s.blocks.On("ByID", mock.Anything).Return(block, nil)
   268  
   269  	// for each transaction in the collection, we should store it
   270  	needed := make(map[flow.Identifier]struct{})
   271  	for _, txID := range light.Transactions {
   272  		needed[txID] = struct{}{}
   273  	}
   274  	s.transactions.On("Store", mock.Anything).Return(nil).Run(
   275  		func(args mock.Arguments) {
   276  			tx := args.Get(0).(*flow.TransactionBody)
   277  			_, pending := needed[tx.ID()]
   278  			s.Assert().True(pending, "tx not pending (%x)", tx.ID())
   279  		},
   280  	)
   281  	er1 := unittest.ExecutionReceiptFixture()
   282  	er2 := unittest.ExecutionReceiptFixture()
   283  
   284  	s.receipts.On("Store", mock.Anything).Return(nil)
   285  	s.blocks.On("ByID", er1.ExecutionResult.BlockID).Return(nil, storerr.ErrNotFound)
   286  
   287  	s.receipts.On("Store", mock.Anything).Return(nil)
   288  	s.blocks.On("ByID", er2.ExecutionResult.BlockID).Return(nil, storerr.ErrNotFound)
   289  
   290  	err := s.eng.handleExecutionReceipt(originID, er1)
   291  	require.NoError(s.T(), err)
   292  
   293  	err = s.eng.handleExecutionReceipt(originID, er2)
   294  	require.NoError(s.T(), err)
   295  
   296  	s.receipts.AssertExpectations(s.T())
   297  	s.results.AssertExpectations(s.T())
   298  	s.receipts.AssertExpectations(s.T())
   299  }
   300  
   301  // TestOnCollectionDuplicate checks that when a duplicate collection is received, the node doesn't
   302  // crash but just ignores its transactions.
   303  func (s *Suite) TestOnCollectionDuplicate() {
   304  	collection := unittest.CollectionFixture(5)
   305  	light := collection.Light()
   306  
   307  	// we should store the light collection and index its transactions
   308  	s.collections.On("StoreLightAndIndexByTransaction", &light).Return(storerr.ErrAlreadyExists).Once()
   309  
   310  	// for each transaction in the collection, we should store it
   311  	needed := make(map[flow.Identifier]struct{})
   312  	for _, txID := range light.Transactions {
   313  		needed[txID] = struct{}{}
   314  	}
   315  	s.transactions.On("Store", mock.Anything).Return(nil).Run(
   316  		func(args mock.Arguments) {
   317  			tx := args.Get(0).(*flow.TransactionBody)
   318  			_, pending := needed[tx.ID()]
   319  			s.Assert().True(pending, "tx not pending (%x)", tx.ID())
   320  		},
   321  	)
   322  
   323  	err := indexer.HandleCollection(&collection, s.collections, s.transactions, s.log, s.collectionExecutedMetric)
   324  	require.NoError(s.T(), err)
   325  
   326  	// check that the collection was stored and indexed, and we stored all transactions
   327  	s.collections.AssertExpectations(s.T())
   328  	s.transactions.AssertNotCalled(s.T(), "Store", "should not store any transactions")
   329  }
   330  
   331  // TestRequestMissingCollections tests that the all missing collections are requested on the call to requestMissingCollections
   332  func (s *Suite) TestRequestMissingCollections() {
   333  
   334  	blkCnt := 3
   335  	startHeight := uint64(1000)
   336  	blocks := make([]flow.Block, blkCnt)
   337  	heightMap := make(map[uint64]*flow.Block, blkCnt)
   338  
   339  	// prepare cluster committee members
   340  	clusterCommittee := unittest.IdentityListFixture(32 * 4).Filter(filter.HasRole(flow.RoleCollection))
   341  
   342  	// generate the test blocks and collections
   343  	var collIDs []flow.Identifier
   344  	refBlockID := unittest.IdentifierFixture()
   345  	for i := 0; i < blkCnt; i++ {
   346  		block := unittest.BlockFixture()
   347  		block.SetPayload(unittest.PayloadFixture(
   348  			unittest.WithGuarantees(
   349  				unittest.CollectionGuaranteesFixture(4, unittest.WithCollRef(refBlockID))...),
   350  		))
   351  		// some blocks may not be present hence add a gap
   352  		height := startHeight + uint64(i)
   353  		block.Header.Height = height
   354  		blocks[i] = block
   355  		heightMap[height] = &block
   356  		for _, c := range block.Payload.Guarantees {
   357  			collIDs = append(collIDs, c.CollectionID)
   358  			c.ReferenceBlockID = refBlockID
   359  
   360  			// guarantee signers must be cluster committee members, so that access will fetch collection from
   361  			// the signers that are specified by guarantee.SignerIndices
   362  			indices, err := signature.EncodeSignersToIndices(clusterCommittee.NodeIDs(), clusterCommittee.NodeIDs())
   363  			require.NoError(s.T(), err)
   364  			c.SignerIndices = indices
   365  		}
   366  	}
   367  
   368  	// setup the block storage mock
   369  	// each block should be queried by height
   370  	s.blocks.On("ByHeight", mock.IsType(uint64(0))).Return(
   371  		func(h uint64) *flow.Block {
   372  			// simulate a db lookup
   373  			return heightMap[h]
   374  		},
   375  		func(h uint64) error {
   376  			if _, ok := heightMap[h]; ok {
   377  				return nil
   378  			}
   379  			return storerr.ErrNotFound
   380  		})
   381  	// consider collections are missing for all blocks
   382  	s.blocks.On("GetLastFullBlockHeight").Return(startHeight-1, nil)
   383  	// consider the last test block as the head
   384  	s.finalizedBlock = blocks[blkCnt-1].Header
   385  
   386  	// p is the probability of not receiving the collection before the next poll and it
   387  	// helps simulate the slow trickle of the requested collections being received
   388  	var p float32
   389  
   390  	// rcvdColl is the map simulating the collection storage key-values
   391  	rcvdColl := make(map[flow.Identifier]struct{})
   392  
   393  	// for the first lookup call for each collection, it will be reported as missing from db
   394  	// for the subsequent calls, it will be reported as present with the probability p
   395  	s.collections.On("LightByID", mock.Anything).Return(
   396  		func(cID flow.Identifier) *flow.LightCollection {
   397  			return nil // the actual collection object return is never really read
   398  		},
   399  		func(cID flow.Identifier) error {
   400  			if _, ok := rcvdColl[cID]; ok {
   401  				return nil
   402  			}
   403  			if rand.Float32() >= p {
   404  				rcvdColl[cID] = struct{}{}
   405  			}
   406  			return storerr.ErrNotFound
   407  		}).
   408  		// simulate some db i/o contention
   409  		After(time.Millisecond * time.Duration(rand.Intn(5)))
   410  
   411  	// setup the requester engine mock
   412  	// entityByID should be called once per collection
   413  	for _, c := range collIDs {
   414  		s.request.On("EntityByID", c, mock.Anything).Return()
   415  	}
   416  	// force should be called once
   417  	s.request.On("Force").Return()
   418  
   419  	cluster := new(protocol.Cluster)
   420  	cluster.On("Members").Return(clusterCommittee, nil)
   421  	epoch := new(protocol.Epoch)
   422  	epoch.On("ClusterByChainID", mock.Anything).Return(cluster, nil)
   423  	epochs := new(protocol.EpochQuery)
   424  	epochs.On("Current").Return(epoch)
   425  	snap := new(protocol.Snapshot)
   426  	snap.On("Epochs").Return(epochs)
   427  	s.proto.state.On("AtBlockID", refBlockID).Return(snap)
   428  
   429  	assertExpectations := func() {
   430  		s.request.AssertExpectations(s.T())
   431  		s.collections.AssertExpectations(s.T())
   432  		s.proto.snapshot.AssertExpectations(s.T())
   433  		s.blocks.AssertExpectations(s.T())
   434  	}
   435  
   436  	// test 1 - collections are not received before timeout
   437  	s.Run("timeout before all missing collections are received", func() {
   438  
   439  		// simulate that collection are never received
   440  		p = 1
   441  
   442  		// timeout after 3 db polls
   443  		ctx, cancel := context.WithTimeout(context.Background(), 100*defaultCollectionCatchupDBPollInterval)
   444  		defer cancel()
   445  
   446  		err := s.eng.requestMissingCollections(ctx)
   447  
   448  		require.Error(s.T(), err)
   449  		require.Contains(s.T(), err.Error(), "context deadline exceeded")
   450  
   451  		assertExpectations()
   452  	})
   453  	// test 2 - all collections are eventually received before the deadline
   454  	s.Run("all missing collections are received", func() {
   455  
   456  		// 90% of the time, collections are reported as not received when the collection storage is queried
   457  		p = 0.9
   458  
   459  		ctx, cancel := context.WithTimeout(context.Background(), defaultCollectionCatchupTimeout)
   460  		defer cancel()
   461  
   462  		err := s.eng.requestMissingCollections(ctx)
   463  
   464  		require.NoError(s.T(), err)
   465  		require.Len(s.T(), rcvdColl, len(collIDs))
   466  
   467  		assertExpectations()
   468  	})
   469  }
   470  
   471  // TestProcessBackgroundCalls tests that updateLastFullBlockReceivedIndex and checkMissingCollections
   472  // function calls keep the FullBlockIndex up-to-date and request collections if blocks with missing
   473  // collections exceed the threshold.
   474  func (s *Suite) TestProcessBackgroundCalls() {
   475  	blkCnt := 3
   476  	collPerBlk := 10
   477  	startHeight := uint64(1000)
   478  	blocks := make([]flow.Block, blkCnt)
   479  	heightMap := make(map[uint64]*flow.Block, blkCnt)
   480  	collMap := make(map[flow.Identifier]*flow.LightCollection, blkCnt*collPerBlk)
   481  
   482  	// prepare cluster committee members
   483  	clusterCommittee := unittest.IdentityListFixture(32 * 4).Filter(filter.HasRole(flow.RoleCollection))
   484  
   485  	refBlockID := unittest.IdentifierFixture()
   486  	// generate the test blocks, cgs and collections
   487  	for i := 0; i < blkCnt; i++ {
   488  		guarantees := make([]*flow.CollectionGuarantee, collPerBlk)
   489  		for j := 0; j < collPerBlk; j++ {
   490  			coll := unittest.CollectionFixture(2).Light()
   491  			collMap[coll.ID()] = &coll
   492  			cg := unittest.CollectionGuaranteeFixture(func(cg *flow.CollectionGuarantee) {
   493  				cg.CollectionID = coll.ID()
   494  				cg.ReferenceBlockID = refBlockID
   495  			})
   496  
   497  			// guarantee signers must be cluster committee members, so that access will fetch collection from
   498  			// the signers that are specified by guarantee.SignerIndices
   499  			indices, err := signature.EncodeSignersToIndices(clusterCommittee.NodeIDs(), clusterCommittee.NodeIDs())
   500  			require.NoError(s.T(), err)
   501  			cg.SignerIndices = indices
   502  			guarantees[j] = cg
   503  		}
   504  		block := unittest.BlockFixture()
   505  		block.SetPayload(unittest.PayloadFixture(unittest.WithGuarantees(guarantees...)))
   506  		// set the height
   507  		height := startHeight + uint64(i)
   508  		block.Header.Height = height
   509  		blocks[i] = block
   510  		heightMap[height] = &block
   511  	}
   512  
   513  	rootBlk := blocks[0]
   514  	rootBlkHeight := rootBlk.Header.Height
   515  	finalizedBlk := blocks[blkCnt-1]
   516  	finalizedHeight := finalizedBlk.Header.Height
   517  
   518  	// setup the block storage mock
   519  	// each block should be queried by height
   520  	s.blocks.On("ByHeight", mock.IsType(uint64(0))).Return(
   521  		func(h uint64) *flow.Block {
   522  			// simulate a db lookup
   523  			return heightMap[h]
   524  		},
   525  		func(h uint64) error {
   526  			if _, ok := heightMap[h]; ok {
   527  				return nil
   528  			}
   529  			return storerr.ErrNotFound
   530  		})
   531  
   532  	cluster := new(protocol.Cluster)
   533  	cluster.On("Members").Return(clusterCommittee, nil)
   534  	epoch := new(protocol.Epoch)
   535  	epoch.On("ClusterByChainID", mock.Anything).Return(cluster, nil)
   536  	epochs := new(protocol.EpochQuery)
   537  	epochs.On("Current").Return(epoch)
   538  	snap := new(protocol.Snapshot)
   539  	snap.On("Epochs").Return(epochs)
   540  	s.proto.state.On("AtBlockID", refBlockID).Return(snap)
   541  
   542  	// blkMissingColl controls which collections are reported as missing by the collections storage mock
   543  	blkMissingColl := make([]bool, blkCnt)
   544  	for i := 0; i < blkCnt; i++ {
   545  		blkMissingColl[i] = false
   546  		for _, cg := range blocks[i].Payload.Guarantees {
   547  			j := i
   548  			s.collections.On("LightByID", cg.CollectionID).Return(
   549  				func(cID flow.Identifier) *flow.LightCollection {
   550  					return collMap[cID]
   551  				},
   552  				func(cID flow.Identifier) error {
   553  					if blkMissingColl[j] {
   554  						return storerr.ErrNotFound
   555  					}
   556  					return nil
   557  				})
   558  		}
   559  	}
   560  
   561  	// consider the last test block as the head
   562  	s.finalizedBlock = finalizedBlk.Header
   563  
   564  	s.Run("full block height index is advanced if newer full blocks are discovered", func() {
   565  		block := blocks[1]
   566  		s.blocks.On("UpdateLastFullBlockHeight", finalizedHeight).Return(nil).Once()
   567  		s.blocks.On("GetLastFullBlockHeight").Return(func() (uint64, error) {
   568  			return block.Header.Height, nil
   569  		}).Once()
   570  
   571  		err := s.eng.updateLastFullBlockReceivedIndex()
   572  		s.Require().NoError(err)
   573  
   574  		s.blocks.AssertExpectations(s.T())
   575  	})
   576  
   577  	s.Run("full block height index is not advanced beyond finalized blocks", func() {
   578  		s.blocks.On("GetLastFullBlockHeight").Return(func() (uint64, error) {
   579  			return finalizedHeight, nil
   580  		}).Once()
   581  
   582  		err := s.eng.updateLastFullBlockReceivedIndex()
   583  		s.Require().NoError(err)
   584  
   585  		s.blocks.AssertExpectations(s.T()) // not new call to UpdateLastFullBlockHeight should be made
   586  	})
   587  
   588  	s.Run("missing collections are requested when count exceeds defaultMissingCollsForBlkThreshold", func() {
   589  		// root block is the last complete block
   590  		s.blocks.On("GetLastFullBlockHeight").Return(func() (uint64, error) {
   591  			return rootBlkHeight, nil
   592  		}).Once()
   593  
   594  		// lower the block threshold to request missing collections
   595  		defaultMissingCollsForBlkThreshold = 2
   596  
   597  		// mark all blocks beyond the root block as incomplete
   598  		for i := 1; i < blkCnt; i++ {
   599  			blkMissingColl[i] = true
   600  			// setup receive engine expectations
   601  			for _, cg := range blocks[i].Payload.Guarantees {
   602  				s.request.On("EntityByID", cg.CollectionID, mock.Anything).Return().Once()
   603  			}
   604  		}
   605  
   606  		err := s.eng.checkMissingCollections()
   607  		s.Require().NoError(err)
   608  
   609  		// assert that missing collections are requested
   610  		s.request.AssertExpectations(s.T())
   611  
   612  		// last full blk index is not advanced
   613  		s.blocks.AssertExpectations(s.T()) // no new call to UpdateLastFullBlockHeight should be made
   614  	})
   615  
   616  	s.Run("missing collections are requested when count exceeds defaultMissingCollsForAgeThreshold", func() {
   617  		// root block is the last complete block
   618  		s.blocks.On("GetLastFullBlockHeight").Return(func() (uint64, error) {
   619  			return rootBlkHeight, nil
   620  		}).Once()
   621  
   622  		// lower the height threshold to request missing collections
   623  		defaultMissingCollsForAgeThreshold = 1
   624  
   625  		// raise the block threshold to ensure it does not trigger missing collection request
   626  		defaultMissingCollsForBlkThreshold = blkCnt + 1
   627  
   628  		// mark all blocks beyond the root block as incomplete
   629  		for i := 1; i < blkCnt; i++ {
   630  			blkMissingColl[i] = true
   631  			// setup receive engine expectations
   632  			for _, cg := range blocks[i].Payload.Guarantees {
   633  				s.request.On("EntityByID", cg.CollectionID, mock.Anything).Return().Once()
   634  			}
   635  		}
   636  
   637  		err := s.eng.checkMissingCollections()
   638  		s.Require().NoError(err)
   639  
   640  		// assert that missing collections are requested
   641  		s.request.AssertExpectations(s.T())
   642  
   643  		// last full blk index is not advanced
   644  		s.blocks.AssertExpectations(s.T()) // not new call to UpdateLastFullBlockHeight should be made
   645  	})
   646  
   647  	s.Run("missing collections are not requested if defaultMissingCollsForBlkThreshold not reached", func() {
   648  		// root block is the last complete block
   649  		s.blocks.On("GetLastFullBlockHeight").Return(func() (uint64, error) {
   650  			return rootBlkHeight, nil
   651  		}).Once()
   652  
   653  		// raise the thresholds to avoid requesting missing collections
   654  		defaultMissingCollsForAgeThreshold = 3
   655  		defaultMissingCollsForBlkThreshold = 3
   656  
   657  		// mark all blocks beyond the root block as incomplete
   658  		for i := 1; i < blkCnt; i++ {
   659  			blkMissingColl[i] = true
   660  		}
   661  
   662  		err := s.eng.checkMissingCollections()
   663  		s.Require().NoError(err)
   664  
   665  		// assert that missing collections are not requested even though there are collections missing
   666  		s.request.AssertExpectations(s.T())
   667  
   668  		// last full blk index is not advanced
   669  		s.blocks.AssertExpectations(s.T()) // not new call to UpdateLastFullBlockHeight should be made
   670  	})
   671  }
   672  
   673  func (s *Suite) TestComponentShutdown() {
   674  	// start then shut down the engine
   675  	unittest.AssertClosesBefore(s.T(), s.eng.Ready(), 10*time.Millisecond)
   676  	s.cancel()
   677  	unittest.AssertClosesBefore(s.T(), s.eng.Done(), 10*time.Millisecond)
   678  
   679  	err := s.eng.ProcessLocal(&flow.ExecutionReceipt{})
   680  	s.Assert().ErrorIs(err, component.ErrComponentShutdown)
   681  }