github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/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 EFM 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 joiningConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus), unittest.WithParticipationStatus(flow.EpochParticipationStatusJoining)) 266 leavingConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus), unittest.WithParticipationStatus(flow.EpochParticipationStatusLeaving)) 267 ejectedConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus), unittest.WithParticipationStatus(flow.EpochParticipationStatusEjected)) 268 validNonConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleVerification)) 269 validConsensusIdentities := []*flow.Identity{ 270 realIdentity, 271 joiningConsensusIdentity, 272 leavingConsensusIdentity, 273 validNonConsensusIdentity, 274 ejectedConsensusIdentity, 275 } 276 fakeID := unittest.IdentifierFixture() 277 blockID := unittest.IdentifierFixture() 278 279 // create a mock epoch for leader selection setup in constructor 280 currEpoch := newMockEpoch(1, unittest.IdentityListFixture(10), 1, 100, unittest.SeedFixture(prg.RandomSourceLength), true) 281 suite.epochs.Add(currEpoch) 282 283 suite.state.On("AtBlockID", blockID).Return(suite.snapshot) 284 for _, identity := range validConsensusIdentities { 285 i := identity // copy 286 suite.snapshot.On("Identity", i.NodeID).Return(i, nil) 287 } 288 suite.snapshot.On("Identity", fakeID).Return(nil, protocol.IdentityNotFoundError{}) 289 290 suite.CreateAndStartCommittee() 291 292 t.Run("non-existent identity should return InvalidSignerError", func(t *testing.T) { 293 _, err := suite.committee.IdentityByBlock(blockID, fakeID) 294 require.True(t, model.IsInvalidSignerError(err)) 295 }) 296 297 t.Run("existent but non-committee-member identity should return InvalidSignerError", func(t *testing.T) { 298 t.Run("joining consensus node", func(t *testing.T) { 299 _, err := suite.committee.IdentityByBlock(blockID, joiningConsensusIdentity.NodeID) 300 require.True(t, model.IsInvalidSignerError(err)) 301 }) 302 303 t.Run("leaving consensus node", func(t *testing.T) { 304 _, err := suite.committee.IdentityByBlock(blockID, leavingConsensusIdentity.NodeID) 305 require.True(t, model.IsInvalidSignerError(err)) 306 }) 307 308 t.Run("ejected consensus node", func(t *testing.T) { 309 _, err := suite.committee.IdentityByBlock(blockID, ejectedConsensusIdentity.NodeID) 310 require.True(t, model.IsInvalidSignerError(err)) 311 }) 312 313 t.Run("otherwise valid non-consensus node", func(t *testing.T) { 314 _, err := suite.committee.IdentityByBlock(blockID, validNonConsensusIdentity.NodeID) 315 require.True(t, model.IsInvalidSignerError(err)) 316 }) 317 }) 318 319 t.Run("should be able to retrieve real identity", func(t *testing.T) { 320 actual, err := suite.committee.IdentityByBlock(blockID, realIdentity.NodeID) 321 require.NoError(t, err) 322 require.Equal(t, realIdentity, actual) 323 }) 324 t.Run("should propagate unexpected errors", func(t *testing.T) { 325 mockErr := errors.New("unexpected") 326 suite.snapshot.On("Identity", mock.Anything).Return(nil, mockErr) 327 _, err := suite.committee.IdentityByBlock(blockID, unittest.IdentifierFixture()) 328 assert.ErrorIs(t, err, mockErr) 329 }) 330 } 331 332 // TestIdentitiesByEpoch tests that identities can be queried by epoch. 333 // * should use static epoch info (initial identities) 334 // * should exclude non-committee members 335 // * should correctly map views to epochs 336 // * should return ErrViewForUnknownEpoch sentinel for unknown epochs 337 func (suite *ConsensusSuite) TestIdentitiesByEpoch() { 338 t := suite.T() 339 340 // epoch 1 identities with varying conditions which would disqualify them 341 // from committee participation 342 realIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus)) 343 zeroWeightConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus), 344 unittest.WithInitialWeight(0)) 345 ejectedConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus), 346 unittest.WithParticipationStatus(flow.EpochParticipationStatusEjected)) 347 validNonConsensusIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleVerification)) 348 epoch1Identities := flow.IdentityList{realIdentity, zeroWeightConsensusIdentity, ejectedConsensusIdentity, validNonConsensusIdentity} 349 350 // a single consensus node for epoch 2: 351 epoch2Identity := unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus)) 352 epoch2Identities := flow.IdentityList{epoch2Identity} 353 354 // create a mock epoch for leader selection setup in constructor 355 epoch1 := newMockEpoch(suite.currentEpochCounter, epoch1Identities, 1, 100, unittest.SeedFixture(prg.RandomSourceLength), true) 356 // initially epoch 2 is not committed 357 epoch2 := newMockEpoch(suite.currentEpochCounter+1, epoch2Identities, 101, 200, unittest.SeedFixture(prg.RandomSourceLength), true) 358 suite.epochs.Add(epoch1) 359 360 suite.CreateAndStartCommittee() 361 362 t.Run("only epoch 1 committed", func(t *testing.T) { 363 t.Run("non-existent identity should return InvalidSignerError", func(t *testing.T) { 364 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), unittest.IdentifierFixture()) 365 require.True(t, model.IsInvalidSignerError(err)) 366 }) 367 368 t.Run("existent but non-committee-member identity should return InvalidSignerError", func(t *testing.T) { 369 t.Run("zero-weight consensus node", func(t *testing.T) { 370 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), zeroWeightConsensusIdentity.NodeID) 371 require.True(t, model.IsInvalidSignerError(err)) 372 }) 373 374 t.Run("otherwise valid non-consensus node", func(t *testing.T) { 375 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), validNonConsensusIdentity.NodeID) 376 require.True(t, model.IsInvalidSignerError(err)) 377 }) 378 }) 379 380 t.Run("should be able to retrieve real identity", func(t *testing.T) { 381 actual, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), realIdentity.NodeID) 382 require.NoError(t, err) 383 require.Equal(t, realIdentity.IdentitySkeleton, *actual) 384 }) 385 386 t.Run("should return ErrViewForUnknownEpoch for view outside existing epoch", func(t *testing.T) { 387 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(101, 1_000_000), epoch2Identity.NodeID) 388 require.Error(t, err) 389 require.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 390 }) 391 }) 392 393 // commit epoch 2 394 suite.CommitEpoch(epoch2) 395 396 t.Run("epoch 1 and 2 committed", func(t *testing.T) { 397 t.Run("should be able to retrieve epoch 1 identity in epoch 1", func(t *testing.T) { 398 actual, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), realIdentity.NodeID) 399 require.NoError(t, err) 400 require.Equal(t, realIdentity.IdentitySkeleton, *actual) 401 }) 402 403 t.Run("should be unable to retrieve epoch 1 identity in epoch 2", func(t *testing.T) { 404 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(101, 200), realIdentity.NodeID) 405 require.Error(t, err) 406 require.True(t, model.IsInvalidSignerError(err)) 407 }) 408 409 t.Run("should be unable to retrieve epoch 2 identity in epoch 1", func(t *testing.T) { 410 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(1, 100), epoch2Identity.NodeID) 411 require.Error(t, err) 412 require.True(t, model.IsInvalidSignerError(err)) 413 }) 414 415 t.Run("should be able to retrieve epoch 2 identity in epoch 2", func(t *testing.T) { 416 actual, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(101, 200), epoch2Identity.NodeID) 417 require.NoError(t, err) 418 require.Equal(t, epoch2Identity.IdentitySkeleton, *actual) 419 }) 420 421 t.Run("should return ErrViewForUnknownEpoch for view outside existing epochs", func(t *testing.T) { 422 _, err := suite.committee.IdentityByEpoch(unittest.Uint64InRange(201, 1_000_000), epoch2Identity.NodeID) 423 require.Error(t, err) 424 require.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 425 }) 426 }) 427 428 } 429 430 // TestThresholds tests that the weight threshold methods return the 431 // correct thresholds for the previous and current epoch and that it returns the 432 // appropriate sentinel for the next epoch if it is not yet ready. 433 // 434 // There are 3 epochs in this test case, each with the same identities but different 435 // weights. 436 func (suite *ConsensusSuite) TestThresholds() { 437 t := suite.T() 438 439 identities := unittest.IdentityListFixture(10) 440 441 prevEpoch := newMockEpoch(suite.currentEpochCounter-1, identities.Map(mapfunc.WithInitialWeight(100)), 1, 100, unittest.SeedFixture(prg.RandomSourceLength), true) 442 currEpoch := newMockEpoch(suite.currentEpochCounter, identities.Map(mapfunc.WithInitialWeight(200)), 101, 200, unittest.SeedFixture(32), true) 443 suite.epochs.Add(prevEpoch) 444 suite.epochs.Add(currEpoch) 445 446 suite.CreateAndStartCommittee() 447 448 t.Run("next epoch not ready", func(t *testing.T) { 449 t.Run("previous epoch", func(t *testing.T) { 450 threshold, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(1, 100)) 451 require.Nil(t, err) 452 assert.Equal(t, WeightThresholdToBuildQC(1000), threshold) 453 threshold, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(1, 100)) 454 require.Nil(t, err) 455 assert.Equal(t, WeightThresholdToTimeout(1000), threshold) 456 }) 457 458 t.Run("current epoch", func(t *testing.T) { 459 threshold, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(101, 200)) 460 require.Nil(t, err) 461 assert.Equal(t, WeightThresholdToBuildQC(2000), threshold) 462 threshold, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(101, 200)) 463 require.Nil(t, err) 464 assert.Equal(t, WeightThresholdToTimeout(2000), threshold) 465 }) 466 467 t.Run("after current epoch - should return ErrViewForUnknownEpoch", func(t *testing.T) { 468 // get threshold for view in next epoch when it is not set up yet 469 _, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(201, 300)) 470 assert.Error(t, err) 471 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 472 _, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(201, 300)) 473 assert.Error(t, err) 474 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 475 }) 476 }) 477 478 // now, add a valid next epoch 479 nextEpoch := newMockEpoch(suite.currentEpochCounter+1, identities.Map(mapfunc.WithInitialWeight(300)), 201, 300, unittest.SeedFixture(prg.RandomSourceLength), true) 480 suite.CommitEpoch(nextEpoch) 481 482 t.Run("next epoch ready", func(t *testing.T) { 483 t.Run("previous epoch", func(t *testing.T) { 484 threshold, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(1, 100)) 485 require.Nil(t, err) 486 assert.Equal(t, WeightThresholdToBuildQC(1000), threshold) 487 threshold, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(1, 100)) 488 require.Nil(t, err) 489 assert.Equal(t, WeightThresholdToTimeout(1000), threshold) 490 }) 491 492 t.Run("current epoch", func(t *testing.T) { 493 threshold, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(101, 200)) 494 require.Nil(t, err) 495 assert.Equal(t, WeightThresholdToBuildQC(2000), threshold) 496 threshold, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(101, 200)) 497 require.Nil(t, err) 498 assert.Equal(t, WeightThresholdToTimeout(2000), threshold) 499 }) 500 501 t.Run("next epoch", func(t *testing.T) { 502 threshold, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(201, 300)) 503 require.Nil(t, err) 504 assert.Equal(t, WeightThresholdToBuildQC(3000), threshold) 505 threshold, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(201, 300)) 506 require.Nil(t, err) 507 assert.Equal(t, WeightThresholdToTimeout(3000), threshold) 508 }) 509 510 t.Run("beyond known epochs", func(t *testing.T) { 511 // get threshold for view in next epoch when it is not set up yet 512 _, err := suite.committee.QuorumThresholdForView(unittest.Uint64InRange(301, 10_000)) 513 assert.Error(t, err) 514 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 515 _, err = suite.committee.TimeoutThresholdForView(unittest.Uint64InRange(301, 10_000)) 516 assert.Error(t, err) 517 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 518 }) 519 }) 520 } 521 522 // TestLeaderForView tests that LeaderForView returns a valid leader 523 // for the previous and current epoch and that it returns the appropriate 524 // sentinel for the next epoch if it is not yet ready 525 func (suite *ConsensusSuite) TestLeaderForView() { 526 t := suite.T() 527 528 identities := unittest.IdentityListFixture(10) 529 530 prevEpoch := newMockEpoch(suite.currentEpochCounter-1, identities, 1, 100, unittest.SeedFixture(prg.RandomSourceLength), true) 531 currEpoch := newMockEpoch(suite.currentEpochCounter, identities, 101, 200, unittest.SeedFixture(32), true) 532 suite.epochs.Add(currEpoch) 533 suite.epochs.Add(prevEpoch) 534 535 suite.CreateAndStartCommittee() 536 537 t.Run("next epoch not ready", func(t *testing.T) { 538 t.Run("previous epoch", func(t *testing.T) { 539 // get leader for view in previous epoch 540 leaderID, err := suite.committee.LeaderForView(unittest.Uint64InRange(1, 100)) 541 assert.NoError(t, err) 542 _, exists := identities.ByNodeID(leaderID) 543 assert.True(t, exists) 544 }) 545 546 t.Run("current epoch", func(t *testing.T) { 547 // get leader for view in current epoch 548 leaderID, err := suite.committee.LeaderForView(unittest.Uint64InRange(101, 200)) 549 assert.NoError(t, err) 550 _, exists := identities.ByNodeID(leaderID) 551 assert.True(t, exists) 552 }) 553 554 t.Run("after current epoch - should return ErrViewForUnknownEpoch", func(t *testing.T) { 555 // get leader for view in next epoch when it is not set up yet 556 _, err := suite.committee.LeaderForView(unittest.Uint64InRange(201, 300)) 557 assert.Error(t, err) 558 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 559 }) 560 }) 561 562 // now, add a valid next epoch 563 nextEpoch := newMockEpoch(suite.currentEpochCounter+1, identities, 201, 300, unittest.SeedFixture(prg.RandomSourceLength), true) 564 suite.CommitEpoch(nextEpoch) 565 566 t.Run("next epoch ready", func(t *testing.T) { 567 t.Run("previous epoch", func(t *testing.T) { 568 // get leader for view in previous epoch 569 leaderID, err := suite.committee.LeaderForView(unittest.Uint64InRange(1, 100)) 570 require.Nil(t, err) 571 _, exists := identities.ByNodeID(leaderID) 572 assert.True(t, exists) 573 }) 574 575 t.Run("current epoch", func(t *testing.T) { 576 // get leader for view in current epoch 577 leaderID, err := suite.committee.LeaderForView(unittest.Uint64InRange(101, 200)) 578 require.Nil(t, err) 579 _, exists := identities.ByNodeID(leaderID) 580 assert.True(t, exists) 581 }) 582 583 t.Run("next epoch", func(t *testing.T) { 584 // get leader for view in next epoch after it has been set up 585 leaderID, err := suite.committee.LeaderForView(unittest.Uint64InRange(201, 300)) 586 require.Nil(t, err) 587 _, exists := identities.ByNodeID(leaderID) 588 assert.True(t, exists) 589 }) 590 591 t.Run("beyond known epochs", func(t *testing.T) { 592 _, err := suite.committee.LeaderForView(unittest.Uint64InRange(301, 1_000_000)) 593 assert.Error(t, err) 594 assert.True(t, errors.Is(err, model.ErrViewForUnknownEpoch)) 595 }) 596 }) 597 } 598 599 // TestRemoveOldEpochs tests that old epochs are pruned 600 func TestRemoveOldEpochs(t *testing.T) { 601 602 identities := unittest.IdentityListFixture(10) 603 me := identities[0].NodeID 604 605 // keep track of epoch counter and views 606 firstEpochCounter := uint64(1) 607 currentEpochCounter := firstEpochCounter 608 epochFinalView := uint64(100) 609 610 epoch1 := newMockEpoch(currentEpochCounter, identities, 1, epochFinalView, unittest.SeedFixture(prg.RandomSourceLength), true) 611 612 // create mocks 613 state := new(protocolmock.State) 614 snapshot := new(protocolmock.Snapshot) 615 params := new(protocolmock.Params) 616 state.On("Final").Return(snapshot) 617 state.On("Params").Return(params) 618 params.On("EpochFallbackTriggered").Return(false, nil) 619 620 epochQuery := mocks.NewEpochQuery(t, currentEpochCounter, epoch1) 621 snapshot.On("Epochs").Return(epochQuery) 622 currentEpochPhase := flow.EpochPhaseStaking 623 snapshot.On("Phase").Return( 624 func() flow.EpochPhase { return currentEpochPhase }, 625 func() error { return nil }, 626 ) 627 628 com, err := NewConsensusCommittee(state, me) 629 require.Nil(t, err) 630 631 ctx, cancel, errCh := irrecoverable.WithSignallerAndCancel(context.Background()) 632 com.Start(ctx) 633 go unittest.FailOnIrrecoverableError(t, ctx.Done(), errCh) 634 defer cancel() 635 636 // we should start with only current epoch (epoch 1) pre-computed 637 // since there is no previous epoch 638 assert.Equal(t, 1, len(com.epochs)) 639 640 // test for 10 epochs 641 for currentEpochCounter < 10 { 642 643 // add another epoch 644 firstView := epochFinalView + 1 645 epochFinalView = epochFinalView + 100 646 currentEpochCounter++ 647 nextEpoch := newMockEpoch(currentEpochCounter, identities, firstView, epochFinalView, unittest.SeedFixture(prg.RandomSourceLength), true) 648 epochQuery.Add(nextEpoch) 649 650 currentEpochPhase = flow.EpochPhaseCommitted 651 firstBlockOfCommittedPhase := unittest.BlockHeaderFixture() 652 state.On("AtBlockID", firstBlockOfCommittedPhase.ID()).Return(snapshot) 653 com.EpochCommittedPhaseStarted(currentEpochCounter, firstBlockOfCommittedPhase) 654 // wait for the protocol event to be processed (async) 655 require.Eventually(t, func() bool { 656 _, err := com.IdentityByEpoch(unittest.Uint64InRange(firstView, epochFinalView), unittest.IdentifierFixture()) 657 return !errors.Is(err, model.ErrViewForUnknownEpoch) 658 }, time.Second, time.Millisecond) 659 660 // query a view from the new epoch 661 _, err = com.LeaderForView(firstView) 662 require.NoError(t, err) 663 // transition to the next epoch 664 epochQuery.Transition() 665 666 t.Run(fmt.Sprintf("epoch %d", currentEpochCounter), func(t *testing.T) { 667 // check we have the right number of epochs stored 668 if currentEpochCounter <= 3 { 669 assert.Equal(t, int(currentEpochCounter), len(com.epochs)) 670 } else { 671 assert.Equal(t, 3, len(com.epochs)) 672 } 673 674 // check we have the correct epochs stored 675 for i := uint64(0); i < 3; i++ { 676 counter := currentEpochCounter - i 677 if counter < firstEpochCounter { 678 break 679 } 680 _, exists := com.epochs[counter] 681 assert.True(t, exists, "missing epoch with counter %d max counter is %d", counter, currentEpochCounter) 682 } 683 }) 684 } 685 } 686 687 // newMockEpoch returns a new mocked epoch with the given fields 688 func newMockEpoch(counter uint64, identities flow.IdentityList, firstView uint64, finalView uint64, seed []byte, committed bool) *protocolmock.Epoch { 689 690 epoch := new(protocolmock.Epoch) 691 epoch.On("Counter").Return(counter, nil) 692 epoch.On("InitialIdentities").Return(identities.ToSkeleton(), nil) 693 epoch.On("FirstView").Return(firstView, nil) 694 epoch.On("FinalView").Return(finalView, nil) 695 if committed { 696 // return nil error to indicate the epoch is committed 697 epoch.On("DKG").Return(nil, nil) 698 } else { 699 epoch.On("DKG").Return(nil, protocol.ErrNextEpochNotCommitted) 700 } 701 702 epoch.On("RandomSource").Return(seed, nil) 703 return epoch 704 }