github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/model/flow/protocol_state_test.go (about) 1 package flow_test 2 3 import ( 4 "fmt" 5 "testing" 6 7 "github.com/stretchr/testify/assert" 8 9 "github.com/onflow/flow-go/model/flow" 10 "github.com/onflow/flow-go/utils/unittest" 11 ) 12 13 // TestNewRichProtocolStateEntry checks that NewRichProtocolStateEntry creates valid identity tables depending on the state 14 // of epoch which is derived from the protocol state entry. 15 func TestNewRichProtocolStateEntry(t *testing.T) { 16 // Conditions right after a spork: 17 // * no previous epoch exists from the perspective of the freshly-sporked protocol state 18 // * network is currently in the staking phase for the next epoch, hence no service events for the next epoch exist 19 t.Run("staking-root-protocol-state", func(t *testing.T) { 20 setup := unittest.EpochSetupFixture() 21 currentEpochCommit := unittest.EpochCommitFixture() 22 identities := make(flow.DynamicIdentityEntryList, 0, len(setup.Participants)) 23 for _, identity := range setup.Participants { 24 identities = append(identities, &flow.DynamicIdentityEntry{ 25 NodeID: identity.NodeID, 26 Ejected: false, 27 }) 28 } 29 stateEntry := &flow.ProtocolStateEntry{ 30 PreviousEpoch: nil, 31 CurrentEpoch: flow.EpochStateContainer{ 32 SetupID: setup.ID(), 33 CommitID: currentEpochCommit.ID(), 34 ActiveIdentities: identities, 35 }, 36 InvalidEpochTransitionAttempted: false, 37 } 38 entry, err := flow.NewRichProtocolStateEntry( 39 stateEntry, 40 nil, 41 nil, 42 setup, 43 currentEpochCommit, 44 nil, 45 nil, 46 ) 47 assert.NoError(t, err) 48 assert.Equal(t, flow.EpochPhaseStaking, entry.EpochPhase()) 49 50 expectedIdentities, err := flow.BuildIdentityTable( 51 setup.Participants, 52 identities, 53 nil, 54 nil, 55 flow.EpochParticipationStatusLeaving, 56 ) 57 assert.NoError(t, err) 58 assert.Equal(t, expectedIdentities, entry.CurrentEpochIdentityTable, "should be equal to current epoch setup participants") 59 }) 60 61 // Common situation during the staking phase for epoch N+1 62 // * we are currently in Epoch N 63 // * previous epoch N-1 is known (specifically EpochSetup and EpochCommit events) 64 // * network is currently in the staking phase for the next epoch, hence no service events for the next epoch exist 65 t.Run("staking-phase", func(t *testing.T) { 66 stateEntry := unittest.EpochStateFixture() 67 richEntry, err := flow.NewRichProtocolStateEntry( 68 stateEntry.ProtocolStateEntry, 69 stateEntry.PreviousEpochSetup, 70 stateEntry.PreviousEpochCommit, 71 stateEntry.CurrentEpochSetup, 72 stateEntry.CurrentEpochCommit, 73 nil, 74 nil, 75 ) 76 assert.NoError(t, err) 77 assert.Equal(t, flow.EpochPhaseStaking, richEntry.EpochPhase()) 78 79 expectedIdentities, err := flow.BuildIdentityTable( 80 stateEntry.CurrentEpochSetup.Participants, 81 stateEntry.CurrentEpoch.ActiveIdentities, 82 stateEntry.PreviousEpochSetup.Participants, 83 stateEntry.PreviousEpoch.ActiveIdentities, 84 flow.EpochParticipationStatusLeaving, 85 ) 86 assert.NoError(t, err) 87 assert.Equal(t, expectedIdentities, richEntry.CurrentEpochIdentityTable, "should be equal to current epoch setup participants + previous epoch setup participants") 88 assert.Nil(t, richEntry.NextEpoch) 89 }) 90 91 // Common situation during the epoch setup phase for epoch N+1 92 // * we are currently in Epoch N 93 // * previous epoch N-1 is known (specifically EpochSetup and EpochCommit events) 94 // * network is currently in the setup phase for the next epoch, i.e. EpochSetup event (starting setup phase) has already been observed 95 t.Run("setup-phase", func(t *testing.T) { 96 stateEntry := unittest.EpochStateFixture(unittest.WithNextEpochProtocolState(), func(entry *flow.RichProtocolStateEntry) { 97 entry.NextEpochCommit = nil 98 entry.NextEpoch.CommitID = flow.ZeroID 99 }) 100 101 richEntry, err := flow.NewRichProtocolStateEntry( 102 stateEntry.ProtocolStateEntry, 103 stateEntry.PreviousEpochSetup, 104 stateEntry.PreviousEpochCommit, 105 stateEntry.CurrentEpochSetup, 106 stateEntry.CurrentEpochCommit, 107 stateEntry.NextEpochSetup, 108 nil, 109 ) 110 assert.NoError(t, err) 111 assert.Equal(t, flow.EpochPhaseSetup, richEntry.EpochPhase()) 112 113 expectedIdentities, err := flow.BuildIdentityTable( 114 stateEntry.CurrentEpochSetup.Participants, 115 stateEntry.CurrentEpoch.ActiveIdentities, 116 stateEntry.NextEpochSetup.Participants, 117 stateEntry.NextEpoch.ActiveIdentities, 118 flow.EpochParticipationStatusJoining, 119 ) 120 assert.NoError(t, err) 121 assert.Equal(t, expectedIdentities, richEntry.CurrentEpochIdentityTable, "should be equal to current epoch setup participants + next epoch setup participants") 122 assert.Nil(t, richEntry.NextEpochCommit) 123 expectedIdentities, err = flow.BuildIdentityTable( 124 stateEntry.NextEpochSetup.Participants, 125 stateEntry.NextEpoch.ActiveIdentities, 126 stateEntry.CurrentEpochSetup.Participants, 127 stateEntry.CurrentEpoch.ActiveIdentities, 128 flow.EpochParticipationStatusLeaving, 129 ) 130 assert.NoError(t, err) 131 assert.Equal(t, expectedIdentities, richEntry.NextEpochIdentityTable, "should be equal to next epoch setup participants + current epoch setup participants") 132 }) 133 134 t.Run("setup-after-spork", func(t *testing.T) { 135 stateEntry := unittest.EpochStateFixture(unittest.WithNextEpochProtocolState(), func(entry *flow.RichProtocolStateEntry) { 136 // no previous epoch since we are in the first epoch 137 entry.PreviousEpochSetup = nil 138 entry.PreviousEpochCommit = nil 139 entry.PreviousEpoch = nil 140 141 // next epoch is setup but not committed 142 entry.NextEpochCommit = nil 143 entry.NextEpoch.CommitID = flow.ZeroID 144 }) 145 // sanity check that previous epoch is not populated in `stateEntry` 146 assert.Nil(t, stateEntry.PreviousEpoch) 147 assert.Nil(t, stateEntry.PreviousEpochSetup) 148 assert.Nil(t, stateEntry.PreviousEpochCommit) 149 150 richEntry, err := flow.NewRichProtocolStateEntry( 151 stateEntry.ProtocolStateEntry, 152 stateEntry.PreviousEpochSetup, 153 stateEntry.PreviousEpochCommit, 154 stateEntry.CurrentEpochSetup, 155 stateEntry.CurrentEpochCommit, 156 stateEntry.NextEpochSetup, 157 nil, 158 ) 159 assert.NoError(t, err) 160 assert.Equal(t, flow.EpochPhaseSetup, richEntry.EpochPhase()) 161 162 expectedIdentities, err := flow.BuildIdentityTable( 163 stateEntry.CurrentEpochSetup.Participants, 164 stateEntry.CurrentEpoch.ActiveIdentities, 165 stateEntry.NextEpochSetup.Participants, 166 stateEntry.NextEpoch.ActiveIdentities, 167 flow.EpochParticipationStatusJoining, 168 ) 169 assert.NoError(t, err) 170 assert.Equal(t, expectedIdentities, richEntry.CurrentEpochIdentityTable, "should be equal to current epoch setup participants + next epoch setup participants") 171 assert.Nil(t, richEntry.NextEpochCommit) 172 expectedIdentities, err = flow.BuildIdentityTable( 173 stateEntry.NextEpochSetup.Participants, 174 stateEntry.NextEpoch.ActiveIdentities, 175 stateEntry.CurrentEpochSetup.Participants, 176 stateEntry.CurrentEpoch.ActiveIdentities, 177 flow.EpochParticipationStatusLeaving, 178 ) 179 assert.NoError(t, err) 180 assert.Equal(t, expectedIdentities, richEntry.NextEpochIdentityTable, "should be equal to next epoch setup participants + current epoch setup participants") 181 }) 182 183 // Common situation during the epoch commit phase for epoch N+1 184 // * we are currently in Epoch N 185 // * previous epoch N-1 is known (specifically EpochSetup and EpochCommit events) 186 // * The network has completed the epoch setup phase, i.e. published the EpochSetup and EpochCommit events for epoch N+1. 187 t.Run("commit-phase", func(t *testing.T) { 188 stateEntry := unittest.EpochStateFixture(unittest.WithNextEpochProtocolState()) 189 190 richEntry, err := flow.NewRichProtocolStateEntry( 191 stateEntry.ProtocolStateEntry, 192 stateEntry.PreviousEpochSetup, 193 stateEntry.PreviousEpochCommit, 194 stateEntry.CurrentEpochSetup, 195 stateEntry.CurrentEpochCommit, 196 stateEntry.NextEpochSetup, 197 stateEntry.NextEpochCommit, 198 ) 199 assert.NoError(t, err) 200 assert.Equal(t, flow.EpochPhaseCommitted, richEntry.EpochPhase()) 201 202 expectedIdentities, err := flow.BuildIdentityTable( 203 stateEntry.CurrentEpochSetup.Participants, 204 stateEntry.CurrentEpoch.ActiveIdentities, 205 stateEntry.NextEpochSetup.Participants, 206 stateEntry.NextEpoch.ActiveIdentities, 207 flow.EpochParticipationStatusJoining, 208 ) 209 assert.NoError(t, err) 210 assert.Equal(t, expectedIdentities, richEntry.CurrentEpochIdentityTable, "should be equal to current epoch setup participants + next epoch setup participants") 211 expectedIdentities, err = flow.BuildIdentityTable( 212 stateEntry.NextEpochSetup.Participants, 213 stateEntry.NextEpoch.ActiveIdentities, 214 stateEntry.CurrentEpochSetup.Participants, 215 stateEntry.CurrentEpoch.ActiveIdentities, 216 flow.EpochParticipationStatusLeaving, 217 ) 218 assert.NoError(t, err) 219 assert.Equal(t, expectedIdentities, richEntry.NextEpochIdentityTable, "should be equal to next epoch setup participants + current epoch setup participants") 220 }) 221 222 t.Run("commit-after-spork", func(t *testing.T) { 223 stateEntry := unittest.EpochStateFixture(unittest.WithNextEpochProtocolState(), func(entry *flow.RichProtocolStateEntry) { 224 // no previous epoch since we are in the first epoch 225 entry.PreviousEpochSetup = nil 226 entry.PreviousEpochCommit = nil 227 entry.PreviousEpoch = nil 228 }) 229 // sanity check that previous epoch is not populated in `stateEntry` 230 assert.Nil(t, stateEntry.PreviousEpoch) 231 assert.Nil(t, stateEntry.PreviousEpochSetup) 232 assert.Nil(t, stateEntry.PreviousEpochCommit) 233 234 richEntry, err := flow.NewRichProtocolStateEntry( 235 stateEntry.ProtocolStateEntry, 236 stateEntry.PreviousEpochSetup, 237 stateEntry.PreviousEpochCommit, 238 stateEntry.CurrentEpochSetup, 239 stateEntry.CurrentEpochCommit, 240 stateEntry.NextEpochSetup, 241 stateEntry.NextEpochCommit, 242 ) 243 assert.NoError(t, err) 244 assert.Equal(t, flow.EpochPhaseCommitted, richEntry.EpochPhase()) 245 246 expectedIdentities, err := flow.BuildIdentityTable( 247 stateEntry.CurrentEpochSetup.Participants, 248 stateEntry.CurrentEpoch.ActiveIdentities, 249 stateEntry.NextEpochSetup.Participants, 250 stateEntry.NextEpoch.ActiveIdentities, 251 flow.EpochParticipationStatusJoining, 252 ) 253 assert.NoError(t, err) 254 assert.Equal(t, expectedIdentities, richEntry.CurrentEpochIdentityTable, "should be equal to current epoch setup participants + next epoch setup participants") 255 expectedIdentities, err = flow.BuildIdentityTable( 256 stateEntry.NextEpochSetup.Participants, 257 stateEntry.NextEpoch.ActiveIdentities, 258 stateEntry.CurrentEpochSetup.Participants, 259 stateEntry.CurrentEpoch.ActiveIdentities, 260 flow.EpochParticipationStatusLeaving, 261 ) 262 assert.NoError(t, err) 263 assert.Equal(t, expectedIdentities, richEntry.NextEpochIdentityTable, "should be equal to next epoch setup participants + current epoch setup participants") 264 }) 265 } 266 267 // TestProtocolStateEntry_Copy tests if the copy method returns a deep copy of the entry. 268 // All changes to copy shouldn't affect the original entry -- except for key changes. 269 func TestProtocolStateEntry_Copy(t *testing.T) { 270 entry := unittest.EpochStateFixture(unittest.WithNextEpochProtocolState()).ProtocolStateEntry 271 cpy := entry.Copy() 272 assert.Equal(t, entry, cpy) 273 assert.NotSame(t, entry.NextEpoch, cpy.NextEpoch) 274 assert.NotSame(t, entry.PreviousEpoch, cpy.PreviousEpoch) 275 assert.NotSame(t, entry.CurrentEpoch, cpy.CurrentEpoch) 276 277 cpy.InvalidEpochTransitionAttempted = !entry.InvalidEpochTransitionAttempted 278 assert.NotEqual(t, entry, cpy) 279 280 assert.Equal(t, entry.CurrentEpoch.ActiveIdentities[0], cpy.CurrentEpoch.ActiveIdentities[0]) 281 cpy.CurrentEpoch.ActiveIdentities[0].Ejected = true 282 assert.NotEqual(t, entry.CurrentEpoch.ActiveIdentities[0], cpy.CurrentEpoch.ActiveIdentities[0]) 283 284 cpy.CurrentEpoch.ActiveIdentities = append(cpy.CurrentEpoch.ActiveIdentities, &flow.DynamicIdentityEntry{ 285 NodeID: unittest.IdentifierFixture(), 286 Ejected: false, 287 }) 288 assert.NotEqual(t, entry.CurrentEpoch.ActiveIdentities, cpy.CurrentEpoch.ActiveIdentities) 289 } 290 291 // TestBuildIdentityTable tests if BuildIdentityTable returns a correct identity, whenever we pass arguments with or without 292 // overlap. It also tests if the function returns an error when the arguments are not ordered in the same order. 293 func TestBuildIdentityTable(t *testing.T) { 294 t.Run("invalid-adjacent-identity-status", func(t *testing.T) { 295 targetEpochIdentities := unittest.IdentityListFixture(10).Sort(flow.Canonical[flow.Identity]) 296 adjacentEpochIdentities := unittest.IdentityListFixture(10).Sort(flow.Canonical[flow.Identity]) 297 298 // Per convention, BuildIdentityTable only accepts EpochParticipationStatusLeaving or EpochParticipationStatusJoining 299 // for the *adjacent* epoch, because these are the only sensible values. 300 for _, status := range []flow.EpochParticipationStatus{flow.EpochParticipationStatusActive, flow.EpochParticipationStatusEjected} { 301 identityList, err := flow.BuildIdentityTable( 302 targetEpochIdentities.ToSkeleton(), 303 flow.DynamicIdentityEntryListFromIdentities(targetEpochIdentities), 304 adjacentEpochIdentities.ToSkeleton(), 305 flow.DynamicIdentityEntryListFromIdentities(adjacentEpochIdentities), 306 status, 307 ) 308 assert.Error(t, err) 309 assert.Empty(t, identityList) 310 } 311 }) 312 t.Run("happy-path-no-identities-overlap", func(t *testing.T) { 313 targetEpochIdentities := unittest.IdentityListFixture(10).Sort(flow.Canonical[flow.Identity]) 314 adjacentEpochIdentities := unittest.IdentityListFixture(10).Sort(flow.Canonical[flow.Identity]) 315 316 identityList, err := flow.BuildIdentityTable( 317 targetEpochIdentities.ToSkeleton(), 318 flow.DynamicIdentityEntryListFromIdentities(targetEpochIdentities), 319 adjacentEpochIdentities.ToSkeleton(), 320 flow.DynamicIdentityEntryListFromIdentities(adjacentEpochIdentities), 321 flow.EpochParticipationStatusLeaving, 322 ) 323 assert.NoError(t, err) 324 325 expectedIdentities := targetEpochIdentities.Union(adjacentEpochIdentities.Map(func(identity flow.Identity) flow.Identity { 326 identity.EpochParticipationStatus = flow.EpochParticipationStatusLeaving 327 return identity 328 })) 329 assert.Equal(t, expectedIdentities, identityList) 330 }) 331 t.Run("happy-path-identities-overlap", func(t *testing.T) { 332 targetEpochIdentities := unittest.IdentityListFixture(10).Sort(flow.Canonical[flow.Identity]) 333 adjacentEpochIdentities := unittest.IdentityListFixture(10) 334 sampledIdentities, err := targetEpochIdentities.Sample(2) 335 // change address so we can assert that we take identities from target epoch and not adjacent epoch 336 for i, identity := range sampledIdentities.Copy() { 337 identity.Address = fmt.Sprintf("%d", i) 338 adjacentEpochIdentities = append(adjacentEpochIdentities, identity) 339 } 340 assert.NoError(t, err) 341 adjacentEpochIdentities = adjacentEpochIdentities.Sort(flow.Canonical[flow.Identity]) 342 343 identityList, err := flow.BuildIdentityTable( 344 targetEpochIdentities.ToSkeleton(), 345 flow.DynamicIdentityEntryListFromIdentities(targetEpochIdentities), 346 adjacentEpochIdentities.ToSkeleton(), 347 flow.DynamicIdentityEntryListFromIdentities(adjacentEpochIdentities), 348 flow.EpochParticipationStatusJoining, 349 ) 350 assert.NoError(t, err) 351 352 expectedIdentities := targetEpochIdentities.Union(adjacentEpochIdentities.Map(func(identity flow.Identity) flow.Identity { 353 identity.EpochParticipationStatus = flow.EpochParticipationStatusJoining 354 return identity 355 })) 356 assert.Equal(t, expectedIdentities, identityList) 357 }) 358 t.Run("target-epoch-identities-not-ordered", func(t *testing.T) { 359 targetEpochIdentities := unittest.IdentityListFixture(10).Sort(flow.Canonical[flow.Identity]) 360 targetEpochIdentitySkeletons, err := targetEpochIdentities.ToSkeleton().Shuffle() 361 assert.NoError(t, err) 362 targetEpochDynamicIdentities := flow.DynamicIdentityEntryListFromIdentities(targetEpochIdentities) 363 364 adjacentEpochIdentities := unittest.IdentityListFixture(10).Sort(flow.Canonical[flow.Identity]) 365 identityList, err := flow.BuildIdentityTable( 366 targetEpochIdentitySkeletons, 367 targetEpochDynamicIdentities, 368 adjacentEpochIdentities.ToSkeleton(), 369 flow.DynamicIdentityEntryListFromIdentities(adjacentEpochIdentities), 370 flow.EpochParticipationStatusLeaving, 371 ) 372 assert.Error(t, err) 373 assert.Empty(t, identityList) 374 }) 375 t.Run("adjacent-epoch-identities-not-ordered", func(t *testing.T) { 376 adjacentEpochIdentities := unittest.IdentityListFixture(10).Sort(flow.Canonical[flow.Identity]) 377 adjacentEpochIdentitySkeletons, err := adjacentEpochIdentities.ToSkeleton().Shuffle() 378 assert.NoError(t, err) 379 adjacentEpochDynamicIdentities := flow.DynamicIdentityEntryListFromIdentities(adjacentEpochIdentities) 380 381 targetEpochIdentities := unittest.IdentityListFixture(10).Sort(flow.Canonical[flow.Identity]) 382 identityList, err := flow.BuildIdentityTable( 383 targetEpochIdentities.ToSkeleton(), 384 flow.DynamicIdentityEntryListFromIdentities(targetEpochIdentities), 385 adjacentEpochIdentitySkeletons, 386 adjacentEpochDynamicIdentities, 387 flow.EpochParticipationStatusLeaving, 388 ) 389 assert.Error(t, err) 390 assert.Empty(t, identityList) 391 }) 392 }