github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/collection/ingest/engine_test.go (about)

     1  package ingest
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/rs/zerolog"
    11  	"github.com/stretchr/testify/mock"
    12  	"github.com/stretchr/testify/suite"
    13  	"golang.org/x/time/rate"
    14  
    15  	"github.com/onflow/flow-go/access"
    16  	"github.com/onflow/flow-go/engine"
    17  	"github.com/onflow/flow-go/model/flow"
    18  	"github.com/onflow/flow-go/model/flow/factory"
    19  	"github.com/onflow/flow-go/model/flow/filter"
    20  	"github.com/onflow/flow-go/module/component"
    21  	"github.com/onflow/flow-go/module/irrecoverable"
    22  	"github.com/onflow/flow-go/module/mempool"
    23  	"github.com/onflow/flow-go/module/mempool/epochs"
    24  	"github.com/onflow/flow-go/module/mempool/herocache"
    25  	"github.com/onflow/flow-go/module/metrics"
    26  	module "github.com/onflow/flow-go/module/mock"
    27  	"github.com/onflow/flow-go/network"
    28  	"github.com/onflow/flow-go/network/mocknetwork"
    29  	realprotocol "github.com/onflow/flow-go/state/protocol"
    30  	protocol "github.com/onflow/flow-go/state/protocol/mock"
    31  	"github.com/onflow/flow-go/storage"
    32  	"github.com/onflow/flow-go/utils/unittest"
    33  	"github.com/onflow/flow-go/utils/unittest/mocks"
    34  )
    35  
    36  type Suite struct {
    37  	suite.Suite
    38  
    39  	N_COLLECTORS int
    40  	N_CLUSTERS   uint
    41  
    42  	conduit *mocknetwork.Conduit
    43  	me      *module.Local
    44  	conf    Config
    45  
    46  	pools *epochs.TransactionPools
    47  
    48  	identities flow.IdentityList
    49  	clusters   flow.ClusterList
    50  
    51  	state      *protocol.State
    52  	snapshot   *protocol.Snapshot
    53  	epochQuery *mocks.EpochQuery
    54  	root       *flow.Block
    55  
    56  	// backend for mocks
    57  	blocks map[flow.Identifier]*flow.Block
    58  	final  *flow.Block
    59  
    60  	engine *Engine
    61  }
    62  
    63  func TestIngest(t *testing.T) {
    64  	suite.Run(t, new(Suite))
    65  }
    66  
    67  func (suite *Suite) SetupTest() {
    68  	var err error
    69  
    70  	suite.N_COLLECTORS = 4
    71  	suite.N_CLUSTERS = 2
    72  
    73  	log := zerolog.New(io.Discard)
    74  	metrics := metrics.NewNoopCollector()
    75  
    76  	net := new(mocknetwork.Network)
    77  	suite.conduit = new(mocknetwork.Conduit)
    78  	net.On("Register", mock.Anything, mock.Anything).Return(suite.conduit, nil).Once()
    79  
    80  	collectors := unittest.IdentityListFixture(suite.N_COLLECTORS, unittest.WithRole(flow.RoleCollection))
    81  	me := collectors[0]
    82  	others := unittest.IdentityListFixture(4, unittest.WithAllRolesExcept(flow.RoleCollection))
    83  	suite.identities = append(collectors, others...)
    84  
    85  	suite.me = new(module.Local)
    86  	suite.me.On("NodeID").Return(me.NodeID)
    87  
    88  	suite.pools = epochs.NewTransactionPools(func(_ uint64) mempool.Transactions {
    89  		return herocache.NewTransactions(1000, log, metrics)
    90  	})
    91  
    92  	assignments := unittest.ClusterAssignment(suite.N_CLUSTERS, collectors.ToSkeleton())
    93  	suite.clusters, err = factory.NewClusterList(assignments, collectors.ToSkeleton())
    94  	suite.Require().NoError(err)
    95  
    96  	suite.root = unittest.GenesisFixture()
    97  	suite.final = suite.root
    98  	suite.blocks = make(map[flow.Identifier]*flow.Block)
    99  	suite.blocks[suite.root.ID()] = suite.root
   100  
   101  	suite.state = new(protocol.State)
   102  	suite.snapshot = new(protocol.Snapshot)
   103  	suite.state.On("Final").Return(suite.snapshot)
   104  	suite.snapshot.On("Head").Return(
   105  		func() *flow.Header { return suite.final.Header },
   106  		func() error { return nil },
   107  	)
   108  	suite.state.On("AtBlockID", mock.Anything).Return(
   109  		func(blockID flow.Identifier) realprotocol.Snapshot {
   110  			snap := new(protocol.Snapshot)
   111  			block, ok := suite.blocks[blockID]
   112  			if ok {
   113  				snap.On("Head").Return(block.Header, nil)
   114  			} else {
   115  				snap.On("Head").Return(nil, storage.ErrNotFound)
   116  			}
   117  			snap.On("Epochs").Return(suite.epochQuery)
   118  			return snap
   119  		})
   120  
   121  	// set up the current epoch by default, with counter=1
   122  	epoch := new(protocol.Epoch)
   123  	epoch.On("Counter").Return(uint64(1), nil)
   124  	epoch.On("Clustering").Return(suite.clusters, nil)
   125  	suite.epochQuery = mocks.NewEpochQuery(suite.T(), 1, epoch)
   126  
   127  	suite.conf = DefaultConfig()
   128  	chain := flow.Testnet.Chain()
   129  	suite.engine, err = New(log, net, suite.state, metrics, metrics, metrics, suite.me, chain, suite.pools, suite.conf, NewAddressRateLimiter(rate.Limit(1), 1))
   130  	suite.Require().NoError(err)
   131  }
   132  
   133  func (suite *Suite) TestInvalidTransaction() {
   134  
   135  	suite.Run("missing field", func() {
   136  		tx := unittest.TransactionBodyFixture()
   137  		tx.ReferenceBlockID = suite.root.ID()
   138  		tx.Script = nil
   139  
   140  		err := suite.engine.ProcessTransaction(&tx)
   141  		suite.Assert().Error(err)
   142  		suite.Assert().True(errors.As(err, &access.IncompleteTransactionError{}))
   143  	})
   144  
   145  	suite.Run("gas limit exceeds the maximum allowed", func() {
   146  		tx := unittest.TransactionBodyFixture()
   147  		tx.Payer = unittest.RandomAddressFixture()
   148  		tx.ReferenceBlockID = suite.root.ID()
   149  		tx.GasLimit = flow.DefaultMaxTransactionGasLimit + 1
   150  
   151  		err := suite.engine.ProcessTransaction(&tx)
   152  		suite.Assert().Error(err)
   153  		suite.Assert().True(errors.As(err, &access.InvalidGasLimitError{}))
   154  	})
   155  
   156  	suite.Run("invalid reference block ID", func() {
   157  		tx := unittest.TransactionBodyFixture()
   158  		tx.ReferenceBlockID = unittest.IdentifierFixture()
   159  
   160  		err := suite.engine.ProcessTransaction(&tx)
   161  		suite.Assert().Error(err)
   162  		suite.Assert().True(errors.As(err, &engine.UnverifiableInputError{}))
   163  	})
   164  
   165  	suite.Run("un-parseable script", func() {
   166  		tx := unittest.TransactionBodyFixture()
   167  		tx.ReferenceBlockID = suite.root.ID()
   168  		tx.Script = []byte("definitely a real transaction")
   169  
   170  		err := suite.engine.ProcessTransaction(&tx)
   171  		suite.Assert().Error(err)
   172  		suite.Assert().True(errors.As(err, &access.InvalidScriptError{}))
   173  	})
   174  
   175  	suite.Run("invalid signature format", func() {
   176  		signer := flow.Testnet.Chain().ServiceAddress()
   177  		keyIndex := uint64(0)
   178  
   179  		sig1 := unittest.TransactionSignatureFixture()
   180  		sig1.KeyIndex = keyIndex
   181  		sig1.Address = signer
   182  		sig1.SignerIndex = 0
   183  
   184  		sig2 := unittest.TransactionSignatureFixture()
   185  		sig2.KeyIndex = keyIndex
   186  		sig2.Address = signer
   187  		sig2.SignerIndex = 1
   188  
   189  		suite.Run("invalid format of an envelope signature", func() {
   190  			invalidSig := unittest.InvalidFormatSignature()
   191  			tx := unittest.TransactionBodyFixture()
   192  			tx.ReferenceBlockID = suite.root.ID()
   193  			tx.EnvelopeSignatures[0] = invalidSig
   194  
   195  			err := suite.engine.ProcessTransaction(&tx)
   196  			suite.Assert().Error(err)
   197  			suite.Assert().True(errors.As(err, &access.InvalidSignatureError{}))
   198  		})
   199  
   200  		suite.Run("invalid format of a payload signature", func() {
   201  			invalidSig := unittest.InvalidFormatSignature()
   202  			tx := unittest.TransactionBodyFixture()
   203  			tx.ReferenceBlockID = suite.root.ID()
   204  			tx.PayloadSignatures = []flow.TransactionSignature{invalidSig}
   205  
   206  			err := suite.engine.ProcessTransaction(&tx)
   207  			suite.Assert().Error(err)
   208  			suite.Assert().True(errors.As(err, &access.InvalidSignatureError{}))
   209  		})
   210  
   211  		suite.Run("duplicated signature (envelope only)", func() {
   212  			tx := unittest.TransactionBodyFixture()
   213  			tx.ReferenceBlockID = suite.root.ID()
   214  			tx.EnvelopeSignatures = []flow.TransactionSignature{sig1, sig2}
   215  			err := suite.engine.ProcessTransaction(&tx)
   216  			suite.Assert().Error(err)
   217  			suite.Assert().True(errors.As(err, &access.DuplicatedSignatureError{}))
   218  		})
   219  
   220  		suite.Run("duplicated signature (payload only)", func() {
   221  			tx := unittest.TransactionBodyFixture()
   222  			tx.ReferenceBlockID = suite.root.ID()
   223  			tx.PayloadSignatures = []flow.TransactionSignature{sig1, sig2}
   224  
   225  			err := suite.engine.ProcessTransaction(&tx)
   226  			suite.Assert().Error(err)
   227  			suite.Assert().True(errors.As(err, &access.DuplicatedSignatureError{}))
   228  		})
   229  
   230  		suite.Run("duplicated signature (cross case)", func() {
   231  			tx := unittest.TransactionBodyFixture()
   232  			tx.ReferenceBlockID = suite.root.ID()
   233  			tx.PayloadSignatures = []flow.TransactionSignature{sig1}
   234  			tx.EnvelopeSignatures = []flow.TransactionSignature{sig2}
   235  
   236  			err := suite.engine.ProcessTransaction(&tx)
   237  			suite.Assert().Error(err)
   238  			suite.Assert().True(errors.As(err, &access.DuplicatedSignatureError{}))
   239  		})
   240  	})
   241  
   242  	suite.Run("invalid signature", func() {
   243  		// TODO cannot check signatures in MVP
   244  		unittest.SkipUnless(suite.T(), unittest.TEST_TODO, "skipping unimplemented test")
   245  	})
   246  
   247  	suite.Run("invalid address", func() {
   248  		invalid := unittest.InvalidAddressFixture()
   249  		tx := unittest.TransactionBodyFixture()
   250  		tx.ReferenceBlockID = suite.root.ID()
   251  		tx.Payer = invalid
   252  
   253  		err := suite.engine.ProcessTransaction(&tx)
   254  		suite.Assert().Error(err)
   255  		suite.Assert().True(errors.As(err, &access.InvalidAddressError{}))
   256  	})
   257  
   258  	suite.Run("expired reference block ID", func() {
   259  		// "finalize" a sufficiently high block that root block is expired
   260  		final := unittest.BlockFixture()
   261  		final.Header.Height = suite.root.Header.Height + flow.DefaultTransactionExpiry + 1
   262  		suite.final = &final
   263  
   264  		tx := unittest.TransactionBodyFixture()
   265  		tx.ReferenceBlockID = suite.root.ID()
   266  
   267  		err := suite.engine.ProcessTransaction(&tx)
   268  		suite.Assert().Error(err)
   269  		suite.Assert().True(errors.As(err, &access.ExpiredTransactionError{}))
   270  	})
   271  
   272  }
   273  
   274  // should return an error if the engine is shutdown and not processing transactions
   275  func (suite *Suite) TestComponentShutdown() {
   276  	tx := unittest.TransactionBodyFixture()
   277  	tx.ReferenceBlockID = suite.root.ID()
   278  
   279  	// start then shut down the engine
   280  	parentCtx, cancel := context.WithCancel(context.Background())
   281  	ctx, _ := irrecoverable.WithSignaler(parentCtx)
   282  	suite.engine.Start(ctx)
   283  	unittest.AssertClosesBefore(suite.T(), suite.engine.Ready(), 10*time.Millisecond)
   284  	cancel()
   285  	unittest.AssertClosesBefore(suite.T(), suite.engine.ShutdownSignal(), 10*time.Millisecond)
   286  
   287  	err := suite.engine.ProcessTransaction(&tx)
   288  	suite.Assert().ErrorIs(err, component.ErrComponentShutdown)
   289  }
   290  
   291  // should store transactions for local cluster and propagate to other cluster members
   292  func (suite *Suite) TestRoutingLocalCluster() {
   293  
   294  	local, _, ok := suite.clusters.ByNodeID(suite.me.NodeID())
   295  	suite.Require().True(ok)
   296  
   297  	// get a transaction that will be routed to local cluster
   298  	tx := unittest.TransactionBodyFixture()
   299  	tx.ReferenceBlockID = suite.root.ID()
   300  	tx = unittest.AlterTransactionForCluster(tx, suite.clusters, local, func(transaction *flow.TransactionBody) {})
   301  
   302  	// should route to local cluster
   303  	suite.conduit.
   304  		On("Multicast", &tx, suite.conf.PropagationRedundancy+1, local.NodeIDs()[0], local.NodeIDs()[1]).
   305  		Return(nil)
   306  
   307  	err := suite.engine.ProcessTransaction(&tx)
   308  	suite.Assert().NoError(err)
   309  
   310  	// should be added to local mempool for the current epoch
   311  	counter, err := suite.epochQuery.Current().Counter()
   312  	suite.Assert().NoError(err)
   313  	suite.Assert().True(suite.pools.ForEpoch(counter).Has(tx.ID()))
   314  	suite.conduit.AssertExpectations(suite.T())
   315  }
   316  
   317  // should not store transactions for a different cluster and should propagate
   318  // to the responsible cluster
   319  func (suite *Suite) TestRoutingRemoteCluster() {
   320  
   321  	// find a remote cluster
   322  	_, index, ok := suite.clusters.ByNodeID(suite.me.NodeID())
   323  	suite.Require().True(ok)
   324  	remote, ok := suite.clusters.ByIndex((index + 1) % suite.N_CLUSTERS)
   325  	suite.Require().True(ok)
   326  
   327  	// get a transaction that will be routed to remote cluster
   328  	tx := unittest.TransactionBodyFixture()
   329  	tx.ReferenceBlockID = suite.root.ID()
   330  	tx = unittest.AlterTransactionForCluster(tx, suite.clusters, remote, func(transaction *flow.TransactionBody) {})
   331  
   332  	// should route to remote cluster
   333  	suite.conduit.
   334  		On("Multicast", &tx, suite.conf.PropagationRedundancy+1, remote[0].NodeID, remote[1].NodeID).
   335  		Return(nil)
   336  
   337  	err := suite.engine.ProcessTransaction(&tx)
   338  	suite.Assert().NoError(err)
   339  
   340  	// should not be added to local mempool
   341  	counter, err := suite.epochQuery.Current().Counter()
   342  	suite.Assert().NoError(err)
   343  	suite.Assert().False(suite.pools.ForEpoch(counter).Has(tx.ID()))
   344  	suite.conduit.AssertExpectations(suite.T())
   345  }
   346  
   347  // should not store transactions for a different cluster and should not fail when propagating
   348  // to an empty cluster
   349  func (suite *Suite) TestRoutingToRemoteClusterWithNoNodes() {
   350  
   351  	// find a remote cluster
   352  	_, index, ok := suite.clusters.ByNodeID(suite.me.NodeID())
   353  	suite.Require().True(ok)
   354  
   355  	// set the next cluster to be empty
   356  	emptyIdentityList := flow.IdentitySkeletonList{}
   357  	nextClusterIndex := (index + 1) % suite.N_CLUSTERS
   358  	suite.clusters[nextClusterIndex] = emptyIdentityList
   359  
   360  	// get a transaction that will be routed to remote cluster
   361  	tx := unittest.TransactionBodyFixture()
   362  	tx.ReferenceBlockID = suite.root.ID()
   363  	tx = unittest.AlterTransactionForCluster(tx, suite.clusters, emptyIdentityList, func(transaction *flow.TransactionBody) {})
   364  
   365  	// should attempt route to remote cluster without providing any node ids
   366  	suite.conduit.
   367  		On("Multicast", &tx, suite.conf.PropagationRedundancy+1).
   368  		Return(network.EmptyTargetList)
   369  
   370  	err := suite.engine.ProcessTransaction(&tx)
   371  	suite.Assert().NoError(err)
   372  
   373  	// should not be added to local mempool
   374  	counter, err := suite.epochQuery.Current().Counter()
   375  	suite.Assert().NoError(err)
   376  	suite.Assert().False(suite.pools.ForEpoch(counter).Has(tx.ID()))
   377  	suite.conduit.AssertExpectations(suite.T())
   378  }
   379  
   380  // should not propagate transactions received from another node (that node is
   381  // responsible for propagation)
   382  func (suite *Suite) TestRoutingLocalClusterFromOtherNode() {
   383  
   384  	local, _, ok := suite.clusters.ByNodeID(suite.me.NodeID())
   385  	suite.Require().True(ok)
   386  
   387  	// another node will send us the transaction
   388  	sender := local.Filter(filter.Not(filter.HasNodeID[flow.IdentitySkeleton](suite.me.NodeID())))[0]
   389  
   390  	// get a transaction that will be routed to local cluster
   391  	tx := unittest.TransactionBodyFixture()
   392  	tx.ReferenceBlockID = suite.root.ID()
   393  	tx = unittest.AlterTransactionForCluster(tx, suite.clusters, local, func(transaction *flow.TransactionBody) {})
   394  
   395  	// should not route to any node
   396  	suite.conduit.AssertNumberOfCalls(suite.T(), "Multicast", 0)
   397  
   398  	err := suite.engine.onTransaction(sender.NodeID, &tx)
   399  	suite.Assert().NoError(err)
   400  
   401  	// should be added to local mempool for current epoch
   402  	counter, err := suite.epochQuery.Current().Counter()
   403  	suite.Assert().NoError(err)
   404  	suite.Assert().True(suite.pools.ForEpoch(counter).Has(tx.ID()))
   405  	suite.conduit.AssertExpectations(suite.T())
   406  }
   407  
   408  // should not route or store invalid transactions
   409  func (suite *Suite) TestRoutingInvalidTransaction() {
   410  
   411  	// find a remote cluster
   412  	_, index, ok := suite.clusters.ByNodeID(suite.me.NodeID())
   413  	suite.Require().True(ok)
   414  	remote, ok := suite.clusters.ByIndex((index + 1) % suite.N_CLUSTERS)
   415  	suite.Require().True(ok)
   416  
   417  	// get transaction for target cluster, but make it invalid
   418  	tx := unittest.TransactionBodyFixture()
   419  	tx = unittest.AlterTransactionForCluster(tx, suite.clusters, remote,
   420  		func(tx *flow.TransactionBody) {
   421  			tx.GasLimit = 0
   422  		})
   423  
   424  	// should not route to any node
   425  	suite.conduit.AssertNumberOfCalls(suite.T(), "Multicast", 0)
   426  
   427  	_ = suite.engine.ProcessTransaction(&tx)
   428  
   429  	// should not be added to local mempool
   430  	counter, err := suite.epochQuery.Current().Counter()
   431  	suite.Assert().NoError(err)
   432  	suite.Assert().False(suite.pools.ForEpoch(counter).Has(tx.ID()))
   433  	suite.conduit.AssertExpectations(suite.T())
   434  }
   435  
   436  // We should route to the appropriate cluster if our cluster assignment changes
   437  // on an epoch boundary. In this test, the clusters in epoch 2 are the reverse
   438  // of those in epoch 1, and we check that the transaction is routed based on
   439  // the clustering in epoch 2.
   440  func (suite *Suite) TestRouting_ClusterAssignmentChanged() {
   441  
   442  	epoch2Clusters := flow.ClusterList{
   443  		suite.clusters[1],
   444  		suite.clusters[0],
   445  	}
   446  	epoch2 := new(protocol.Epoch)
   447  	epoch2.On("Counter").Return(uint64(2), nil)
   448  	epoch2.On("Clustering").Return(epoch2Clusters, nil)
   449  	// update the mocks to behave as though we have transitioned to epoch 2
   450  	suite.epochQuery.Add(epoch2)
   451  	suite.epochQuery.Transition()
   452  
   453  	// get the local cluster in epoch 2
   454  	epoch2Local, _, ok := epoch2Clusters.ByNodeID(suite.me.NodeID())
   455  	suite.Require().True(ok)
   456  
   457  	// get a transaction that will be routed to local cluster
   458  	tx := unittest.TransactionBodyFixture()
   459  	tx.ReferenceBlockID = suite.root.ID()
   460  	tx = unittest.AlterTransactionForCluster(tx, epoch2Clusters, epoch2Local, func(transaction *flow.TransactionBody) {})
   461  
   462  	// should route to local cluster
   463  	suite.conduit.On("Multicast", &tx, suite.conf.PropagationRedundancy+1, epoch2Local.NodeIDs()[0], epoch2Local.NodeIDs()[1]).Return(nil).Once()
   464  
   465  	err := suite.engine.ProcessTransaction(&tx)
   466  	suite.Assert().NoError(err)
   467  
   468  	// should add to local mempool for epoch 2 only
   469  	suite.Assert().True(suite.pools.ForEpoch(2).Has(tx.ID()))
   470  	suite.Assert().False(suite.pools.ForEpoch(1).Has(tx.ID()))
   471  	suite.conduit.AssertExpectations(suite.T())
   472  }
   473  
   474  // We will discard all transactions when we aren't assigned to any cluster.
   475  func (suite *Suite) TestRouting_ClusterAssignmentRemoved() {
   476  
   477  	// remove ourselves from the cluster assignment for epoch 2
   478  	withoutMe := suite.identities.
   479  		Filter(filter.Not(filter.HasNodeID[flow.Identity](suite.me.NodeID()))).
   480  		Filter(filter.HasRole[flow.Identity](flow.RoleCollection)).ToSkeleton()
   481  	epoch2Assignment := unittest.ClusterAssignment(suite.N_CLUSTERS, withoutMe)
   482  	epoch2Clusters, err := factory.NewClusterList(epoch2Assignment, withoutMe)
   483  	suite.Require().NoError(err)
   484  
   485  	epoch2 := new(protocol.Epoch)
   486  	epoch2.On("Counter").Return(uint64(2), nil)
   487  	epoch2.On("InitialIdentities").Return(withoutMe, nil)
   488  	epoch2.On("Clustering").Return(epoch2Clusters, nil)
   489  	// update the mocks to behave as though we have transitioned to epoch 2
   490  	suite.epochQuery.Add(epoch2)
   491  	suite.epochQuery.Transition()
   492  
   493  	// any transaction is OK here, since we're not in any cluster
   494  	tx := unittest.TransactionBodyFixture()
   495  	tx.ReferenceBlockID = suite.root.ID()
   496  
   497  	err = suite.engine.ProcessTransaction(&tx)
   498  	suite.Assert().Error(err)
   499  
   500  	// should not add to mempool
   501  	suite.Assert().False(suite.pools.ForEpoch(2).Has(tx.ID()))
   502  	suite.Assert().False(suite.pools.ForEpoch(1).Has(tx.ID()))
   503  	// should not propagate
   504  	suite.conduit.AssertNumberOfCalls(suite.T(), "Multicast", 0)
   505  }
   506  
   507  // The node is not a participant in epoch 2 and joins in epoch 3. We start the
   508  // test in epoch 2.
   509  //
   510  // Test that the node discards transactions in epoch 2 and handles them
   511  // in epoch 3.
   512  func (suite *Suite) TestRouting_ClusterAssignmentAdded() {
   513  
   514  	// EPOCH 2:
   515  
   516  	// remove ourselves from the cluster assignment for epoch 2
   517  	withoutMe := suite.identities.
   518  		Filter(filter.Not(filter.HasNodeID[flow.Identity](suite.me.NodeID()))).
   519  		Filter(filter.HasRole[flow.Identity](flow.RoleCollection)).ToSkeleton()
   520  	epoch2Assignment := unittest.ClusterAssignment(suite.N_CLUSTERS, withoutMe)
   521  	epoch2Clusters, err := factory.NewClusterList(epoch2Assignment, withoutMe)
   522  	suite.Require().NoError(err)
   523  
   524  	epoch2 := new(protocol.Epoch)
   525  	epoch2.On("Counter").Return(uint64(2), nil)
   526  	epoch2.On("InitialIdentities").Return(withoutMe, nil)
   527  	epoch2.On("Clustering").Return(epoch2Clusters, nil)
   528  	// update the mocks to behave as though we have transitioned to epoch 2
   529  	suite.epochQuery.Add(epoch2)
   530  	suite.epochQuery.Transition()
   531  
   532  	// any transaction is OK here, since we're not in any cluster
   533  	tx := unittest.TransactionBodyFixture()
   534  	tx.ReferenceBlockID = suite.root.ID()
   535  
   536  	err = suite.engine.ProcessTransaction(&tx)
   537  	suite.Assert().Error(err)
   538  
   539  	// should not add to mempool
   540  	suite.Assert().False(suite.pools.ForEpoch(2).Has(tx.ID()))
   541  	suite.Assert().False(suite.pools.ForEpoch(1).Has(tx.ID()))
   542  	// should not propagate
   543  	suite.conduit.AssertNumberOfCalls(suite.T(), "Multicast", 0)
   544  
   545  	// EPOCH 3:
   546  
   547  	// include ourselves in cluster assignment
   548  	withMe := suite.identities.Filter(filter.HasRole[flow.Identity](flow.RoleCollection)).ToSkeleton()
   549  	epoch3Assignment := unittest.ClusterAssignment(suite.N_CLUSTERS, withMe)
   550  	epoch3Clusters, err := factory.NewClusterList(epoch3Assignment, withMe)
   551  	suite.Require().NoError(err)
   552  
   553  	epoch3 := new(protocol.Epoch)
   554  	epoch3.On("Counter").Return(uint64(3), nil)
   555  	epoch3.On("Clustering").Return(epoch3Clusters, nil)
   556  	// transition to epoch 3
   557  	suite.epochQuery.Add(epoch3)
   558  	suite.epochQuery.Transition()
   559  
   560  	// get the local cluster in epoch 2
   561  	epoch3Local, _, ok := epoch3Clusters.ByNodeID(suite.me.NodeID())
   562  	suite.Require().True(ok)
   563  
   564  	// get a transaction that will be routed to local cluster
   565  	tx = unittest.TransactionBodyFixture()
   566  	tx.ReferenceBlockID = suite.root.ID()
   567  	tx = unittest.AlterTransactionForCluster(tx, epoch3Clusters, epoch3Local, func(transaction *flow.TransactionBody) {})
   568  
   569  	// should route to local cluster
   570  	suite.conduit.On("Multicast", &tx, suite.conf.PropagationRedundancy+1, epoch3Local.NodeIDs()[0], epoch3Local.NodeIDs()[1]).Return(nil).Once()
   571  
   572  	err = suite.engine.ProcessTransaction(&tx)
   573  	suite.Assert().NoError(err)
   574  }