github.com/onflow/flow-go@v0.33.17/engine/execution/ingestion/engine_test.go (about) 1 package ingestion 2 3 import ( 4 "context" 5 "crypto/rand" 6 "fmt" 7 "sync" 8 "testing" 9 "time" 10 11 "github.com/rs/zerolog" 12 "github.com/stretchr/testify/mock" 13 "github.com/stretchr/testify/require" 14 15 "github.com/onflow/flow-go/crypto" 16 "github.com/onflow/flow-go/fvm/storage/snapshot" 17 18 enginePkg "github.com/onflow/flow-go/engine" 19 "github.com/onflow/flow-go/engine/execution" 20 computation "github.com/onflow/flow-go/engine/execution/computation/mock" 21 "github.com/onflow/flow-go/engine/execution/ingestion/loader" 22 "github.com/onflow/flow-go/engine/execution/ingestion/mocks" 23 "github.com/onflow/flow-go/engine/execution/ingestion/stop" 24 "github.com/onflow/flow-go/engine/execution/ingestion/uploader" 25 uploadermock "github.com/onflow/flow-go/engine/execution/ingestion/uploader/mock" 26 provider "github.com/onflow/flow-go/engine/execution/provider/mock" 27 stateMock "github.com/onflow/flow-go/engine/execution/state/mock" 28 "github.com/onflow/flow-go/engine/testutil/mocklocal" 29 "github.com/onflow/flow-go/model/flow" 30 "github.com/onflow/flow-go/module/mempool/entity" 31 "github.com/onflow/flow-go/module/metrics" 32 "github.com/onflow/flow-go/module/trace" 33 "github.com/onflow/flow-go/network/mocknetwork" 34 protocol "github.com/onflow/flow-go/state/protocol/mock" 35 storageerr "github.com/onflow/flow-go/storage" 36 storage "github.com/onflow/flow-go/storage/mock" 37 "github.com/onflow/flow-go/utils/unittest" 38 ) 39 40 type testingContext struct { 41 t *testing.T 42 engine *Engine 43 headers *storage.Headers 44 blocks *storage.Blocks 45 collections *mocks.MockCollectionStore 46 state *protocol.State 47 computationManager *computation.ComputationManager 48 providerEngine *provider.ProviderEngine 49 executionState *stateMock.ExecutionState 50 stopControl *stop.StopControl 51 uploadMgr *uploader.Manager 52 fetcher *mocks.MockFetcher 53 54 mu *sync.Mutex 55 } 56 57 func runWithEngine(t *testing.T, f func(testingContext)) { 58 59 net := new(mocknetwork.EngineRegistry) 60 61 // generates signing identity including staking key for signing 62 seed := make([]byte, crypto.KeyGenSeedMinLen) 63 n, err := rand.Read(seed) 64 require.Equal(t, n, crypto.KeyGenSeedMinLen) 65 require.NoError(t, err) 66 sk, err := crypto.GeneratePrivateKey(crypto.BLSBLS12381, seed) 67 require.NoError(t, err) 68 myIdentity := unittest.IdentityFixture() 69 myIdentity.Role = flow.RoleExecution 70 myIdentity.StakingPubKey = sk.PublicKey() 71 72 me := mocklocal.NewMockLocal(sk, myIdentity.ID(), t) 73 74 headers := storage.NewHeaders(t) 75 blocks := storage.NewBlocks(t) 76 collections := mocks.NewMockCollectionStore() 77 78 computationManager := computation.NewComputationManager(t) 79 providerEngine := provider.NewProviderEngine(t) 80 protocolState := protocol.NewState(t) 81 executionState := stateMock.NewExecutionState(t) 82 83 var engine *Engine 84 85 defer func() { 86 <-engine.Done() 87 computationManager.AssertExpectations(t) 88 protocolState.AssertExpectations(t) 89 executionState.AssertExpectations(t) 90 providerEngine.AssertExpectations(t) 91 }() 92 93 log := unittest.Logger() 94 metrics := metrics.NewNoopCollector() 95 96 tracer, err := trace.NewTracer(log, "test", "test", trace.SensitivityCaptureAll) 97 require.NoError(t, err) 98 99 unit := enginePkg.NewUnit() 100 stopControl := stop.NewStopControl( 101 unit, 102 time.Second, 103 zerolog.Nop(), 104 executionState, 105 headers, 106 nil, 107 nil, 108 &flow.Header{Height: 1}, 109 false, 110 false, 111 ) 112 113 uploadMgr := uploader.NewManager(trace.NewNoopTracer()) 114 115 fetcher := mocks.NewMockFetcher() 116 loader := loader.NewUnexecutedLoader(log, protocolState, headers, executionState) 117 118 engine, err = New( 119 unit, 120 log, 121 net, 122 me, 123 fetcher, 124 headers, 125 blocks, 126 collections, 127 computationManager, 128 providerEngine, 129 executionState, 130 metrics, 131 tracer, 132 false, 133 nil, 134 uploadMgr, 135 stopControl, 136 loader, 137 ) 138 require.NoError(t, err) 139 140 f(testingContext{ 141 t: t, 142 engine: engine, 143 headers: headers, 144 blocks: blocks, 145 collections: collections, 146 state: protocolState, 147 computationManager: computationManager, 148 providerEngine: providerEngine, 149 executionState: executionState, 150 uploadMgr: uploadMgr, 151 stopControl: stopControl, 152 fetcher: fetcher, 153 154 mu: &sync.Mutex{}, 155 }) 156 157 <-engine.Done() 158 } 159 160 // TestExecuteOneBlock verifies after collection is received, 161 // block is executed, uploaded, and broadcasted 162 func TestExecuteOneBlock(t *testing.T) { 163 runWithEngine(t, func(ctx testingContext) { 164 // create a mocked storage that has similar behavior as the real execution state. 165 // the mocked storage allows us to prepare results for the prepared blocks, so that 166 // the mocked methods know what to return, and it also allows us to verify that the 167 // mocked API is called with correct data. 168 store := mocks.NewMockBlockStore(t) 169 170 col := unittest.CollectionFixture(1) 171 // Root <- A 172 blockA := makeBlockWithCollection(store.RootBlock, &col) 173 result := store.CreateBlockAndMockResult(t, blockA) 174 175 ctx.mockIsBlockExecuted(store) 176 ctx.mockStateCommitmentByBlockID(store) 177 ctx.mockGetExecutionResultID(store) 178 ctx.mockNewStorageSnapshot(result) 179 180 // receive block 181 err := ctx.engine.handleBlock(context.Background(), blockA.Block) 182 require.NoError(t, err) 183 184 wg := sync.WaitGroup{} 185 wg.Add(1) // wait for block A to be executed 186 187 ctx.mockComputeBlock(store) 188 ctx.mockSaveExecutionResults(store, &wg) 189 190 // verify upload will be called 191 uploader := uploadermock.NewUploader(ctx.t) 192 uploader.On("Upload", result).Return(nil).Once() 193 ctx.uploadMgr.AddUploader(uploader) 194 195 // verify broadcast will be called 196 ctx.providerEngine.On("BroadcastExecutionReceipt", 197 mock.Anything, 198 blockA.Block.Header.Height, 199 result.ExecutionReceipt).Return(true, nil).Once() 200 201 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col) 202 require.NoError(t, err) 203 204 unittest.AssertReturnsBefore(t, wg.Wait, 10*time.Second) 205 206 // verify collection is fetched 207 require.True(t, ctx.fetcher.IsFetched(col.ID())) 208 209 // verify block is executed 210 store.AssertExecuted(t, "A", blockA.ID()) 211 }) 212 } 213 214 // verify block will be executed if collection is received first 215 func TestExecuteBlocks(t *testing.T) { 216 217 runWithEngine(t, func(ctx testingContext) { 218 store := mocks.NewMockBlockStore(t) 219 220 col1 := unittest.CollectionFixture(1) 221 col2 := unittest.CollectionFixture(1) 222 // Root <- A[C1] <- B[C2] 223 // prepare two blocks, so that receiving C2 before C1 won't trigger any block to be executed, 224 // which creates the case where C2 collection is received first, and block B will become 225 // executable as soon as its parent block A is executed. 226 blockA := makeBlockWithCollection(store.RootBlock, &col1) 227 blockB := makeBlockWithCollection(blockA.Block.Header, &col2) 228 resultA := store.CreateBlockAndMockResult(t, blockA) 229 resultB := store.CreateBlockAndMockResult(t, blockB) 230 231 ctx.mockIsBlockExecuted(store) 232 ctx.mockStateCommitmentByBlockID(store) 233 ctx.mockGetExecutionResultID(store) 234 ctx.mockNewStorageSnapshot(resultA) 235 ctx.mockNewStorageSnapshot(resultB) 236 ctx.providerEngine.On("BroadcastExecutionReceipt", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) 237 238 // receive block 239 err := ctx.engine.handleBlock(context.Background(), blockA.Block) 240 require.NoError(t, err) 241 242 err = ctx.engine.handleBlock(context.Background(), blockB.Block) 243 require.NoError(t, err) 244 245 ctx.mockComputeBlock(store) 246 wg := sync.WaitGroup{} 247 wg.Add(2) // wait for 2 blocks to be executed 248 ctx.mockSaveExecutionResults(store, &wg) 249 250 require.NoError(t, ctx.engine.handleCollection(unittest.IdentifierFixture(), &col2)) 251 require.NoError(t, ctx.engine.handleCollection(unittest.IdentifierFixture(), &col1)) 252 253 unittest.AssertReturnsBefore(t, wg.Wait, 10*time.Second) 254 255 // verify collection is fetched 256 require.True(t, ctx.fetcher.IsFetched(col1.ID())) 257 require.True(t, ctx.fetcher.IsFetched(col2.ID())) 258 259 // verify block is executed 260 store.AssertExecuted(t, "A", blockA.ID()) 261 store.AssertExecuted(t, "B", blockB.ID()) 262 }) 263 } 264 265 // verify block will be executed if collection is already in storage 266 func TestExecuteNextBlockIfCollectionIsReady(t *testing.T) { 267 runWithEngine(t, func(ctx testingContext) { 268 store := mocks.NewMockBlockStore(t) 269 270 col1 := unittest.CollectionFixture(1) 271 col2 := unittest.CollectionFixture(1) 272 273 // Root <- A[C1] <- B[C2] 274 blockA := makeBlockWithCollection(store.RootBlock, &col1) 275 blockB := makeBlockWithCollection(blockA.Block.Header, &col2) 276 resultA := store.CreateBlockAndMockResult(t, blockA) 277 resultB := store.CreateBlockAndMockResult(t, blockB) 278 279 // C2 is available in storage 280 require.NoError(t, ctx.collections.Store(&col2)) 281 282 ctx.mockIsBlockExecuted(store) 283 ctx.mockStateCommitmentByBlockID(store) 284 ctx.mockGetExecutionResultID(store) 285 ctx.mockNewStorageSnapshot(resultA) 286 ctx.mockNewStorageSnapshot(resultB) 287 288 // receiving block A and B will not trigger any execution 289 // because A is missing collection C1, B is waiting for A to be executed 290 err := ctx.engine.handleBlock(context.Background(), blockA.Block) 291 require.NoError(t, err) 292 293 err = ctx.engine.handleBlock(context.Background(), blockB.Block) 294 require.NoError(t, err) 295 296 ctx.providerEngine.On("BroadcastExecutionReceipt", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) 297 ctx.mockComputeBlock(store) 298 wg := sync.WaitGroup{} 299 wg.Add(2) // waiting for A and B to be executed 300 ctx.mockSaveExecutionResults(store, &wg) 301 302 // receiving collection C1 will execute both A and B 303 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col1) 304 require.NoError(t, err) 305 306 unittest.AssertReturnsBefore(t, wg.Wait, 10*time.Second) 307 308 // verify collection is fetched 309 require.True(t, ctx.fetcher.IsFetched(col1.ID())) 310 require.False(t, ctx.fetcher.IsFetched(col2.ID())) 311 312 // verify block is executed 313 store.AssertExecuted(t, "A", blockA.ID()) 314 store.AssertExecuted(t, "B", blockB.ID()) 315 }) 316 } 317 318 // verify block will only be executed once even if block or collection are received multiple times 319 func TestExecuteBlockOnlyOnce(t *testing.T) { 320 runWithEngine(t, func(ctx testingContext) { 321 store := mocks.NewMockBlockStore(t) 322 323 col := unittest.CollectionFixture(1) 324 // Root <- A[C] 325 blockA := makeBlockWithCollection(store.RootBlock, &col) 326 resultA := store.CreateBlockAndMockResult(t, blockA) 327 328 ctx.mockIsBlockExecuted(store) 329 ctx.mockStateCommitmentByBlockID(store) 330 ctx.mockGetExecutionResultID(store) 331 ctx.mockNewStorageSnapshot(resultA) 332 333 // receive block 334 err := ctx.engine.handleBlock(context.Background(), blockA.Block) 335 require.NoError(t, err) 336 337 // receive block again before collection is received 338 err = ctx.engine.handleBlock(context.Background(), blockA.Block) 339 require.NoError(t, err) 340 341 ctx.mockComputeBlock(store) 342 wg := sync.WaitGroup{} 343 wg.Add(1) // wait for block A to be executed 344 ctx.mockSaveExecutionResults(store, &wg) 345 ctx.providerEngine.On("BroadcastExecutionReceipt", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) 346 347 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col) 348 require.NoError(t, err) 349 350 // receiving collection again before block is executed 351 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col) 352 require.NoError(t, err) 353 354 unittest.AssertReturnsBefore(t, wg.Wait, 10*time.Second) 355 356 // receiving collection again after block is executed 357 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col) 358 require.NoError(t, err) 359 360 // verify collection is fetched 361 require.True(t, ctx.fetcher.IsFetched(col.ID())) 362 363 // verify block is executed 364 store.AssertExecuted(t, "A", blockA.ID()) 365 }) 366 } 367 368 // given two blocks depend on the same root block and contain same collections, 369 // receiving all collections will trigger the execution of both blocks concurrently 370 func TestExecuteForkConcurrently(t *testing.T) { 371 runWithEngine(t, func(ctx testingContext) { 372 store := mocks.NewMockBlockStore(t) 373 374 // create A and B that have the same collections and same parent 375 // Root <- A[C1, C2] 376 // <- B[C1, C2] 377 col1 := unittest.CollectionFixture(1) 378 col2 := unittest.CollectionFixture(1) 379 380 blockA := makeBlockWithCollection(store.RootBlock, &col1, &col2) 381 blockB := makeBlockWithCollection(store.RootBlock, &col1, &col2) 382 resultA := store.CreateBlockAndMockResult(t, blockA) 383 resultB := store.CreateBlockAndMockResult(t, blockB) 384 385 ctx.mockIsBlockExecuted(store) 386 ctx.mockStateCommitmentByBlockID(store) 387 ctx.mockGetExecutionResultID(store) 388 ctx.mockNewStorageSnapshot(resultA) 389 ctx.mockNewStorageSnapshot(resultB) 390 391 // receive blocks 392 err := ctx.engine.handleBlock(context.Background(), blockA.Block) 393 require.NoError(t, err) 394 395 err = ctx.engine.handleBlock(context.Background(), blockB.Block) 396 require.NoError(t, err) 397 398 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col1) 399 require.NoError(t, err) 400 401 ctx.providerEngine.On("BroadcastExecutionReceipt", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) 402 ctx.mockComputeBlock(store) 403 wg := sync.WaitGroup{} 404 wg.Add(2) // wait for A and B to be executed 405 ctx.mockSaveExecutionResults(store, &wg) 406 407 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col2) 408 require.NoError(t, err) 409 410 unittest.AssertReturnsBefore(t, wg.Wait, 10*time.Second) 411 412 // verify block is executed 413 store.AssertExecuted(t, "A", blockA.ID()) 414 store.AssertExecuted(t, "B", blockB.ID()) 415 }) 416 } 417 418 // verify block will be executed in order 419 func TestExecuteBlockInOrder(t *testing.T) { 420 runWithEngine(t, func(ctx testingContext) { 421 store := mocks.NewMockBlockStore(t) 422 // create A and B that have the same collections and same parent 423 // Root <- A[C1, C2] 424 // <- B[C2] <- C[C3] 425 // verify receiving C3, C1, then C2 will trigger all blocks to be executed 426 col1 := unittest.CollectionFixture(1) 427 col2 := unittest.CollectionFixture(1) 428 col3 := unittest.CollectionFixture(1) 429 430 blockA := makeBlockWithCollection(store.RootBlock, &col1, &col2) 431 blockB := makeBlockWithCollection(store.RootBlock, &col2) 432 blockC := makeBlockWithCollection(store.RootBlock, &col3) 433 resultA := store.CreateBlockAndMockResult(t, blockA) 434 resultB := store.CreateBlockAndMockResult(t, blockB) 435 resultC := store.CreateBlockAndMockResult(t, blockC) 436 437 ctx.mockIsBlockExecuted(store) 438 ctx.mockStateCommitmentByBlockID(store) 439 ctx.mockGetExecutionResultID(store) 440 ctx.mockNewStorageSnapshot(resultA) 441 ctx.mockNewStorageSnapshot(resultB) 442 ctx.mockNewStorageSnapshot(resultC) 443 444 // receive blocks 445 err := ctx.engine.handleBlock(context.Background(), blockA.Block) 446 require.NoError(t, err) 447 448 err = ctx.engine.handleBlock(context.Background(), blockB.Block) 449 require.NoError(t, err) 450 451 err = ctx.engine.handleBlock(context.Background(), blockC.Block) 452 require.NoError(t, err) 453 454 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col3) 455 require.NoError(t, err) 456 457 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col1) 458 require.NoError(t, err) 459 460 ctx.providerEngine.On("BroadcastExecutionReceipt", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) 461 ctx.mockComputeBlock(store) 462 wg := sync.WaitGroup{} 463 wg.Add(3) // waiting for A, B, C to be executed 464 ctx.mockSaveExecutionResults(store, &wg) 465 466 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col2) 467 require.NoError(t, err) 468 469 unittest.AssertReturnsBefore(t, wg.Wait, 10*time.Second) 470 471 // verify block is executed 472 store.AssertExecuted(t, "A", blockA.ID()) 473 store.AssertExecuted(t, "B", blockB.ID()) 474 store.AssertExecuted(t, "C", blockC.ID()) 475 }) 476 } 477 478 func logBlocks(blocks map[string]*entity.ExecutableBlock) { 479 log := unittest.Logger() 480 for name, b := range blocks { 481 log.Debug().Msgf("creating blocks for testing, block %v's ID:%v", name, b.ID()) 482 } 483 } 484 485 // verify that when blocks above the stop height are finalized, they won't 486 // be executed 487 func TestStopAtHeightWhenFinalizedBeforeExecuted(t *testing.T) { 488 runWithEngine(t, func(ctx testingContext) { 489 store := mocks.NewMockBlockStore(t) 490 491 // this collection is used as trigger of execution 492 executionTrigger := unittest.CollectionFixture(1) 493 blockA := makeBlockWithCollection(store.RootBlock, &executionTrigger) 494 blockB := makeBlockWithCollection(blockA.Block.Header) 495 blockC := makeBlockWithCollection(blockB.Block.Header) 496 blockD := makeBlockWithCollection(blockC.Block.Header) 497 498 resultA := store.CreateBlockAndMockResult(t, blockA) 499 resultB := store.CreateBlockAndMockResult(t, blockB) 500 store.CreateBlockAndMockResult(t, blockC) 501 store.CreateBlockAndMockResult(t, blockD) 502 503 stopHeight := store.RootBlock.Height + 3 504 require.Equal(t, stopHeight, blockC.Block.Header.Height) // stop at C (C will not be executed) 505 err := ctx.stopControl.SetStopParameters(stop.StopParameters{ 506 StopBeforeHeight: stopHeight, 507 }) 508 require.NoError(t, err) 509 510 ctx.mockIsBlockExecuted(store) 511 ctx.mockStateCommitmentByBlockID(store) 512 ctx.mockGetExecutionResultID(store) 513 ctx.mockNewStorageSnapshot(resultA) 514 ctx.mockNewStorageSnapshot(resultB) 515 516 // receive blocks 517 err = ctx.engine.handleBlock(context.Background(), blockA.Block) 518 require.NoError(t, err) 519 520 err = ctx.engine.handleBlock(context.Background(), blockB.Block) 521 require.NoError(t, err) 522 523 err = ctx.engine.handleBlock(context.Background(), blockC.Block) 524 require.NoError(t, err) 525 526 err = ctx.engine.handleBlock(context.Background(), blockD.Block) 527 require.NoError(t, err) 528 529 ctx.providerEngine.On("BroadcastExecutionReceipt", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) 530 ctx.mockComputeBlock(store) 531 wg := sync.WaitGroup{} 532 wg.Add(2) // only 2 blocks (A, B) will be executed 533 ctx.mockSaveExecutionResults(store, &wg) 534 535 // all blocks finalized 536 ctx.stopControl.BlockFinalizedForTesting(blockA.Block.Header) 537 ctx.stopControl.BlockFinalizedForTesting(blockB.Block.Header) 538 ctx.stopControl.BlockFinalizedForTesting(blockC.Block.Header) 539 ctx.stopControl.BlockFinalizedForTesting(blockD.Block.Header) 540 541 // receiving the colleciton to trigger all blocks to be executed 542 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &executionTrigger) 543 require.NoError(t, err) 544 545 unittest.AssertReturnsBefore(t, wg.Wait, 10*time.Second) 546 547 // since stop height is C, verify that only A and B are executed, C and D are not executed 548 store.AssertExecuted(t, "A", blockA.ID()) 549 store.AssertExecuted(t, "B", blockB.ID()) 550 551 store.AssertNotExecuted(t, "C", blockC.ID()) 552 store.AssertNotExecuted(t, "D", blockD.ID()) 553 }) 554 } 555 556 // verify that blocks above the stop height won't be executed, even if they are 557 // later they got finalized 558 func TestStopAtHeightWhenExecutedBeforeFinalized(t *testing.T) { 559 runWithEngine(t, func(ctx testingContext) { 560 store := mocks.NewMockBlockStore(t) 561 562 blockA := makeBlockWithCollection(store.RootBlock) 563 blockB := makeBlockWithCollection(blockA.Block.Header) 564 blockC := makeBlockWithCollection(blockB.Block.Header) 565 blockD := makeBlockWithCollection(blockC.Block.Header) 566 567 resultA := store.CreateBlockAndMockResult(t, blockA) 568 resultB := store.CreateBlockAndMockResult(t, blockB) 569 store.CreateBlockAndMockResult(t, blockC) 570 store.CreateBlockAndMockResult(t, blockD) 571 572 stopHeight := store.RootBlock.Height + 3 573 require.Equal(t, stopHeight, blockC.Block.Header.Height) // stop at C (C will not be executed) 574 err := ctx.stopControl.SetStopParameters(stop.StopParameters{ 575 StopBeforeHeight: stopHeight, 576 }) 577 require.NoError(t, err) 578 579 ctx.mockIsBlockExecuted(store) 580 ctx.mockStateCommitmentByBlockID(store) 581 ctx.mockGetExecutionResultID(store) 582 ctx.mockNewStorageSnapshot(resultA) 583 ctx.mockNewStorageSnapshot(resultB) 584 585 ctx.providerEngine.On("BroadcastExecutionReceipt", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) 586 ctx.mockComputeBlock(store) 587 wg := sync.WaitGroup{} 588 wg.Add(2) // waiting for only A, B to be executed 589 ctx.mockSaveExecutionResults(store, &wg) 590 591 // receive blocks 592 err = ctx.engine.handleBlock(context.Background(), blockA.Block) 593 require.NoError(t, err) 594 595 err = ctx.engine.handleBlock(context.Background(), blockB.Block) 596 require.NoError(t, err) 597 598 err = ctx.engine.handleBlock(context.Background(), blockC.Block) 599 require.NoError(t, err) 600 601 err = ctx.engine.handleBlock(context.Background(), blockD.Block) 602 require.NoError(t, err) 603 604 // all blocks finalized 605 ctx.stopControl.BlockFinalizedForTesting(blockA.Block.Header) 606 ctx.stopControl.BlockFinalizedForTesting(blockB.Block.Header) 607 ctx.stopControl.BlockFinalizedForTesting(blockC.Block.Header) 608 ctx.stopControl.BlockFinalizedForTesting(blockD.Block.Header) 609 610 unittest.AssertReturnsBefore(t, wg.Wait, 10*time.Second) 611 612 // since stop height is C, verify that only A and B are executed, C and D are not executed 613 store.AssertExecuted(t, "A", blockA.ID()) 614 store.AssertExecuted(t, "B", blockB.ID()) 615 616 store.AssertNotExecuted(t, "C", blockC.ID()) 617 store.AssertNotExecuted(t, "D", blockD.ID()) 618 }) 619 } 620 621 // verify that when blocks execution and finalization happen concurrently 622 func TestStopAtHeightWhenExecutionFinalization(t *testing.T) { 623 runWithEngine(t, func(ctx testingContext) { 624 store := mocks.NewMockBlockStore(t) 625 626 // Root <- A <- B (stop height, won't execute) <- C 627 // verify when executing A and finalizing B happens concurrently, 628 // still won't allow B and C to be executed 629 blockA := makeBlockWithCollection(store.RootBlock) 630 blockB := makeBlockWithCollection(blockA.Block.Header) 631 blockC := makeBlockWithCollection(blockB.Block.Header) 632 633 resultA := store.CreateBlockAndMockResult(t, blockA) 634 store.CreateBlockAndMockResult(t, blockB) 635 store.CreateBlockAndMockResult(t, blockC) 636 637 err := ctx.stopControl.SetStopParameters(stop.StopParameters{ 638 StopBeforeHeight: blockB.Block.Header.Height, 639 }) 640 require.NoError(t, err) 641 642 ctx.mockIsBlockExecuted(store) 643 ctx.mockStateCommitmentByBlockID(store) 644 ctx.mockGetExecutionResultID(store) 645 ctx.mockNewStorageSnapshot(resultA) 646 647 ctx.providerEngine.On("BroadcastExecutionReceipt", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) 648 ctx.mockComputeBlock(store) 649 wg := sync.WaitGroup{} 650 // waiting for: 651 // 1. A, B, C to be handled 652 // 2. A, B, C to be finalized 653 // 3. only A to be executed 654 wg.Add(3) 655 ctx.mockSaveExecutionResults(store, &wg) 656 657 // receive blocks 658 go func(wg *sync.WaitGroup) { 659 err = ctx.engine.handleBlock(context.Background(), blockA.Block) 660 require.NoError(t, err) 661 662 err = ctx.engine.handleBlock(context.Background(), blockB.Block) 663 require.NoError(t, err) 664 665 err = ctx.engine.handleBlock(context.Background(), blockC.Block) 666 require.NoError(t, err) 667 wg.Done() 668 }(&wg) 669 670 go func(wg *sync.WaitGroup) { 671 // all blocks finalized 672 ctx.stopControl.BlockFinalizedForTesting(blockA.Block.Header) 673 ctx.stopControl.BlockFinalizedForTesting(blockB.Block.Header) 674 ctx.stopControl.BlockFinalizedForTesting(blockC.Block.Header) 675 wg.Done() 676 }(&wg) 677 678 unittest.AssertReturnsBefore(t, wg.Wait, 10*time.Second) 679 680 // since stop height is C, verify that only A and B are executed, C and D are not executed 681 store.AssertExecuted(t, "A", blockA.ID()) 682 store.AssertNotExecuted(t, "B", blockB.ID()) 683 store.AssertNotExecuted(t, "C", blockC.ID()) 684 }) 685 } 686 687 // TestExecutedBlockUploadedFailureDoesntBlock tests that block processing continues even the 688 // uploader fails with an error 689 func TestExecutedBlockUploadedFailureDoesntBlock(t *testing.T) { 690 runWithEngine(t, func(ctx testingContext) { 691 store := mocks.NewMockBlockStore(t) 692 693 col := unittest.CollectionFixture(1) 694 // Root <- A 695 blockA := makeBlockWithCollection(store.RootBlock, &col) 696 result := store.CreateBlockAndMockResult(t, blockA) 697 698 ctx.mockIsBlockExecuted(store) 699 ctx.mockStateCommitmentByBlockID(store) 700 ctx.mockGetExecutionResultID(store) 701 ctx.mockNewStorageSnapshot(result) 702 703 // receive block 704 err := ctx.engine.handleBlock(context.Background(), blockA.Block) 705 require.NoError(t, err) 706 707 ctx.mockComputeBlock(store) 708 wg := sync.WaitGroup{} 709 wg.Add(1) // wait for block A to be executed 710 ctx.mockSaveExecutionResults(store, &wg) 711 712 // verify upload will fail 713 uploader1 := uploadermock.NewUploader(ctx.t) 714 uploader1.On("Upload", result).Return(fmt.Errorf("error uploading")).Once() 715 ctx.uploadMgr.AddUploader(uploader1) 716 717 // verify broadcast will be called 718 ctx.providerEngine.On("BroadcastExecutionReceipt", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) 719 720 err = ctx.engine.handleCollection(unittest.IdentifierFixture(), &col) 721 require.NoError(t, err) 722 723 unittest.AssertReturnsBefore(t, wg.Wait, 10*time.Second) 724 725 // verify collection is fetched 726 require.True(t, ctx.fetcher.IsFetched(col.ID())) 727 728 // verify block is executed 729 store.AssertExecuted(t, "A", blockA.ID()) 730 }) 731 } 732 733 func makeCollection() (*flow.Collection, *flow.CollectionGuarantee) { 734 col := unittest.CollectionFixture(1) 735 gua := col.Guarantee() 736 return &col, &gua 737 } 738 739 func makeBlockWithCollection(parent *flow.Header, cols ...*flow.Collection) *entity.ExecutableBlock { 740 block := unittest.BlockWithParentFixture(parent) 741 completeCollections := make(map[flow.Identifier]*entity.CompleteCollection, len(block.Payload.Guarantees)) 742 for _, col := range cols { 743 g := col.Guarantee() 744 block.Payload.Guarantees = append(block.Payload.Guarantees, &g) 745 746 cc := &entity.CompleteCollection{ 747 Guarantee: &g, 748 Transactions: col.Transactions, 749 } 750 completeCollections[col.ID()] = cc 751 } 752 block.Header.PayloadHash = block.Payload.Hash() 753 754 executableBlock := &entity.ExecutableBlock{ 755 Block: block, 756 CompleteCollections: completeCollections, 757 StartState: unittest.StateCommitmentPointerFixture(), 758 } 759 return executableBlock 760 } 761 762 func (ctx *testingContext) mockIsBlockExecuted(store *mocks.MockBlockStore) { 763 ctx.executionState.On("IsBlockExecuted", mock.Anything, mock.Anything). 764 Return(func(height uint64, blockID flow.Identifier) (bool, error) { 765 _, err := store.GetExecuted(blockID) 766 if err != nil { 767 return false, nil 768 } 769 return true, nil 770 }) 771 } 772 773 func (ctx *testingContext) mockStateCommitmentByBlockID(store *mocks.MockBlockStore) { 774 ctx.executionState.On("StateCommitmentByBlockID", mock.Anything). 775 Return(func(blockID flow.Identifier) (flow.StateCommitment, error) { 776 result, err := store.GetExecuted(blockID) 777 if err != nil { 778 return flow.StateCommitment{}, storageerr.ErrNotFound 779 } 780 return result.Result.CurrentEndState(), nil 781 }) 782 } 783 784 func (ctx *testingContext) mockGetExecutionResultID(store *mocks.MockBlockStore) { 785 ctx.executionState.On("GetExecutionResultID", mock.Anything, mock.Anything). 786 Return(func(ctx context.Context, blockID flow.Identifier) (flow.Identifier, error) { 787 blockResult, err := store.GetExecuted(blockID) 788 if err != nil { 789 return flow.ZeroID, storageerr.ErrNotFound 790 } 791 792 return blockResult.Result.ExecutionReceipt.ExecutionResult.ID(), nil 793 }) 794 } 795 796 func (ctx *testingContext) mockNewStorageSnapshot(result *execution.ComputationResult) { 797 // the result is the mocked result for the block, in other words, if the ingestion executes this block, 798 // the mocked computationManager will produce this result. 799 // so when mocking the StorageSnapshot method, it must be called with the StartState, as well as its 800 // parent block, which is used for retrieving the storage state at the end of the parent block. 801 ctx.executionState.On("NewStorageSnapshot", 802 *result.ExecutableBlock.StartState, 803 result.ExecutableBlock.Block.Header.ParentID, 804 result.ExecutableBlock.Block.Header.Height-1).Return(nil) 805 } 806 807 func (ctx *testingContext) mockComputeBlock(store *mocks.MockBlockStore) { 808 ctx.computationManager.On("ComputeBlock", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 809 Return(func(ctx context.Context, 810 parentBlockExecutionResultID flow.Identifier, 811 block *entity.ExecutableBlock, 812 snapshot snapshot.StorageSnapshot) ( 813 *execution.ComputationResult, error) { 814 blockResult, ok := store.ResultByBlock[block.ID()] 815 if !ok { 816 return nil, fmt.Errorf("block %s not found", block.ID()) 817 } 818 return blockResult.Result, nil 819 }) 820 } 821 822 func (ctx *testingContext) mockSaveExecutionResults(store *mocks.MockBlockStore, wg *sync.WaitGroup) { 823 ctx.executionState.On("SaveExecutionResults", mock.Anything, mock.Anything). 824 Return(func(ctx context.Context, result *execution.ComputationResult) error { 825 defer wg.Done() 826 err := store.MarkExecuted(result) 827 if err != nil { 828 return err 829 } 830 return nil 831 }) 832 }