diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index 8af451c60..0a7215558 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -559,6 +559,26 @@ func (c *Client) getStorage(params []any) ([]byte, error) { return resp, nil } +// GetStorageByIDHistoric returns the historical stored value according to the +// contract ID and, stored key and specified stateroot. +func (c *Client) GetStorageByIDHistoric(root util.Uint256, id int32, key []byte) ([]byte, error) { + return c.getStorageHistoric([]any{root.StringLE(), id, key}) +} + +// GetStorageByHashHistoric returns the historical stored value according to the +// contract script hash, the stored key and specified stateroot. +func (c *Client) GetStorageByHashHistoric(root util.Uint256, hash util.Uint160, key []byte) ([]byte, error) { + return c.getStorageHistoric([]any{root.StringLE(), hash.StringLE(), key}) +} + +func (c *Client) getStorageHistoric(params []any) ([]byte, error) { + var resp []byte + if err := c.performRequest("getstoragehistoric", params, &resp); err != nil { + return nil, err + } + return resp, nil +} + // FindStorageByHash returns contract storage items by the given contract hash and prefix. // If `start` index is specified, items starting from `start` index are being returned // (including item located at the start index). diff --git a/pkg/rpcclient/rpc_test.go b/pkg/rpcclient/rpc_test.go index 1a7cdbea2..5562a21ac 100644 --- a/pkg/rpcclient/rpc_test.go +++ b/pkg/rpcclient/rpc_test.go @@ -1040,6 +1040,50 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ }, }, }, + "getstoragehistoric": { + { + name: "by hash, positive", + invoke: func(c *Client) (any, error) { + root, _ := util.Uint256DecodeStringLE("252e9d73d49c95c7618d40650da504e05183a1b2eed0685e42c360413c329170") + hash, err := util.Uint160DecodeStringLE("03febccf81ac85e3d795bc5cbd4e84e907812aa3") + if err != nil { + panic(err) + } + key, err := hex.DecodeString("5065746572") + if err != nil { + panic(err) + } + return c.GetStorageByHashHistoric(root, hash, key) + }, + serverResponse: `{"jsonrpc":"2.0","id":1,"result":"TGlu"}`, + result: func(c *Client) any { + value, err := hex.DecodeString("4c696e") + if err != nil { + panic(err) + } + return value + }, + }, + { + name: "by ID, positive", + invoke: func(c *Client) (any, error) { + root, _ := util.Uint256DecodeStringLE("252e9d73d49c95c7618d40650da504e05183a1b2eed0685e42c360413c329170") + key, err := hex.DecodeString("5065746572") + if err != nil { + panic(err) + } + return c.GetStorageByIDHistoric(root, -1, key) + }, + serverResponse: `{"jsonrpc":"2.0","id":1,"result":"TGlu"}`, + result: func(c *Client) any { + value, err := hex.DecodeString("4c696e") + if err != nil { + panic(err) + } + return value + }, + }, + }, "gettransactionheight": { { name: "positive", diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index 2bbef0d5a..0e3b0dd6d 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -2690,3 +2690,36 @@ func TestClient_FindStorageHistoric(t *testing.T) { require.NoError(t, err) require.Equal(t, result.FindStorage{}, actual) } + +func TestClient_GetStorageHistoric(t *testing.T) { + chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) + defer chain.Close() + defer rpcSrv.Shutdown() + + c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) + require.NoError(t, err) + require.NoError(t, c.Init()) + + root, err := util.Uint256DecodeStringLE(block20StateRootLE) + require.NoError(t, err) + h, err := util.Uint160DecodeStringLE(testContractHash) + require.NoError(t, err) + key := []byte("aa10") + expected := []byte("v2") + + // By hash. + actual, err := c.GetStorageByHashHistoric(root, h, key) + require.NoError(t, err) + require.Equal(t, expected, actual) + + // By ID. + actual, err = c.GetStorageByIDHistoric(root, 1, key) // Rubles contract + require.NoError(t, err) + require.Equal(t, expected, actual) + + // Missing item. + earlyRoot, err := chain.GetStateRoot(15) // there's no `aa10` value in Rubles contract by the moment of block #15 + require.NoError(t, err) + _, err = c.GetStorageByHashHistoric(earlyRoot.Root, h, key) + require.ErrorIs(t, neorpc.ErrUnknownStorageItem, err) +} diff --git a/pkg/services/rpcsrv/server.go b/pkg/services/rpcsrv/server.go index 865c963cd..5c936e7d9 100644 --- a/pkg/services/rpcsrv/server.go +++ b/pkg/services/rpcsrv/server.go @@ -229,6 +229,7 @@ var rpcHandlers = map[string]func(*Server, params.Params) (any, *neorpc.Error){ "getstateheight": (*Server).getStateHeight, "getstateroot": (*Server).getStateRoot, "getstorage": (*Server).getStorage, + "getstoragehistoric": (*Server).getStorageHistoric, "gettransactionheight": (*Server).getTransactionHeight, "getunclaimedgas": (*Server).getUnclaimedGas, "getnextblockvalidators": (*Server).getNextBlockValidators, @@ -1855,6 +1856,36 @@ func (s *Server) getStorage(ps params.Params) (any, *neorpc.Error) { return []byte(item), nil } +func (s *Server) getStorageHistoric(ps params.Params) (any, *neorpc.Error) { + root, respErr := s.getStateRootFromParam(ps.Value(0)) + if respErr != nil { + return nil, respErr + } + if len(ps) < 2 { + return nil, neorpc.ErrInvalidParams + } + + id, rErr := s.contractIDFromParam(ps.Value(1), root) + if rErr != nil { + return nil, rErr + } + key, err := ps.Value(2).GetBytesBase64() + if err != nil { + return nil, neorpc.ErrInvalidParams + } + pKey := makeStorageKey(id, key) + + v, err := s.chain.GetStateModule().GetState(root, pKey) + if err != nil && !errors.Is(err, mpt.ErrNotFound) { + return nil, neorpc.NewInternalServerError(fmt.Sprintf("failed to get state item: %s", err)) + } + if v == nil { + return "", neorpc.ErrUnknownStorageItem + } + + return v, nil +} + func (s *Server) getrawtransaction(reqParams params.Params) (any, *neorpc.Error) { txHash, err := reqParams.Value(0).GetUint256() if err != nil { diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index 98ed281bf..b85640d25 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -659,6 +659,58 @@ var rpcTestCases = map[string][]rpcTestCase{ errCode: neorpc.InvalidParamsCode, }, }, + "getstoragehistoric": { + { + name: "positive", + params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa10"))), + result: func(e *executor) any { + v := base64.StdEncoding.EncodeToString([]byte("v2")) + return &v + }, + }, + { + name: "missing key", + params: fmt.Sprintf(`["%s", "%s", "dGU="]`, block20StateRootLE, testContractHash), + fail: true, + errCode: neorpc.ErrUnknownStorageItemCode, + }, + { + name: "no params", + params: `[]`, + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "no second parameter", + params: fmt.Sprintf(`["%s"]`, block20StateRootLE), + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "no third parameter", + params: fmt.Sprintf(`["%s", "%s"]`, block20StateRootLE, testContractHash), + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "invalid stateroot", + params: `["notahex"]`, + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "invalid hash", + params: fmt.Sprintf(`["%s", "notahex"]`, block20StateRootLE), + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "invalid key", + params: fmt.Sprintf(`["%s", "%s", "notabase64$"]`, block20StateRootLE, testContractHash), + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + }, "findstorage": { { name: "not truncated",