Merge pull request #2207 from nspcc-dev/rpc/gethistoricalstate
rpc: implement `getstate` and `findstates` RPC methods
This commit is contained in:
commit
2bec43511d
21 changed files with 741 additions and 56 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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` |
|
||||
|
|
|
@ -52,6 +52,7 @@ func LoadFile(configPath string) (Config, error) {
|
|||
PingTimeout: 90,
|
||||
RPC: rpc.Config{
|
||||
MaxIteratorResultItems: 100,
|
||||
MaxFindResultItems: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
if len(from) == 0 {
|
||||
bytes := slice.Copy(curr.Bytes())
|
||||
if process(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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
@ -96,6 +98,7 @@ func TestCompatibility(t *testing.T) {
|
|||
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(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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
13
pkg/rpc/response/result/findstates.go
Normal file
13
pkg/rpc/response/result/findstates.go
Normal 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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(),
|
||||
|
|
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
Binary file not shown.
Loading…
Reference in a new issue