diff --git a/docs/node-configuration.md b/docs/node-configuration.md index b46d55d75..1fb8cfdef 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -140,6 +140,8 @@ RPC: Address: "" EnableCORSWorkaround: false MaxGasInvoke: 50 + MaxIteratorResultItems: 100 + MaxFindResultItems: 100 Port: 10332 TLSConfig: Address: "" @@ -155,6 +157,11 @@ where: you're accessing RPC interface from the browser. - `MaxGasInvoke` is the maximum GAS allowed to spend during `invokefunction` and `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. - `TLS` section configures TLS protocol. diff --git a/docs/rpc.md b/docs/rpc.md index 7e25e1516..af0c14b98 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -36,6 +36,7 @@ which would yield the response: | Method | | ------- | | `calculatenetworkfee` | +| `findstates` | | `getapplicationlog` | | `getbestblockhash` | | `getblock` | @@ -54,6 +55,7 @@ which would yield the response: | `getproof` | | `getrawmempool` | | `getrawtransaction` | +| `getstate` | | `getstateheight` | | `getstateroot` | | `getstorage` | diff --git a/pkg/config/config.go b/pkg/config/config.go index 731ab3e22..54fca42df 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -52,6 +52,7 @@ func LoadFile(configPath string) (Config, error) { PingTimeout: 90, RPC: rpc.Config{ MaxIteratorResultItems: 100, + MaxFindResultItems: 100, }, }, } diff --git a/pkg/core/blockchainer/state_root.go b/pkg/core/blockchainer/state_root.go index 9a540bda8..0ba680da0 100644 --- a/pkg/core/blockchainer/state_root.go +++ b/pkg/core/blockchainer/state_root.go @@ -2,6 +2,7 @@ package blockchainer import ( "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/util" ) @@ -13,6 +14,8 @@ type StateRoot interface { CurrentLocalHeight() uint32 CurrentLocalStateRoot() util.Uint256 CurrentValidatedHeight() uint32 + FindStates(root util.Uint256, prefix, start []byte, max int) ([]storage.KeyValue, error) + GetState(root util.Uint256, key []byte) ([]byte, error) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) GetStateRoot(height uint32) (*state.MPTRoot, error) GetStateValidators(height uint32) keys.PublicKeys diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index e91c28db3..182f88544 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -501,6 +501,24 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, err) checkResult(t, res, stackitem.Null{}) + // Invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call + script.Reset() + 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.Nonce = getNextNonce() + txInv.ValidUntilBlock = validUntilBlock + txInv.Signers = []transaction.Signer{{Account: priv0ScriptHash}} + require.NoError(t, addNetworkFee(bc, txInv, acc0)) + require.NoError(t, acc0.SignTx(testchain.Network(), txInv)) + b = bc.newBlock(txInv) + require.NoError(t, bc.AddBlock(b)) + checkTxHalt(t, bc, txInv.Hash()) + // Compile contract to test `invokescript` RPC call _, _ = newDeployTx(t, bc, priv0ScriptHash, prefix+"invokescript_contract.go", "ContractForInvokescriptTest", nil) } diff --git a/pkg/core/mpt/billet.go b/pkg/core/mpt/billet.go index b2e19c3c8..05212aafa 100644 --- a/pkg/core/mpt/billet.go +++ b/pkg/core/mpt/billet.go @@ -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 // returned from `process` function. It also replaces all HashNodes to their // "unhashed" counterparts until the stop condition is satisfied. -func (b *Billet) Traverse(process func(node Node, nodeBytes []byte) bool, ignoreStorageErr bool) error { - r, err := b.traverse(b.root, process, ignoreStorageErr) +func (b *Billet) Traverse(process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool) error { + r, err := b.traverse(b.root, []byte{}, []byte{}, process, ignoreStorageErr) if err != nil && !errors.Is(err, errStop) { return err } @@ -208,7 +208,7 @@ func (b *Billet) Traverse(process func(node Node, nodeBytes []byte) bool, ignore 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, process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool) (Node, error) { if _, ok := curr.(EmptyNode); ok { // We're not interested in EmptyNodes, and they do not affect the // traversal process, thus remain them untouched. @@ -222,30 +222,56 @@ func (b *Billet) traverse(curr Node, process func(node Node, nodeBytes []byte) b } return nil, err } - return b.traverse(r, process, ignoreStorageErr) + return b.traverse(r, path, from, process, ignoreStorageErr) } - bytes := slice.Copy(curr.Bytes()) - if process(curr, bytes) { - return curr, errStop + if len(from) == 0 { + bytes := slice.Copy(curr.Bytes()) + if process(fromNibbles(path), curr, bytes) { + return curr, errStop + } } switch n := curr.(type) { case *LeafNode: return b.tryCollapseLeaf(n), nil case *BranchNode: - for i := range n.Children { - r, err := b.traverse(n.Children[i], process, ignoreStorageErr) + var ( + startIndex byte + endIndex byte = childrenCount + ) + if len(from) != 0 { + endIndex = lastChild + startIndex, from = splitPath(from) + } + for i := startIndex; i < endIndex; i++ { + var newPath []byte + if i == lastChild { + newPath = path + } else { + newPath = append(path, i) + } + if i != startIndex { + from = []byte{} + } + r, err := b.traverse(n.Children[i], newPath, from, process, ignoreStorageErr) if err != nil { if !errors.Is(err, errStop) { return nil, err } n.Children[i] = r - return n, err + return b.tryCollapseBranch(n), err } n.Children[i] = r } return b.tryCollapseBranch(n), nil case *ExtensionNode: - r, err := b.traverse(n.next, process, ignoreStorageErr) + if len(from) != 0 && bytes.HasPrefix(from, n.key) { + from = from[len(n.key):] + } else if len(from) == 0 || bytes.Compare(n.key, from) > 0 { + from = []byte{} + } else { + return b.tryCollapseExtension(n), nil + } + r, err := b.traverse(n.next, append(path, n.key...), from, process, ignoreStorageErr) if err != nil && !errors.Is(err, errStop) { return nil, err } diff --git a/pkg/core/mpt/compat_test.go b/pkg/core/mpt/compat_test.go index f83273e80..bcd9be184 100644 --- a/pkg/core/mpt/compat_test.go +++ b/pkg/core/mpt/compat_test.go @@ -40,12 +40,13 @@ func prepareMPTCompat() *Trie { // There are some differences, though: // 1. In our implementation delete is silent, i.e. we do not return an error is the key is missing or empty. // However, we do return error when contents of hash node are missing from the store +// (corresponds to exception in C# implementation). However, if the key is too big, an error is returned // (corresponds to exception in C# implementation). // 2. In our implementation put returns error if something goes wrong, while C# implementation throws // an exception and returns nothing. // 3. In our implementation get does not immediately return error in case of an empty key. An error is returned // only if value is missing from the storage. C# implementation checks that key is not empty and throws an error -// otherwice. +// otherwice. However, if the key is too big, an error is returned (corresponds to exception in C# implementation). func TestCompatibility(t *testing.T) { mainTrie := prepareMPTCompat() @@ -59,6 +60,7 @@ func TestCompatibility(t *testing.T) { tr.testHas(t, []byte{0xac, 0x01, 0x00}, nil) tr.testHas(t, []byte{0xac, 0x99, 0x10}, nil) tr.testHas(t, []byte{0xac, 0xf1}, nil) + tr.testHas(t, make([]byte, MaxKeyLength), nil) }) t.Run("TryGetResolve", func(t *testing.T) { @@ -95,7 +97,8 @@ func TestCompatibility(t *testing.T) { require.NoError(t, tr.Delete(nil)) require.NoError(t, tr.Delete([]byte{0xac, 0x20})) - require.Error(t, tr.Delete([]byte{0xac, 0xf1})) // error for can't resolve + require.Error(t, tr.Delete([]byte{0xac, 0xf1})) // error for can't resolve + require.Error(t, tr.Delete(make([]byte, MaxKeyLength+1))) // error for too big key // In our implementation missing keys are ignored. require.NoError(t, tr.Delete([]byte{0xac})) @@ -202,6 +205,7 @@ func TestCompatibility(t *testing.T) { testGetProof(t, tr, nil, 0) testGetProof(t, tr, []byte{0xac, 0x01, 0x00}, 0) testGetProof(t, tr, []byte{0xac, 0xf1}, 0) + testGetProof(t, tr, make([]byte, MaxKeyLength), 0) }) t.Run("VerifyProof", func(t *testing.T) { @@ -374,3 +378,32 @@ func newFilledTrie(t *testing.T, args ...[]byte) *Trie { } 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("30"), 1) + }) + t.Run("non-matching", func(t *testing.T) { + check(t, []byte("60"), 0) + }) + }) + t.Run("from is in tree", func(t *testing.T) { + check(t, []byte("10"), 1) // without `from` key + }) + t.Run("from matching start", func(t *testing.T) { + check(t, []byte{}, 2) // without `from` key + }) +} diff --git a/pkg/core/mpt/proof.go b/pkg/core/mpt/proof.go index 8308a6f63..cc8b5876d 100644 --- a/pkg/core/mpt/proof.go +++ b/pkg/core/mpt/proof.go @@ -2,6 +2,7 @@ package mpt import ( "bytes" + "errors" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" @@ -13,6 +14,9 @@ import ( // Proof consist of serialized nodes occurring on path from the root to the leaf of key. func (t *Trie) GetProof(key []byte) ([][]byte, error) { var proof [][]byte + if len(key) > MaxKeyLength { + return nil, errors.New("key is too big") + } path := toNibbles(key) r, err := t.getProof(t.root, path, &proof) if err != nil { @@ -68,6 +72,9 @@ func VerifyProof(rh util.Uint256, key []byte, proofs [][]byte) ([]byte, bool) { // no errors in Put to memory store _ = tr.Store.Put(makeStorageKey(h[:]), proofs[i]) } - _, bs, err := tr.getWithPath(tr.root, path) - return bs, err == nil + _, leaf, _, err := tr.getWithPath(tr.root, path, true) + if err != nil { + return nil, false + } + return slice.Copy(leaf.(*LeafNode).value), true } diff --git a/pkg/core/mpt/trie.go b/pkg/core/mpt/trie.go index 59970a585..ac54fed65 100644 --- a/pkg/core/mpt/trie.go +++ b/pkg/core/mpt/trie.go @@ -49,50 +49,66 @@ func NewTrie(root Node, enableRefCount bool, store *storage.MemCachedStore) *Tri // Get returns value for the provided key in t. func (t *Trie) Get(key []byte) ([]byte, error) { + if len(key) > MaxKeyLength { + return nil, errors.New("key is too big") + } path := toNibbles(key) - r, bs, err := t.getWithPath(t.root, path) + r, leaf, _, err := t.getWithPath(t.root, path, true) if err != nil { return nil, err } 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. -// It also returns a current node with all hash nodes along the path -// replaced to their "unhashed" counterparts. -func (t *Trie) getWithPath(curr Node, path []byte) (Node, []byte, error) { +// getWithPath returns a current node with all hash nodes along the path replaced +// to their "unhashed" counterparts. It also returns node the provided path in a +// subtrie rooting in curr points to. In case of `strict` set to `false` the +// 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) { case *LeafNode: if len(path) == 0 { - return curr, slice.Copy(n.value), nil + return curr, n, []byte{}, nil } case *BranchNode: 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 { - return nil, nil, err + return nil, nil, nil, err } n.Children[i] = r - return n, bs, nil + return n, res, append([]byte{i}, prefix...), nil case EmptyNode: case *HashNode: if r, err := t.getFromStore(n.hash); err == nil { - return t.getWithPath(r, path) + return t.getWithPath(r, path, strict) } case *ExtensionNode: + if len(path) == 0 && !strict { + return curr, n.next, n.key, nil + } 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 { - return nil, nil, err + return nil, nil, nil, err } 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: panic("invalid MPT node type") } - return curr, nil, ErrNotFound + return curr, nil, nil, ErrNotFound } // Put puts key-value pair in t. @@ -235,6 +251,9 @@ func (t *Trie) putIntoNode(curr Node, path []byte, val Node) (Node, error) { // Delete removes key from trie. // It returns no error on missing key. func (t *Trie) Delete(key []byte) error { + if len(key) > MaxKeyLength { + return errors.New("key is too big") + } path := toNibbles(key) r, err := t.deleteFromNode(t.root, path) if err != nil { @@ -505,3 +524,64 @@ func collapse(depth int, node Node) Node { } return node } + +// Find returns list of storage key-value pairs whose key is prefixed by the specified +// prefix starting from the specified `prefix`+`from` path (not including the item at +// the specified `prefix`+`from` 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-len(prefix) { + return nil, errors.New("invalid from length") + } + prefixP := toNibbles(prefix) + fromP := []byte{} + if len(from) > 0 { + 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) + } + path = path[len(prefixP):] + + if len(fromP) > 0 { + if len(path) <= len(fromP) && bytes.HasPrefix(fromP, path) { + fromP = fromP[len(path):] + } else if len(path) > len(fromP) && bytes.HasPrefix(path, fromP) { + fromP = []byte{} + } else { + cmp := bytes.Compare(path, fromP) + switch { + case cmp < 0: + return []storage.KeyValue{}, nil + case cmp > 0: + fromP = []byte{} + } + } + } + + 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 { + if from == nil || !bytes.Equal(pathToNode, from) { // (*Billet).traverse includes `from` path into result if so. Need to filter out manually. + res = append(res, storage.KeyValue{ + Key: append(slice.Copy(prefix), pathToNode...), + Value: slice.Copy(leaf.value), + }) + count++ + } + } + return count >= max + } + _, err = b.traverse(start, path, fromP, process, false) + if err != nil && !errors.Is(err, errStop) { + return nil, err + } + return res, nil +} diff --git a/pkg/core/mpt/trie_test.go b/pkg/core/mpt/trie_test.go index 6c795bb95..81d7c9fe3 100644 --- a/pkg/core/mpt/trie_test.go +++ b/pkg/core/mpt/trie_test.go @@ -632,3 +632,58 @@ func TestTrie_Collapse(t *testing.T) { 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()) + }) + }) +} diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index edb9236a8..c66e9dc8d 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -38,7 +38,7 @@ type Management struct { } const ( - managementContractID = -1 + ManagementContractID = -1 prefixContract = 8 @@ -55,15 +55,15 @@ var ( keyMinimumDeploymentFee = []byte{20} ) -// makeContractKey creates a key from account script hash. -func makeContractKey(h util.Uint160) []byte { +// MakeContractKey creates a key from account script hash. +func MakeContractKey(h util.Uint160) []byte { return makeUint160Key(prefixContract, h) } // newManagement creates new Management native contract. func newManagement() *Management { var m = &Management{ - ContractMD: *interop.NewContractMD(nativenames.Management, managementContractID), + ContractMD: *interop.NewContractMD(nativenames.Management, ManagementContractID), contracts: make(map[util.Uint160]*state.Contract), nep17: make(map[util.Uint160]struct{}), } @@ -156,7 +156,7 @@ func (m *Management) GetContract(d dao.DAO, hash util.Uint160) (*state.Contract, func (m *Management) getContractFromDAO(d dao.DAO, hash util.Uint160) (*state.Contract, error) { contract := new(state.Contract) - key := makeContractKey(hash) + key := MakeContractKey(hash) err := getConvertibleFromDAO(m.ID, d, key, contract) if err != nil { return nil, err @@ -268,7 +268,7 @@ func (m *Management) markUpdated(h util.Uint160) { // It doesn't run _deploy method and doesn't emit notification. func (m *Management) Deploy(d dao.DAO, sender util.Uint160, neff *nef.File, manif *manifest.Manifest) (*state.Contract, error) { h := state.CreateContractHash(sender, neff.Checksum, manif.Name) - key := makeContractKey(h) + key := MakeContractKey(h) si := d.GetStorageItem(m.ID, key) if si != nil { return nil, errors.New("contract already exists") @@ -382,7 +382,7 @@ func (m *Management) Destroy(d dao.DAO, hash util.Uint160) error { if err != nil { return err } - key := makeContractKey(hash) + key := MakeContractKey(hash) err = d.DeleteStorageItem(m.ID, key) if err != nil { return err @@ -553,7 +553,7 @@ func (m *Management) Initialize(ic *interop.Context) error { // PutContractState saves given contract state into given DAO. func (m *Management) PutContractState(d dao.DAO, cs *state.Contract) error { - key := makeContractKey(cs.Hash) + key := MakeContractKey(cs.Hash) if err := putConvertibleToDAO(m.ID, d, key, cs); err != nil { return err } diff --git a/pkg/core/stateroot/module.go b/pkg/core/stateroot/module.go index 838e4717a..a96bac1f1 100644 --- a/pkg/core/stateroot/module.go +++ b/pkg/core/stateroot/module.go @@ -55,6 +55,22 @@ func NewModule(bc blockchainer.Blockchainer, log *zap.Logger, s *storage.MemCach } } +// GetState returns value at the specified key fom the MPT with the specified root. +func (s *Module) GetState(root util.Uint256, key []byte) ([]byte, error) { + tr := mpt.NewTrie(mpt.NewHashNode(root), false, storage.NewMemCachedStore(s.Store)) + 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. func (s *Module) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) { tr := mpt.NewTrie(mpt.NewHashNode(root), false, storage.NewMemCachedStore(s.Store)) diff --git a/pkg/core/statesync/module.go b/pkg/core/statesync/module.go index 801b51a97..adc6a389a 100644 --- a/pkg/core/statesync/module.go +++ b/pkg/core/statesync/module.go @@ -200,7 +200,7 @@ func (s *Module) defineSyncStage() error { zap.String("state root", header.PrevStateRoot.StringBE())) pool := NewPool() 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()) if !ok { // 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() 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). diff --git a/pkg/core/statesync_test.go b/pkg/core/statesync_test.go index 0751ab65e..c701c6700 100644 --- a/pkg/core/statesync_test.go +++ b/pkg/core/statesync_test.go @@ -220,16 +220,24 @@ func TestStateSyncModule_Init(t *testing.T) { require.Equal(t, uint32(stateSyncPoint), module.BlockHeight()) // add the rest of MPT nodes and jump to state + alreadyRequested := make(map[util.Uint256]struct{}) for { unknownHashes := module.GetUnknownMPTNodesBatch(1) // restore nodes one-by-one if len(unknownHashes) == 0 { break } + if _, ok := alreadyRequested[unknownHashes[0]]; ok { + t.Fatal("bug: node was requested twice") + } + alreadyRequested[unknownHashes[0]] = struct{}{} + var callbackCalled bool err := bcSpout.GetStateSyncModule().Traverse(unknownHashes[0], func(node mpt.Node, nodeBytes []byte) bool { require.NoError(t, module.AddMPTNodes([][]byte{slice.Copy(nodeBytes)})) + callbackCalled = true return true // add nodes one-by-one }) require.NoError(t, err) + require.True(t, callbackCalled) } // check that module is inactive and statejump is completed @@ -285,7 +293,6 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) { // make spout chain higher that latest state sync point require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock())) require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock())) - require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock())) require.Equal(t, uint32(stateSyncPoint+2), bcSpout.BlockHeight()) boltCfg := func(c *config.Config) { diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 5e0de0217..b0a02824e 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -1,7 +1,6 @@ package client import ( - "encoding/base64" "errors" "fmt" @@ -374,6 +373,38 @@ func (c *Client) GetRawTransactionVerbose(hash util.Uint256) (*result.Transactio return resp, nil } +// GetState returns historical contract storage item state by the given stateroot, +// historical contract hash and historical item key. +func (c *Client) GetState(stateroot util.Uint256, historicalContractHash util.Uint160, historicalKey []byte) ([]byte, error) { + var ( + params = request.NewRawParams(stateroot.StringLE(), historicalContractHash.StringLE(), historicalKey) + resp []byte + ) + if err := c.performRequest("getstate", params, &resp); err != nil { + return nil, err + } + 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. func (c *Client) GetStateHeight() (*result.StateHeight, error) { var ( @@ -388,12 +419,12 @@ func (c *Client) GetStateHeight() (*result.StateHeight, error) { // GetStorageByID returns the stored value, according to the contract ID and the stored key. func (c *Client) GetStorageByID(id int32, key []byte) ([]byte, error) { - return c.getStorage(request.NewRawParams(id, base64.StdEncoding.EncodeToString(key))) + return c.getStorage(request.NewRawParams(id, key)) } // GetStorageByHash returns the stored value, according to the contract script hash and the stored key. func (c *Client) GetStorageByHash(hash util.Uint160, key []byte) ([]byte, error) { - return c.getStorage(request.NewRawParams(hash.StringLE(), base64.StdEncoding.EncodeToString(key))) + return c.getStorage(request.NewRawParams(hash.StringLE(), key)) } func (c *Client) getStorage(params request.RawParams) ([]byte, error) { diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 874a7e303..bafb99d4e 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -25,6 +25,7 @@ import ( "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/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/response/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" @@ -675,6 +676,43 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ }, }, }, + "getstate": { + { + name: "positive", + invoke: func(c *Client) (interface{}, error) { + root, _ := util.Uint256DecodeStringLE("252e9d73d49c95c7618d40650da504e05183a1b2eed0685e42c360413c329170") + cHash, _ := util.Uint160DecodeStringLE("5c9e40a12055c6b9e3f72271c9779958c842135d") + return c.GetState(root, cHash, []byte("testkey")) + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":"dGVzdHZhbHVl"}`, + result: func(c *Client) interface{} { + return []byte("testvalue") + }, + }, + }, + "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": { { name: "positive", diff --git a/pkg/rpc/response/result/findstates.go b/pkg/rpc/response/result/findstates.go new file mode 100644 index 000000000..26821d334 --- /dev/null +++ b/pkg/rpc/response/result/findstates.go @@ -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"` +} diff --git a/pkg/rpc/rpc_config.go b/pkg/rpc/rpc_config.go index 17aedec0c..1785f80a8 100644 --- a/pkg/rpc/rpc_config.go +++ b/pkg/rpc/rpc_config.go @@ -14,6 +14,7 @@ type ( // can be spent during RPC call. MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"` MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"` + MaxFindResultItems int `yaml:"MaxFindResultItems"` Port uint16 `yaml:"Port"` TLSConfig TLSConfig `yaml:"TLSConfig"` } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 0f03f4307..ab9ac6ce1 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -24,6 +24,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/mempoolevent" "github.com/nspcc-dev/neo-go/pkg/core/mpt" + "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" @@ -44,6 +45,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "go.uber.org/zap" ) @@ -100,6 +102,7 @@ const ( var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){ "calculatenetworkfee": (*Server).calculateNetworkFee, + "findstates": (*Server).findStates, "getapplicationlog": (*Server).getApplicationLog, "getbestblockhash": (*Server).getBestBlockHash, "getblock": (*Server).getBlock, @@ -118,6 +121,7 @@ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *respon "getproof": (*Server).getProof, "getrawmempool": (*Server).getRawMempool, "getrawtransaction": (*Server).getrawtransaction, + "getstate": (*Server).getState, "getstateheight": (*Server).getStateHeight, "getstateroot": (*Server).getStateRoot, "getstorage": (*Server).getStorage, @@ -1026,6 +1030,148 @@ func (s *Server) verifyProof(ps request.Params) (interface{}, *response.Error) { return vp, nil } +func (s *Server) getState(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("'getstate' 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")) + } + key, err := ps.Value(2).GetBytesBase64() + if err != nil { + 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(key) > 0 { + if !bytes.HasPrefix(key, prefix) { + return nil, response.WrapErrorWithData(response.ErrInvalidParams, errors.New("key doesn't match prefix")) + } + key = key[len(prefix):] + } else { + // empty ("") key shouldn't exclude item matching prefix from the result + key = nil + } + } + 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) + kvs, err := s.chain.GetStateModule().FindStates(root, pKey, key, 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)) + csBytes, err := s.chain.GetStateModule().GetState(root, csKey) + if err != nil { + return nil, response.NewInternalServerError("failed to get historical contract state", err) + } + contract := new(state.Contract) + err = stackitem.DeserializeConvertible(csBytes, contract) + if err != nil { + return nil, response.NewInternalServerError("failed to deserialize historical contract state", err) + } + return contract, nil +} + func (s *Server) getStateHeight(_ request.Params) (interface{}, *response.Error) { var height = s.chain.BlockHeight() var stateHeight = s.chain.GetStateModule().CurrentValidatedHeight() diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index c93aa591a..c0fca9a7b 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -55,7 +55,7 @@ type rpcTestCase struct { } const testContractHash = "5c9e40a12055c6b9e3f72271c9779958c842135d" -const deploymentTxHash = "fefc10d2f7e323282cb50838174b68979b1794c1e5131f2b4737acbc5dde5932" +const deploymentTxHash = "8de63ea12ca8a9c5233ebf8664a442c881ae1bb83708d82da7fa1da2305ecf14" const genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4" const verifyContractHash = "f68822e4ecd93de334bdf1f7c409eda3431bcbd0" @@ -316,6 +316,70 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "getstate": { + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "invalid root", + params: `["0xabcdef"]`, + fail: true, + }, + { + name: "invalid contract", + params: `["0000000000000000000000000000000000000000000000000000000000000000", "0xabcdef"]`, + fail: true, + }, + { + name: "invalid key", + params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHash + `", "notabase64%"]`, + fail: true, + }, + { + name: "unknown contract", + params: `["0000000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000", "QQ=="]`, + fail: true, + }, + { + name: "unknown root/item", + params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHash + `", "QQ=="]`, + 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": { { name: "positive", @@ -347,7 +411,7 @@ var rpcTestCases = map[string][]rpcTestCase{ name: "positive", params: fmt.Sprintf(`["%s", "dGVzdGtleQ=="]`, testContractHash), result: func(e *executor) interface{} { - v := base64.StdEncoding.EncodeToString([]byte("testvalue")) + v := base64.StdEncoding.EncodeToString([]byte("newtestvalue")) return &v }, }, @@ -650,7 +714,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) expected := result.UnclaimedGas{ Address: testchain.MultisigScriptHash(), - Unclaimed: *big.NewInt(7500), + Unclaimed: *big.NewInt(8000), } assert.Equal(t, expected, *actual) }, @@ -1387,6 +1451,128 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] t.Run("ByHeight", func(t *testing.T) { testRoot(t, strconv.FormatInt(5, 10)) }) t.Run("ByHash", func(t *testing.T) { testRoot(t, `"`+chain.GetHeaderHash(5).StringLE()+`"`) }) }) + t.Run("getstate", func(t *testing.T) { + testGetState := func(t *testing.T, p string, expected string) { + rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getstate", "params": [%s]}`, p) + body := doRPCCall(rpc, httpSrv.URL, t) + rawRes := checkErrGetResult(t, body, false) + + var actual string + require.NoError(t, json.Unmarshal(rawRes, &actual)) + require.Equal(t, expected, actual) + } + t.Run("good: historical state", func(t *testing.T) { + root, err := e.chain.GetStateModule().GetStateRoot(4) + require.NoError(t, err) + // `testkey`-`testvalue` pair was put to the contract storage at block #3 + params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("testkey"))) + testGetState(t, params, base64.StdEncoding.EncodeToString([]byte("testvalue"))) + }) + t.Run("good: fresh state", func(t *testing.T) { + root, err := e.chain.GetStateModule().GetStateRoot(16) + require.NoError(t, err) + // `testkey`-`newtestvalue` pair was put to the contract storage at block #16 + params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("testkey"))) + 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: empty prefix, no limit", func(t *testing.T) { + // empty prefix should be considered as no prefix specified. + 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: empty 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) { block, _ := chain.GetBlock(chain.GetHeaderHash(1)) @@ -1430,7 +1616,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] require.NoErrorf(t, err, "could not parse response: %s", txOut) assert.Equal(t, *block.Transactions[0], actual.Transaction) - assert.Equal(t, 16, actual.Confirmations) + assert.Equal(t, 17, actual.Confirmations) assert.Equal(t, TXHash, actual.Transaction.Hash()) }) @@ -1543,12 +1729,12 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] require.NoError(t, json.Unmarshal(res, actual)) checkNep17TransfersAux(t, e, actual, sent, rcvd) } - t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{9, 10, 11, 12}, []int{2, 3}) }) + t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{10, 11, 12, 13}, []int{2, 3}) }) t.Run("no res", func(t *testing.T) { testNEP17T(t, 100, 100, 0, 0, []int{}, []int{}) }) - t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{6, 7}, []int{1}) }) - t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{9}, []int{2}) }) - t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{8, 9}, []int{2}) }) - t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{10, 11}, []int{3}) }) + t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{7, 8}, []int{1}) }) + t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{10}, []int{2}) }) + t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{9, 10}, []int{2}) }) + t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{11, 12}, []int{3}) }) }) } @@ -1651,8 +1837,8 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "57898138260", - LastUpdated: 15, + Amount: "57796785740", + LastUpdated: 16, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), } @@ -1661,7 +1847,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { } func checkNep17Transfers(t *testing.T, e *executor, acc interface{}) { - checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, []int{0, 1, 2, 3, 4, 5, 6, 7}) + checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, []int{0, 1, 2, 3, 4, 5, 6, 7}) } func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) { @@ -1670,6 +1856,11 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc rublesHash, err := util.Uint160DecodeStringLE(testContractHash) require.NoError(t, err) + blockPutNewTestValue, err := e.chain.GetBlock(e.chain.GetHeaderHash(16)) // invoke `put` method of `test_contract.go` with `testkey`, `newtestvalue` args + require.NoError(t, err) + require.Equal(t, 1, len(blockPutNewTestValue.Transactions)) + txPutNewTestValue := blockPutNewTestValue.Transactions[0] + blockSetRecord, err := e.chain.GetBlock(e.chain.GetHeaderHash(15)) // add type A record to `neo.com` domain via NNS require.NoError(t, err) require.Equal(t, 1, len(blockSetRecord.Transactions)) @@ -1746,6 +1937,14 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc // duplicate the Server method. expected := result.NEP17Transfers{ Sent: []result.NEP17Transfer{ + { + Timestamp: blockPutNewTestValue.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: big.NewInt(txPutNewTestValue.SystemFee + txPutNewTestValue.NetworkFee).String(), + Index: 16, + TxHash: blockPutNewTestValue.Hash(), + }, { Timestamp: blockSetRecord.Timestamp, Asset: e.chain.UtilityTokenHash(), diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index 0f48ffd78..7ce922f59 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ