github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/consensus/ingestion/core_test.go (about)

     1  package ingestion
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/stretchr/testify/mock"
     7  	"github.com/stretchr/testify/require"
     8  	"github.com/stretchr/testify/suite"
     9  
    10  	"github.com/onflow/flow-go/engine"
    11  	"github.com/onflow/flow-go/model/flow"
    12  	mockmempool "github.com/onflow/flow-go/module/mempool/mock"
    13  	"github.com/onflow/flow-go/module/metrics"
    14  	"github.com/onflow/flow-go/module/signature"
    15  	"github.com/onflow/flow-go/module/trace"
    16  	"github.com/onflow/flow-go/state/cluster"
    17  	"github.com/onflow/flow-go/state/protocol"
    18  	mockprotocol "github.com/onflow/flow-go/state/protocol/mock"
    19  	mockstorage "github.com/onflow/flow-go/storage/mock"
    20  	"github.com/onflow/flow-go/utils/unittest"
    21  )
    22  
    23  func TestIngestionCore(t *testing.T) {
    24  	suite.Run(t, new(IngestionCoreSuite))
    25  }
    26  
    27  type IngestionCoreSuite struct {
    28  	suite.Suite
    29  
    30  	accessID flow.Identifier
    31  	collID   flow.Identifier
    32  	conID    flow.Identifier
    33  	execID   flow.Identifier
    34  	verifID  flow.Identifier
    35  	head     *flow.Header
    36  
    37  	finalIdentities flow.IdentityList // identities at finalized state
    38  	refIdentities   flow.IdentityList // identities at reference block state
    39  	epochCounter    uint64            // epoch for the cluster originating the guarantee
    40  	clusterMembers  flow.IdentityList // members of the cluster originating the guarantee
    41  	clusterID       flow.ChainID      // chain ID of the cluster originating the guarantee
    42  
    43  	final *mockprotocol.Snapshot // finalized state snapshot
    44  	ref   *mockprotocol.Snapshot // state snapshot w.r.t. reference block
    45  
    46  	query   *mockprotocol.EpochQuery
    47  	epoch   *mockprotocol.Epoch
    48  	headers *mockstorage.Headers
    49  	pool    *mockmempool.Guarantees
    50  
    51  	core *Core
    52  }
    53  
    54  func (suite *IngestionCoreSuite) SetupTest() {
    55  
    56  	head := unittest.BlockHeaderFixture()
    57  	head.Height = 2 * flow.DefaultTransactionExpiry
    58  
    59  	access := unittest.IdentityFixture(unittest.WithRole(flow.RoleAccess))
    60  	con := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus))
    61  	coll := unittest.IdentityFixture(unittest.WithRole(flow.RoleCollection))
    62  	exec := unittest.IdentityFixture(unittest.WithRole(flow.RoleExecution))
    63  	verif := unittest.IdentityFixture(unittest.WithRole(flow.RoleVerification))
    64  
    65  	suite.accessID = access.NodeID
    66  	suite.conID = con.NodeID
    67  	suite.collID = coll.NodeID
    68  	suite.execID = exec.NodeID
    69  	suite.verifID = verif.NodeID
    70  
    71  	suite.epochCounter = 1
    72  	suite.clusterMembers = flow.IdentityList{coll}
    73  	suite.clusterID = cluster.CanonicalClusterID(suite.epochCounter, suite.clusterMembers.NodeIDs())
    74  
    75  	identities := flow.IdentityList{access, con, coll, exec, verif}
    76  	suite.finalIdentities = identities.Copy()
    77  	suite.refIdentities = identities.Copy()
    78  
    79  	metrics := metrics.NewNoopCollector()
    80  	tracer := trace.NewNoopTracer()
    81  	state := &mockprotocol.State{}
    82  	final := &mockprotocol.Snapshot{}
    83  	ref := &mockprotocol.Snapshot{}
    84  	suite.query = &mockprotocol.EpochQuery{}
    85  	suite.epoch = &mockprotocol.Epoch{}
    86  	headers := &mockstorage.Headers{}
    87  	pool := &mockmempool.Guarantees{}
    88  	cluster := &mockprotocol.Cluster{}
    89  
    90  	// this state basically works like a normal protocol state
    91  	// returning everything correctly, using the created header
    92  	// as head of the protocol state
    93  	state.On("Final").Return(final)
    94  	final.On("Head").Return(head, nil)
    95  	final.On("Identity", mock.Anything).Return(
    96  		func(nodeID flow.Identifier) *flow.Identity {
    97  			identity, _ := suite.finalIdentities.ByNodeID(nodeID)
    98  			return identity
    99  		},
   100  		func(nodeID flow.Identifier) error {
   101  			_, ok := suite.finalIdentities.ByNodeID(nodeID)
   102  			if !ok {
   103  				return protocol.IdentityNotFoundError{NodeID: nodeID}
   104  			}
   105  			return nil
   106  		},
   107  	)
   108  	final.On("Identities", mock.Anything).Return(
   109  		func(selector flow.IdentityFilter[flow.Identity]) flow.IdentityList {
   110  			return suite.finalIdentities.Filter(selector)
   111  		},
   112  		nil,
   113  	)
   114  	ref.On("Epochs").Return(suite.query)
   115  	suite.query.On("Current").Return(suite.epoch)
   116  	cluster.On("Members").Return(suite.clusterMembers.ToSkeleton())
   117  	suite.epoch.On("ClusterByChainID", mock.Anything).Return(
   118  		func(chainID flow.ChainID) protocol.Cluster {
   119  			if chainID == suite.clusterID {
   120  				return cluster
   121  			}
   122  			return nil
   123  		},
   124  		func(chainID flow.ChainID) error {
   125  			if chainID == suite.clusterID {
   126  				return nil
   127  			}
   128  			return protocol.ErrClusterNotFound
   129  		})
   130  
   131  	state.On("AtBlockID", mock.Anything).Return(ref)
   132  	ref.On("Identity", mock.Anything).Return(
   133  		func(nodeID flow.Identifier) *flow.Identity {
   134  			identity, _ := suite.refIdentities.ByNodeID(nodeID)
   135  			return identity
   136  		},
   137  		func(nodeID flow.Identifier) error {
   138  			_, ok := suite.refIdentities.ByNodeID(nodeID)
   139  			if !ok {
   140  				return protocol.IdentityNotFoundError{NodeID: nodeID}
   141  			}
   142  			return nil
   143  		},
   144  	)
   145  
   146  	// we need to return the head as it's also used as reference block
   147  	headers.On("ByBlockID", head.ID()).Return(head, nil)
   148  
   149  	// only used for metrics, nobody cares
   150  	pool.On("Size").Return(uint(0))
   151  
   152  	ingest := NewCore(unittest.Logger(), tracer, metrics, state, headers, pool)
   153  
   154  	suite.head = head
   155  	suite.final = final
   156  	suite.ref = ref
   157  	suite.headers = headers
   158  	suite.pool = pool
   159  	suite.core = ingest
   160  }
   161  
   162  func (suite *IngestionCoreSuite) TestOnGuaranteeNewFromCollection() {
   163  
   164  	guarantee := suite.validGuarantee()
   165  
   166  	// the guarantee is not part of the memory pool yet
   167  	suite.pool.On("Has", guarantee.ID()).Return(false)
   168  	suite.pool.On("Add", guarantee).Return(true)
   169  
   170  	// submit the guarantee as if it was sent by a collection node
   171  	err := suite.core.OnGuarantee(suite.collID, guarantee)
   172  	suite.Assert().NoError(err, "should not error on new guarantee from collection node")
   173  
   174  	// check that the guarantee has been added to the mempool
   175  	suite.pool.AssertCalled(suite.T(), "Add", guarantee)
   176  
   177  }
   178  
   179  func (suite *IngestionCoreSuite) TestOnGuaranteeOld() {
   180  
   181  	guarantee := suite.validGuarantee()
   182  
   183  	// the guarantee is part of the memory pool
   184  	suite.pool.On("Has", guarantee.ID()).Return(true)
   185  	suite.pool.On("Add", guarantee).Return(true)
   186  
   187  	// submit the guarantee as if it was sent by a collection node
   188  	err := suite.core.OnGuarantee(suite.collID, guarantee)
   189  	suite.Assert().NoError(err, "should not error on old guarantee")
   190  
   191  	// check that the guarantee has _not_ been added to the mempool
   192  	suite.pool.AssertNotCalled(suite.T(), "Add", guarantee)
   193  
   194  }
   195  
   196  func (suite *IngestionCoreSuite) TestOnGuaranteeNotAdded() {
   197  
   198  	guarantee := suite.validGuarantee()
   199  
   200  	// the guarantee is not already part of the memory pool
   201  	suite.pool.On("Has", guarantee.ID()).Return(false)
   202  	suite.pool.On("Add", guarantee).Return(false)
   203  
   204  	// submit the guarantee as if it was sent by a collection node
   205  	err := suite.core.OnGuarantee(suite.collID, guarantee)
   206  	suite.Assert().NoError(err, "should not error when guarantee was already added")
   207  
   208  	// check that the guarantee has been added to the mempool
   209  	suite.pool.AssertCalled(suite.T(), "Add", guarantee)
   210  
   211  }
   212  
   213  // TestOnGuaranteeNoGuarantors tests that a collection without any guarantors is rejected.
   214  // We expect an engine.InvalidInputError.
   215  func (suite *IngestionCoreSuite) TestOnGuaranteeNoGuarantors() {
   216  	// create a guarantee without any signers
   217  	guarantee := suite.validGuarantee()
   218  	guarantee.SignerIndices = nil
   219  
   220  	// the guarantee is not part of the memory pool
   221  	suite.pool.On("Has", guarantee.ID()).Return(false)
   222  	suite.pool.On("Add", guarantee).Return(true)
   223  
   224  	// submit the guarantee as if it was sent by a consensus node
   225  	err := suite.core.OnGuarantee(suite.collID, guarantee)
   226  	suite.Assert().Error(err, "should error with missing guarantor")
   227  	suite.Assert().True(engine.IsInvalidInputError(err))
   228  
   229  	// check that the guarantee has _not_ been added to the mempool
   230  	suite.pool.AssertNotCalled(suite.T(), "Add", guarantee)
   231  }
   232  
   233  func (suite *IngestionCoreSuite) TestOnGuaranteeExpired() {
   234  
   235  	// create an alternative block
   236  	header := unittest.BlockHeaderFixture()
   237  	header.Height = suite.head.Height - flow.DefaultTransactionExpiry - 1
   238  	suite.headers.On("ByBlockID", header.ID()).Return(header, nil)
   239  
   240  	// create a guarantee signed by the collection node and referencing the
   241  	// current head of the protocol state
   242  	guarantee := suite.validGuarantee()
   243  	guarantee.ReferenceBlockID = header.ID()
   244  
   245  	// the guarantee is not part of the memory pool
   246  	suite.pool.On("Has", guarantee.ID()).Return(false)
   247  	suite.pool.On("Add", guarantee).Return(true)
   248  
   249  	// submit the guarantee as if it was sent by a consensus node
   250  	err := suite.core.OnGuarantee(suite.collID, guarantee)
   251  	suite.Assert().Error(err, "should error with expired collection")
   252  	suite.Assert().True(engine.IsOutdatedInputError(err))
   253  }
   254  
   255  // TestOnGuaranteeReferenceBlockFromWrongEpoch validates that guarantees which contain a ChainID
   256  // that is inconsistent with the reference block (ie. the ChainID either refers to a non-existent
   257  // cluster, or a cluster for a different epoch) should be considered invalid inputs.
   258  func (suite *IngestionCoreSuite) TestOnGuaranteeReferenceBlockFromWrongEpoch() {
   259  	// create a guarantee from a cluster in a different epoch
   260  	guarantee := suite.validGuarantee()
   261  	guarantee.ChainID = cluster.CanonicalClusterID(suite.epochCounter+1, suite.clusterMembers.NodeIDs())
   262  
   263  	// the guarantee is not part of the memory pool
   264  	suite.pool.On("Has", guarantee.ID()).Return(false)
   265  
   266  	// submit the guarantee as if it was sent by a collection node
   267  	err := suite.core.OnGuarantee(suite.collID, guarantee)
   268  	suite.Assert().Error(err, "should error with expired collection")
   269  	suite.Assert().True(engine.IsInvalidInputError(err))
   270  }
   271  
   272  // TestOnGuaranteeInvalidGuarantor verifiers that collections with any _unknown_
   273  // signer are rejected.
   274  func (suite *IngestionCoreSuite) TestOnGuaranteeInvalidGuarantor() {
   275  
   276  	// create a guarantee  and add random (unknown) signer ID
   277  	guarantee := suite.validGuarantee()
   278  	guarantee.SignerIndices = []byte{4}
   279  
   280  	// the guarantee is not part of the memory pool
   281  	suite.pool.On("Has", guarantee.ID()).Return(false)
   282  	suite.pool.On("Add", guarantee).Return(true)
   283  
   284  	// submit the guarantee as if it was sent by a collection node
   285  	err := suite.core.OnGuarantee(suite.collID, guarantee)
   286  	suite.Assert().Error(err, "should error with invalid guarantor")
   287  	suite.Assert().True(engine.IsInvalidInputError(err), err)
   288  	suite.Assert().True(signature.IsInvalidSignerIndicesError(err), err)
   289  
   290  	// check that the guarantee has _not_ been added to the mempool
   291  	suite.pool.AssertNotCalled(suite.T(), "Add", guarantee)
   292  }
   293  
   294  // test that just after an epoch boundary we still accept guarantees from collectors
   295  // in clusters from the previous epoch (and collectors which are leaving the network
   296  // at this epoch boundary).
   297  func (suite *IngestionCoreSuite) TestOnGuaranteeEpochEnd() {
   298  
   299  	// The finalized state contains the identity of a collector that:
   300  	//  * was active in the previous epoch but is leaving as of the current epoch
   301  	//  * wasn't ejected and has positive initial weight
   302  	// This happens when we finalize the final block of the epoch during
   303  	// which this node requested to unstake
   304  	colID, ok := suite.finalIdentities.ByNodeID(suite.collID)
   305  	suite.Require().True(ok)
   306  	colID.EpochParticipationStatus = flow.EpochParticipationStatusLeaving
   307  
   308  	guarantee := suite.validGuarantee()
   309  
   310  	// the guarantee is not part of the memory pool
   311  	suite.pool.On("Has", guarantee.ID()).Return(false)
   312  	suite.pool.On("Add", guarantee).Return(true).Once()
   313  
   314  	// submit the guarantee as if it was sent by the collection node which
   315  	// is leaving at the current epoch boundary
   316  	err := suite.core.OnGuarantee(suite.collID, guarantee)
   317  	suite.Assert().NoError(err, "should not error with collector from ending epoch")
   318  
   319  	// check that the guarantee has been added to the mempool
   320  	suite.pool.AssertExpectations(suite.T())
   321  }
   322  
   323  func (suite *IngestionCoreSuite) TestOnGuaranteeUnknownOrigin() {
   324  
   325  	guarantee := suite.validGuarantee()
   326  
   327  	// the guarantee is not part of the memory pool
   328  	suite.pool.On("Has", guarantee.ID()).Return(false)
   329  	suite.pool.On("Add", guarantee).Return(true)
   330  
   331  	// submit the guarantee with an unknown origin
   332  	err := suite.core.OnGuarantee(unittest.IdentifierFixture(), guarantee)
   333  	suite.Assert().Error(err)
   334  	suite.Assert().True(engine.IsInvalidInputError(err))
   335  
   336  	suite.pool.AssertNotCalled(suite.T(), "Add", guarantee)
   337  
   338  }
   339  
   340  // validGuarantee returns a valid collection guarantee based on the suite state.
   341  func (suite *IngestionCoreSuite) validGuarantee() *flow.CollectionGuarantee {
   342  	guarantee := unittest.CollectionGuaranteeFixture()
   343  	guarantee.ChainID = suite.clusterID
   344  
   345  	signerIndices, err := signature.EncodeSignersToIndices(
   346  		[]flow.Identifier{suite.collID}, []flow.Identifier{suite.collID})
   347  	require.NoError(suite.T(), err)
   348  
   349  	guarantee.SignerIndices = signerIndices
   350  	guarantee.ReferenceBlockID = suite.head.ID()
   351  	return guarantee
   352  }