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  }