github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/state/protocol/protocol_state/epochs/statemachine_test.go (about)

     1  package epochs_test
     2  
     3  import (
     4  	"errors"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/assert"
     8  	mocks "github.com/stretchr/testify/mock"
     9  	"github.com/stretchr/testify/require"
    10  	"github.com/stretchr/testify/suite"
    11  
    12  	"github.com/onflow/flow-go/model/flow"
    13  	"github.com/onflow/flow-go/module/irrecoverable"
    14  	"github.com/onflow/flow-go/state/protocol"
    15  	protocolmock "github.com/onflow/flow-go/state/protocol/mock"
    16  	"github.com/onflow/flow-go/state/protocol/protocol_state/epochs"
    17  	"github.com/onflow/flow-go/state/protocol/protocol_state/epochs/mock"
    18  	protocol_statemock "github.com/onflow/flow-go/state/protocol/protocol_state/mock"
    19  	"github.com/onflow/flow-go/storage/badger/transaction"
    20  	storagemock "github.com/onflow/flow-go/storage/mock"
    21  	"github.com/onflow/flow-go/utils/unittest"
    22  )
    23  
    24  func TestEpochStateMachine(t *testing.T) {
    25  	suite.Run(t, new(EpochStateMachineSuite))
    26  }
    27  
    28  // EpochStateMachineSuite is a dedicated test suite for testing hierarchical epoch state machine.
    29  // All needed dependencies are mocked, including KV store as a whole, and all the necessary storages.
    30  // Tests in this suite are designed to rely on automatic assertions when leaving the scope of the test.
    31  type EpochStateMachineSuite struct {
    32  	suite.Suite
    33  	epochStateDB                    *storagemock.ProtocolState
    34  	setupsDB                        *storagemock.EpochSetups
    35  	commitsDB                       *storagemock.EpochCommits
    36  	globalParams                    *protocolmock.GlobalParams
    37  	parentState                     *protocolmock.KVStoreReader
    38  	parentEpochState                *flow.RichProtocolStateEntry
    39  	mutator                         *protocol_statemock.KVStoreMutator
    40  	happyPathStateMachine           *mock.StateMachine
    41  	happyPathStateMachineFactory    *mock.StateMachineFactoryMethod
    42  	fallbackPathStateMachineFactory *mock.StateMachineFactoryMethod
    43  	candidate                       *flow.Header
    44  
    45  	stateMachine *epochs.EpochStateMachine
    46  }
    47  
    48  func (s *EpochStateMachineSuite) SetupTest() {
    49  	s.epochStateDB = storagemock.NewProtocolState(s.T())
    50  	s.setupsDB = storagemock.NewEpochSetups(s.T())
    51  	s.commitsDB = storagemock.NewEpochCommits(s.T())
    52  	s.globalParams = protocolmock.NewGlobalParams(s.T())
    53  	s.globalParams.On("EpochCommitSafetyThreshold").Return(uint64(1_000))
    54  	s.parentState = protocolmock.NewKVStoreReader(s.T())
    55  	s.parentEpochState = unittest.EpochStateFixture()
    56  	s.mutator = protocol_statemock.NewKVStoreMutator(s.T())
    57  	s.candidate = unittest.BlockHeaderFixture(unittest.HeaderWithView(s.parentEpochState.CurrentEpochSetup.FirstView + 1))
    58  	s.happyPathStateMachine = mock.NewStateMachine(s.T())
    59  	s.happyPathStateMachineFactory = mock.NewStateMachineFactoryMethod(s.T())
    60  	s.fallbackPathStateMachineFactory = mock.NewStateMachineFactoryMethod(s.T())
    61  
    62  	s.epochStateDB.On("ByBlockID", mocks.Anything).Return(func(_ flow.Identifier) *flow.RichProtocolStateEntry {
    63  		return s.parentEpochState
    64  	}, func(_ flow.Identifier) error {
    65  		return nil
    66  	})
    67  	s.parentState.On("GetEpochStateID").Return(func() flow.Identifier {
    68  		return s.parentEpochState.ID()
    69  	})
    70  
    71  	s.happyPathStateMachineFactory.On("Execute", s.candidate.View, s.parentEpochState).
    72  		Return(s.happyPathStateMachine, nil).Once()
    73  
    74  	s.happyPathStateMachine.On("ParentState").Return(s.parentEpochState).Maybe()
    75  
    76  	var err error
    77  	s.stateMachine, err = epochs.NewEpochStateMachine(
    78  		s.candidate.View,
    79  		s.candidate.ParentID,
    80  		s.globalParams,
    81  		s.setupsDB,
    82  		s.commitsDB,
    83  		s.epochStateDB,
    84  		s.parentState,
    85  		s.mutator,
    86  		s.happyPathStateMachineFactory.Execute,
    87  		s.fallbackPathStateMachineFactory.Execute,
    88  	)
    89  	require.NoError(s.T(), err)
    90  }
    91  
    92  // TestBuild_NoChanges tests that hierarchical epoch state machine maintains index of epoch states and commits
    93  // epoch state ID in the KV store even when there were no events to process.
    94  func (s *EpochStateMachineSuite) TestBuild_NoChanges() {
    95  	s.happyPathStateMachine.On("ParentState").Return(s.parentEpochState)
    96  	s.happyPathStateMachine.On("Build").Return(s.parentEpochState.ProtocolStateEntry, s.parentEpochState.ID(), false).Once()
    97  
    98  	err := s.stateMachine.EvolveState(nil)
    99  	require.NoError(s.T(), err)
   100  
   101  	indexTxDeferredUpdate := storagemock.NewDeferredDBUpdate(s.T())
   102  	indexTxDeferredUpdate.On("Execute", mocks.Anything).Return(nil).Once()
   103  
   104  	s.epochStateDB.On("Index", s.candidate.ID(), s.parentEpochState.ID()).Return(indexTxDeferredUpdate.Execute, nil).Once()
   105  	s.mutator.On("SetEpochStateID", s.parentEpochState.ID()).Return(nil).Once()
   106  
   107  	dbUpdates, err := s.stateMachine.Build()
   108  	require.NoError(s.T(), err)
   109  	// Provide the blockID and execute the resulting `DeferredDBUpdate`. Thereby,
   110  	// the expected mock methods should be called, which is asserted by the testify framework
   111  	err = dbUpdates.Pending().WithBlock(s.candidate.ID())(&transaction.Tx{})
   112  	require.NoError(s.T(), err)
   113  }
   114  
   115  // TestBuild_HappyPath tests that hierarchical epoch state machine maintains index of epoch states and commits
   116  // as well as stores updated epoch state in respective storage when there were updates made to the epoch state.
   117  // This test also ensures that updated state ID is committed in the KV store.
   118  func (s *EpochStateMachineSuite) TestBuild_HappyPath() {
   119  	s.happyPathStateMachine.On("ParentState").Return(s.parentEpochState)
   120  	updatedState := unittest.EpochStateFixture().ProtocolStateEntry
   121  	updatedStateID := updatedState.ID()
   122  	s.happyPathStateMachine.On("Build").Return(updatedState, updatedStateID, true).Once()
   123  
   124  	epochSetup := unittest.EpochSetupFixture()
   125  	epochCommit := unittest.EpochCommitFixture()
   126  
   127  	// expected both events to be processed
   128  	s.happyPathStateMachine.On("ProcessEpochSetup", epochSetup).Return(true, nil).Once()
   129  	s.happyPathStateMachine.On("ProcessEpochCommit", epochCommit).Return(true, nil).Once()
   130  
   131  	// prepare a DB update for epoch setup
   132  	storeEpochSetupTx := storagemock.NewDeferredDBUpdate(s.T())
   133  	storeEpochSetupTx.On("Execute", mocks.Anything).Return(nil).Once()
   134  	s.setupsDB.On("StoreTx", epochSetup).Return(storeEpochSetupTx.Execute, nil).Once()
   135  
   136  	// prepare a DB update for epoch commit
   137  	storeEpochCommitTx := storagemock.NewDeferredDBUpdate(s.T())
   138  	storeEpochCommitTx.On("Execute", mocks.Anything).Return(nil).Once()
   139  	s.commitsDB.On("StoreTx", epochCommit).Return(storeEpochCommitTx.Execute, nil).Once()
   140  
   141  	err := s.stateMachine.EvolveState([]flow.ServiceEvent{epochSetup.ServiceEvent(), epochCommit.ServiceEvent()})
   142  	require.NoError(s.T(), err)
   143  
   144  	// prepare a DB update for epoch state
   145  	indexTxDeferredUpdate := storagemock.NewDeferredDBUpdate(s.T())
   146  	indexTxDeferredUpdate.On("Execute", mocks.Anything).Return(nil).Once()
   147  	storeTxDeferredUpdate := storagemock.NewDeferredDBUpdate(s.T())
   148  	storeTxDeferredUpdate.On("Execute", mocks.Anything).Return(nil).Once()
   149  
   150  	s.epochStateDB.On("Index", s.candidate.ID(), updatedStateID).Return(indexTxDeferredUpdate.Execute, nil).Once()
   151  	s.epochStateDB.On("StoreTx", updatedStateID, updatedState).Return(storeTxDeferredUpdate.Execute, nil).Once()
   152  	s.mutator.On("SetEpochStateID", updatedStateID).Return(nil).Once()
   153  
   154  	dbUpdates, err := s.stateMachine.Build()
   155  	require.NoError(s.T(), err)
   156  	// Provide the blockID and execute the resulting `DeferredDBUpdate`. Thereby,
   157  	// the expected mock methods should be called, which is asserted by the testify framework
   158  	err = dbUpdates.Pending().WithBlock(s.candidate.ID())(&transaction.Tx{})
   159  	require.NoError(s.T(), err)
   160  }
   161  
   162  // TestEpochStateMachine_Constructor tests the behavior of the EpochStateMachine constructor.
   163  // Specifically, we test the scenario, where the EpochCommit Service Event is still missing
   164  // by the time we cross the `EpochCommitSafetyThreshold`. We expect the constructor to select the
   165  // appropriate internal state machine constructor (HappyPathStateMachine before the threshold
   166  // and FallbackStateMachine when reaching or exceeding the view threshold).
   167  // Any exceptions encountered when constructing the internal state machines should be passed up.
   168  func (s *EpochStateMachineSuite) TestEpochStateMachine_Constructor() {
   169  	s.Run("EpochStaking phase", func() {
   170  		// Since we are before the epoch commitment deadline, we should instantiate a happy-path state machine
   171  		s.Run("before commitment deadline", func() {
   172  			happyPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   173  			// expect to be called
   174  			happyPathStateMachineFactory.On("Execute", s.candidate.View, s.parentEpochState).
   175  				Return(s.happyPathStateMachine, nil).Once()
   176  			// don't expect to be called
   177  			fallbackPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   178  
   179  			candidate := unittest.BlockHeaderFixture(unittest.HeaderWithView(s.parentEpochState.CurrentEpochSetup.FirstView + 1))
   180  			stateMachine, err := epochs.NewEpochStateMachine(
   181  				candidate.View,
   182  				candidate.ParentID,
   183  				s.globalParams,
   184  				s.setupsDB,
   185  				s.commitsDB,
   186  				s.epochStateDB,
   187  				s.parentState,
   188  				s.mutator,
   189  				happyPathStateMachineFactory.Execute,
   190  				fallbackPathStateMachineFactory.Execute,
   191  			)
   192  			require.NoError(s.T(), err)
   193  			assert.NotNil(s.T(), stateMachine)
   194  		})
   195  		// Since we are past the epoch commitment deadline, and have not entered the EpochCommitted
   196  		// phase, we should use the epoch fallback state machine.
   197  		s.Run("past commitment deadline", func() {
   198  			// don't expect to be called
   199  			happyPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   200  			// expect to be called
   201  			fallbackPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   202  
   203  			candidate := unittest.BlockHeaderFixture(unittest.HeaderWithView(s.parentEpochState.CurrentEpochSetup.FinalView - 1))
   204  			fallbackPathStateMachineFactory.On("Execute", candidate.View, s.parentEpochState).
   205  				Return(s.happyPathStateMachine, nil).Once()
   206  			stateMachine, err := epochs.NewEpochStateMachine(
   207  				candidate.View,
   208  				candidate.ParentID,
   209  				s.globalParams,
   210  				s.setupsDB,
   211  				s.commitsDB,
   212  				s.epochStateDB,
   213  				s.parentState,
   214  				s.mutator,
   215  				happyPathStateMachineFactory.Execute,
   216  				fallbackPathStateMachineFactory.Execute,
   217  			)
   218  			require.NoError(s.T(), err)
   219  			assert.NotNil(s.T(), stateMachine)
   220  		})
   221  	})
   222  
   223  	s.Run("EpochSetup phase", func() {
   224  		s.parentEpochState = unittest.EpochStateFixture(unittest.WithNextEpochProtocolState())
   225  		s.parentEpochState.NextEpochCommit = nil
   226  		s.parentEpochState.NextEpoch.CommitID = flow.ZeroID
   227  
   228  		// Since we are before the epoch commitment deadline, we should instantiate a happy-path state machine
   229  		s.Run("before commitment deadline", func() {
   230  			happyPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   231  			// don't expect to be called
   232  			fallbackPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   233  
   234  			candidate := unittest.BlockHeaderFixture(unittest.HeaderWithView(s.parentEpochState.CurrentEpochSetup.FirstView + 1))
   235  			// expect to be called
   236  			happyPathStateMachineFactory.On("Execute", candidate.View, s.parentEpochState).
   237  				Return(s.happyPathStateMachine, nil).Once()
   238  			stateMachine, err := epochs.NewEpochStateMachine(
   239  				candidate.View,
   240  				candidate.ParentID,
   241  				s.globalParams,
   242  				s.setupsDB,
   243  				s.commitsDB,
   244  				s.epochStateDB,
   245  				s.parentState,
   246  				s.mutator,
   247  				happyPathStateMachineFactory.Execute,
   248  				fallbackPathStateMachineFactory.Execute,
   249  			)
   250  			require.NoError(s.T(), err)
   251  			assert.NotNil(s.T(), stateMachine)
   252  		})
   253  		// Since we are past the epoch commitment deadline, and have not entered the EpochCommitted
   254  		// phase, we should use the epoch fallback state machine.
   255  		s.Run("past commitment deadline", func() {
   256  			// don't expect to be called
   257  			happyPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   258  			fallbackPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   259  
   260  			candidate := unittest.BlockHeaderFixture(unittest.HeaderWithView(s.parentEpochState.CurrentEpochSetup.FinalView - 1))
   261  			// expect to be called
   262  			fallbackPathStateMachineFactory.On("Execute", candidate.View, s.parentEpochState).
   263  				Return(s.happyPathStateMachine, nil).Once()
   264  			stateMachine, err := epochs.NewEpochStateMachine(
   265  				candidate.View,
   266  				candidate.ParentID,
   267  				s.globalParams,
   268  				s.setupsDB,
   269  				s.commitsDB,
   270  				s.epochStateDB,
   271  				s.parentState,
   272  				s.mutator,
   273  				happyPathStateMachineFactory.Execute,
   274  				fallbackPathStateMachineFactory.Execute,
   275  			)
   276  			require.NoError(s.T(), err)
   277  			assert.NotNil(s.T(), stateMachine)
   278  		})
   279  	})
   280  
   281  	s.Run("EpochCommitted phase", func() {
   282  		s.parentEpochState = unittest.EpochStateFixture(unittest.WithNextEpochProtocolState())
   283  		// Since we are before the epoch commitment deadline, we should instantiate a happy-path state machine
   284  		s.Run("before commitment deadline", func() {
   285  			happyPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   286  			// expect to be called
   287  			happyPathStateMachineFactory.On("Execute", s.candidate.View, s.parentEpochState).
   288  				Return(s.happyPathStateMachine, nil).Once()
   289  			// don't expect to be called
   290  			fallbackPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   291  
   292  			candidate := unittest.BlockHeaderFixture(unittest.HeaderWithView(s.parentEpochState.CurrentEpochSetup.FirstView + 1))
   293  			stateMachine, err := epochs.NewEpochStateMachine(
   294  				candidate.View,
   295  				candidate.ParentID,
   296  				s.globalParams,
   297  				s.setupsDB,
   298  				s.commitsDB,
   299  				s.epochStateDB,
   300  				s.parentState,
   301  				s.mutator,
   302  				happyPathStateMachineFactory.Execute,
   303  				fallbackPathStateMachineFactory.Execute,
   304  			)
   305  			require.NoError(s.T(), err)
   306  			assert.NotNil(s.T(), stateMachine)
   307  		})
   308  		// Despite being past the epoch commitment deadline, since we are in the EpochCommitted phase
   309  		// already, we should proceed with the happy-path state machine
   310  		s.Run("past commitment deadline", func() {
   311  			happyPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   312  			// don't expect to be called
   313  			fallbackPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   314  
   315  			candidate := unittest.BlockHeaderFixture(unittest.HeaderWithView(s.parentEpochState.CurrentEpochSetup.FinalView - 1))
   316  			// expect to be called
   317  			happyPathStateMachineFactory.On("Execute", candidate.View, s.parentEpochState).
   318  				Return(s.happyPathStateMachine, nil).Once()
   319  			stateMachine, err := epochs.NewEpochStateMachine(
   320  				candidate.View,
   321  				candidate.ParentID,
   322  				s.globalParams,
   323  				s.setupsDB,
   324  				s.commitsDB,
   325  				s.epochStateDB,
   326  				s.parentState,
   327  				s.mutator,
   328  				happyPathStateMachineFactory.Execute,
   329  				fallbackPathStateMachineFactory.Execute,
   330  			)
   331  			require.NoError(s.T(), err)
   332  			assert.NotNil(s.T(), stateMachine)
   333  		})
   334  	})
   335  
   336  	// if a state machine constructor returns an error, the stateMutator constructor should fail
   337  	// and propagate the error to the caller
   338  	s.Run("state machine constructor returns error", func() {
   339  		s.Run("happy-path", func() {
   340  			exception := irrecoverable.NewExceptionf("exception")
   341  			happyPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   342  			happyPathStateMachineFactory.On("Execute", s.candidate.View, s.parentEpochState).Return(nil, exception).Once()
   343  			fallbackPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   344  
   345  			stateMachine, err := epochs.NewEpochStateMachine(
   346  				s.candidate.View,
   347  				s.candidate.ParentID,
   348  				s.globalParams,
   349  				s.setupsDB,
   350  				s.commitsDB,
   351  				s.epochStateDB,
   352  				s.parentState,
   353  				s.mutator,
   354  				happyPathStateMachineFactory.Execute,
   355  				fallbackPathStateMachineFactory.Execute,
   356  			)
   357  			assert.ErrorIs(s.T(), err, exception)
   358  			assert.Nil(s.T(), stateMachine)
   359  		})
   360  		s.Run("epoch-fallback", func() {
   361  			s.parentEpochState.InvalidEpochTransitionAttempted = true // ensure we use epoch-fallback state machine
   362  			exception := irrecoverable.NewExceptionf("exception")
   363  			happyPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   364  			fallbackPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   365  			fallbackPathStateMachineFactory.On("Execute", s.candidate.View, s.parentEpochState).Return(nil, exception).Once()
   366  
   367  			stateMachine, err := epochs.NewEpochStateMachine(
   368  				s.candidate.View,
   369  				s.candidate.ParentID,
   370  				s.globalParams,
   371  				s.setupsDB,
   372  				s.commitsDB,
   373  				s.epochStateDB,
   374  				s.parentState,
   375  				s.mutator,
   376  				happyPathStateMachineFactory.Execute,
   377  				fallbackPathStateMachineFactory.Execute,
   378  			)
   379  			assert.ErrorIs(s.T(), err, exception)
   380  			assert.Nil(s.T(), stateMachine)
   381  		})
   382  	})
   383  }
   384  
   385  // TestEvolveState_InvalidEpochSetup tests that hierarchical state machine rejects invalid epoch setup events
   386  // (indicated by `InvalidServiceEventError` sentinel error) and replaces the happy path state machine with the
   387  // fallback state machine. Errors other than `InvalidServiceEventError` should be bubbled up as exceptions.
   388  func (s *EpochStateMachineSuite) TestEvolveState_InvalidEpochSetup() {
   389  	s.Run("invalid-epoch-setup", func() {
   390  		happyPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   391  		happyPathStateMachineFactory.On("Execute", s.candidate.View, s.parentEpochState).Return(s.happyPathStateMachine, nil).Once()
   392  		fallbackPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   393  		stateMachine, err := epochs.NewEpochStateMachine(
   394  			s.candidate.View,
   395  			s.candidate.ParentID,
   396  			s.globalParams,
   397  			s.setupsDB,
   398  			s.commitsDB,
   399  			s.epochStateDB,
   400  			s.parentState,
   401  			s.mutator,
   402  			happyPathStateMachineFactory.Execute,
   403  			fallbackPathStateMachineFactory.Execute,
   404  		)
   405  		require.NoError(s.T(), err)
   406  
   407  		epochSetup := unittest.EpochSetupFixture()
   408  
   409  		s.happyPathStateMachine.On("ParentState").Return(s.parentEpochState)
   410  		s.happyPathStateMachine.On("ProcessEpochSetup", epochSetup).
   411  			Return(false, protocol.NewInvalidServiceEventErrorf("")).Once()
   412  
   413  		fallbackStateMachine := mock.NewStateMachine(s.T())
   414  		fallbackStateMachine.On("ProcessEpochSetup", epochSetup).Return(false, nil).Once()
   415  		fallbackPathStateMachineFactory.On("Execute", s.candidate.View, s.parentEpochState).Return(fallbackStateMachine, nil).Once()
   416  
   417  		err = stateMachine.EvolveState([]flow.ServiceEvent{epochSetup.ServiceEvent()})
   418  		require.NoError(s.T(), err)
   419  	})
   420  	s.Run("process-epoch-setup-exception", func() {
   421  		epochSetup := unittest.EpochSetupFixture()
   422  
   423  		exception := errors.New("exception")
   424  		s.happyPathStateMachine.On("ProcessEpochSetup", epochSetup).Return(false, exception).Once()
   425  
   426  		err := s.stateMachine.EvolveState([]flow.ServiceEvent{epochSetup.ServiceEvent()})
   427  		require.Error(s.T(), err)
   428  		require.False(s.T(), protocol.IsInvalidServiceEventError(err))
   429  	})
   430  }
   431  
   432  // TestEvolveState_InvalidEpochCommit tests that hierarchical state machine rejects invalid epoch commit events
   433  // (indicated by `InvalidServiceEventError` sentinel error) and replaces the happy path state machine with the
   434  // fallback state machine. Errors other than `InvalidServiceEventError` should be bubbled up as exceptions.
   435  func (s *EpochStateMachineSuite) TestEvolveState_InvalidEpochCommit() {
   436  	s.Run("invalid-epoch-commit", func() {
   437  		happyPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   438  		happyPathStateMachineFactory.On("Execute", s.candidate.View, s.parentEpochState).Return(s.happyPathStateMachine, nil).Once()
   439  		fallbackPathStateMachineFactory := mock.NewStateMachineFactoryMethod(s.T())
   440  		stateMachine, err := epochs.NewEpochStateMachine(
   441  			s.candidate.View,
   442  			s.candidate.ParentID,
   443  			s.globalParams,
   444  			s.setupsDB,
   445  			s.commitsDB,
   446  			s.epochStateDB,
   447  			s.parentState,
   448  			s.mutator,
   449  			happyPathStateMachineFactory.Execute,
   450  			fallbackPathStateMachineFactory.Execute,
   451  		)
   452  		require.NoError(s.T(), err)
   453  
   454  		epochCommit := unittest.EpochCommitFixture()
   455  
   456  		s.happyPathStateMachine.On("ParentState").Return(s.parentEpochState)
   457  		s.happyPathStateMachine.On("ProcessEpochCommit", epochCommit).
   458  			Return(false, protocol.NewInvalidServiceEventErrorf("")).Once()
   459  
   460  		fallbackStateMachine := mock.NewStateMachine(s.T())
   461  		fallbackStateMachine.On("ProcessEpochCommit", epochCommit).Return(false, nil).Once()
   462  		fallbackPathStateMachineFactory.On("Execute", s.candidate.View, s.parentEpochState).Return(fallbackStateMachine, nil).Once()
   463  
   464  		err = stateMachine.EvolveState([]flow.ServiceEvent{epochCommit.ServiceEvent()})
   465  		require.NoError(s.T(), err)
   466  	})
   467  	s.Run("process-epoch-commit-exception", func() {
   468  		epochCommit := unittest.EpochCommitFixture()
   469  
   470  		exception := errors.New("exception")
   471  		s.happyPathStateMachine.On("ProcessEpochCommit", epochCommit).Return(false, exception).Once()
   472  
   473  		err := s.stateMachine.EvolveState([]flow.ServiceEvent{epochCommit.ServiceEvent()})
   474  		require.Error(s.T(), err)
   475  		require.False(s.T(), protocol.IsInvalidServiceEventError(err))
   476  	})
   477  }
   478  
   479  // TestEvolveStateTransitionToNextEpoch tests that EpochStateMachine transitions to the next epoch
   480  // when the epoch has been committed, and we are at the first block of the next epoch.
   481  func (s *EpochStateMachineSuite) TestEvolveStateTransitionToNextEpoch() {
   482  	parentState := unittest.EpochStateFixture(unittest.WithNextEpochProtocolState())
   483  	s.happyPathStateMachine.On("ParentState").Unset()
   484  	s.happyPathStateMachine.On("ParentState").Return(parentState)
   485  	// we are at the first block of the next epoch
   486  	s.happyPathStateMachine.On("View").Return(parentState.CurrentEpochSetup.FinalView + 1)
   487  	s.happyPathStateMachine.On("TransitionToNextEpoch").Return(nil).Once()
   488  	err := s.stateMachine.EvolveState(nil)
   489  	require.NoError(s.T(), err)
   490  }
   491  
   492  // TestEvolveStateTransitionToNextEpoch_Error tests that error that has been
   493  // observed when transitioning to the next epoch and propagated to the caller.
   494  func (s *EpochStateMachineSuite) TestEvolveStateTransitionToNextEpoch_Error() {
   495  	parentState := unittest.EpochStateFixture(unittest.WithNextEpochProtocolState())
   496  	s.happyPathStateMachine.On("ParentState").Unset()
   497  	s.happyPathStateMachine.On("ParentState").Return(parentState)
   498  	// we are at the first block of the next epoch
   499  	s.happyPathStateMachine.On("View").Return(parentState.CurrentEpochSetup.FinalView + 1)
   500  	exception := errors.New("exception")
   501  	s.happyPathStateMachine.On("TransitionToNextEpoch").Return(exception).Once()
   502  	err := s.stateMachine.EvolveState(nil)
   503  	require.ErrorIs(s.T(), err, exception)
   504  	require.False(s.T(), protocol.IsInvalidServiceEventError(err))
   505  }
   506  
   507  // TestEvolveState_EventsAreFiltered tests that EpochStateMachine filters out all events that are not expected.
   508  func (s *EpochStateMachineSuite) TestEvolveState_EventsAreFiltered() {
   509  	err := s.stateMachine.EvolveState([]flow.ServiceEvent{
   510  		unittest.ProtocolStateVersionUpgradeFixture().ServiceEvent(),
   511  	})
   512  	require.NoError(s.T(), err)
   513  }
   514  
   515  // TestEvolveStateTransitionToNextEpoch_WithInvalidStateTransition tests that EpochStateMachine transitions to the next epoch
   516  // if an invalid state transition has been detected in a block which triggers transitioning to the next epoch.
   517  // In such situation, we still need to enter the next epoch (because it has already been committed), but persist in the
   518  // state that we have entered Epoch fallback mode (`flow.ProtocolStateEntry.InvalidEpochTransitionAttempted` is set to `true`).
   519  // This test ensures that we don't drop previously committed next epoch.
   520  func (s *EpochStateMachineSuite) TestEvolveStateTransitionToNextEpoch_WithInvalidStateTransition() {
   521  	unittest.SkipUnless(s.T(), unittest.TEST_TODO,
   522  		"This test is broken with current implementation but must pass when EFM recovery has been implemented."+
   523  			"See for details https://github.com/onflow/flow-go/issues/5631.")
   524  	s.parentEpochState = unittest.EpochStateFixture(unittest.WithNextEpochProtocolState())
   525  	s.candidate.View = s.parentEpochState.NextEpochSetup.FirstView
   526  	stateMachine, err := epochs.NewEpochStateMachineFactory(
   527  		s.globalParams,
   528  		s.setupsDB,
   529  		s.commitsDB,
   530  		s.epochStateDB,
   531  	).Create(s.candidate.View, s.candidate.ParentID, s.parentState, s.mutator)
   532  	require.NoError(s.T(), err)
   533  
   534  	invalidServiceEvent := unittest.EpochSetupFixture()
   535  	err = stateMachine.EvolveState([]flow.ServiceEvent{invalidServiceEvent.ServiceEvent()})
   536  	require.NoError(s.T(), err)
   537  
   538  	indexTxDeferredUpdate := storagemock.NewDeferredDBUpdate(s.T())
   539  	indexTxDeferredUpdate.On("Execute", mocks.Anything).Return(nil).Once()
   540  	s.epochStateDB.On("Index", s.candidate.ID(), mocks.Anything).Return(indexTxDeferredUpdate.Execute, nil).Once()
   541  
   542  	expectedEpochState := &flow.ProtocolStateEntry{
   543  		PreviousEpoch:                   s.parentEpochState.CurrentEpoch.Copy(),
   544  		CurrentEpoch:                    *s.parentEpochState.NextEpoch.Copy(),
   545  		NextEpoch:                       nil,
   546  		InvalidEpochTransitionAttempted: true,
   547  	}
   548  
   549  	storeTxDeferredUpdate := storagemock.NewDeferredDBUpdate(s.T())
   550  	storeTxDeferredUpdate.On("Execute", mocks.Anything).Return(nil).Once()
   551  	s.epochStateDB.On("StoreTx", expectedEpochState.ID(), expectedEpochState).Return(storeTxDeferredUpdate.Execute, nil).Once()
   552  	s.mutator.On("SetEpochStateID", expectedEpochState.ID()).Return().Once()
   553  
   554  	dbOps, err := stateMachine.Build()
   555  	require.NoError(s.T(), err)
   556  	// Provide the blockID and execute the resulting `DeferredDBUpdate`. Thereby,
   557  	// the expected mock methods should be called, which is asserted by the testify framework
   558  	err = dbOps.Pending().WithBlock(s.candidate.ID())(&transaction.Tx{})
   559  	require.NoError(s.T(), err)
   560  }