github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/state/protocol/protocol_state/epochs/happy_path_statemachine_test.go (about) 1 package epochs 2 3 import ( 4 "testing" 5 6 "github.com/stretchr/testify/require" 7 "github.com/stretchr/testify/suite" 8 9 "github.com/onflow/flow-go/model/flow" 10 "github.com/onflow/flow-go/model/flow/filter" 11 "github.com/onflow/flow-go/state/protocol" 12 "github.com/onflow/flow-go/utils/unittest" 13 ) 14 15 func TestProtocolStateMachine(t *testing.T) { 16 suite.Run(t, new(ProtocolStateMachineSuite)) 17 } 18 19 // BaseStateMachineSuite is a base test suite that holds common functionality for testing protocol state machines. 20 // It reflects the portion of data which is present in baseStateMachine. 21 type BaseStateMachineSuite struct { 22 suite.Suite 23 24 parentProtocolState *flow.RichProtocolStateEntry 25 parentBlock *flow.Header 26 candidate *flow.Header 27 } 28 29 func (s *BaseStateMachineSuite) SetupTest() { 30 s.parentProtocolState = unittest.EpochStateFixture() 31 s.parentBlock = unittest.BlockHeaderFixture(unittest.HeaderWithView(s.parentProtocolState.CurrentEpochSetup.FirstView + 1)) 32 s.candidate = unittest.BlockHeaderWithParentFixture(s.parentBlock) 33 } 34 35 // ProtocolStateMachineSuite is a dedicated test suite for testing happy path state machine. 36 type ProtocolStateMachineSuite struct { 37 BaseStateMachineSuite 38 stateMachine *HappyPathStateMachine 39 } 40 41 func (s *ProtocolStateMachineSuite) SetupTest() { 42 s.BaseStateMachineSuite.SetupTest() 43 var err error 44 s.stateMachine, err = NewHappyPathStateMachine(s.candidate.View, s.parentProtocolState.Copy()) 45 require.NoError(s.T(), err) 46 } 47 48 // TestNewstateMachine tests if the constructor correctly setups invariants for HappyPathStateMachine. 49 func (s *ProtocolStateMachineSuite) TestNewstateMachine() { 50 require.NotSame(s.T(), s.stateMachine.parentState, s.stateMachine.state, "except to take deep copy of parent state") 51 require.Nil(s.T(), s.stateMachine.parentState.NextEpoch) 52 require.Nil(s.T(), s.stateMachine.state.NextEpoch) 53 require.Equal(s.T(), s.candidate.View, s.stateMachine.View()) 54 require.Equal(s.T(), s.parentProtocolState, s.stateMachine.ParentState()) 55 } 56 57 // TestTransitionToNextEpoch tests a scenario where the HappyPathStateMachine processes first block from next epoch. 58 // It has to discard the parent state and build a new state with data from next epoch. 59 func (s *ProtocolStateMachineSuite) TestTransitionToNextEpoch() { 60 // update protocol state with next epoch information 61 unittest.WithNextEpochProtocolState()(s.parentProtocolState) 62 63 candidate := unittest.BlockHeaderFixture( 64 unittest.HeaderWithView(s.parentProtocolState.CurrentEpochSetup.FinalView + 1)) 65 var err error 66 // since the candidate block is from next epoch, HappyPathStateMachine should transition to next epoch 67 s.stateMachine, err = NewHappyPathStateMachine(candidate.View, s.parentProtocolState.Copy()) 68 require.NoError(s.T(), err) 69 err = s.stateMachine.TransitionToNextEpoch() 70 require.NoError(s.T(), err) 71 updatedState, stateID, hasChanges := s.stateMachine.Build() 72 require.True(s.T(), hasChanges) 73 require.NotEqual(s.T(), s.parentProtocolState.ID(), updatedState.ID()) 74 require.Equal(s.T(), updatedState.ID(), stateID) 75 require.Equal(s.T(), s.parentProtocolState.ID(), s.stateMachine.ParentState().ID(), "should not modify parent protocol state") 76 require.Equal(s.T(), updatedState.CurrentEpoch.ID(), s.parentProtocolState.NextEpoch.ID(), "should transition into next epoch") 77 require.Nil(s.T(), updatedState.NextEpoch, "next epoch protocol state should be nil") 78 } 79 80 // TestTransitionToNextEpochNotAllowed tests different scenarios where transition to next epoch is not allowed. 81 func (s *ProtocolStateMachineSuite) TestTransitionToNextEpochNotAllowed() { 82 s.Run("no next epoch protocol state", func() { 83 protocolState := unittest.EpochStateFixture() 84 candidate := unittest.BlockHeaderFixture( 85 unittest.HeaderWithView(protocolState.CurrentEpochSetup.FinalView + 1)) 86 stateMachine, err := NewHappyPathStateMachine(candidate.View, protocolState) 87 require.NoError(s.T(), err) 88 err = stateMachine.TransitionToNextEpoch() 89 require.Error(s.T(), err, "should not allow transition to next epoch if there is no next epoch protocol state") 90 }) 91 s.Run("next epoch not committed", func() { 92 protocolState := unittest.EpochStateFixture(unittest.WithNextEpochProtocolState(), func(entry *flow.RichProtocolStateEntry) { 93 entry.NextEpoch.CommitID = flow.ZeroID 94 entry.NextEpochCommit = nil 95 }) 96 candidate := unittest.BlockHeaderFixture( 97 unittest.HeaderWithView(protocolState.CurrentEpochSetup.FinalView + 1)) 98 stateMachine, err := NewHappyPathStateMachine(candidate.View, protocolState) 99 require.NoError(s.T(), err) 100 err = stateMachine.TransitionToNextEpoch() 101 require.Error(s.T(), err, "should not allow transition to next epoch if it is not committed") 102 }) 103 s.Run("candidate block is not from next epoch", func() { 104 protocolState := unittest.EpochStateFixture(unittest.WithNextEpochProtocolState()) 105 candidate := unittest.BlockHeaderFixture( 106 unittest.HeaderWithView(protocolState.CurrentEpochSetup.FinalView)) 107 stateMachine, err := NewHappyPathStateMachine(candidate.View, protocolState) 108 require.NoError(s.T(), err) 109 err = stateMachine.TransitionToNextEpoch() 110 require.Error(s.T(), err, "should not allow transition to next epoch if next block is not first block from next epoch") 111 }) 112 } 113 114 // TestBuild tests if the HappyPathStateMachine returns correct protocol state. 115 func (s *ProtocolStateMachineSuite) TestBuild() { 116 updatedState, stateID, hasChanges := s.stateMachine.Build() 117 require.Equal(s.T(), stateID, s.parentProtocolState.ID(), "should return same protocol state") 118 require.False(s.T(), hasChanges, "should not have changes") 119 require.NotSame(s.T(), updatedState, s.stateMachine.state, "should return a copy of protocol state") 120 require.Equal(s.T(), updatedState.ID(), stateID, "should return correct ID") 121 require.Equal(s.T(), s.parentProtocolState.ID(), s.stateMachine.ParentState().ID(), "should not modify parent protocol state") 122 123 updatedDynamicIdentity := s.parentProtocolState.CurrentEpochIdentityTable[0].NodeID 124 err := s.stateMachine.EjectIdentity(updatedDynamicIdentity) 125 require.NoError(s.T(), err) 126 updatedState, stateID, hasChanges = s.stateMachine.Build() 127 require.True(s.T(), hasChanges, "should have changes") 128 require.NotEqual(s.T(), stateID, s.parentProtocolState.ID(), "protocol state was modified but still has same ID") 129 require.Equal(s.T(), updatedState.ID(), stateID, "should return correct ID") 130 require.Equal(s.T(), s.parentProtocolState.ID(), s.stateMachine.ParentState().ID(), "should not modify parent protocol state") 131 } 132 133 // TestCreateStateMachineAfterInvalidStateTransitionAttempted tests if creating state machine after observing invalid state transition 134 // results in error . 135 func (s *ProtocolStateMachineSuite) TestCreateStateMachineAfterInvalidStateTransitionAttempted() { 136 s.parentProtocolState.InvalidEpochTransitionAttempted = true 137 var err error 138 // create new HappyPathStateMachine with next epoch information 139 s.stateMachine, err = NewHappyPathStateMachine(s.candidate.View, s.parentProtocolState.Copy()) 140 require.Error(s.T(), err) 141 } 142 143 // TestProcessEpochCommit tests if processing epoch commit event correctly updates internal state of HappyPathStateMachine and 144 // correctly behaves when invariants are violated. 145 func (s *ProtocolStateMachineSuite) TestProcessEpochCommit() { 146 var err error 147 s.Run("invalid counter", func() { 148 s.stateMachine, err = NewHappyPathStateMachine(s.candidate.View, s.parentProtocolState.Copy()) 149 require.NoError(s.T(), err) 150 commit := unittest.EpochCommitFixture(func(commit *flow.EpochCommit) { 151 commit.Counter = s.parentProtocolState.CurrentEpochSetup.Counter + 10 // set invalid counter for next epoch 152 }) 153 _, err := s.stateMachine.ProcessEpochCommit(commit) 154 require.Error(s.T(), err) 155 require.True(s.T(), protocol.IsInvalidServiceEventError(err)) 156 }) 157 s.Run("no next epoch protocol state", func() { 158 s.stateMachine, err = NewHappyPathStateMachine(s.candidate.View, s.parentProtocolState.Copy()) 159 require.NoError(s.T(), err) 160 commit := unittest.EpochCommitFixture(func(commit *flow.EpochCommit) { 161 commit.Counter = s.parentProtocolState.CurrentEpochSetup.Counter + 1 162 }) 163 _, err := s.stateMachine.ProcessEpochCommit(commit) 164 require.Error(s.T(), err) 165 require.True(s.T(), protocol.IsInvalidServiceEventError(err)) 166 }) 167 s.Run("conflicting epoch commit", func() { 168 s.stateMachine, err = NewHappyPathStateMachine(s.candidate.View, s.parentProtocolState.Copy()) 169 require.NoError(s.T(), err) 170 setup := unittest.EpochSetupFixture( 171 unittest.SetupWithCounter(s.parentProtocolState.CurrentEpochSetup.Counter+1), 172 unittest.WithFirstView(s.parentProtocolState.CurrentEpochSetup.FinalView+1), 173 unittest.WithFinalView(s.parentProtocolState.CurrentEpochSetup.FinalView+1000), 174 ) 175 // processing setup event results in creating next epoch protocol state 176 _, err := s.stateMachine.ProcessEpochSetup(setup) 177 require.NoError(s.T(), err) 178 179 updatedState, _, _ := s.stateMachine.Build() 180 181 parentState, err := flow.NewRichProtocolStateEntry(updatedState, 182 s.parentProtocolState.PreviousEpochSetup, 183 s.parentProtocolState.PreviousEpochCommit, 184 s.parentProtocolState.CurrentEpochSetup, 185 s.parentProtocolState.CurrentEpochCommit, 186 setup, 187 nil, 188 ) 189 require.NoError(s.T(), err) 190 s.stateMachine, err = NewHappyPathStateMachine(s.candidate.View+1, parentState) 191 require.NoError(s.T(), err) 192 commit := unittest.EpochCommitFixture( 193 unittest.CommitWithCounter(setup.Counter), 194 unittest.WithDKGFromParticipants(setup.Participants), 195 ) 196 197 _, err = s.stateMachine.ProcessEpochCommit(commit) 198 require.NoError(s.T(), err) 199 200 // processing another epoch commit has to be an error since we have already processed one 201 _, err = s.stateMachine.ProcessEpochCommit(commit) 202 require.Error(s.T(), err) 203 require.True(s.T(), protocol.IsInvalidServiceEventError(err)) 204 205 newState, _, _ := s.stateMachine.Build() 206 require.Equal(s.T(), commit.ID(), newState.NextEpoch.CommitID, "next epoch should be committed since we have observed, a valid event") 207 }) 208 s.Run("happy path processing", func() { 209 s.stateMachine, err = NewHappyPathStateMachine(s.candidate.View, s.parentProtocolState.Copy()) 210 require.NoError(s.T(), err) 211 setup := unittest.EpochSetupFixture( 212 unittest.SetupWithCounter(s.parentProtocolState.CurrentEpochSetup.Counter+1), 213 unittest.WithFirstView(s.parentProtocolState.CurrentEpochSetup.FinalView+1), 214 unittest.WithFinalView(s.parentProtocolState.CurrentEpochSetup.FinalView+1000), 215 ) 216 // processing setup event results in creating next epoch protocol state 217 _, err := s.stateMachine.ProcessEpochSetup(setup) 218 require.NoError(s.T(), err) 219 220 updatedState, stateID, hasChanges := s.stateMachine.Build() 221 require.True(s.T(), hasChanges) 222 require.NotEqual(s.T(), s.parentProtocolState.ID(), updatedState.ID()) 223 require.Equal(s.T(), updatedState.ID(), stateID) 224 require.Equal(s.T(), s.parentProtocolState.ID(), s.stateMachine.ParentState().ID(), "should not modify parent protocol state") 225 226 parentState, err := flow.NewRichProtocolStateEntry(updatedState, 227 s.parentProtocolState.PreviousEpochSetup, 228 s.parentProtocolState.PreviousEpochCommit, 229 s.parentProtocolState.CurrentEpochSetup, 230 s.parentProtocolState.CurrentEpochCommit, 231 setup, 232 nil, 233 ) 234 require.NoError(s.T(), err) 235 s.stateMachine, err = NewHappyPathStateMachine(s.candidate.View+1, parentState.Copy()) 236 require.NoError(s.T(), err) 237 commit := unittest.EpochCommitFixture( 238 unittest.CommitWithCounter(setup.Counter), 239 unittest.WithDKGFromParticipants(setup.Participants), 240 ) 241 242 _, err = s.stateMachine.ProcessEpochCommit(commit) 243 require.NoError(s.T(), err) 244 245 newState, newStateID, newStateHasChanges := s.stateMachine.Build() 246 require.True(s.T(), newStateHasChanges) 247 require.Equal(s.T(), commit.ID(), newState.NextEpoch.CommitID, "next epoch should be committed") 248 require.Equal(s.T(), newState.ID(), newStateID) 249 require.NotEqual(s.T(), s.parentProtocolState.ID(), newState.ID()) 250 require.NotEqual(s.T(), updatedState.ID(), newState.ID()) 251 require.Equal(s.T(), parentState.ID(), s.stateMachine.ParentState().ID(), 252 "should not modify parent protocol state") 253 }) 254 } 255 256 // TestUpdateIdentityUnknownIdentity tests if updating the identity of unknown node results in an error. 257 func (s *ProtocolStateMachineSuite) TestUpdateIdentityUnknownIdentity() { 258 err := s.stateMachine.EjectIdentity(unittest.IdentifierFixture()) 259 require.Error(s.T(), err, "should not be able to update data of unknown identity") 260 require.True(s.T(), protocol.IsInvalidServiceEventError(err)) 261 262 updatedState, updatedStateID, hasChanges := s.stateMachine.Build() 263 require.False(s.T(), hasChanges, "should not have changes") 264 require.Equal(s.T(), updatedState.ID(), s.parentProtocolState.ID()) 265 require.Equal(s.T(), updatedState.ID(), updatedStateID) 266 } 267 268 // TestUpdateIdentityHappyPath tests if identity updates are correctly processed and reflected in the resulting protocol state. 269 func (s *ProtocolStateMachineSuite) TestUpdateIdentityHappyPath() { 270 // update protocol state to have next epoch protocol state 271 unittest.WithNextEpochProtocolState()(s.parentProtocolState) 272 var err error 273 s.stateMachine, err = NewHappyPathStateMachine(s.candidate.View, s.parentProtocolState.Copy()) 274 require.NoError(s.T(), err) 275 276 currentEpochParticipants := s.parentProtocolState.CurrentEpochIdentityTable.Copy() 277 ejectedChanges, err := currentEpochParticipants.Sample(2) 278 require.NoError(s.T(), err) 279 280 for _, update := range ejectedChanges { 281 err := s.stateMachine.EjectIdentity(update.NodeID) 282 require.NoError(s.T(), err) 283 } 284 updatedState, updatedStateID, hasChanges := s.stateMachine.Build() 285 require.True(s.T(), hasChanges, "should have changes") 286 require.Equal(s.T(), updatedState.ID(), updatedStateID) 287 require.NotEqual(s.T(), s.parentProtocolState.ID(), updatedState.ID()) 288 require.Equal(s.T(), s.parentProtocolState.ID(), s.stateMachine.ParentState().ID(), 289 "should not modify parent protocol state") 290 291 // assert that all changes made in the previous epoch are preserved 292 currentEpochLookup := updatedState.CurrentEpoch.ActiveIdentities.Lookup() 293 nextEpochLookup := updatedState.NextEpoch.ActiveIdentities.Lookup() 294 295 for _, updated := range ejectedChanges { 296 currentEpochIdentity, foundInCurrentEpoch := currentEpochLookup[updated.NodeID] 297 if foundInCurrentEpoch { 298 require.Equal(s.T(), updated.NodeID, currentEpochIdentity.NodeID) 299 require.True(s.T(), currentEpochIdentity.Ejected) 300 } 301 302 nextEpochIdentity, foundInNextEpoch := nextEpochLookup[updated.NodeID] 303 if foundInNextEpoch { 304 require.Equal(s.T(), updated.NodeID, nextEpochIdentity.NodeID) 305 require.True(s.T(), nextEpochIdentity.Ejected) 306 } 307 require.True(s.T(), foundInCurrentEpoch || foundInNextEpoch, "identity should be found in either current or next epoch") 308 } 309 } 310 311 // TestProcessEpochSetupInvariants tests if processing epoch setup when invariants are violated doesn't update internal structures. 312 func (s *ProtocolStateMachineSuite) TestProcessEpochSetupInvariants() { 313 s.Run("invalid counter", func() { 314 setup := unittest.EpochSetupFixture(func(setup *flow.EpochSetup) { 315 setup.Counter = s.parentProtocolState.CurrentEpochSetup.Counter + 10 // set invalid counter for next epoch 316 }) 317 _, err := s.stateMachine.ProcessEpochSetup(setup) 318 require.Error(s.T(), err) 319 require.True(s.T(), protocol.IsInvalidServiceEventError(err)) 320 }) 321 s.Run("processing second epoch setup", func() { 322 stateMachine, err := NewHappyPathStateMachine(s.candidate.View, s.parentProtocolState.Copy()) 323 require.NoError(s.T(), err) 324 setup := unittest.EpochSetupFixture( 325 unittest.SetupWithCounter(s.parentProtocolState.CurrentEpochSetup.Counter+1), 326 unittest.WithFirstView(s.parentProtocolState.CurrentEpochSetup.FinalView+1), 327 unittest.WithFinalView(s.parentProtocolState.CurrentEpochSetup.FinalView+1000), 328 ) 329 _, err = stateMachine.ProcessEpochSetup(setup) 330 require.NoError(s.T(), err) 331 332 _, err = stateMachine.ProcessEpochSetup(setup) 333 require.Error(s.T(), err) 334 require.True(s.T(), protocol.IsInvalidServiceEventError(err)) 335 }) 336 s.Run("participants not sorted", func() { 337 stateMachine, err := NewHappyPathStateMachine(s.candidate.View, s.parentProtocolState.Copy()) 338 require.NoError(s.T(), err) 339 setup := unittest.EpochSetupFixture(func(setup *flow.EpochSetup) { 340 setup.Counter = s.parentProtocolState.CurrentEpochSetup.Counter + 1 341 var err error 342 setup.Participants, err = setup.Participants.Shuffle() 343 require.NoError(s.T(), err) 344 }) 345 _, err = stateMachine.ProcessEpochSetup(setup) 346 require.Error(s.T(), err) 347 require.True(s.T(), protocol.IsInvalidServiceEventError(err)) 348 }) 349 s.Run("epoch setup state conflicts with protocol state", func() { 350 conflictingIdentity := s.parentProtocolState.ProtocolStateEntry.CurrentEpoch.ActiveIdentities[0] 351 conflictingIdentity.Ejected = true 352 353 stateMachine, err := NewHappyPathStateMachine(s.candidate.View, s.parentProtocolState.Copy()) 354 require.NoError(s.T(), err) 355 setup := unittest.EpochSetupFixture(func(setup *flow.EpochSetup) { 356 setup.Counter = s.parentProtocolState.CurrentEpochSetup.Counter + 1 357 // using same identities as in previous epoch should result in an error since 358 // we have ejected conflicting identity but it was added back in epoch setup 359 // such epoch setup event is invalid. 360 setup.Participants = s.parentProtocolState.CurrentEpochSetup.Participants 361 }) 362 363 _, err = stateMachine.ProcessEpochSetup(setup) 364 require.Error(s.T(), err) 365 require.True(s.T(), protocol.IsInvalidServiceEventError(err)) 366 }) 367 } 368 369 // TestProcessEpochSetupHappyPath tests if processing epoch setup when invariants are not violated updates internal structures. 370 // We test correct construction of the *active identities* for the current and next epoch. Specifically, observing an EpochSetup 371 // event should leave `PreviousEpoch` and `CurrentEpoch`'s EpochStateContainer unchanged. 372 // The next epoch's EpochStateContainer should reference the EpochSetup event and hold the respective ActiveIdentities. 373 func (s *ProtocolStateMachineSuite) TestProcessEpochSetupHappyPath() { 374 setupParticipants := unittest.IdentityListFixture(5, unittest.WithAllRoles()).Sort(flow.Canonical[flow.Identity]) 375 setupParticipants[0].InitialWeight = 13 376 setup := unittest.EpochSetupFixture( 377 unittest.SetupWithCounter(s.parentProtocolState.CurrentEpochSetup.Counter+1), 378 unittest.WithFirstView(s.parentProtocolState.CurrentEpochSetup.FinalView+1), 379 unittest.WithFinalView(s.parentProtocolState.CurrentEpochSetup.FinalView+1000), 380 unittest.WithParticipants(setupParticipants.ToSkeleton()), 381 ) 382 383 // for next epoch we will have all the identities from setup event 384 expectedNextEpochActiveIdentities := flow.DynamicIdentityEntryListFromIdentities(setupParticipants) 385 386 // process actual event 387 _, err := s.stateMachine.ProcessEpochSetup(setup) 388 require.NoError(s.T(), err) 389 390 updatedState, _, hasChanges := s.stateMachine.Build() 391 require.True(s.T(), hasChanges, "should have changes") 392 require.Equal(s.T(), s.parentProtocolState.PreviousEpoch, updatedState.PreviousEpoch, "previous epoch's EpochStateContainer should not change") 393 require.Equal(s.T(), s.parentProtocolState.CurrentEpoch, updatedState.CurrentEpoch, "current epoch's EpochStateContainer should not change") 394 nextEpoch := updatedState.NextEpoch 395 require.NotNil(s.T(), nextEpoch, "should have next epoch protocol state") 396 require.Equal(s.T(), nextEpoch.SetupID, setup.ID(), 397 "should have correct setup ID for next protocol state") 398 require.Equal(s.T(), nextEpoch.CommitID, flow.ZeroID, "ID for EpochCommit event should still be nil") 399 require.Equal(s.T(), expectedNextEpochActiveIdentities, nextEpoch.ActiveIdentities, 400 "should have filled active identities for next epoch") 401 } 402 403 // TestProcessEpochSetupWithSameParticipants tests that processing epoch setup with overlapping participants results in correctly 404 // built updated protocol state. It should build a union of participants from current and next epoch for current and 405 // next epoch protocol states respectively. 406 func (s *ProtocolStateMachineSuite) TestProcessEpochSetupWithSameParticipants() { 407 participantsFromCurrentEpochSetup, err := flow.ComposeFullIdentities( 408 s.parentProtocolState.CurrentEpochSetup.Participants, 409 s.parentProtocolState.CurrentEpoch.ActiveIdentities, 410 flow.EpochParticipationStatusActive, 411 ) 412 require.NoError(s.T(), err) 413 // Function `ComposeFullIdentities` verified that `Participants` and `ActiveIdentities` have identical ordering w.r.t nodeID. 414 // By construction, `participantsFromCurrentEpochSetup` lists the full Identities in the same ordering as `Participants` and 415 // `ActiveIdentities`. By confirming that `participantsFromCurrentEpochSetup` follows canonical ordering, we can conclude that 416 // also `Participants` and `ActiveIdentities` are canonically ordered. 417 require.True(s.T(), participantsFromCurrentEpochSetup.Sorted(flow.Canonical[flow.Identity]), "participants in current epoch's setup event are not in canonical order") 418 419 overlappingNodes, err := participantsFromCurrentEpochSetup.Sample(2) 420 require.NoError(s.T(), err) 421 setupParticipants := append(unittest.IdentityListFixture(len(s.parentProtocolState.CurrentEpochIdentityTable), unittest.WithAllRoles()), 422 overlappingNodes...).Sort(flow.Canonical[flow.Identity]) 423 setup := unittest.EpochSetupFixture( 424 unittest.SetupWithCounter(s.parentProtocolState.CurrentEpochSetup.Counter+1), 425 unittest.WithFirstView(s.parentProtocolState.CurrentEpochSetup.FinalView+1), 426 unittest.WithFinalView(s.parentProtocolState.CurrentEpochSetup.FinalView+1000), 427 unittest.WithParticipants(setupParticipants.ToSkeleton()), 428 ) 429 _, err = s.stateMachine.ProcessEpochSetup(setup) 430 require.NoError(s.T(), err) 431 updatedState, _, _ := s.stateMachine.Build() 432 433 require.Equal(s.T(), s.parentProtocolState.CurrentEpoch.ActiveIdentities, 434 updatedState.CurrentEpoch.ActiveIdentities, 435 "should not change active identities for current epoch") 436 437 expectedNextEpochActiveIdentities := flow.DynamicIdentityEntryListFromIdentities(setupParticipants) 438 require.Equal(s.T(), expectedNextEpochActiveIdentities, updatedState.NextEpoch.ActiveIdentities, 439 "should have filled active identities for next epoch") 440 } 441 442 // TestEpochSetupAfterIdentityChange tests that after processing epoch an setup event, all previously made changes to the identity table 443 // are preserved and reflected in the resulting protocol state. 444 func (s *ProtocolStateMachineSuite) TestEpochSetupAfterIdentityChange() { 445 participantsFromCurrentEpochSetup := s.parentProtocolState.CurrentEpochIdentityTable.Filter(func(i *flow.Identity) bool { 446 _, exists := s.parentProtocolState.CurrentEpochSetup.Participants.ByNodeID(i.NodeID) 447 return exists 448 }).Sort(flow.Canonical[flow.Identity]) 449 ejectedChanges, err := participantsFromCurrentEpochSetup.Sample(2) 450 require.NoError(s.T(), err) 451 for _, update := range ejectedChanges { 452 err := s.stateMachine.EjectIdentity(update.NodeID) 453 require.NoError(s.T(), err) 454 } 455 updatedState, _, _ := s.stateMachine.Build() 456 457 // Construct a valid flow.RichProtocolStateEntry for next block 458 // We do this by copying the parent protocol state and updating the identities manually 459 updatedRichProtocolState := &flow.RichProtocolStateEntry{ 460 ProtocolStateEntry: updatedState, 461 PreviousEpochSetup: s.parentProtocolState.PreviousEpochSetup, 462 PreviousEpochCommit: s.parentProtocolState.PreviousEpochCommit, 463 CurrentEpochSetup: s.parentProtocolState.CurrentEpochSetup, 464 CurrentEpochCommit: s.parentProtocolState.CurrentEpochCommit, 465 NextEpochSetup: nil, 466 NextEpochCommit: nil, 467 CurrentEpochIdentityTable: s.parentProtocolState.CurrentEpochIdentityTable.Copy(), 468 NextEpochIdentityTable: flow.IdentityList{}, 469 } 470 // Update enriched data with the changes made to the low-level updated table 471 for _, identity := range ejectedChanges { 472 toBeUpdated, _ := updatedRichProtocolState.CurrentEpochIdentityTable.ByNodeID(identity.NodeID) 473 toBeUpdated.EpochParticipationStatus = flow.EpochParticipationStatusEjected 474 } 475 476 // now we can use it to construct HappyPathStateMachine for next block, which will process epoch setup event. 477 nextBlock := unittest.BlockHeaderWithParentFixture(s.candidate) 478 s.stateMachine, err = NewHappyPathStateMachine(nextBlock.View, updatedRichProtocolState) 479 require.NoError(s.T(), err) 480 481 setup := unittest.EpochSetupFixture( 482 unittest.SetupWithCounter(s.parentProtocolState.CurrentEpochSetup.Counter+1), 483 unittest.WithFirstView(s.parentProtocolState.CurrentEpochSetup.FinalView+1), 484 unittest.WithFinalView(s.parentProtocolState.CurrentEpochSetup.FinalView+1000), 485 func(setup *flow.EpochSetup) { 486 // add those nodes that were changed in the previous epoch, but not those that were ejected 487 // it's important to exclude ejected nodes, since we expect that service smart contract has emitted ejection operation 488 // and service events are delivered (asynchronously) in an *order-preserving* manner meaning if ejection has happened before 489 // epoch setup then there is no possible way that it will include ejected node unless there is a severe bug in the service contract. 490 setup.Participants = setup.Participants.Filter( 491 filter.Not(filter.In(ejectedChanges.ToSkeleton()))).Sort(flow.Canonical[flow.IdentitySkeleton]) 492 }, 493 ) 494 495 _, err = s.stateMachine.ProcessEpochSetup(setup) 496 require.NoError(s.T(), err) 497 498 updatedState, _, _ = s.stateMachine.Build() 499 500 // assert that all changes made in previous epoch are preserved 501 currentEpochLookup := updatedState.CurrentEpoch.ActiveIdentities.Lookup() 502 nextEpochLookup := updatedState.NextEpoch.ActiveIdentities.Lookup() 503 504 for _, updated := range ejectedChanges { 505 currentEpochIdentity := currentEpochLookup[updated.NodeID] 506 require.Equal(s.T(), updated.NodeID, currentEpochIdentity.NodeID) 507 require.True(s.T(), currentEpochIdentity.Ejected) 508 509 _, foundInNextEpoch := nextEpochLookup[updated.NodeID] 510 require.False(s.T(), foundInNextEpoch) 511 } 512 }