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 }