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