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  }