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 }