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  }