From e12757148f3dc17adccda7b064cc72cd7e01d704 Mon Sep 17 00:00:00 2001 From: Tim Barry Date: Wed, 22 Jan 2025 15:30:23 -0800 Subject: [PATCH 1/7] Create TentativeEpoch interface Updates EpochQuery.Next() to return a TentativeEpoch. (Will later be renamed to NextUnsafe). All users of methods not present on TentativeEpoch now use new method EpochQuery.NextCommitted(), which returns a regular Epoch (to later be renamed to CommittedEpoch). If NextUnsafe is called during the EpochCommitted phase, it returns an error. --- cmd/bootstrap/transit/cmd/snapshot.go | 6 +- .../committees/consensus_committee.go | 4 +- .../cruisectl/block_time_controller.go | 4 +- .../cruisectl/block_time_controller_test.go | 2 +- consensus/integration/nodes_test.go | 2 +- engine/collection/epochmgr/engine.go | 2 +- engine/consensus/dkg/reactor_engine.go | 2 +- .../epochs/dynamic_epoch_transition_suite.go | 2 +- module/epochs.go | 2 +- module/epochs/epoch_lookup.go | 4 +- module/epochs/qc_voter.go | 2 +- module/mock/cluster_root_qc_voter.go | 4 +- state/protocol/badger/mutator_test.go | 38 +++--- state/protocol/badger/snapshot.go | 27 +++- state/protocol/badger/snapshot_test.go | 15 ++- state/protocol/badger/state_test.go | 4 +- state/protocol/epoch.go | 34 +++++- state/protocol/errors.go | 6 +- state/protocol/inmem/epoch.go | 13 +- state/protocol/invalid/epoch.go | 4 +- state/protocol/mock/epoch_query.go | 22 +++- state/protocol/mock/tentative_epoch.go | 115 ++++++++++++++++++ utils/unittest/mocks/epoch_query.go | 13 +- 23 files changed, 278 insertions(+), 49 deletions(-) create mode 100644 state/protocol/mock/tentative_epoch.go diff --git a/cmd/bootstrap/transit/cmd/snapshot.go b/cmd/bootstrap/transit/cmd/snapshot.go index 562adf56707..4e2caa6c506 100644 --- a/cmd/bootstrap/transit/cmd/snapshot.go +++ b/cmd/bootstrap/transit/cmd/snapshot.go @@ -77,7 +77,8 @@ func snapshot(cmd *cobra.Command, args []string) { } // check if given NodeID is part of the current or next epoch - currentIdentities, err := snapshot.Epochs().Current().InitialIdentities() + currentEpoch := snapshot.Epochs().Current() + currentIdentities, err := currentEpoch.InitialIdentities() if err != nil { log.Fatal().Err(err).Msg("could not get initial identities from current epoch") } @@ -89,7 +90,8 @@ func snapshot(cmd *cobra.Command, args []string) { return } - nextIdentities, err := snapshot.Epochs().Next().InitialIdentities() + nextEpoch := snapshot.Epochs().NextCommitted() + nextIdentities, err := nextEpoch.InitialIdentities() if err != nil { log.Fatal().Err(err).Msg("could not get initial identities from next epoch") } diff --git a/consensus/hotstuff/committees/consensus_committee.go b/consensus/hotstuff/committees/consensus_committee.go index 5f9c749004f..46ee5ece41b 100644 --- a/consensus/hotstuff/committees/consensus_committee.go +++ b/consensus/hotstuff/committees/consensus_committee.go @@ -168,7 +168,7 @@ func NewConsensusCommittee(state protocol.State, me flow.Identifier) (*Consensus return nil, fmt.Errorf("could not check epoch phase: %w", err) } if phase == flow.EpochPhaseCommitted { - epochs = append(epochs, final.Epochs().Next()) + epochs = append(epochs, final.Epochs().NextCommitted()) } for _, epoch := range epochs { @@ -384,7 +384,7 @@ func (c *Consensus) handleEpochExtended(epochCounter uint64, extension flow.Epoc // When the next epoch is committed, we compute leader selection for the epoch and cache it. // No errors are expected during normal operation. func (c *Consensus) handleEpochCommittedPhaseStarted(refBlock *flow.Header) error { - epoch := c.state.AtHeight(refBlock.Height).Epochs().Next() + epoch := c.state.AtHeight(refBlock.Height).Epochs().NextCommitted() _, err := c.prepareEpoch(epoch) if err != nil { return fmt.Errorf("could not cache data for committed next epoch: %w", err) diff --git a/consensus/hotstuff/cruisectl/block_time_controller.go b/consensus/hotstuff/cruisectl/block_time_controller.go index e825dca7b22..96a76187c0b 100644 --- a/consensus/hotstuff/cruisectl/block_time_controller.go +++ b/consensus/hotstuff/cruisectl/block_time_controller.go @@ -191,7 +191,7 @@ func (ctl *BlockTimeController) initEpochTiming() error { return fmt.Errorf("could not check snapshot phase: %w", err) } if phase == flow.EpochPhaseCommitted { - ctl.nextEpochTiming, err = newEpochTiming(finalSnapshot.Epochs().Next()) + ctl.nextEpochTiming, err = newEpochTiming(finalSnapshot.Epochs().NextCommitted()) if err != nil { return fmt.Errorf("failed to retrieve the next epoch's timing information: %w", err) } @@ -459,7 +459,7 @@ func (ctl *BlockTimeController) processEpochExtended(first *flow.Header) error { func (ctl *BlockTimeController) processEpochCommittedPhaseStarted(first *flow.Header) error { var err error snapshot := ctl.state.AtHeight(first.Height) - ctl.nextEpochTiming, err = newEpochTiming(snapshot.Epochs().Next()) + ctl.nextEpochTiming, err = newEpochTiming(snapshot.Epochs().NextCommitted()) if err != nil { return fmt.Errorf("failed to retrieve the next epoch's timing information: %w", err) } diff --git a/consensus/hotstuff/cruisectl/block_time_controller_test.go b/consensus/hotstuff/cruisectl/block_time_controller_test.go index b76ce0a7080..269e40530fd 100644 --- a/consensus/hotstuff/cruisectl/block_time_controller_test.go +++ b/consensus/hotstuff/cruisectl/block_time_controller_test.go @@ -142,7 +142,7 @@ func (bs *BlockTimeControllerSuite) AssertCorrectInitialization() { // if next epoch is committed, final view should be set if phase := bs.epochs.Phase(); phase == flow.EpochPhaseCommitted { - finalView, err := bs.epochs.Next().FinalView() + finalView, err := bs.epochs.NextCommitted().FinalView() require.NoError(bs.T(), err) require.NotNil(bs.T(), bs.ctl.nextEpochTiming) assert.Equal(bs.T(), finalView, bs.ctl.nextEpochTiming.finalView) diff --git a/consensus/integration/nodes_test.go b/consensus/integration/nodes_test.go index 96d1d532936..a3ddc5d325d 100644 --- a/consensus/integration/nodes_test.go +++ b/consensus/integration/nodes_test.go @@ -193,7 +193,7 @@ func createNodes(t *testing.T, participants *ConsensusParticipants, rootSnapshot require.NoError(t, err) epochViewLookup := buildEpochLookupList(rootSnapshot.Epochs().Current(), - rootSnapshot.Epochs().Next()) + rootSnapshot.Epochs().NextCommitted()) epochLookup := &mockmodule.EpochLookup{} epochLookup.On("EpochForView", mock.Anything).Return( diff --git a/engine/collection/epochmgr/engine.go b/engine/collection/epochmgr/engine.go index c2fca8cd0df..7346b006e74 100644 --- a/engine/collection/epochmgr/engine.go +++ b/engine/collection/epochmgr/engine.go @@ -454,7 +454,7 @@ func (e *Engine) prepareToStopEpochComponents(epochCounter, epochMaxHeight uint6 // setup phase, or when the node is restarted during the epoch setup phase. It // kicks off setup tasks for the phase, in particular submitting a vote for the // next epoch's root cluster QC. -func (e *Engine) onEpochSetupPhaseStarted(ctx irrecoverable.SignalerContext, nextEpoch protocol.Epoch) { +func (e *Engine) onEpochSetupPhaseStarted(ctx irrecoverable.SignalerContext, nextEpoch protocol.TentativeEpoch) { ctxWithCancel, cancel := context.WithCancel(ctx) defer cancel() diff --git a/engine/consensus/dkg/reactor_engine.go b/engine/consensus/dkg/reactor_engine.go index a3b1114d973..8ae8cbf7681 100644 --- a/engine/consensus/dkg/reactor_engine.go +++ b/engine/consensus/dkg/reactor_engine.go @@ -281,7 +281,7 @@ func (e *ReactorEngine) handleEpochCommittedPhaseStarted(currentEpochCounter uin // phase is finalized, the block's snapshot is guaranteed to already be // accessible in the protocol state at this point (even though the Badger // transaction finalizing the block has not been committed yet). - nextDKG, err := e.State.AtBlockID(firstBlock.ID()).Epochs().Next().DKG() + nextDKG, err := e.State.AtBlockID(firstBlock.ID()).Epochs().NextCommitted().DKG() if err != nil { // CAUTION: this should never happen, indicates a storage failure or corruption // TODO use irrecoverable context diff --git a/integration/tests/epochs/dynamic_epoch_transition_suite.go b/integration/tests/epochs/dynamic_epoch_transition_suite.go index 47dc158c631..44cac54436b 100644 --- a/integration/tests/epochs/dynamic_epoch_transition_suite.go +++ b/integration/tests/epochs/dynamic_epoch_transition_suite.go @@ -359,7 +359,7 @@ func (s *DynamicEpochTransitionSuite) AssertInEpochPhase(ctx context.Context, ex // AssertNodeNotParticipantInEpoch asserts that the given node ID does not exist // in the epoch's identity table. -func (s *DynamicEpochTransitionSuite) AssertNodeNotParticipantInEpoch(epoch protocol.Epoch, nodeID flow.Identifier) { +func (s *DynamicEpochTransitionSuite) AssertNodeNotParticipantInEpoch(epoch protocol.TentativeEpoch, nodeID flow.Identifier) { identities, err := epoch.InitialIdentities() require.NoError(s.T(), err) require.NotContains(s.T(), identities.NodeIDs(), nodeID) diff --git a/module/epochs.go b/module/epochs.go index cd5ca42b7e5..90855306482 100644 --- a/module/epochs.go +++ b/module/epochs.go @@ -18,7 +18,7 @@ type ClusterRootQCVoter interface { // Error returns: // - epochs.ClusterQCNoVoteError if we fail to vote for a benign reason // - generic error in case of critical unexpected failure - Vote(context.Context, protocol.Epoch) error + Vote(context.Context, protocol.TentativeEpoch) error } // QCContractClient enables interacting with the cluster QC aggregator smart diff --git a/module/epochs/epoch_lookup.go b/module/epochs/epoch_lookup.go index a1ab3b8d40e..989980825fc 100644 --- a/module/epochs/epoch_lookup.go +++ b/module/epochs/epoch_lookup.go @@ -189,7 +189,7 @@ func NewEpochLookup(state protocol.State) (*EpochLookup, error) { return nil, fmt.Errorf("could not check epoch phase: %w", err) } if phase == flow.EpochPhaseCommitted { - err := lookup.cacheEpoch(final.Epochs().Next()) + err := lookup.cacheEpoch(final.Epochs().NextCommitted()) if err != nil { return nil, fmt.Errorf("could not prepare previous epoch: %w", err) } @@ -317,7 +317,7 @@ func (lookup *EpochLookup) EpochExtended(epochCounter uint64, _ *flow.Header, ex // No errors are expected to be returned by the process callback during normal operation. func (lookup *EpochLookup) EpochCommittedPhaseStarted(_ uint64, first *flow.Header) { lookup.epochEvents <- func() error { - epoch := lookup.state.AtBlockID(first.ID()).Epochs().Next() + epoch := lookup.state.AtBlockID(first.ID()).Epochs().NextCommitted() err := lookup.cacheEpoch(epoch) if err != nil { return fmt.Errorf("failed to cache next epoch: %w", err) diff --git a/module/epochs/qc_voter.go b/module/epochs/qc_voter.go index 42d18cf1426..cb7aafa7eb5 100644 --- a/module/epochs/qc_voter.go +++ b/module/epochs/qc_voter.go @@ -77,7 +77,7 @@ func NewRootQCVoter( // Error returns: // - epochs.ClusterQCNoVoteError if we fail to vote for a benign reason // - generic error in case of critical unexpected failure -func (voter *RootQCVoter) Vote(ctx context.Context, epoch protocol.Epoch) error { +func (voter *RootQCVoter) Vote(ctx context.Context, epoch protocol.TentativeEpoch) error { counter, err := epoch.Counter() if err != nil { return fmt.Errorf("could not get epoch counter: %w", err) diff --git a/module/mock/cluster_root_qc_voter.go b/module/mock/cluster_root_qc_voter.go index 3006f4d33ab..9f9de8b826f 100644 --- a/module/mock/cluster_root_qc_voter.go +++ b/module/mock/cluster_root_qc_voter.go @@ -16,7 +16,7 @@ type ClusterRootQCVoter struct { } // Vote provides a mock function with given fields: _a0, _a1 -func (_m *ClusterRootQCVoter) Vote(_a0 context.Context, _a1 protocol.Epoch) error { +func (_m *ClusterRootQCVoter) Vote(_a0 context.Context, _a1 protocol.TentativeEpoch) error { ret := _m.Called(_a0, _a1) if len(ret) == 0 { @@ -24,7 +24,7 @@ func (_m *ClusterRootQCVoter) Vote(_a0 context.Context, _a1 protocol.Epoch) erro } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, protocol.Epoch) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, protocol.TentativeEpoch) error); ok { r0 = rf(_a0, _a1) } else { r0 = ret.Error(0) diff --git a/state/protocol/badger/mutator_test.go b/state/protocol/badger/mutator_test.go index b4a17e1c33b..a3b42fde05a 100644 --- a/state/protocol/badger/mutator_test.go +++ b/state/protocol/badger/mutator_test.go @@ -970,7 +970,7 @@ func TestExtendEpochTransitionValid(t *testing.T) { assert.NoError(t, err) // only setup event is finalized, not commit, so shouldn't be able to get certain info - _, err = state.AtBlockID(block3.ID()).Epochs().Next().DKG() + _, err = state.AtBlockID(block3.ID()).Epochs().NextCommitted().DKG() require.Error(t, err) // insert B4 @@ -1028,16 +1028,16 @@ func TestExtendEpochTransitionValid(t *testing.T) { // we should NOT be able to query epoch 2 commit info wrt blocks before 6 for _, blockID := range []flow.Identifier{block4.ID(), block5.ID()} { - _, err = state.AtBlockID(blockID).Epochs().Next().DKG() + _, err = state.AtBlockID(blockID).Epochs().NextCommitted().DKG() require.Error(t, err) } // now epoch 2 is fully ready, we can query anything we want about it wrt block 6 (or later) - _, err = state.AtBlockID(block6.ID()).Epochs().Next().InitialIdentities() + _, err = state.AtBlockID(block6.ID()).Epochs().NextCommitted().InitialIdentities() require.NoError(t, err) - _, err = state.AtBlockID(block6.ID()).Epochs().Next().Clustering() + _, err = state.AtBlockID(block6.ID()).Epochs().NextCommitted().Clustering() require.NoError(t, err) - _, err = state.AtBlockID(block6.ID()).Epochs().Next().DKG() + _, err = state.AtBlockID(block6.ID()).Epochs().NextCommitted().DKG() assert.NoError(t, err) // now that the commit event has been emitted, we should be in the committed phase @@ -1221,7 +1221,7 @@ func TestExtendConflictingEpochEvents(t *testing.T) { err = state.Extend(context.Background(), block6) require.NoError(t, err) - // block 7 builds on block 5, contains QC for block 7 + // block 7 builds on block 5, contains QC for block 5 block7 := unittest.BlockWithParentProtocolState(block5) err = state.Extend(context.Background(), block7) require.NoError(t, err) @@ -1232,13 +1232,14 @@ func TestExtendConflictingEpochEvents(t *testing.T) { require.NoError(t, err) // should be able query each epoch from the appropriate reference block - setup1FinalView, err := state.AtBlockID(block7.ID()).Epochs().Next().FinalView() + // TODO improve this check now that FinalView is not part of TentativeEpoch API + setup1counter, err := state.AtBlockID(block7.ID()).Epochs().Next().Counter() assert.NoError(t, err) - require.Equal(t, nextEpochSetup1.FinalView, setup1FinalView) + require.Equal(t, nextEpochSetup1.Counter, setup1counter) - setup2FinalView, err := state.AtBlockID(block8.ID()).Epochs().Next().FinalView() + setup2counter, err := state.AtBlockID(block8.ID()).Epochs().Next().Counter() assert.NoError(t, err) - require.Equal(t, nextEpochSetup2.FinalView, setup2FinalView) + require.Equal(t, nextEpochSetup2.Counter, setup2counter) }) } @@ -1331,7 +1332,7 @@ func TestExtendDuplicateEpochEvents(t *testing.T) { err = state.Extend(context.Background(), block6) require.NoError(t, err) - // block 7 builds on block 5, contains QC for block 7 + // block 7 builds on block 5, contains QC for block 5 block7 := unittest.BlockWithParentProtocolState(block5) err = state.Extend(context.Background(), block7) require.NoError(t, err) @@ -1342,14 +1343,15 @@ func TestExtendDuplicateEpochEvents(t *testing.T) { err = state.Extend(context.Background(), block8) require.NoError(t, err) - // should be able query each epoch from the appropriate reference block - finalView, err := state.AtBlockID(block7.ID()).Epochs().Next().FinalView() + // should be able to query each epoch from the appropriate reference block + // TODO improve this check now that FinalView is not part of TentativeEpoch API + counter, err := state.AtBlockID(block7.ID()).Epochs().Next().Counter() assert.NoError(t, err) - require.Equal(t, nextEpochSetup.FinalView, finalView) + require.Equal(t, nextEpochSetup.Counter, counter) - finalView, err = state.AtBlockID(block8.ID()).Epochs().Next().FinalView() + counter, err = state.AtBlockID(block8.ID()).Epochs().Next().Counter() assert.NoError(t, err) - require.Equal(t, nextEpochSetup.FinalView, finalView) + require.Equal(t, nextEpochSetup.Counter, counter) }) } @@ -1970,8 +1972,9 @@ func TestRecoveryFromEpochFallbackMode(t *testing.T) { epochState, err := state.Final().EpochProtocolState() require.NoError(t, err) epochPhase := epochState.EpochPhase() + require.Equal(t, flow.EpochPhaseCommitted, epochPhase, "next epoch has to be committed") - nextEpochQuery := state.Final().Epochs().Next() + nextEpochQuery := state.Final().Epochs().NextCommitted() nextEpochSetup, err := realprotocol.ToEpochSetup(nextEpochQuery) require.NoError(t, err) nextEpochCommit, err := realprotocol.ToEpochCommit(nextEpochQuery) @@ -1979,7 +1982,6 @@ func TestRecoveryFromEpochFallbackMode(t *testing.T) { require.Equal(t, &epochRecover.EpochSetup, nextEpochSetup, "next epoch has to be setup according to EpochRecover") require.Equal(t, &epochRecover.EpochCommit, nextEpochCommit, "next epoch has to be committed according to EpochRecover") - require.Equal(t, flow.EpochPhaseCommitted, epochPhase, "next epoch has to be committed") } // if we enter EFM in the EpochStaking phase, we should be able to recover by incorporating a valid EpochRecover event diff --git a/state/protocol/badger/snapshot.go b/state/protocol/badger/snapshot.go index f71d6fec188..b9b9a1cab45 100644 --- a/state/protocol/badger/snapshot.go +++ b/state/protocol/badger/snapshot.go @@ -403,7 +403,7 @@ func (q *EpochQuery) Current() protocol.Epoch { } // Next returns the next epoch, if it is available. -func (q *EpochQuery) Next() protocol.Epoch { +func (q *EpochQuery) Next() protocol.TentativeEpoch { epochState, err := q.snap.state.protocolState.EpochStateAtBlockID(q.snap.blockID) if err != nil { @@ -429,6 +429,31 @@ func (q *EpochQuery) Next() protocol.Epoch { return invalid.NewEpochf("data corruption: unknown epoch phase implies malformed protocol state epoch data") } +func (q *EpochQuery) NextCommitted() protocol.Epoch { + epochState, err := q.snap.state.protocolState.EpochStateAtBlockID(q.snap.blockID) + if err != nil { + return invalid.NewEpochf("could not get protocol state snapshot at block %x: %w", q.snap.blockID, err) + } + phase := epochState.EpochPhase() + entry := epochState.Entry() + + // if we are in the staking or fallback phase, the next epoch is not setup yet + if phase == flow.EpochPhaseStaking || phase == flow.EpochPhaseFallback { + return invalid.NewEpoch(protocol.ErrNextEpochNotSetup) + } + if phase == flow.EpochPhaseSetup { + return invalid.NewEpoch(protocol.ErrNextEpochNotCommitted) + } + // if we are in setup phase, return a SetupEpoch + nextSetup := entry.NextEpochSetup + // if we are in committed phase, return a CommittedEpoch + nextCommit := entry.NextEpochCommit + if phase == flow.EpochPhaseCommitted { + return inmem.NewCommittedEpoch(nextSetup, entry.NextEpoch.EpochExtensions, nextCommit) + } + return invalid.NewEpochf("data corruption: unknown epoch phase implies malformed protocol state epoch data") +} + // Previous returns the previous epoch. During the first epoch after the root // block, this returns a sentinel error (since there is no previous epoch). // For all other epochs, returns the previous epoch. diff --git a/state/protocol/badger/snapshot_test.go b/state/protocol/badger/snapshot_test.go index 7231f516dbc..71e4dbc9c2f 100644 --- a/state/protocol/badger/snapshot_test.go +++ b/state/protocol/badger/snapshot_test.go @@ -1238,11 +1238,16 @@ func TestSnapshot_EpochQuery(t *testing.T) { }) t.Run("epoch 2: after next epoch available", func(t *testing.T) { - for _, height := range append(epoch1.SetupRange(), epoch1.CommittedRange()...) { + for _, height := range epoch1.SetupRange() { counter, err := state.AtHeight(height).Epochs().Next().Counter() require.NoError(t, err) assert.Equal(t, epoch2Counter, counter) } + for _, height := range epoch1.CommittedRange() { + counter, err := state.AtHeight(height).Epochs().NextCommitted().Counter() + require.NoError(t, err) + assert.Equal(t, epoch2Counter, counter) + } }) }) @@ -1329,8 +1334,8 @@ func TestSnapshot_EpochFirstView(t *testing.T) { // test w.r.t. epoch 1 snapshot t.Run("Next", func(t *testing.T) { - for _, height := range append(epoch1.SetupRange(), epoch1.CommittedRange()...) { - actualFirstView, err := state.AtHeight(height).Epochs().Next().FirstView() + for _, height := range epoch1.CommittedRange() { + actualFirstView, err := state.AtHeight(height).Epochs().NextCommitted().FirstView() require.NoError(t, err) assert.Equal(t, epoch2FirstView, actualFirstView) } @@ -1386,9 +1391,9 @@ func TestSnapshot_EpochHeightBoundaries(t *testing.T) { _, err = state.Final().Epochs().Current().FinalHeight() assert.ErrorIs(t, err, protocol.ErrUnknownEpochBoundary) // first and final height of not started next epoch should be unknown - _, err = state.Final().Epochs().Next().FirstHeight() + _, err = state.Final().Epochs().NextCommitted().FirstHeight() assert.ErrorIs(t, err, protocol.ErrUnknownEpochBoundary) - _, err = state.Final().Epochs().Next().FinalHeight() + _, err = state.Final().Epochs().NextCommitted().FinalHeight() assert.ErrorIs(t, err, protocol.ErrUnknownEpochBoundary) }) diff --git a/state/protocol/badger/state_test.go b/state/protocol/badger/state_test.go index 789e4d9db27..2d1d107bbc8 100644 --- a/state/protocol/badger/state_test.go +++ b/state/protocol/badger/state_test.go @@ -216,9 +216,9 @@ func TestBootstrap_EpochHeightBoundaries(t *testing.T) { _, err = state.Final().Epochs().Current().FinalHeight() assert.ErrorIs(t, err, protocol.ErrUnknownEpochBoundary) // first and final height of not started next epoch should be unknown - _, err = state.Final().Epochs().Next().FirstHeight() + _, err = state.Final().Epochs().NextCommitted().FirstHeight() assert.ErrorIs(t, err, protocol.ErrUnknownEpochBoundary) - _, err = state.Final().Epochs().Next().FinalHeight() + _, err = state.Final().Epochs().NextCommitted().FinalHeight() assert.ErrorIs(t, err, protocol.ErrUnknownEpochBoundary) // first and final height of nonexistent previous epoch should be unknown _, err = state.Final().Epochs().Previous().FirstHeight() diff --git a/state/protocol/epoch.go b/state/protocol/epoch.go index 79888343e78..fdcce001d8d 100644 --- a/state/protocol/epoch.go +++ b/state/protocol/epoch.go @@ -17,7 +17,14 @@ type EpochQuery interface { // // Returns invalid.Epoch with ErrNextEpochNotSetup in the case that this method // is queried w.r.t. a snapshot within the flow.EpochPhaseStaking phase. - Next() Epoch + Next() TentativeEpoch + + // NextCommitted returns the next epoch as of this snapshot, only if it has + // been committed already (after flow.EpochPhaseCommitted) + // + // Returns invalid.Epoch with ErrNextEpochNotCommitted in the case that + // the current phase is flow.EpochPhaseStaking or flow.EpochPhaseSetup. + NextCommitted() Epoch // Previous returns the previous epoch as of this snapshot. Valid snapshots // must have a previous epoch for all epochs except that immediately after @@ -199,3 +206,28 @@ type Epoch interface { // * state.ErrUnknownSnapshotReference - if the epoch is queried from an unresolvable snapshot. FinalHeight() (uint64, error) } + +type TentativeEpoch interface { + + // Counter returns the Epoch's counter. + // Error returns: + // * protocol.ErrNoPreviousEpoch - if the epoch represents a previous epoch which does not exist. + // * protocol.ErrNextEpochNotSetup - if the epoch represents a next epoch which has not been set up. + // * state.ErrUnknownSnapshotReference - if the epoch is queried from an unresolvable snapshot. + Counter() (uint64, error) + + // InitialIdentities returns the identities for this epoch as they were + // specified in the EpochSetup service event. + // Error returns: + // * protocol.ErrNoPreviousEpoch - if the epoch represents a previous epoch which does not exist. + // * protocol.ErrNextEpochNotSetup - if the epoch represents a next epoch which has not been set up. + // * state.ErrUnknownSnapshotReference - if the epoch is queried from an unresolvable snapshot. + InitialIdentities() (flow.IdentitySkeletonList, error) + + // Clustering returns the cluster assignment for this epoch. + // Error returns: + // * protocol.ErrNoPreviousEpoch - if the epoch represents a previous epoch which does not exist. + // * protocol.ErrNextEpochNotSetup - if the epoch represents a next epoch which has not been set up. + // * state.ErrUnknownSnapshotReference - if the epoch is queried from an unresolvable snapshot. + Clustering() (flow.ClusterList, error) +} diff --git a/state/protocol/errors.go b/state/protocol/errors.go index c16a279d6ee..0211b12ac8e 100644 --- a/state/protocol/errors.go +++ b/state/protocol/errors.go @@ -20,7 +20,11 @@ var ( // ErrNextEpochNotCommitted is a sentinel error returned when the next epoch // has not been committed and information is queried that is only accessible // in the EpochCommitted phase. - ErrNextEpochNotCommitted = fmt.Errorf("queried info from EpochCommit event before it was emitted") + ErrNextEpochNotCommitted = fmt.Errorf("next epoch has not yet been committed") + + // ErrNextEpochAlreadyCommitted is a sentinel error returned when code tries + // to retrieve an uncommitted TentativeEpoch during the EpochCommitted phase + ErrNextEpochAlreadyCommitted = fmt.Errorf("retrieving tentative data when epoch is already committed") // ErrUnknownEpochBoundary is a sentinel returned when a query is made for an // epoch boundary which is unknown to this node. diff --git a/state/protocol/inmem/epoch.go b/state/protocol/inmem/epoch.go index 29e19e1f531..93168d73b83 100644 --- a/state/protocol/inmem/epoch.go +++ b/state/protocol/inmem/epoch.go @@ -30,18 +30,29 @@ func (eq Epochs) Current() protocol.Epoch { return NewCommittedEpoch(eq.entry.CurrentEpochSetup, eq.entry.CurrentEpoch.EpochExtensions, eq.entry.CurrentEpochCommit) } -func (eq Epochs) Next() protocol.Epoch { +func (eq Epochs) Next() protocol.TentativeEpoch { switch eq.entry.EpochPhase() { case flow.EpochPhaseStaking, flow.EpochPhaseFallback: return invalid.NewEpoch(protocol.ErrNextEpochNotSetup) case flow.EpochPhaseSetup: return NewSetupEpoch(eq.entry.NextEpochSetup, eq.entry.NextEpoch.EpochExtensions) + case flow.EpochPhaseCommitted: + return invalid.NewEpoch(protocol.ErrNextEpochAlreadyCommitted) + } + return invalid.NewEpochf("unexpected unknown phase in protocol state entry") +} + +func (eq Epochs) NextCommitted() protocol.Epoch { + switch eq.entry.EpochPhase() { + case flow.EpochPhaseStaking, flow.EpochPhaseFallback, flow.EpochPhaseSetup: + return invalid.NewEpoch(protocol.ErrNextEpochNotCommitted) case flow.EpochPhaseCommitted: return NewCommittedEpoch(eq.entry.NextEpochSetup, eq.entry.NextEpoch.EpochExtensions, eq.entry.NextEpochCommit) } return invalid.NewEpochf("unexpected unknown phase in protocol state entry") } + // setupEpoch is an implementation of protocol.Epoch backed by an EpochSetup service event. // Includes any extensions which have been included as of the reference block. // This is used for converting service events to inmem.Epoch. diff --git a/state/protocol/invalid/epoch.go b/state/protocol/invalid/epoch.go index ebda9919e3b..252dd7571aa 100644 --- a/state/protocol/invalid/epoch.go +++ b/state/protocol/invalid/epoch.go @@ -118,10 +118,12 @@ func (u *Epochs) Current() protocol.Epoch { return NewEpoch(u.err) } -func (u *Epochs) Next() protocol.Epoch { +func (u *Epochs) Next() protocol.TentativeEpoch { return NewEpoch(u.err) } +func (u *Epochs) NextCommitted() protocol.Epoch { return NewEpoch(u.err) } + func (u *Epochs) Previous() protocol.Epoch { return NewEpoch(u.err) } diff --git a/state/protocol/mock/epoch_query.go b/state/protocol/mock/epoch_query.go index 51c0458ff92..f60c8169f49 100644 --- a/state/protocol/mock/epoch_query.go +++ b/state/protocol/mock/epoch_query.go @@ -33,13 +33,33 @@ func (_m *EpochQuery) Current() protocol.Epoch { } // Next provides a mock function with given fields: -func (_m *EpochQuery) Next() protocol.Epoch { +func (_m *EpochQuery) Next() protocol.TentativeEpoch { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Next") } + var r0 protocol.TentativeEpoch + if rf, ok := ret.Get(0).(func() protocol.TentativeEpoch); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(protocol.TentativeEpoch) + } + } + + return r0 +} + +// NextCommitted provides a mock function with given fields: +func (_m *EpochQuery) NextCommitted() protocol.Epoch { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for NextCommitted") + } + var r0 protocol.Epoch if rf, ok := ret.Get(0).(func() protocol.Epoch); ok { r0 = rf() diff --git a/state/protocol/mock/tentative_epoch.go b/state/protocol/mock/tentative_epoch.go new file mode 100644 index 00000000000..4360d07f3d7 --- /dev/null +++ b/state/protocol/mock/tentative_epoch.go @@ -0,0 +1,115 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mock + +import ( + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" +) + +// TentativeEpoch is an autogenerated mock type for the TentativeEpoch type +type TentativeEpoch struct { + mock.Mock +} + +// Clustering provides a mock function with given fields: +func (_m *TentativeEpoch) Clustering() (flow.ClusterList, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Clustering") + } + + var r0 flow.ClusterList + var r1 error + if rf, ok := ret.Get(0).(func() (flow.ClusterList, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() flow.ClusterList); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(flow.ClusterList) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Counter provides a mock function with given fields: +func (_m *TentativeEpoch) Counter() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Counter") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InitialIdentities provides a mock function with given fields: +func (_m *TentativeEpoch) InitialIdentities() (flow.GenericIdentityList[flow.IdentitySkeleton], error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for InitialIdentities") + } + + var r0 flow.GenericIdentityList[flow.IdentitySkeleton] + var r1 error + if rf, ok := ret.Get(0).(func() (flow.GenericIdentityList[flow.IdentitySkeleton], error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() flow.GenericIdentityList[flow.IdentitySkeleton]); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(flow.GenericIdentityList[flow.IdentitySkeleton]) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewTentativeEpoch creates a new instance of TentativeEpoch. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTentativeEpoch(t interface { + mock.TestingT + Cleanup(func()) +}) *TentativeEpoch { + mock := &TentativeEpoch{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/utils/unittest/mocks/epoch_query.go b/utils/unittest/mocks/epoch_query.go index a38dfe3443d..bc57ae5ca54 100644 --- a/utils/unittest/mocks/epoch_query.go +++ b/utils/unittest/mocks/epoch_query.go @@ -13,6 +13,7 @@ import ( // EpochQuery implements protocol.EpochQuery for testing purposes. // Safe for concurrent use by multiple goroutines. +// Only supports committed epochs. type EpochQuery struct { t *testing.T mu sync.RWMutex @@ -40,7 +41,17 @@ func (mock *EpochQuery) Current() protocol.Epoch { return mock.byCounter[mock.counter] } -func (mock *EpochQuery) Next() protocol.Epoch { +func (mock *EpochQuery) Next() protocol.TentativeEpoch { + mock.mu.RLock() + defer mock.mu.RUnlock() + epoch, exists := mock.byCounter[mock.counter+1] + if !exists { + return invalid.NewEpoch(protocol.ErrNextEpochNotSetup) + } + return epoch +} + +func (mock *EpochQuery) NextCommitted() protocol.Epoch { mock.mu.RLock() defer mock.mu.RUnlock() epoch, exists := mock.byCounter[mock.counter+1] From 56d63c6d672447c61808a0f397830abf1d8b441c Mon Sep 17 00:00:00 2001 From: Tim Barry Date: Mon, 27 Jan 2025 11:46:45 -0800 Subject: [PATCH 2/7] update TestExtendConflictingEpochEvents --- state/protocol/badger/mutator_test.go | 38 ++++++++++++++++----------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/state/protocol/badger/mutator_test.go b/state/protocol/badger/mutator_test.go index a3b42fde05a..86a1e1e951d 100644 --- a/state/protocol/badger/mutator_test.go +++ b/state/protocol/badger/mutator_test.go @@ -1154,15 +1154,15 @@ func TestExtendConflictingEpochEvents(t *testing.T) { rootSetup := result.ServiceEvents[0].Event.(*flow.EpochSetup) - // create two conflicting epoch setup events for the next epoch (final view differs) + // create two conflicting epoch setup events for the next epoch (participants differ) nextEpochSetup1 := unittest.EpochSetupFixture( - unittest.WithParticipants(rootSetup.Participants), + unittest.WithParticipants(rootSetup.Participants[:len(rootSetup.Participants)]), unittest.SetupWithCounter(rootSetup.Counter+1), unittest.WithFinalView(rootSetup.FinalView+1000), unittest.WithFirstView(rootSetup.FinalView+1), ) nextEpochSetup2 := unittest.EpochSetupFixture( - unittest.WithParticipants(rootSetup.Participants), + unittest.WithParticipants(rootSetup.Participants[:len(rootSetup.Participants)-1]), unittest.SetupWithCounter(rootSetup.Counter+1), unittest.WithFinalView(rootSetup.FinalView+2000), // final view differs unittest.WithFirstView(rootSetup.FinalView+1), @@ -1192,7 +1192,7 @@ func TestExtendConflictingEpochEvents(t *testing.T) { block4.SetPayload(flow.Payload{ Receipts: []*flow.ExecutionReceiptMeta{block2Receipt.Meta()}, Results: []*flow.ExecutionResult{&block2Receipt.ExecutionResult}, - ProtocolStateID: block1.Payload.ProtocolStateID, + ProtocolStateID: block2.Payload.ProtocolStateID, }) err = state.Extend(context.Background(), block4) require.NoError(t, err) @@ -1231,15 +1231,24 @@ func TestExtendConflictingEpochEvents(t *testing.T) { err = state.Extend(context.Background(), block8) require.NoError(t, err) - // should be able query each epoch from the appropriate reference block - // TODO improve this check now that FinalView is not part of TentativeEpoch API - setup1counter, err := state.AtBlockID(block7.ID()).Epochs().Next().Counter() + // should be able to query each epoch from the appropriate reference block + setup1identities, err := state.AtBlockID(block7.ID()).Epochs().Next().InitialIdentities() assert.NoError(t, err) - require.Equal(t, nextEpochSetup1.Counter, setup1counter) + require.Equal(t, nextEpochSetup1.Participants, setup1identities) - setup2counter, err := state.AtBlockID(block8.ID()).Epochs().Next().Counter() + phase, err := state.AtBlockID(block8.ID()).EpochPhase() assert.NoError(t, err) - require.Equal(t, nextEpochSetup2.Counter, setup2counter) + switch phase { + case flow.EpochPhaseSetup: + setup2identities, err := state.AtBlockID(block8.ID()).Epochs().Next().InitialIdentities() + assert.NoError(t, err) + require.Equal(t, nextEpochSetup2.Participants, setup2identities) + case flow.EpochPhaseFallback: + t.Fatal("reached epoch fallback phase instead of epoch setup phase") + default: + t.Fatal("unexpected epoch phase") + } + }) } @@ -1344,14 +1353,13 @@ func TestExtendDuplicateEpochEvents(t *testing.T) { require.NoError(t, err) // should be able to query each epoch from the appropriate reference block - // TODO improve this check now that FinalView is not part of TentativeEpoch API - counter, err := state.AtBlockID(block7.ID()).Epochs().Next().Counter() + identities, err := state.AtBlockID(block7.ID()).Epochs().Next().InitialIdentities() assert.NoError(t, err) - require.Equal(t, nextEpochSetup.Counter, counter) + require.Equal(t, nextEpochSetup.Participants, identities) - counter, err = state.AtBlockID(block8.ID()).Epochs().Next().Counter() + identities, err = state.AtBlockID(block8.ID()).Epochs().Next().InitialIdentities() assert.NoError(t, err) - require.Equal(t, nextEpochSetup.Counter, counter) + require.Equal(t, nextEpochSetup.Participants, identities) }) } From b8ceaf13e5dc591ddb3ffbeb13d9eae3631e4a88 Mon Sep 17 00:00:00 2001 From: Tim Barry Date: Mon, 27 Jan 2025 12:09:35 -0800 Subject: [PATCH 3/7] rename EpochQuery.Next to NextUnsafe --- engine/collection/epochmgr/engine.go | 2 +- engine/collection/epochmgr/engine_test.go | 8 +++---- engine/consensus/dkg/reactor_engine.go | 2 +- .../epochs/dynamic_epoch_transition_suite.go | 2 +- state/protocol/badger/mutator_test.go | 16 ++++++------- state/protocol/badger/snapshot.go | 11 ++++----- state/protocol/badger/snapshot_test.go | 4 ++-- state/protocol/epoch.go | 9 ++++--- state/protocol/inmem/epoch.go | 3 +-- state/protocol/invalid/epoch.go | 2 +- state/protocol/mock/epoch_query.go | 24 +++++++++---------- utils/unittest/mocks/epoch_query.go | 2 +- 12 files changed, 42 insertions(+), 43 deletions(-) diff --git a/engine/collection/epochmgr/engine.go b/engine/collection/epochmgr/engine.go index 7346b006e74..9e936b7ee0c 100644 --- a/engine/collection/epochmgr/engine.go +++ b/engine/collection/epochmgr/engine.go @@ -343,7 +343,7 @@ func (e *Engine) handleEpochEvents(ctx irrecoverable.SignalerContext, ready comp ctx.Throw(err) } case firstBlock := <-e.epochSetupPhaseStartedEvents: - nextEpoch := e.state.AtBlockID(firstBlock.ID()).Epochs().Next() + nextEpoch := e.state.AtBlockID(firstBlock.ID()).Epochs().NextUnsafe() e.onEpochSetupPhaseStarted(ctx, nextEpoch) case epochCounter := <-e.epochStopEvents: err := e.stopEpochComponents(epochCounter) diff --git a/engine/collection/epochmgr/engine_test.go b/engine/collection/epochmgr/engine_test.go index 24a2152a667..8cc99259852 100644 --- a/engine/collection/epochmgr/engine_test.go +++ b/engine/collection/epochmgr/engine_test.go @@ -280,7 +280,7 @@ func (suite *Suite) TestRestartInSetupPhase() { suite.phase = flow.EpochPhaseSetup // should call voter with next epoch var called = make(chan struct{}) - suite.voter.On("Vote", mock.Anything, suite.epochQuery.Next()). + suite.voter.On("Vote", mock.Anything, suite.epochQuery.NextUnsafe()). Return(nil). Run(func(args mock.Arguments) { close(called) @@ -418,7 +418,7 @@ func (suite *Suite) TestStartAsUnauthorizedNode() { suite.phase = flow.EpochPhaseSetup // should call voter with next epoch var called = make(chan struct{}) - suite.voter.On("Vote", mock.Anything, suite.epochQuery.Next()). + suite.voter.On("Vote", mock.Anything, suite.epochQuery.NextUnsafe()). Return(nil). Run(func(args mock.Arguments) { close(called) @@ -444,7 +444,7 @@ func (suite *Suite) TestRespondToPhaseChange() { suite.phase = flow.EpochPhaseStaking // should call voter with next epoch var called = make(chan struct{}) - suite.voter.On("Vote", mock.Anything, suite.epochQuery.Next()). + suite.voter.On("Vote", mock.Anything, suite.epochQuery.NextUnsafe()). Return(nil). Run(func(args mock.Arguments) { close(called) @@ -543,7 +543,7 @@ func (suite *Suite) TestStopQcVoting() { suite.engineEventsDistributor.On("ActiveClustersChanged", mock.AnythingOfType("flow.ChainIDList")).Once() receivedCancelSignal := make(chan struct{}) - suite.voter.On("Vote", mock.Anything, suite.epochQuery.Next()). + suite.voter.On("Vote", mock.Anything, suite.epochQuery.NextUnsafe()). Return(nil). Run(func(args mock.Arguments) { ctx := args.Get(0).(context.Context) diff --git a/engine/consensus/dkg/reactor_engine.go b/engine/consensus/dkg/reactor_engine.go index 8ae8cbf7681..b43992f6c20 100644 --- a/engine/consensus/dkg/reactor_engine.go +++ b/engine/consensus/dkg/reactor_engine.go @@ -338,7 +338,7 @@ func (e *ReactorEngine) handleEpochCommittedPhaseStarted(currentEpochCounter uin // TODO document error returns func (e *ReactorEngine) getDKGInfo(firstBlockID flow.Identifier) (*dkgInfo, error) { currEpoch := e.State.AtBlockID(firstBlockID).Epochs().Current() - nextEpoch := e.State.AtBlockID(firstBlockID).Epochs().Next() + nextEpoch := e.State.AtBlockID(firstBlockID).Epochs().NextUnsafe() identities, err := nextEpoch.InitialIdentities() if err != nil { diff --git a/integration/tests/epochs/dynamic_epoch_transition_suite.go b/integration/tests/epochs/dynamic_epoch_transition_suite.go index 44cac54436b..fb6c90b05a0 100644 --- a/integration/tests/epochs/dynamic_epoch_transition_suite.go +++ b/integration/tests/epochs/dynamic_epoch_transition_suite.go @@ -534,7 +534,7 @@ func (s *DynamicEpochTransitionSuite) RunTestEpochJoinAndLeave(role flow.Role, c s.TimedLogf("observed finalized view %d -> pausing container", epoch1FinalView+1) // make sure container to replace is not a member of epoch 2 - s.AssertNodeNotParticipantInEpoch(rootSnapshot.Epochs().Next(), containerToReplace.Config.NodeID) + s.AssertNodeNotParticipantInEpoch(rootSnapshot.Epochs().NextUnsafe(), containerToReplace.Config.NodeID) // assert transition to second epoch happened as expected // if counter is still 0, epoch emergency fallback was triggered and we can fail early diff --git a/state/protocol/badger/mutator_test.go b/state/protocol/badger/mutator_test.go index 86a1e1e951d..4de4cd6ce27 100644 --- a/state/protocol/badger/mutator_test.go +++ b/state/protocol/badger/mutator_test.go @@ -957,16 +957,16 @@ func TestExtendEpochTransitionValid(t *testing.T) { // we should NOT be able to query epoch 2 wrt blocks before 3 for _, blockID := range []flow.Identifier{block1.ID(), block2.ID()} { - _, err = state.AtBlockID(blockID).Epochs().Next().InitialIdentities() + _, err = state.AtBlockID(blockID).Epochs().NextUnsafe().InitialIdentities() require.Error(t, err) - _, err = state.AtBlockID(blockID).Epochs().Next().Clustering() + _, err = state.AtBlockID(blockID).Epochs().NextUnsafe().Clustering() require.Error(t, err) } // we should be able to query epoch 2 wrt block 3 - _, err = state.AtBlockID(block3.ID()).Epochs().Next().InitialIdentities() + _, err = state.AtBlockID(block3.ID()).Epochs().NextUnsafe().InitialIdentities() assert.NoError(t, err) - _, err = state.AtBlockID(block3.ID()).Epochs().Next().Clustering() + _, err = state.AtBlockID(block3.ID()).Epochs().NextUnsafe().Clustering() assert.NoError(t, err) // only setup event is finalized, not commit, so shouldn't be able to get certain info @@ -1232,7 +1232,7 @@ func TestExtendConflictingEpochEvents(t *testing.T) { require.NoError(t, err) // should be able to query each epoch from the appropriate reference block - setup1identities, err := state.AtBlockID(block7.ID()).Epochs().Next().InitialIdentities() + setup1identities, err := state.AtBlockID(block7.ID()).Epochs().NextUnsafe().InitialIdentities() assert.NoError(t, err) require.Equal(t, nextEpochSetup1.Participants, setup1identities) @@ -1240,7 +1240,7 @@ func TestExtendConflictingEpochEvents(t *testing.T) { assert.NoError(t, err) switch phase { case flow.EpochPhaseSetup: - setup2identities, err := state.AtBlockID(block8.ID()).Epochs().Next().InitialIdentities() + setup2identities, err := state.AtBlockID(block8.ID()).Epochs().NextUnsafe().InitialIdentities() assert.NoError(t, err) require.Equal(t, nextEpochSetup2.Participants, setup2identities) case flow.EpochPhaseFallback: @@ -1353,11 +1353,11 @@ func TestExtendDuplicateEpochEvents(t *testing.T) { require.NoError(t, err) // should be able to query each epoch from the appropriate reference block - identities, err := state.AtBlockID(block7.ID()).Epochs().Next().InitialIdentities() + identities, err := state.AtBlockID(block7.ID()).Epochs().NextUnsafe().InitialIdentities() assert.NoError(t, err) require.Equal(t, nextEpochSetup.Participants, identities) - identities, err = state.AtBlockID(block8.ID()).Epochs().Next().InitialIdentities() + identities, err = state.AtBlockID(block8.ID()).Epochs().NextUnsafe().InitialIdentities() assert.NoError(t, err) require.Equal(t, nextEpochSetup.Participants, identities) }) diff --git a/state/protocol/badger/snapshot.go b/state/protocol/badger/snapshot.go index b9b9a1cab45..a4c1b28dc72 100644 --- a/state/protocol/badger/snapshot.go +++ b/state/protocol/badger/snapshot.go @@ -402,8 +402,8 @@ func (q *EpochQuery) Current() protocol.Epoch { return inmem.NewCommittedEpoch(setup, epochState.EpochExtensions(), commit) } -// Next returns the next epoch, if it is available. -func (q *EpochQuery) Next() protocol.TentativeEpoch { +// NextUnsafe returns the next epoch, if it is has been setup but not yet committed. +func (q *EpochQuery) NextUnsafe() protocol.TentativeEpoch { epochState, err := q.snap.state.protocolState.EpochStateAtBlockID(q.snap.blockID) if err != nil { @@ -421,10 +421,9 @@ func (q *EpochQuery) Next() protocol.TentativeEpoch { if phase == flow.EpochPhaseSetup { return inmem.NewSetupEpoch(nextSetup, entry.NextEpoch.EpochExtensions) } - // if we are in committed phase, return a CommittedEpoch - nextCommit := entry.NextEpochCommit + // if we are in committed phase, return an error if phase == flow.EpochPhaseCommitted { - return inmem.NewCommittedEpoch(nextSetup, entry.NextEpoch.EpochExtensions, nextCommit) + return invalid.NewEpoch(protocol.ErrNextEpochAlreadyCommitted) } return invalid.NewEpochf("data corruption: unknown epoch phase implies malformed protocol state epoch data") } @@ -444,9 +443,7 @@ func (q *EpochQuery) NextCommitted() protocol.Epoch { if phase == flow.EpochPhaseSetup { return invalid.NewEpoch(protocol.ErrNextEpochNotCommitted) } - // if we are in setup phase, return a SetupEpoch nextSetup := entry.NextEpochSetup - // if we are in committed phase, return a CommittedEpoch nextCommit := entry.NextEpochCommit if phase == flow.EpochPhaseCommitted { return inmem.NewCommittedEpoch(nextSetup, entry.NextEpoch.EpochExtensions, nextCommit) diff --git a/state/protocol/badger/snapshot_test.go b/state/protocol/badger/snapshot_test.go index 71e4dbc9c2f..55652a66188 100644 --- a/state/protocol/badger/snapshot_test.go +++ b/state/protocol/badger/snapshot_test.go @@ -1231,7 +1231,7 @@ func TestSnapshot_EpochQuery(t *testing.T) { t.Run("Next", func(t *testing.T) { t.Run("epoch 1: before next epoch available", func(t *testing.T) { for _, height := range epoch1.StakingRange() { - _, err := state.AtHeight(height).Epochs().Next().Counter() + _, err := state.AtHeight(height).Epochs().NextUnsafe().Counter() assert.Error(t, err) assert.True(t, errors.Is(err, protocol.ErrNextEpochNotSetup)) } @@ -1239,7 +1239,7 @@ func TestSnapshot_EpochQuery(t *testing.T) { t.Run("epoch 2: after next epoch available", func(t *testing.T) { for _, height := range epoch1.SetupRange() { - counter, err := state.AtHeight(height).Epochs().Next().Counter() + counter, err := state.AtHeight(height).Epochs().NextUnsafe().Counter() require.NoError(t, err) assert.Equal(t, epoch2Counter, counter) } diff --git a/state/protocol/epoch.go b/state/protocol/epoch.go index fdcce001d8d..03f9856a68a 100644 --- a/state/protocol/epoch.go +++ b/state/protocol/epoch.go @@ -12,12 +12,15 @@ type EpochQuery interface { // have a current epoch. Current() Epoch - // Next returns the next epoch as of this snapshot. Valid snapshots must + // NextUnsafe should only be used by components that actively advance the + // epoch from flow.EpochPhaseSetup to flow.EpochPhaseCommitted. + // NextUnsafe returns the next epoch as of this snapshot. Valid snapshots must // have a next epoch available after the transition to epoch setup phase. // // Returns invalid.Epoch with ErrNextEpochNotSetup in the case that this method - // is queried w.r.t. a snapshot within the flow.EpochPhaseStaking phase. - Next() TentativeEpoch + // is queried w.r.t. a snapshot within the flow.EpochPhaseStaking phase, or + // ErrNextEpochAlreadyCommitted during the flow.EpochPhaseCommitted phase. + NextUnsafe() TentativeEpoch // NextCommitted returns the next epoch as of this snapshot, only if it has // been committed already (after flow.EpochPhaseCommitted) diff --git a/state/protocol/inmem/epoch.go b/state/protocol/inmem/epoch.go index 93168d73b83..10d356e3fc5 100644 --- a/state/protocol/inmem/epoch.go +++ b/state/protocol/inmem/epoch.go @@ -30,7 +30,7 @@ func (eq Epochs) Current() protocol.Epoch { return NewCommittedEpoch(eq.entry.CurrentEpochSetup, eq.entry.CurrentEpoch.EpochExtensions, eq.entry.CurrentEpochCommit) } -func (eq Epochs) Next() protocol.TentativeEpoch { +func (eq Epochs) NextUnsafe() protocol.TentativeEpoch { switch eq.entry.EpochPhase() { case flow.EpochPhaseStaking, flow.EpochPhaseFallback: return invalid.NewEpoch(protocol.ErrNextEpochNotSetup) @@ -52,7 +52,6 @@ func (eq Epochs) NextCommitted() protocol.Epoch { return invalid.NewEpochf("unexpected unknown phase in protocol state entry") } - // setupEpoch is an implementation of protocol.Epoch backed by an EpochSetup service event. // Includes any extensions which have been included as of the reference block. // This is used for converting service events to inmem.Epoch. diff --git a/state/protocol/invalid/epoch.go b/state/protocol/invalid/epoch.go index 252dd7571aa..1818c1b94c3 100644 --- a/state/protocol/invalid/epoch.go +++ b/state/protocol/invalid/epoch.go @@ -118,7 +118,7 @@ func (u *Epochs) Current() protocol.Epoch { return NewEpoch(u.err) } -func (u *Epochs) Next() protocol.TentativeEpoch { +func (u *Epochs) NextUnsafe() protocol.TentativeEpoch { return NewEpoch(u.err) } diff --git a/state/protocol/mock/epoch_query.go b/state/protocol/mock/epoch_query.go index f60c8169f49..e9f8513e3f2 100644 --- a/state/protocol/mock/epoch_query.go +++ b/state/protocol/mock/epoch_query.go @@ -32,40 +32,40 @@ func (_m *EpochQuery) Current() protocol.Epoch { return r0 } -// Next provides a mock function with given fields: -func (_m *EpochQuery) Next() protocol.TentativeEpoch { +// NextCommitted provides a mock function with given fields: +func (_m *EpochQuery) NextCommitted() protocol.Epoch { ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for Next") + panic("no return value specified for NextCommitted") } - var r0 protocol.TentativeEpoch - if rf, ok := ret.Get(0).(func() protocol.TentativeEpoch); ok { + var r0 protocol.Epoch + if rf, ok := ret.Get(0).(func() protocol.Epoch); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(protocol.TentativeEpoch) + r0 = ret.Get(0).(protocol.Epoch) } } return r0 } -// NextCommitted provides a mock function with given fields: -func (_m *EpochQuery) NextCommitted() protocol.Epoch { +// NextUnsafe provides a mock function with given fields: +func (_m *EpochQuery) NextUnsafe() protocol.TentativeEpoch { ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for NextCommitted") + panic("no return value specified for NextUnsafe") } - var r0 protocol.Epoch - if rf, ok := ret.Get(0).(func() protocol.Epoch); ok { + var r0 protocol.TentativeEpoch + if rf, ok := ret.Get(0).(func() protocol.TentativeEpoch); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(protocol.Epoch) + r0 = ret.Get(0).(protocol.TentativeEpoch) } } diff --git a/utils/unittest/mocks/epoch_query.go b/utils/unittest/mocks/epoch_query.go index bc57ae5ca54..5d11dd0e7bf 100644 --- a/utils/unittest/mocks/epoch_query.go +++ b/utils/unittest/mocks/epoch_query.go @@ -41,7 +41,7 @@ func (mock *EpochQuery) Current() protocol.Epoch { return mock.byCounter[mock.counter] } -func (mock *EpochQuery) Next() protocol.TentativeEpoch { +func (mock *EpochQuery) NextUnsafe() protocol.TentativeEpoch { mock.mu.RLock() defer mock.mu.RUnlock() epoch, exists := mock.byCounter[mock.counter+1] From 0886915afa99f6fb8328628f682bed1a8226da95 Mon Sep 17 00:00:00 2001 From: Tim Barry Date: Mon, 27 Jan 2025 15:25:43 -0800 Subject: [PATCH 4/7] Epoch interface: change toEpochSetup to return TentativeEpoch --- consensus/hotstuff/cruisectl/block_time_controller_test.go | 3 ++- state/protocol/convert_test.go | 3 ++- state/protocol/inmem/epoch.go | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/consensus/hotstuff/cruisectl/block_time_controller_test.go b/consensus/hotstuff/cruisectl/block_time_controller_test.go index 269e40530fd..4caa2777904 100644 --- a/consensus/hotstuff/cruisectl/block_time_controller_test.go +++ b/consensus/hotstuff/cruisectl/block_time_controller_test.go @@ -232,8 +232,9 @@ func (bs *BlockTimeControllerSuite) TestOnEpochExtended() { FirstView: setupFixture.FirstView, FinalView: setupFixture.FinalView, } + commitFixture := unittest.EpochCommitFixture() - epoch := inmem.NewSetupEpoch(setupFixture, []flow.EpochExtension{extension}) + epoch := inmem.NewCommittedEpoch(setupFixture, []flow.EpochExtension{extension}, commitFixture) bs.epochs.Add(epoch) bs.epochs.Transition() diff --git a/state/protocol/convert_test.go b/state/protocol/convert_test.go index 30e49ad8091..4dc772c2465 100644 --- a/state/protocol/convert_test.go +++ b/state/protocol/convert_test.go @@ -13,7 +13,8 @@ import ( func TestToEpochSetup(t *testing.T) { expected := unittest.EpochSetupFixture() - epoch := inmem.NewSetupEpoch(expected, nil) + commit := unittest.EpochCommitFixture() + epoch := inmem.NewCommittedEpoch(expected, nil, commit) got, err := protocol.ToEpochSetup(epoch) require.NoError(t, err) diff --git a/state/protocol/inmem/epoch.go b/state/protocol/inmem/epoch.go index 10d356e3fc5..03df67c52b7 100644 --- a/state/protocol/inmem/epoch.go +++ b/state/protocol/inmem/epoch.go @@ -52,7 +52,7 @@ func (eq Epochs) NextCommitted() protocol.Epoch { return invalid.NewEpochf("unexpected unknown phase in protocol state entry") } -// setupEpoch is an implementation of protocol.Epoch backed by an EpochSetup service event. +// setupEpoch is an implementation of protocol.TentativeEpoch backed by an EpochSetup service event. // Includes any extensions which have been included as of the reference block. // This is used for converting service events to inmem.Epoch. type setupEpoch struct { @@ -272,7 +272,7 @@ func (e *heightBoundedEpoch) FinalHeight() (uint64, error) { // NewSetupEpoch returns a memory-backed epoch implementation based on an EpochSetup event. // Epoch information available after the setup phase will not be accessible in the resulting epoch instance. // No errors are expected during normal operations. -func NewSetupEpoch(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension) protocol.Epoch { +func NewSetupEpoch(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension) protocol.TentativeEpoch { return &setupEpoch{ setupEvent: setupEvent, extensions: extensions, From 07af6732131969235734dc4f612f918630dea0ad Mon Sep 17 00:00:00 2001 From: Tim Barry Date: Mon, 27 Jan 2025 15:26:37 -0800 Subject: [PATCH 5/7] Epoch interface: rename Epoch to CommittedEpoch --- cmd/util/cmd/epochs/cmd/reset.go | 2 +- .../hotstuff/committees/cluster_committee.go | 2 +- .../committees/consensus_committee.go | 6 +- .../committees/consensus_committee_test.go | 2 +- .../hotstuff/committees/leader/cluster.go | 2 +- .../hotstuff/committees/leader/consensus.go | 2 +- .../cruisectl/block_time_controller.go | 2 +- consensus/integration/nodes_test.go | 2 +- engine/collection/epochmgr/engine.go | 2 +- engine/collection/epochmgr/engine_test.go | 34 +- engine/collection/epochmgr/factories/epoch.go | 2 +- .../collection/epochmgr/factories/hotstuff.go | 2 +- engine/collection/epochmgr/factory.go | 2 +- .../epochmgr/mock/epoch_components_factory.go | 20 +- engine/collection/ingest/engine.go | 4 +- .../test/cluster_switchover_test.go | 2 +- module/epochs/epoch_lookup.go | 2 +- state/protocol/badger/snapshot.go | 6 +- state/protocol/convert.go | 10 +- state/protocol/epoch.go | 12 +- state/protocol/inmem/epoch.go | 18 +- state/protocol/invalid/epoch.go | 6 +- state/protocol/mock/committed_epoch.go | 489 ++++++++++++++++++ state/protocol/mock/epoch.go | 3 +- state/protocol/mock/epoch_query.go | 24 +- utils/unittest/mocks/epoch_query.go | 18 +- 26 files changed, 588 insertions(+), 88 deletions(-) create mode 100644 state/protocol/mock/committed_epoch.go diff --git a/cmd/util/cmd/epochs/cmd/reset.go b/cmd/util/cmd/epochs/cmd/reset.go index 2a1469dab35..188a9b7f7c7 100644 --- a/cmd/util/cmd/epochs/cmd/reset.go +++ b/cmd/util/cmd/epochs/cmd/reset.go @@ -142,7 +142,7 @@ func extractResetEpochArgs(snapshot *inmem.Snapshot) []cadence.Value { // ^ ^ ^-dkgPhase2FinalView // | `-dkgPhase1FinalView // `-stakingEndView -func getStakingAuctionEndView(epoch protocol.Epoch) (uint64, error) { +func getStakingAuctionEndView(epoch protocol.CommittedEpoch) (uint64, error) { dkgPhase1FinalView, err := epoch.DKGPhase1FinalView() if err != nil { return 0, err diff --git a/consensus/hotstuff/committees/cluster_committee.go b/consensus/hotstuff/committees/cluster_committee.go index 1a018bd7b55..ba331574c76 100644 --- a/consensus/hotstuff/committees/cluster_committee.go +++ b/consensus/hotstuff/committees/cluster_committee.go @@ -41,7 +41,7 @@ func NewClusterCommittee( state protocol.State, payloads storage.ClusterPayloads, cluster protocol.Cluster, - epoch protocol.Epoch, + epoch protocol.CommittedEpoch, me flow.Identifier, ) (*Cluster, error) { selection, err := leader.SelectionForCluster(cluster, epoch) diff --git a/consensus/hotstuff/committees/consensus_committee.go b/consensus/hotstuff/committees/consensus_committee.go index 46ee5ece41b..c307ec662f5 100644 --- a/consensus/hotstuff/committees/consensus_committee.go +++ b/consensus/hotstuff/committees/consensus_committee.go @@ -69,7 +69,7 @@ func (e *epochInfo) recomputeLeaderSelectionForExtendedViewRange(extension flow. // newEpochInfo retrieves the committee information and computes leader selection. // This can be cached and used for all by-view queries for this epoch. // No errors are expected during normal operation. -func newEpochInfo(epoch protocol.Epoch) (*epochInfo, error) { +func newEpochInfo(epoch protocol.CommittedEpoch) (*epochInfo, error) { randomSeed, err := epoch.RandomSource() if err != nil { return nil, fmt.Errorf("could not get epoch random source: %w", err) @@ -148,7 +148,7 @@ func NewConsensusCommittee(state protocol.State, me flow.Identifier) (*Consensus final := state.Final() // pre-compute leader selection for all presently relevant committed epochs - epochs := make([]protocol.Epoch, 0, 3) + epochs := make([]protocol.CommittedEpoch, 0, 3) // we prepare the previous epoch, if one exists exists, err := protocol.PreviousEpochExists(final) @@ -417,7 +417,7 @@ func (c *Consensus) epochInfoByView(view uint64) (*epochInfo, error) { // Calling prepareEpoch multiple times for the same epoch returns cached epoch information. // Input must be a committed epoch. // No errors are expected during normal operation. -func (c *Consensus) prepareEpoch(epoch protocol.Epoch) (*epochInfo, error) { +func (c *Consensus) prepareEpoch(epoch protocol.CommittedEpoch) (*epochInfo, error) { counter, err := epoch.Counter() if err != nil { return nil, fmt.Errorf("could not get counter for epoch to prepare: %w", err) diff --git a/consensus/hotstuff/committees/consensus_committee_test.go b/consensus/hotstuff/committees/consensus_committee_test.go index b262e51f41e..2826c499b6c 100644 --- a/consensus/hotstuff/committees/consensus_committee_test.go +++ b/consensus/hotstuff/committees/consensus_committee_test.go @@ -85,7 +85,7 @@ func (suite *ConsensusSuite) CreateAndStartCommittee() { // CommitEpoch adds the epoch to the protocol state and mimics the protocol state // behaviour when committing an epoch, by sending the protocol event to the committee. -func (suite *ConsensusSuite) CommitEpoch(epoch protocol.Epoch) { +func (suite *ConsensusSuite) CommitEpoch(epoch protocol.CommittedEpoch) { firstBlockOfCommittedPhase := unittest.BlockHeaderFixture() suite.state.On("AtHeight", firstBlockOfCommittedPhase.Height).Return(suite.snapshot) suite.epochs.Add(epoch) diff --git a/consensus/hotstuff/committees/leader/cluster.go b/consensus/hotstuff/committees/leader/cluster.go index ac95d0ce357..5f76a50ff4c 100644 --- a/consensus/hotstuff/committees/leader/cluster.go +++ b/consensus/hotstuff/committees/leader/cluster.go @@ -11,7 +11,7 @@ import ( // committee in the given epoch. A cluster containing nodes with zero `InitialWeight` // is an accepted input as long as there are nodes with positive weights. Zero-weight nodes // have zero probability of being selected as leaders in accordance with their weight. -func SelectionForCluster(cluster protocol.Cluster, epoch protocol.Epoch) (*LeaderSelection, error) { +func SelectionForCluster(cluster protocol.Cluster, epoch protocol.CommittedEpoch) (*LeaderSelection, error) { // sanity check to ensure the cluster and epoch match counter, err := epoch.Counter() if err != nil { diff --git a/consensus/hotstuff/committees/leader/consensus.go b/consensus/hotstuff/committees/leader/consensus.go index 1576122b1e1..14ae310a5f9 100644 --- a/consensus/hotstuff/committees/leader/consensus.go +++ b/consensus/hotstuff/committees/leader/consensus.go @@ -10,7 +10,7 @@ import ( ) // SelectionForConsensusFromEpoch is a ... -func SelectionForConsensusFromEpoch(epoch protocol.Epoch) (*LeaderSelection, error) { +func SelectionForConsensusFromEpoch(epoch protocol.CommittedEpoch) (*LeaderSelection, error) { identities, err := epoch.InitialIdentities() if err != nil { diff --git a/consensus/hotstuff/cruisectl/block_time_controller.go b/consensus/hotstuff/cruisectl/block_time_controller.go index 96a76187c0b..d796bc24e8e 100644 --- a/consensus/hotstuff/cruisectl/block_time_controller.go +++ b/consensus/hotstuff/cruisectl/block_time_controller.go @@ -40,7 +40,7 @@ type epochTiming struct { } // newEpochTiming queries the timing information from the given `epoch` and returns it as a new `epochTiming` instance. -func newEpochTiming(epoch protocol.Epoch) (*epochTiming, error) { +func newEpochTiming(epoch protocol.CommittedEpoch) (*epochTiming, error) { firstView, err := epoch.FirstView() if err != nil { return nil, fmt.Errorf("could not retrieve epoch's first view: %w", err) diff --git a/consensus/integration/nodes_test.go b/consensus/integration/nodes_test.go index a3ddc5d325d..f8b1961a97f 100644 --- a/consensus/integration/nodes_test.go +++ b/consensus/integration/nodes_test.go @@ -160,7 +160,7 @@ type epochInfo struct { } // buildEpochLookupList is a helper function which builds an auxiliary structure of epochs sorted by counter -func buildEpochLookupList(epochs ...protocol.Epoch) []epochInfo { +func buildEpochLookupList(epochs ...protocol.CommittedEpoch) []epochInfo { infos := make([]epochInfo, 0) for _, epoch := range epochs { finalView, err := epoch.FinalView() diff --git a/engine/collection/epochmgr/engine.go b/engine/collection/epochmgr/engine.go index 9e936b7ee0c..4aaa890769d 100644 --- a/engine/collection/epochmgr/engine.go +++ b/engine/collection/epochmgr/engine.go @@ -288,7 +288,7 @@ func (e *Engine) Done() <-chan struct{} { // the given epoch, using the configured factory. // Error returns: // - ErrNotAuthorizedForEpoch if this node is not authorized in the epoch. -func (e *Engine) createEpochComponents(epoch protocol.Epoch) (*EpochComponents, error) { +func (e *Engine) createEpochComponents(epoch protocol.CommittedEpoch) (*EpochComponents, error) { counter, err := epoch.Counter() if err != nil { return nil, fmt.Errorf("could not get epoch counter: %w", err) diff --git a/engine/collection/epochmgr/engine_test.go b/engine/collection/epochmgr/engine_test.go index 8cc99259852..9f2d11c04fd 100644 --- a/engine/collection/epochmgr/engine_test.go +++ b/engine/collection/epochmgr/engine_test.go @@ -110,25 +110,35 @@ type Suite struct { func (suite *Suite) MockFactoryCreate(arg any) { suite.factory.On("Create", arg). Run(func(args mock.Arguments) { - epoch, ok := args.Get(0).(realprotocol.Epoch) + epoch, ok := args.Get(0).(realprotocol.CommittedEpoch) suite.Require().Truef(ok, "invalid type %T", args.Get(0)) counter, err := epoch.Counter() suite.Require().Nil(err) suite.components[counter] = newMockComponents(suite.T()) }). Return( - func(epoch realprotocol.Epoch) realcluster.State { return suite.ComponentsForEpoch(epoch).state }, - func(epoch realprotocol.Epoch) component.Component { return suite.ComponentsForEpoch(epoch).prop }, - func(epoch realprotocol.Epoch) realmodule.ReadyDoneAware { return suite.ComponentsForEpoch(epoch).sync }, - func(epoch realprotocol.Epoch) realmodule.HotStuff { return suite.ComponentsForEpoch(epoch).hotstuff }, - func(epoch realprotocol.Epoch) hotstuff.VoteAggregator { + func(epoch realprotocol.CommittedEpoch) realcluster.State { + return suite.ComponentsForEpoch(epoch).state + }, + func(epoch realprotocol.CommittedEpoch) component.Component { + return suite.ComponentsForEpoch(epoch).prop + }, + func(epoch realprotocol.CommittedEpoch) realmodule.ReadyDoneAware { + return suite.ComponentsForEpoch(epoch).sync + }, + func(epoch realprotocol.CommittedEpoch) realmodule.HotStuff { + return suite.ComponentsForEpoch(epoch).hotstuff + }, + func(epoch realprotocol.CommittedEpoch) hotstuff.VoteAggregator { return suite.ComponentsForEpoch(epoch).voteAggregator }, - func(epoch realprotocol.Epoch) hotstuff.TimeoutAggregator { + func(epoch realprotocol.CommittedEpoch) hotstuff.TimeoutAggregator { return suite.ComponentsForEpoch(epoch).timeoutAggregator }, - func(epoch realprotocol.Epoch) component.Component { return suite.ComponentsForEpoch(epoch).messageHub }, - func(epoch realprotocol.Epoch) error { return nil }, + func(epoch realprotocol.CommittedEpoch) component.Component { + return suite.ComponentsForEpoch(epoch).messageHub + }, + func(epoch realprotocol.CommittedEpoch) error { return nil }, ).Maybe() } @@ -239,7 +249,7 @@ func (suite *Suite) AssertEpochStopped(counter uint64) { components.sync.AssertCalled(suite.T(), "Done") } -func (suite *Suite) ComponentsForEpoch(epoch realprotocol.Epoch) *mockComponents { +func (suite *Suite) ComponentsForEpoch(epoch realprotocol.CommittedEpoch) *mockComponents { counter, err := epoch.Counter() suite.Require().Nil(err, "cannot get counter") components, ok := suite.components[counter] @@ -252,12 +262,12 @@ func (suite *Suite) ComponentsForEpoch(epoch realprotocol.Epoch) *mockComponents func (suite *Suite) MockAsUnauthorizedNode(forEpoch uint64) { // mock as unauthorized for given epoch only - unauthorizedMatcher := func(epoch realprotocol.Epoch) bool { + unauthorizedMatcher := func(epoch realprotocol.CommittedEpoch) bool { counter, err := epoch.Counter() require.NoError(suite.T(), err) return counter == forEpoch } - authorizedMatcher := func(epoch realprotocol.Epoch) bool { return !unauthorizedMatcher(epoch) } + authorizedMatcher := func(epoch realprotocol.CommittedEpoch) bool { return !unauthorizedMatcher(epoch) } suite.factory = epochmgr.NewEpochComponentsFactory(suite.T()) suite.factory. diff --git a/engine/collection/epochmgr/factories/epoch.go b/engine/collection/epochmgr/factories/epoch.go index 25f6c42ab89..3efadcb6c81 100644 --- a/engine/collection/epochmgr/factories/epoch.go +++ b/engine/collection/epochmgr/factories/epoch.go @@ -55,7 +55,7 @@ func NewEpochComponentsFactory( } func (factory *EpochComponentsFactory) Create( - epoch protocol.Epoch, + epoch protocol.CommittedEpoch, ) ( state cluster.State, compliance component.Component, diff --git a/engine/collection/epochmgr/factories/hotstuff.go b/engine/collection/epochmgr/factories/hotstuff.go index 05bc6c0ebfa..6b4895136aa 100644 --- a/engine/collection/epochmgr/factories/hotstuff.go +++ b/engine/collection/epochmgr/factories/hotstuff.go @@ -65,7 +65,7 @@ func NewHotStuffFactory( } func (f *HotStuffFactory) CreateModules( - epoch protocol.Epoch, + epoch protocol.CommittedEpoch, cluster protocol.Cluster, clusterState cluster.State, headers storage.Headers, diff --git a/engine/collection/epochmgr/factory.go b/engine/collection/epochmgr/factory.go index 801f314ff87..c6370674e51 100644 --- a/engine/collection/epochmgr/factory.go +++ b/engine/collection/epochmgr/factory.go @@ -18,7 +18,7 @@ type EpochComponentsFactory interface { // a given epoch counter. // // Must return ErrNotAuthorizedForEpoch if this node is not authorized in the epoch. - Create(epoch protocol.Epoch) ( + Create(epoch protocol.CommittedEpoch) ( state cluster.State, proposal component.Component, sync module.ReadyDoneAware, diff --git a/engine/collection/epochmgr/mock/epoch_components_factory.go b/engine/collection/epochmgr/mock/epoch_components_factory.go index 3caae7cb6c3..e9db8bbc972 100644 --- a/engine/collection/epochmgr/mock/epoch_components_factory.go +++ b/engine/collection/epochmgr/mock/epoch_components_factory.go @@ -21,7 +21,7 @@ type EpochComponentsFactory struct { } // Create provides a mock function with given fields: epoch -func (_m *EpochComponentsFactory) Create(epoch protocol.Epoch) (cluster.State, component.Component, module.ReadyDoneAware, module.HotStuff, hotstuff.VoteAggregator, hotstuff.TimeoutAggregator, component.Component, error) { +func (_m *EpochComponentsFactory) Create(epoch protocol.CommittedEpoch) (cluster.State, component.Component, module.ReadyDoneAware, module.HotStuff, hotstuff.VoteAggregator, hotstuff.TimeoutAggregator, component.Component, error) { ret := _m.Called(epoch) if len(ret) == 0 { @@ -36,10 +36,10 @@ func (_m *EpochComponentsFactory) Create(epoch protocol.Epoch) (cluster.State, c var r5 hotstuff.TimeoutAggregator var r6 component.Component var r7 error - if rf, ok := ret.Get(0).(func(protocol.Epoch) (cluster.State, component.Component, module.ReadyDoneAware, module.HotStuff, hotstuff.VoteAggregator, hotstuff.TimeoutAggregator, component.Component, error)); ok { + if rf, ok := ret.Get(0).(func(protocol.CommittedEpoch) (cluster.State, component.Component, module.ReadyDoneAware, module.HotStuff, hotstuff.VoteAggregator, hotstuff.TimeoutAggregator, component.Component, error)); ok { return rf(epoch) } - if rf, ok := ret.Get(0).(func(protocol.Epoch) cluster.State); ok { + if rf, ok := ret.Get(0).(func(protocol.CommittedEpoch) cluster.State); ok { r0 = rf(epoch) } else { if ret.Get(0) != nil { @@ -47,7 +47,7 @@ func (_m *EpochComponentsFactory) Create(epoch protocol.Epoch) (cluster.State, c } } - if rf, ok := ret.Get(1).(func(protocol.Epoch) component.Component); ok { + if rf, ok := ret.Get(1).(func(protocol.CommittedEpoch) component.Component); ok { r1 = rf(epoch) } else { if ret.Get(1) != nil { @@ -55,7 +55,7 @@ func (_m *EpochComponentsFactory) Create(epoch protocol.Epoch) (cluster.State, c } } - if rf, ok := ret.Get(2).(func(protocol.Epoch) module.ReadyDoneAware); ok { + if rf, ok := ret.Get(2).(func(protocol.CommittedEpoch) module.ReadyDoneAware); ok { r2 = rf(epoch) } else { if ret.Get(2) != nil { @@ -63,7 +63,7 @@ func (_m *EpochComponentsFactory) Create(epoch protocol.Epoch) (cluster.State, c } } - if rf, ok := ret.Get(3).(func(protocol.Epoch) module.HotStuff); ok { + if rf, ok := ret.Get(3).(func(protocol.CommittedEpoch) module.HotStuff); ok { r3 = rf(epoch) } else { if ret.Get(3) != nil { @@ -71,7 +71,7 @@ func (_m *EpochComponentsFactory) Create(epoch protocol.Epoch) (cluster.State, c } } - if rf, ok := ret.Get(4).(func(protocol.Epoch) hotstuff.VoteAggregator); ok { + if rf, ok := ret.Get(4).(func(protocol.CommittedEpoch) hotstuff.VoteAggregator); ok { r4 = rf(epoch) } else { if ret.Get(4) != nil { @@ -79,7 +79,7 @@ func (_m *EpochComponentsFactory) Create(epoch protocol.Epoch) (cluster.State, c } } - if rf, ok := ret.Get(5).(func(protocol.Epoch) hotstuff.TimeoutAggregator); ok { + if rf, ok := ret.Get(5).(func(protocol.CommittedEpoch) hotstuff.TimeoutAggregator); ok { r5 = rf(epoch) } else { if ret.Get(5) != nil { @@ -87,7 +87,7 @@ func (_m *EpochComponentsFactory) Create(epoch protocol.Epoch) (cluster.State, c } } - if rf, ok := ret.Get(6).(func(protocol.Epoch) component.Component); ok { + if rf, ok := ret.Get(6).(func(protocol.CommittedEpoch) component.Component); ok { r6 = rf(epoch) } else { if ret.Get(6) != nil { @@ -95,7 +95,7 @@ func (_m *EpochComponentsFactory) Create(epoch protocol.Epoch) (cluster.State, c } } - if rf, ok := ret.Get(7).(func(protocol.Epoch) error); ok { + if rf, ok := ret.Get(7).(func(protocol.CommittedEpoch) error); ok { r7 = rf(epoch) } else { r7 = ret.Error(7) diff --git a/engine/collection/ingest/engine.go b/engine/collection/ingest/engine.go index ae21f71253f..860fd84737f 100644 --- a/engine/collection/ingest/engine.go +++ b/engine/collection/ingest/engine.go @@ -298,7 +298,7 @@ func (e *Engine) onTransaction(originID flow.Identifier, tx *flow.TransactionBod // a member of the reference epoch. This is an expected condition and the transaction // should be discarded. // - other error for any other, unexpected error condition. -func (e *Engine) getLocalCluster(refEpoch protocol.Epoch) (flow.IdentitySkeletonList, error) { +func (e *Engine) getLocalCluster(refEpoch protocol.CommittedEpoch) (flow.IdentitySkeletonList, error) { epochCounter, err := refEpoch.Counter() if err != nil { return nil, fmt.Errorf("could not get counter for reference epoch: %w", err) @@ -336,7 +336,7 @@ func (e *Engine) getLocalCluster(refEpoch protocol.Epoch) (flow.IdentitySkeleton // * other error for any other unexpected error condition. func (e *Engine) ingestTransaction( log zerolog.Logger, - refEpoch protocol.Epoch, + refEpoch protocol.CommittedEpoch, tx *flow.TransactionBody, txID flow.Identifier, localClusterFingerprint flow.Identifier, diff --git a/engine/collection/test/cluster_switchover_test.go b/engine/collection/test/cluster_switchover_test.go index 5f089ff0e52..be2034340d7 100644 --- a/engine/collection/test/cluster_switchover_test.go +++ b/engine/collection/test/cluster_switchover_test.go @@ -322,7 +322,7 @@ func (tc *ClusterSwitchoverTestCase) Collector(id flow.Identifier) testmock.Coll } // Clusters returns the clusters for the current epoch. -func (tc *ClusterSwitchoverTestCase) Clusters(epoch protocol.Epoch) []protocol.Cluster { +func (tc *ClusterSwitchoverTestCase) Clusters(epoch protocol.CommittedEpoch) []protocol.Cluster { clustering, err := epoch.Clustering() require.NoError(tc.T(), err) diff --git a/module/epochs/epoch_lookup.go b/module/epochs/epoch_lookup.go index 989980825fc..dbf2275f14a 100644 --- a/module/epochs/epoch_lookup.go +++ b/module/epochs/epoch_lookup.go @@ -200,7 +200,7 @@ func NewEpochLookup(state protocol.State) (*EpochLookup, error) { // cacheEpoch caches the given epoch's view range. Must only be called with committed epochs. // No errors are expected during normal operation. -func (lookup *EpochLookup) cacheEpoch(epoch protocol.Epoch) error { +func (lookup *EpochLookup) cacheEpoch(epoch protocol.CommittedEpoch) error { counter, err := epoch.Counter() if err != nil { return err diff --git a/state/protocol/badger/snapshot.go b/state/protocol/badger/snapshot.go index a4c1b28dc72..0fa7ca72980 100644 --- a/state/protocol/badger/snapshot.go +++ b/state/protocol/badger/snapshot.go @@ -382,7 +382,7 @@ type EpochQuery struct { } // Current returns the current epoch. -func (q *EpochQuery) Current() protocol.Epoch { +func (q *EpochQuery) Current() protocol.CommittedEpoch { // all errors returned from storage reads here are unexpected, because all // snapshots reside within a current epoch, which must be queryable epochState, err := q.snap.state.protocolState.EpochStateAtBlockID(q.snap.blockID) @@ -428,7 +428,7 @@ func (q *EpochQuery) NextUnsafe() protocol.TentativeEpoch { return invalid.NewEpochf("data corruption: unknown epoch phase implies malformed protocol state epoch data") } -func (q *EpochQuery) NextCommitted() protocol.Epoch { +func (q *EpochQuery) NextCommitted() protocol.CommittedEpoch { epochState, err := q.snap.state.protocolState.EpochStateAtBlockID(q.snap.blockID) if err != nil { return invalid.NewEpochf("could not get protocol state snapshot at block %x: %w", q.snap.blockID, err) @@ -454,7 +454,7 @@ func (q *EpochQuery) NextCommitted() protocol.Epoch { // Previous returns the previous epoch. During the first epoch after the root // block, this returns a sentinel error (since there is no previous epoch). // For all other epochs, returns the previous epoch. -func (q *EpochQuery) Previous() protocol.Epoch { +func (q *EpochQuery) Previous() protocol.CommittedEpoch { epochState, err := q.snap.state.protocolState.EpochStateAtBlockID(q.snap.blockID) if err != nil { diff --git a/state/protocol/convert.go b/state/protocol/convert.go index 9431bfcac8c..1018814b0b0 100644 --- a/state/protocol/convert.go +++ b/state/protocol/convert.go @@ -10,13 +10,13 @@ import ( "github.com/onflow/flow-go/module/signature" ) -// ToEpochSetup converts an Epoch interface instance to the underlying concrete +// ToEpochSetup converts a CommittedEpoch interface instance to the underlying concrete // epoch setup service event. The input must be a valid, set up epoch. // Error returns: // * protocol.ErrNoPreviousEpoch - if the epoch represents a previous epoch which does not exist. // * protocol.ErrNextEpochNotSetup - if the epoch represents a next epoch which has not been set up. // * state.ErrUnknownSnapshotReference - if the epoch is queried from an unresolvable snapshot. -func ToEpochSetup(epoch Epoch) (*flow.EpochSetup, error) { +func ToEpochSetup(epoch CommittedEpoch) (*flow.EpochSetup, error) { counter, err := epoch.Counter() if err != nil { return nil, fmt.Errorf("could not get epoch counter: %w", err) @@ -71,14 +71,14 @@ func ToEpochSetup(epoch Epoch) (*flow.EpochSetup, error) { return setup, nil } -// ToEpochCommit converts an Epoch interface instance to the underlying +// ToEpochCommit converts a CommittedEpoch interface instance to the underlying // concrete epoch commit service event. The epoch must have been committed. // Error returns: // * protocol.ErrNoPreviousEpoch - if the epoch represents a previous epoch which does not exist. // * protocol.ErrNextEpochNotSetup - if the epoch represents a next epoch which has not been set up. // * protocol.ErrNextEpochNotCommitted - if the epoch has not been committed. // * state.ErrUnknownSnapshotReference - if the epoch is queried from an unresolvable snapshot. -func ToEpochCommit(epoch Epoch) (*flow.EpochCommit, error) { +func ToEpochCommit(epoch CommittedEpoch) (*flow.EpochCommit, error) { counter, err := epoch.Counter() if err != nil { return nil, fmt.Errorf("could not get epoch counter: %w", err) @@ -162,7 +162,7 @@ func GetDKGParticipantKeys(dkg DKG, participants flow.IdentitySkeletonList) ([]c // * protocol.ErrNextEpochNotSetup - if the epoch represents a next epoch which has not been set up. // * protocol.ErrNextEpochNotCommitted - if the epoch has not been committed. // * state.ErrUnknownSnapshotReference - if the epoch is queried from an unresolvable snapshot. -func DKGPhaseViews(epoch Epoch) (phase1FinalView uint64, phase2FinalView uint64, phase3FinalView uint64, err error) { +func DKGPhaseViews(epoch CommittedEpoch) (phase1FinalView uint64, phase2FinalView uint64, phase3FinalView uint64, err error) { phase1FinalView, err = epoch.DKGPhase1FinalView() if err != nil { return diff --git a/state/protocol/epoch.go b/state/protocol/epoch.go index 03f9856a68a..adbd7d994fe 100644 --- a/state/protocol/epoch.go +++ b/state/protocol/epoch.go @@ -10,7 +10,7 @@ type EpochQuery interface { // Current returns the current epoch as of this snapshot. All valid snapshots // have a current epoch. - Current() Epoch + Current() CommittedEpoch // NextUnsafe should only be used by components that actively advance the // epoch from flow.EpochPhaseSetup to flow.EpochPhaseCommitted. @@ -27,7 +27,7 @@ type EpochQuery interface { // // Returns invalid.Epoch with ErrNextEpochNotCommitted in the case that // the current phase is flow.EpochPhaseStaking or flow.EpochPhaseSetup. - NextCommitted() Epoch + NextCommitted() CommittedEpoch // Previous returns the previous epoch as of this snapshot. Valid snapshots // must have a previous epoch for all epochs except that immediately after @@ -36,10 +36,10 @@ type EpochQuery interface { // // Returns invalid.Epoch with ErrNoPreviousEpoch in the case that this method // is queried w.r.t. a snapshot from the first epoch after the root block. - Previous() Epoch + Previous() CommittedEpoch } -// Epoch contains the information specific to a certain Epoch (defined +// CommittedEpoch contains the information specific to a certain Epoch (defined // by the epoch Counter). Note that the Epoch preparation can differ along // different forks, since the emission of service events is fork-dependent. // Therefore, an epoch exists RELATIVE to the snapshot from which it was @@ -48,7 +48,7 @@ type EpochQuery interface { // CAUTION: Clients must ensure to query epochs only for finalized blocks to // ensure they query finalized epoch information. // -// An Epoch instance is constant and reports the identical information +// A CommittedEpoch instance is constant and reports the identical information // even if progress is made later and more information becomes available in // subsequent blocks. // @@ -66,7 +66,7 @@ type EpochQuery interface { // 2. The error caching pattern encourages potentially dangerous snapshot query patterns // // See https://github.com/dapperlabs/flow-go/issues/6368 for details and proposal -type Epoch interface { +type CommittedEpoch interface { // Counter returns the Epoch's counter. // Error returns: diff --git a/state/protocol/inmem/epoch.go b/state/protocol/inmem/epoch.go index 03df67c52b7..6740d1b6f7a 100644 --- a/state/protocol/inmem/epoch.go +++ b/state/protocol/inmem/epoch.go @@ -19,14 +19,14 @@ type Epochs struct { var _ protocol.EpochQuery = (*Epochs)(nil) -func (eq Epochs) Previous() protocol.Epoch { +func (eq Epochs) Previous() protocol.CommittedEpoch { if eq.entry.PreviousEpoch == nil { return invalid.NewEpoch(protocol.ErrNoPreviousEpoch) } return NewCommittedEpoch(eq.entry.PreviousEpochSetup, eq.entry.PreviousEpoch.EpochExtensions, eq.entry.PreviousEpochCommit) } -func (eq Epochs) Current() protocol.Epoch { +func (eq Epochs) Current() protocol.CommittedEpoch { return NewCommittedEpoch(eq.entry.CurrentEpochSetup, eq.entry.CurrentEpoch.EpochExtensions, eq.entry.CurrentEpochCommit) } @@ -42,7 +42,7 @@ func (eq Epochs) NextUnsafe() protocol.TentativeEpoch { return invalid.NewEpochf("unexpected unknown phase in protocol state entry") } -func (eq Epochs) NextCommitted() protocol.Epoch { +func (eq Epochs) NextCommitted() protocol.CommittedEpoch { switch eq.entry.EpochPhase() { case flow.EpochPhaseStaking, flow.EpochPhaseFallback, flow.EpochPhaseSetup: return invalid.NewEpoch(protocol.ErrNextEpochNotCommitted) @@ -162,7 +162,7 @@ func (es *setupEpoch) FinalHeight() (uint64, error) { return 0, protocol.ErrUnknownEpochBoundary } -// committedEpoch is an implementation of protocol.Epoch backed by an EpochSetup +// committedEpoch is an implementation of protocol.CommittedEpoch backed by an EpochSetup // and EpochCommit service event. This is used for converting service events to // inmem.Epoch. type committedEpoch struct { @@ -253,7 +253,7 @@ type heightBoundedEpoch struct { finalHeight *uint64 } -var _ protocol.Epoch = (*heightBoundedEpoch)(nil) +var _ protocol.CommittedEpoch = (*heightBoundedEpoch)(nil) func (e *heightBoundedEpoch) FirstHeight() (uint64, error) { if e.firstHeight != nil { @@ -282,7 +282,7 @@ func NewSetupEpoch(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension // NewCommittedEpoch returns a memory-backed epoch implementation based on an // EpochSetup and EpochCommit events. // No errors are expected during normal operations. -func NewCommittedEpoch(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit) protocol.Epoch { +func NewCommittedEpoch(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit) protocol.CommittedEpoch { return &committedEpoch{ setupEpoch: setupEpoch{ setupEvent: setupEvent, @@ -295,7 +295,7 @@ func NewCommittedEpoch(setupEvent *flow.EpochSetup, extensions []flow.EpochExten // NewEpochWithStartBoundary returns a memory-backed epoch implementation based on an // EpochSetup and EpochCommit events, and the epoch's first block height (start boundary). // No errors are expected during normal operations. -func NewEpochWithStartBoundary(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit, firstHeight uint64) protocol.Epoch { +func NewEpochWithStartBoundary(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit, firstHeight uint64) protocol.CommittedEpoch { return &heightBoundedEpoch{ committedEpoch: committedEpoch{ setupEpoch: setupEpoch{ @@ -312,7 +312,7 @@ func NewEpochWithStartBoundary(setupEvent *flow.EpochSetup, extensions []flow.Ep // NewEpochWithEndBoundary returns a memory-backed epoch implementation based on an // EpochSetup and EpochCommit events, and the epoch's final block height (end boundary). // No errors are expected during normal operations. -func NewEpochWithEndBoundary(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit, finalHeight uint64) protocol.Epoch { +func NewEpochWithEndBoundary(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit, finalHeight uint64) protocol.CommittedEpoch { return &heightBoundedEpoch{ committedEpoch: committedEpoch{ setupEpoch: setupEpoch{ @@ -329,7 +329,7 @@ func NewEpochWithEndBoundary(setupEvent *flow.EpochSetup, extensions []flow.Epoc // NewEpochWithStartAndEndBoundaries returns a memory-backed epoch implementation based on an // EpochSetup and EpochCommit events, and the epoch's first and final block heights (start+end boundaries). // No errors are expected during normal operations. -func NewEpochWithStartAndEndBoundaries(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit, firstHeight, finalHeight uint64) protocol.Epoch { +func NewEpochWithStartAndEndBoundaries(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit, firstHeight, finalHeight uint64) protocol.CommittedEpoch { return &heightBoundedEpoch{ committedEpoch: committedEpoch{ setupEpoch: setupEpoch{ diff --git a/state/protocol/invalid/epoch.go b/state/protocol/invalid/epoch.go index 1818c1b94c3..e138147aafe 100644 --- a/state/protocol/invalid/epoch.go +++ b/state/protocol/invalid/epoch.go @@ -114,7 +114,7 @@ func (u *Snapshot) Epochs() protocol.EpochQuery { return &Epochs{err: u.err} } -func (u *Epochs) Current() protocol.Epoch { +func (u *Epochs) Current() protocol.CommittedEpoch { return NewEpoch(u.err) } @@ -122,8 +122,8 @@ func (u *Epochs) NextUnsafe() protocol.TentativeEpoch { return NewEpoch(u.err) } -func (u *Epochs) NextCommitted() protocol.Epoch { return NewEpoch(u.err) } +func (u *Epochs) NextCommitted() protocol.CommittedEpoch { return NewEpoch(u.err) } -func (u *Epochs) Previous() protocol.Epoch { +func (u *Epochs) Previous() protocol.CommittedEpoch { return NewEpoch(u.err) } diff --git a/state/protocol/mock/committed_epoch.go b/state/protocol/mock/committed_epoch.go new file mode 100644 index 00000000000..fcf7b83696c --- /dev/null +++ b/state/protocol/mock/committed_epoch.go @@ -0,0 +1,489 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mock + +import ( + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" + + protocol "github.com/onflow/flow-go/state/protocol" +) + +// CommittedEpoch is an autogenerated mock type for the CommittedEpoch type +type CommittedEpoch struct { + mock.Mock +} + +// Cluster provides a mock function with given fields: index +func (_m *CommittedEpoch) Cluster(index uint) (protocol.Cluster, error) { + ret := _m.Called(index) + + if len(ret) == 0 { + panic("no return value specified for Cluster") + } + + var r0 protocol.Cluster + var r1 error + if rf, ok := ret.Get(0).(func(uint) (protocol.Cluster, error)); ok { + return rf(index) + } + if rf, ok := ret.Get(0).(func(uint) protocol.Cluster); ok { + r0 = rf(index) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(protocol.Cluster) + } + } + + if rf, ok := ret.Get(1).(func(uint) error); ok { + r1 = rf(index) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ClusterByChainID provides a mock function with given fields: chainID +func (_m *CommittedEpoch) ClusterByChainID(chainID flow.ChainID) (protocol.Cluster, error) { + ret := _m.Called(chainID) + + if len(ret) == 0 { + panic("no return value specified for ClusterByChainID") + } + + var r0 protocol.Cluster + var r1 error + if rf, ok := ret.Get(0).(func(flow.ChainID) (protocol.Cluster, error)); ok { + return rf(chainID) + } + if rf, ok := ret.Get(0).(func(flow.ChainID) protocol.Cluster); ok { + r0 = rf(chainID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(protocol.Cluster) + } + } + + if rf, ok := ret.Get(1).(func(flow.ChainID) error); ok { + r1 = rf(chainID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Clustering provides a mock function with given fields: +func (_m *CommittedEpoch) Clustering() (flow.ClusterList, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Clustering") + } + + var r0 flow.ClusterList + var r1 error + if rf, ok := ret.Get(0).(func() (flow.ClusterList, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() flow.ClusterList); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(flow.ClusterList) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Counter provides a mock function with given fields: +func (_m *CommittedEpoch) Counter() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Counter") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DKG provides a mock function with given fields: +func (_m *CommittedEpoch) DKG() (protocol.DKG, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for DKG") + } + + var r0 protocol.DKG + var r1 error + if rf, ok := ret.Get(0).(func() (protocol.DKG, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() protocol.DKG); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(protocol.DKG) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DKGPhase1FinalView provides a mock function with given fields: +func (_m *CommittedEpoch) DKGPhase1FinalView() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for DKGPhase1FinalView") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DKGPhase2FinalView provides a mock function with given fields: +func (_m *CommittedEpoch) DKGPhase2FinalView() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for DKGPhase2FinalView") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DKGPhase3FinalView provides a mock function with given fields: +func (_m *CommittedEpoch) DKGPhase3FinalView() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for DKGPhase3FinalView") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FinalHeight provides a mock function with given fields: +func (_m *CommittedEpoch) FinalHeight() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FinalHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FinalView provides a mock function with given fields: +func (_m *CommittedEpoch) FinalView() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FinalView") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FirstHeight provides a mock function with given fields: +func (_m *CommittedEpoch) FirstHeight() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FirstHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FirstView provides a mock function with given fields: +func (_m *CommittedEpoch) FirstView() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FirstView") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InitialIdentities provides a mock function with given fields: +func (_m *CommittedEpoch) InitialIdentities() (flow.GenericIdentityList[flow.IdentitySkeleton], error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for InitialIdentities") + } + + var r0 flow.GenericIdentityList[flow.IdentitySkeleton] + var r1 error + if rf, ok := ret.Get(0).(func() (flow.GenericIdentityList[flow.IdentitySkeleton], error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() flow.GenericIdentityList[flow.IdentitySkeleton]); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(flow.GenericIdentityList[flow.IdentitySkeleton]) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RandomSource provides a mock function with given fields: +func (_m *CommittedEpoch) RandomSource() ([]byte, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RandomSource") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func() ([]byte, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []byte); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TargetDuration provides a mock function with given fields: +func (_m *CommittedEpoch) TargetDuration() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for TargetDuration") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TargetEndTime provides a mock function with given fields: +func (_m *CommittedEpoch) TargetEndTime() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for TargetEndTime") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewCommittedEpoch creates a new instance of CommittedEpoch. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCommittedEpoch(t interface { + mock.TestingT + Cleanup(func()) +}) *CommittedEpoch { + mock := &CommittedEpoch{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/state/protocol/mock/epoch.go b/state/protocol/mock/epoch.go index 14b79232dff..48a513f7550 100644 --- a/state/protocol/mock/epoch.go +++ b/state/protocol/mock/epoch.go @@ -3,9 +3,10 @@ package mock import ( - flow "github.com/onflow/flow-go/model/flow" mock "github.com/stretchr/testify/mock" + flow "github.com/onflow/flow-go/model/flow" + protocol "github.com/onflow/flow-go/state/protocol" ) diff --git a/state/protocol/mock/epoch_query.go b/state/protocol/mock/epoch_query.go index e9f8513e3f2..f9e8be78372 100644 --- a/state/protocol/mock/epoch_query.go +++ b/state/protocol/mock/epoch_query.go @@ -13,19 +13,19 @@ type EpochQuery struct { } // Current provides a mock function with given fields: -func (_m *EpochQuery) Current() protocol.Epoch { +func (_m *EpochQuery) Current() protocol.CommittedEpoch { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Current") } - var r0 protocol.Epoch - if rf, ok := ret.Get(0).(func() protocol.Epoch); ok { + var r0 protocol.CommittedEpoch + if rf, ok := ret.Get(0).(func() protocol.CommittedEpoch); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(protocol.Epoch) + r0 = ret.Get(0).(protocol.CommittedEpoch) } } @@ -33,19 +33,19 @@ func (_m *EpochQuery) Current() protocol.Epoch { } // NextCommitted provides a mock function with given fields: -func (_m *EpochQuery) NextCommitted() protocol.Epoch { +func (_m *EpochQuery) NextCommitted() protocol.CommittedEpoch { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for NextCommitted") } - var r0 protocol.Epoch - if rf, ok := ret.Get(0).(func() protocol.Epoch); ok { + var r0 protocol.CommittedEpoch + if rf, ok := ret.Get(0).(func() protocol.CommittedEpoch); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(protocol.Epoch) + r0 = ret.Get(0).(protocol.CommittedEpoch) } } @@ -73,19 +73,19 @@ func (_m *EpochQuery) NextUnsafe() protocol.TentativeEpoch { } // Previous provides a mock function with given fields: -func (_m *EpochQuery) Previous() protocol.Epoch { +func (_m *EpochQuery) Previous() protocol.CommittedEpoch { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Previous") } - var r0 protocol.Epoch - if rf, ok := ret.Get(0).(func() protocol.Epoch); ok { + var r0 protocol.CommittedEpoch + if rf, ok := ret.Get(0).(func() protocol.CommittedEpoch); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(protocol.Epoch) + r0 = ret.Get(0).(protocol.CommittedEpoch) } } diff --git a/utils/unittest/mocks/epoch_query.go b/utils/unittest/mocks/epoch_query.go index 5d11dd0e7bf..b6389690d54 100644 --- a/utils/unittest/mocks/epoch_query.go +++ b/utils/unittest/mocks/epoch_query.go @@ -17,15 +17,15 @@ import ( type EpochQuery struct { t *testing.T mu sync.RWMutex - counter uint64 // represents the current epoch - byCounter map[uint64]protocol.Epoch // all epochs + counter uint64 // represents the current epoch + byCounter map[uint64]protocol.CommittedEpoch // all epochs } -func NewEpochQuery(t *testing.T, counter uint64, epochs ...protocol.Epoch) *EpochQuery { +func NewEpochQuery(t *testing.T, counter uint64, epochs ...protocol.CommittedEpoch) *EpochQuery { mock := &EpochQuery{ t: t, counter: counter, - byCounter: make(map[uint64]protocol.Epoch), + byCounter: make(map[uint64]protocol.CommittedEpoch), } for _, epoch := range epochs { @@ -35,7 +35,7 @@ func NewEpochQuery(t *testing.T, counter uint64, epochs ...protocol.Epoch) *Epoc return mock } -func (mock *EpochQuery) Current() protocol.Epoch { +func (mock *EpochQuery) Current() protocol.CommittedEpoch { mock.mu.RLock() defer mock.mu.RUnlock() return mock.byCounter[mock.counter] @@ -51,7 +51,7 @@ func (mock *EpochQuery) NextUnsafe() protocol.TentativeEpoch { return epoch } -func (mock *EpochQuery) NextCommitted() protocol.Epoch { +func (mock *EpochQuery) NextCommitted() protocol.CommittedEpoch { mock.mu.RLock() defer mock.mu.RUnlock() epoch, exists := mock.byCounter[mock.counter+1] @@ -61,7 +61,7 @@ func (mock *EpochQuery) NextCommitted() protocol.Epoch { return epoch } -func (mock *EpochQuery) Previous() protocol.Epoch { +func (mock *EpochQuery) Previous() protocol.CommittedEpoch { mock.mu.RLock() defer mock.mu.RUnlock() epoch, exists := mock.byCounter[mock.counter-1] @@ -82,7 +82,7 @@ func (mock *EpochQuery) Phase() flow.EpochPhase { return flow.EpochPhaseStaking } -func (mock *EpochQuery) ByCounter(counter uint64) protocol.Epoch { +func (mock *EpochQuery) ByCounter(counter uint64) protocol.CommittedEpoch { mock.mu.RLock() defer mock.mu.RUnlock() return mock.byCounter[counter] @@ -94,7 +94,7 @@ func (mock *EpochQuery) Transition() { mock.counter++ } -func (mock *EpochQuery) Add(epoch protocol.Epoch) { +func (mock *EpochQuery) Add(epoch protocol.CommittedEpoch) { mock.mu.Lock() defer mock.mu.Unlock() counter, err := epoch.Counter() From aadf86235bbc40cbec35a72c1545adaddacdc55a Mon Sep 17 00:00:00 2001 From: Tim Barry Date: Tue, 28 Jan 2025 13:54:28 -0800 Subject: [PATCH 6/7] update TestExtendConflictingEpochEvents Use conflicting Clustering/collector assignments. When simply setting a different list of participants without going through a staking or unstaking process, Epoch Fallback mode was often but not always triggered. --- state/protocol/badger/mutator_test.go | 27 ++++++++++++++++++--------- utils/unittest/fixtures.go | 6 +++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/state/protocol/badger/mutator_test.go b/state/protocol/badger/mutator_test.go index 4de4cd6ce27..b85c3ebd5cb 100644 --- a/state/protocol/badger/mutator_test.go +++ b/state/protocol/badger/mutator_test.go @@ -1131,7 +1131,11 @@ func TestExtendEpochTransitionValid(t *testing.T) { // ROOT <--+ // \--B2<--B4(R2)<--B6(S2)<--B8 func TestExtendConflictingEpochEvents(t *testing.T) { - rootSnapshot := unittest.RootSnapshotFixture(participants) + // add more collectors so that we can have multiple distinct clustering assignments + extraCollectors := unittest.IdentityListFixture(2, func(identity *flow.Identity) { + identity.Role = flow.RoleCollection + }) + rootSnapshot := unittest.RootSnapshotFixture(append(participants, extraCollectors...)) rootProtocolStateID := getRootProtocolStateID(t, rootSnapshot) util.RunWithFullProtocolStateAndMutator(t, rootSnapshot, func(db *badger.DB, state *protocol.ParticipantState, mutableState realprotocol.MutableProtocolState) { expectedStateIdCalculator := calculateExpectedStateId(t, mutableState) @@ -1154,19 +1158,22 @@ func TestExtendConflictingEpochEvents(t *testing.T) { rootSetup := result.ServiceEvents[0].Event.(*flow.EpochSetup) - // create two conflicting epoch setup events for the next epoch (participants differ) + // create two conflicting epoch setup events for the next epoch (clustering differs) nextEpochSetup1 := unittest.EpochSetupFixture( - unittest.WithParticipants(rootSetup.Participants[:len(rootSetup.Participants)]), + unittest.WithParticipants(rootSetup.Participants), unittest.SetupWithCounter(rootSetup.Counter+1), unittest.WithFinalView(rootSetup.FinalView+1000), unittest.WithFirstView(rootSetup.FinalView+1), ) + nextEpochSetup1.Assignments = unittest.ClusterAssignment(1, rootSetup.Participants) nextEpochSetup2 := unittest.EpochSetupFixture( - unittest.WithParticipants(rootSetup.Participants[:len(rootSetup.Participants)-1]), + unittest.WithParticipants(rootSetup.Participants), unittest.SetupWithCounter(rootSetup.Counter+1), - unittest.WithFinalView(rootSetup.FinalView+2000), // final view differs + unittest.WithFinalView(rootSetup.FinalView+1000), unittest.WithFirstView(rootSetup.FinalView+1), ) + nextEpochSetup2.Assignments = unittest.ClusterAssignment(2, rootSetup.Participants) + assert.NotEqual(t, nextEpochSetup1.Assignments, nextEpochSetup2.Assignments) // add blocks containing receipts for block1 and block2 (necessary for sealing) // block 1 receipt contains nextEpochSetup1 @@ -1232,17 +1239,19 @@ func TestExtendConflictingEpochEvents(t *testing.T) { require.NoError(t, err) // should be able to query each epoch from the appropriate reference block - setup1identities, err := state.AtBlockID(block7.ID()).Epochs().NextUnsafe().InitialIdentities() + nextEpoch1 := state.AtBlockID(block7.ID()).Epochs().NextUnsafe() + setup1clustering, err := nextEpoch1.Clustering() assert.NoError(t, err) - require.Equal(t, nextEpochSetup1.Participants, setup1identities) + require.Equal(t, nextEpochSetup1.Assignments, setup1clustering.Assignments()) phase, err := state.AtBlockID(block8.ID()).EpochPhase() assert.NoError(t, err) switch phase { case flow.EpochPhaseSetup: - setup2identities, err := state.AtBlockID(block8.ID()).Epochs().NextUnsafe().InitialIdentities() + nextEpoch2 := state.AtBlockID(block8.ID()).Epochs().NextUnsafe() + setup2clustering, err := nextEpoch2.Clustering() assert.NoError(t, err) - require.Equal(t, nextEpochSetup2.Participants, setup2identities) + require.Equal(t, nextEpochSetup2.Assignments, setup2clustering.Assignments()) case flow.EpochPhaseFallback: t.Fatal("reached epoch fallback phase instead of epoch setup phase") default: diff --git a/utils/unittest/fixtures.go b/utils/unittest/fixtures.go index de86955d25d..42f71b9f509 100644 --- a/utils/unittest/fixtures.go +++ b/utils/unittest/fixtures.go @@ -1237,14 +1237,14 @@ func WithRandomPublicKeys() func(*flow.Identity) { } } -// WithAllRoles can be used used to ensure an IdentityList fixtures contains +// WithAllRoles can be used to ensure an IdentityList fixtures contains // all the roles required for a valid genesis block. func WithAllRoles() func(*flow.Identity) { return WithAllRolesExcept() } -// Same as above, but omitting a certain role for cases where we are manually -// setting up nodes or a particular role. +// WithAllRolesExcept is used to ensure an IdentityList fixture contains all roles +// except omitting a certain role, for cases where we are manually setting up nodes. func WithAllRolesExcept(except ...flow.Role) func(*flow.Identity) { i := 0 roles := flow.Roles() From 9df7a53c69c86d59dcdda80407147e397ae74e29 Mon Sep 17 00:00:00 2001 From: Tim Barry <21149133+tim-barry@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:55:32 -0800 Subject: [PATCH 7/7] update comments and TentativeEpoch doc comment Co-authored-by: Jordan Schalm --- consensus/hotstuff/committees/leader/consensus.go | 3 ++- engine/consensus/dkg/reactor_engine.go | 4 +++- state/protocol/badger/mutator_test.go | 6 +++--- state/protocol/epoch.go | 6 ++++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/consensus/hotstuff/committees/leader/consensus.go b/consensus/hotstuff/committees/leader/consensus.go index 14ae310a5f9..c2fae1b4f74 100644 --- a/consensus/hotstuff/committees/leader/consensus.go +++ b/consensus/hotstuff/committees/leader/consensus.go @@ -9,7 +9,8 @@ import ( "github.com/onflow/flow-go/state/protocol/prg" ) -// SelectionForConsensusFromEpoch is a ... +// SelectionForConsensusFromEpoch returns the leader selection for the input epoch. +// See [SelectionForConsensus] for additional details. func SelectionForConsensusFromEpoch(epoch protocol.CommittedEpoch) (*LeaderSelection, error) { identities, err := epoch.InitialIdentities() diff --git a/engine/consensus/dkg/reactor_engine.go b/engine/consensus/dkg/reactor_engine.go index b43992f6c20..37f4ed4b347 100644 --- a/engine/consensus/dkg/reactor_engine.go +++ b/engine/consensus/dkg/reactor_engine.go @@ -335,7 +335,9 @@ func (e *ReactorEngine) handleEpochCommittedPhaseStarted(currentEpochCounter uin log.Info().Msgf("successfully ended DKG, my beacon pub key for epoch %d is %s", nextEpochCounter, localPubKey) } -// TODO document error returns +// getDKGInfo returns the information required to initiate the DKG for the current epoch. +// firstBlockID must be the first block of the EpochSetup phase. +// No errors are expected during normal operation. func (e *ReactorEngine) getDKGInfo(firstBlockID flow.Identifier) (*dkgInfo, error) { currEpoch := e.State.AtBlockID(firstBlockID).Epochs().Current() nextEpoch := e.State.AtBlockID(firstBlockID).Epochs().NextUnsafe() diff --git a/state/protocol/badger/mutator_test.go b/state/protocol/badger/mutator_test.go index b85c3ebd5cb..d1a787549d0 100644 --- a/state/protocol/badger/mutator_test.go +++ b/state/protocol/badger/mutator_test.go @@ -963,13 +963,13 @@ func TestExtendEpochTransitionValid(t *testing.T) { require.Error(t, err) } - // we should be able to query epoch 2 wrt block 3 + // we should be able to query epoch 2 as a TentativeEpoch wrt block 3 _, err = state.AtBlockID(block3.ID()).Epochs().NextUnsafe().InitialIdentities() assert.NoError(t, err) _, err = state.AtBlockID(block3.ID()).Epochs().NextUnsafe().Clustering() assert.NoError(t, err) - // only setup event is finalized, not commit, so shouldn't be able to get certain info + // only setup event is finalized, not commit, so shouldn't be able to read a CommittedEpoch _, err = state.AtBlockID(block3.ID()).Epochs().NextCommitted().DKG() require.Error(t, err) @@ -1032,7 +1032,7 @@ func TestExtendEpochTransitionValid(t *testing.T) { require.Error(t, err) } - // now epoch 2 is fully ready, we can query anything we want about it wrt block 6 (or later) + // now epoch 2 is committed, we can query anything we want about it wrt block 6 (or later) _, err = state.AtBlockID(block6.ID()).Epochs().NextCommitted().InitialIdentities() require.NoError(t, err) _, err = state.AtBlockID(block6.ID()).Epochs().NextCommitted().Clustering() diff --git a/state/protocol/epoch.go b/state/protocol/epoch.go index adbd7d994fe..6f3585c9dce 100644 --- a/state/protocol/epoch.go +++ b/state/protocol/epoch.go @@ -210,6 +210,12 @@ type CommittedEpoch interface { FinalHeight() (uint64, error) } +// TentativeEpoch returns the data associated with the "working next epoch", +// the upcoming epoch which the protocol is in the process of committing. +// Only the data that is strictly necessary for committing the epoch is exposed; +// after commitment, all epoch data is accessible through the [CommittedEpoch] interface. +// This should only be used by components that participate in committing the epoch +// (transition from [flow.EpochPhaseSetup] to [flow.EpochPhaseCommitted]). type TentativeEpoch interface { // Counter returns the Epoch's counter.