diff --git a/config/protocol.unit_testnet.yml b/config/protocol.unit_testnet.yml index 51def919e..0349b2064 100644 --- a/config/protocol.unit_testnet.yml +++ b/config/protocol.unit_testnet.yml @@ -66,6 +66,7 @@ ApplicationConfiguration: EnableCORSWorkaround: false SessionEnabled: true SessionExpirationTime: 2 # enough for tests as they run locally. + MaxFindStoragePageSize: 2 # small value to test server-side paging Prometheus: Enabled: false #since it's not useful for unit tests. Addresses: diff --git a/docs/node-configuration.md b/docs/node-configuration.md index 6ff7d8e47..77baa9a3a 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -202,6 +202,7 @@ RPC: MaxGasInvoke: 50 MaxIteratorResultItems: 100 MaxFindResultItems: 100 + MaxFindStoragePageSize: 50 MaxNEP11Tokens: 100 MaxWebSocketClients: 64 SessionEnabled: false @@ -238,6 +239,7 @@ where: `n`, only `n` iterations are returned and truncated is true, indicating that there is still data to be returned. - `MaxFindResultItems` - the maximum number of elements for `findstates` response. +- `MaxFindStoragePageSize` - the maximum number of elements for `findstorage` response per single page. - `MaxNEP11Tokens` - limit for the number of tokens returned from `getnep11balances` call. - `MaxWebSocketClients` - the maximum simultaneous websocket client connection diff --git a/pkg/config/config.go b/pkg/config/config.go index 93cc4ca43..ede5b4102 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -19,6 +19,9 @@ const ( // DefaultMaxIteratorResultItems is the default upper bound of traversed // iterator items per JSON-RPC response. DefaultMaxIteratorResultItems = 100 + // DefaultMaxFindStorageResultItems is the default maximum number of resulting + // contract storage items that can be retrieved by `findstorge` JSON-RPC handler. + DefaultMaxFindStorageResultItems = 50 ) // Version is the version of the node, set at the build time. @@ -71,9 +74,10 @@ func LoadFile(configPath string) (Config, error) { PingTimeout: 90 * time.Second, }, RPC: RPC{ - MaxIteratorResultItems: DefaultMaxIteratorResultItems, - MaxFindResultItems: 100, - MaxNEP11Tokens: 100, + MaxIteratorResultItems: DefaultMaxIteratorResultItems, + MaxFindResultItems: 100, + MaxFindStorageResultItems: DefaultMaxFindStorageResultItems, + MaxNEP11Tokens: 100, }, }, } diff --git a/pkg/config/rpc_config.go b/pkg/config/rpc_config.go index 44df2aa38..bd9d0acdd 100644 --- a/pkg/config/rpc_config.go +++ b/pkg/config/rpc_config.go @@ -11,17 +11,18 @@ type ( EnableCORSWorkaround bool `yaml:"EnableCORSWorkaround"` // MaxGasInvoke is the maximum amount of GAS which // can be spent during an RPC call. - MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"` - MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"` - MaxFindResultItems int `yaml:"MaxFindResultItems"` - MaxNEP11Tokens int `yaml:"MaxNEP11Tokens"` - MaxWebSocketClients int `yaml:"MaxWebSocketClients"` - SessionEnabled bool `yaml:"SessionEnabled"` - SessionExpirationTime int `yaml:"SessionExpirationTime"` - SessionBackedByMPT bool `yaml:"SessionBackedByMPT"` - SessionPoolSize int `yaml:"SessionPoolSize"` - StartWhenSynchronized bool `yaml:"StartWhenSynchronized"` - TLSConfig TLS `yaml:"TLSConfig"` + MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"` + MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"` + MaxFindResultItems int `yaml:"MaxFindResultItems"` + MaxFindStorageResultItems int `yaml:"MaxFindStoragePageSize"` + MaxNEP11Tokens int `yaml:"MaxNEP11Tokens"` + MaxWebSocketClients int `yaml:"MaxWebSocketClients"` + SessionEnabled bool `yaml:"SessionEnabled"` + SessionExpirationTime int `yaml:"SessionExpirationTime"` + SessionBackedByMPT bool `yaml:"SessionBackedByMPT"` + SessionPoolSize int `yaml:"SessionPoolSize"` + StartWhenSynchronized bool `yaml:"StartWhenSynchronized"` + TLSConfig TLS `yaml:"TLSConfig"` } // TLS describes SSL/TLS configuration. diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index e2327de60..6e71555a2 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -2133,6 +2133,11 @@ func (bc *Blockchain) GetStorageItem(id int32, key []byte) state.StorageItem { return bc.dao.GetStorageItem(id, key) } +// SeekStorage performs seek operation over contract storage. +func (bc *Blockchain) SeekStorage(id int32, prefix []byte, cont func(k, v []byte) bool) { + bc.dao.Seek(id, storage.SeekRange{Prefix: prefix}, cont) +} + // GetBlock returns a Block by the given hash. func (bc *Blockchain) GetBlock(hash util.Uint256) (*block.Block, error) { topBlock := bc.topBlock.Load() diff --git a/pkg/neorpc/result/findstorage.go b/pkg/neorpc/result/findstorage.go new file mode 100644 index 000000000..ad3bfe61f --- /dev/null +++ b/pkg/neorpc/result/findstorage.go @@ -0,0 +1,10 @@ +package result + +// FindStorage represents the result of `findstorage` RPC handler. +type FindStorage struct { + Results []KeyValue `json:"results"` + // Next contains the index of the next subsequent element of the contract storage + // that can be retrieved during the next iteration. + Next int `json:"next"` + Truncated bool `json:"truncated"` +} diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index 07cfbe077..8af451c60 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -559,6 +559,74 @@ func (c *Client) getStorage(params []any) ([]byte, error) { 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). +func (c *Client) FindStorageByHash(contractHash util.Uint160, prefix []byte, start *int) (result.FindStorage, error) { + var params = []any{contractHash.StringLE(), prefix} + if start != nil { + params = append(params, *start) + } + return c.findStorage(params) +} + +// FindStorageByID returns contract storage items by the given contract ID and prefix. +// If `start` index is specified, items starting from `start` index are being returned +// (including item located at the start index). +func (c *Client) FindStorageByID(contractID int32, prefix []byte, start *int) (result.FindStorage, error) { + var params = []any{contractID, prefix} + if start != nil { + params = append(params, *start) + } + return c.findStorage(params) +} + +func (c *Client) findStorage(params []any) (result.FindStorage, error) { + var resp result.FindStorage + if err := c.performRequest("findstorage", params, &resp); err != nil { + return resp, err + } + return resp, nil +} + +// FindStorageByHashHistoric returns historical contract storage items by the given stateroot, +// historical contract hash and historical prefix. If `start` index is specified, then items +// starting from `start` index are being returned (including item located at the start index). +func (c *Client) FindStorageByHashHistoric(stateroot util.Uint256, historicalContractHash util.Uint160, historicalPrefix []byte, + start *int) (result.FindStorage, error) { + if historicalPrefix == nil { + historicalPrefix = []byte{} + } + var params = []any{stateroot.StringLE(), historicalContractHash.StringLE(), historicalPrefix} + if start != nil { + params = append(params, start) + } + return c.findStorageHistoric(params) +} + +// FindStorageByIDHistoric returns historical contract storage items by the given stateroot, +// historical contract ID and historical prefix. If `start` index is specified, then items +// starting from `start` index are being returned (including item located at the start index). +func (c *Client) FindStorageByIDHistoric(stateroot util.Uint256, historicalContractID int32, historicalPrefix []byte, + start *int) (result.FindStorage, error) { + if historicalPrefix == nil { + historicalPrefix = []byte{} + } + var params = []any{stateroot.StringLE(), historicalContractID, historicalPrefix} + if start != nil { + params = append(params, start) + } + return c.findStorageHistoric(params) +} + +func (c *Client) findStorageHistoric(params []any) (result.FindStorage, error) { + var resp result.FindStorage + if err := c.performRequest("findstoragehistoric", params, &resp); err != nil { + return resp, err + } + return resp, nil +} + // GetTransactionHeight returns the block index where the transaction is found. func (c *Client) GetTransactionHeight(hash util.Uint256) (uint32, error) { var ( diff --git a/pkg/rpcclient/rpc_test.go b/pkg/rpcclient/rpc_test.go index 590b82872..1a7cdbea2 100644 --- a/pkg/rpcclient/rpc_test.go +++ b/pkg/rpcclient/rpc_test.go @@ -915,6 +915,74 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ }, }, }, + "findstorage": { + { + name: "positive by hash", + invoke: func(c *Client) (any, error) { + cHash, _ := util.Uint160DecodeStringLE("5c9e40a12055c6b9e3f72271c9779958c842135d") + start := 1 + return c.FindStorageByHash(cHash, []byte("aa"), &start) + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"results":[{"key":"YWExMA==","value":"djI="}],"truncated":true, "next": 1}}`, + result: func(c *Client) any { + return result.FindStorage{ + Results: []result.KeyValue{{Key: []byte("aa10"), Value: []byte("v2")}}, + Truncated: true, + Next: 1, + } + }, + }, + { + name: "positive by ID", + invoke: func(c *Client) (any, error) { + start := 1 + return c.FindStorageByID(1, []byte("aa"), &start) + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"results":[{"key":"YWExMA==","value":"djI="}],"truncated":true, "next": 1}}`, + result: func(c *Client) any { + return result.FindStorage{ + Results: []result.KeyValue{{Key: []byte("aa10"), Value: []byte("v2")}}, + Truncated: true, + Next: 1, + } + }, + }, + }, + "findstoragehistoric": { + { + name: "positive by hash", + invoke: func(c *Client) (any, error) { + root, _ := util.Uint256DecodeStringLE("252e9d73d49c95c7618d40650da504e05183a1b2eed0685e42c360413c329170") + cHash, _ := util.Uint160DecodeStringLE("5c9e40a12055c6b9e3f72271c9779958c842135d") + start := 1 + return c.FindStorageByHashHistoric(root, cHash, []byte("aa"), &start) + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"results":[{"key":"YWExMA==","value":"djI="}],"truncated":true, "next": 1}}`, + result: func(c *Client) any { + return result.FindStorage{ + Results: []result.KeyValue{{Key: []byte("aa10"), Value: []byte("v2")}}, + Truncated: true, + Next: 1, + } + }, + }, + { + name: "positive by ID", + invoke: func(c *Client) (any, error) { + root, _ := util.Uint256DecodeStringLE("252e9d73d49c95c7618d40650da504e05183a1b2eed0685e42c360413c329170") + start := 1 + return c.FindStorageByIDHistoric(root, 1, []byte("aa"), &start) + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"results":[{"key":"YWExMA==","value":"djI="}],"truncated":true, "next": 1}}`, + result: func(c *Client) any { + return result.FindStorage{ + Results: []result.KeyValue{{Key: []byte("aa10"), Value: []byte("v2")}}, + Truncated: true, + Next: 1, + } + }, + }, + }, "getstateheight": { { name: "positive", diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index a7e2b3784..2bbef0d5a 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -2562,3 +2562,131 @@ func TestActor_CallWithNilParam(t *testing.T) { require.True(t, strings.Contains(res.FaultException, "invalid conversion: Null/ByteString"), res.FaultException) } + +func TestClient_FindStorage(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()) + + h, err := util.Uint160DecodeStringLE(testContractHash) + require.NoError(t, err) + prefix := []byte("aa") + expected := result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte("aa"), + Value: []byte("v1"), + }, + { + Key: []byte("aa10"), + Value: []byte("v2"), + }, + }, + Next: 2, + Truncated: true, + } + + // By hash. + actual, err := c.FindStorageByHash(h, prefix, nil) + require.NoError(t, err) + require.Equal(t, expected, actual) + + // By ID. + actual, err = c.FindStorageByID(1, prefix, nil) // Rubles contract + require.NoError(t, err) + require.Equal(t, expected, actual) + + // Non-nil start. + start := 1 + actual, err = c.FindStorageByHash(h, prefix, &start) + require.NoError(t, err) + require.Equal(t, result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte("aa10"), + Value: []byte("v2"), + }, + { + Key: []byte("aa50"), + Value: []byte("v3"), + }, + }, + Next: 3, + Truncated: false, + }, actual) + + // Missing item. + actual, err = c.FindStorageByHash(h, []byte("unknown prefix"), nil) + require.NoError(t, err) + require.Equal(t, result.FindStorage{}, actual) +} + +func TestClient_FindStorageHistoric(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) + prefix := []byte("aa") + expected := result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte("aa10"), + Value: []byte("v2"), + }, + { + Key: []byte("aa50"), + Value: []byte("v3"), + }, + }, + Next: 2, + Truncated: true, + } + + // By hash. + actual, err := c.FindStorageByHashHistoric(root, h, prefix, nil) + require.NoError(t, err) + require.Equal(t, expected, actual) + + // By ID. + actual, err = c.FindStorageByIDHistoric(root, 1, prefix, nil) // Rubles contract + require.NoError(t, err) + require.Equal(t, expected, actual) + + // Non-nil start. + start := 1 + actual, err = c.FindStorageByHashHistoric(root, h, prefix, &start) + require.NoError(t, err) + require.Equal(t, result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte("aa50"), + Value: []byte("v3"), + }, + { + Key: []byte("aa"), // order differs due to MPT traversal strategy. + Value: []byte("v1"), + }, + }, + Next: 3, + Truncated: false, + }, 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) + actual, err = c.FindStorageByHashHistoric(earlyRoot.Root, h, prefix, nil) + require.NoError(t, err) + require.Equal(t, result.FindStorage{}, actual) +} diff --git a/pkg/services/rpcsrv/server.go b/pkg/services/rpcsrv/server.go index 45d629278..865c963cd 100644 --- a/pkg/services/rpcsrv/server.go +++ b/pkg/services/rpcsrv/server.go @@ -51,6 +51,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/util/slice" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" @@ -98,6 +99,7 @@ type ( GetValidators() ([]*keys.PublicKey, error) HeaderHeight() uint32 InitVerificationContext(ic *interop.Context, hash util.Uint160, witness *transaction.Witness) error + SeekStorage(id int32, prefix []byte, cont func(k, v []byte) bool) SubscribeForBlocks(ch chan *block.Block) SubscribeForExecutions(ch chan *state.AppExecResult) SubscribeForNotifications(ch chan *state.ContainedNotificationEvent) @@ -199,6 +201,8 @@ const ( var rpcHandlers = map[string]func(*Server, params.Params) (any, *neorpc.Error){ "calculatenetworkfee": (*Server).calculateNetworkFee, "findstates": (*Server).findStates, + "findstorage": (*Server).findStorage, + "findstoragehistoric": (*Server).findStorageHistoric, "getapplicationlog": (*Server).getApplicationLog, "getbestblockhash": (*Server).getBestBlockHash, "getblock": (*Server).getBlock, @@ -1414,17 +1418,25 @@ func (s *Server) getHash(contractID int32, cache map[int32]util.Uint160) (util.U return h, nil } -func (s *Server) contractIDFromParam(param *params.Param) (int32, *neorpc.Error) { +func (s *Server) contractIDFromParam(param *params.Param, root ...util.Uint256) (int32, *neorpc.Error) { var result int32 if param == nil { return 0, neorpc.ErrInvalidParams } if scriptHash, err := param.GetUint160FromHex(); err == nil { - cs := s.chain.GetContractState(scriptHash) - if cs == nil { - return 0, neorpc.ErrUnknownContract + if len(root) == 0 { + cs := s.chain.GetContractState(scriptHash) + if cs == nil { + return 0, neorpc.ErrUnknownContract + } + result = cs.ID + } else { + cs, respErr := s.getHistoricalContractState(root[0], scriptHash) + if respErr != nil { + return 0, respErr + } + result = cs.ID } - result = cs.ID } else { id, err := param.GetInt() if err != nil { @@ -1539,18 +1551,9 @@ func (s *Server) verifyProof(ps params.Params) (any, *neorpc.Error) { } func (s *Server) getState(ps params.Params) (any, *neorpc.Error) { - root, err := ps.Value(0).GetUint256() - if err != nil { - return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, "invalid stateroot") - } - if s.chain.GetConfig().Ledger.KeepOnlyLatestState { - curr, err := s.chain.GetStateModule().GetStateRoot(s.chain.BlockHeight()) - if err != nil { - return nil, neorpc.NewInternalServerError(fmt.Sprintf("failed to get current stateroot: %s", err)) - } - if !curr.Root.Equals(root) { - return nil, neorpc.WrapErrorWithData(neorpc.ErrUnsupportedState, fmt.Sprintf("'getstate' is not supported for old states: %s", errKeepOnlyLatestState)) - } + root, respErr := s.getStateRootFromParam(ps.Value(0)) + if respErr != nil { + return nil, respErr } csHash, err := ps.Value(1).GetUint160FromHex() if err != nil { @@ -1576,18 +1579,9 @@ func (s *Server) getState(ps params.Params) (any, *neorpc.Error) { } func (s *Server) findStates(ps params.Params) (any, *neorpc.Error) { - root, err := ps.Value(0).GetUint256() - if err != nil { - return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, "invalid stateroot") - } - if s.chain.GetConfig().Ledger.KeepOnlyLatestState { - curr, err := s.chain.GetStateModule().GetStateRoot(s.chain.BlockHeight()) - if err != nil { - return nil, neorpc.NewInternalServerError(fmt.Sprintf("failed to get current stateroot: %s", err)) - } - if !curr.Root.Equals(root) { - return nil, neorpc.WrapErrorWithData(neorpc.ErrUnsupportedState, fmt.Sprintf("'findstates' is not supported for old states: %s", errKeepOnlyLatestState)) - } + root, respErr := s.getStateRootFromParam(ps.Value(0)) + if respErr != nil { + return nil, respErr } csHash, err := ps.Value(1).GetUint160FromHex() if err != nil { @@ -1669,6 +1663,127 @@ func (s *Server) findStates(ps params.Params) (any, *neorpc.Error) { return res, nil } +// getStateRootFromParam retrieves state root hash from the provided parameter +// (only util.Uint256 serialized representation is allowed) and checks whether +// MPT states are supported for the old stateroot. +func (s *Server) getStateRootFromParam(p *params.Param) (util.Uint256, *neorpc.Error) { + root, err := p.GetUint256() + if err != nil { + return util.Uint256{}, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, "invalid stateroot") + } + if s.chain.GetConfig().Ledger.KeepOnlyLatestState { + curr, err := s.chain.GetStateModule().GetStateRoot(s.chain.BlockHeight()) + if err != nil { + return util.Uint256{}, neorpc.NewInternalServerError(fmt.Sprintf("failed to get current stateroot: %s", err)) + } + if !curr.Root.Equals(root) { + return util.Uint256{}, neorpc.WrapErrorWithData(neorpc.ErrUnsupportedState, fmt.Sprintf("state-based methods are not supported for old states: %s", errKeepOnlyLatestState)) + } + } + return root, nil +} + +func (s *Server) findStorage(reqParams params.Params) (any, *neorpc.Error) { + id, prefix, start, take, respErr := s.getFindStorageParams(reqParams) + if respErr != nil { + return nil, respErr + } + var ( + i int + end = start + take + res = new(result.FindStorage) + ) + s.chain.SeekStorage(id, prefix, func(k, v []byte) bool { + if i < start { + i++ + return true + } + if i < end { + res.Results = append(res.Results, result.KeyValue{ + Key: slice.Copy(append(prefix, k...)), // Don't strip prefix, as it is done in C#. + Value: v, + }) + i++ + return true + } + res.Truncated = true + return false + }) + res.Next = i + return res, nil +} + +func (s *Server) findStorageHistoric(reqParams params.Params) (any, *neorpc.Error) { + root, respErr := s.getStateRootFromParam(reqParams.Value(0)) + if respErr != nil { + return nil, respErr + } + if len(reqParams) < 2 { + return nil, neorpc.ErrInvalidParams + } + id, prefix, start, take, respErr := s.getFindStorageParams(reqParams[1:], root) + if respErr != nil { + return nil, respErr + } + + var ( + end = start + take + res = new(result.FindStorage) + pKey = makeStorageKey(id, prefix) + ) + // @roman-khimov, retrieving only the necessary part of the contract storage + // requires an mpt Billet refactoring, we can do it in a separate issue, create? + kvs, err := s.chain.GetStateModule().FindStates(root, pKey, nil, end+1) // +1 to define result truncation + if err != nil && !errors.Is(err, mpt.ErrNotFound) { + return nil, neorpc.NewInternalServerError(fmt.Sprintf("failed to find state items: %s", err)) + } + if len(kvs) == end+1 { + res.Truncated = true + kvs = kvs[:len(kvs)-1] + } + if start >= len(kvs) { + kvs = nil + } else { + kvs = kvs[start:] + } + + if len(kvs) != 0 { // keep consistency with `findstorage` response + res.Results = make([]result.KeyValue, len(kvs)) + for i := range res.Results { + res.Results[i] = result.KeyValue{ + Key: kvs[i].Key[4:], // Cut contract ID as it is done in C#. + Value: kvs[i].Value, + } + } + } + res.Next = start + len(res.Results) + return res, nil +} + +func (s *Server) getFindStorageParams(reqParams params.Params, root ...util.Uint256) (int32, []byte, int, int, *neorpc.Error) { + if len(reqParams) < 2 { + return 0, nil, 0, 0, neorpc.ErrInvalidParams + } + id, respErr := s.contractIDFromParam(reqParams.Value(0), root...) + if respErr != nil { + return 0, nil, 0, 0, respErr + } + + prefix, err := reqParams.Value(1).GetBytesBase64() + if err != nil { + return 0, nil, 0, 0, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("invalid prefix: %s", err)) + } + + var skip int + if len(reqParams) > 2 { + skip, err = reqParams.Value(2).GetInt() + if err != nil { + return 0, nil, 0, 0, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("invalid start: %s", err)) + } + } + return id, prefix, skip, s.config.MaxFindStorageResultItems, nil +} + func (s *Server) getHistoricalContractState(root util.Uint256, csHash util.Uint160) (*state.Contract, *neorpc.Error) { csKey := makeStorageKey(native.ManagementContractID, native.MakeContractKey(csHash)) csBytes, err := s.chain.GetStateModule().GetState(root, csKey) diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index 5fa25ff44..98ed281bf 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -128,6 +128,14 @@ var rpcFunctionsWithUnsupportedStatesTestCases = map[string][]rpcTestCase{ errCode: neorpc.ErrUnsupportedStateCode, }, }, + "findstoragehistoric": { + { + name: "unsupported state", + params: `["` + block20StateRootLE + `", "0xabcdef"]`, + fail: true, + errCode: neorpc.ErrUnsupportedStateCode, + }, + }, "invokefunctionhistoric": { { name: "unsupported state", @@ -651,6 +659,294 @@ var rpcTestCases = map[string][]rpcTestCase{ errCode: neorpc.InvalidParamsCode, }, }, + "findstorage": { + { + name: "not truncated", + params: fmt.Sprintf(`["%s", "%s"]`, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa1"))), + result: func(_ *executor) any { return new(result.FindStorage) }, + check: func(t *testing.T, e *executor, res any) { + actual, ok := res.(*result.FindStorage) + require.True(t, ok) + + expected := &result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte("aa10"), + Value: []byte("v2"), + }, + }, + Next: 1, + Truncated: false, + } + require.Equal(t, expected, actual) + }, + }, + { + name: "truncated first page", + params: fmt.Sprintf(`["%s", "%s"]`, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa"))), + result: func(_ *executor) any { return new(result.FindStorage) }, + check: func(t *testing.T, e *executor, res any) { + actual, ok := res.(*result.FindStorage) + require.True(t, ok) + + expected := &result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte("aa"), + Value: []byte("v1"), + }, + { + Key: []byte("aa10"), + Value: []byte("v2"), + }, + }, + Next: 2, + Truncated: true, + } + require.Equal(t, expected, actual) + }, + }, + { + name: "truncated second page", + params: fmt.Sprintf(`["%s", "%s", 2]`, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa"))), + result: func(_ *executor) any { return new(result.FindStorage) }, + check: func(t *testing.T, e *executor, res any) { + actual, ok := res.(*result.FindStorage) + require.True(t, ok) + + expected := &result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte("aa50"), + Value: []byte("v3"), + }, + }, + Next: 3, + Truncated: false, + } + require.Equal(t, expected, actual) + }, + }, + { + name: "empty prefix", + params: fmt.Sprintf(`["%s", ""]`, storageContractHash), + result: func(_ *executor) any { return new(result.FindStorage) }, + check: func(t *testing.T, e *executor, res any) { + actual, ok := res.(*result.FindStorage) + require.True(t, ok) + + expected := &result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte{0x01, 0x00}, + Value: []byte{}, + }, + { + Key: []byte{0x01, 0x01}, + Value: []byte{0x01}, + }, + }, + Next: 2, + Truncated: true, + } + require.Equal(t, expected, actual) + }, + }, + { + name: "unknown key", + params: fmt.Sprintf(`["%s", "%s"]`, testContractHash, base64.StdEncoding.EncodeToString([]byte("unknown-key"))), + result: func(_ *executor) any { return new(result.FindStorage) }, + check: func(t *testing.T, e *executor, res any) { + actual, ok := res.(*result.FindStorage) + require.True(t, ok) + + expected := &result.FindStorage{ + Results: nil, + Next: 0, + Truncated: false, + } + require.Equal(t, expected, actual) + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "no second parameter", + params: fmt.Sprintf(`["%s"]`, testContractHash), + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "invalid hash", + params: `["notahex"]`, + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "invalid key", + params: fmt.Sprintf(`["%s", "notabase64$"]`, testContractHash), + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "invalid page", + params: fmt.Sprintf(`["%s", "", "not-an-int"]`, testContractHash), + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + }, + "findstoragehistoric": { + { + name: "not truncated", + params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa1"))), + result: func(_ *executor) any { return new(result.FindStorage) }, + check: func(t *testing.T, e *executor, res any) { + actual, ok := res.(*result.FindStorage) + require.True(t, ok) + + expected := &result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte("aa10"), + Value: []byte("v2"), + }, + }, + Next: 1, + Truncated: false, + } + require.Equal(t, expected, actual) + }, + }, + { + name: "truncated first page", + params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa"))), + result: func(_ *executor) any { return new(result.FindStorage) }, + check: func(t *testing.T, e *executor, res any) { + actual, ok := res.(*result.FindStorage) + require.True(t, ok) + + expected := &result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte("aa10"), // items traversal order may differ from the one provided by `findstorage` due to MPT traversal strategy. + Value: []byte("v2"), + }, + { + Key: []byte("aa50"), + Value: []byte("v3"), + }, + }, + Next: 2, + Truncated: true, + } + require.Equal(t, expected, actual) + }, + }, + { + name: "truncated second page", + params: fmt.Sprintf(`["%s","%s", "%s", 2]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa"))), + result: func(_ *executor) any { return new(result.FindStorage) }, + check: func(t *testing.T, e *executor, res any) { + actual, ok := res.(*result.FindStorage) + require.True(t, ok) + + expected := &result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte("aa"), + Value: []byte("v1"), + }, + }, + Next: 3, + Truncated: false, + } + require.Equal(t, expected, actual) + }, + }, + { + name: "empty prefix", + params: fmt.Sprintf(`["%s", "%s", ""]`, block20StateRootLE, nnsContractHash), + result: func(_ *executor) any { return new(result.FindStorage) }, + check: func(t *testing.T, e *executor, res any) { + actual, ok := res.(*result.FindStorage) + require.True(t, ok) + + expected := &result.FindStorage{ + Results: []result.KeyValue{ + { + Key: []byte{0x00}, // total supply + Value: []byte{0x01}, + }, + { + Key: append([]byte{0x01}, testchain.PrivateKeyByID(0).GetScriptHash().BytesBE()...), // balance of priv0 + Value: []byte{0x01}, + }, + }, + Next: 2, + Truncated: true, + } + require.Equal(t, expected, actual) + }, + }, + { + name: "unknown key", + params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("unknown-key"))), + result: func(_ *executor) any { return new(result.FindStorage) }, + check: func(t *testing.T, e *executor, res any) { + actual, ok := res.(*result.FindStorage) + require.True(t, ok) + + expected := &result.FindStorage{} + require.Equal(t, expected, actual) + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "invalid stateroot", + params: `[12345]`, + 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 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, + }, + { + name: "invalid page", + params: fmt.Sprintf(`["%s", "%s", "", "not-an-int"]`, block20StateRootLE, testContractHash), + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + }, "getbestblockhash": { { params: "[]",