github.com/onflow/flow-go@v0.33.17/engine/consensus/ingestion/core_test.go (about)

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