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