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 }