github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/committees/consensus_committee_test.go (about) 1 package committees 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "testing" 8 "time" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/mock" 12 "github.com/stretchr/testify/require" 13 "github.com/stretchr/testify/suite" 14 15 "github.com/onflow/flow-go/consensus/hotstuff/model" 16 "github.com/onflow/flow-go/model/flow" 17 "github.com/onflow/flow-go/model/flow/mapfunc" 18 "github.com/onflow/flow-go/module/irrecoverable" 19 "github.com/onflow/flow-go/state/protocol" 20 protocolmock "github.com/onflow/flow-go/state/protocol/mock" 21 "github.com/onflow/flow-go/state/protocol/prg" 22 "github.com/onflow/flow-go/utils/unittest" 23 "github.com/onflow/flow-go/utils/unittest/mocks" 24 ) 25 26 func TestConsensusCommittee(t *testing.T) { 27 suite.Run(t, new(ConsensusSuite)) 28 } 29 30 type ConsensusSuite struct { 31 suite.Suite 32 33 // mocks 34 state *protocolmock.State 35 snapshot *protocolmock.Snapshot 36 params *protocolmock.Params 37 epochs *mocks.EpochQuery 38 39 // backend for mocked functions 40 phase flow.EpochPhase 41 epochFallbackTriggered bool 42 currentEpochCounter uint64 43 myID flow.Identifier 44 45 committee *Consensus 46 cancel context.CancelFunc 47 } 48 49 func (suite *ConsensusSuite) SetupTest() { 50 suite.phase = flow.EpochPhaseStaking 51 suite.epochFallbackTriggered = false 52 suite.currentEpochCounter = 1 53 suite.myID = unittest.IdentifierFixture() 54 55 suite.state = new(protocolmock.State) 56 suite.snapshot = new(protocolmock.Snapshot) 57 suite.params = new(protocolmock.Params) 58 suite.epochs = mocks.NewEpochQuery(suite.T(), suite.currentEpochCounter) 59 60 suite.state.On("Final").Return(suite.snapshot) 61 suite.state.On("Params").Return(suite.params) 62 suite.params.On("EpochFallbackTriggered").Return( 63 func() bool { return suite.epochFallbackTriggered }, 64 func() error { return nil }, 65 ) 66 suite.snapshot.On("Phase").Return( 67 func() flow.EpochPhase { return suite.phase }, 68 func() error { return nil }, 69 ) 70 suite.snapshot.On("Epochs").Return(suite.epochs) 71 } 72 73 func (suite *ConsensusSuite) TearDownTest() { 74 if suite.cancel != nil { 75 suite.cancel() 76 } 77 } 78 79 // CreateAndStartCommittee instantiates and starts the committee. 80 // Should be called only once per test, after initial epoch mocks are created. 81 // It spawns a goroutine to detect fatal errors from the committee's error channel. 82 func (suite *ConsensusSuite) CreateAndStartCommittee() { 83 committee, err := NewConsensusCommittee(suite.state, suite.myID) 84 require.NoError(suite.T(), err) 85 ctx, cancel, errCh := irrecoverable.WithSignallerAndCancel(context.Background()) 86 committee.Start(ctx) 87 go unittest.FailOnIrrecoverableError(suite.T(), ctx.Done(), errCh) 88 89 suite.committee = committee 90 suite.cancel = cancel 91 } 92 93 // CommitEpoch adds the epoch to the protocol state and mimics the protocol state 94 // behaviour when committing an epoch, by sending the protocol event to the committee. 95 func (suite *ConsensusSuite) CommitEpoch(epoch protocol.Epoch) { 96 firstBlockOfCommittedPhase := unittest.BlockHeaderFixture() 97 suite.state.On("AtBlockID", firstBlockOfCommittedPhase.ID()).Return(suite.snapshot) 98 suite.epochs.Add(epoch) 99 suite.committee.EpochCommittedPhaseStarted(1, firstBlockOfCommittedPhase) 100 101 // get the first view, to test when the epoch has been processed 102 firstView, err := epoch.FirstView() 103 require.NoError(suite.T(), err) 104 105 // wait for the protocol event to be processed (async) 106 assert.Eventually(suite.T(), func() bool { 107 _, err := suite.committee.IdentitiesByEpoch(firstView) 108 return err == nil 109 }, time.Second, time.Millisecond) 110 } 111 112 // AssertStoredEpochCounterRange asserts that the cached epochs are for exactly 113 // the given contiguous, inclusive counter range. 114 // Eg. for the input (2,4), the committee must have epochs cached with counters 2,3,4 115 func (suite *ConsensusSuite) AssertStoredEpochCounterRange(from, to uint64) { 116 set := make(map[uint64]struct{}) 117 for i := from; i <= to; i++ { 118 set[i] = struct{}{} 119 } 120 121 suite.committee.mu.RLock() 122 defer suite.committee.mu.RUnlock() 123 for epoch := range suite.committee.epochs { 124 delete(set, epoch) 125 } 126 127 if !assert.Len(suite.T(), set, 0) { 128 suite.T().Logf("%v should be empty, but isn't; expected epoch range [%d,%d]", set, from, to) 129 } 130 } 131 132 // TestConstruction_CurrentEpoch tests construction with only a current epoch. 133 // Only the current epoch should be cached after construction. 134 func (suite *ConsensusSuite) TestConstruction_CurrentEpoch() { 135 curEpoch := newMockEpoch(suite.currentEpochCounter, unittest.IdentityListFixture(10), 101, 200, unittest.SeedFixture(32), true) 136 suite.epochs.Add(curEpoch) 137 138 suite.CreateAndStartCommittee() 139 suite.Assert().Len(suite.committee.epochs, 1) 140 suite.AssertStoredEpochCounterRange(suite.currentEpochCounter, suite.currentEpochCounter) 141 } 142 143 // TestConstruction_PreviousEpoch tests construction with a previous epoch. 144 // Both current and previous epoch should be cached after construction. 145 func (suite *ConsensusSuite) TestConstruction_PreviousEpoch() { 146 prevEpoch := newMockEpoch(suite.currentEpochCounter-1, unittest.IdentityListFixture(10), 1, 100, unittest.SeedFixture(32), true) 147 curEpoch := newMockEpoch(suite.currentEpochCounter, unittest.IdentityListFixture(10), 101, 200, unittest.SeedFixture(32), true) 148 suite.epochs.Add(prevEpoch) 149 suite.epochs.Add(curEpoch) 150 151 suite.CreateAndStartCommittee() 152 suite.Assert().Len(suite.committee.epochs, 2) 153 suite.AssertStoredEpochCounterRange(suite.currentEpochCounter-1, suite.currentEpochCounter) 154 } 155 156 // TestConstruction_UncommittedNextEpoch tests construction with an uncommitted next epoch. 157 // Only the current epoch should be cached after construction. 158 func (suite *ConsensusSuite) TestConstruction_UncommittedNextEpoch() { 159 suite.phase = flow.EpochPhaseSetup 160 curEpoch := newMockEpoch(suite.currentEpochCounter, unittest.IdentityListFixture(10), 101, 200, unittest.SeedFixture(32), true) 161 nextEpoch := newMockEpoch(suite.currentEpochCounter+1, unittest.IdentityListFixture(10), 201, 300, unittest.SeedFixture(32), false) 162 suite.epochs.Add(curEpoch) 163 suite.epochs.Add(nextEpoch) 164 165 suite.CreateAndStartCommittee() 166 suite.Assert().Len(suite.committee.epochs, 1) 167 suite.AssertStoredEpochCounterRange(suite.currentEpochCounter, suite.currentEpochCounter) 168 } 169 170 // TestConstruction_CommittedNextEpoch tests construction with a committed next epoch. 171 // Both current and next epochs should be cached after construction. 172 func (suite *ConsensusSuite) TestConstruction_CommittedNextEpoch() { 173 curEpoch := newMockEpoch(suite.currentEpochCounter, unittest.IdentityListFixture(10), 101, 200, unittest.SeedFixture(32), true) 174 nextEpoch := newMockEpoch(suite.currentEpochCounter+1, unittest.IdentityListFixture(10), 201, 300, unittest.SeedFixture(32), true) 175 suite.epochs.Add(curEpoch) 176 suite.epochs.Add(nextEpoch) 177 suite.phase = flow.EpochPhaseCommitted 178 179 suite.CreateAndStartCommittee() 180 suite.Assert().Len(suite.committee.epochs, 2) 181 suite.AssertStoredEpochCounterRange(suite.currentEpochCounter, suite.currentEpochCounter+1) 182 } 183 184 // TestConstruction_EpochFallbackTriggered tests construction when EECC has been triggered. 185 // Both current and the injected fallback epoch should be cached after construction. 186 func (suite *ConsensusSuite) TestConstruction_EpochFallbackTriggered() { 187 curEpoch := newMockEpoch(suite.currentEpochCounter, unittest.IdentityListFixture(10), 101, 200, unittest.SeedFixture(32), true) 188 suite.epochs.Add(curEpoch) 189 suite.epochFallbackTriggered = true 190 191 suite.CreateAndStartCommittee() 192 suite.Assert().Len(suite.committee.epochs, 2) 193 suite.AssertStoredEpochCounterRange(suite.currentEpochCounter, suite.currentEpochCounter+1) 194 } 195 196 // TestProtocolEvents_CommittedEpoch tests that protocol events notifying of a newly 197 // committed epoch are handled correctly. A committed epoch should be cached, and 198 // repeated events should be no-ops. 199 func (suite *ConsensusSuite) TestProtocolEvents_CommittedEpoch() { 200 curEpoch := newMockEpoch(suite.currentEpochCounter, unittest.IdentityListFixture(10), 101, 200, unittest.SeedFixture(32), true) 201 suite.epochs.Add(curEpoch) 202 203 suite.CreateAndStartCommittee() 204 205 nextEpoch := newMockEpoch(suite.currentEpochCounter+1, unittest.IdentityListFixture(10), 201, 300, unittest.SeedFixture(32), true) 206 207 firstBlockOfCommittedPhase := unittest.BlockHeaderFixture() 208 suite.state.On("AtBlockID", firstBlockOfCommittedPhase.ID()).Return(suite.snapshot) 209 suite.epochs.Add(nextEpoch) 210 suite.committee.EpochCommittedPhaseStarted(suite.currentEpochCounter, firstBlockOfCommittedPhase) 211 // wait for the protocol event to be processed (async) 212 assert.Eventually(suite.T(), func() bool { 213 _, err := suite.committee.IdentitiesByEpoch(unittest.Uint64InRange(201, 300)) 214 return err == nil 215 }, 30*time.Second, 50*time.Millisecond) 216 217 suite.Assert().Len(suite.committee.epochs, 2) 218 suite.AssertStoredEpochCounterRange(suite.currentEpochCounter, suite.currentEpochCounter+1) 219 220 // should handle multiple deliveries of the protocol event 221 suite.committee.EpochCommittedPhaseStarted(suite.currentEpochCounter, firstBlockOfCommittedPhase) 222 suite.committee.EpochCommittedPhaseStarted(suite.currentEpochCounter, firstBlockOfCommittedPhase) 223 suite.committee.EpochCommittedPhaseStarted(suite.currentEpochCounter, firstBlockOfCommittedPhase) 224 225 suite.Assert().Len(suite.committee.epochs, 2) 226 suite.AssertStoredEpochCounterRange(suite.currentEpochCounter, suite.currentEpochCounter+1) 227 228 } 229 230 // TestProtocolEvents_EpochFallback tests that protocol events notifying of epoch 231 // fallback are handled correctly. Epoch fallback triggering should result in a 232 // fallback epoch being injected, and repeated events should be no-ops. 233 func (suite *ConsensusSuite) TestProtocolEvents_EpochFallback() { 234 curEpoch := newMockEpoch(suite.currentEpochCounter, unittest.IdentityListFixture(10), 101, 200, unittest.SeedFixture(32), true) 235 suite.epochs.Add(curEpoch) 236 237 suite.CreateAndStartCommittee() 238 239 suite.committee.EpochEmergencyFallbackTriggered() 240 // wait for the protocol event to be processed (async) 241 require.Eventually(suite.T(), func() bool { 242 _, err := suite.committee.IdentitiesByEpoch(unittest.Uint64InRange(201, 300)) 243 return err == nil 244 }, 30*time.Second, 50*time.Millisecond) 245 246 suite.Assert().Len(suite.committee.epochs, 2) 247 suite.AssertStoredEpochCounterRange(suite.currentEpochCounter, suite.currentEpochCounter+1) 248 249 // should handle multiple deliveries of the protocol event 250 suite.committee.EpochEmergencyFallbackTriggered() 251 suite.committee.EpochEmergencyFallbackTriggered() 252 suite.committee.EpochEmergencyFallbackTriggered() 253 254 suite.Assert().Len(suite.committee.epochs, 2) 255 suite.AssertStoredEpochCounterRange(suite.currentEpochCounter, suite.currentEpochCounter+1) 256 } 257 258 // TestIdentitiesByBlock tests retrieving committee members by block. 259 // * should use up-to-block committee information 260 // * should exclude non-committee members 261 func (suite *ConsensusSuite) TestIdentitiesByBlock() { 262 t := suite.T() 263 264 realIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus)) 265 zeroWeightConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus), unittest.WithWeight(0)) 266 ejectedConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus), unittest.WithEjected(true)) 267 validNonConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleVerification)) 268 fakeID := unittest.IdentifierFixture() 269 blockID := unittest.IdentifierFixture() 270 271 // create a mock epoch for leader selection setup in constructor 272 currEpoch := newMockEpoch(1, unittest.IdentityListFixture(10), 1, 100, unittest.SeedFixture(prg.RandomSourceLength), true) 273 suite.epochs.Add(currEpoch) 274 275 suite.state.On("AtBlockID", blockID).Return(suite.snapshot) 276 suite.snapshot.On("Identity", realIdentity.NodeID).Return(realIdentity, nil) 277 suite.snapshot.On("Identity", zeroWeightConsensusIdentity.NodeID).Return(zeroWeightConsensusIdentity, nil) 278 suite.snapshot.On("Identity", ejectedConsensusIdentity.NodeID).Return(ejectedConsensusIdentity, nil) 279 suite.snapshot.On("Identity", validNonConsensusIdentity.NodeID).Return(validNonConsensusIdentity, nil) 280 suite.snapshot.On("Identity", fakeID).Return(nil, protocol.IdentityNotFoundError{}) 281 282 suite.CreateAndStartCommittee() 283 284 t.Run("non-existent identity should return InvalidSignerError", func(t *testing.T) { 285 _, err := suite.committee.IdentityByBlock(blockID, fakeID) 286 require.True(t, model.IsInvalidSignerError(err)) 287 }) 288 289 t.Run("existent but non-committee-member identity should return InvalidSignerError", func(t *testing.T) { 290 t.Run("zero-weight consensus node", func(t *testing.T) { 291 _, err := suite.committee.IdentityByBlock(blockID, zeroWeightConsensusIdentity.NodeID) 292 require.True(t, model.IsInvalidSignerError(err)) 293 }) 294 295 t.Run("ejected consensus node", func(t *testing.T) { 296 _, err := suite.committee.IdentityByBlock(blockID, ejectedConsensusIdentity.NodeID) 297 require.True(t, model.IsInvalidSignerError(err)) 298 }) 299 300 t.Run("otherwise valid non-consensus node", func(t *testing.T) { 301 _, err := suite.committee.IdentityByBlock(blockID, validNonConsensusIdentity.NodeID) 302 require.True(t, model.IsInvalidSignerError(err)) 303 }) 304 }) 305 306 t.Run("should be able to retrieve real identity", func(t *testing.T) { 307 actual, err := suite.committee.IdentityByBlock(blockID, realIdentity.NodeID) 308 require.NoError(t, err) 309 require.Equal(t, realIdentity, actual) 310 }) 311 t.Run("should propagate unexpected errors", func(t *testing.T) { 312 mockErr := errors.New("unexpected") 313 suite.snapshot.On("Identity", mock.Anything).Return(nil, mockErr) 314 _, err := suite.committee.IdentityByBlock(blockID, unittest.IdentifierFixture()) 315 assert.ErrorIs(t, err, mockErr) 316 }) 317 } 318 319 // TestIdentitiesByEpoch tests that identities can be queried by epoch. 320 // * should use static epoch info (initial identities) 321 // * should exclude non-committee members 322 // * should correctly map views to epochs 323 // * should return ErrViewForUnknownEpoch sentinel for unknown epochs 324 func (suite *ConsensusSuite) TestIdentitiesByEpoch() { 325 t := suite.T() 326 327 // epoch 1 identities with varying conditions which would disqualify them 328 // from committee participation 329 realIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus)) 330 zeroWeightConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus), unittest.WithWeight(0)) 331 ejectedConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus), unittest.WithEjected(true)) 332 validNonConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleVerification)) 333 epoch1Identities := flow.IdentityList{realIdentity, zeroWeightConsensusIdentity, ejectedConsensusIdentity, validNonConsensusIdentity} 334 335 // a single consensus node for epoch 2: 336 epoch2Identity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus)) 337 epoch2Identities := flow.IdentityList{epoch2Identity} 338 339 // create a mock epoch for leader selection setup in constructor 340 epoch1 := newMockEpoch(suite.currentEpochCounter, epoch1Identities, 1, 100, unittest.SeedFixture(prg.RandomSourceLength), true) 341 // initially epoch 2 is not committed 342 epoch2 := newMockEpoch(suite.currentEpochCounter+1, epoch2Identities, 101, 200, unittest.SeedFixture(prg.RandomSourceLength), true) 343 suite.epochs.Add(epoch1) 344 345 suite.CreateAndStartCommittee() 346 347 t.Run("only epoch 1 committed", func(t *testing.T) { 348 t.Run("non-existent identity should return InvalidSignerError", func(t *testing.T) { 349 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), unittest.IdentifierFixture()) 350 require.True(t, model.IsInvalidSignerError(err)) 351 }) 352 353 t.Run("existent but non-committee-member identity should return InvalidSignerError", func(t *testing.T) { 354 t.Run("zero-weight consensus node", func(t *testing.T) { 355 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), zeroWeightConsensusIdentity.NodeID) 356 require.True(t, model.IsInvalidSignerError(err)) 357 }) 358 359 t.Run("ejected consensus node", func(t *testing.T) { 360 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), ejectedConsensusIdentity.NodeID) 361 require.True(t, model.IsInvalidSignerError(err)) 362 }) 363 364 t.Run("otherwise valid non-consensus node", func(t *testing.T) { 365 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), validNonConsensusIdentity.NodeID) 366 require.True(t, model.IsInvalidSignerError(err)) 367 }) 368 }) 369 370 t.Run("should be able to retrieve real identity", func(t *testing.T) { 371 actual, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), realIdentity.NodeID) 372 require.NoError(t, err) 373 require.Equal(t, realIdentity, actual) 374 }) 375 376 t.Run("should return ErrViewForUnknownEpoch for view outside existing epoch", func(t *testing.T) { 377 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(101, 1_000_000), epoch2Identity.NodeID) 378 require.Error(t, err) 379 require.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 380 }) 381 }) 382 383 // commit epoch 2 384 suite.CommitEpoch(epoch2) 385 386 t.Run("epoch 1 and 2 committed", func(t *testing.T) { 387 t.Run("should be able to retrieve epoch 1 identity in epoch 1", func(t *testing.T) { 388 actual, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), realIdentity.NodeID) 389 require.NoError(t, err) 390 require.Equal(t, realIdentity, actual) 391 }) 392 393 t.Run("should be unable to retrieve epoch 1 identity in epoch 2", func(t *testing.T) { 394 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(101, 200), realIdentity.NodeID) 395 require.Error(t, err) 396 require.True(t, model.IsInvalidSignerError(err)) 397 }) 398 399 t.Run("should be unable to retrieve epoch 2 identity in epoch 1", func(t *testing.T) { 400 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), epoch2Identity.NodeID) 401 require.Error(t, err) 402 require.True(t, model.IsInvalidSignerError(err)) 403 }) 404 405 t.Run("should be able to retrieve epoch 2 identity in epoch 2", func(t *testing.T) { 406 actual, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(101, 200), epoch2Identity.NodeID) 407 require.NoError(t, err) 408 require.Equal(t, epoch2Identity, actual) 409 }) 410 411 t.Run("should return ErrViewForUnknownEpoch for view outside existing epochs", func(t *testing.T) { 412 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(201, 1_000_000), epoch2Identity.NodeID) 413 require.Error(t, err) 414 require.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 415 }) 416 }) 417 418 } 419 420 // TestThresholds tests that the weight threshold methods return the 421 // correct thresholds for the previous and current epoch and that it returns the 422 // appropriate sentinel for the next epoch if it is not yet ready. 423 // 424 // There are 3 epochs in this test case, each with the same identities but different 425 // weights. 426 func (suite *ConsensusSuite) TestThresholds() { 427 t := suite.T() 428 429 identities := unittest.IdentityListFixture(10) 430 431 prevEpoch := newMockEpoch(suite.currentEpochCounter-1, identities.Map(mapfunc.WithWeight(100)), 1, 100, unittest.SeedFixture(prg.RandomSourceLength), true) 432 currEpoch := newMockEpoch(suite.currentEpochCounter, identities.Map(mapfunc.WithWeight(200)), 101, 200, unittest.SeedFixture(32), true) 433 suite.epochs.Add(prevEpoch) 434 suite.epochs.Add(currEpoch) 435 436 suite.CreateAndStartCommittee() 437 438 t.Run("next epoch not ready", func(t *testing.T) { 439 t.Run("previous epoch", func(t *testing.T) { 440 threshold, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(1, 100)) 441 require.Nil(t, err) 442 assert.Equal(t, WeightThresholdToBuildQC(1000), threshold) 443 threshold, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(1, 100)) 444 require.Nil(t, err) 445 assert.Equal(t, WeightThresholdToTimeout(1000), threshold) 446 }) 447 448 t.Run("current epoch", func(t *testing.T) { 449 threshold, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(101, 200)) 450 require.Nil(t, err) 451 assert.Equal(t, WeightThresholdToBuildQC(2000), threshold) 452 threshold, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(101, 200)) 453 require.Nil(t, err) 454 assert.Equal(t, WeightThresholdToTimeout(2000), threshold) 455 }) 456 457 t.Run("after current epoch - should return ErrViewForUnknownEpoch", func(t *testing.T) { 458 // get threshold for view in next epoch when it is not set up yet 459 _, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(201, 300)) 460 assert.Error(t, err) 461 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 462 _, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(201, 300)) 463 assert.Error(t, err) 464 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 465 }) 466 }) 467 468 // now, add a valid next epoch 469 nextEpoch := newMockEpoch(suite.currentEpochCounter+1, identities.Map(mapfunc.WithWeight(300)), 201, 300, unittest.SeedFixture(prg.RandomSourceLength), true) 470 suite.CommitEpoch(nextEpoch) 471 472 t.Run("next epoch ready", func(t *testing.T) { 473 t.Run("previous epoch", func(t *testing.T) { 474 threshold, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(1, 100)) 475 require.Nil(t, err) 476 assert.Equal(t, WeightThresholdToBuildQC(1000), threshold) 477 threshold, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(1, 100)) 478 require.Nil(t, err) 479 assert.Equal(t, WeightThresholdToTimeout(1000), threshold) 480 }) 481 482 t.Run("current epoch", func(t *testing.T) { 483 threshold, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(101, 200)) 484 require.Nil(t, err) 485 assert.Equal(t, WeightThresholdToBuildQC(2000), threshold) 486 threshold, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(101, 200)) 487 require.Nil(t, err) 488 assert.Equal(t, WeightThresholdToTimeout(2000), threshold) 489 }) 490 491 t.Run("next epoch", func(t *testing.T) { 492 threshold, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(201, 300)) 493 require.Nil(t, err) 494 assert.Equal(t, WeightThresholdToBuildQC(3000), threshold) 495 threshold, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(201, 300)) 496 require.Nil(t, err) 497 assert.Equal(t, WeightThresholdToTimeout(3000), threshold) 498 }) 499 500 t.Run("beyond known epochs", func(t *testing.T) { 501 // get threshold for view in next epoch when it is not set up yet 502 _, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(301, 10_000)) 503 assert.Error(t, err) 504 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 505 _, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(301, 10_000)) 506 assert.Error(t, err) 507 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 508 }) 509 }) 510 } 511 512 // TestLeaderForView tests that LeaderForView returns a valid leader 513 // for the previous and current epoch and that it returns the appropriate 514 // sentinel for the next epoch if it is not yet ready 515 func (suite *ConsensusSuite) TestLeaderForView() { 516 t := suite.T() 517 518 identities := unittest.IdentityListFixture(10) 519 520 prevEpoch := newMockEpoch(suite.currentEpochCounter-1, identities, 1, 100, unittest.SeedFixture(prg.RandomSourceLength), true) 521 currEpoch := newMockEpoch(suite.currentEpochCounter, identities, 101, 200, unittest.SeedFixture(32), true) 522 suite.epochs.Add(currEpoch) 523 suite.epochs.Add(prevEpoch) 524 525 suite.CreateAndStartCommittee() 526 527 t.Run("next epoch not ready", func(t *testing.T) { 528 t.Run("previous epoch", func(t *testing.T) { 529 // get leader for view in previous epoch 530 leaderID, err := suite.committee.LeaderForView(unittest.Uint64InRange(1, 100)) 531 assert.NoError(t, err) 532 _, exists := identities.ByNodeID(leaderID) 533 assert.True(t, exists) 534 }) 535 536 t.Run("current epoch", func(t *testing.T) { 537 // get leader for view in current epoch 538 leaderID, err := suite.committee.LeaderForView(unittest.Uint64InRange(101, 200)) 539 assert.NoError(t, err) 540 _, exists := identities.ByNodeID(leaderID) 541 assert.True(t, exists) 542 }) 543 544 t.Run("after current epoch - should return ErrViewForUnknownEpoch", func(t *testing.T) { 545 // get leader for view in next epoch when it is not set up yet 546 _, err := suite.committee.LeaderForView(unittest.Uint64InRange(201, 300)) 547 assert.Error(t, err) 548 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 549 }) 550 }) 551 552 // now, add a valid next epoch 553 nextEpoch := newMockEpoch(suite.currentEpochCounter+1, identities, 201, 300, unittest.SeedFixture(prg.RandomSourceLength), true) 554 suite.CommitEpoch(nextEpoch) 555 556 t.Run("next epoch ready", func(t *testing.T) { 557 t.Run("previous epoch", func(t *testing.T) { 558 // get leader for view in previous epoch 559 leaderID, err := suite.committee.LeaderForView(unittest.Uint64InRange(1, 100)) 560 require.Nil(t, err) 561 _, exists := identities.ByNodeID(leaderID) 562 assert.True(t, exists) 563 }) 564 565 t.Run("current epoch", func(t *testing.T) { 566 // get leader for view in current epoch 567 leaderID, err := suite.committee.LeaderForView(unittest.Uint64InRange(101, 200)) 568 require.Nil(t, err) 569 _, exists := identities.ByNodeID(leaderID) 570 assert.True(t, exists) 571 }) 572 573 t.Run("next epoch", func(t *testing.T) { 574 // get leader for view in next epoch after it has been set up 575 leaderID, err := suite.committee.LeaderForView(unittest.Uint64InRange(201, 300)) 576 require.Nil(t, err) 577 _, exists := identities.ByNodeID(leaderID) 578 assert.True(t, exists) 579 }) 580 581 t.Run("beyond known epochs", func(t *testing.T) { 582 _, err := suite.committee.LeaderForView(unittest.Uint64InRange(301, 1_000_000)) 583 assert.Error(t, err) 584 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 585 }) 586 }) 587 } 588 589 // TestRemoveOldEpochs tests that old epochs are pruned 590 func TestRemoveOldEpochs(t *testing.T) { 591 592 identities := unittest.IdentityListFixture(10) 593 me := identities[0].NodeID 594 595 // keep track of epoch counter and views 596 firstEpochCounter := uint64(1) 597 currentEpochCounter := firstEpochCounter 598 epochFinalView := uint64(100) 599 600 epoch1 := newMockEpoch(currentEpochCounter, identities, 1, epochFinalView, unittest.SeedFixture(prg.RandomSourceLength), true) 601 602 // create mocks 603 state := new(protocolmock.State) 604 snapshot := new(protocolmock.Snapshot) 605 params := new(protocolmock.Params) 606 state.On("Final").Return(snapshot) 607 state.On("Params").Return(params) 608 params.On("EpochFallbackTriggered").Return(false, nil) 609 610 epochQuery := mocks.NewEpochQuery(t, currentEpochCounter, epoch1) 611 snapshot.On("Epochs").Return(epochQuery) 612 currentEpochPhase := flow.EpochPhaseStaking 613 snapshot.On("Phase").Return( 614 func() flow.EpochPhase { return currentEpochPhase }, 615 func() error { return nil }, 616 ) 617 618 com, err := NewConsensusCommittee(state, me) 619 require.Nil(t, err) 620 621 ctx, cancel, errCh := irrecoverable.WithSignallerAndCancel(context.Background()) 622 com.Start(ctx) 623 go unittest.FailOnIrrecoverableError(t, ctx.Done(), errCh) 624 defer cancel() 625 626 // we should start with only current epoch (epoch 1) pre-computed 627 // since there is no previous epoch 628 assert.Equal(t, 1, len(com.epochs)) 629 630 // test for 10 epochs 631 for currentEpochCounter < 10 { 632 633 // add another epoch 634 firstView := epochFinalView + 1 635 epochFinalView = epochFinalView + 100 636 currentEpochCounter++ 637 nextEpoch := newMockEpoch(currentEpochCounter, identities, firstView, epochFinalView, unittest.SeedFixture(prg.RandomSourceLength), true) 638 epochQuery.Add(nextEpoch) 639 640 currentEpochPhase = flow.EpochPhaseCommitted 641 firstBlockOfCommittedPhase := unittest.BlockHeaderFixture() 642 state.On("AtBlockID", firstBlockOfCommittedPhase.ID()).Return(snapshot) 643 com.EpochCommittedPhaseStarted(currentEpochCounter, firstBlockOfCommittedPhase) 644 // wait for the protocol event to be processed (async) 645 require.Eventually(t, func() bool { 646 _, err := com.IdentityByEpoch(unittest.Uint64InRange(firstView, epochFinalView), unittest.IdentifierFixture()) 647 return !errors.Is(err, model.ErrViewForUnknownEpoch) 648 }, time.Second, time.Millisecond) 649 650 // query a view from the new epoch 651 _, err = com.LeaderForView(firstView) 652 require.NoError(t, err) 653 // transition to the next epoch 654 epochQuery.Transition() 655 656 t.Run(fmt.Sprintf("epoch %d", currentEpochCounter), func(t *testing.T) { 657 // check we have the right number of epochs stored 658 if currentEpochCounter <= 3 { 659 assert.Equal(t, int(currentEpochCounter), len(com.epochs)) 660 } else { 661 assert.Equal(t, 3, len(com.epochs)) 662 } 663 664 // check we have the correct epochs stored 665 for i := uint64(0); i < 3; i++ { 666 counter := currentEpochCounter - i 667 if counter < firstEpochCounter { 668 break 669 } 670 _, exists := com.epochs[counter] 671 assert.True(t, exists, "missing epoch with counter %d max counter is %d", counter, currentEpochCounter) 672 } 673 }) 674 } 675 } 676 677 // newMockEpoch returns a new mocked epoch with the given fields 678 func newMockEpoch(counter uint64, identities flow.IdentityList, firstView uint64, finalView uint64, seed []byte, committed bool) *protocolmock.Epoch { 679 680 epoch := new(protocolmock.Epoch) 681 epoch.On("Counter").Return(counter, nil) 682 epoch.On("InitialIdentities").Return(identities, nil) 683 epoch.On("FirstView").Return(firstView, nil) 684 epoch.On("FinalView").Return(finalView, nil) 685 if committed { 686 // return nil error to indicate the epoch is committed 687 epoch.On("DKG").Return(nil, nil) 688 } else { 689 epoch.On("DKG").Return(nil, protocol.ErrNextEpochNotCommitted) 690 } 691 692 epoch.On("RandomSource").Return(seed, nil) 693 return epoch 694 }