rpc: implement findstates RPC handler

This commit is contained in:
Anna Shaleva 2021-10-07 16:56:27 +03:00
parent 7da394fd3f
commit 43ac4e1517
19 changed files with 515 additions and 38 deletions

View file

@ -140,6 +140,8 @@ RPC:
Address: "" Address: ""
EnableCORSWorkaround: false EnableCORSWorkaround: false
MaxGasInvoke: 50 MaxGasInvoke: 50
MaxIteratorResultItems: 100
MaxFindResultItems: 100
Port: 10332 Port: 10332
TLSConfig: TLSConfig:
Address: "" Address: ""
@ -155,6 +157,11 @@ where:
you're accessing RPC interface from the browser. you're accessing RPC interface from the browser.
- `MaxGasInvoke` is the maximum GAS allowed to spend during `invokefunction` and - `MaxGasInvoke` is the maximum GAS allowed to spend during `invokefunction` and
`invokescript` RPC-calls. `invokescript` RPC-calls.
- `MaxIteratorResultItems` - maximum number of elements extracted from iterator
returned by `invoke*` call. When the `MaxIteratorResultItems` value is set to
`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.
- `Port` is an RPC server port it should be bound to. - `Port` is an RPC server port it should be bound to.
- `TLS` section configures TLS protocol. - `TLS` section configures TLS protocol.

View file

@ -36,6 +36,7 @@ which would yield the response:
| Method | | Method |
| ------- | | ------- |
| `calculatenetworkfee` | | `calculatenetworkfee` |
| `findstates` |
| `getapplicationlog` | | `getapplicationlog` |
| `getbestblockhash` | | `getbestblockhash` |
| `getblock` | | `getblock` |

View file

@ -52,6 +52,7 @@ func LoadFile(configPath string) (Config, error) {
PingTimeout: 90, PingTimeout: 90,
RPC: rpc.Config{ RPC: rpc.Config{
MaxIteratorResultItems: 100, MaxIteratorResultItems: 100,
MaxFindResultItems: 100,
}, },
}, },
} }

View file

@ -2,6 +2,7 @@ package blockchainer
import ( import (
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
) )
@ -13,6 +14,7 @@ type StateRoot interface {
CurrentLocalHeight() uint32 CurrentLocalHeight() uint32
CurrentLocalStateRoot() util.Uint256 CurrentLocalStateRoot() util.Uint256
CurrentValidatedHeight() uint32 CurrentValidatedHeight() uint32
FindStates(root util.Uint256, prefix, start []byte, max int) ([]storage.KeyValue, error)
GetState(root util.Uint256, key []byte) ([]byte, error) GetState(root util.Uint256, key []byte) ([]byte, error)
GetStateProof(root util.Uint256, key []byte) ([][]byte, error) GetStateProof(root util.Uint256, key []byte) ([][]byte, error)
GetStateRoot(height uint32) (*state.MPTRoot, error) GetStateRoot(height uint32) (*state.MPTRoot, error)

View file

@ -504,6 +504,10 @@ func initBasicChain(t *testing.T, bc *Blockchain) {
// Invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call // Invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call
script.Reset() script.Reset()
emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "testkey", "newtestvalue") emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "testkey", "newtestvalue")
// Invoke `test_contract.go`: put values to check `findstates` RPC call
emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "aa", "v1")
emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "aa10", "v2")
emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "aa50", "v3")
txInv = transaction.New(script.Bytes(), 1*native.GASFactor) txInv = transaction.New(script.Bytes(), 1*native.GASFactor)
txInv.Nonce = getNextNonce() txInv.Nonce = getNextNonce()

View file

@ -199,8 +199,8 @@ func (b *Billet) incrementRefAndStore(h util.Uint256, bs []byte) {
// to its children calling `process` for each serialised node until true is // to its children calling `process` for each serialised node until true is
// returned from `process` function. It also replaces all HashNodes to their // returned from `process` function. It also replaces all HashNodes to their
// "unhashed" counterparts until the stop condition is satisfied. // "unhashed" counterparts until the stop condition is satisfied.
func (b *Billet) Traverse(process func(node Node, nodeBytes []byte) bool, ignoreStorageErr bool) error { func (b *Billet) Traverse(process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool) error {
r, err := b.traverse(b.root, process, ignoreStorageErr) r, err := b.traverse(b.root, []byte{}, []byte{}, 0, process, ignoreStorageErr)
if err != nil && !errors.Is(err, errStop) { if err != nil && !errors.Is(err, errStop) {
return err return err
} }
@ -208,7 +208,7 @@ func (b *Billet) Traverse(process func(node Node, nodeBytes []byte) bool, ignore
return nil return nil
} }
func (b *Billet) traverse(curr Node, process func(node Node, nodeBytes []byte) bool, ignoreStorageErr bool) (Node, error) { func (b *Billet) traverse(curr Node, path, from []byte, offset int, process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool) (Node, error) {
if _, ok := curr.(EmptyNode); ok { if _, ok := curr.(EmptyNode); ok {
// We're not interested in EmptyNodes, and they do not affect the // We're not interested in EmptyNodes, and they do not affect the
// traversal process, thus remain them untouched. // traversal process, thus remain them untouched.
@ -222,18 +222,45 @@ func (b *Billet) traverse(curr Node, process func(node Node, nodeBytes []byte) b
} }
return nil, err return nil, err
} }
return b.traverse(r, process, ignoreStorageErr) return b.traverse(r, path, from, offset, process, ignoreStorageErr)
} }
if _, ok := curr.(*LeafNode); !ok || len(from) <= offset && (len(from) == 0 || !bytes.Equal(path, from)) {
bytes := slice.Copy(curr.Bytes()) bytes := slice.Copy(curr.Bytes())
if process(curr, bytes) { if process(fromNibbles(path), curr, bytes) {
return curr, errStop return curr, errStop
} }
}
switch n := curr.(type) { switch n := curr.(type) {
case *LeafNode: case *LeafNode:
return b.tryCollapseLeaf(n), nil return b.tryCollapseLeaf(n), nil
case *BranchNode: case *BranchNode:
for i := range n.Children { if len(from) > offset {
r, err := b.traverse(n.Children[i], process, ignoreStorageErr) startIndex := from[offset]
for i := startIndex; i < lastChild; i++ {
newOffset := len(from)
if i == startIndex {
newOffset = offset + 1
}
r, err := b.traverse(n.Children[i], append(path, i), from, newOffset, process, ignoreStorageErr)
if err != nil {
if !errors.Is(err, errStop) {
return nil, err
}
n.Children[i] = r
return n, err
}
n.Children[i] = r
}
return b.tryCollapseBranch(n), nil
}
for i, child := range n.Children {
var newPath []byte
if i == lastChild {
newPath = path
} else {
newPath = append(path, byte(i))
}
r, err := b.traverse(child, newPath, from, offset, process, ignoreStorageErr)
if err != nil { if err != nil {
if !errors.Is(err, errStop) { if !errors.Is(err, errStop) {
return nil, err return nil, err
@ -245,12 +272,23 @@ func (b *Billet) traverse(curr Node, process func(node Node, nodeBytes []byte) b
} }
return b.tryCollapseBranch(n), nil return b.tryCollapseBranch(n), nil
case *ExtensionNode: case *ExtensionNode:
r, err := b.traverse(n.next, process, ignoreStorageErr) var newOffset int
if len(from) > offset && bytes.HasPrefix(from[offset:], n.key) {
newOffset = offset + len(n.key)
} else if len(from) <= offset || bytes.Compare(n.key, from[offset:]) > 0 {
newOffset = len(from)
} else {
return b.tryCollapseExtension(n), nil
}
r, err := b.traverse(n.next, append(path, n.key...), from, newOffset, process, ignoreStorageErr)
if err != nil && !errors.Is(err, errStop) { if err != nil && !errors.Is(err, errStop) {
return nil, err return nil, err
} }
n.next = r n.next = r
return b.tryCollapseExtension(n), err if err != nil {
return n, err
}
return b.tryCollapseExtension(n), nil
default: default:
return nil, ErrNotFound return nil, ErrNotFound
} }

View file

@ -378,3 +378,29 @@ func newFilledTrie(t *testing.T, args ...[]byte) *Trie {
} }
return tr return tr
} }
func TestCompatibility_Find(t *testing.T) {
check := func(t *testing.T, from []byte, expectedResLen int) {
tr := NewTrie(nil, false, newTestStore())
require.NoError(t, tr.Put([]byte("aa"), []byte("02")))
require.NoError(t, tr.Put([]byte("aa10"), []byte("03")))
require.NoError(t, tr.Put([]byte("aa50"), []byte("04")))
res, err := tr.Find([]byte("aa"), from, 10)
require.NoError(t, err)
require.Equal(t, expectedResLen, len(res))
}
t.Run("no from", func(t *testing.T) {
check(t, nil, 3)
})
t.Run("from is not in tree", func(t *testing.T) {
t.Run("matching", func(t *testing.T) {
check(t, []byte("aa30"), 1)
})
t.Run("non-matching", func(t *testing.T) {
check(t, []byte("aa60"), 0)
})
})
t.Run("from is in tree", func(t *testing.T) {
check(t, []byte("aa10"), 1) // without `from` key
})
}

View file

@ -72,6 +72,9 @@ func VerifyProof(rh util.Uint256, key []byte, proofs [][]byte) ([]byte, bool) {
// no errors in Put to memory store // no errors in Put to memory store
_ = tr.Store.Put(makeStorageKey(h[:]), proofs[i]) _ = tr.Store.Put(makeStorageKey(h[:]), proofs[i])
} }
_, bs, err := tr.getWithPath(tr.root, path) _, leaf, _, err := tr.getWithPath(tr.root, path, true)
return bs, err == nil if err != nil {
return nil, false
}
return slice.Copy(leaf.(*LeafNode).value), true
} }

View file

@ -53,49 +53,62 @@ func (t *Trie) Get(key []byte) ([]byte, error) {
return nil, errors.New("key is too big") return nil, errors.New("key is too big")
} }
path := toNibbles(key) path := toNibbles(key)
r, bs, err := t.getWithPath(t.root, path) r, leaf, _, err := t.getWithPath(t.root, path, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
t.root = r t.root = r
return bs, nil return slice.Copy(leaf.(*LeafNode).value), nil
} }
// getWithPath returns value the provided path in a subtrie rooting in curr. // getWithPath returns a current node with all hash nodes along the path replaced
// It also returns a current node with all hash nodes along the path // to their "unhashed" counterparts. It also returns node the provided path in a
// replaced to their "unhashed" counterparts. // subtrie rooting in curr points to. In case of `strict` set to `false` the
func (t *Trie) getWithPath(curr Node, path []byte) (Node, []byte, error) { // provided path can be incomplete, so it also returns full path that points to
// the node found at the specified incomplete path. In case of `strict` set to `true`
// the resulting path matches the provided one.
func (t *Trie) getWithPath(curr Node, path []byte, strict bool) (Node, Node, []byte, error) {
switch n := curr.(type) { switch n := curr.(type) {
case *LeafNode: case *LeafNode:
if len(path) == 0 { if len(path) == 0 {
return curr, slice.Copy(n.value), nil return curr, n, []byte{}, nil
} }
case *BranchNode: case *BranchNode:
i, path := splitPath(path) i, path := splitPath(path)
r, bs, err := t.getWithPath(n.Children[i], path) if i == lastChild && !strict {
return curr, n, []byte{}, nil
}
r, res, prefix, err := t.getWithPath(n.Children[i], path, strict)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
n.Children[i] = r n.Children[i] = r
return n, bs, nil return n, res, append([]byte{i}, prefix...), nil
case EmptyNode: case EmptyNode:
case *HashNode: case *HashNode:
if r, err := t.getFromStore(n.hash); err == nil { if r, err := t.getFromStore(n.hash); err == nil {
return t.getWithPath(r, path) return t.getWithPath(r, path, strict)
} }
case *ExtensionNode: case *ExtensionNode:
if len(path) == 0 && !strict {
return curr, n.next, n.key, nil
}
if bytes.HasPrefix(path, n.key) { if bytes.HasPrefix(path, n.key) {
r, bs, err := t.getWithPath(n.next, path[len(n.key):]) r, res, prefix, err := t.getWithPath(n.next, path[len(n.key):], strict)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
n.next = r n.next = r
return curr, bs, err return curr, res, append(n.key, prefix...), err
}
if !strict && bytes.HasPrefix(n.key, path) {
// path is shorter than prefix, stop seeking
return curr, n.next, n.key, nil
} }
default: default:
panic("invalid MPT node type") panic("invalid MPT node type")
} }
return curr, nil, ErrNotFound return curr, nil, nil, ErrNotFound
} }
// Put puts key-value pair in t. // Put puts key-value pair in t.
@ -511,3 +524,48 @@ func collapse(depth int, node Node) Node {
} }
return node return node
} }
// Find returns list of storage key-value pairs whose key is prefixed by the specified
// prefix starting from the specified path (not including the item at the specified path
// if so). The `max` number of elements is returned at max.
func (t *Trie) Find(prefix, from []byte, max int) ([]storage.KeyValue, error) {
if len(prefix) > MaxKeyLength {
return nil, errors.New("invalid prefix length")
}
if len(from) > MaxKeyLength {
return nil, errors.New("invalid from length")
}
prefixP := toNibbles(prefix)
fromP := []byte{}
if len(from) > 0 {
if !bytes.HasPrefix(from, prefix) {
return nil, errors.New("`from` argument doesn't match specified prefix")
}
fromP = toNibbles(from)
}
_, start, path, err := t.getWithPath(t.root, prefixP, false)
if err != nil {
return nil, fmt.Errorf("failed to determine the start node: %w", err)
}
var (
res []storage.KeyValue
count int
)
b := NewBillet(t.root.Hash(), false, t.Store)
process := func(pathToNode []byte, node Node, _ []byte) bool {
if leaf, ok := node.(*LeafNode); ok {
res = append(res, storage.KeyValue{
Key: pathToNode,
Value: slice.Copy(leaf.value),
})
count++
}
return count >= max
}
_, err = b.traverse(start, path, fromP, len(path), process, false)
if err != nil && !errors.Is(err, errStop) {
return nil, err
}
return res, nil
}

View file

@ -632,3 +632,58 @@ func TestTrie_Collapse(t *testing.T) {
require.Equal(t, NewHashNode(h), newRoot) require.Equal(t, NewHashNode(h), newRoot)
}) })
} }
func TestTrie_Seek(t *testing.T) {
tr := newTestTrie(t)
t.Run("extension", func(t *testing.T) {
check := func(t *testing.T, prefix []byte) {
_, res, prefix, err := tr.getWithPath(tr.root, prefix, false)
require.NoError(t, err)
require.Equal(t, []byte{0x0A, 0x0C}, prefix)
require.Equal(t, BranchT, res.Type()) // extension's next is branch
}
t.Run("seek prefix points to extension", func(t *testing.T) {
check(t, []byte{})
})
t.Run("seek prefix is a part of extension key", func(t *testing.T) {
check(t, []byte{0x0A})
})
t.Run("seek prefix match extension key", func(t *testing.T) {
check(t, []byte{0x0A, 0x0C}) // path to extension's next
})
})
t.Run("branch", func(t *testing.T) {
t.Run("seek prefix points to branch", func(t *testing.T) {
_, res, prefix, err := tr.getWithPath(tr.root, []byte{0x0A, 0x0C}, false)
require.NoError(t, err)
require.Equal(t, []byte{0x0A, 0x0C}, prefix)
require.Equal(t, BranchT, res.Type())
})
t.Run("seek prefix points to empty branch child", func(t *testing.T) {
_, _, _, err := tr.getWithPath(tr.root, []byte{0x0A, 0x0C, 0x02}, false)
require.Error(t, err)
})
t.Run("seek prefix points to non-empty branch child", func(t *testing.T) {
_, res, prefix, err := tr.getWithPath(tr.root, []byte{0x0A, 0x0C, 0x01}, false)
require.NoError(t, err)
require.Equal(t, []byte{0x0A, 0x0C, 0x01, 0x03}, prefix)
require.Equal(t, LeafT, res.Type())
})
})
t.Run("leaf", func(t *testing.T) {
t.Run("seek prefix points to leaf", func(t *testing.T) {
_, res, prefix, err := tr.getWithPath(tr.root, []byte{0x0A, 0x0C, 0x01, 0x03}, false)
require.NoError(t, err)
require.Equal(t, []byte{0x0A, 0x0C, 0x01, 0x03}, prefix)
require.Equal(t, LeafT, res.Type())
})
})
t.Run("hash", func(t *testing.T) {
t.Run("seek prefix points to hash", func(t *testing.T) {
_, res, prefix, err := tr.getWithPath(tr.root, []byte{0x0A, 0x0C, 0x0A, 0x0E}, false)
require.NoError(t, err)
require.Equal(t, []byte{0x0A, 0x0C, 0x0A, 0x0E}, prefix)
require.Equal(t, LeafT, res.Type())
})
})
}

View file

@ -61,6 +61,16 @@ func (s *Module) GetState(root util.Uint256, key []byte) ([]byte, error) {
return tr.Get(key) return tr.Get(key)
} }
// FindStates returns set of key-value pairs with key matching the prefix starting
// from the `prefix`+`start` path from MPT trie with the specified root. `max` is
// the maximum number of elements to be returned. If nil `start` specified, then
// item with key equals to prefix is included into result; if empty `start` specified,
// then item with key equals to prefix is not included into result.
func (s *Module) FindStates(root util.Uint256, prefix, start []byte, max int) ([]storage.KeyValue, error) {
tr := mpt.NewTrie(mpt.NewHashNode(root), false, storage.NewMemCachedStore(s.Store))
return tr.Find(prefix, start, max)
}
// GetStateProof returns proof of having key in the MPT with the specified root. // GetStateProof returns proof of having key in the MPT with the specified root.
func (s *Module) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) { func (s *Module) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) {
tr := mpt.NewTrie(mpt.NewHashNode(root), false, storage.NewMemCachedStore(s.Store)) tr := mpt.NewTrie(mpt.NewHashNode(root), false, storage.NewMemCachedStore(s.Store))

View file

@ -200,7 +200,7 @@ func (s *Module) defineSyncStage() error {
zap.String("state root", header.PrevStateRoot.StringBE())) zap.String("state root", header.PrevStateRoot.StringBE()))
pool := NewPool() pool := NewPool()
pool.Add(header.PrevStateRoot, []byte{}) pool.Add(header.PrevStateRoot, []byte{})
err = s.billet.Traverse(func(n mpt.Node, _ []byte) bool { err = s.billet.Traverse(func(_ []byte, n mpt.Node, _ []byte) bool {
nPaths, ok := pool.TryGet(n.Hash()) nPaths, ok := pool.TryGet(n.Hash())
if !ok { if !ok {
// if this situation occurs, then it's a bug in MPT pool or Traverse. // if this situation occurs, then it's a bug in MPT pool or Traverse.
@ -467,7 +467,9 @@ func (s *Module) Traverse(root util.Uint256, process func(node mpt.Node, nodeByt
defer s.lock.RUnlock() defer s.lock.RUnlock()
b := mpt.NewBillet(root, s.bc.GetConfig().KeepOnlyLatestState, storage.NewMemCachedStore(s.dao.Store)) b := mpt.NewBillet(root, s.bc.GetConfig().KeepOnlyLatestState, storage.NewMemCachedStore(s.dao.Store))
return b.Traverse(process, false) return b.Traverse(func(pathToNode []byte, node mpt.Node, nodeBytes []byte) bool {
return process(node, nodeBytes)
}, false)
} }
// GetUnknownMPTNodesBatch returns set of currently unknown MPT nodes (`limit` at max). // GetUnknownMPTNodesBatch returns set of currently unknown MPT nodes (`limit` at max).

View file

@ -387,6 +387,25 @@ func (c *Client) GetState(stateroot util.Uint256, historicalContractHash util.Ui
return resp, nil return resp, nil
} }
// FindStates returns historical contract storage item states by the given stateroot,
// historical contract hash and historical prefix. If `start` path is specified, then items
// starting from `start` path are being returned (excluding item located at the start path).
// If `maxCount` specified, then maximum number of items to be returned equals to `maxCount`.
func (c *Client) FindStates(stateroot util.Uint256, historicalContractHash util.Uint160, historicalPrefix []byte,
start []byte, maxCount *int) (result.FindStates, error) {
var (
params = request.NewRawParams(stateroot.StringLE(), historicalContractHash.StringLE(), historicalPrefix, historicalPrefix, start)
resp result.FindStates
)
if maxCount != nil {
params.Values = append(params.Values, *maxCount)
}
if err := c.performRequest("findstates", params, &resp); err != nil {
return resp, err
}
return resp, nil
}
// GetStateHeight returns current validated and local node state height. // GetStateHeight returns current validated and local node state height.
func (c *Client) GetStateHeight() (*result.StateHeight, error) { func (c *Client) GetStateHeight() (*result.StateHeight, error) {
var ( var (

View file

@ -25,6 +25,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/rpc/request" "github.com/nspcc-dev/neo-go/pkg/rpc/request"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
@ -689,6 +690,29 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
}, },
}, },
}, },
"findstates": {
{
name: "positive",
invoke: func(c *Client) (interface{}, error) {
root, _ := util.Uint256DecodeStringLE("252e9d73d49c95c7618d40650da504e05183a1b2eed0685e42c360413c329170")
cHash, _ := util.Uint160DecodeStringLE("5c9e40a12055c6b9e3f72271c9779958c842135d")
count := 1
return c.FindStates(root, cHash, []byte("aa"), []byte("aa00"), &count)
},
serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"results":[{"key":"YWExMA==","value":"djI="}],"firstProof":"CAEAAABhYTEwCXIAA5KNHjQ1+LFX4lQBLjMAhaLtTJnfuI86O7WnNdlshsYWBAQEBAQEBAQDTzD7MJp2KW6E8BNVjjjgZMTAjI/GI3ZrTmR2UUOtSeIEBAQEBAPKPqb0qnb4Ywz6gqpNKCUNQsfBmAnKc5p3dxokSQRpwgRSAAQDPplG1wee4KOfkehaF94R5uoKSgvQL1j5gkFTN4ywYaIEBAOhOyI39MZfoKc940g57XeqwRnxh7P62fKjnfEtBzQxHQQEBAQEBAQEBAQEBCkBBgAAAAAAAAM6A1UrwFYZAEMfe6go3jX25xz2sHsovQ2UO/UHqZZOXLIABAOwg7pkXyaTR85yQIvYnoGaG/OVRLRHOj+nhZnXb6dVtAQEBAPnciBUp3uspLQTajKTlAxgrNe+3tlqlbwlNRkz0eNmhQMzoMcWOFi9nCyn+eM5lA6Pq67DxzTQDlHljh8g8kRtJAPq9hxzTgreK0qDTavsethixguZYfV7wDmKfumMglnoqQQEBAQEBAM1x2dVBdf5BJ0Xvw2qqhvpKqxdHb8/HMFWiXkJj1uAAQQEJgEDAQYBA5kV2WLkgey9C5z6gZT69VLKcEuwyY8P853rNtGhT3NeUgAEBAQDiX59K9PuJ5RE7Z1uj7q/QJ8FGf8avLdWM7hwmWkVH2gEBAQEBAQEBAQEBAQD1SubX5XhFHcUOWdUzg1bXmDwWJwt+wpU3FOdFkU1PXBSAAQDHCzfEQyqwOO263EE6HER1vWDrwz8JiEHEOXfZ3kX7NYEBAQDEH++Hy8wBcniKuWVevaAwzHCh60kzncU30E5fDC3gJsEBAQEBAQEBAQEBCUBAgMAA1wt18LbxMKdYcJ+nEDMMWZbRsu550l8HGhcYhpl6DjSBAICdjI=","truncated":true}}`,
result: func(c *Client) interface{} {
proofB, _ := base64.StdEncoding.DecodeString("CAEAAABhYTEwCXIAA5KNHjQ1+LFX4lQBLjMAhaLtTJnfuI86O7WnNdlshsYWBAQEBAQEBAQDTzD7MJp2KW6E8BNVjjjgZMTAjI/GI3ZrTmR2UUOtSeIEBAQEBAPKPqb0qnb4Ywz6gqpNKCUNQsfBmAnKc5p3dxokSQRpwgRSAAQDPplG1wee4KOfkehaF94R5uoKSgvQL1j5gkFTN4ywYaIEBAOhOyI39MZfoKc940g57XeqwRnxh7P62fKjnfEtBzQxHQQEBAQEBAQEBAQEBCkBBgAAAAAAAAM6A1UrwFYZAEMfe6go3jX25xz2sHsovQ2UO/UHqZZOXLIABAOwg7pkXyaTR85yQIvYnoGaG/OVRLRHOj+nhZnXb6dVtAQEBAPnciBUp3uspLQTajKTlAxgrNe+3tlqlbwlNRkz0eNmhQMzoMcWOFi9nCyn+eM5lA6Pq67DxzTQDlHljh8g8kRtJAPq9hxzTgreK0qDTavsethixguZYfV7wDmKfumMglnoqQQEBAQEBAM1x2dVBdf5BJ0Xvw2qqhvpKqxdHb8/HMFWiXkJj1uAAQQEJgEDAQYBA5kV2WLkgey9C5z6gZT69VLKcEuwyY8P853rNtGhT3NeUgAEBAQDiX59K9PuJ5RE7Z1uj7q/QJ8FGf8avLdWM7hwmWkVH2gEBAQEBAQEBAQEBAQD1SubX5XhFHcUOWdUzg1bXmDwWJwt+wpU3FOdFkU1PXBSAAQDHCzfEQyqwOO263EE6HER1vWDrwz8JiEHEOXfZ3kX7NYEBAQDEH++Hy8wBcniKuWVevaAwzHCh60kzncU30E5fDC3gJsEBAQEBAQEBAQEBCUBAgMAA1wt18LbxMKdYcJ+nEDMMWZbRsu550l8HGhcYhpl6DjSBAICdjI=")
proof := &result.ProofWithKey{}
r := io.NewBinReaderFromBuf(proofB)
proof.DecodeBinary(r)
return result.FindStates{
Results: []result.KeyValue{{Key: []byte("aa10"), Value: []byte("v2")}},
FirstProof: proof,
Truncated: true,
}
},
},
},
"getstateheight": { "getstateheight": {
{ {
name: "positive", name: "positive",

View file

@ -0,0 +1,13 @@
package result
type FindStates struct {
Results []KeyValue `json:"results"`
FirstProof *ProofWithKey `json:"firstProof,omitempty"`
LastProof *ProofWithKey `json:"lastProof,omitempty"`
Truncated bool `json:"truncated"`
}
type KeyValue struct {
Key []byte `json:"key"`
Value []byte `json:"value"`
}

View file

@ -14,6 +14,7 @@ type (
// can be spent during RPC call. // can be spent during RPC call.
MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"` MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"`
MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"` MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"`
MaxFindResultItems int `yaml:"MaxFindResultItems"`
Port uint16 `yaml:"Port"` Port uint16 `yaml:"Port"`
TLSConfig TLSConfig `yaml:"TLSConfig"` TLSConfig TLSConfig `yaml:"TLSConfig"`
} }

View file

@ -102,6 +102,7 @@ const (
var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){
"calculatenetworkfee": (*Server).calculateNetworkFee, "calculatenetworkfee": (*Server).calculateNetworkFee,
"findstates": (*Server).findStates,
"getapplicationlog": (*Server).getApplicationLog, "getapplicationlog": (*Server).getApplicationLog,
"getbestblockhash": (*Server).getBestBlockHash, "getbestblockhash": (*Server).getBestBlockHash,
"getblock": (*Server).getBlock, "getblock": (*Server).getBlock,
@ -1051,6 +1052,108 @@ func (s *Server) getState(ps request.Params) (interface{}, *response.Error) {
if err != nil { if err != nil {
return nil, response.WrapErrorWithData(response.ErrInvalidParams, errors.New("invalid key")) return nil, response.WrapErrorWithData(response.ErrInvalidParams, errors.New("invalid key"))
} }
cs, respErr := s.getHistoricalContractState(root, csHash)
if respErr != nil {
return nil, respErr
}
sKey := makeStorageKey(cs.ID, key)
res, err := s.chain.GetStateModule().GetState(root, sKey)
if err != nil {
return nil, response.NewInternalServerError("failed to get historical item state", err)
}
return res, nil
}
func (s *Server) findStates(ps request.Params) (interface{}, *response.Error) {
root, err := ps.Value(0).GetUint256()
if err != nil {
return nil, response.WrapErrorWithData(response.ErrInvalidParams, errors.New("invalid stateroot"))
}
if s.chain.GetConfig().KeepOnlyLatestState {
curr, err := s.chain.GetStateModule().GetStateRoot(s.chain.BlockHeight())
if err != nil {
return nil, response.NewInternalServerError("failed to get current stateroot", err)
}
if !curr.Root.Equals(root) {
return nil, response.NewInvalidRequestError("'findstates' is not supported for old states", errKeepOnlyLatestState)
}
}
csHash, err := ps.Value(1).GetUint160FromHex()
if err != nil {
return nil, response.WrapErrorWithData(response.ErrInvalidParams, errors.New("invalid contract hash"))
}
prefix, err := ps.Value(2).GetBytesBase64()
if err != nil {
return nil, response.WrapErrorWithData(response.ErrInvalidParams, errors.New("invalid prefix"))
}
var (
key []byte
count = s.config.MaxFindResultItems
)
if len(ps) > 3 {
key, err = ps.Value(3).GetBytesBase64()
if err != nil {
return nil, response.WrapErrorWithData(response.ErrInvalidParams, errors.New("invalid key"))
}
}
if len(ps) > 4 {
count, err = ps.Value(4).GetInt()
if err != nil {
return nil, response.WrapErrorWithData(response.ErrInvalidParams, errors.New("invalid count"))
}
if count > s.config.MaxFindResultItems {
count = s.config.MaxFindResultItems
}
}
cs, respErr := s.getHistoricalContractState(root, csHash)
if respErr != nil {
return nil, respErr
}
pKey := makeStorageKey(cs.ID, prefix)
var sKey []byte
if len(key) > 0 {
sKey = makeStorageKey(cs.ID, key)
}
kvs, err := s.chain.GetStateModule().FindStates(root, pKey, sKey, count+1) // +1 to define result truncation
if err != nil {
return nil, response.NewInternalServerError("failed to find historical items", err)
}
res := result.FindStates{}
if len(kvs) == count+1 {
res.Truncated = true
kvs = kvs[:len(kvs)-1]
}
if len(kvs) > 0 {
proof, err := s.chain.GetStateModule().GetStateProof(root, kvs[0].Key)
if err != nil {
return nil, response.NewInternalServerError("failed to get first proof", err)
}
res.FirstProof = &result.ProofWithKey{
Key: kvs[0].Key,
Proof: proof,
}
}
if len(kvs) > 1 {
proof, err := s.chain.GetStateModule().GetStateProof(root, kvs[len(kvs)-1].Key)
if err != nil {
return nil, response.NewInternalServerError("failed to get first proof", err)
}
res.LastProof = &result.ProofWithKey{
Key: kvs[len(kvs)-1].Key,
Proof: proof,
}
}
res.Results = make([]result.KeyValue, len(kvs))
for i, kv := range kvs {
res.Results[i] = result.KeyValue{
Key: kv.Key[4:], // cut contract ID as it is done in C#
Value: kv.Value,
}
}
return res, nil
}
func (s *Server) getHistoricalContractState(root util.Uint256, csHash util.Uint160) (*state.Contract, *response.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)
if err != nil { if err != nil {
@ -1061,12 +1164,7 @@ func (s *Server) getState(ps request.Params) (interface{}, *response.Error) {
if err != nil { if err != nil {
return nil, response.NewInternalServerError("failed to deserialize historical contract state", err) return nil, response.NewInternalServerError("failed to deserialize historical contract state", err)
} }
sKey := makeStorageKey(contract.ID, key) return contract, nil
res, err := s.chain.GetStateModule().GetState(root, sKey)
if err != nil {
return nil, response.NewInternalServerError("failed to get historical item state", err)
}
return res, nil
} }
func (s *Server) getStateHeight(_ request.Params) (interface{}, *response.Error) { func (s *Server) getStateHeight(_ request.Params) (interface{}, *response.Error) {

View file

@ -55,7 +55,7 @@ type rpcTestCase struct {
} }
const testContractHash = "5c9e40a12055c6b9e3f72271c9779958c842135d" const testContractHash = "5c9e40a12055c6b9e3f72271c9779958c842135d"
const deploymentTxHash = "cb17eac9594d7ffa318545ab36e3227eedf30b4d13d76d3b49c94243fb3b2bde" const deploymentTxHash = "8de63ea12ca8a9c5233ebf8664a442c881ae1bb83708d82da7fa1da2305ecf14"
const genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4" const genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4"
const verifyContractHash = "f68822e4ecd93de334bdf1f7c409eda3431bcbd0" const verifyContractHash = "f68822e4ecd93de334bdf1f7c409eda3431bcbd0"
@ -348,6 +348,38 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true, fail: true,
}, },
}, },
"findstates": {
{
name: "no params",
params: `[]`,
fail: true,
},
{
name: "invalid root",
params: `["0xabcdef"]`,
fail: true,
},
{
name: "invalid contract",
params: `["0000000000000000000000000000000000000000000000000000000000000000", "0xabcdef"]`,
fail: true,
},
{
name: "invalid prefix",
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHash + `", "notabase64%"]`,
fail: true,
},
{
name: "invalid key",
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHash + `", "QQ==", "notabase64%"]`,
fail: true,
},
{
name: "unknown contract/large count",
params: `["0000000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000", "QQ==", "QQ==", 101]`,
fail: true,
},
},
"getstateheight": { "getstateheight": {
{ {
name: "positive", name: "positive",
@ -1444,6 +1476,89 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
testGetState(t, params, base64.StdEncoding.EncodeToString([]byte("newtestvalue"))) testGetState(t, params, base64.StdEncoding.EncodeToString([]byte("newtestvalue")))
}) })
}) })
t.Run("findstates", func(t *testing.T) {
testFindStates := func(t *testing.T, p string, root util.Uint256, expected result.FindStates) {
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "findstates", "params": [%s]}`, p)
body := doRPCCall(rpc, httpSrv.URL, t)
rawRes := checkErrGetResult(t, body, false)
var actual result.FindStates
require.NoError(t, json.Unmarshal(rawRes, &actual))
require.Equal(t, expected.Results, actual.Results)
checkProof := func(t *testing.T, proof *result.ProofWithKey, value []byte) {
rpc = fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "verifyproof", "params": ["%s", "%s"]}`,
root.StringLE(), proof.String())
body = doRPCCall(rpc, httpSrv.URL, t)
rawRes = checkErrGetResult(t, body, false)
vp := new(result.VerifyProof)
require.NoError(t, json.Unmarshal(rawRes, vp))
require.Equal(t, value, vp.Value)
}
checkProof(t, actual.FirstProof, actual.Results[0].Value)
if len(actual.Results) > 1 {
checkProof(t, actual.LastProof, actual.Results[len(actual.Results)-1].Value)
}
require.Equal(t, expected.Truncated, actual.Truncated)
}
t.Run("good: no prefix, no limit", func(t *testing.T) {
// pairs for this test where put to the contract storage at block #16
root, err := e.chain.GetStateModule().GetStateRoot(16)
require.NoError(t, err)
params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")))
testFindStates(t, params, root.Root, result.FindStates{
Results: []result.KeyValue{
{Key: []byte("aa10"), Value: []byte("v2")},
{Key: []byte("aa50"), Value: []byte("v3")},
{Key: []byte("aa"), Value: []byte("v1")},
},
Truncated: false,
})
})
t.Run("good: with prefix, no limit", func(t *testing.T) {
// pairs for this test where put to the contract storage at block #16
root, err := e.chain.GetStateModule().GetStateRoot(16)
require.NoError(t, err)
params := fmt.Sprintf(`"%s", "%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")), base64.StdEncoding.EncodeToString([]byte("aa10")))
testFindStates(t, params, root.Root, result.FindStates{
Results: []result.KeyValue{
{Key: []byte("aa50"), Value: []byte("v3")},
},
Truncated: false,
})
})
t.Run("good: no prefix, with limit", func(t *testing.T) {
for limit := 2; limit < 5; limit++ {
// pairs for this test where put to the contract storage at block #16
root, err := e.chain.GetStateModule().GetStateRoot(16)
require.NoError(t, err)
params := fmt.Sprintf(`"%s", "%s", "%s", "", %d`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")), limit)
expected := result.FindStates{
Results: []result.KeyValue{
{Key: []byte("aa10"), Value: []byte("v2")},
{Key: []byte("aa50"), Value: []byte("v3")},
},
Truncated: limit == 2,
}
if limit != 2 {
expected.Results = append(expected.Results, result.KeyValue{Key: []byte("aa"), Value: []byte("v1")})
}
testFindStates(t, params, root.Root, expected)
}
})
t.Run("good: with prefix, with limit", func(t *testing.T) {
// pairs for this test where put to the contract storage at block #16
root, err := e.chain.GetStateModule().GetStateRoot(16)
require.NoError(t, err)
params := fmt.Sprintf(`"%s", "%s", "%s", "%s", %d`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")), base64.StdEncoding.EncodeToString([]byte("aa00")), 1)
testFindStates(t, params, root.Root, result.FindStates{
Results: []result.KeyValue{
{Key: []byte("aa10"), Value: []byte("v2")},
},
Truncated: true,
})
})
})
t.Run("getrawtransaction", func(t *testing.T) { t.Run("getrawtransaction", func(t *testing.T) {
block, _ := chain.GetBlock(chain.GetHeaderHash(1)) block, _ := chain.GetBlock(chain.GetHeaderHash(1))
@ -1708,7 +1823,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) {
}, },
{ {
Asset: e.chain.UtilityTokenHash(), Asset: e.chain.UtilityTokenHash(),
Amount: "57796933740", Amount: "57796785740",
LastUpdated: 16, LastUpdated: 16,
}}, }},
Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(),

Binary file not shown.