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  }