github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/ingestion/engine_test.go (about)

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