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 }