github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/state/protocol/protocol_state/kvstore/kvstore_storage_test.go (about) 1 package kvstore_test 2 3 import ( 4 "errors" 5 "math" 6 "testing" 7 8 "github.com/stretchr/testify/mock" 9 "github.com/stretchr/testify/require" 10 11 "github.com/onflow/flow-go/model/flow" 12 "github.com/onflow/flow-go/state/protocol/protocol_state/kvstore" 13 protocol_statemock "github.com/onflow/flow-go/state/protocol/protocol_state/mock" 14 "github.com/onflow/flow-go/storage/badger/transaction" 15 storagemock "github.com/onflow/flow-go/storage/mock" 16 "github.com/onflow/flow-go/utils/unittest" 17 ) 18 19 // TestProtocolKVStore_StoreTx verifies correct functioning of `ProtocolKVStore.StoreTx`. In a nutshell, 20 // `ProtocolKVStore` should encode the provided snapshot and call the lower-level storage abstraction 21 // to persist the encoded result. 22 func TestProtocolKVStore_StoreTx(t *testing.T) { 23 llStorage := storagemock.NewProtocolKVStore(t) // low-level storage of versioned binary Protocol State snapshots 24 kvState := protocol_statemock.NewKVStoreAPI(t) // instance of key-value store, which we want to persist 25 kvStateID := unittest.IdentifierFixture() 26 27 store := kvstore.NewProtocolKVStore(llStorage) // instance that we are testing 28 29 // On the happy path, where the input `kvState` encodes its state successfully, the wrapped store 30 // should be called to persist the version-encoded snapshot. 31 t.Run("happy path", func(t *testing.T) { 32 expectedVersion := uint64(13) 33 encData := unittest.RandomBytes(117) 34 versionedSnapshot := &flow.PSKeyValueStoreData{ 35 Version: expectedVersion, 36 Data: encData, 37 } 38 kvState.On("VersionedEncode").Return(expectedVersion, encData, nil).Once() 39 40 deferredUpdate := storagemock.NewDeferredDBUpdate(t) 41 deferredUpdate.On("Execute", mock.Anything).Return(nil).Once() 42 llStorage.On("StoreTx", kvStateID, versionedSnapshot).Return(deferredUpdate.Execute).Once() 43 44 // Calling `StoreTx` should return the output of the wrapped low-level storage, which is a deferred database 45 // update. Conceptually, it is possible that `ProtocolKVStore` wraps the deferred database operation in faulty 46 // code, such that it cannot be executed. Therefore, we execute the top-level deferred database update below 47 // and verify that the deferred database operation returned by the lower-level is actually reached. 48 dbUpdate := store.StoreTx(kvStateID, kvState) 49 err := dbUpdate(&transaction.Tx{}) 50 require.NoError(t, err) 51 }) 52 53 // On the unhappy path, i.e. when the encoding of input `kvState` failed, `ProtocolKVStore` should produce 54 // a deferred database update that always returns the encoding error. 55 t.Run("encoding fails", func(t *testing.T) { 56 encodingError := errors.New("encoding error") 57 kvState.On("VersionedEncode").Return(uint64(0), nil, encodingError).Once() 58 59 dbUpdate := store.StoreTx(kvStateID, kvState) 60 err := dbUpdate(&transaction.Tx{}) 61 require.ErrorIs(t, err, encodingError) 62 }) 63 } 64 65 // TestProtocolKVStore_IndexTx verifies that `ProtocolKVStore.IndexTx` delegate all calls directly to the 66 // low-level storage abstraction. 67 func TestProtocolKVStore_IndexTx(t *testing.T) { 68 blockID := unittest.IdentifierFixture() 69 stateID := unittest.IdentifierFixture() 70 llStorage := storagemock.NewProtocolKVStore(t) // low-level storage of versioned binary Protocol State snapshots 71 72 store := kvstore.NewProtocolKVStore(llStorage) // instance that we are testing 73 74 // should be called to persist the version-encoded snapshot. 75 t.Run("happy path", func(t *testing.T) { 76 deferredUpdate := storagemock.NewDeferredDBUpdate(t) 77 deferredUpdate.On("Execute", mock.Anything).Return(nil).Once() 78 llStorage.On("IndexTx", blockID, stateID).Return(deferredUpdate.Execute).Once() 79 80 // Calling `IndexTx` should return the output of the wrapped low-level storage, which is a deferred database 81 // update. Conceptually, it is possible that `ProtocolKVStore` wraps the deferred database operation in faulty 82 // code, such that it cannot be executed. Therefore, we execute the top-level deferred database update below 83 // and verify that the deferred database operation returned by the lower-level is actually reached. 84 dbUpdate := store.IndexTx(blockID, stateID) 85 err := dbUpdate(&transaction.Tx{}) 86 require.NoError(t, err) 87 }) 88 89 // On the unhappy path, the deferred database update from the lower level just errors upon execution. 90 // This error should be escalated. 91 t.Run("unhappy path", func(t *testing.T) { 92 indexingError := errors.New("indexing error") 93 deferredUpdate := storagemock.NewDeferredDBUpdate(t) 94 deferredUpdate.On("Execute", mock.Anything).Return(indexingError).Once() 95 llStorage.On("IndexTx", blockID, stateID).Return(deferredUpdate.Execute).Once() 96 97 dbUpdate := store.IndexTx(blockID, stateID) 98 err := dbUpdate(&transaction.Tx{}) 99 require.ErrorIs(t, err, indexingError) 100 }) 101 } 102 103 // TestProtocolKVStore_ByBlockID verifies correct functioning of `ProtocolKVStore.ByBlockID`. In a nutshell, 104 // `ProtocolKVStore` should attempt to retrieve the encoded snapshot from the lower-level storage abstraction 105 // and return the decoded result. 106 func TestProtocolKVStore_ByBlockID(t *testing.T) { 107 blockID := unittest.IdentifierFixture() 108 llStorage := storagemock.NewProtocolKVStore(t) // low-level storage of versioned binary Protocol State snapshots 109 110 store := kvstore.NewProtocolKVStore(llStorage) // instance that we are testing 111 112 // On the happy path, `ProtocolKVStore` should decode the snapshot retrieved by the lowe-level storage abstraction. 113 // should be called to persist the version-encoded snapshot. 114 t.Run("happy path", func(t *testing.T) { 115 expectedState := &kvstore.Modelv1{ 116 Modelv0: kvstore.Modelv0{ 117 UpgradableModel: kvstore.UpgradableModel{}, 118 EpochStateID: unittest.IdentifierFixture(), 119 }, 120 } 121 version, encStateData, err := expectedState.VersionedEncode() 122 require.NoError(t, err) 123 encExpectedState := &flow.PSKeyValueStoreData{ 124 Version: version, 125 Data: encStateData, 126 } 127 llStorage.On("ByBlockID", blockID).Return(encExpectedState, nil).Once() 128 129 decodedState, err := store.ByBlockID(blockID) 130 require.NoError(t, err) 131 require.Equal(t, expectedState, decodedState) 132 }) 133 134 // On the unhappy path, either `ProtocolKVStore.ByBlockID` could error, or the decoding could fail. In either case, 135 // the error should be escalated to the caller. 136 t.Run("low-level `ProtocolKVStore.ByBlockID` errors", func(t *testing.T) { 137 someError := errors.New("some problem") 138 llStorage.On("ByBlockID", blockID).Return(nil, someError).Once() 139 140 _, err := store.ByBlockID(blockID) 141 require.ErrorIs(t, err, someError) 142 }) 143 t.Run("decoding fails with `ErrUnsupportedVersion`", func(t *testing.T) { 144 versionedSnapshot := &flow.PSKeyValueStoreData{ 145 Version: math.MaxUint64, 146 Data: unittest.RandomBytes(117), 147 } 148 llStorage.On("ByBlockID", blockID).Return(versionedSnapshot, nil).Once() 149 150 _, err := store.ByBlockID(blockID) 151 require.ErrorIs(t, err, kvstore.ErrUnsupportedVersion) 152 }) 153 t.Run("decoding yields exception", func(t *testing.T) { 154 versionedSnapshot := &flow.PSKeyValueStoreData{ 155 Version: 1, // model version 1 is known, but data is random, which should yield an `irrecoverable.Exception` 156 Data: unittest.RandomBytes(117), 157 } 158 llStorage.On("ByBlockID", blockID).Return(versionedSnapshot, nil).Once() 159 160 _, err := store.ByBlockID(blockID) 161 require.NotErrorIs(t, err, kvstore.ErrUnsupportedVersion) 162 }) 163 } 164 165 // TestProtocolKVStore_ByID verifies correct functioning of `ProtocolKVStore.ByID`. In a nutshell, 166 // `ProtocolKVStore` should attempt to retrieve the encoded snapshot from the lower-level storage 167 // abstraction and return the decoded result. 168 func TestProtocolKVStore_ByID(t *testing.T) { 169 protocolStateID := unittest.IdentifierFixture() 170 llStorage := storagemock.NewProtocolKVStore(t) // low-level storage of versioned binary Protocol State snapshots 171 172 store := kvstore.NewProtocolKVStore(llStorage) // instance that we are testing 173 174 // On the happy path, `ProtocolKVStore` should decode the snapshot retrieved by the lowe-level storage abstraction. 175 // should be called to persist the version-encoded snapshot. 176 t.Run("happy path", func(t *testing.T) { 177 expectedState := &kvstore.Modelv1{ 178 Modelv0: kvstore.Modelv0{ 179 UpgradableModel: kvstore.UpgradableModel{}, 180 EpochStateID: unittest.IdentifierFixture(), 181 }, 182 } 183 version, encStateData, err := expectedState.VersionedEncode() 184 require.NoError(t, err) 185 encExpectedState := &flow.PSKeyValueStoreData{ 186 Version: version, 187 Data: encStateData, 188 } 189 llStorage.On("ByID", protocolStateID).Return(encExpectedState, nil).Once() 190 191 decodedState, err := store.ByID(protocolStateID) 192 require.NoError(t, err) 193 require.Equal(t, expectedState, decodedState) 194 }) 195 196 // On the unhappy path, either `ProtocolKVStore.ByID` could error, or the decoding could fail. In either case, 197 // the error should be escalated to the caller. 198 t.Run("low-level `ProtocolKVStore.ByID` errors", func(t *testing.T) { 199 someError := errors.New("some problem") 200 llStorage.On("ByID", protocolStateID).Return(nil, someError).Once() 201 202 _, err := store.ByID(protocolStateID) 203 require.ErrorIs(t, err, someError) 204 }) 205 t.Run("decoding fails with `ErrUnsupportedVersion`", func(t *testing.T) { 206 versionedSnapshot := &flow.PSKeyValueStoreData{ 207 Version: math.MaxUint64, 208 Data: unittest.RandomBytes(117), 209 } 210 llStorage.On("ByID", protocolStateID).Return(versionedSnapshot, nil).Once() 211 212 _, err := store.ByID(protocolStateID) 213 require.ErrorIs(t, err, kvstore.ErrUnsupportedVersion) 214 }) 215 t.Run("decoding yields exception", func(t *testing.T) { 216 versionedSnapshot := &flow.PSKeyValueStoreData{ 217 Version: 1, // model version 1 is known, but data is random, which should yield an `irrecoverable.Exception` 218 Data: unittest.RandomBytes(117), 219 } 220 llStorage.On("ByID", protocolStateID).Return(versionedSnapshot, nil).Once() 221 222 _, err := store.ByID(protocolStateID) 223 require.NotErrorIs(t, err, kvstore.ErrUnsupportedVersion) 224 }) 225 }