github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/builder/consensus/builder_test.go (about) 1 package consensus 2 3 import ( 4 "math/rand" 5 "os" 6 "testing" 7 8 "github.com/dgraph-io/badger/v2" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/mock" 11 "github.com/stretchr/testify/require" 12 "github.com/stretchr/testify/suite" 13 14 "github.com/onflow/flow-go/model/flow" 15 mempoolAPIs "github.com/onflow/flow-go/module/mempool" 16 mempoolImpl "github.com/onflow/flow-go/module/mempool/consensus" 17 mempool "github.com/onflow/flow-go/module/mempool/mock" 18 "github.com/onflow/flow-go/module/metrics" 19 "github.com/onflow/flow-go/module/trace" 20 realproto "github.com/onflow/flow-go/state/protocol" 21 protocol "github.com/onflow/flow-go/state/protocol/mock" 22 storerr "github.com/onflow/flow-go/storage" 23 "github.com/onflow/flow-go/storage/badger/operation" 24 "github.com/onflow/flow-go/storage/badger/transaction" 25 storage "github.com/onflow/flow-go/storage/mock" 26 "github.com/onflow/flow-go/utils/unittest" 27 ) 28 29 func TestConsensusBuilder(t *testing.T) { 30 suite.Run(t, new(BuilderSuite)) 31 } 32 33 type BuilderSuite struct { 34 suite.Suite 35 36 // test helpers 37 firstID flow.Identifier // first block in the range we look at 38 finalID flow.Identifier // last finalized block 39 parentID flow.Identifier // Parent block we build on 40 finalizedBlockIDs []flow.Identifier // blocks between first and final 41 pendingBlockIDs []flow.Identifier // blocks between final and parent 42 resultForBlock map[flow.Identifier]*flow.ExecutionResult // map: BlockID -> Execution Result 43 resultByID map[flow.Identifier]*flow.ExecutionResult // map: result ID -> Execution Result 44 receiptsByID map[flow.Identifier]*flow.ExecutionReceipt // map: receipt ID -> ExecutionReceipt 45 receiptsByBlockID map[flow.Identifier]flow.ExecutionReceiptList // map: block ID -> flow.ExecutionReceiptList 46 47 // used to populate and test the seal mempool 48 chain []*flow.Seal // chain of seals starting first 49 irsList []*flow.IncorporatedResultSeal // chain of IncorporatedResultSeals 50 irsMap map[flow.Identifier]*flow.IncorporatedResultSeal // index for irsList 51 52 // mempools consumed by builder 53 pendingGuarantees []*flow.CollectionGuarantee 54 pendingReceipts []*flow.ExecutionReceipt 55 pendingSeals map[flow.Identifier]*flow.IncorporatedResultSeal // storage for the seal mempool 56 57 // storage for dbs 58 headers map[flow.Identifier]*flow.Header 59 index map[flow.Identifier]*flow.Index 60 blocks map[flow.Identifier]*flow.Block 61 blockChildren map[flow.Identifier][]flow.Identifier // ids of children blocks 62 63 lastSeal *flow.Seal 64 65 // real dependencies 66 dir string 67 db *badger.DB 68 sentinel uint64 69 setter func(*flow.Header) error 70 sign func(*flow.Header) error 71 72 // mocked dependencies 73 state *protocol.ParticipantState 74 headerDB *storage.Headers 75 sealDB *storage.Seals 76 indexDB *storage.Index 77 blockDB *storage.Blocks 78 resultDB *storage.ExecutionResults 79 receiptsDB *storage.ExecutionReceipts 80 stateMutator *protocol.MutableProtocolState 81 82 guarPool *mempool.Guarantees 83 sealPool *mempool.IncorporatedResultSeals 84 recPool *mempool.ExecutionTree 85 86 // tracking behaviour 87 assembled *flow.Payload // built payload 88 89 // component under test 90 build *Builder 91 } 92 93 func (bs *BuilderSuite) storeBlock(block *flow.Block) { 94 bs.headers[block.ID()] = block.Header 95 bs.blocks[block.ID()] = block 96 bs.index[block.ID()] = block.Payload.Index() 97 bs.blockChildren[block.Header.ParentID] = append(bs.blockChildren[block.Header.ParentID], block.ID()) 98 for _, result := range block.Payload.Results { 99 bs.resultByID[result.ID()] = result 100 } 101 } 102 103 // createAndRecordBlock creates a new block chained to the previous block. 104 // The new block contains a receipt for a result of the previous 105 // block, which is also used to create a seal for the previous block. The seal 106 // and the result are combined in an IncorporatedResultSeal which is a candidate 107 // for the seals mempool. 108 func (bs *BuilderSuite) createAndRecordBlock(parentBlock *flow.Block, candidateSealForParent bool) *flow.Block { 109 block := unittest.BlockWithParentFixture(parentBlock.Header) 110 111 // Create a receipt for a result of the parentBlock block, 112 // and add it to the payload. The corresponding IncorporatedResult will be used to 113 // seal the parentBlock, and to create an IncorporatedResultSeal for the seal mempool. 114 var incorporatedResultForPrevBlock *flow.IncorporatedResult 115 previousResult, found := bs.resultForBlock[parentBlock.ID()] 116 if !found { 117 panic("missing execution result for parent") 118 } 119 receipt := unittest.ExecutionReceiptFixture(unittest.WithResult(previousResult)) 120 block.Payload.Receipts = append(block.Payload.Receipts, receipt.Meta()) 121 block.Payload.Results = append(block.Payload.Results, &receipt.ExecutionResult) 122 123 incorporatedResultForPrevBlock = unittest.IncorporatedResult.Fixture( 124 unittest.IncorporatedResult.WithResult(previousResult), 125 unittest.IncorporatedResult.WithIncorporatedBlockID(block.ID()), 126 ) 127 128 result := unittest.ExecutionResultFixture( 129 unittest.WithBlock(block), 130 unittest.WithPreviousResult(*previousResult), 131 ) 132 133 bs.resultForBlock[result.BlockID] = result 134 bs.resultByID[result.ID()] = result 135 bs.receiptsByID[receipt.ID()] = receipt 136 bs.receiptsByBlockID[receipt.ExecutionResult.BlockID] = append(bs.receiptsByBlockID[receipt.ExecutionResult.BlockID], receipt) 137 138 // record block in dbs 139 bs.storeBlock(block) 140 141 if candidateSealForParent { 142 // seal the parentBlock block with the result included in this block. 143 bs.chainSeal(incorporatedResultForPrevBlock) 144 } 145 146 return block 147 } 148 149 // Create a seal for the result's block. The corresponding 150 // IncorporatedResultSeal, which ties the seal to the incorporated result it 151 // seals, is also recorded for future access. 152 func (bs *BuilderSuite) chainSeal(incorporatedResult *flow.IncorporatedResult) { 153 incorporatedResultSeal := unittest.IncorporatedResultSeal.Fixture( 154 unittest.IncorporatedResultSeal.WithResult(incorporatedResult.Result), 155 unittest.IncorporatedResultSeal.WithIncorporatedBlockID(incorporatedResult.IncorporatedBlockID), 156 ) 157 158 bs.chain = append(bs.chain, incorporatedResultSeal.Seal) 159 bs.irsMap[incorporatedResultSeal.ID()] = incorporatedResultSeal 160 bs.irsList = append(bs.irsList, incorporatedResultSeal) 161 } 162 163 // SetupTest constructs the following chain of blocks: 164 // 165 // [first] <- [F0] <- [F1] <- [F2] <- [F3] <- [final] <- [A0] <- [A1] <- [A2] <- [A3] <- [parent] 166 // 167 // Where block 168 // - [first] is sealed and finalized 169 // - [F0] ... [F4] and [final] are finalized, unsealed blocks with candidate seals are included in mempool 170 // - [A0] ... [A2] are non-finalized, unsealed blocks with candidate seals are included in mempool 171 // - [A3] and [parent] are non-finalized, unsealed blocks _without_ candidate seals 172 // 173 // Each block incorporates the result for its immediate parent. 174 // 175 // Note: In the happy path, the blocks [A3] and [parent] will not have candidate seal for the following reason: 176 // For the verifiers to start checking a result R, they need a source of randomness for the block _incorporating_ 177 // result R. The result for block [A3] is incorporated in [parent], which does _not_ have a child yet. 178 func (bs *BuilderSuite) SetupTest() { 179 180 // set up no-op dependencies 181 noopMetrics := metrics.NewNoopCollector() 182 noopTracer := trace.NewNoopTracer() 183 184 // set up test parameters 185 numFinalizedBlocks := 4 186 numPendingBlocks := 4 187 188 // reset test helpers 189 bs.pendingBlockIDs = nil 190 bs.finalizedBlockIDs = nil 191 bs.resultForBlock = make(map[flow.Identifier]*flow.ExecutionResult) 192 bs.resultByID = make(map[flow.Identifier]*flow.ExecutionResult) 193 bs.receiptsByID = make(map[flow.Identifier]*flow.ExecutionReceipt) 194 bs.receiptsByBlockID = make(map[flow.Identifier]flow.ExecutionReceiptList) 195 196 bs.chain = nil 197 bs.irsMap = make(map[flow.Identifier]*flow.IncorporatedResultSeal) 198 bs.irsList = nil 199 200 // initialize the pools 201 bs.pendingGuarantees = nil 202 bs.pendingSeals = nil 203 bs.pendingReceipts = nil 204 205 // initialise the dbs 206 bs.lastSeal = nil 207 bs.headers = make(map[flow.Identifier]*flow.Header) 208 //bs.heights = make(map[uint64]*flow.Header) 209 bs.index = make(map[flow.Identifier]*flow.Index) 210 bs.blocks = make(map[flow.Identifier]*flow.Block) 211 bs.blockChildren = make(map[flow.Identifier][]flow.Identifier) 212 213 // initialize behaviour tracking 214 bs.assembled = nil 215 216 // Construct the [first] block: 217 first := unittest.BlockFixture() 218 bs.storeBlock(&first) 219 bs.firstID = first.ID() 220 firstResult := unittest.ExecutionResultFixture(unittest.WithBlock(&first)) 221 bs.lastSeal = unittest.Seal.Fixture(unittest.Seal.WithResult(firstResult)) 222 bs.resultForBlock[firstResult.BlockID] = firstResult 223 bs.resultByID[firstResult.ID()] = firstResult 224 225 // Construct finalized blocks [F0] ... [F4] 226 previous := &first 227 for n := 0; n < numFinalizedBlocks; n++ { 228 finalized := bs.createAndRecordBlock(previous, n > 0) // Do not construct candidate seal for [first], as it is already sealed 229 bs.finalizedBlockIDs = append(bs.finalizedBlockIDs, finalized.ID()) 230 previous = finalized 231 } 232 233 // Construct the last finalized block [final] 234 final := bs.createAndRecordBlock(previous, true) 235 bs.finalID = final.ID() 236 237 // Construct the pending (i.e. unfinalized) ancestors [A0], ..., [A3] 238 previous = final 239 for n := 0; n < numPendingBlocks; n++ { 240 pending := bs.createAndRecordBlock(previous, true) 241 bs.pendingBlockIDs = append(bs.pendingBlockIDs, pending.ID()) 242 previous = pending 243 } 244 245 // Construct [parent] block; but do _not_ add candidate seal for its parent 246 parent := bs.createAndRecordBlock(previous, false) 247 bs.parentID = parent.ID() 248 249 // set up temporary database for tests 250 bs.db, bs.dir = unittest.TempBadgerDB(bs.T()) 251 252 err := bs.db.Update(operation.InsertFinalizedHeight(final.Header.Height)) 253 bs.Require().NoError(err) 254 err = bs.db.Update(operation.IndexBlockHeight(final.Header.Height, bs.finalID)) 255 bs.Require().NoError(err) 256 257 err = bs.db.Update(operation.InsertRootHeight(13)) 258 bs.Require().NoError(err) 259 260 err = bs.db.Update(operation.InsertSealedHeight(first.Header.Height)) 261 bs.Require().NoError(err) 262 err = bs.db.Update(operation.IndexBlockHeight(first.Header.Height, first.ID())) 263 bs.Require().NoError(err) 264 265 bs.sentinel = 1337 266 267 bs.setter = func(header *flow.Header) error { 268 header.View = 1337 269 return nil 270 } 271 bs.sign = func(_ *flow.Header) error { 272 return nil 273 } 274 275 bs.state = &protocol.ParticipantState{} 276 bs.state.On("Extend", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 277 block := args.Get(1).(*flow.Block) 278 bs.Assert().Equal(bs.sentinel, block.Header.View) 279 bs.assembled = block.Payload 280 }).Return(nil) 281 bs.state.On("Final").Return(func() realproto.Snapshot { 282 if block, ok := bs.blocks[bs.finalID]; ok { 283 snapshot := unittest.StateSnapshotForKnownBlock(block.Header, nil) 284 snapshot.On("Descendants").Return(bs.blockChildren[bs.finalID], nil) 285 return snapshot 286 } 287 return unittest.StateSnapshotForUnknownBlock() 288 }) 289 290 // set up storage mocks for tests 291 bs.sealDB = &storage.Seals{} 292 bs.sealDB.On("HighestInFork", mock.Anything).Return(bs.lastSeal, nil) 293 294 bs.headerDB = &storage.Headers{} 295 bs.headerDB.On("ByBlockID", mock.Anything).Return( 296 func(blockID flow.Identifier) *flow.Header { 297 return bs.headers[blockID] 298 }, 299 func(blockID flow.Identifier) error { 300 _, exists := bs.headers[blockID] 301 if !exists { 302 return storerr.ErrNotFound 303 } 304 return nil 305 }, 306 ) 307 308 bs.indexDB = &storage.Index{} 309 bs.indexDB.On("ByBlockID", mock.Anything).Return( 310 func(blockID flow.Identifier) *flow.Index { 311 return bs.index[blockID] 312 }, 313 func(blockID flow.Identifier) error { 314 _, exists := bs.index[blockID] 315 if !exists { 316 return storerr.ErrNotFound 317 } 318 return nil 319 }, 320 ) 321 322 bs.blockDB = &storage.Blocks{} 323 bs.blockDB.On("ByID", mock.Anything).Return( 324 func(blockID flow.Identifier) *flow.Block { 325 return bs.blocks[blockID] 326 }, 327 func(blockID flow.Identifier) error { 328 _, exists := bs.blocks[blockID] 329 if !exists { 330 return storerr.ErrNotFound 331 } 332 return nil 333 }, 334 ) 335 336 bs.resultDB = &storage.ExecutionResults{} 337 bs.resultDB.On("ByID", mock.Anything).Return( 338 func(resultID flow.Identifier) *flow.ExecutionResult { 339 return bs.resultByID[resultID] 340 }, 341 func(resultID flow.Identifier) error { 342 _, exists := bs.resultByID[resultID] 343 if !exists { 344 return storerr.ErrNotFound 345 } 346 return nil 347 }, 348 ) 349 350 bs.receiptsDB = &storage.ExecutionReceipts{} 351 bs.receiptsDB.On("ByID", mock.Anything).Return( 352 func(receiptID flow.Identifier) *flow.ExecutionReceipt { 353 return bs.receiptsByID[receiptID] 354 }, 355 func(receiptID flow.Identifier) error { 356 _, exists := bs.receiptsByID[receiptID] 357 if !exists { 358 return storerr.ErrNotFound 359 } 360 return nil 361 }, 362 ) 363 bs.receiptsDB.On("ByBlockID", mock.Anything).Return( 364 func(blockID flow.Identifier) flow.ExecutionReceiptList { 365 return bs.receiptsByBlockID[blockID] 366 }, 367 func(blockID flow.Identifier) error { 368 _, exists := bs.receiptsByBlockID[blockID] 369 if !exists { 370 return storerr.ErrNotFound 371 } 372 return nil 373 }, 374 ) 375 376 // set up memory pool mocks for tests 377 bs.guarPool = &mempool.Guarantees{} 378 bs.guarPool.On("Size").Return(uint(0)) // only used by metrics 379 bs.guarPool.On("All").Return( 380 func() []*flow.CollectionGuarantee { 381 return bs.pendingGuarantees 382 }, 383 ) 384 385 bs.sealPool = &mempool.IncorporatedResultSeals{} 386 bs.sealPool.On("Size").Return(uint(0)) // only used by metrics 387 bs.sealPool.On("All").Return( 388 func() []*flow.IncorporatedResultSeal { 389 res := make([]*flow.IncorporatedResultSeal, 0, len(bs.pendingSeals)) 390 for _, ps := range bs.pendingSeals { 391 res = append(res, ps) 392 } 393 return res 394 }, 395 ) 396 bs.sealPool.On("ByID", mock.Anything).Return( 397 func(id flow.Identifier) *flow.IncorporatedResultSeal { 398 return bs.pendingSeals[id] 399 }, 400 func(id flow.Identifier) bool { 401 _, exists := bs.pendingSeals[id] 402 return exists 403 }, 404 ) 405 406 bs.recPool = &mempool.ExecutionTree{} 407 bs.recPool.On("PruneUpToHeight", mock.Anything).Return(nil).Maybe() 408 bs.recPool.On("Size").Return(uint(0)).Maybe() // used for metrics only 409 bs.recPool.On("AddResult", mock.Anything, mock.Anything).Return(nil).Maybe() 410 bs.recPool.On("AddReceipt", mock.Anything, mock.Anything).Return(false, nil).Maybe() 411 bs.recPool.On("ReachableReceipts", mock.Anything, mock.Anything, mock.Anything).Return( 412 func(resultID flow.Identifier, blockFilter mempoolAPIs.BlockFilter, receiptFilter mempoolAPIs.ReceiptFilter) []*flow.ExecutionReceipt { 413 return bs.pendingReceipts 414 }, 415 nil, 416 ) 417 418 // setup mock state mutator, we don't need a real once since we are using mocked participant state. 419 bs.stateMutator = protocol.NewMutableProtocolState(bs.T()) 420 bs.stateMutator.On("EvolveState", mock.Anything, mock.Anything, mock.Anything).Return(unittest.IdentifierFixture(), transaction.NewDeferredBlockPersist(), nil) 421 422 // initialize the builder 423 bs.build, err = NewBuilder( 424 noopMetrics, 425 bs.db, 426 bs.state, 427 bs.headerDB, 428 bs.sealDB, 429 bs.indexDB, 430 bs.blockDB, 431 bs.resultDB, 432 bs.receiptsDB, 433 bs.stateMutator, 434 bs.guarPool, 435 bs.sealPool, 436 bs.recPool, 437 noopTracer, 438 ) 439 require.NoError(bs.T(), err) 440 441 bs.build.cfg.expiry = 11 442 } 443 444 func (bs *BuilderSuite) TearDownTest() { 445 err := bs.db.Close() 446 bs.Assert().NoError(err) 447 err = os.RemoveAll(bs.dir) 448 bs.Assert().NoError(err) 449 } 450 451 func (bs *BuilderSuite) TestPayloadEmptyValid() { 452 453 // we should build an empty block with default setup 454 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 455 bs.Require().NoError(err) 456 bs.Assert().Empty(bs.assembled.Guarantees, "should have no guarantees in payload with empty mempool") 457 bs.Assert().Empty(bs.assembled.Seals, "should have no seals in payload with empty mempool") 458 } 459 460 func (bs *BuilderSuite) TestPayloadGuaranteeValid() { 461 462 // add sixteen guarantees to the pool 463 bs.pendingGuarantees = unittest.CollectionGuaranteesFixture(16, unittest.WithCollRef(bs.finalID)) 464 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 465 bs.Require().NoError(err) 466 bs.Assert().ElementsMatch(bs.pendingGuarantees, bs.assembled.Guarantees, "should have guarantees from mempool in payload") 467 } 468 469 func (bs *BuilderSuite) TestPayloadGuaranteeDuplicate() { 470 471 // create some valid guarantees 472 valid := unittest.CollectionGuaranteesFixture(4, unittest.WithCollRef(bs.finalID)) 473 474 forkBlocks := append(bs.finalizedBlockIDs, bs.pendingBlockIDs...) 475 476 // create some duplicate guarantees and add to random blocks on the fork 477 duplicated := unittest.CollectionGuaranteesFixture(12, unittest.WithCollRef(bs.finalID)) 478 for _, guarantee := range duplicated { 479 blockID := forkBlocks[rand.Intn(len(forkBlocks))] 480 index := bs.index[blockID] 481 index.CollectionIDs = append(index.CollectionIDs, guarantee.ID()) 482 bs.index[blockID] = index 483 } 484 485 // add sixteen guarantees to the pool 486 bs.pendingGuarantees = append(valid, duplicated...) 487 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 488 bs.Require().NoError(err) 489 bs.Assert().ElementsMatch(valid, bs.assembled.Guarantees, "should have valid guarantees from mempool in payload") 490 } 491 492 func (bs *BuilderSuite) TestPayloadGuaranteeReferenceUnknown() { 493 494 // create 12 valid guarantees 495 valid := unittest.CollectionGuaranteesFixture(12, unittest.WithCollRef(bs.finalID)) 496 497 // create 4 guarantees with unknown reference 498 unknown := unittest.CollectionGuaranteesFixture(4, unittest.WithCollRef(unittest.IdentifierFixture())) 499 500 // add all guarantees to the pool 501 bs.pendingGuarantees = append(valid, unknown...) 502 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 503 bs.Require().NoError(err) 504 bs.Assert().ElementsMatch(valid, bs.assembled.Guarantees, "should have valid from mempool in payload") 505 } 506 507 func (bs *BuilderSuite) TestPayloadGuaranteeReferenceExpired() { 508 509 // create 12 valid guarantees 510 valid := unittest.CollectionGuaranteesFixture(12, unittest.WithCollRef(bs.finalID)) 511 512 // create 4 expired guarantees 513 header := unittest.BlockHeaderFixture() 514 header.Height = bs.headers[bs.finalID].Height - 12 515 bs.headers[header.ID()] = header 516 expired := unittest.CollectionGuaranteesFixture(4, unittest.WithCollRef(header.ID())) 517 518 // add all guarantees to the pool 519 bs.pendingGuarantees = append(valid, expired...) 520 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 521 bs.Require().NoError(err) 522 bs.Assert().ElementsMatch(valid, bs.assembled.Guarantees, "should have valid from mempool in payload") 523 } 524 525 // TestPayloadSeals_AllValid checks that builder seals as many blocks as possible (happy path): 526 // 527 // [first] <- [F0] <- [F1] <- [F2] <- [F3] <- [final] <- [A0] <- [A1] <- [A2] <- [A3] <- [parent] 528 // 529 // Where block 530 // - [first] is sealed and finalized 531 // - [F0] ... [F4] and [final] are finalized, unsealed blocks with candidate seals are included in mempool 532 // - [A0] ... [A2] are non-finalized, unsealed blocks with candidate seals are included in mempool 533 // - [A3] and [parent] are non-finalized, unsealed blocks _without_ candidate seals 534 // 535 // Expected behaviour: 536 // - builder should include seals [F0], ..., [A4] 537 // - note: Block [A3] will not have a seal in the happy path for the following reason: 538 // In our example, the result for block A3 is incorporated in block A4. But, for the verifiers to start 539 // their work, they need a child block of A4, because the child contains the source of randomness for 540 // A4. But we are just constructing this child right now. Hence, the verifiers couldn't have checked 541 // the result for A3. 542 func (bs *BuilderSuite) TestPayloadSeals_AllValid() { 543 // Populate seals mempool with valid chain of seals for blocks [F0], ..., [A2] 544 bs.pendingSeals = bs.irsMap 545 546 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 547 bs.Require().NoError(err) 548 bs.Assert().Empty(bs.assembled.Guarantees, "should have no guarantees in payload with empty mempool") 549 bs.Assert().ElementsMatch(bs.chain, bs.assembled.Seals, "should have included valid chain of seals") 550 } 551 552 // TestPayloadSeals_Limit verifies that builder does not exceed maxSealLimit 553 func (bs *BuilderSuite) TestPayloadSeals_Limit() { 554 // use valid chain of seals in mempool 555 bs.pendingSeals = bs.irsMap 556 557 // change maxSealCount to one less than the number of items in the mempool 558 limit := uint(2) 559 bs.build.cfg.maxSealCount = limit 560 561 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 562 bs.Require().NoError(err) 563 bs.Assert().Empty(bs.assembled.Guarantees, "should have no guarantees in payload with empty mempool") 564 bs.Assert().Equal(bs.chain[:limit], bs.assembled.Seals, "should have excluded seals above maxSealCount") 565 } 566 567 // TestPayloadSeals_OnlyFork checks that the builder only includes seals corresponding 568 // to blocks on the current fork (and _not_ seals for sealable blocks on other forks) 569 func (bs *BuilderSuite) TestPayloadSeals_OnlyFork() { 570 // in the test setup, we already created a single fork 571 // [first] <- [F0] <- [F1] <- [F2] <- [F3] <- [final] <- [A0] <- [A1] <- [A2] .. 572 // For this test, we add fork: ^ 573 // └--- [B0] <- [B1] <- ....<- [B6] <- [B7] 574 // Where block 575 // * [first] is sealed and finalized 576 // * [F0] ... [F4] and [final] are finalized, unsealed blocks with candidate seals are included in mempool 577 // * [A0] ... [A2] are non-finalized, unsealed blocks with candidate seals are included in mempool 578 forkHead := bs.blocks[bs.finalID] 579 for i := 0; i < 8; i++ { 580 // Usually, the blocks [B6] and [B7] will not have candidate seal for the following reason: 581 // For the verifiers to start checking a result R, they need a source of randomness for the block _incorporating_ 582 // result R. The result for block [B6] is incorporated in [B7], which does _not_ have a child yet. 583 forkHead = bs.createAndRecordBlock(forkHead, i < 6) 584 } 585 586 bs.pendingSeals = bs.irsMap 587 _, err := bs.build.BuildOn(forkHead.ID(), bs.setter, bs.sign) 588 bs.Require().NoError(err) 589 590 // expected seals: [F0] <- ... <- [final] <- [B0] <- ... <- [B5] 591 // Note: bs.chain contains seals for blocks [F0]...[A2] followed by seals for [final], [B0]...[B5] 592 bs.Assert().Equal(10, len(bs.assembled.Seals), "unexpected number of seals") 593 bs.Assert().ElementsMatch(bs.chain[:4], bs.assembled.Seals[:4], "should have included only valid chain of seals") 594 bs.Assert().ElementsMatch(bs.chain[8:], bs.assembled.Seals[4:], "should have included only valid chain of seals") 595 596 bs.Assert().Empty(bs.assembled.Guarantees, "should have no guarantees in payload with empty mempool") 597 } 598 599 // TestPayloadSeals_EnforceGap checks that builder leaves a 1-block gap between block incorporating the result 600 // and the block sealing the result. Without this gap, some nodes might not be able to compute the Verifier 601 // assignment for the seal and therefore reject the block. This edge case only occurs in a very specific situation: 602 // 603 // ┌---- [A5] (orphaned fork) 604 // v 605 // ...<- [B0] <- [B1] <- [B2] <- [B3] <- [B4{incorporates result R for B1}] <- ░newBlock░ 606 // 607 // SCENARIO: 608 // - block B0 is sealed 609 // Proposer for ░newBlock░ knows block A5. Hence, it knows a QC for block B4, which contains the Source Of Randomness (SOR) for B4. 610 // Therefore, the proposer can construct the verifier assignment for [B4{incorporates result R for B1}] 611 // - Assume that verification was fast enough, so the proposer has sufficient approvals for result R. 612 // Therefore, the proposer has a candidate seal, sealing result R for block B4, in its mempool. 613 // 614 // Replica trying to verify ░newBlock░: 615 // 616 // - Assume that the replica does _not_ know A5. Therefore, it _cannot_ compute the verifier assignment for B4. 617 // 618 // Problem: If the proposer included the seal for B1, the replica could not check it. 619 // Solution: There must be a gap between the block incorporating the result (here B4) and 620 // the block sealing the result. A gap of one block is sufficient. 621 // 622 // ┌---- [A5] (orphaned fork) 623 // v 624 // ...<- [B0] <- [B1] <- [B2] <- [B3] <- [B4{incorporates result R for B1}] <- [B5] <- [B6{seals B1}] 625 // ~~~~~~ 626 // gap 627 // 628 // We test the two distinct cases: 629 // 630 // (i) Builder does _not_ include seal for B1 when constructing block B5 631 // (ii) Builder _includes_ seal for B1 when constructing block B6 632 func (bs *BuilderSuite) TestPayloadSeals_EnforceGap() { 633 // we use bs.parentID as block B0 634 b0result := bs.resultForBlock[bs.parentID] 635 b0seal := unittest.Seal.Fixture(unittest.Seal.WithResult(b0result)) 636 637 // create blocks B1 to B4: 638 b1 := bs.createAndRecordBlock(bs.blocks[bs.parentID], true) 639 bchain := unittest.ChainFixtureFrom(3, b1.Header) // creates blocks b2, b3, b4 640 b4 := bchain[2] 641 642 // Incorporate result for block B1 into payload of block B4 643 resultB1 := bs.resultForBlock[b1.ID()] 644 receiptB1 := unittest.ExecutionReceiptFixture(unittest.WithResult(resultB1)) 645 b4.SetPayload( 646 flow.Payload{ 647 Results: []*flow.ExecutionResult{&receiptB1.ExecutionResult}, 648 Receipts: []*flow.ExecutionReceiptMeta{receiptB1.Meta()}, 649 }) 650 651 // add blocks B2, B3, B4, A5 to the mocked storage layer (block b0 and b1 are already added): 652 a5 := unittest.BlockWithParentFixture(b4.Header) 653 for _, b := range append(bchain, a5) { 654 bs.storeBlock(b) 655 } 656 657 // mock for of candidate seal mempool: 658 bs.pendingSeals = make(map[flow.Identifier]*flow.IncorporatedResultSeal) 659 b1seal := storeSealForIncorporatedResult(resultB1, b4.ID(), bs.pendingSeals) 660 661 // mock for seals storage layer: 662 bs.sealDB = &storage.Seals{} 663 bs.build.seals = bs.sealDB 664 665 bs.T().Run("Build on top of B4 and check that no seals are included", func(t *testing.T) { 666 bs.sealDB.On("HighestInFork", b4.ID()).Return(b0seal, nil) 667 668 _, err := bs.build.BuildOn(b4.ID(), bs.setter, bs.sign) 669 require.NoError(t, err) 670 bs.recPool.AssertExpectations(t) 671 require.Empty(t, bs.assembled.Seals, "should not include any seals") 672 }) 673 674 bs.T().Run("Build on top of B5 and check that seals for B1 is included", func(t *testing.T) { 675 b5 := unittest.BlockWithParentFixture(b4.Header) // creating block b5 676 bs.storeBlock(b5) 677 bs.sealDB.On("HighestInFork", b5.ID()).Return(b0seal, nil) 678 679 _, err := bs.build.BuildOn(b5.ID(), bs.setter, bs.sign) 680 require.NoError(t, err) 681 bs.recPool.AssertExpectations(t) 682 require.Equal(t, 1, len(bs.assembled.Seals), "only seal for B1 expected") 683 require.Equal(t, b1seal.Seal, bs.assembled.Seals[0]) 684 }) 685 } 686 687 // TestPayloadSeals_Duplicates verifies that the builder does not duplicate seals for already sealed blocks: 688 // 689 // ... <- [F0] <- [F1] <- [F2] <- [F3] <- [A0] <- [A1] <- [A2] <- [A3] 690 // 691 // Where block 692 // - [F0] ... [F3] sealed blocks but their candidate seals are still included in mempool 693 // - [A0] ... [A3] unsealed blocks with candidate seals are included in mempool 694 // 695 // Expected behaviour: 696 // - builder should only include seals [A0], ..., [A3] 697 func (bs *BuilderSuite) TestPayloadSeals_Duplicate() { 698 // Pretend that the first n blocks are already sealed 699 n := 4 700 lastSeal := bs.chain[n-1] 701 mockSealDB := &storage.Seals{} 702 mockSealDB.On("HighestInFork", mock.Anything).Return(lastSeal, nil) 703 bs.build.seals = mockSealDB 704 705 // seals for all blocks [F0], ..., [A3] are still in the mempool: 706 bs.pendingSeals = bs.irsMap 707 708 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 709 bs.Require().NoError(err) 710 bs.Assert().Equal(bs.chain[n:], bs.assembled.Seals, "should have rejected duplicate seals") 711 } 712 713 // TestPayloadSeals_MissingNextSeal checks how the builder handles the fork 714 // 715 // [S] <- [F0] <- [F1] <- [F2] <- [F3] <- [A0] <- [A1] <- [A2] <- [A3] 716 // 717 // Where block 718 // - [S] is sealed and finalized 719 // - [F0] finalized, unsealed block but _without_ candidate seal in mempool 720 // - [F1] ... [F3] are finalized, unsealed blocks with candidate seals are included in mempool 721 // - [A0] ... [A3] non-finalized, unsealed blocks with candidate seals are included in mempool 722 // 723 // Expected behaviour: 724 // - builder should not include any seals as the immediately next seal is not in mempool 725 func (bs *BuilderSuite) TestPayloadSeals_MissingNextSeal() { 726 // remove the seal for block [F0] 727 firstSeal := bs.irsList[0] 728 delete(bs.irsMap, firstSeal.ID()) 729 bs.pendingSeals = bs.irsMap 730 731 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 732 bs.Require().NoError(err) 733 bs.Assert().Empty(bs.assembled.Guarantees, "should have no guarantees in payload with empty mempool") 734 bs.Assert().Empty(bs.assembled.Seals, "should not have included any seals from cutoff chain") 735 } 736 737 // TestPayloadSeals_MissingInterimSeal checks how the builder handles the fork 738 // 739 // [S] <- [F0] <- [F1] <- [F2] <- [F3] <- [A0] <- [A1] <- [A2] <- [A3] 740 // 741 // Where block 742 // - [S] is sealed and finalized 743 // - [F0] ... [F2] are finalized, unsealed blocks with candidate seals are included in mempool 744 // - [F4] finalized, unsealed block but _without_ candidate seal in mempool 745 // - [A0] ... [A3] non-finalized, unsealed blocks with candidate seals are included in mempool 746 // 747 // Expected behaviour: 748 // - builder should only include candidate seals for [F0], [F1], [F2] 749 func (bs *BuilderSuite) TestPayloadSeals_MissingInterimSeal() { 750 // remove a seal for block [F4] 751 seal := bs.irsList[3] 752 delete(bs.irsMap, seal.ID()) 753 bs.pendingSeals = bs.irsMap 754 755 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 756 bs.Require().NoError(err) 757 bs.Assert().Empty(bs.assembled.Guarantees, "should have no guarantees in payload with empty mempool") 758 bs.Assert().ElementsMatch(bs.chain[:3], bs.assembled.Seals, "should have included only beginning of broken chain") 759 } 760 761 // TestValidatePayloadSeals_ExecutionForks checks how the builder's seal-inclusion logic 762 // handles execution forks. 763 // 764 // we have the chain in storage: 765 // 766 // F <- A{Result[F]_1, Result[F]_2, ReceiptMeta[F]_1, ReceiptMeta[F]_2} 767 // <- B{Result[A]_1, Result[A]_2, ReceiptMeta[A]_1, ReceiptMeta[A]_2} 768 // <- C{Result[B]_1, Result[B]_2, ReceiptMeta[B]_1, ReceiptMeta[B]_2} 769 // <- D{Seal for Result[F]_1} 770 // 771 // here F is the latest finalized block (with ID bs.finalID) 772 // 773 // Note that we are explicitly testing the handling of an execution fork that 774 // was incorporated _before_ the seal 775 // 776 // Blocks: F <----------- A <----------- B 777 // Results: Result[F]_1 <- Result[A]_1 <- Result[B]_1 :: the root of this execution tree is sealed 778 // Result[F]_2 <- Result[A]_2 <- Result[B]_2 :: the root of this execution tree conflicts with sealed result 779 // 780 // The builder is tasked with creating the payload for block X: 781 // 782 // F <- A{..} <- B{..} <- C{..} <- D{..} <- X 783 // 784 // We test the two distinct cases: 785 // 786 // (i) verify that execution fork conflicting with sealed result is not sealed 787 // (ii) verify that multiple execution forks are properly handled 788 func (bs *BuilderSuite) TestValidatePayloadSeals_ExecutionForks() { 789 bs.build.cfg.expiry = 4 // reduce expiry so collection dedup algorithm doesn't walk past [lastSeal] 790 791 blockF := bs.blocks[bs.finalID] 792 blocks := []*flow.Block{blockF} 793 blocks = append(blocks, unittest.ChainFixtureFrom(4, blockF.Header)...) // elements [F, A, B, C, D] 794 receiptChain1 := unittest.ReceiptChainFor(blocks, unittest.ExecutionResultFixture()) // elements [Result[F]_1, Result[A]_1, Result[B]_1, ...] 795 receiptChain2 := unittest.ReceiptChainFor(blocks, unittest.ExecutionResultFixture()) // elements [Result[F]_2, Result[A]_2, Result[B]_2, ...] 796 797 for i := 1; i <= 3; i++ { // set payload for blocks A, B, C 798 blocks[i].SetPayload(flow.Payload{ 799 Results: []*flow.ExecutionResult{&receiptChain1[i-1].ExecutionResult, &receiptChain2[i-1].ExecutionResult}, 800 Receipts: []*flow.ExecutionReceiptMeta{receiptChain1[i-1].Meta(), receiptChain2[i-1].Meta()}, 801 }) 802 } 803 sealedResult := receiptChain1[0].ExecutionResult 804 sealF := unittest.Seal.Fixture(unittest.Seal.WithResult(&sealedResult)) 805 blocks[4].SetPayload(flow.Payload{ // set payload for block D 806 Seals: []*flow.Seal{sealF}, 807 }) 808 for i := 0; i <= 4; i++ { 809 // we need to run this several times, as in each iteration as we have _multiple_ execution chains. 810 // In each iteration, we only mange to reconnect one additional height 811 unittest.ReconnectBlocksAndReceipts(blocks, receiptChain1) 812 unittest.ReconnectBlocksAndReceipts(blocks, receiptChain2) 813 } 814 815 for _, b := range blocks { 816 bs.storeBlock(b) 817 } 818 bs.sealDB = &storage.Seals{} 819 bs.build.seals = bs.sealDB 820 bs.sealDB.On("HighestInFork", mock.Anything).Return(sealF, nil) 821 bs.resultByID[sealedResult.ID()] = &sealedResult 822 823 bs.T().Run("verify that execution fork conflicting with sealed result is not sealed", func(t *testing.T) { 824 bs.pendingSeals = make(map[flow.Identifier]*flow.IncorporatedResultSeal) 825 storeSealForIncorporatedResult(&receiptChain2[1].ExecutionResult, blocks[2].ID(), bs.pendingSeals) 826 827 _, err := bs.build.BuildOn(blocks[4].ID(), bs.setter, bs.sign) 828 require.NoError(t, err) 829 require.Empty(t, bs.assembled.Seals, "should not have included seal for conflicting execution fork") 830 }) 831 832 bs.T().Run("verify that multiple execution forks are properly handled", func(t *testing.T) { 833 bs.pendingSeals = make(map[flow.Identifier]*flow.IncorporatedResultSeal) 834 sealResultA_1 := storeSealForIncorporatedResult(&receiptChain1[1].ExecutionResult, blocks[2].ID(), bs.pendingSeals) 835 sealResultB_1 := storeSealForIncorporatedResult(&receiptChain1[2].ExecutionResult, blocks[3].ID(), bs.pendingSeals) 836 storeSealForIncorporatedResult(&receiptChain2[1].ExecutionResult, blocks[2].ID(), bs.pendingSeals) 837 storeSealForIncorporatedResult(&receiptChain2[2].ExecutionResult, blocks[3].ID(), bs.pendingSeals) 838 839 _, err := bs.build.BuildOn(blocks[4].ID(), bs.setter, bs.sign) 840 require.NoError(t, err) 841 require.ElementsMatch(t, []*flow.Seal{sealResultA_1.Seal, sealResultB_1.Seal}, bs.assembled.Seals, "valid fork should have been sealed") 842 }) 843 } 844 845 // TestPayloadReceipts_TraverseExecutionTreeFromLastSealedResult tests the receipt selection: 846 // Expectation: Builder should trigger ExecutionTree to search Execution Tree from 847 // last sealed result on respective fork. 848 // 849 // We test with the following main chain tree 850 // 851 // ┌-[X0] <- [X1{seals ..F4}] 852 // v 853 // [lastSeal] <- [F0] <- [F1] <- [F2] <- [F3] <- [F4] <- [A0] <- [A1{seals ..F2}] <- [A2] <- [A3] 854 // 855 // Where 856 // * blocks [lastSeal], [F1], ... [F4], [A0], ... [A4], are created by BuilderSuite 857 // * latest sealed block for a specific fork is provided by test-local seals storage mock 858 func (bs *BuilderSuite) TestPayloadReceipts_TraverseExecutionTreeFromLastSealedResult() { 859 bs.build.cfg.expiry = 4 // reduce expiry so collection dedup algorithm doesn't walk past [lastSeal] 860 x0 := bs.createAndRecordBlock(bs.blocks[bs.finalID], true) 861 x1 := bs.createAndRecordBlock(x0, true) 862 863 // set last sealed blocks: 864 f2 := bs.blocks[bs.finalizedBlockIDs[2]] 865 f2eal := unittest.Seal.Fixture(unittest.Seal.WithResult(bs.resultForBlock[f2.ID()])) 866 f4Seal := unittest.Seal.Fixture(unittest.Seal.WithResult(bs.resultForBlock[bs.finalID])) 867 bs.sealDB = &storage.Seals{} 868 bs.build.seals = bs.sealDB 869 870 // reset receipts mempool to verify calls made by Builder 871 bs.recPool = &mempool.ExecutionTree{} 872 bs.recPool.On("Size").Return(uint(0)).Maybe() 873 bs.build.recPool = bs.recPool 874 875 // building on top of X0: latest finalized block in fork is [lastSeal]; expect search to start with sealed result 876 bs.sealDB.On("HighestInFork", x0.ID()).Return(bs.lastSeal, nil) 877 bs.recPool.On("ReachableReceipts", bs.lastSeal.ResultID, mock.Anything, mock.Anything).Return([]*flow.ExecutionReceipt{}, nil).Once() 878 _, err := bs.build.BuildOn(x0.ID(), bs.setter, bs.sign) 879 bs.Require().NoError(err) 880 bs.recPool.AssertExpectations(bs.T()) 881 882 // building on top of X1: latest finalized block in fork is [F4]; expect search to start with sealed result 883 bs.sealDB.On("HighestInFork", x1.ID()).Return(f4Seal, nil) 884 bs.recPool.On("ReachableReceipts", f4Seal.ResultID, mock.Anything, mock.Anything).Return([]*flow.ExecutionReceipt{}, nil).Once() 885 _, err = bs.build.BuildOn(x1.ID(), bs.setter, bs.sign) 886 bs.Require().NoError(err) 887 bs.recPool.AssertExpectations(bs.T()) 888 889 // building on top of A3 (with ID bs.parentID): latest finalized block in fork is [F4]; expect search to start with sealed result 890 bs.sealDB.On("HighestInFork", bs.parentID).Return(f2eal, nil) 891 bs.recPool.On("ReachableReceipts", f2eal.ResultID, mock.Anything, mock.Anything).Return([]*flow.ExecutionReceipt{}, nil).Once() 892 _, err = bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 893 bs.Require().NoError(err) 894 bs.recPool.AssertExpectations(bs.T()) 895 } 896 897 // TestPayloadReceipts_IncludeOnlyReceiptsForCurrentFork tests the receipt selection: 898 // In this test, we check that the Builder provides a BlockFilter which only allows 899 // blocks on the fork, which we are extending. We construct the following chain tree: 900 // 901 // ┌--[X1] ┌-[Y2] ┌-- [A6] 902 // v v v 903 // <- [Final] <- [*B1*] <- [*B2*] <- [*B3*] <- [*B4{seals B1}*] <- [*B5*] <- ░newBlock░ 904 // ^ 905 // └-- [C3] <- [C4] 906 // ^--- [D4] 907 // 908 // Expectation: BlockFilter should pass blocks marked with star: B1, ... ,B5 909 // All other blocks should be filtered out. 910 // 911 // Context: 912 // While the receipt selection itself is performed by the ExecutionTree, the Builder 913 // controls the selection by providing suitable BlockFilter and ReceiptFilter. 914 func (bs *BuilderSuite) TestPayloadReceipts_IncludeOnlyReceiptsForCurrentFork() { 915 b1 := bs.createAndRecordBlock(bs.blocks[bs.finalID], true) 916 b2 := bs.createAndRecordBlock(b1, true) 917 b3 := bs.createAndRecordBlock(b2, true) 918 b4 := bs.createAndRecordBlock(b3, true) 919 b5 := bs.createAndRecordBlock(b4, true) 920 921 x1 := bs.createAndRecordBlock(bs.blocks[bs.finalID], true) 922 y2 := bs.createAndRecordBlock(b1, true) 923 a6 := bs.createAndRecordBlock(b5, true) 924 925 c3 := bs.createAndRecordBlock(b2, true) 926 c4 := bs.createAndRecordBlock(c3, true) 927 d4 := bs.createAndRecordBlock(c3, true) 928 929 // set last sealed blocks: 930 b1Seal := unittest.Seal.Fixture(unittest.Seal.WithResult(bs.resultForBlock[b1.ID()])) 931 bs.sealDB = &storage.Seals{} 932 bs.sealDB.On("HighestInFork", b5.ID()).Return(b1Seal, nil) 933 bs.build.seals = bs.sealDB 934 935 // setup mock to test the BlockFilter provided by Builder 936 bs.recPool = &mempool.ExecutionTree{} 937 bs.recPool.On("Size").Return(uint(0)).Maybe() 938 bs.recPool.On("ReachableReceipts", b1Seal.ResultID, mock.Anything, mock.Anything).Run( 939 func(args mock.Arguments) { 940 blockFilter := args[1].(mempoolAPIs.BlockFilter) 941 for _, h := range []*flow.Header{b1.Header, b2.Header, b3.Header, b4.Header, b5.Header} { 942 assert.True(bs.T(), blockFilter(h)) 943 } 944 for _, h := range []*flow.Header{bs.blocks[bs.finalID].Header, x1.Header, y2.Header, a6.Header, c3.Header, c4.Header, d4.Header} { 945 assert.False(bs.T(), blockFilter(h)) 946 } 947 }).Return([]*flow.ExecutionReceipt{}, nil).Once() 948 bs.build.recPool = bs.recPool 949 950 _, err := bs.build.BuildOn(b5.ID(), bs.setter, bs.sign) 951 bs.Require().NoError(err) 952 bs.recPool.AssertExpectations(bs.T()) 953 } 954 955 // TestPayloadReceipts_SkipDuplicatedReceipts tests the receipt selection: 956 // Expectation: we check that the Builder provides a ReceiptFilter which 957 // filters out duplicated receipts. 958 // Comment: 959 // While the receipt selection itself is performed by the ExecutionTree, the Builder 960 // controls the selection by providing suitable BlockFilter and ReceiptFilter. 961 func (bs *BuilderSuite) TestPayloadReceipts_SkipDuplicatedReceipts() { 962 // setup mock to test the ReceiptFilter provided by Builder 963 bs.recPool = &mempool.ExecutionTree{} 964 bs.recPool.On("Size").Return(uint(0)).Maybe() 965 bs.recPool.On("ReachableReceipts", bs.lastSeal.ResultID, mock.Anything, mock.Anything).Run( 966 func(args mock.Arguments) { 967 receiptFilter := args[2].(mempoolAPIs.ReceiptFilter) 968 // verify that all receipts already included in blocks are filtered out: 969 for _, block := range bs.blocks { 970 resultByID := block.Payload.Results.Lookup() 971 for _, meta := range block.Payload.Receipts { 972 result := resultByID[meta.ResultID] 973 rcpt := flow.ExecutionReceiptFromMeta(*meta, *result) 974 assert.False(bs.T(), receiptFilter(rcpt)) 975 } 976 } 977 // Verify that receipts for unsealed blocks, which are _not_ already incorporated are accepted: 978 for _, block := range bs.blocks { 979 if block.ID() != bs.firstID { // block with ID bs.firstID is already sealed 980 rcpt := unittest.ReceiptForBlockFixture(block) 981 assert.True(bs.T(), receiptFilter(rcpt)) 982 } 983 } 984 }).Return([]*flow.ExecutionReceipt{}, nil).Once() 985 bs.build.recPool = bs.recPool 986 987 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 988 bs.Require().NoError(err) 989 bs.recPool.AssertExpectations(bs.T()) 990 } 991 992 // TestPayloadReceipts_SkipReceiptsForSealedBlock tests the receipt selection: 993 // Expectation: we check that the Builder provides a ReceiptFilter which 994 // filters out _any_ receipt for the sealed block. 995 // 996 // Comment: 997 // While the receipt selection itself is performed by the ExecutionTree, the Builder 998 // controls the selection by providing suitable BlockFilter and ReceiptFilter. 999 func (bs *BuilderSuite) TestPayloadReceipts_SkipReceiptsForSealedBlock() { 1000 // setup mock to test the ReceiptFilter provided by Builder 1001 bs.recPool = &mempool.ExecutionTree{} 1002 bs.recPool.On("Size").Return(uint(0)).Maybe() 1003 bs.recPool.On("ReachableReceipts", bs.lastSeal.ResultID, mock.Anything, mock.Anything).Run( 1004 func(args mock.Arguments) { 1005 receiptFilter := args[2].(mempoolAPIs.ReceiptFilter) 1006 1007 // receipt for sealed block committing to same result as the sealed result 1008 rcpt := unittest.ExecutionReceiptFixture(unittest.WithResult(bs.resultForBlock[bs.firstID])) 1009 assert.False(bs.T(), receiptFilter(rcpt)) 1010 1011 // receipt for sealed block committing to different result as the sealed result 1012 rcpt = unittest.ReceiptForBlockFixture(bs.blocks[bs.firstID]) 1013 assert.False(bs.T(), receiptFilter(rcpt)) 1014 }).Return([]*flow.ExecutionReceipt{}, nil).Once() 1015 bs.build.recPool = bs.recPool 1016 1017 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 1018 bs.Require().NoError(err) 1019 bs.recPool.AssertExpectations(bs.T()) 1020 } 1021 1022 // TestPayloadReceipts_BlockLimit tests that the builder does not include more 1023 // receipts than the configured maxReceiptCount. 1024 func (bs *BuilderSuite) TestPayloadReceipts_BlockLimit() { 1025 1026 // Populate the mempool with 5 valid receipts 1027 receipts := []*flow.ExecutionReceipt{} 1028 metas := []*flow.ExecutionReceiptMeta{} 1029 expectedResults := []*flow.ExecutionResult{} 1030 var i uint64 1031 for i = 0; i < 5; i++ { 1032 blockOnFork := bs.blocks[bs.irsList[i].Seal.BlockID] 1033 pendingReceipt := unittest.ReceiptForBlockFixture(blockOnFork) 1034 receipts = append(receipts, pendingReceipt) 1035 metas = append(metas, pendingReceipt.Meta()) 1036 expectedResults = append(expectedResults, &pendingReceipt.ExecutionResult) 1037 } 1038 bs.pendingReceipts = receipts 1039 1040 // set maxReceiptCount to 3 1041 var limit uint = 3 1042 bs.build.cfg.maxReceiptCount = limit 1043 1044 // ensure that only 3 of the 5 receipts were included 1045 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 1046 bs.Require().NoError(err) 1047 bs.Assert().ElementsMatch(metas[:limit], bs.assembled.Receipts, "should have excluded receipts above maxReceiptCount") 1048 bs.Assert().ElementsMatch(expectedResults[:limit], bs.assembled.Results, "should have excluded results above maxReceiptCount") 1049 } 1050 1051 // TestPayloadReceipts_AsProvidedByReceiptForest tests the receipt selection. 1052 // Expectation: Builder should embed the Receipts as provided by the ExecutionTree 1053 func (bs *BuilderSuite) TestPayloadReceipts_AsProvidedByReceiptForest() { 1054 var expectedReceipts []*flow.ExecutionReceipt 1055 var expectedMetas []*flow.ExecutionReceiptMeta 1056 var expectedResults []*flow.ExecutionResult 1057 for i := 0; i < 10; i++ { 1058 expectedReceipts = append(expectedReceipts, unittest.ExecutionReceiptFixture()) 1059 expectedMetas = append(expectedMetas, expectedReceipts[i].Meta()) 1060 expectedResults = append(expectedResults, &expectedReceipts[i].ExecutionResult) 1061 } 1062 bs.recPool = &mempool.ExecutionTree{} 1063 bs.recPool.On("Size").Return(uint(0)).Maybe() 1064 bs.recPool.On("AddResult", mock.Anything, mock.Anything).Return(nil).Maybe() 1065 bs.recPool.On("ReachableReceipts", mock.Anything, mock.Anything, mock.Anything).Return(expectedReceipts, nil).Once() 1066 bs.build.recPool = bs.recPool 1067 1068 _, err := bs.build.BuildOn(bs.parentID, bs.setter, bs.sign) 1069 bs.Require().NoError(err) 1070 bs.Assert().ElementsMatch(expectedMetas, bs.assembled.Receipts, "should include receipts as returned by ExecutionTree") 1071 bs.Assert().ElementsMatch(expectedResults, bs.assembled.Results, "should include results as returned by ExecutionTree") 1072 bs.recPool.AssertExpectations(bs.T()) 1073 } 1074 1075 // TestIntegration_PayloadReceiptNoParentResult is a mini-integration test combining the 1076 // Builder with a full ExecutionTree mempool. We check that the builder does not include 1077 // receipts whose PreviousResult is not already incorporated in the chain. 1078 // 1079 // Here we create 4 consecutive blocks S, A, B, and C, where A contains a valid 1080 // receipt for block S, but blocks B and C have empty payloads. 1081 // 1082 // We populate the mempool with valid receipts for blocks A, and C, but NOT for 1083 // block B. 1084 // 1085 // The expected behaviour is that the builder should not include the receipt for 1086 // block C, because the chain and the mempool do not contain a valid receipt for 1087 // the parent result (block B's result). 1088 // 1089 // ... <- S[ER{parent}] <- A[ER{S}] <- B <- C <- X (candidate) 1090 func (bs *BuilderSuite) TestIntegration_PayloadReceiptNoParentResult() { 1091 // make blocks S, A, B, C 1092 parentReceipt := unittest.ExecutionReceiptFixture(unittest.WithResult(bs.resultForBlock[bs.parentID])) 1093 blockSABC := unittest.ChainFixtureFrom(4, bs.blocks[bs.parentID].Header) 1094 resultS := unittest.ExecutionResultFixture(unittest.WithBlock(blockSABC[0]), unittest.WithPreviousResult(*bs.resultForBlock[bs.parentID])) 1095 receiptSABC := unittest.ReceiptChainFor(blockSABC, resultS) 1096 blockSABC[0].Payload.Receipts = []*flow.ExecutionReceiptMeta{parentReceipt.Meta()} 1097 blockSABC[0].Payload.Results = []*flow.ExecutionResult{&parentReceipt.ExecutionResult} 1098 blockSABC[1].Payload.Receipts = []*flow.ExecutionReceiptMeta{receiptSABC[0].Meta()} 1099 blockSABC[1].Payload.Results = []*flow.ExecutionResult{&receiptSABC[0].ExecutionResult} 1100 blockSABC[2].Payload.Receipts = []*flow.ExecutionReceiptMeta{} 1101 blockSABC[3].Payload.Receipts = []*flow.ExecutionReceiptMeta{} 1102 unittest.ReconnectBlocksAndReceipts(blockSABC, receiptSABC) // update block header so that blocks are chained together 1103 1104 bs.storeBlock(blockSABC[0]) 1105 bs.storeBlock(blockSABC[1]) 1106 bs.storeBlock(blockSABC[2]) 1107 bs.storeBlock(blockSABC[3]) 1108 1109 // Instantiate real Execution Tree mempool; 1110 bs.build.recPool = mempoolImpl.NewExecutionTree() 1111 for _, block := range bs.blocks { 1112 resultByID := block.Payload.Results.Lookup() 1113 for _, meta := range block.Payload.Receipts { 1114 result := resultByID[meta.ResultID] 1115 rcpt := flow.ExecutionReceiptFromMeta(*meta, *result) 1116 _, err := bs.build.recPool.AddReceipt(rcpt, bs.blocks[rcpt.ExecutionResult.BlockID].Header) 1117 bs.NoError(err) 1118 } 1119 } 1120 // for receipts _not_ included in blocks, add only receipt for A and C but NOT B 1121 _, _ = bs.build.recPool.AddReceipt(receiptSABC[1], blockSABC[1].Header) 1122 _, _ = bs.build.recPool.AddReceipt(receiptSABC[3], blockSABC[3].Header) 1123 1124 _, err := bs.build.BuildOn(blockSABC[3].ID(), bs.setter, bs.sign) 1125 bs.Require().NoError(err) 1126 expectedReceipts := flow.ExecutionReceiptMetaList{receiptSABC[1].Meta()} 1127 expectedResults := flow.ExecutionResultList{&receiptSABC[1].ExecutionResult} 1128 bs.Assert().Equal(expectedReceipts, bs.assembled.Receipts, "payload should contain only receipt for block a") 1129 bs.Assert().ElementsMatch(expectedResults, bs.assembled.Results, "payload should contain only result for block a") 1130 } 1131 1132 // TestIntegration_ExtendDifferentExecutionPathsOnSameFork tests that the 1133 // builder includes receipts that form different valid execution paths contained 1134 // on the current fork. 1135 // 1136 // candidate 1137 // P <- A[ER{P}] <- B[ER{A}, ER{A}'] <- X[ER{B}, ER{B}'] 1138 func (bs *BuilderSuite) TestIntegration_ExtendDifferentExecutionPathsOnSameFork() { 1139 1140 // A is a block containing a valid receipt for block P 1141 recP := unittest.ExecutionReceiptFixture(unittest.WithResult(bs.resultForBlock[bs.parentID])) 1142 A := unittest.BlockWithParentFixture(bs.headers[bs.parentID]) 1143 A.SetPayload(flow.Payload{ 1144 Receipts: []*flow.ExecutionReceiptMeta{recP.Meta()}, 1145 Results: []*flow.ExecutionResult{&recP.ExecutionResult}, 1146 }) 1147 1148 // B is a block containing two valid receipts, with different results, for 1149 // block A 1150 resA1 := unittest.ExecutionResultFixture(unittest.WithBlock(A), unittest.WithPreviousResult(recP.ExecutionResult)) 1151 recA1 := unittest.ExecutionReceiptFixture(unittest.WithResult(resA1)) 1152 resA2 := unittest.ExecutionResultFixture(unittest.WithBlock(A), unittest.WithPreviousResult(recP.ExecutionResult)) 1153 recA2 := unittest.ExecutionReceiptFixture(unittest.WithResult(resA2)) 1154 B := unittest.BlockWithParentFixture(A.Header) 1155 B.SetPayload(flow.Payload{ 1156 Receipts: []*flow.ExecutionReceiptMeta{recA1.Meta(), recA2.Meta()}, 1157 Results: []*flow.ExecutionResult{&recA1.ExecutionResult, &recA2.ExecutionResult}, 1158 }) 1159 1160 bs.storeBlock(A) 1161 bs.storeBlock(B) 1162 1163 // Instantiate real Execution Tree mempool; 1164 bs.build.recPool = mempoolImpl.NewExecutionTree() 1165 for _, block := range bs.blocks { 1166 resultByID := block.Payload.Results.Lookup() 1167 for _, meta := range block.Payload.Receipts { 1168 result := resultByID[meta.ResultID] 1169 rcpt := flow.ExecutionReceiptFromMeta(*meta, *result) 1170 _, err := bs.build.recPool.AddReceipt(rcpt, bs.blocks[rcpt.ExecutionResult.BlockID].Header) 1171 bs.NoError(err) 1172 } 1173 } 1174 1175 // Create two valid receipts for block B which build on different receipts 1176 // for the parent block (A); recB1 builds on top of RecA1, whilst recB2 1177 // builds on top of RecA2. 1178 resB1 := unittest.ExecutionResultFixture(unittest.WithBlock(B), unittest.WithPreviousResult(recA1.ExecutionResult)) 1179 recB1 := unittest.ExecutionReceiptFixture(unittest.WithResult(resB1)) 1180 resB2 := unittest.ExecutionResultFixture(unittest.WithBlock(B), unittest.WithPreviousResult(recA2.ExecutionResult)) 1181 recB2 := unittest.ExecutionReceiptFixture(unittest.WithResult(resB2)) 1182 1183 // Add recB1 and recB2 to the mempool for inclusion in the next candidate 1184 _, _ = bs.build.recPool.AddReceipt(recB1, B.Header) 1185 _, _ = bs.build.recPool.AddReceipt(recB2, B.Header) 1186 1187 _, err := bs.build.BuildOn(B.ID(), bs.setter, bs.sign) 1188 bs.Require().NoError(err) 1189 expectedReceipts := flow.ExecutionReceiptMetaList{recB1.Meta(), recB2.Meta()} 1190 expectedResults := flow.ExecutionResultList{&recB1.ExecutionResult, &recB2.ExecutionResult} 1191 bs.Assert().Equal(expectedReceipts, bs.assembled.Receipts, "payload should contain receipts from valid execution forks") 1192 bs.Assert().ElementsMatch(expectedResults, bs.assembled.Results, "payload should contain results from valid execution forks") 1193 } 1194 1195 // TestIntegration_ExtendDifferentExecutionPathsOnDifferentForks tests that the 1196 // builder picks up receipts that were already included in a different fork. 1197 // 1198 // candidate 1199 // P <- A[ER{P}] <- B[ER{A}] <- X[ER{A}',ER{B}, ER{B}'] 1200 // | 1201 // < ------ C[ER{A}'] 1202 // 1203 // Where: 1204 // - ER{A} and ER{A}' are receipts for block A that don't have the same 1205 // result. 1206 // - ER{B} is a receipt for B with parent result ER{A} 1207 // - ER{B}' is a receipt for B with parent result ER{A}' 1208 // 1209 // When buiding on top of B, we expect the candidate payload to contain ER{A}', 1210 // ER{B}, and ER{B}' 1211 // 1212 // ER{P} <- ER{A} <- ER{B} 1213 // | 1214 // < ER{A}' <- ER{B}' 1215 func (bs *BuilderSuite) TestIntegration_ExtendDifferentExecutionPathsOnDifferentForks() { 1216 // A is a block containing a valid receipt for block P 1217 recP := unittest.ExecutionReceiptFixture(unittest.WithResult(bs.resultForBlock[bs.parentID])) 1218 A := unittest.BlockWithParentFixture(bs.headers[bs.parentID]) 1219 A.SetPayload(flow.Payload{ 1220 Receipts: []*flow.ExecutionReceiptMeta{recP.Meta()}, 1221 Results: []*flow.ExecutionResult{&recP.ExecutionResult}, 1222 }) 1223 1224 // B is a block that builds on A containing a valid receipt for A 1225 resA1 := unittest.ExecutionResultFixture(unittest.WithBlock(A), unittest.WithPreviousResult(recP.ExecutionResult)) 1226 recA1 := unittest.ExecutionReceiptFixture(unittest.WithResult(resA1)) 1227 B := unittest.BlockWithParentFixture(A.Header) 1228 B.SetPayload(flow.Payload{ 1229 Receipts: []*flow.ExecutionReceiptMeta{recA1.Meta()}, 1230 Results: []*flow.ExecutionResult{&recA1.ExecutionResult}, 1231 }) 1232 1233 // C is another block that builds on A containing a valid receipt for A but 1234 // different from the receipt contained in B 1235 resA2 := unittest.ExecutionResultFixture(unittest.WithBlock(A), unittest.WithPreviousResult(recP.ExecutionResult)) 1236 recA2 := unittest.ExecutionReceiptFixture(unittest.WithResult(resA2)) 1237 C := unittest.BlockWithParentFixture(A.Header) 1238 C.SetPayload(flow.Payload{ 1239 Receipts: []*flow.ExecutionReceiptMeta{recA2.Meta()}, 1240 Results: []*flow.ExecutionResult{&recA2.ExecutionResult}, 1241 }) 1242 1243 bs.storeBlock(A) 1244 bs.storeBlock(B) 1245 bs.storeBlock(C) 1246 1247 // Instantiate real Execution Tree mempool; 1248 bs.build.recPool = mempoolImpl.NewExecutionTree() 1249 for _, block := range bs.blocks { 1250 resultByID := block.Payload.Results.Lookup() 1251 for _, meta := range block.Payload.Receipts { 1252 result := resultByID[meta.ResultID] 1253 rcpt := flow.ExecutionReceiptFromMeta(*meta, *result) 1254 _, err := bs.build.recPool.AddReceipt(rcpt, bs.blocks[rcpt.ExecutionResult.BlockID].Header) 1255 bs.NoError(err) 1256 } 1257 } 1258 1259 // create and add a receipt for block B which builds on top of recA2, which 1260 // is not on the same execution fork 1261 resB1 := unittest.ExecutionResultFixture(unittest.WithBlock(B), unittest.WithPreviousResult(recA1.ExecutionResult)) 1262 recB1 := unittest.ExecutionReceiptFixture(unittest.WithResult(resB1)) 1263 resB2 := unittest.ExecutionResultFixture(unittest.WithBlock(B), unittest.WithPreviousResult(recA2.ExecutionResult)) 1264 recB2 := unittest.ExecutionReceiptFixture(unittest.WithResult(resB2)) 1265 1266 _, err := bs.build.recPool.AddReceipt(recB1, B.Header) 1267 bs.Require().NoError(err) 1268 _, err = bs.build.recPool.AddReceipt(recB2, B.Header) 1269 bs.Require().NoError(err) 1270 1271 _, err = bs.build.BuildOn(B.ID(), bs.setter, bs.sign) 1272 bs.Require().NoError(err) 1273 expectedReceipts := []*flow.ExecutionReceiptMeta{recA2.Meta(), recB1.Meta(), recB2.Meta()} 1274 expectedResults := []*flow.ExecutionResult{&recA2.ExecutionResult, &recB1.ExecutionResult, &recB2.ExecutionResult} 1275 bs.Assert().ElementsMatch(expectedReceipts, bs.assembled.Receipts, "builder should extend different execution paths") 1276 bs.Assert().ElementsMatch(expectedResults, bs.assembled.Results, "builder should extend different execution paths") 1277 } 1278 1279 // TestIntegration_DuplicateReceipts checks that the builder does not re-include 1280 // receipts that are already incorporated in blocks on the fork. 1281 // 1282 // P <- A(r_P) <- B(r_A) <- X (candidate) 1283 func (bs *BuilderSuite) TestIntegration_DuplicateReceipts() { 1284 // A is a block containing a valid receipt for block P 1285 recP := unittest.ExecutionReceiptFixture(unittest.WithResult(bs.resultForBlock[bs.parentID])) 1286 A := unittest.BlockWithParentFixture(bs.headers[bs.parentID]) 1287 A.SetPayload(flow.Payload{ 1288 Receipts: []*flow.ExecutionReceiptMeta{recP.Meta()}, 1289 Results: []*flow.ExecutionResult{&recP.ExecutionResult}, 1290 }) 1291 1292 // B is a block that builds on A containing a valid receipt for A 1293 resA1 := unittest.ExecutionResultFixture(unittest.WithBlock(A), unittest.WithPreviousResult(recP.ExecutionResult)) 1294 recA1 := unittest.ExecutionReceiptFixture(unittest.WithResult(resA1)) 1295 B := unittest.BlockWithParentFixture(A.Header) 1296 B.SetPayload(flow.Payload{ 1297 Receipts: []*flow.ExecutionReceiptMeta{recA1.Meta()}, 1298 Results: []*flow.ExecutionResult{&recA1.ExecutionResult}, 1299 }) 1300 1301 bs.storeBlock(A) 1302 bs.storeBlock(B) 1303 1304 // Instantiate real Execution Tree mempool; 1305 bs.build.recPool = mempoolImpl.NewExecutionTree() 1306 for _, block := range bs.blocks { 1307 resultByID := block.Payload.Results.Lookup() 1308 for _, meta := range block.Payload.Receipts { 1309 result := resultByID[meta.ResultID] 1310 rcpt := flow.ExecutionReceiptFromMeta(*meta, *result) 1311 _, err := bs.build.recPool.AddReceipt(rcpt, bs.blocks[rcpt.ExecutionResult.BlockID].Header) 1312 bs.NoError(err) 1313 } 1314 } 1315 1316 _, err := bs.build.BuildOn(B.ID(), bs.setter, bs.sign) 1317 bs.Require().NoError(err) 1318 expectedReceipts := []*flow.ExecutionReceiptMeta{} 1319 expectedResults := []*flow.ExecutionResult{} 1320 bs.Assert().ElementsMatch(expectedReceipts, bs.assembled.Receipts, "builder should not include receipts that are already incorporated in the current fork") 1321 bs.Assert().ElementsMatch(expectedResults, bs.assembled.Results, "builder should not include results that were already incorporated") 1322 } 1323 1324 // TestIntegration_ResultAlreadyIncorporated checks that the builder includes 1325 // receipts for results that were already incorporated in blocks on the fork. 1326 // 1327 // P <- A(ER[P]) <- X (candidate) 1328 func (bs *BuilderSuite) TestIntegration_ResultAlreadyIncorporated() { 1329 // A is a block containing a valid receipt for block P 1330 recP := unittest.ExecutionReceiptFixture(unittest.WithResult(bs.resultForBlock[bs.parentID])) 1331 A := unittest.BlockWithParentFixture(bs.headers[bs.parentID]) 1332 A.SetPayload(flow.Payload{ 1333 Receipts: []*flow.ExecutionReceiptMeta{recP.Meta()}, 1334 Results: []*flow.ExecutionResult{&recP.ExecutionResult}, 1335 }) 1336 1337 recP_B := unittest.ExecutionReceiptFixture(unittest.WithResult(&recP.ExecutionResult)) 1338 1339 bs.storeBlock(A) 1340 1341 // Instantiate real Execution Tree mempool; 1342 bs.build.recPool = mempoolImpl.NewExecutionTree() 1343 for _, block := range bs.blocks { 1344 resultByID := block.Payload.Results.Lookup() 1345 for _, meta := range block.Payload.Receipts { 1346 result := resultByID[meta.ResultID] 1347 rcpt := flow.ExecutionReceiptFromMeta(*meta, *result) 1348 _, err := bs.build.recPool.AddReceipt(rcpt, bs.blocks[rcpt.ExecutionResult.BlockID].Header) 1349 bs.NoError(err) 1350 } 1351 } 1352 1353 _, err := bs.build.recPool.AddReceipt(recP_B, bs.blocks[recP_B.ExecutionResult.BlockID].Header) 1354 bs.NoError(err) 1355 1356 _, err = bs.build.BuildOn(A.ID(), bs.setter, bs.sign) 1357 bs.Require().NoError(err) 1358 expectedReceipts := []*flow.ExecutionReceiptMeta{recP_B.Meta()} 1359 expectedResults := []*flow.ExecutionResult{} 1360 bs.Assert().ElementsMatch(expectedReceipts, bs.assembled.Receipts, "builder should include receipt metas for results that were already incorporated") 1361 bs.Assert().ElementsMatch(expectedResults, bs.assembled.Results, "builder should not include results that were already incorporated") 1362 } 1363 1364 func storeSealForIncorporatedResult(result *flow.ExecutionResult, incorporatingBlockID flow.Identifier, pendingSeals map[flow.Identifier]*flow.IncorporatedResultSeal) *flow.IncorporatedResultSeal { 1365 incorporatedResultSeal := unittest.IncorporatedResultSeal.Fixture( 1366 unittest.IncorporatedResultSeal.WithResult(result), 1367 unittest.IncorporatedResultSeal.WithIncorporatedBlockID(incorporatingBlockID), 1368 ) 1369 pendingSeals[incorporatedResultSeal.ID()] = incorporatedResultSeal 1370 return incorporatedResultSeal 1371 } 1372 1373 // TestIntegration_RepopulateExecutionTreeAtStartup tests that the 1374 // builder includes receipts for candidate block after fresh start, meaning 1375 // it will repopulate execution tree in constructor 1376 // 1377 // P <- A[ER{P}] <- B[ER{A}, ER{A}'] <- C <- X[ER{B}, ER{B}', ER{C} ] 1378 // | 1379 // finalized 1380 func (bs *BuilderSuite) TestIntegration_RepopulateExecutionTreeAtStartup() { 1381 // setup initial state 1382 // A is a block containing a valid receipt for block P 1383 recP := unittest.ExecutionReceiptFixture(unittest.WithResult(bs.resultForBlock[bs.parentID])) 1384 A := unittest.BlockWithParentFixture(bs.headers[bs.parentID]) 1385 A.SetPayload(flow.Payload{ 1386 Receipts: []*flow.ExecutionReceiptMeta{recP.Meta()}, 1387 Results: []*flow.ExecutionResult{&recP.ExecutionResult}, 1388 }) 1389 1390 // B is a block containing two valid receipts, with different results, for 1391 // block A 1392 resA1 := unittest.ExecutionResultFixture(unittest.WithBlock(A), unittest.WithPreviousResult(recP.ExecutionResult)) 1393 recA1 := unittest.ExecutionReceiptFixture(unittest.WithResult(resA1)) 1394 resA2 := unittest.ExecutionResultFixture(unittest.WithBlock(A), unittest.WithPreviousResult(recP.ExecutionResult)) 1395 recA2 := unittest.ExecutionReceiptFixture(unittest.WithResult(resA2)) 1396 B := unittest.BlockWithParentFixture(A.Header) 1397 B.SetPayload(flow.Payload{ 1398 Receipts: []*flow.ExecutionReceiptMeta{recA1.Meta(), recA2.Meta()}, 1399 Results: []*flow.ExecutionResult{&recA1.ExecutionResult, &recA2.ExecutionResult}, 1400 }) 1401 1402 C := unittest.BlockWithParentFixture(B.Header) 1403 1404 bs.storeBlock(A) 1405 bs.storeBlock(B) 1406 bs.storeBlock(C) 1407 1408 // store execution results 1409 for _, block := range []*flow.Block{A, B, C} { 1410 // for current block create empty receipts list 1411 bs.receiptsByBlockID[block.ID()] = flow.ExecutionReceiptList{} 1412 1413 for _, result := range block.Payload.Results { 1414 bs.resultByID[result.ID()] = result 1415 } 1416 for _, meta := range block.Payload.Receipts { 1417 receipt := flow.ExecutionReceiptFromMeta(*meta, *bs.resultByID[meta.ResultID]) 1418 bs.receiptsByID[meta.ID()] = receipt 1419 bs.receiptsByBlockID[receipt.ExecutionResult.BlockID] = append(bs.receiptsByBlockID[receipt.ExecutionResult.BlockID], receipt) 1420 } 1421 } 1422 1423 // mark A as finalized 1424 bs.finalID = A.ID() 1425 1426 // set up no-op dependencies 1427 noopMetrics := metrics.NewNoopCollector() 1428 noopTracer := trace.NewNoopTracer() 1429 1430 // Instantiate real Execution Tree mempool; 1431 recPool := mempoolImpl.NewExecutionTree() 1432 1433 // create builder which has to repopulate execution tree 1434 var err error 1435 bs.build, err = NewBuilder( 1436 noopMetrics, 1437 bs.db, 1438 bs.state, 1439 bs.headerDB, 1440 bs.sealDB, 1441 bs.indexDB, 1442 bs.blockDB, 1443 bs.resultDB, 1444 bs.receiptsDB, 1445 bs.stateMutator, 1446 bs.guarPool, 1447 bs.sealPool, 1448 recPool, 1449 noopTracer, 1450 ) 1451 require.NoError(bs.T(), err) 1452 bs.build.cfg.expiry = 11 1453 1454 // Create two valid receipts for block B which build on different receipts 1455 // for the parent block (A); recB1 builds on top of RecA1, whilst recB2 1456 // builds on top of RecA2. 1457 resB1 := unittest.ExecutionResultFixture(unittest.WithBlock(B), unittest.WithPreviousResult(recA1.ExecutionResult)) 1458 recB1 := unittest.ExecutionReceiptFixture(unittest.WithResult(resB1)) 1459 resB2 := unittest.ExecutionResultFixture(unittest.WithBlock(B), unittest.WithPreviousResult(recA2.ExecutionResult)) 1460 recB2 := unittest.ExecutionReceiptFixture(unittest.WithResult(resB2)) 1461 resC := unittest.ExecutionResultFixture(unittest.WithBlock(C), unittest.WithPreviousResult(recB1.ExecutionResult)) 1462 recC := unittest.ExecutionReceiptFixture(unittest.WithResult(resC)) 1463 1464 // Add recB1 and recB2 to the mempool for inclusion in the next candidate 1465 _, _ = bs.build.recPool.AddReceipt(recB1, B.Header) 1466 _, _ = bs.build.recPool.AddReceipt(recB2, B.Header) 1467 _, _ = bs.build.recPool.AddReceipt(recC, C.Header) 1468 1469 _, err = bs.build.BuildOn(C.ID(), bs.setter, bs.sign) 1470 bs.Require().NoError(err) 1471 expectedReceipts := flow.ExecutionReceiptMetaList{recB1.Meta(), recB2.Meta(), recC.Meta()} 1472 expectedResults := flow.ExecutionResultList{&recB1.ExecutionResult, &recB2.ExecutionResult, &recC.ExecutionResult} 1473 bs.Assert().ElementsMatch(expectedReceipts, bs.assembled.Receipts, "payload should contain receipts from valid execution forks") 1474 bs.Assert().ElementsMatch(expectedResults, bs.assembled.Results, "payload should contain results from valid execution forks") 1475 }