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:
parent
124c3df2ff
commit
617c628c24
11 changed files with 741 additions and 43 deletions
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
10
pkg/neorpc/result/findstorage.go
Normal file
10
pkg/neorpc/result/findstorage.go
Normal 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"`
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: "[]",
|
||||
|
|
Loading…
Reference in a new issue