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  }