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