diff --git a/dot/rpc/modules/api.go b/dot/rpc/modules/api.go index 9abc40a4d8..e3daceb029 100644 --- a/dot/rpc/modules/api.go +++ b/dot/rpc/modules/api.go @@ -114,6 +114,8 @@ type RuntimeStorageAPI interface { SetPersistent(k, v []byte) error GetLocal(k []byte) ([]byte, error) GetPersistent(k []byte) ([]byte, error) + ClearLocal(k []byte) error + ClearPersistent(k []byte) error } // SyncStateAPI is the interface to interact with sync state. diff --git a/dot/rpc/modules/mocks/mocks.go b/dot/rpc/modules/mocks/mocks.go index f951c97339..616bd8532f 100644 --- a/dot/rpc/modules/mocks/mocks.go +++ b/dot/rpc/modules/mocks/mocks.go @@ -1033,6 +1033,34 @@ func (m *MockRuntimeStorageAPI) EXPECT() *MockRuntimeStorageAPIMockRecorder { return m.recorder } +// ClearLocal mocks base method. +func (m *MockRuntimeStorageAPI) ClearLocal(k []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClearLocal", k) + ret0, _ := ret[0].(error) + return ret0 +} + +// ClearLocal indicates an expected call of ClearLocal. +func (mr *MockRuntimeStorageAPIMockRecorder) ClearLocal(k any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearLocal", reflect.TypeOf((*MockRuntimeStorageAPI)(nil).ClearLocal), k) +} + +// ClearPersistent mocks base method. +func (m *MockRuntimeStorageAPI) ClearPersistent(k []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClearPersistent", k) + ret0, _ := ret[0].(error) + return ret0 +} + +// ClearPersistent indicates an expected call of ClearPersistent. +func (mr *MockRuntimeStorageAPIMockRecorder) ClearPersistent(k any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearPersistent", reflect.TypeOf((*MockRuntimeStorageAPI)(nil).ClearPersistent), k) +} + // GetLocal mocks base method. func (m *MockRuntimeStorageAPI) GetLocal(k []byte) ([]byte, error) { m.ctrl.T.Helper() diff --git a/dot/rpc/modules/offchain.go b/dot/rpc/modules/offchain.go index c6950d45ed..23e4f53033 100644 --- a/dot/rpc/modules/offchain.go +++ b/dot/rpc/modules/offchain.go @@ -15,6 +15,12 @@ const ( offchainLocal = "LOCAL" ) +// OffchainLocalStorageClear represents the request format to remove data from offchain storage +type OffchainLocalStorageClear struct { + Kind string + Key string +} + // OffchainLocalStorageGet represents the request format to retrieve data from offchain storage type OffchainLocalStorageGet struct { Kind string @@ -40,6 +46,33 @@ func NewOffchainModule(ns RuntimeStorageAPI) *OffchainModule { } } +// LocalStorageClear clear offchain local storage under given key and prefix +func (s *OffchainModule) LocalStorageClear(_ *http.Request, req *OffchainLocalStorageClear, _ *StringResponse) error { + var ( + key []byte + err error + ) + + if key, err = common.HexToBytes(req.Key); err != nil { + return err + } + + switch req.Kind { + case offchainPersistent: + err = s.nodeStorage.ClearPersistent(key) + case offchainLocal: + err = s.nodeStorage.ClearLocal(key) + default: + return fmt.Errorf("storage kind not found: %s", req.Kind) + } + + if err != nil { + return err + } + + return nil +} + // LocalStorageGet get offchain local storage under given key and prefix func (s *OffchainModule) LocalStorageGet(_ *http.Request, req *OffchainLocalStorageGet, res *StringResponse) error { var ( diff --git a/dot/rpc/modules/offchain_integration_test.go b/dot/rpc/modules/offchain_integration_test.go index 4b963eb805..ed973575ec 100644 --- a/dot/rpc/modules/offchain_integration_test.go +++ b/dot/rpc/modules/offchain_integration_test.go @@ -16,6 +16,71 @@ import ( "go.uber.org/mock/gomock" ) +func Test_OffchainModule_LocalStorageClear(t *testing.T) { + t.Run("clear_local_error", func(t *testing.T) { + ctrl := gomock.NewController(t) + + runtimeStorage := mocks.NewMockRuntimeStorageAPI(ctrl) + offchainModule := &OffchainModule{ + nodeStorage: runtimeStorage, + } + + const keyHex = "0x11111111111111" + request := &OffchainLocalStorageClear{ + Kind: offchainLocal, + Key: keyHex, + } + errTest := errors.New("test error") + runtimeStorage.EXPECT().ClearLocal(common.MustHexToBytes(keyHex)). + Return(errTest) + + err := offchainModule.LocalStorageClear(nil, request, nil) + assert.ErrorIs(t, err, errTest) + }) + + t.Run("local_kind", func(t *testing.T) { + ctrl := gomock.NewController(t) + + runtimeStorage := mocks.NewMockRuntimeStorageAPI(ctrl) + offchainModule := &OffchainModule{ + nodeStorage: runtimeStorage, + } + + const keyHex = "0x11111111111111" + request := &OffchainLocalStorageClear{ + Kind: offchainLocal, + Key: keyHex, + } + runtimeStorage.EXPECT().ClearLocal(common.MustHexToBytes(keyHex)). + Return(nil) + var response StringResponse + err := offchainModule.LocalStorageClear(nil, request, &response) + require.NoError(t, err) + assert.Empty(t, response) + }) + + t.Run("persistent_kind", func(t *testing.T) { + ctrl := gomock.NewController(t) + + runtimeStorage := mocks.NewMockRuntimeStorageAPI(ctrl) + offchainModule := &OffchainModule{ + nodeStorage: runtimeStorage, + } + + const keyHex = "0x11111111111111" + request := &OffchainLocalStorageClear{ + Kind: offchainPersistent, + Key: keyHex, + } + runtimeStorage.EXPECT().ClearPersistent(common.MustHexToBytes(keyHex)). + Return(nil) + var response StringResponse + err := offchainModule.LocalStorageClear(nil, request, &response) + require.NoError(t, err) + assert.Empty(t, response) + }) +} + func Test_OffchainModule_LocalStorageGet(t *testing.T) { t.Run("get_local_error", func(t *testing.T) { ctrl := gomock.NewController(t) @@ -94,11 +159,18 @@ func TestOffchainStorage_OtherKind(t *testing.T) { Kind: "another kind", Key: "0x11111111111111", } + clearReq := &OffchainLocalStorageClear{ + Kind: "another kind", + Key: "0x11111111111111", + } err := m.LocalStorageSet(nil, setReq, nil) require.Error(t, err, "storage kind not found: another kind") err = m.LocalStorageGet(nil, getReq, nil) require.Error(t, err, "storage kind not found: another kind") + + err = m.LocalStorageClear(nil, clearReq, nil) + require.Error(t, err, "storage kind not found: another kind") } func Test_OffchainModule_LocalStorageSet(t *testing.T) { diff --git a/dot/rpc/modules/offchain_test.go b/dot/rpc/modules/offchain_test.go index eaafc705b2..43d0ff9e79 100644 --- a/dot/rpc/modules/offchain_test.go +++ b/dot/rpc/modules/offchain_test.go @@ -16,6 +16,96 @@ import ( "github.com/stretchr/testify/assert" ) +func TestOffchainModule_LocalStorageClear(t *testing.T) { + ctrl := gomock.NewController(t) + + mockRuntimeStorageAPI := mocks.NewMockRuntimeStorageAPI(ctrl) + mockRuntimeStorageAPI.EXPECT().ClearPersistent(common.MustHexToBytes("0x11111111111111")). + Return(errors.New("ClearPersistent error")) + mockRuntimeStorageAPI.EXPECT().ClearLocal(common.MustHexToBytes("0x11111111111111")).Return(nil) + offChainModule := NewOffchainModule(mockRuntimeStorageAPI) + + type fields struct { + nodeStorage RuntimeStorageAPI + } + type args struct { + in0 *http.Request + req *OffchainLocalStorageClear + } + tests := []struct { + name string + fields fields + args args + expErr error + }{ + { + name: "ClearPersistent_error", + fields: fields{ + offChainModule.nodeStorage, + }, + args: args{ + req: &OffchainLocalStorageClear{ + Kind: offchainPersistent, + Key: "0x11111111111111", + }, + }, + expErr: errors.New("ClearPersistent error"), + }, + { + name: "Invalid_Storage_Kind", + fields: fields{ + offChainModule.nodeStorage, + }, + args: args{ + req: &OffchainLocalStorageClear{ + Kind: "invalid kind", + Key: "0x11111111111111", + }, + }, + expErr: fmt.Errorf("storage kind not found: invalid kind"), + }, + { + name: "ClearLocal_OK", + fields: fields{ + offChainModule.nodeStorage, + }, + args: args{ + req: &OffchainLocalStorageClear{ + Kind: offchainLocal, + Key: "0x11111111111111", + }, + }, + }, + { + name: "Invalid_key", + fields: fields{ + offChainModule.nodeStorage, + }, + args: args{ + req: &OffchainLocalStorageClear{ + Kind: offchainLocal, + Key: "0x1", + }, + }, + expErr: errors.New("encoding/hex: odd length hex string: 0x1"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &OffchainModule{ + nodeStorage: tt.fields.nodeStorage, + } + res := StringResponse("") + err := s.LocalStorageClear(tt.args.in0, tt.args.req, &res) + if tt.expErr != nil { + assert.EqualError(t, err, tt.expErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + func TestOffchainModule_LocalStorageGet(t *testing.T) { ctrl := gomock.NewController(t) diff --git a/lib/runtime/types.go b/lib/runtime/types.go index b7c2fb4e52..d7d1a03b44 100644 --- a/lib/runtime/types.go +++ b/lib/runtime/types.go @@ -37,6 +37,11 @@ func (n *NodeStorage) GetLocal(k []byte) ([]byte, error) { return n.LocalStorage.Get(k) } +// ClearLocal removes a key and value from LOCAL node storage +func (n *NodeStorage) ClearLocal(k []byte) error { + return n.LocalStorage.Del(k) +} + // SetPersistent persists a key and value into PERSISTENT node storage func (n *NodeStorage) SetPersistent(k, v []byte) error { return n.PersistentStorage.Put(k, v) @@ -47,6 +52,11 @@ func (n *NodeStorage) GetPersistent(k []byte) ([]byte, error) { return n.PersistentStorage.Get(k) } +// ClearPersistent removes a key and value from PERSISTENT node storage +func (n *NodeStorage) ClearPersistent(k []byte) error { + return n.PersistentStorage.Del(k) +} + type Allocator interface { Allocate(mem Memory, size uint32) (uint32, error) Deallocate(mem Memory, ptr uint32) error diff --git a/tests/rpc/rpc_04-offchain_test.go b/tests/rpc/rpc_04-offchain_test.go index 43d6e7db9b..33b848a78c 100644 --- a/tests/rpc/rpc_04-offchain_test.go +++ b/tests/rpc/rpc_04-offchain_test.go @@ -13,8 +13,6 @@ import ( ) func TestOffchainRPC(t *testing.T) { //nolint:tparallel - t.SkipNow() // TODO - genesisPath := libutils.GetWestendDevRawGenesisPath(t) tomlConfig := config.Default() tomlConfig.ChainSpec = genesisPath @@ -22,33 +20,18 @@ func TestOffchainRPC(t *testing.T) { //nolint:tparallel ctx, cancel := context.WithCancel(context.Background()) node.InitAndStartTest(ctx, t, cancel) - t.Run("offchain_localStorageSet", func(t *testing.T) { - t.Parallel() - - var response struct{} // TODO - - fetchWithTimeout(ctx, t, "offchain_localStorageSet", "", &response) + // t.Run("offchain_localStorageSet", ...) // TODO + // t.Run("offchain_localStorageGet", ...) // TODO - // TODO assert response - }) - - t.Run("offchain_localStorageGet", func(t *testing.T) { + t.Run("offchain_localStorageClear", func(t *testing.T) { t.Parallel() - var response struct{} // TODO - - fetchWithTimeout(ctx, t, "offchain_localStorageGet", "", &response) - - // TODO assert response - }) - - t.Run("offchain_localStorageGet", func(t *testing.T) { - t.Parallel() - - var response struct{} // TODO - - fetchWithTimeout(ctx, t, "offchain_localStorageGet", "", &response) + var setResponse any + fetchWithTimeout(ctx, t, "offchain_localStorageSet", + `["PERSISTENT", "0x11111111111111", "0x22222222222222"]`, &setResponse) - // TODO assert response + var clearResponse any + fetchWithTimeout(ctx, t, "offchain_localStorageClear", + `["PERSISTENT", "0x11111111111111"]`, &clearResponse) }) }