rpcsrv, rpcclient: support findstorage and findstoragehistoric

Close #3095 and add the corresponding historic extension.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
This commit is contained in:
Anna Shaleva 2023-08-22 19:00:43 +03:00
parent 124c3df2ff
commit 617c628c24
11 changed files with 741 additions and 43 deletions

View file

@ -66,6 +66,7 @@ ApplicationConfiguration:
EnableCORSWorkaround: false EnableCORSWorkaround: false
SessionEnabled: true SessionEnabled: true
SessionExpirationTime: 2 # enough for tests as they run locally. SessionExpirationTime: 2 # enough for tests as they run locally.
MaxFindStoragePageSize: 2 # small value to test server-side paging
Prometheus: Prometheus:
Enabled: false #since it's not useful for unit tests. Enabled: false #since it's not useful for unit tests.
Addresses: Addresses:

View file

@ -202,6 +202,7 @@ RPC:
MaxGasInvoke: 50 MaxGasInvoke: 50
MaxIteratorResultItems: 100 MaxIteratorResultItems: 100
MaxFindResultItems: 100 MaxFindResultItems: 100
MaxFindStoragePageSize: 50
MaxNEP11Tokens: 100 MaxNEP11Tokens: 100
MaxWebSocketClients: 64 MaxWebSocketClients: 64
SessionEnabled: false SessionEnabled: false
@ -238,6 +239,7 @@ where:
`n`, only `n` iterations are returned and truncated is true, indicating that `n`, only `n` iterations are returned and truncated is true, indicating that
there is still data to be returned. there is still data to be returned.
- `MaxFindResultItems` - the maximum number of elements for `findstates` response. - `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 - `MaxNEP11Tokens` - limit for the number of tokens returned from
`getnep11balances` call. `getnep11balances` call.
- `MaxWebSocketClients` - the maximum simultaneous websocket client connection - `MaxWebSocketClients` - the maximum simultaneous websocket client connection

View file

@ -19,6 +19,9 @@ const (
// DefaultMaxIteratorResultItems is the default upper bound of traversed // DefaultMaxIteratorResultItems is the default upper bound of traversed
// iterator items per JSON-RPC response. // iterator items per JSON-RPC response.
DefaultMaxIteratorResultItems = 100 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. // 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, PingTimeout: 90 * time.Second,
}, },
RPC: RPC{ RPC: RPC{
MaxIteratorResultItems: DefaultMaxIteratorResultItems, MaxIteratorResultItems: DefaultMaxIteratorResultItems,
MaxFindResultItems: 100, MaxFindResultItems: 100,
MaxNEP11Tokens: 100, MaxFindStorageResultItems: DefaultMaxFindStorageResultItems,
MaxNEP11Tokens: 100,
}, },
}, },
} }

View file

@ -11,17 +11,18 @@ type (
EnableCORSWorkaround bool `yaml:"EnableCORSWorkaround"` EnableCORSWorkaround bool `yaml:"EnableCORSWorkaround"`
// MaxGasInvoke is the maximum amount of GAS which // MaxGasInvoke is the maximum amount of GAS which
// can be spent during an RPC call. // can be spent during an RPC call.
MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"` MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"`
MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"` MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"`
MaxFindResultItems int `yaml:"MaxFindResultItems"` MaxFindResultItems int `yaml:"MaxFindResultItems"`
MaxNEP11Tokens int `yaml:"MaxNEP11Tokens"` MaxFindStorageResultItems int `yaml:"MaxFindStoragePageSize"`
MaxWebSocketClients int `yaml:"MaxWebSocketClients"` MaxNEP11Tokens int `yaml:"MaxNEP11Tokens"`
SessionEnabled bool `yaml:"SessionEnabled"` MaxWebSocketClients int `yaml:"MaxWebSocketClients"`
SessionExpirationTime int `yaml:"SessionExpirationTime"` SessionEnabled bool `yaml:"SessionEnabled"`
SessionBackedByMPT bool `yaml:"SessionBackedByMPT"` SessionExpirationTime int `yaml:"SessionExpirationTime"`
SessionPoolSize int `yaml:"SessionPoolSize"` SessionBackedByMPT bool `yaml:"SessionBackedByMPT"`
StartWhenSynchronized bool `yaml:"StartWhenSynchronized"` SessionPoolSize int `yaml:"SessionPoolSize"`
TLSConfig TLS `yaml:"TLSConfig"` StartWhenSynchronized bool `yaml:"StartWhenSynchronized"`
TLSConfig TLS `yaml:"TLSConfig"`
} }
// TLS describes SSL/TLS configuration. // TLS describes SSL/TLS configuration.

View file

@ -2133,6 +2133,11 @@ func (bc *Blockchain) GetStorageItem(id int32, key []byte) state.StorageItem {
return bc.dao.GetStorageItem(id, key) 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. // GetBlock returns a Block by the given hash.
func (bc *Blockchain) GetBlock(hash util.Uint256) (*block.Block, error) { func (bc *Blockchain) GetBlock(hash util.Uint256) (*block.Block, error) {
topBlock := bc.topBlock.Load() topBlock := bc.topBlock.Load()

View file

@ -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"`
}

View file

@ -559,6 +559,74 @@ func (c *Client) getStorage(params []any) ([]byte, error) {
return resp, nil 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. // GetTransactionHeight returns the block index where the transaction is found.
func (c *Client) GetTransactionHeight(hash util.Uint256) (uint32, error) { func (c *Client) GetTransactionHeight(hash util.Uint256) (uint32, error) {
var ( var (

View file

@ -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": { "getstateheight": {
{ {
name: "positive", name: "positive",

View file

@ -2562,3 +2562,131 @@ func TestActor_CallWithNilParam(t *testing.T) {
require.True(t, strings.Contains(res.FaultException, "invalid conversion: Null/ByteString"), res.FaultException) 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)
}

View file

@ -51,6 +51,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard" "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/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util" "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"
"github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
@ -98,6 +99,7 @@ type (
GetValidators() ([]*keys.PublicKey, error) GetValidators() ([]*keys.PublicKey, error)
HeaderHeight() uint32 HeaderHeight() uint32
InitVerificationContext(ic *interop.Context, hash util.Uint160, witness *transaction.Witness) error 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) SubscribeForBlocks(ch chan *block.Block)
SubscribeForExecutions(ch chan *state.AppExecResult) SubscribeForExecutions(ch chan *state.AppExecResult)
SubscribeForNotifications(ch chan *state.ContainedNotificationEvent) SubscribeForNotifications(ch chan *state.ContainedNotificationEvent)
@ -199,6 +201,8 @@ const (
var rpcHandlers = map[string]func(*Server, params.Params) (any, *neorpc.Error){ var rpcHandlers = map[string]func(*Server, params.Params) (any, *neorpc.Error){
"calculatenetworkfee": (*Server).calculateNetworkFee, "calculatenetworkfee": (*Server).calculateNetworkFee,
"findstates": (*Server).findStates, "findstates": (*Server).findStates,
"findstorage": (*Server).findStorage,
"findstoragehistoric": (*Server).findStorageHistoric,
"getapplicationlog": (*Server).getApplicationLog, "getapplicationlog": (*Server).getApplicationLog,
"getbestblockhash": (*Server).getBestBlockHash, "getbestblockhash": (*Server).getBestBlockHash,
"getblock": (*Server).getBlock, "getblock": (*Server).getBlock,
@ -1414,17 +1418,25 @@ func (s *Server) getHash(contractID int32, cache map[int32]util.Uint160) (util.U
return h, nil 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 var result int32
if param == nil { if param == nil {
return 0, neorpc.ErrInvalidParams return 0, neorpc.ErrInvalidParams
} }
if scriptHash, err := param.GetUint160FromHex(); err == nil { if scriptHash, err := param.GetUint160FromHex(); err == nil {
cs := s.chain.GetContractState(scriptHash) if len(root) == 0 {
if cs == nil { cs := s.chain.GetContractState(scriptHash)
return 0, neorpc.ErrUnknownContract 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 { } else {
id, err := param.GetInt() id, err := param.GetInt()
if err != nil { 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) { func (s *Server) getState(ps params.Params) (any, *neorpc.Error) {
root, err := ps.Value(0).GetUint256() root, respErr := s.getStateRootFromParam(ps.Value(0))
if err != nil { if respErr != nil {
return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, "invalid stateroot") return nil, respErr
}
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))
}
} }
csHash, err := ps.Value(1).GetUint160FromHex() csHash, err := ps.Value(1).GetUint160FromHex()
if err != nil { 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) { func (s *Server) findStates(ps params.Params) (any, *neorpc.Error) {
root, err := ps.Value(0).GetUint256() root, respErr := s.getStateRootFromParam(ps.Value(0))
if err != nil { if respErr != nil {
return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, "invalid stateroot") return nil, respErr
}
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))
}
} }
csHash, err := ps.Value(1).GetUint160FromHex() csHash, err := ps.Value(1).GetUint160FromHex()
if err != nil { if err != nil {
@ -1669,6 +1663,127 @@ func (s *Server) findStates(ps params.Params) (any, *neorpc.Error) {
return res, nil 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) { func (s *Server) getHistoricalContractState(root util.Uint256, csHash util.Uint160) (*state.Contract, *neorpc.Error) {
csKey := makeStorageKey(native.ManagementContractID, native.MakeContractKey(csHash)) csKey := makeStorageKey(native.ManagementContractID, native.MakeContractKey(csHash))
csBytes, err := s.chain.GetStateModule().GetState(root, csKey) csBytes, err := s.chain.GetStateModule().GetState(root, csKey)

View file

@ -128,6 +128,14 @@ var rpcFunctionsWithUnsupportedStatesTestCases = map[string][]rpcTestCase{
errCode: neorpc.ErrUnsupportedStateCode, errCode: neorpc.ErrUnsupportedStateCode,
}, },
}, },
"findstoragehistoric": {
{
name: "unsupported state",
params: `["` + block20StateRootLE + `", "0xabcdef"]`,
fail: true,
errCode: neorpc.ErrUnsupportedStateCode,
},
},
"invokefunctionhistoric": { "invokefunctionhistoric": {
{ {
name: "unsupported state", name: "unsupported state",
@ -651,6 +659,294 @@ var rpcTestCases = map[string][]rpcTestCase{
errCode: neorpc.InvalidParamsCode, 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": { "getbestblockhash": {
{ {
params: "[]", params: "[]",