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 }