github.com/onflow/flow-go@v0.33.17/state/cluster/badger/mutator_test.go (about) 1 package badger 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "math/rand" 8 "os" 9 "testing" 10 11 "github.com/dgraph-io/badger/v2" 12 "github.com/rs/zerolog" 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 "github.com/stretchr/testify/suite" 16 17 model "github.com/onflow/flow-go/model/cluster" 18 "github.com/onflow/flow-go/model/flow" 19 "github.com/onflow/flow-go/module/metrics" 20 "github.com/onflow/flow-go/module/trace" 21 "github.com/onflow/flow-go/state" 22 "github.com/onflow/flow-go/state/cluster" 23 "github.com/onflow/flow-go/state/protocol" 24 pbadger "github.com/onflow/flow-go/state/protocol/badger" 25 "github.com/onflow/flow-go/state/protocol/events" 26 "github.com/onflow/flow-go/state/protocol/inmem" 27 protocolutil "github.com/onflow/flow-go/state/protocol/util" 28 storage "github.com/onflow/flow-go/storage/badger" 29 "github.com/onflow/flow-go/storage/badger/operation" 30 "github.com/onflow/flow-go/storage/badger/procedure" 31 "github.com/onflow/flow-go/storage/util" 32 "github.com/onflow/flow-go/utils/unittest" 33 ) 34 35 type MutatorSuite struct { 36 suite.Suite 37 db *badger.DB 38 dbdir string 39 40 genesis *model.Block 41 chainID flow.ChainID 42 epochCounter uint64 43 44 // protocol state for reference blocks for transactions 45 protoState protocol.FollowerState 46 protoGenesis *flow.Header 47 48 state cluster.MutableState 49 } 50 51 // runs before each test runs 52 func (suite *MutatorSuite) SetupTest() { 53 var err error 54 55 suite.genesis = model.Genesis() 56 suite.chainID = suite.genesis.Header.ChainID 57 58 suite.dbdir = unittest.TempDir(suite.T()) 59 suite.db = unittest.BadgerDB(suite.T(), suite.dbdir) 60 61 metrics := metrics.NewNoopCollector() 62 tracer := trace.NewNoopTracer() 63 log := zerolog.Nop() 64 all := util.StorageLayer(suite.T(), suite.db) 65 colPayloads := storage.NewClusterPayloads(metrics, suite.db) 66 67 // just bootstrap with a genesis block, we'll use this as reference 68 genesis, result, seal := unittest.BootstrapFixture(unittest.IdentityListFixture(5, unittest.WithAllRoles())) 69 // ensure we don't enter a new epoch for tests that build many blocks 70 result.ServiceEvents[0].Event.(*flow.EpochSetup).FinalView = genesis.Header.View + 100_000 71 seal.ResultID = result.ID() 72 qc := unittest.QuorumCertificateFixture(unittest.QCWithRootBlockID(genesis.ID())) 73 rootSnapshot, err := inmem.SnapshotFromBootstrapState(genesis, result, seal, qc) 74 require.NoError(suite.T(), err) 75 suite.epochCounter = rootSnapshot.Encodable().Epochs.Current.Counter 76 77 suite.protoGenesis = genesis.Header 78 state, err := pbadger.Bootstrap( 79 metrics, 80 suite.db, 81 all.Headers, 82 all.Seals, 83 all.Results, 84 all.Blocks, 85 all.QuorumCertificates, 86 all.Setups, 87 all.EpochCommits, 88 all.Statuses, 89 all.VersionBeacons, 90 rootSnapshot, 91 ) 92 require.NoError(suite.T(), err) 93 suite.protoState, err = pbadger.NewFollowerState(log, tracer, events.NewNoop(), state, all.Index, all.Payloads, protocolutil.MockBlockTimer()) 94 require.NoError(suite.T(), err) 95 96 clusterStateRoot, err := NewStateRoot(suite.genesis, unittest.QuorumCertificateFixture(), suite.epochCounter) 97 suite.NoError(err) 98 clusterState, err := Bootstrap(suite.db, clusterStateRoot) 99 suite.Assert().Nil(err) 100 suite.state, err = NewMutableState(clusterState, tracer, all.Headers, colPayloads) 101 suite.Assert().Nil(err) 102 } 103 104 // runs after each test finishes 105 func (suite *MutatorSuite) TearDownTest() { 106 err := suite.db.Close() 107 suite.Assert().Nil(err) 108 err = os.RemoveAll(suite.dbdir) 109 suite.Assert().Nil(err) 110 } 111 112 // Payload returns a valid cluster block payload containing the given transactions. 113 func (suite *MutatorSuite) Payload(transactions ...*flow.TransactionBody) model.Payload { 114 final, err := suite.protoState.Final().Head() 115 suite.Require().Nil(err) 116 117 // find the oldest reference block among the transactions 118 minRefID := final.ID() // use final by default 119 minRefHeight := uint64(math.MaxUint64) 120 for _, tx := range transactions { 121 refBlock, err := suite.protoState.AtBlockID(tx.ReferenceBlockID).Head() 122 if err != nil { 123 continue 124 } 125 if refBlock.Height < minRefHeight { 126 minRefHeight = refBlock.Height 127 minRefID = refBlock.ID() 128 } 129 } 130 return model.PayloadFromTransactions(minRefID, transactions...) 131 } 132 133 // BlockWithParent returns a valid block with the given parent. 134 func (suite *MutatorSuite) BlockWithParent(parent *model.Block) model.Block { 135 block := unittest.ClusterBlockWithParent(parent) 136 payload := suite.Payload() 137 block.SetPayload(payload) 138 return block 139 } 140 141 // Block returns a valid cluster block with genesis as parent. 142 func (suite *MutatorSuite) Block() model.Block { 143 return suite.BlockWithParent(suite.genesis) 144 } 145 146 func (suite *MutatorSuite) FinalizeBlock(block model.Block) { 147 err := suite.db.Update(func(tx *badger.Txn) error { 148 var refBlock flow.Header 149 err := operation.RetrieveHeader(block.Payload.ReferenceBlockID, &refBlock)(tx) 150 if err != nil { 151 return err 152 } 153 err = procedure.FinalizeClusterBlock(block.ID())(tx) 154 if err != nil { 155 return err 156 } 157 err = operation.IndexClusterBlockByReferenceHeight(refBlock.Height, block.ID())(tx) 158 return err 159 }) 160 suite.Assert().NoError(err) 161 } 162 163 func (suite *MutatorSuite) Tx(opts ...func(*flow.TransactionBody)) flow.TransactionBody { 164 final, err := suite.protoState.Final().Head() 165 suite.Require().Nil(err) 166 167 tx := unittest.TransactionBodyFixture(opts...) 168 tx.ReferenceBlockID = final.ID() 169 return tx 170 } 171 172 func TestMutator(t *testing.T) { 173 suite.Run(t, new(MutatorSuite)) 174 } 175 176 func (suite *MutatorSuite) TestBootstrap_InvalidHeight() { 177 suite.genesis.Header.Height = 1 178 179 _, err := NewStateRoot(suite.genesis, unittest.QuorumCertificateFixture(), suite.epochCounter) 180 suite.Assert().Error(err) 181 } 182 183 func (suite *MutatorSuite) TestBootstrap_InvalidParentHash() { 184 suite.genesis.Header.ParentID = unittest.IdentifierFixture() 185 186 _, err := NewStateRoot(suite.genesis, unittest.QuorumCertificateFixture(), suite.epochCounter) 187 suite.Assert().Error(err) 188 } 189 190 func (suite *MutatorSuite) TestBootstrap_InvalidPayloadHash() { 191 suite.genesis.Header.PayloadHash = unittest.IdentifierFixture() 192 193 _, err := NewStateRoot(suite.genesis, unittest.QuorumCertificateFixture(), suite.epochCounter) 194 suite.Assert().Error(err) 195 } 196 197 func (suite *MutatorSuite) TestBootstrap_InvalidPayload() { 198 // this is invalid because genesis collection should be empty 199 suite.genesis.Payload = unittest.ClusterPayloadFixture(2) 200 201 _, err := NewStateRoot(suite.genesis, unittest.QuorumCertificateFixture(), suite.epochCounter) 202 suite.Assert().Error(err) 203 } 204 205 func (suite *MutatorSuite) TestBootstrap_Successful() { 206 err := suite.db.View(func(tx *badger.Txn) error { 207 208 // should insert collection 209 var collection flow.LightCollection 210 err := operation.RetrieveCollection(suite.genesis.Payload.Collection.ID(), &collection)(tx) 211 suite.Assert().Nil(err) 212 suite.Assert().Equal(suite.genesis.Payload.Collection.Light(), collection) 213 214 // should index collection 215 collection = flow.LightCollection{} // reset the collection 216 err = operation.LookupCollectionPayload(suite.genesis.ID(), &collection.Transactions)(tx) 217 suite.Assert().Nil(err) 218 suite.Assert().Equal(suite.genesis.Payload.Collection.Light(), collection) 219 220 // should insert header 221 var header flow.Header 222 err = operation.RetrieveHeader(suite.genesis.ID(), &header)(tx) 223 suite.Assert().Nil(err) 224 suite.Assert().Equal(suite.genesis.Header.ID(), header.ID()) 225 226 // should insert block height -> ID lookup 227 var blockID flow.Identifier 228 err = operation.LookupClusterBlockHeight(suite.genesis.Header.ChainID, suite.genesis.Header.Height, &blockID)(tx) 229 suite.Assert().Nil(err) 230 suite.Assert().Equal(suite.genesis.ID(), blockID) 231 232 // should insert boundary 233 var boundary uint64 234 err = operation.RetrieveClusterFinalizedHeight(suite.genesis.Header.ChainID, &boundary)(tx) 235 suite.Assert().Nil(err) 236 suite.Assert().Equal(suite.genesis.Header.Height, boundary) 237 238 return nil 239 }) 240 suite.Assert().Nil(err) 241 } 242 243 func (suite *MutatorSuite) TestExtend_WithoutBootstrap() { 244 block := unittest.ClusterBlockWithParent(suite.genesis) 245 err := suite.state.Extend(&block) 246 suite.Assert().Error(err) 247 } 248 249 func (suite *MutatorSuite) TestExtend_InvalidChainID() { 250 block := suite.Block() 251 // change the chain ID 252 block.Header.ChainID = flow.ChainID(fmt.Sprintf("%s-invalid", block.Header.ChainID)) 253 254 err := suite.state.Extend(&block) 255 suite.Assert().Error(err) 256 suite.Assert().True(state.IsInvalidExtensionError(err)) 257 } 258 259 func (suite *MutatorSuite) TestExtend_InvalidBlockHeight() { 260 block := suite.Block() 261 // change the block height 262 block.Header.Height = block.Header.Height - 1 263 264 err := suite.state.Extend(&block) 265 suite.Assert().Error(err) 266 suite.Assert().True(state.IsInvalidExtensionError(err)) 267 } 268 269 // TestExtend_InvalidParentView tests if mutator rejects block with invalid ParentView. ParentView must be consistent 270 // with view of block referred by ParentID. 271 func (suite *MutatorSuite) TestExtend_InvalidParentView() { 272 block := suite.Block() 273 // change the block parent view 274 block.Header.ParentView-- 275 276 err := suite.state.Extend(&block) 277 suite.Assert().Error(err) 278 suite.Assert().True(state.IsInvalidExtensionError(err)) 279 } 280 281 func (suite *MutatorSuite) TestExtend_DuplicateTxInPayload() { 282 block := suite.Block() 283 // add the same transaction to a payload twice 284 tx := suite.Tx() 285 payload := suite.Payload(&tx, &tx) 286 block.SetPayload(payload) 287 288 // should fail to extend block with invalid payload 289 err := suite.state.Extend(&block) 290 suite.Assert().Error(err) 291 suite.Assert().True(state.IsInvalidExtensionError(err)) 292 } 293 294 func (suite *MutatorSuite) TestExtend_OnParentOfFinalized() { 295 // build one block on top of genesis 296 block1 := suite.Block() 297 err := suite.state.Extend(&block1) 298 suite.Assert().Nil(err) 299 300 // finalize the block 301 suite.FinalizeBlock(block1) 302 303 // insert another block on top of genesis 304 // since we have already finalized block 1, this is invalid 305 block2 := suite.Block() 306 307 // try to extend with the invalid block 308 err = suite.state.Extend(&block2) 309 suite.Assert().Error(err) 310 suite.Assert().True(state.IsOutdatedExtensionError(err)) 311 } 312 313 func (suite *MutatorSuite) TestExtend_Success() { 314 block := suite.Block() 315 err := suite.state.Extend(&block) 316 suite.Assert().Nil(err) 317 318 // should be able to retrieve the block 319 var extended model.Block 320 err = suite.db.View(procedure.RetrieveClusterBlock(block.ID(), &extended)) 321 suite.Assert().Nil(err) 322 suite.Assert().Equal(*block.Payload, *extended.Payload) 323 324 // the block should be indexed by its parent 325 var childIDs flow.IdentifierList 326 err = suite.db.View(procedure.LookupBlockChildren(suite.genesis.ID(), &childIDs)) 327 suite.Assert().Nil(err) 328 suite.Require().Len(childIDs, 1) 329 suite.Assert().Equal(block.ID(), childIDs[0]) 330 } 331 332 func (suite *MutatorSuite) TestExtend_WithEmptyCollection() { 333 block := suite.Block() 334 // set an empty collection as the payload 335 block.SetPayload(suite.Payload()) 336 err := suite.state.Extend(&block) 337 suite.Assert().Nil(err) 338 } 339 340 // an unknown reference block is unverifiable 341 func (suite *MutatorSuite) TestExtend_WithNonExistentReferenceBlock() { 342 suite.Run("empty collection", func() { 343 block := suite.Block() 344 block.Payload.ReferenceBlockID = unittest.IdentifierFixture() 345 block.SetPayload(*block.Payload) 346 err := suite.state.Extend(&block) 347 suite.Assert().Error(err) 348 suite.Assert().True(state.IsUnverifiableExtensionError(err)) 349 }) 350 suite.Run("non-empty collection", func() { 351 block := suite.Block() 352 tx := suite.Tx() 353 payload := suite.Payload(&tx) 354 // set a random reference block ID 355 payload.ReferenceBlockID = unittest.IdentifierFixture() 356 block.SetPayload(payload) 357 err := suite.state.Extend(&block) 358 suite.Assert().Error(err) 359 suite.Assert().True(state.IsUnverifiableExtensionError(err)) 360 }) 361 } 362 363 // a collection with an expired reference block is a VALID extension of chain state 364 func (suite *MutatorSuite) TestExtend_WithExpiredReferenceBlock() { 365 // build enough blocks so that using genesis as a reference block causes 366 // the collection to be expired 367 parent := suite.protoGenesis 368 for i := 0; i < flow.DefaultTransactionExpiry+1; i++ { 369 next := unittest.BlockWithParentFixture(parent) 370 next.Payload.Guarantees = nil 371 next.SetPayload(*next.Payload) 372 err := suite.protoState.ExtendCertified(context.Background(), next, unittest.CertifyBlock(next.Header)) 373 suite.Require().Nil(err) 374 err = suite.protoState.Finalize(context.Background(), next.ID()) 375 suite.Require().Nil(err) 376 parent = next.Header 377 } 378 379 block := suite.Block() 380 // set genesis as reference block 381 block.SetPayload(model.EmptyPayload(suite.protoGenesis.ID())) 382 err := suite.state.Extend(&block) 383 suite.Assert().Nil(err) 384 } 385 386 func (suite *MutatorSuite) TestExtend_WithReferenceBlockFromClusterChain() { 387 // TODO skipping as this isn't implemented yet 388 unittest.SkipUnless(suite.T(), unittest.TEST_TODO, "skipping as this isn't implemented yet") 389 390 block := suite.Block() 391 // set genesis from cluster chain as reference block 392 block.SetPayload(model.EmptyPayload(suite.genesis.ID())) 393 err := suite.state.Extend(&block) 394 suite.Assert().Error(err) 395 } 396 397 // TestExtend_WithReferenceBlockFromDifferentEpoch tests extending the cluster state 398 // using a reference block in a different epoch than the cluster's epoch. 399 func (suite *MutatorSuite) TestExtend_WithReferenceBlockFromDifferentEpoch() { 400 // build and complete the current epoch, then use a reference block from next epoch 401 eb := unittest.NewEpochBuilder(suite.T(), suite.protoState) 402 eb.BuildEpoch().CompleteEpoch() 403 heights, ok := eb.EpochHeights(1) 404 require.True(suite.T(), ok) 405 nextEpochHeader, err := suite.protoState.AtHeight(heights.FinalHeight() + 1).Head() 406 require.NoError(suite.T(), err) 407 408 block := suite.Block() 409 block.SetPayload(model.EmptyPayload(nextEpochHeader.ID())) 410 err = suite.state.Extend(&block) 411 suite.Assert().Error(err) 412 suite.Assert().True(state.IsInvalidExtensionError(err)) 413 } 414 415 // TestExtend_WithUnfinalizedReferenceBlock tests that extending the cluster state 416 // with a reference block which is un-finalized and above the finalized boundary 417 // should be considered an unverifiable extension. It's possible that this reference 418 // block has been finalized, we just haven't processed it yet. 419 func (suite *MutatorSuite) TestExtend_WithUnfinalizedReferenceBlock() { 420 unfinalized := unittest.BlockWithParentFixture(suite.protoGenesis) 421 unfinalized.Payload.Guarantees = nil 422 unfinalized.SetPayload(*unfinalized.Payload) 423 err := suite.protoState.ExtendCertified(context.Background(), unfinalized, unittest.CertifyBlock(unfinalized.Header)) 424 suite.Require().NoError(err) 425 426 block := suite.Block() 427 block.SetPayload(model.EmptyPayload(unfinalized.ID())) 428 err = suite.state.Extend(&block) 429 suite.Assert().Error(err) 430 suite.Assert().True(state.IsUnverifiableExtensionError(err)) 431 } 432 433 // TestExtend_WithOrphanedReferenceBlock tests that extending the cluster state 434 // with a un-finalized reference block below the finalized boundary 435 // (i.e. orphaned) should be considered an invalid extension. As the proposer is supposed 436 // to only use finalized blocks as reference, the proposer knowingly generated an invalid 437 func (suite *MutatorSuite) TestExtend_WithOrphanedReferenceBlock() { 438 // create a block extending genesis which is not finalized 439 orphaned := unittest.BlockWithParentFixture(suite.protoGenesis) 440 err := suite.protoState.ExtendCertified(context.Background(), orphaned, unittest.CertifyBlock(orphaned.Header)) 441 suite.Require().NoError(err) 442 443 // create a block extending genesis (conflicting with previous) which is finalized 444 finalized := unittest.BlockWithParentFixture(suite.protoGenesis) 445 finalized.Payload.Guarantees = nil 446 finalized.SetPayload(*finalized.Payload) 447 err = suite.protoState.ExtendCertified(context.Background(), finalized, unittest.CertifyBlock(finalized.Header)) 448 suite.Require().NoError(err) 449 err = suite.protoState.Finalize(context.Background(), finalized.ID()) 450 suite.Require().NoError(err) 451 452 // test referencing the orphaned block 453 block := suite.Block() 454 block.SetPayload(model.EmptyPayload(orphaned.ID())) 455 err = suite.state.Extend(&block) 456 suite.Assert().Error(err) 457 suite.Assert().True(state.IsInvalidExtensionError(err)) 458 } 459 460 func (suite *MutatorSuite) TestExtend_UnfinalizedBlockWithDupeTx() { 461 tx1 := suite.Tx() 462 463 // create a block extending genesis containing tx1 464 block1 := suite.Block() 465 payload1 := suite.Payload(&tx1) 466 block1.SetPayload(payload1) 467 468 // should be able to extend block 1 469 err := suite.state.Extend(&block1) 470 suite.Assert().Nil(err) 471 472 // create a block building on block1 ALSO containing tx1 473 block2 := suite.BlockWithParent(&block1) 474 payload2 := suite.Payload(&tx1) 475 block2.SetPayload(payload2) 476 477 // should be unable to extend block 2, as it contains a dupe transaction 478 err = suite.state.Extend(&block2) 479 suite.Assert().Error(err) 480 suite.Assert().True(state.IsInvalidExtensionError(err)) 481 } 482 483 func (suite *MutatorSuite) TestExtend_FinalizedBlockWithDupeTx() { 484 tx1 := suite.Tx() 485 486 // create a block extending genesis containing tx1 487 block1 := suite.Block() 488 payload1 := suite.Payload(&tx1) 489 block1.SetPayload(payload1) 490 491 // should be able to extend block 1 492 err := suite.state.Extend(&block1) 493 suite.Assert().Nil(err) 494 495 // should be able to finalize block 1 496 suite.FinalizeBlock(block1) 497 suite.Assert().Nil(err) 498 499 // create a block building on block1 ALSO containing tx1 500 block2 := suite.BlockWithParent(&block1) 501 payload2 := suite.Payload(&tx1) 502 block2.SetPayload(payload2) 503 504 // should be unable to extend block 2, as it contains a dupe transaction 505 err = suite.state.Extend(&block2) 506 suite.Assert().Error(err) 507 suite.Assert().True(state.IsInvalidExtensionError(err)) 508 } 509 510 func (suite *MutatorSuite) TestExtend_ConflictingForkWithDupeTx() { 511 tx1 := suite.Tx() 512 513 // create a block extending genesis containing tx1 514 block1 := suite.Block() 515 payload1 := suite.Payload(&tx1) 516 block1.SetPayload(payload1) 517 518 // should be able to extend block 1 519 err := suite.state.Extend(&block1) 520 suite.Assert().Nil(err) 521 522 // create a block ALSO extending genesis ALSO containing tx1 523 block2 := suite.Block() 524 payload2 := suite.Payload(&tx1) 525 block2.SetPayload(payload2) 526 527 // should be able to extend block2 528 // although it conflicts with block1, it is on a different fork 529 err = suite.state.Extend(&block2) 530 suite.Assert().Nil(err) 531 } 532 533 func (suite *MutatorSuite) TestExtend_LargeHistory() { 534 t := suite.T() 535 536 // get a valid reference block ID 537 final, err := suite.protoState.Final().Head() 538 require.NoError(t, err) 539 refID := final.ID() 540 541 // keep track of the head of the chain 542 head := *suite.genesis 543 544 // keep track of transactions in orphaned forks (eligible for inclusion in future block) 545 var invalidatedTransactions []*flow.TransactionBody 546 // keep track of the oldest transactions (further back in ancestry than the expiry window) 547 var oldTransactions []*flow.TransactionBody 548 549 // create a large history of blocks with invalidated forks every 3 blocks on 550 // average - build until the height exceeds transaction expiry 551 for i := 0; ; i++ { 552 553 // create a transaction 554 tx := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { 555 tx.ReferenceBlockID = refID 556 tx.ProposalKey.SequenceNumber = uint64(i) 557 }) 558 559 // 1/3 of the time create a conflicting fork that will be invalidated 560 // don't do this the first and last few times to ensure we don't 561 // try to fork genesis and the last block is the valid fork. 562 conflicting := rand.Intn(3) == 0 && i > 5 && i < 995 563 564 // by default, build on the head - if we are building a 565 // conflicting fork, build on the parent of the head 566 parent := head 567 if conflicting { 568 err = suite.db.View(procedure.RetrieveClusterBlock(parent.Header.ParentID, &parent)) 569 assert.NoError(t, err) 570 // add the transaction to the invalidated list 571 invalidatedTransactions = append(invalidatedTransactions, &tx) 572 } else if head.Header.Height < 50 { 573 oldTransactions = append(oldTransactions, &tx) 574 } 575 576 // create a block containing the transaction 577 block := unittest.ClusterBlockWithParent(&head) 578 payload := suite.Payload(&tx) 579 block.SetPayload(payload) 580 err = suite.state.Extend(&block) 581 assert.NoError(t, err) 582 583 // reset the valid head if we aren't building a conflicting fork 584 if !conflicting { 585 head = block 586 suite.FinalizeBlock(block) 587 assert.NoError(t, err) 588 } 589 590 // stop building blocks once we've built a history which exceeds the transaction 591 // expiry length - this tests that deduplication works properly against old blocks 592 // which nevertheless have a potentially conflicting reference block 593 if head.Header.Height > flow.DefaultTransactionExpiry+100 { 594 break 595 } 596 } 597 598 t.Log("conflicting: ", len(invalidatedTransactions)) 599 600 t.Run("should be able to extend with transactions in orphaned forks", func(t *testing.T) { 601 block := unittest.ClusterBlockWithParent(&head) 602 payload := suite.Payload(invalidatedTransactions...) 603 block.SetPayload(payload) 604 err = suite.state.Extend(&block) 605 assert.NoError(t, err) 606 }) 607 608 t.Run("should be unable to extend with conflicting transactions within reference height range of extending block", func(t *testing.T) { 609 block := unittest.ClusterBlockWithParent(&head) 610 payload := suite.Payload(oldTransactions...) 611 block.SetPayload(payload) 612 err = suite.state.Extend(&block) 613 assert.Error(t, err) 614 suite.Assert().True(state.IsInvalidExtensionError(err)) 615 }) 616 }