Merge pull request #3099 from nspcc-dev/findstorage
rpcsrv, rpcclient: support `findstorage`, `findstoragehistoric` and `getstoragehistoric` calls
This commit is contained in:
commit
7d75526c20
13 changed files with 981 additions and 60 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
|
||||
|
@ -279,7 +281,7 @@ where:
|
|||
pool, then invocation result will contain corresponding error inside the
|
||||
`FaultException` field.
|
||||
- `StartWhenSynchronized` controls when RPC server will be started, by default
|
||||
(`false` setting) it's started immediately and RPC is availabe during node
|
||||
(`false` setting) it's started immediately and RPC is available during node
|
||||
synchronization. Setting it to `true` will make the node start RPC service only
|
||||
after full synchronization.
|
||||
- `TLS` section configures TLS protocol.
|
||||
|
|
45
docs/rpc.md
45
docs/rpc.md
|
@ -37,6 +37,7 @@ which would yield the response:
|
|||
| ------- |
|
||||
| `calculatenetworkfee` |
|
||||
| `findstates` |
|
||||
| `findstorage` |
|
||||
| `getapplicationlog` |
|
||||
| `getbestblockhash` |
|
||||
| `getblock` |
|
||||
|
@ -237,7 +238,30 @@ block. It can be removed in future versions, but at the moment you can use it
|
|||
to see how much GAS is burned with a particular block (because system fees are
|
||||
burned).
|
||||
|
||||
#### `invokecontractverifyhistoric`, `invokefunctionhistoric` and `invokescripthistoric` calls
|
||||
#### Historic calls
|
||||
|
||||
A set of `*historic` extension methods provide the ability of interacting with
|
||||
*historical* chain state including invoking contract methods, running scripts and
|
||||
retrieving contract storage items. It means that the contracts' storage state has
|
||||
all its values got from MPT with the specified stateroot from past (or, which is
|
||||
the same, with the stateroot of the block of the specified height). All
|
||||
operations related to the contract storage will be performed using this past
|
||||
contracts' storage state and using interop context (if required by the RPC
|
||||
handler) with a block which is next to the block with the specified height.
|
||||
|
||||
Any historical RPC call needs the historical chain state to be presented in the
|
||||
node storage, thus if the node keeps only latest MPT state the historical call
|
||||
can not be handled properly and
|
||||
[neorpc.ErrUnsupportedState](https://github.com/nspcc-dev/neo-go/blob/87e4b6beaafa3c180184cbbe88ba143378c5024c/pkg/neorpc/errors.go#L134)
|
||||
is returned in this case. The historical calls only guaranteed to correctly work
|
||||
on archival node that stores all MPT data. If a node keeps the number of latest
|
||||
states and has the GC on (this setting corresponds to the
|
||||
`RemoveUntraceableBlocks` set to `true`), then the behaviour of historical RPC
|
||||
call is undefined. GC can always kick some data out of the storage while the
|
||||
historical call is executing, thus keep in mind that the call can be processed
|
||||
with `RemoveUntraceableBlocks` only with limitations on available data.
|
||||
|
||||
##### `invokecontractverifyhistoric`, `invokefunctionhistoric` and `invokescripthistoric` calls
|
||||
|
||||
These methods provide the ability of *historical* calls and accept block hash or
|
||||
block index or stateroot hash as the first parameter and the list of parameters
|
||||
|
@ -250,16 +274,15 @@ the block with the specified height. This allows to perform test invocation usin
|
|||
the specified past chain state. These methods may be useful for debugging
|
||||
purposes.
|
||||
|
||||
Behavior note: any historical RPC call need the historical chain state to be
|
||||
presented in the node storage, thus if the node keeps only latest MPT state
|
||||
the historical call can not be handled properly.The historical calls only
|
||||
guaranteed to correctly work on archival node that stores all MPT data. If a
|
||||
node keeps the number of latest states and has the GC on (this setting
|
||||
corresponds to the `RemoveUntraceableBlocks` set to `true`), then the behaviour
|
||||
of historical RPC call is undefined. GC can always kick some data out of the
|
||||
storage while the historical call is executing, thus keep in mind that the call
|
||||
can be processed with `RemoveUntraceableBlocks` only with limitations on
|
||||
available data.
|
||||
##### `getstoragehistoric` and `findstoragehistoric` calls
|
||||
|
||||
These methods provide the ability of retrieving *historical* contract storage
|
||||
items and accept stateroot hash as the first parameter and the list of parameters
|
||||
that is the same as of `getstorage` and `findstorage` correspondingly. The
|
||||
historical storage items retrieval process assume that the contracts' storage
|
||||
state has all its values got from MPT with the specified stateroot. This allows
|
||||
to track the contract storage scheme using the specified past chain state. These
|
||||
methods may be useful for debugging purposes.
|
||||
|
||||
#### `submitnotaryrequest` call
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -195,6 +195,7 @@ type StateRoot interface {
|
|||
CurrentLocalStateRoot() util.Uint256
|
||||
CurrentValidatedHeight() uint32
|
||||
FindStates(root util.Uint256, prefix, start []byte, max int) ([]storage.KeyValue, error)
|
||||
SeekStates(root util.Uint256, prefix []byte, f func(k, v []byte) bool)
|
||||
GetState(root util.Uint256, key []byte) ([]byte, error)
|
||||
GetStateProof(root util.Uint256, key []byte) ([][]byte, error)
|
||||
GetStateRoot(height uint32) (*state.MPTRoot, error)
|
||||
|
@ -2133,6 +2134,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()
|
||||
|
|
|
@ -93,6 +93,32 @@ func (s *Module) FindStates(root util.Uint256, prefix, start []byte, max int) ([
|
|||
return tr.Find(prefix, start, max)
|
||||
}
|
||||
|
||||
// SeekStates traverses over contract storage with the state based on the
|
||||
// specified root. `prefix` is expected to consist of contract ID and the desired
|
||||
// storage items prefix. `cont` is called for every matching key-value pair;
|
||||
// the resulting key does not include contract ID and the desired storage item
|
||||
// prefix (they are stripped to match the Blockchain's SeekStorage behaviour.
|
||||
// The result includes item with the key that equals to the `prefix` (if
|
||||
// such item is found in the storage). Traversal process is stopped when `false`
|
||||
// is returned from `cont`.
|
||||
func (s *Module) SeekStates(root util.Uint256, prefix []byte, cont func(k, v []byte) bool) {
|
||||
// Allow accessing old values, it's RO thing.
|
||||
store := mpt.NewTrieStore(root, s.mode&^mpt.ModeGCFlag, storage.NewMemCachedStore(s.Store))
|
||||
|
||||
// Tiny hack to satisfy TrieStore with the given prefix. This
|
||||
// storage.STStorage prefix is a stub that will be stripped by the
|
||||
// TrieStore.Seek while performing MPT traversal and isn't actually relevant
|
||||
// here.
|
||||
key := make([]byte, len(prefix)+1)
|
||||
key[0] = byte(storage.STStorage)
|
||||
copy(key[1:], prefix)
|
||||
|
||||
store.Seek(storage.SeekRange{Prefix: key}, func(k, v []byte) bool {
|
||||
// Cut the prefix to match the Blockchain's SeekStorage behaviour.
|
||||
return cont(k[len(key):], v)
|
||||
})
|
||||
}
|
||||
|
||||
// GetStateProof returns proof of having key in the MPT with the specified root.
|
||||
func (s *Module) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) {
|
||||
// Allow accessing old values, it's RO thing.
|
||||
|
|
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,94 @@ func (c *Client) getStorage(params []any) ([]byte, error) {
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
// GetStorageByIDHistoric returns the historical stored value according to the
|
||||
// contract ID and, stored key and specified stateroot.
|
||||
func (c *Client) GetStorageByIDHistoric(root util.Uint256, id int32, key []byte) ([]byte, error) {
|
||||
return c.getStorageHistoric([]any{root.StringLE(), id, key})
|
||||
}
|
||||
|
||||
// GetStorageByHashHistoric returns the historical stored value according to the
|
||||
// contract script hash, the stored key and specified stateroot.
|
||||
func (c *Client) GetStorageByHashHistoric(root util.Uint256, hash util.Uint160, key []byte) ([]byte, error) {
|
||||
return c.getStorageHistoric([]any{root.StringLE(), hash.StringLE(), key})
|
||||
}
|
||||
|
||||
func (c *Client) getStorageHistoric(params []any) ([]byte, error) {
|
||||
var resp []byte
|
||||
if err := c.performRequest("getstoragehistoric", params, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// FindStorageByHash returns contract storage items by the given contract hash and prefix.
|
||||
// If `start` index is specified, items starting from `start` index are being returned
|
||||
// (including item located at the start index).
|
||||
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",
|
||||
|
@ -972,6 +1040,50 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
|
|||
},
|
||||
},
|
||||
},
|
||||
"getstoragehistoric": {
|
||||
{
|
||||
name: "by hash, positive",
|
||||
invoke: func(c *Client) (any, error) {
|
||||
root, _ := util.Uint256DecodeStringLE("252e9d73d49c95c7618d40650da504e05183a1b2eed0685e42c360413c329170")
|
||||
hash, err := util.Uint160DecodeStringLE("03febccf81ac85e3d795bc5cbd4e84e907812aa3")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
key, err := hex.DecodeString("5065746572")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c.GetStorageByHashHistoric(root, hash, key)
|
||||
},
|
||||
serverResponse: `{"jsonrpc":"2.0","id":1,"result":"TGlu"}`,
|
||||
result: func(c *Client) any {
|
||||
value, err := hex.DecodeString("4c696e")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return value
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "by ID, positive",
|
||||
invoke: func(c *Client) (any, error) {
|
||||
root, _ := util.Uint256DecodeStringLE("252e9d73d49c95c7618d40650da504e05183a1b2eed0685e42c360413c329170")
|
||||
key, err := hex.DecodeString("5065746572")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c.GetStorageByIDHistoric(root, -1, key)
|
||||
},
|
||||
serverResponse: `{"jsonrpc":"2.0","id":1,"result":"TGlu"}`,
|
||||
result: func(c *Client) any {
|
||||
value, err := hex.DecodeString("4c696e")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return value
|
||||
},
|
||||
},
|
||||
},
|
||||
"gettransactionheight": {
|
||||
{
|
||||
name: "positive",
|
||||
|
|
|
@ -2562,3 +2562,164 @@ 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)
|
||||
}
|
||||
|
||||
func TestClient_GetStorageHistoric(t *testing.T) {
|
||||
chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t)
|
||||
defer chain.Close()
|
||||
defer rpcSrv.Shutdown()
|
||||
|
||||
c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.Init())
|
||||
|
||||
root, err := util.Uint256DecodeStringLE(block20StateRootLE)
|
||||
require.NoError(t, err)
|
||||
h, err := util.Uint160DecodeStringLE(testContractHash)
|
||||
require.NoError(t, err)
|
||||
key := []byte("aa10")
|
||||
expected := []byte("v2")
|
||||
|
||||
// By hash.
|
||||
actual, err := c.GetStorageByHashHistoric(root, h, key)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, actual)
|
||||
|
||||
// By ID.
|
||||
actual, err = c.GetStorageByIDHistoric(root, 1, key) // Rubles contract
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, actual)
|
||||
|
||||
// Missing item.
|
||||
earlyRoot, err := chain.GetStateRoot(15) // there's no `aa10` value in Rubles contract by the moment of block #15
|
||||
require.NoError(t, err)
|
||||
_, err = c.GetStorageByHashHistoric(earlyRoot.Root, h, key)
|
||||
require.ErrorIs(t, neorpc.ErrUnknownStorageItem, err)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
@ -109,6 +110,13 @@ type (
|
|||
VerifyTx(*transaction.Transaction) error
|
||||
VerifyWitness(util.Uint160, hash.Hashable, *transaction.Witness, int64) (int64, error)
|
||||
mempool.Feer // fee interface
|
||||
ContractStorageSeeker
|
||||
}
|
||||
|
||||
// ContractStorageSeeker is the interface `findstorage*` handlers need to be able to
|
||||
// seek over contract storage.
|
||||
ContractStorageSeeker interface {
|
||||
SeekStorage(id int32, prefix []byte, cont func(k, v []byte) bool)
|
||||
}
|
||||
|
||||
// OracleHandler is the interface oracle service needs to provide for the Server.
|
||||
|
@ -199,6 +207,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,
|
||||
|
@ -225,6 +235,7 @@ var rpcHandlers = map[string]func(*Server, params.Params) (any, *neorpc.Error){
|
|||
"getstateheight": (*Server).getStateHeight,
|
||||
"getstateroot": (*Server).getStateRoot,
|
||||
"getstorage": (*Server).getStorage,
|
||||
"getstoragehistoric": (*Server).getStorageHistoric,
|
||||
"gettransactionheight": (*Server).getTransactionHeight,
|
||||
"getunclaimedgas": (*Server).getUnclaimedGas,
|
||||
"getnextblockvalidators": (*Server).getNextBlockValidators,
|
||||
|
@ -1414,17 +1425,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 +1558,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 +1586,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 +1670,114 @@ 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
|
||||
}
|
||||
return s.findStorageInternal(id, prefix, start, take, s.chain)
|
||||
}
|
||||
|
||||
func (s *Server) findStorageInternal(id int32, prefix []byte, start, take int, seeker ContractStorageSeeker) (any, *neorpc.Error) {
|
||||
var (
|
||||
i int
|
||||
end = start + take
|
||||
res = new(result.FindStorage)
|
||||
)
|
||||
seeker.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
|
||||
}
|
||||
|
||||
return s.findStorageInternal(id, prefix, start, take, mptStorageSeeker{
|
||||
root: root,
|
||||
module: s.chain.GetStateModule(),
|
||||
})
|
||||
}
|
||||
|
||||
// mptStorageSeeker is an auxiliary structure that implements ContractStorageSeeker interface.
|
||||
type mptStorageSeeker struct {
|
||||
root util.Uint256
|
||||
module core.StateRoot
|
||||
}
|
||||
|
||||
func (s mptStorageSeeker) SeekStorage(id int32, prefix []byte, cont func(k, v []byte) bool) {
|
||||
key := makeStorageKey(id, prefix)
|
||||
s.module.SeekStates(s.root, key, cont)
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -1740,6 +1849,36 @@ func (s *Server) getStorage(ps params.Params) (any, *neorpc.Error) {
|
|||
return []byte(item), nil
|
||||
}
|
||||
|
||||
func (s *Server) getStorageHistoric(ps params.Params) (any, *neorpc.Error) {
|
||||
root, respErr := s.getStateRootFromParam(ps.Value(0))
|
||||
if respErr != nil {
|
||||
return nil, respErr
|
||||
}
|
||||
if len(ps) < 2 {
|
||||
return nil, neorpc.ErrInvalidParams
|
||||
}
|
||||
|
||||
id, rErr := s.contractIDFromParam(ps.Value(1), root)
|
||||
if rErr != nil {
|
||||
return nil, rErr
|
||||
}
|
||||
key, err := ps.Value(2).GetBytesBase64()
|
||||
if err != nil {
|
||||
return nil, neorpc.ErrInvalidParams
|
||||
}
|
||||
pKey := makeStorageKey(id, key)
|
||||
|
||||
v, err := s.chain.GetStateModule().GetState(root, pKey)
|
||||
if err != nil && !errors.Is(err, mpt.ErrNotFound) {
|
||||
return nil, neorpc.NewInternalServerError(fmt.Sprintf("failed to get state item: %s", err))
|
||||
}
|
||||
if v == nil {
|
||||
return "", neorpc.ErrUnknownStorageItem
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (s *Server) getrawtransaction(reqParams params.Params) (any, *neorpc.Error) {
|
||||
txHash, err := reqParams.Value(0).GetUint256()
|
||||
if err != nil {
|
||||
|
|
|
@ -98,7 +98,7 @@ var (
|
|||
var rpcFunctionsWithUnsupportedStatesTestCases = map[string][]rpcTestCase{
|
||||
"getproof": {
|
||||
{
|
||||
name: "no params",
|
||||
name: "unsupported state",
|
||||
params: `[]`,
|
||||
fail: true,
|
||||
errCode: neorpc.ErrUnsupportedStateCode,
|
||||
|
@ -106,7 +106,7 @@ var rpcFunctionsWithUnsupportedStatesTestCases = map[string][]rpcTestCase{
|
|||
},
|
||||
"verifyproof": {
|
||||
{
|
||||
name: "no params",
|
||||
name: "unsupported state",
|
||||
params: `[]`,
|
||||
fail: true,
|
||||
errCode: neorpc.ErrUnsupportedStateCode,
|
||||
|
@ -114,7 +114,7 @@ var rpcFunctionsWithUnsupportedStatesTestCases = map[string][]rpcTestCase{
|
|||
},
|
||||
"getstate": {
|
||||
{
|
||||
name: "unknown root/item",
|
||||
name: "unsupported state",
|
||||
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHash + `", "QQ=="]`,
|
||||
fail: true,
|
||||
errCode: neorpc.ErrUnsupportedStateCode,
|
||||
|
@ -122,7 +122,15 @@ var rpcFunctionsWithUnsupportedStatesTestCases = map[string][]rpcTestCase{
|
|||
},
|
||||
"findstates": {
|
||||
{
|
||||
name: "invalid contract",
|
||||
name: "unsupported state",
|
||||
params: `["` + block20StateRootLE + `", "0xabcdef"]`,
|
||||
fail: true,
|
||||
errCode: neorpc.ErrUnsupportedStateCode,
|
||||
},
|
||||
},
|
||||
"findstoragehistoric": {
|
||||
{
|
||||
name: "unsupported state",
|
||||
params: `["` + block20StateRootLE + `", "0xabcdef"]`,
|
||||
fail: true,
|
||||
errCode: neorpc.ErrUnsupportedStateCode,
|
||||
|
@ -130,7 +138,7 @@ var rpcFunctionsWithUnsupportedStatesTestCases = map[string][]rpcTestCase{
|
|||
},
|
||||
"invokefunctionhistoric": {
|
||||
{
|
||||
name: "no params",
|
||||
name: "unsupported state",
|
||||
params: `[]`,
|
||||
fail: true,
|
||||
errCode: neorpc.ErrUnsupportedStateCode,
|
||||
|
@ -651,6 +659,346 @@ var rpcTestCases = map[string][]rpcTestCase{
|
|||
errCode: neorpc.InvalidParamsCode,
|
||||
},
|
||||
},
|
||||
"getstoragehistoric": {
|
||||
{
|
||||
name: "positive",
|
||||
params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa10"))),
|
||||
result: func(e *executor) any {
|
||||
v := base64.StdEncoding.EncodeToString([]byte("v2"))
|
||||
return &v
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing key",
|
||||
params: fmt.Sprintf(`["%s", "%s", "dGU="]`, block20StateRootLE, testContractHash),
|
||||
fail: true,
|
||||
errCode: neorpc.ErrUnknownStorageItemCode,
|
||||
},
|
||||
{
|
||||
name: "no params",
|
||||
params: `[]`,
|
||||
fail: true,
|
||||
errCode: neorpc.InvalidParamsCode,
|
||||
},
|
||||
{
|
||||
name: "no second parameter",
|
||||
params: fmt.Sprintf(`["%s"]`, block20StateRootLE),
|
||||
fail: true,
|
||||
errCode: neorpc.InvalidParamsCode,
|
||||
},
|
||||
{
|
||||
name: "no third parameter",
|
||||
params: fmt.Sprintf(`["%s", "%s"]`, block20StateRootLE, testContractHash),
|
||||
fail: true,
|
||||
errCode: neorpc.InvalidParamsCode,
|
||||
},
|
||||
{
|
||||
name: "invalid stateroot",
|
||||
params: `["notahex"]`,
|
||||
fail: true,
|
||||
errCode: neorpc.InvalidParamsCode,
|
||||
},
|
||||
{
|
||||
name: "invalid hash",
|
||||
params: fmt.Sprintf(`["%s", "notahex"]`, block20StateRootLE),
|
||||
fail: true,
|
||||
errCode: neorpc.InvalidParamsCode,
|
||||
},
|
||||
{
|
||||
name: "invalid key",
|
||||
params: fmt.Sprintf(`["%s", "%s", "notabase64$"]`, block20StateRootLE, testContractHash),
|
||||
fail: true,
|
||||
errCode: neorpc.InvalidParamsCode,
|
||||
},
|
||||
},
|
||||
"findstorage": {
|
||||
{
|
||||
name: "not truncated",
|
||||
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