github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/storage/badger/protocol_state_test.go (about)

     1  package badger
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/dgraph-io/badger/v2"
     7  	"github.com/stretchr/testify/assert"
     8  	"github.com/stretchr/testify/require"
     9  
    10  	"github.com/onflow/flow-go/model/flow"
    11  	"github.com/onflow/flow-go/model/flow/mapfunc"
    12  	"github.com/onflow/flow-go/module/metrics"
    13  	"github.com/onflow/flow-go/storage/badger/transaction"
    14  	"github.com/onflow/flow-go/utils/unittest"
    15  )
    16  
    17  // TestProtocolStateStorage tests if the protocol state is stored, retrieved and indexed correctly
    18  func TestProtocolStateStorage(t *testing.T) {
    19  	unittest.RunWithBadgerDB(t, func(db *badger.DB) {
    20  		metrics := metrics.NewNoopCollector()
    21  
    22  		setups := NewEpochSetups(metrics, db)
    23  		commits := NewEpochCommits(metrics, db)
    24  		store := NewProtocolState(metrics, setups, commits, db, DefaultProtocolStateCacheSize, DefaultProtocolStateByBlockIDCacheSize)
    25  
    26  		expected := unittest.EpochStateFixture(unittest.WithNextEpochProtocolState())
    27  		protocolStateID := expected.ID()
    28  		blockID := unittest.IdentifierFixture()
    29  
    30  		// store protocol state and auxiliary info
    31  		err := transaction.Update(db, func(tx *transaction.Tx) error {
    32  			// store epoch events to be able to retrieve them later
    33  			err := setups.StoreTx(expected.PreviousEpochSetup)(tx)
    34  			require.NoError(t, err)
    35  			err = setups.StoreTx(expected.CurrentEpochSetup)(tx)
    36  			require.NoError(t, err)
    37  			err = setups.StoreTx(expected.NextEpochSetup)(tx)
    38  			require.NoError(t, err)
    39  			err = commits.StoreTx(expected.PreviousEpochCommit)(tx)
    40  			require.NoError(t, err)
    41  			err = commits.StoreTx(expected.CurrentEpochCommit)(tx)
    42  			require.NoError(t, err)
    43  			err = commits.StoreTx(expected.NextEpochCommit)(tx)
    44  			require.NoError(t, err)
    45  
    46  			err = store.StoreTx(protocolStateID, expected.ProtocolStateEntry)(tx)
    47  			require.NoError(t, err)
    48  			return store.Index(blockID, protocolStateID)(tx)
    49  		})
    50  		require.NoError(t, err)
    51  
    52  		// fetch protocol state
    53  		actual, err := store.ByID(protocolStateID)
    54  		require.NoError(t, err)
    55  		require.Equal(t, expected, actual)
    56  
    57  		assertRichProtocolStateValidity(t, actual)
    58  
    59  		// fetch protocol state by block ID
    60  		actualByBlockID, err := store.ByBlockID(blockID)
    61  		require.NoError(t, err)
    62  		require.Equal(t, expected, actualByBlockID)
    63  
    64  		assertRichProtocolStateValidity(t, actualByBlockID)
    65  	})
    66  }
    67  
    68  // TestProtocolStateStoreInvalidProtocolState tests that storing protocol state which has unsorted identities fails for
    69  // current and next epoch protocol states.
    70  func TestProtocolStateStoreInvalidProtocolState(t *testing.T) {
    71  	unittest.RunWithBadgerDB(t, func(db *badger.DB) {
    72  		metrics := metrics.NewNoopCollector()
    73  		setups := NewEpochSetups(metrics, db)
    74  		commits := NewEpochCommits(metrics, db)
    75  		store := NewProtocolState(metrics, setups, commits, db, DefaultProtocolStateCacheSize, DefaultProtocolStateByBlockIDCacheSize)
    76  		invalid := unittest.EpochStateFixture().ProtocolStateEntry
    77  		// swap first and second elements to break canonical order
    78  		invalid.CurrentEpoch.ActiveIdentities[0], invalid.CurrentEpoch.ActiveIdentities[1] = invalid.CurrentEpoch.ActiveIdentities[1], invalid.CurrentEpoch.ActiveIdentities[0]
    79  
    80  		err := transaction.Update(db, store.StoreTx(invalid.ID(), invalid))
    81  		require.Error(t, err)
    82  
    83  		invalid = unittest.EpochStateFixture(unittest.WithNextEpochProtocolState()).ProtocolStateEntry
    84  		// swap first and second elements to break canonical order
    85  		invalid.NextEpoch.ActiveIdentities[0], invalid.NextEpoch.ActiveIdentities[1] = invalid.NextEpoch.ActiveIdentities[1], invalid.NextEpoch.ActiveIdentities[0]
    86  
    87  		err = transaction.Update(db, store.StoreTx(invalid.ID(), invalid))
    88  		require.Error(t, err)
    89  	})
    90  }
    91  
    92  // TestProtocolStateMergeParticipants tests that merging participants between epochs works correctly. We always take participants
    93  // from current epoch and additionally add participants from previous epoch if they are not present in current epoch.
    94  // If the same participant is in the previous and current epochs, we should see it only once in the merged list and the dynamic portion has to be from current epoch.
    95  func TestProtocolStateMergeParticipants(t *testing.T) {
    96  	unittest.RunWithBadgerDB(t, func(db *badger.DB) {
    97  		metrics := metrics.NewNoopCollector()
    98  
    99  		setups := NewEpochSetups(metrics, db)
   100  		commits := NewEpochCommits(metrics, db)
   101  		store := NewProtocolState(metrics, setups, commits, db, DefaultProtocolStateCacheSize, DefaultProtocolStateByBlockIDCacheSize)
   102  
   103  		stateEntry := unittest.EpochStateFixture()
   104  		// change address of participant in current epoch, so we can distinguish it from the one in previous epoch
   105  		// when performing assertion.
   106  		newAddress := "123"
   107  		nodeID := stateEntry.CurrentEpochSetup.Participants[1].NodeID
   108  		stateEntry.CurrentEpochSetup.Participants[1].Address = newAddress
   109  		stateEntry.CurrentEpoch.SetupID = stateEntry.CurrentEpochSetup.ID()
   110  		identity, _ := stateEntry.CurrentEpochIdentityTable.ByNodeID(nodeID)
   111  		identity.Address = newAddress
   112  		protocolStateID := stateEntry.ID()
   113  
   114  		// store protocol state and auxiliary info
   115  		err := transaction.Update(db, func(tx *transaction.Tx) error {
   116  			// store epoch events to be able to retrieve them later
   117  			err := setups.StoreTx(stateEntry.PreviousEpochSetup)(tx)
   118  			require.NoError(t, err)
   119  			err = setups.StoreTx(stateEntry.CurrentEpochSetup)(tx)
   120  			require.NoError(t, err)
   121  			err = commits.StoreTx(stateEntry.PreviousEpochCommit)(tx)
   122  			require.NoError(t, err)
   123  			err = commits.StoreTx(stateEntry.CurrentEpochCommit)(tx)
   124  			require.NoError(t, err)
   125  
   126  			return store.StoreTx(protocolStateID, stateEntry.ProtocolStateEntry)(tx)
   127  		})
   128  		require.NoError(t, err)
   129  
   130  		// fetch protocol state
   131  		actual, err := store.ByID(protocolStateID)
   132  		require.NoError(t, err)
   133  		require.Equal(t, stateEntry, actual)
   134  
   135  		assertRichProtocolStateValidity(t, actual)
   136  		identity, ok := actual.CurrentEpochIdentityTable.ByNodeID(nodeID)
   137  		require.True(t, ok)
   138  		require.Equal(t, newAddress, identity.Address)
   139  	})
   140  }
   141  
   142  // TestProtocolStateRootSnapshot tests that storing and retrieving root protocol state (in case of bootstrap) works as expected.
   143  // Specifically, this means that no prior epoch exists (situation after a spork) from the perspective of the freshly-sporked network.
   144  func TestProtocolStateRootSnapshot(t *testing.T) {
   145  	unittest.RunWithBadgerDB(t, func(db *badger.DB) {
   146  		metrics := metrics.NewNoopCollector()
   147  
   148  		setups := NewEpochSetups(metrics, db)
   149  		commits := NewEpochCommits(metrics, db)
   150  		store := NewProtocolState(metrics, setups, commits, db, DefaultProtocolStateCacheSize, DefaultProtocolStateByBlockIDCacheSize)
   151  		expected := unittest.RootProtocolStateFixture()
   152  
   153  		protocolStateID := expected.ID()
   154  		blockID := unittest.IdentifierFixture()
   155  
   156  		// store protocol state and auxiliary info
   157  		err := transaction.Update(db, func(tx *transaction.Tx) error {
   158  			// store epoch events to be able to retrieve them later
   159  			err := setups.StoreTx(expected.CurrentEpochSetup)(tx)
   160  			require.NoError(t, err)
   161  			err = commits.StoreTx(expected.CurrentEpochCommit)(tx)
   162  			require.NoError(t, err)
   163  
   164  			err = store.StoreTx(protocolStateID, expected.ProtocolStateEntry)(tx)
   165  			require.NoError(t, err)
   166  			return store.Index(blockID, protocolStateID)(tx)
   167  		})
   168  		require.NoError(t, err)
   169  
   170  		// fetch protocol state
   171  		actual, err := store.ByID(protocolStateID)
   172  		require.NoError(t, err)
   173  		require.Equal(t, expected, actual)
   174  
   175  		assertRichProtocolStateValidity(t, actual)
   176  
   177  		// fetch protocol state by block ID
   178  		actualByBlockID, err := store.ByBlockID(blockID)
   179  		require.NoError(t, err)
   180  		require.Equal(t, expected, actualByBlockID)
   181  
   182  		assertRichProtocolStateValidity(t, actualByBlockID)
   183  	})
   184  }
   185  
   186  // assertRichProtocolStateValidity checks if RichProtocolState holds its invariant and is correctly populated by storage layer.
   187  func assertRichProtocolStateValidity(t *testing.T, state *flow.RichProtocolStateEntry) {
   188  	// invariants:
   189  	//  - CurrentEpochSetup and CurrentEpochCommit are for the same epoch. Never nil.
   190  	//  - CurrentEpochSetup and CurrentEpochCommit IDs match respective commitments in the `ProtocolStateEntry`.
   191  	assert.Equal(t, state.CurrentEpochSetup.Counter, state.CurrentEpochCommit.Counter, "current epoch setup and commit should be for the same epoch")
   192  	assert.Equal(t, state.CurrentEpochSetup.ID(), state.ProtocolStateEntry.CurrentEpoch.SetupID, "epoch setup should be for correct event ID")
   193  	assert.Equal(t, state.CurrentEpochCommit.ID(), state.ProtocolStateEntry.CurrentEpoch.CommitID, "epoch commit should be for correct event ID")
   194  
   195  	var (
   196  		previousEpochParticipants flow.IdentityList
   197  		err                       error
   198  	)
   199  	// invariant: PreviousEpochSetup and PreviousEpochCommit should be present if respective ID is not zero.
   200  	if state.PreviousEpoch != nil {
   201  		// invariant: PreviousEpochSetup and PreviousEpochCommit are for the same epoch. Never nil.
   202  		assert.Equal(t, state.PreviousEpochSetup.Counter+1, state.CurrentEpochSetup.Counter, "current epoch (%d) should be following right after previous epoch (%d)", state.CurrentEpochSetup.Counter, state.PreviousEpochSetup.Counter)
   203  		assert.Equal(t, state.PreviousEpochSetup.Counter, state.PreviousEpochCommit.Counter, "previous epoch setup and commit should be for the same epoch")
   204  
   205  		// invariant: PreviousEpochSetup and PreviousEpochCommit IDs are the equal to the ID of the protocol state entry. Never nil.
   206  		assert.Equal(t, state.PreviousEpochSetup.ID(), state.ProtocolStateEntry.PreviousEpoch.SetupID, "epoch setup should be for correct event ID")
   207  		assert.Equal(t, state.PreviousEpochCommit.ID(), state.ProtocolStateEntry.PreviousEpoch.CommitID, "epoch commit should be for correct event ID")
   208  
   209  		// invariant: ComposeFullIdentities ensures that we can build full identities of previous epoch's active participants. This step also confirms that the
   210  		// previous epoch's `Participants` [IdentitySkeletons] and `ActiveIdentities` [DynamicIdentity properties] list the same nodes in canonical ordering.
   211  		previousEpochParticipants, err = flow.ComposeFullIdentities(
   212  			state.PreviousEpochSetup.Participants,
   213  			state.PreviousEpoch.ActiveIdentities,
   214  			flow.EpochParticipationStatusActive,
   215  		)
   216  		assert.NoError(t, err, "should be able to reconstruct previous epoch active participants")
   217  		// Function `ComposeFullIdentities` verified that `Participants` and `ActiveIdentities` have identical ordering w.r.t nodeID.
   218  		// By construction, `participantsFromCurrentEpochSetup` lists the full Identities in the same ordering as `Participants` and
   219  		// `ActiveIdentities`. By confirming that `participantsFromCurrentEpochSetup` follows canonical ordering, we can conclude that
   220  		// also `Participants` and `ActiveIdentities` are canonically ordered.
   221  		require.True(t, previousEpochParticipants.Sorted(flow.Canonical[flow.Identity]), "participants in previous epoch's setup event are not in canonical order")
   222  	}
   223  
   224  	// invariant: ComposeFullIdentities ensures that we can build full identities of current epoch's *active* participants. This step also confirms that the
   225  	// current epoch's `Participants` [IdentitySkeletons] and `ActiveIdentities` [DynamicIdentity properties] list the same nodes in canonical ordering.
   226  	participantsFromCurrentEpochSetup, err := flow.ComposeFullIdentities(
   227  		state.CurrentEpochSetup.Participants,
   228  		state.CurrentEpoch.ActiveIdentities,
   229  		flow.EpochParticipationStatusActive,
   230  	)
   231  	assert.NoError(t, err, "should be able to reconstruct current epoch active participants")
   232  	require.True(t, participantsFromCurrentEpochSetup.Sorted(flow.Canonical[flow.Identity]), "participants in current epoch's setup event are not in canonical order")
   233  
   234  	// invariants for `CurrentEpochIdentityTable`:
   235  	//  - full identity table containing *active* nodes for the current epoch + weight-zero identities of adjacent epoch
   236  	//  - Identities are sorted in canonical order. Without duplicates. Never nil.
   237  	var allIdentities, participantsFromNextEpochSetup flow.IdentityList
   238  	if state.NextEpoch != nil {
   239  		// setup/commit phase
   240  		// invariant: ComposeFullIdentities ensures that we can build full identities of next epoch's *active* participants. This step also confirms that the
   241  		// next epoch's `Participants` [IdentitySkeletons] and `ActiveIdentities` [DynamicIdentity properties] list the same nodes in canonical ordering.
   242  		participantsFromNextEpochSetup, err = flow.ComposeFullIdentities(
   243  			state.NextEpochSetup.Participants,
   244  			state.NextEpoch.ActiveIdentities,
   245  			flow.EpochParticipationStatusActive,
   246  		)
   247  		assert.NoError(t, err, "should be able to reconstruct next epoch active participants")
   248  		allIdentities = participantsFromCurrentEpochSetup.Union(participantsFromNextEpochSetup.Copy().Map(mapfunc.WithEpochParticipationStatus(flow.EpochParticipationStatusJoining)))
   249  	} else {
   250  		// staking phase
   251  		allIdentities = participantsFromCurrentEpochSetup.Union(previousEpochParticipants.Copy().Map(mapfunc.WithEpochParticipationStatus(flow.EpochParticipationStatusLeaving)))
   252  	}
   253  	assert.Equal(t, allIdentities, state.CurrentEpochIdentityTable, "identities should be a full identity table for the current epoch, without duplicates")
   254  	require.True(t, allIdentities.Sorted(flow.Canonical[flow.Identity]), "current epoch's identity table is not in canonical order")
   255  
   256  	// check next epoch; only applicable during setup/commit phase
   257  	if state.NextEpoch == nil { // during staking phase, next epoch is not yet specified; hence there is nothing else to check
   258  		return
   259  	}
   260  
   261  	// invariants:
   262  	//  - NextEpochSetup and NextEpochCommit are for the same epoch. Never nil.
   263  	//  - NextEpochSetup and NextEpochCommit IDs match respective commitments in the `ProtocolStateEntry`.
   264  	assert.Equal(t, state.CurrentEpochSetup.Counter+1, state.NextEpochSetup.Counter, "next epoch (%d) should be following right after current epoch (%d)", state.NextEpochSetup.Counter, state.CurrentEpochSetup.Counter)
   265  	assert.Equal(t, state.NextEpochSetup.Counter, state.NextEpochCommit.Counter, "next epoch setup and commit should be for the same epoch")
   266  	assert.Equal(t, state.NextEpochSetup.ID(), state.NextEpoch.SetupID, "epoch setup should be for correct event ID")
   267  	assert.Equal(t, state.NextEpochCommit.ID(), state.NextEpoch.CommitID, "epoch commit should be for correct event ID")
   268  
   269  	// invariants for `NextEpochIdentityTable`:
   270  	//  - full identity table containing *active* nodes for next epoch + weight-zero identities of current epoch
   271  	//  - Identities are sorted in canonical order. Without duplicates. Never nil.
   272  	allIdentities = participantsFromNextEpochSetup.Union(participantsFromCurrentEpochSetup.Copy().Map(mapfunc.WithEpochParticipationStatus(flow.EpochParticipationStatusLeaving)))
   273  	assert.Equal(t, allIdentities, state.NextEpochIdentityTable, "identities should be a full identity table for the next epoch, without duplicates")
   274  }