github.com/onflow/flow-go@v0.33.17/module/builder/collection/builder_test.go (about) 1 package collection_test 2 3 import ( 4 "context" 5 "math/rand" 6 "os" 7 "testing" 8 9 "github.com/dgraph-io/badger/v2" 10 "github.com/rs/zerolog" 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 "github.com/stretchr/testify/suite" 14 15 model "github.com/onflow/flow-go/model/cluster" 16 "github.com/onflow/flow-go/model/flow" 17 builder "github.com/onflow/flow-go/module/builder/collection" 18 "github.com/onflow/flow-go/module/mempool" 19 "github.com/onflow/flow-go/module/mempool/herocache" 20 "github.com/onflow/flow-go/module/metrics" 21 "github.com/onflow/flow-go/module/trace" 22 "github.com/onflow/flow-go/state/cluster" 23 clusterkv "github.com/onflow/flow-go/state/cluster/badger" 24 "github.com/onflow/flow-go/state/protocol" 25 pbadger "github.com/onflow/flow-go/state/protocol/badger" 26 "github.com/onflow/flow-go/state/protocol/events" 27 "github.com/onflow/flow-go/state/protocol/inmem" 28 "github.com/onflow/flow-go/state/protocol/util" 29 "github.com/onflow/flow-go/storage" 30 bstorage "github.com/onflow/flow-go/storage/badger" 31 "github.com/onflow/flow-go/storage/badger/operation" 32 "github.com/onflow/flow-go/storage/badger/procedure" 33 sutil "github.com/onflow/flow-go/storage/util" 34 "github.com/onflow/flow-go/utils/unittest" 35 ) 36 37 var noopSetter = func(*flow.Header) error { return nil } 38 39 type BuilderSuite struct { 40 suite.Suite 41 db *badger.DB 42 dbdir string 43 44 genesis *model.Block 45 chainID flow.ChainID 46 epochCounter uint64 47 48 headers storage.Headers 49 payloads storage.ClusterPayloads 50 blocks storage.Blocks 51 52 state cluster.MutableState 53 54 // protocol state for reference blocks for transactions 55 protoState protocol.FollowerState 56 57 pool mempool.Transactions 58 builder *builder.Builder 59 } 60 61 // runs before each test runs 62 func (suite *BuilderSuite) SetupTest() { 63 var err error 64 65 suite.genesis = model.Genesis() 66 suite.chainID = suite.genesis.Header.ChainID 67 68 suite.pool = herocache.NewTransactions(1000, unittest.Logger(), metrics.NewNoopCollector()) 69 70 suite.dbdir = unittest.TempDir(suite.T()) 71 suite.db = unittest.BadgerDB(suite.T(), suite.dbdir) 72 73 metrics := metrics.NewNoopCollector() 74 tracer := trace.NewNoopTracer() 75 log := zerolog.Nop() 76 all := sutil.StorageLayer(suite.T(), suite.db) 77 consumer := events.NewNoop() 78 79 suite.headers = all.Headers 80 suite.blocks = all.Blocks 81 suite.payloads = bstorage.NewClusterPayloads(metrics, suite.db) 82 83 // just bootstrap with a genesis block, we'll use this as reference 84 root, result, seal := unittest.BootstrapFixture(unittest.IdentityListFixture(5, unittest.WithAllRoles())) 85 // ensure we don't enter a new epoch for tests that build many blocks 86 result.ServiceEvents[0].Event.(*flow.EpochSetup).FinalView = root.Header.View + 100000 87 seal.ResultID = result.ID() 88 rootSnapshot, err := inmem.SnapshotFromBootstrapState(root, result, seal, unittest.QuorumCertificateFixture(unittest.QCWithRootBlockID(root.ID()))) 89 require.NoError(suite.T(), err) 90 suite.epochCounter = rootSnapshot.Encodable().Epochs.Current.Counter 91 92 clusterQC := unittest.QuorumCertificateFixture(unittest.QCWithRootBlockID(suite.genesis.ID())) 93 clusterStateRoot, err := clusterkv.NewStateRoot(suite.genesis, clusterQC, suite.epochCounter) 94 suite.Require().NoError(err) 95 clusterState, err := clusterkv.Bootstrap(suite.db, clusterStateRoot) 96 suite.Require().NoError(err) 97 98 suite.state, err = clusterkv.NewMutableState(clusterState, tracer, suite.headers, suite.payloads) 99 suite.Require().NoError(err) 100 101 state, err := pbadger.Bootstrap( 102 metrics, 103 suite.db, 104 all.Headers, 105 all.Seals, 106 all.Results, 107 all.Blocks, 108 all.QuorumCertificates, 109 all.Setups, 110 all.EpochCommits, 111 all.Statuses, 112 all.VersionBeacons, 113 rootSnapshot, 114 ) 115 require.NoError(suite.T(), err) 116 117 suite.protoState, err = pbadger.NewFollowerState( 118 log, 119 tracer, 120 consumer, 121 state, 122 all.Index, 123 all.Payloads, 124 util.MockBlockTimer(), 125 ) 126 require.NoError(suite.T(), err) 127 128 // add some transactions to transaction pool 129 for i := 0; i < 3; i++ { 130 transaction := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { 131 tx.ReferenceBlockID = root.ID() 132 tx.ProposalKey.SequenceNumber = uint64(i) 133 tx.GasLimit = uint64(9999) 134 }) 135 added := suite.pool.Add(&transaction) 136 suite.Assert().True(added) 137 } 138 139 suite.builder, _ = builder.NewBuilder(suite.db, tracer, suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter) 140 } 141 142 // runs after each test finishes 143 func (suite *BuilderSuite) TearDownTest() { 144 err := suite.db.Close() 145 suite.Assert().NoError(err) 146 err = os.RemoveAll(suite.dbdir) 147 suite.Assert().NoError(err) 148 } 149 150 func (suite *BuilderSuite) InsertBlock(block model.Block) { 151 err := suite.db.Update(procedure.InsertClusterBlock(&block)) 152 suite.Assert().NoError(err) 153 } 154 155 func (suite *BuilderSuite) FinalizeBlock(block model.Block) { 156 err := suite.db.Update(func(tx *badger.Txn) error { 157 var refBlock flow.Header 158 err := operation.RetrieveHeader(block.Payload.ReferenceBlockID, &refBlock)(tx) 159 if err != nil { 160 return err 161 } 162 err = procedure.FinalizeClusterBlock(block.ID())(tx) 163 if err != nil { 164 return err 165 } 166 err = operation.IndexClusterBlockByReferenceHeight(refBlock.Height, block.ID())(tx) 167 return err 168 }) 169 suite.Assert().NoError(err) 170 } 171 172 // Payload returns a payload containing the given transactions, with a valid 173 // reference block ID. 174 func (suite *BuilderSuite) Payload(transactions ...*flow.TransactionBody) model.Payload { 175 final, err := suite.protoState.Final().Head() 176 suite.Require().NoError(err) 177 return model.PayloadFromTransactions(final.ID(), transactions...) 178 } 179 180 // ProtoStateRoot returns the root block of the protocol state. 181 func (suite *BuilderSuite) ProtoStateRoot() *flow.Header { 182 root, err := suite.protoState.Params().FinalizedRoot() 183 suite.Require().NoError(err) 184 return root 185 } 186 187 // ClearPool removes all items from the pool 188 func (suite *BuilderSuite) ClearPool() { 189 // TODO use Clear() 190 for _, tx := range suite.pool.All() { 191 suite.pool.Remove(tx.ID()) 192 } 193 } 194 195 // FillPool adds n transactions to the pool, using the given generator function. 196 func (suite *BuilderSuite) FillPool(n int, create func() *flow.TransactionBody) { 197 for i := 0; i < n; i++ { 198 tx := create() 199 suite.pool.Add(tx) 200 } 201 } 202 203 func TestBuilder(t *testing.T) { 204 suite.Run(t, new(BuilderSuite)) 205 } 206 207 func (suite *BuilderSuite) TestBuildOn_NonExistentParent() { 208 // use a non-existent parent ID 209 parentID := unittest.IdentifierFixture() 210 211 _, err := suite.builder.BuildOn(parentID, noopSetter) 212 suite.Assert().Error(err) 213 } 214 215 func (suite *BuilderSuite) TestBuildOn_Success() { 216 217 var expectedHeight uint64 = 42 218 setter := func(h *flow.Header) error { 219 h.Height = expectedHeight 220 return nil 221 } 222 223 header, err := suite.builder.BuildOn(suite.genesis.ID(), setter) 224 suite.Require().NoError(err) 225 226 // setter should have been run 227 suite.Assert().Equal(expectedHeight, header.Height) 228 229 // should be able to retrieve built block from storage 230 var built model.Block 231 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 232 suite.Assert().NoError(err) 233 builtCollection := built.Payload.Collection 234 235 // should reference a valid reference block 236 // (since genesis is the only block, it's the only valid reference) 237 mainGenesis, err := suite.protoState.AtHeight(0).Head() 238 suite.Assert().NoError(err) 239 suite.Assert().Equal(mainGenesis.ID(), built.Payload.ReferenceBlockID) 240 241 // payload should include only items from mempool 242 mempoolTransactions := suite.pool.All() 243 suite.Assert().Len(builtCollection.Transactions, 3) 244 suite.Assert().True(collectionContains(builtCollection, flow.GetIDs(mempoolTransactions)...)) 245 } 246 247 // when there are transactions with an unknown reference block in the pool, we should not include them in collections 248 func (suite *BuilderSuite) TestBuildOn_WithUnknownReferenceBlock() { 249 250 // before modifying the mempool, note the valid transactions already in the pool 251 validMempoolTransactions := suite.pool.All() 252 253 // add a transaction unknown reference block to the pool 254 unknownReferenceTx := unittest.TransactionBodyFixture() 255 unknownReferenceTx.ReferenceBlockID = unittest.IdentifierFixture() 256 suite.pool.Add(&unknownReferenceTx) 257 258 header, err := suite.builder.BuildOn(suite.genesis.ID(), noopSetter) 259 suite.Require().NoError(err) 260 261 // should be able to retrieve built block from storage 262 var built model.Block 263 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 264 suite.Assert().NoError(err) 265 builtCollection := built.Payload.Collection 266 267 suite.Assert().Len(builtCollection.Transactions, 3) 268 // payload should include only the transactions with a valid reference block 269 suite.Assert().True(collectionContains(builtCollection, flow.GetIDs(validMempoolTransactions)...)) 270 // should not contain the unknown-reference transaction 271 suite.Assert().False(collectionContains(builtCollection, unknownReferenceTx.ID())) 272 } 273 274 // when there are transactions with a known but unfinalized reference block in the pool, we should not include them in collections 275 func (suite *BuilderSuite) TestBuildOn_WithUnfinalizedReferenceBlock() { 276 277 // before modifying the mempool, note the valid transactions already in the pool 278 validMempoolTransactions := suite.pool.All() 279 280 // add an unfinalized block to the protocol state 281 genesis, err := suite.protoState.Final().Head() 282 suite.Require().NoError(err) 283 unfinalizedReferenceBlock := unittest.BlockWithParentFixture(genesis) 284 unfinalizedReferenceBlock.SetPayload(flow.EmptyPayload()) 285 err = suite.protoState.ExtendCertified(context.Background(), unfinalizedReferenceBlock, 286 unittest.CertifyBlock(unfinalizedReferenceBlock.Header)) 287 suite.Require().NoError(err) 288 289 // add a transaction with unfinalized reference block to the pool 290 unfinalizedReferenceTx := unittest.TransactionBodyFixture() 291 unfinalizedReferenceTx.ReferenceBlockID = unfinalizedReferenceBlock.ID() 292 suite.pool.Add(&unfinalizedReferenceTx) 293 294 header, err := suite.builder.BuildOn(suite.genesis.ID(), noopSetter) 295 suite.Require().NoError(err) 296 297 // should be able to retrieve built block from storage 298 var built model.Block 299 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 300 suite.Assert().NoError(err) 301 builtCollection := built.Payload.Collection 302 303 suite.Assert().Len(builtCollection.Transactions, 3) 304 // payload should include only the transactions with a valid reference block 305 suite.Assert().True(collectionContains(builtCollection, flow.GetIDs(validMempoolTransactions)...)) 306 // should not contain the unfinalized-reference transaction 307 suite.Assert().False(collectionContains(builtCollection, unfinalizedReferenceTx.ID())) 308 } 309 310 // when there are transactions with an orphaned reference block in the pool, we should not include them in collections 311 func (suite *BuilderSuite) TestBuildOn_WithOrphanedReferenceBlock() { 312 313 // before modifying the mempool, note the valid transactions already in the pool 314 validMempoolTransactions := suite.pool.All() 315 316 // add an orphaned block to the protocol state 317 genesis, err := suite.protoState.Final().Head() 318 suite.Require().NoError(err) 319 // create a block extending genesis which will be orphaned 320 orphan := unittest.BlockWithParentFixture(genesis) 321 orphan.SetPayload(flow.EmptyPayload()) 322 err = suite.protoState.ExtendCertified(context.Background(), orphan, unittest.CertifyBlock(orphan.Header)) 323 suite.Require().NoError(err) 324 // create and finalize a block on top of genesis, orphaning `orphan` 325 block1 := unittest.BlockWithParentFixture(genesis) 326 block1.SetPayload(flow.EmptyPayload()) 327 err = suite.protoState.ExtendCertified(context.Background(), block1, unittest.CertifyBlock(block1.Header)) 328 suite.Require().NoError(err) 329 err = suite.protoState.Finalize(context.Background(), block1.ID()) 330 suite.Require().NoError(err) 331 332 // add a transaction with orphaned reference block to the pool 333 orphanedReferenceTx := unittest.TransactionBodyFixture() 334 orphanedReferenceTx.ReferenceBlockID = orphan.ID() 335 suite.pool.Add(&orphanedReferenceTx) 336 337 header, err := suite.builder.BuildOn(suite.genesis.ID(), noopSetter) 338 suite.Require().NoError(err) 339 340 // should be able to retrieve built block from storage 341 var built model.Block 342 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 343 suite.Assert().NoError(err) 344 builtCollection := built.Payload.Collection 345 346 suite.Assert().Len(builtCollection.Transactions, 3) 347 // payload should include only the transactions with a valid reference block 348 suite.Assert().True(collectionContains(builtCollection, flow.GetIDs(validMempoolTransactions)...)) 349 // should not contain the unknown-reference transaction 350 suite.Assert().False(collectionContains(builtCollection, orphanedReferenceTx.ID())) 351 // the transaction with orphaned reference should be removed from the mempool 352 suite.Assert().False(suite.pool.Has(orphanedReferenceTx.ID())) 353 } 354 355 func (suite *BuilderSuite) TestBuildOn_WithForks() { 356 t := suite.T() 357 358 mempoolTransactions := suite.pool.All() 359 tx1 := mempoolTransactions[0] // in fork 1 360 tx2 := mempoolTransactions[1] // in fork 2 361 tx3 := mempoolTransactions[2] // in no block 362 363 // build first fork on top of genesis 364 payload1 := suite.Payload(tx1) 365 block1 := unittest.ClusterBlockWithParent(suite.genesis) 366 block1.SetPayload(payload1) 367 368 // insert block on fork 1 369 suite.InsertBlock(block1) 370 371 // build second fork on top of genesis 372 payload2 := suite.Payload(tx2) 373 block2 := unittest.ClusterBlockWithParent(suite.genesis) 374 block2.SetPayload(payload2) 375 376 // insert block on fork 2 377 suite.InsertBlock(block2) 378 379 // build on top of fork 1 380 header, err := suite.builder.BuildOn(block1.ID(), noopSetter) 381 require.NoError(t, err) 382 383 // should be able to retrieve built block from storage 384 var built model.Block 385 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 386 assert.NoError(t, err) 387 builtCollection := built.Payload.Collection 388 389 // payload should include ONLY tx2 and tx3 390 assert.Len(t, builtCollection.Transactions, 2) 391 assert.True(t, collectionContains(builtCollection, tx2.ID(), tx3.ID())) 392 assert.False(t, collectionContains(builtCollection, tx1.ID())) 393 } 394 395 func (suite *BuilderSuite) TestBuildOn_ConflictingFinalizedBlock() { 396 t := suite.T() 397 398 mempoolTransactions := suite.pool.All() 399 tx1 := mempoolTransactions[0] // in a finalized block 400 tx2 := mempoolTransactions[1] // in an un-finalized block 401 tx3 := mempoolTransactions[2] // in no blocks 402 403 t.Logf("tx1: %s\ntx2: %s\ntx3: %s", tx1.ID(), tx2.ID(), tx3.ID()) 404 405 // build a block containing tx1 on genesis 406 finalizedPayload := suite.Payload(tx1) 407 finalizedBlock := unittest.ClusterBlockWithParent(suite.genesis) 408 finalizedBlock.SetPayload(finalizedPayload) 409 suite.InsertBlock(finalizedBlock) 410 t.Logf("finalized: height=%d id=%s txs=%s parent_id=%s\t\n", finalizedBlock.Header.Height, finalizedBlock.ID(), finalizedPayload.Collection.Light(), finalizedBlock.Header.ParentID) 411 412 // build a block containing tx2 on the first block 413 unFinalizedPayload := suite.Payload(tx2) 414 unFinalizedBlock := unittest.ClusterBlockWithParent(&finalizedBlock) 415 unFinalizedBlock.SetPayload(unFinalizedPayload) 416 suite.InsertBlock(unFinalizedBlock) 417 t.Logf("finalized: height=%d id=%s txs=%s parent_id=%s\t\n", unFinalizedBlock.Header.Height, unFinalizedBlock.ID(), unFinalizedPayload.Collection.Light(), unFinalizedBlock.Header.ParentID) 418 419 // finalize first block 420 suite.FinalizeBlock(finalizedBlock) 421 422 // build on the un-finalized block 423 header, err := suite.builder.BuildOn(unFinalizedBlock.ID(), noopSetter) 424 require.NoError(t, err) 425 426 // retrieve the built block from storage 427 var built model.Block 428 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 429 assert.NoError(t, err) 430 builtCollection := built.Payload.Collection 431 432 // payload should only contain tx3 433 assert.Len(t, builtCollection.Light().Transactions, 1) 434 assert.True(t, collectionContains(builtCollection, tx3.ID())) 435 assert.False(t, collectionContains(builtCollection, tx1.ID(), tx2.ID())) 436 437 // tx1 should be removed from mempool, as it is in a finalized block 438 assert.False(t, suite.pool.Has(tx1.ID())) 439 // tx2 should NOT be removed from mempool, as it is in an un-finalized block 440 assert.True(t, suite.pool.Has(tx2.ID())) 441 } 442 443 func (suite *BuilderSuite) TestBuildOn_ConflictingInvalidatedForks() { 444 t := suite.T() 445 446 mempoolTransactions := suite.pool.All() 447 tx1 := mempoolTransactions[0] // in a finalized block 448 tx2 := mempoolTransactions[1] // in an invalidated block 449 tx3 := mempoolTransactions[2] // in no blocks 450 451 t.Logf("tx1: %s\ntx2: %s\ntx3: %s", tx1.ID(), tx2.ID(), tx3.ID()) 452 453 // build a block containing tx1 on genesis - will be finalized 454 finalizedPayload := suite.Payload(tx1) 455 finalizedBlock := unittest.ClusterBlockWithParent(suite.genesis) 456 finalizedBlock.SetPayload(finalizedPayload) 457 458 suite.InsertBlock(finalizedBlock) 459 t.Logf("finalized: id=%s\tparent_id=%s\theight=%d\n", finalizedBlock.ID(), finalizedBlock.Header.ParentID, finalizedBlock.Header.Height) 460 461 // build a block containing tx2 ALSO on genesis - will be invalidated 462 invalidatedPayload := suite.Payload(tx2) 463 invalidatedBlock := unittest.ClusterBlockWithParent(suite.genesis) 464 invalidatedBlock.SetPayload(invalidatedPayload) 465 suite.InsertBlock(invalidatedBlock) 466 t.Logf("invalidated: id=%s\tparent_id=%s\theight=%d\n", invalidatedBlock.ID(), invalidatedBlock.Header.ParentID, invalidatedBlock.Header.Height) 467 468 // finalize first block - this indirectly invalidates the second block 469 suite.FinalizeBlock(finalizedBlock) 470 471 // build on the finalized block 472 header, err := suite.builder.BuildOn(finalizedBlock.ID(), noopSetter) 473 require.NoError(t, err) 474 475 // retrieve the built block from storage 476 var built model.Block 477 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 478 assert.NoError(t, err) 479 builtCollection := built.Payload.Collection 480 481 // tx2 and tx3 should be in the built collection 482 assert.Len(t, builtCollection.Light().Transactions, 2) 483 assert.True(t, collectionContains(builtCollection, tx2.ID(), tx3.ID())) 484 assert.False(t, collectionContains(builtCollection, tx1.ID())) 485 } 486 487 func (suite *BuilderSuite) TestBuildOn_LargeHistory() { 488 t := suite.T() 489 490 // use a mempool with 2000 transactions, one per block 491 suite.pool = herocache.NewTransactions(2000, unittest.Logger(), metrics.NewNoopCollector()) 492 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter, builder.WithMaxCollectionSize(10000)) 493 494 // get a valid reference block ID 495 final, err := suite.protoState.Final().Head() 496 require.NoError(t, err) 497 refID := final.ID() 498 499 // keep track of the head of the chain 500 head := *suite.genesis 501 502 // keep track of invalidated transaction IDs 503 var invalidatedTxIds []flow.Identifier 504 505 // create a large history of blocks with invalidated forks every 3 blocks on 506 // average - build until the height exceeds transaction expiry 507 for i := 0; ; i++ { 508 509 // create a transaction 510 tx := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { 511 tx.ReferenceBlockID = refID 512 tx.ProposalKey.SequenceNumber = uint64(i) 513 }) 514 added := suite.pool.Add(&tx) 515 assert.True(t, added) 516 517 // 1/3 of the time create a conflicting fork that will be invalidated 518 // don't do this the first and last few times to ensure we don't 519 // try to fork genesis and the last block is the valid fork. 520 conflicting := rand.Intn(3) == 0 && i > 5 && i < 995 521 522 // by default, build on the head - if we are building a 523 // conflicting fork, build on the parent of the head 524 parent := head 525 if conflicting { 526 err = suite.db.View(procedure.RetrieveClusterBlock(parent.Header.ParentID, &parent)) 527 assert.NoError(t, err) 528 // add the transaction to the invalidated list 529 invalidatedTxIds = append(invalidatedTxIds, tx.ID()) 530 } 531 532 // create a block containing the transaction 533 block := unittest.ClusterBlockWithParent(&head) 534 payload := suite.Payload(&tx) 535 block.SetPayload(payload) 536 suite.InsertBlock(block) 537 538 // reset the valid head if we aren't building a conflicting fork 539 if !conflicting { 540 head = block 541 suite.FinalizeBlock(block) 542 assert.NoError(t, err) 543 } 544 545 // stop building blocks once we've built a history which exceeds the transaction 546 // expiry length - this tests that deduplication works properly against old blocks 547 // which nevertheless have a potentially conflicting reference block 548 if head.Header.Height > flow.DefaultTransactionExpiry+100 { 549 break 550 } 551 } 552 553 t.Log("conflicting: ", len(invalidatedTxIds)) 554 555 // build on the head block 556 header, err := suite.builder.BuildOn(head.ID(), noopSetter) 557 require.NoError(t, err) 558 559 // retrieve the built block from storage 560 var built model.Block 561 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 562 require.NoError(t, err) 563 builtCollection := built.Payload.Collection 564 565 // payload should only contain transactions from invalidated blocks 566 assert.Len(t, builtCollection.Transactions, len(invalidatedTxIds), "expected len=%d, got len=%d", len(invalidatedTxIds), len(builtCollection.Transactions)) 567 assert.True(t, collectionContains(builtCollection, invalidatedTxIds...)) 568 } 569 570 func (suite *BuilderSuite) TestBuildOn_MaxCollectionSize() { 571 // set the max collection size to 1 572 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter, builder.WithMaxCollectionSize(1)) 573 574 // build a block 575 header, err := suite.builder.BuildOn(suite.genesis.ID(), noopSetter) 576 suite.Require().NoError(err) 577 578 // retrieve the built block from storage 579 var built model.Block 580 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 581 suite.Require().NoError(err) 582 builtCollection := built.Payload.Collection 583 584 // should be only 1 transaction in the collection 585 suite.Assert().Equal(builtCollection.Len(), 1) 586 } 587 588 func (suite *BuilderSuite) TestBuildOn_MaxCollectionByteSize() { 589 // set the max collection byte size to 400 (each tx is about 150 bytes) 590 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter, builder.WithMaxCollectionByteSize(400)) 591 592 // build a block 593 header, err := suite.builder.BuildOn(suite.genesis.ID(), noopSetter) 594 suite.Require().NoError(err) 595 596 // retrieve the built block from storage 597 var built model.Block 598 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 599 suite.Require().NoError(err) 600 builtCollection := built.Payload.Collection 601 602 // should be only 2 transactions in the collection, since each tx is ~273 bytes and the limit is 600 bytes 603 suite.Assert().Equal(builtCollection.Len(), 2) 604 } 605 606 func (suite *BuilderSuite) TestBuildOn_MaxCollectionTotalGas() { 607 // set the max gas to 20,000 608 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter, builder.WithMaxCollectionTotalGas(20000)) 609 610 // build a block 611 header, err := suite.builder.BuildOn(suite.genesis.ID(), noopSetter) 612 suite.Require().NoError(err) 613 614 // retrieve the built block from storage 615 var built model.Block 616 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 617 suite.Require().NoError(err) 618 builtCollection := built.Payload.Collection 619 620 // should be only 2 transactions in collection, since each transaction has gas limit of 9,999 and collection limit is set to 20,000 621 suite.Assert().Equal(builtCollection.Len(), 2) 622 } 623 624 func (suite *BuilderSuite) TestBuildOn_ExpiredTransaction() { 625 626 // create enough main-chain blocks that an expired transaction is possible 627 genesis, err := suite.protoState.Final().Head() 628 suite.Require().NoError(err) 629 630 head := genesis 631 for i := 0; i < flow.DefaultTransactionExpiry+1; i++ { 632 block := unittest.BlockWithParentFixture(head) 633 block.Payload.Guarantees = nil 634 block.Payload.Seals = nil 635 block.Header.PayloadHash = block.Payload.Hash() 636 err = suite.protoState.ExtendCertified(context.Background(), block, unittest.CertifyBlock(block.Header)) 637 suite.Require().NoError(err) 638 err = suite.protoState.Finalize(context.Background(), block.ID()) 639 suite.Require().NoError(err) 640 head = block.Header 641 } 642 643 // reset the pool and builder 644 suite.pool = herocache.NewTransactions(10, unittest.Logger(), metrics.NewNoopCollector()) 645 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter) 646 647 // insert a transaction referring genesis (now expired) 648 tx1 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { 649 tx.ReferenceBlockID = genesis.ID() 650 tx.ProposalKey.SequenceNumber = 0 651 }) 652 added := suite.pool.Add(&tx1) 653 suite.Assert().True(added) 654 655 // insert a transaction referencing the head (valid) 656 tx2 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { 657 tx.ReferenceBlockID = head.ID() 658 tx.ProposalKey.SequenceNumber = 1 659 }) 660 added = suite.pool.Add(&tx2) 661 suite.Assert().True(added) 662 663 suite.T().Log("tx1: ", tx1.ID()) 664 suite.T().Log("tx2: ", tx2.ID()) 665 666 // build a block 667 header, err := suite.builder.BuildOn(suite.genesis.ID(), noopSetter) 668 suite.Require().NoError(err) 669 670 // retrieve the built block from storage 671 var built model.Block 672 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 673 suite.Require().NoError(err) 674 builtCollection := built.Payload.Collection 675 676 // the block should only contain the un-expired transaction 677 suite.Assert().False(collectionContains(builtCollection, tx1.ID())) 678 suite.Assert().True(collectionContains(builtCollection, tx2.ID())) 679 // the expired transaction should have been removed from the mempool 680 suite.Assert().False(suite.pool.Has(tx1.ID())) 681 } 682 683 func (suite *BuilderSuite) TestBuildOn_EmptyMempool() { 684 685 // start with an empty mempool 686 suite.pool = herocache.NewTransactions(1000, unittest.Logger(), metrics.NewNoopCollector()) 687 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter) 688 689 header, err := suite.builder.BuildOn(suite.genesis.ID(), noopSetter) 690 suite.Require().NoError(err) 691 692 var built model.Block 693 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 694 suite.Require().NoError(err) 695 696 // should reference a valid reference block 697 // (since genesis is the only block, it's the only valid reference) 698 mainGenesis, err := suite.protoState.AtHeight(0).Head() 699 suite.Assert().NoError(err) 700 suite.Assert().Equal(mainGenesis.ID(), built.Payload.ReferenceBlockID) 701 702 // the payload should be empty 703 suite.Assert().Equal(0, built.Payload.Collection.Len()) 704 } 705 706 // With rate limiting turned off, we should fill collections as fast as we can 707 // regardless of how many transactions with the same payer we include. 708 func (suite *BuilderSuite) TestBuildOn_NoRateLimiting() { 709 710 // start with an empty mempool 711 suite.ClearPool() 712 713 // create builder with no rate limit and max 10 tx/collection 714 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter, 715 builder.WithMaxCollectionSize(10), 716 builder.WithMaxPayerTransactionRate(0), 717 ) 718 719 // fill the pool with 100 transactions from the same payer 720 payer := unittest.RandomAddressFixture() 721 create := func() *flow.TransactionBody { 722 tx := unittest.TransactionBodyFixture() 723 tx.ReferenceBlockID = suite.ProtoStateRoot().ID() 724 tx.Payer = payer 725 return &tx 726 } 727 suite.FillPool(100, create) 728 729 // since we have no rate limiting we should fill all collections and in 10 blocks 730 parentID := suite.genesis.ID() 731 for i := 0; i < 10; i++ { 732 header, err := suite.builder.BuildOn(parentID, noopSetter) 733 suite.Require().NoError(err) 734 parentID = header.ID() 735 736 // each collection should be full with 10 transactions 737 var built model.Block 738 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 739 suite.Assert().NoError(err) 740 suite.Assert().Len(built.Payload.Collection.Transactions, 10) 741 } 742 } 743 744 // With rate limiting turned on, we should be able to fill transactions as fast 745 // as possible so long as per-payer limits are not reached. This test generates 746 // transactions such that the number of transactions with a given proposer exceeds 747 // the rate limit -- since it's the proposer not the payer, it shouldn't limit 748 // our collections. 749 func (suite *BuilderSuite) TestBuildOn_RateLimitNonPayer() { 750 751 // start with an empty mempool 752 suite.ClearPool() 753 754 // create builder with 5 tx/payer and max 10 tx/collection 755 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter, 756 builder.WithMaxCollectionSize(10), 757 builder.WithMaxPayerTransactionRate(5), 758 ) 759 760 // fill the pool with 100 transactions with the same proposer 761 // since it's not the same payer, rate limit does not apply 762 proposer := unittest.RandomAddressFixture() 763 create := func() *flow.TransactionBody { 764 tx := unittest.TransactionBodyFixture() 765 tx.ReferenceBlockID = suite.ProtoStateRoot().ID() 766 tx.Payer = unittest.RandomAddressFixture() 767 tx.ProposalKey = flow.ProposalKey{ 768 Address: proposer, 769 KeyIndex: rand.Uint64(), 770 SequenceNumber: rand.Uint64(), 771 } 772 return &tx 773 } 774 suite.FillPool(100, create) 775 776 // since rate limiting does not apply to non-payer keys, we should fill all collections in 10 blocks 777 parentID := suite.genesis.ID() 778 for i := 0; i < 10; i++ { 779 header, err := suite.builder.BuildOn(parentID, noopSetter) 780 suite.Require().NoError(err) 781 parentID = header.ID() 782 783 // each collection should be full with 10 transactions 784 var built model.Block 785 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 786 suite.Assert().NoError(err) 787 suite.Assert().Len(built.Payload.Collection.Transactions, 10) 788 } 789 } 790 791 // When configured with a rate limit of k>1, we should be able to include up to 792 // k transactions with a given payer per collection 793 func (suite *BuilderSuite) TestBuildOn_HighRateLimit() { 794 795 // start with an empty mempool 796 suite.ClearPool() 797 798 // create builder with 5 tx/payer and max 10 tx/collection 799 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter, 800 builder.WithMaxCollectionSize(10), 801 builder.WithMaxPayerTransactionRate(5), 802 ) 803 804 // fill the pool with 50 transactions from the same payer 805 payer := unittest.RandomAddressFixture() 806 create := func() *flow.TransactionBody { 807 tx := unittest.TransactionBodyFixture() 808 tx.ReferenceBlockID = suite.ProtoStateRoot().ID() 809 tx.Payer = payer 810 return &tx 811 } 812 suite.FillPool(50, create) 813 814 // rate-limiting should be applied, resulting in half-full collections (5/10) 815 parentID := suite.genesis.ID() 816 for i := 0; i < 10; i++ { 817 header, err := suite.builder.BuildOn(parentID, noopSetter) 818 suite.Require().NoError(err) 819 parentID = header.ID() 820 821 // each collection should be half-full with 5 transactions 822 var built model.Block 823 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 824 suite.Assert().NoError(err) 825 suite.Assert().Len(built.Payload.Collection.Transactions, 5) 826 } 827 } 828 829 // When configured with a rate limit of k<1, we should be able to include 1 830 // transactions with a given payer every ceil(1/k) collections 831 func (suite *BuilderSuite) TestBuildOn_LowRateLimit() { 832 833 // start with an empty mempool 834 suite.ClearPool() 835 836 // create builder with .5 tx/payer and max 10 tx/collection 837 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter, 838 builder.WithMaxCollectionSize(10), 839 builder.WithMaxPayerTransactionRate(.5), 840 ) 841 842 // fill the pool with 5 transactions from the same payer 843 payer := unittest.RandomAddressFixture() 844 create := func() *flow.TransactionBody { 845 tx := unittest.TransactionBodyFixture() 846 tx.ReferenceBlockID = suite.ProtoStateRoot().ID() 847 tx.Payer = payer 848 return &tx 849 } 850 suite.FillPool(5, create) 851 852 // rate-limiting should be applied, resulting in every ceil(1/k) collections 853 // having one transaction and empty collections otherwise 854 parentID := suite.genesis.ID() 855 for i := 0; i < 10; i++ { 856 header, err := suite.builder.BuildOn(parentID, noopSetter) 857 suite.Require().NoError(err) 858 parentID = header.ID() 859 860 // collections should either be empty or have 1 transaction 861 var built model.Block 862 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 863 suite.Assert().NoError(err) 864 if i%2 == 0 { 865 suite.Assert().Len(built.Payload.Collection.Transactions, 1) 866 } else { 867 suite.Assert().Len(built.Payload.Collection.Transactions, 0) 868 } 869 } 870 } 871 func (suite *BuilderSuite) TestBuildOn_UnlimitedPayer() { 872 873 // start with an empty mempool 874 suite.ClearPool() 875 876 // create builder with 5 tx/payer and max 10 tx/collection 877 // configure an unlimited payer 878 payer := unittest.RandomAddressFixture() 879 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter, 880 builder.WithMaxCollectionSize(10), 881 builder.WithMaxPayerTransactionRate(5), 882 builder.WithUnlimitedPayers(payer), 883 ) 884 885 // fill the pool with 100 transactions from the same payer 886 create := func() *flow.TransactionBody { 887 tx := unittest.TransactionBodyFixture() 888 tx.ReferenceBlockID = suite.ProtoStateRoot().ID() 889 tx.Payer = payer 890 return &tx 891 } 892 suite.FillPool(100, create) 893 894 // rate-limiting should not be applied, since the payer is marked as unlimited 895 parentID := suite.genesis.ID() 896 for i := 0; i < 10; i++ { 897 header, err := suite.builder.BuildOn(parentID, noopSetter) 898 suite.Require().NoError(err) 899 parentID = header.ID() 900 901 // each collection should be full with 10 transactions 902 var built model.Block 903 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 904 suite.Assert().NoError(err) 905 suite.Assert().Len(built.Payload.Collection.Transactions, 10) 906 907 } 908 } 909 910 // TestBuildOn_RateLimitDryRun tests that rate limiting rules aren't enforced 911 // if dry-run is enabled. 912 func (suite *BuilderSuite) TestBuildOn_RateLimitDryRun() { 913 914 // start with an empty mempool 915 suite.ClearPool() 916 917 // create builder with 5 tx/payer and max 10 tx/collection 918 // configure an unlimited payer 919 payer := unittest.RandomAddressFixture() 920 suite.builder, _ = builder.NewBuilder(suite.db, trace.NewNoopTracer(), suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter, 921 builder.WithMaxCollectionSize(10), 922 builder.WithMaxPayerTransactionRate(5), 923 builder.WithRateLimitDryRun(true), 924 ) 925 926 // fill the pool with 100 transactions from the same payer 927 create := func() *flow.TransactionBody { 928 tx := unittest.TransactionBodyFixture() 929 tx.ReferenceBlockID = suite.ProtoStateRoot().ID() 930 tx.Payer = payer 931 return &tx 932 } 933 suite.FillPool(100, create) 934 935 // rate-limiting should not be applied, since dry-run setting is enabled 936 parentID := suite.genesis.ID() 937 for i := 0; i < 10; i++ { 938 header, err := suite.builder.BuildOn(parentID, noopSetter) 939 suite.Require().NoError(err) 940 parentID = header.ID() 941 942 // each collection should be full with 10 transactions 943 var built model.Block 944 err = suite.db.View(procedure.RetrieveClusterBlock(header.ID(), &built)) 945 suite.Assert().NoError(err) 946 suite.Assert().Len(built.Payload.Collection.Transactions, 10) 947 } 948 } 949 950 // helper to check whether a collection contains each of the given transactions. 951 func collectionContains(collection flow.Collection, txIDs ...flow.Identifier) bool { 952 953 lookup := make(map[flow.Identifier]struct{}, len(txIDs)) 954 for _, tx := range collection.Transactions { 955 lookup[tx.ID()] = struct{}{} 956 } 957 958 for _, txID := range txIDs { 959 _, exists := lookup[txID] 960 if !exists { 961 return false 962 } 963 } 964 965 return true 966 } 967 968 func BenchmarkBuildOn10(b *testing.B) { benchmarkBuildOn(b, 10) } 969 func BenchmarkBuildOn100(b *testing.B) { benchmarkBuildOn(b, 100) } 970 func BenchmarkBuildOn1000(b *testing.B) { benchmarkBuildOn(b, 1000) } 971 func BenchmarkBuildOn10000(b *testing.B) { benchmarkBuildOn(b, 10000) } 972 func BenchmarkBuildOn100000(b *testing.B) { benchmarkBuildOn(b, 100000) } 973 974 func benchmarkBuildOn(b *testing.B, size int) { 975 b.StopTimer() 976 b.ResetTimer() 977 978 // re-use the builder suite 979 suite := new(BuilderSuite) 980 981 // Copied from SetupTest. We can't use that function because suite.Assert 982 // is incompatible with benchmarks. 983 // ref: https://github.com/stretchr/testify/issues/811 984 { 985 var err error 986 987 suite.genesis = model.Genesis() 988 suite.chainID = suite.genesis.Header.ChainID 989 990 suite.pool = herocache.NewTransactions(1000, unittest.Logger(), metrics.NewNoopCollector()) 991 992 suite.dbdir = unittest.TempDir(b) 993 suite.db = unittest.BadgerDB(b, suite.dbdir) 994 defer func() { 995 err = suite.db.Close() 996 assert.NoError(b, err) 997 err = os.RemoveAll(suite.dbdir) 998 assert.NoError(b, err) 999 }() 1000 1001 metrics := metrics.NewNoopCollector() 1002 tracer := trace.NewNoopTracer() 1003 all := sutil.StorageLayer(suite.T(), suite.db) 1004 suite.headers = all.Headers 1005 suite.blocks = all.Blocks 1006 suite.payloads = bstorage.NewClusterPayloads(metrics, suite.db) 1007 1008 qc := unittest.QuorumCertificateFixture(unittest.QCWithRootBlockID(suite.genesis.ID())) 1009 stateRoot, err := clusterkv.NewStateRoot(suite.genesis, qc, suite.epochCounter) 1010 1011 state, err := clusterkv.Bootstrap(suite.db, stateRoot) 1012 assert.NoError(b, err) 1013 1014 suite.state, err = clusterkv.NewMutableState(state, tracer, suite.headers, suite.payloads) 1015 assert.NoError(b, err) 1016 1017 // add some transactions to transaction pool 1018 for i := 0; i < 3; i++ { 1019 tx := unittest.TransactionBodyFixture() 1020 added := suite.pool.Add(&tx) 1021 assert.True(b, added) 1022 } 1023 1024 // create the builder 1025 suite.builder, _ = builder.NewBuilder(suite.db, tracer, suite.protoState, suite.state, suite.headers, suite.headers, suite.payloads, suite.pool, unittest.Logger(), suite.epochCounter) 1026 } 1027 1028 // create a block history to test performance against 1029 final := suite.genesis 1030 for i := 0; i < size; i++ { 1031 block := unittest.ClusterBlockWithParent(final) 1032 err := suite.db.Update(procedure.InsertClusterBlock(&block)) 1033 require.NoError(b, err) 1034 1035 // finalize the block 80% of the time, resulting in a fork-rate of 20% 1036 if rand.Intn(100) < 80 { 1037 err = suite.db.Update(procedure.FinalizeClusterBlock(block.ID())) 1038 require.NoError(b, err) 1039 final = &block 1040 } 1041 } 1042 1043 b.StartTimer() 1044 for n := 0; n < b.N; n++ { 1045 _, err := suite.builder.BuildOn(final.ID(), noopSetter) 1046 assert.NoError(b, err) 1047 } 1048 }