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 }