From c8120a139d9260843e75b169b99bef4c85923b2b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 6 Oct 2021 16:37:23 +0300 Subject: [PATCH 1/8] core: add MaxKeyLength restrictions to MPT's operations --- pkg/core/mpt/compat_test.go | 8 ++++++-- pkg/core/mpt/proof.go | 4 ++++ pkg/core/mpt/trie.go | 6 ++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pkg/core/mpt/compat_test.go b/pkg/core/mpt/compat_test.go index f83273e80..707b7987e 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) { diff --git a/pkg/core/mpt/proof.go b/pkg/core/mpt/proof.go index 8308a6f63..d3cfb9134 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 { diff --git a/pkg/core/mpt/trie.go b/pkg/core/mpt/trie.go index 59970a585..678dab26a 100644 --- a/pkg/core/mpt/trie.go +++ b/pkg/core/mpt/trie.go @@ -49,6 +49,9 @@ 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) if err != nil { @@ -235,6 +238,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 { From 01143da6213fd4d757c13195495f7e8b590a5191 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 7 Oct 2021 12:03:37 +0300 Subject: [PATCH 2/8] rpc: add `getstate` RPC handler --- docs/rpc.md | 1 + pkg/core/blockchainer/state_root.go | 1 + pkg/core/helper_test.go | 14 ++++ pkg/core/native/management.go | 16 ++--- pkg/core/stateroot/module.go | 6 ++ pkg/core/statesync_test.go | 1 - pkg/rpc/client/rpc.go | 13 ++++ pkg/rpc/client/rpc_test.go | 14 ++++ pkg/rpc/server/server.go | 43 +++++++++++ pkg/rpc/server/server_test.go | 94 +++++++++++++++++++++---- pkg/rpc/server/testdata/testblocks.acc | Bin 23934 -> 24617 bytes 11 files changed, 182 insertions(+), 21 deletions(-) diff --git a/docs/rpc.md b/docs/rpc.md index 7790bad2b..dce0b0632 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -54,6 +54,7 @@ which would yield the response: | `getproof` | | `getrawmempool` | | `getrawtransaction` | +| `getstate` | | `getstateheight` | | `getstateroot` | | `getstorage` | diff --git a/pkg/core/blockchainer/state_root.go b/pkg/core/blockchainer/state_root.go index 9a540bda8..d3b4b76d0 100644 --- a/pkg/core/blockchainer/state_root.go +++ b/pkg/core/blockchainer/state_root.go @@ -13,6 +13,7 @@ type StateRoot interface { CurrentLocalHeight() uint32 CurrentLocalStateRoot() util.Uint256 CurrentValidatedHeight() uint32 + 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..c2ffcbd91 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -501,6 +501,20 @@ 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") + + 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/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..3447524c2 100644 --- a/pkg/core/stateroot/module.go +++ b/pkg/core/stateroot/module.go @@ -55,6 +55,12 @@ 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) +} + // 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_test.go b/pkg/core/statesync_test.go index 0751ab65e..9f7ac6bd4 100644 --- a/pkg/core/statesync_test.go +++ b/pkg/core/statesync_test.go @@ -285,7 +285,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 1792d6ce5..69559962b 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -374,6 +374,19 @@ 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 +} + // GetStateHeight returns current validated and local node state height. func (c *Client) GetStateHeight() (*result.StateHeight, error) { var ( diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 4641f2517..78ab119d4 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -675,6 +675,20 @@ 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") + }, + }, + }, "getstateheight": { { name: "positive", diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 0f03f4307..b2c42e481 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" ) @@ -118,6 +120,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 +1029,46 @@ 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")) + } + 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) + } + sKey := makeStorageKey(contract.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) 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..c273a2774 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 = "cb17eac9594d7ffa318545ab36e3227eedf30b4d13d76d3b49c94243fb3b2bde" const genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4" const verifyContractHash = "f68822e4ecd93de334bdf1f7c409eda3431bcbd0" @@ -316,6 +316,38 @@ 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, + }, + }, "getstateheight": { { name: "positive", @@ -347,7 +379,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 +682,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 +1419,31 @@ 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("getrawtransaction", func(t *testing.T) { block, _ := chain.GetBlock(chain.GetHeaderHash(1)) @@ -1430,7 +1487,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 +1600,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 +1708,8 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "57898138260", - LastUpdated: 15, + Amount: "57796933740", + LastUpdated: 16, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), } @@ -1661,7 +1718,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 +1727,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 +1808,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 0f48ffd7870602f42558a1405234f506559d94f7..e025c1364dc77caacde64cf1f6a77ffccdd927ce 100644 GIT binary patch delta 5888 zcmc(jXEfYV+wP5CM(-w)A=>CfucM6KyHP^aC_#wM=p`h|KRQ9w=siS|35JB|QGzHT zBp8C|GMvcsoU_h)KcBVE{<6RAwXWZN?R#JMzI+XO`5pwK$pGmR=0$E=XBfdyShBhV zG?~Nayi_;ys>F=j6Xh}AC!uG>fd%~~Jd_045gl)!J2i&Pmz2LawEBUj|QlSUon)z%QmEKvB=4S@FiOi zhrC{0u45|XtvT3m{jHX3?FvIEd+~$eFdvBzDc}R+^&HnQEFEs4wNBQe+Topepf&Qu zO1faN^K;XSfx=)LoqJmL8_qO44a`w}dYfme?Dh)6v?ooOudunDX`^FSY@6=yY(#Hx_Zd&dn!{0wr{=`7c5ijI+$LC@U&(Qp2Ji&sMOlHL|(BlKKgxZ8ZE>PPwze>-V$ zx!JdmCZ-m@-g$HS{aY#>vynJvJNlcuW30?m=Ro6WY}nOGwXK0c8m6uaxN2mxJQGGd zMLz_?$$lZB*7a+?A`xML+EC_t^VTWU6v-`pcUIua)%7uCpCN6%mWfSAuI*WPa(E^{ZhA1kpEqFg#7doCoD9x3m3| zy}t)%QMtwuLxad_U7wv?EO^!P471!!y~RcDsdukY@K{d3r=TQOQ2Sj;64hD#FC>SW zJ>kEr>C6NIL2Vv%g`76DoIaH;`X+a1<$J^<4YVltHgBvx7dY2$lmgJyPGyjW%%?6lGddZ@o{UU^tK(IV47dN`+^Hg zJGPQlU$1BAYPcM$ErQ`6D%LI2FmHe?3-9RO->iqpCfj|oK|pTkORu=GlRHm$V=PL? z(X0_9SADrhcBfs+lOvYOW=f!v2L@#}h`!BDFueYq>G3@+RU`pqCHNPJQey!J|J<)D zYn}tNOtBf$Di3V#+Pa~sWYLy!L!IKw%$LQ|@oq8c+6;Xzx2C&K1^$WWXYB()P00fd z34A$b8TooLHr?7grIV#&ojY$cdN_cGX(&v&0ZIsz@cFfPF$fGb#!tbx%#!2jad~kO zcBRI&fU5tLz83wC9ivrr3^C9cmB8!xqGQM*G3Dn=>-OYD(#Ky1Iww8Ou8N}1V{kR~ zV0eXnKA~2KU#x*Wn*|ezQf9f48h5B_TZ=?hZ6)*l2~e0J0|vc&+|Wk6*C>&UwztZs zDb9?@3ADSR@hlHfe{&rSpC7;7yZ^Sgt;$q>?Cji!6Q|?n!hp*8Hc;&Bb`32=4ShRlyP3&ODC_aE$dnFaVSH)E562Z z%nn1Bl)z=e#vnbmSv0wUCiRP8Nb6>)CKI)S3HyGJ6 zjfvW7!W}!yf{OP4;Cze}ni&fgty$bfBP$S36#T$&BUSG9jE@^Ul#VHq>+(ods1z6EczgZDYCHVDEE9&@b2!nsBv75C;SKi%nAkZQ%RlBdqAqx7{!gvkfpw=B zw&kCs7m?JA%EoW>EP*mC)Zb=0`F-R{KuN${gV6lWKk**7|C&O#;{ohTn`*nEpPp^+ z<@C}t$HNuz@Iy`TpJ|nEF8ou{+~MtL$Io#ne2U}m!!Gvf`zN#E5&jVE$GV{pDpY%)Gj=Ce8?oR}F>Y`rHX-C?A zPsr8%vpe#BxGp^QJ+c1L>htD7yjNm<{8(;t&iMov(9GJ6BnubD#4Rz+rl& z&h5oVrF_9YPrSlrS&(*2imCzIoRhq`IWA^rNaLuo_amQH7ysUz33<4#%7-<z_p%(9Fu5>99=k@cVx+<^<`N-MceEwoa#{g7;Tc z-v4hUsQ)KQz;NW@ll{p$<4kk&G=}?w=AnKmPJER*!cPX|29q{yu#yX%hm^_MYi1?n z<5>K)LkP6XIryxX$DOo>JLuQkU6dqX1Y1@2p&AUTRbzboTA^P5LmEumK+$$oxx<+ixP71;@b3q{4ms_?T&Co zEsJZ`A&!z;9$~(<&$WLGtP$;#=1yx=IXnCKu!Co7&t)cbr_woKU*zOeSiV(wG83!X z^O9@cdEW*MHyeHaV@u04CLW3PXH#?;-6L}+(~6bp)#>@9KS}3uRvNm^ms#ZdxiFuw zP3NZd?Hlu0AkHHpNi8JCF2j4W95~;kI8T1O5gMS*9=Ys^6}LPB!X>f6^kIw zX4Vv=!}smiTBiI!MY)|LXC;k~PkrsCo_TZfW5ddroX8RuIa#NbwYV!lfk_c!-LmR0 zC*np@V*YZO7KWjS5<`&k{aaEJEu(j70!$J|B}nr81!!3xJc|E*8)8tMZuC`S+Vx^qk-Q#n$5OD(_ zI%OzIh37yt#2oUHU-Id_LdvM=u9gr2w3f}`VjJQ4`=%Devht0;M<~$zq!oMPY8!+z zS?_X7(Qq1mG_6)Y+cm3<$E7c10NCpE~sb_%lolGE1HU>b+2LY}!QNXFZ>%TBRaVHRn zlFkMU#R33paTDOXOb&pPwfP(51OJA5T7X9e4)lm+{|)<0zymoI04L!KG|C|XO^G@H z3I(@%D8$jjQ?nAjYNJ29&^u{c8ExP8C$4@Mm=!72mlj&Clt-%wm-yJI8|aGaw9gLu z4RWKCEq4wWzVq^C12Zjw#8xb))^*6Yens$~>FFbHA2pfUOb6y^L8dtUh$VKq12ZVR(M%#y&}7gj6|hHed`5UH!zlN(`;27jg&lL?0(AvQ|t#`w=e(O?zt;}8N|B@4z#W-3m#$v;KTveNyS+otHO8V+`vZAnbFTggYsgs;O;&+EVDyZ)<;_5;=dy z_D#~8;?B6!>Wc}w_);*O>MlaW@0A(waP@%^>D41+Wz#j)fDu3Vcs#irqf%eOgxXx& zmX&sDg<%L;2+w!ZS5^9;H0OYPd`7cRGYGa-dN4fq{gD6;)aclc&K`MRA1kA9I`#5l zLh=BqClzr%Q-5u`U^)`pX=c7syBqOH?fC(+CYqUEw3pqFma>vYW;^IwlpZDFe~OR9 zrC3mOXXf?KEjZkJyq2WR)Bd|GAfR1e#KHGbLrmMCkL&Hsn;3iwM+&;Zp3o%S_P4op zp>HSlyaQ<0_d=qcT;FJWGZ>25XqZ|~RW)#EW}L2plAcHbPWYNG)w_ehWHg&z8|P=^ z;f6gw!!+^JbsY8(2+GtLjL*zS+@%qfmo&U;w}}D6d4y!Hc;r?0f5hw^{A|^xK57*2 zPX0!Z@47+|OK?0lQQ{hU7itzs@w!>*-AvX3C(B7_;NARS8evfN!a9OAB=&D|BD5+> z%7QpN(?Wkw34Qb=IIZzd#Xz^dz#G^1x)#3*1Bl3n$Iq7XBONkB>nw@uk*b87v0Ea~ zNVL9?4(G?+{BON~C>9j}>*n^Tcu{fLjLMKAFA}7qB@xp@Valq{dQ0Iss=D0uGi>Wi za~y<(+%cbvGE#*ZJRL-oPtO|Ha`g8cHG58^Lc+}d{+OuQ1Y*WI zcUFImu%E5bw{(@-{Y)8KtUR37vCb)%FAQt$AtpAEQ-E_E9n5}VLI2+dCW<%T;G}lcaI{9 z_9DQ%;p-(A><7bn&s#pwR#=4owy4$1?L9ECWoV=VRNj%HyQBBN>;D zBMbFE?jLY(ce|}qIlWTo*NQsWvVzIw#s^Q?-a5OTI2>;w*FDDw`IwdKhsb?;Z5bac z7%Ckes3y4w(&_AxwhR($TEOi1peRH;|4vG7)SW& za_kFNs)xR3@$=A3N>}{v{}H;rYp%dNb~z z;PIx332Cb)|Cp~9>I+1WB}dXL#rK_HSA|oaFC6)4|D!Ea|7*)9R58WD{H)N#&UeDh z=w0tUBJQCf+^mwXa6y&0@oup{q2wOFw!f_E)yKQBwwtGSOM6wDD zx5jX7vvldIoVvJ6jdO7_9e?*Gl#U`K%3?YU1=(AFoc+QJ7v!uDogr<($M9`P`JQ4Y zQ-+rXCO-6+=OeZczk=aW%VR>+(ZuM0N4D-I0H=gKyA zI~Kj}M9g2f4ZiAf12I^&SjoFx-3oiOb?6g!&725?$7G@jRNH#P15hdZs{QR*P+cF6 zpD-o!0cn9!WE^9kdRIZA!tb14@|#uGOSmUU9K^e$#2Xyi4397}&Y)WKRUpq`OzzY8 z_Zn9H*WFDNX^xtN@u|^L)LU6%N(z0t`_s=t%1mO5+fn$ES$x{2@h{M-l~b3gMZ-fI zPEiMh(^0#TLnHXrXKPu>c1h{~13S$>?88c>&nl_n%cqxkkW}^pc-H#NjrW7~O;ZZ$ zq6of*#(WWUu6x=umMy;@e<5>&ZeBA}X3%MNxd^FzBtmNY>;V`qu%qeZ`g&KdT>Qfh z%syIHfwRbn^tXE!6D8PJFw-EHgZAL{wD6Bjg$eAiIFw$lV+UUQ13i)Z9f@1VvMw@t5iNbrWlHiEA26EIhXG5lY!u16|~DxB9LqyxY| z28WjrppLV}rvTl`F3UjOK0gJc23d4U`zbD?hz5XsR*|#V!|r`$Ej}Vy-T63v7Pc(E z#vZ0`sichlWxS`D*APCz_XGkol|WBVQVwvlE7+zm(uk{|SP;7y9Z#uZwjVsi9g@1E zw`J4F#gU6Hdrul?b-bja4#GFI<#nF-KVe5=`{{iGYdKTuC(#!%weKaB++vzVXq=#U z{krI-@9S-U$HR4V$88vU63U)Rp&?|vWY%6KuFTCuzS)yPot5sOMlFHY$iv0Dem->% zbrb$qmOym0|2`PtAq9PiR;!Ep1nOvd@!MiWP%OAAPn#{&g1^Y`%8 z^DBEn9;8SkYND*bmr5;cy$t6ArqC0JMuDn*M@#maW*dVPkL?Bz9J~{%s0E6clK*THi~iy8xI6{7i15r_Is(dg26QpWI@DMbjNMznpH+^kJu zYSIZwk*KdTJG_NK4;1<#v$F7hyC`=U}F z+{NLapM~KMB zD<7X#)|J$jd;R2#pn9e{KSO((V}UH_pgns3D{-Cbyn?kB0wl@|j$oX*sIgIe(-Hjf zmbNVfh6{Cdn396IdDf&yz*0hI=0%2FIT})lMEyc#Pm2^f-~&Gx^3TZ2wCzZJ&#yu>yJyGo~5HxYz5l4y^D6Q zLFUc3=DJ^*UyT@dbGp=0r7s&s@=U&l0PBLLwd(}r1`a^s3LQyr#|0vYGu6DDSL9(3 zGe8TVmU#N3ID@hKlBAWbyhp0#Tc~8Rh~;Ntr>GQ$M)fY)-yZhrEbrO2@VKI_<|h=<*LK?|rU?Fle{jDK9r}|i)AfgG z5O+Zjq3j@2$Ix5GbvNC(oFwzusgk1u8)1-$4vH0h2QNqm3sIrh4ZJ0;Mmp;|K43{J z@A2jBPf|UEkLRR`L)y#1xx6tM1juzO;^5t2mcIIOt~CRo+P^P)-9^teOM% z>Qe&c##pE-;5ap>Kv)+!- zm(snCio5DbzD!}Gd+Qkf8L-bZu^C-^ljc4oVY_yiMDVti#tCKvc^u{lRX<>4MqV>XQOt<-S4FF^qJ~ zvNtS31Q*LGvIUh8;I`{%d_71!t$b8Rb}TBNMUlXOosmW|fSm0hRCx2~oV+*UWi(n} z?BHyL^dENI!?)WAtjCaqbL+BaI~VZI&~6Bzjp+Y{Y_!{=<7a`C)F(M>*!SWfyX6Zr ztM~*eL?Wup;`jF46L?~_pXV@oyf@1wvsg*)@ixfozjV&XKRti*PbPes_%NWK;=e8Z zVbh}`0Nl@)rr9G2!XF#hG<>99Mbzt=n~?+H05cRT<_>;`85WXr>PBi$STMV$#$5Dz zm&xbc7ow7a#)Pk?ckFsz+UY9Yt0Pt2vt}FqJ>0QCpqdYDB0aqs66C?{7j%W{@V|lp z4Sp_Sy28D#A9>4Kccd_rbZv!nneS;JZTbo(sRRj^!d!;adJCq9CTtR~T`|DNSd-3! zyqd}*Vd4dKil_GakwIDrCQjzCa7R! zq^-3WQI|HHA<~}4E`MOffATKoakmRHs~aT6POW!h{hJBn-5H_?R;Guf)pXUzw!BkY z_L{#({Hp>C2-;@*`FVsiTV!DJ)`n}xb$!Q~pQk5V8R<-p#=1Ax@+=!UqH%hCI@Mk~= zt+r}AwfGx!aVbUDKFw;t4`1K&v+WMPmSn}Gj>`o{JxcofXffzCf4c(q@TX(-vJZrS z+8o&--cD8{k4h~xz1y>hvM%ghPyrShI%T1jh_m8ipWJEF3ugp%&s*yIf` zwTY-RWGcyZQZOWswao9k3NP$B<4NzddA8~E{2a; z-u(O?(^Npvs>=y`CMfv@6Z^Xto(t7UgdAfGycCx zLjF%kAV6mHldR8E+fPj;6ZHl}-V`GS=&ZjSTpzn(!1kOS&W@(NWSe9{hSJ7;8~T`v z*|`XDF#_>Rsi*NQ79|R616dYsx68 z8uP$XRG22N4c&8M?fiQ^u!_~WhT9DQwfDFWE#r1Zb~nX?p@QYuTz4pId>r-T9J@Ws z*sBQwFs3T(G!9*1Puk%3@5s85#8cwq-@KcPI&7Q9|LMoQcu2gHv+>5*pSMKz5isWB zFj%w{?b<{$$9!OenE>Z{y9xoUStK9ZU9RtEr@z8jKbaC~!u`tVS-H?8)P#DKt5@Um zt-CDrTW+1IG>I$kdIozA-ut0OqSj%@`{`XWKgQ}u|Cxg4+?g1Pt|NKq%}c*EShJ(; zC{hBZ9P3_@t{gXoHY~H_-yizkz6OyvU*kXamO62zU1(AL-Ysz9w=||nulP15RvfxzVFXGa;FPu zAaDf+i$4diB~!qCaXnB&Fa`WA{2KHS^ZC25lY;|N8K4Iq0l59p16&i-19yb{Krx|u z&`LN3{2`VGw&ULe+av)nR5TP+lt={;BH@3(almB>eb5%43M_pn2O3G4{4D_#;A2T^ z5MR_6yb*P{Tlnjpq$A0Jz9sHy_pkmih1Ms0HA7=+4+5nED6Vf)h@UgMvnwap&6!)! z_lB^qqdpTcWZK0e=f`6^M(BPKEs1t_4i%CP-7WjZO_={~QJxbSyh8=UWzn0f+#_PP z7XA)eH}p^E)^oEbWdEeSz^nvPkLJN5$ICUJdSQJDVuBc%Ogs9W8&$6{aO1w<<4y! zbhHZOzykO{Cn@A}TBl5D=Nsub{&!z&x2c%Sq-ApIsmgRb0G3&)7#3#Z!`u_xB7u&w z_|wBEtUi(A4Uh#|zWTMl7=4X$5a*rt z?9@o3vZA@K{BunWk&PxYw&4QrvJTvdNg6Lt)2__|x!As@My9?^qq)0C*MaYqw$9!_%X}HhHQW!#0*Fk7h|Mfswa= zfCrTye@*SB)1_vpj|kegQ4o#~CEVR^b3IrHZryJ$_1d3rTb32QZ|fO5Um9Y6{(+*4 zPvxI&=8qa~80r(z{Y7E(D~`ji=2oN_?Va#Q=`$bO*G=bbhgHdNHV9B;vdeqlelVYM zQ{MYRRCDbn>wckcgVPed{P|27JZi|a?LaWclAL;x{y4d_-#1t^Y(K$Ctj5FS!l;`7 z&T8_vOK$pcz0Bl@!%l3+?EaA$E~7h-U2gwLy7u(Zr{WTUk36mXqmWg_ zbS!D^-7)>4;??p|Gfh_SS#XSiK+i~J+-+cjU`6~3m*i2k#D(fynSKa>Sq;&+M^9tr zK7z-&^@!-A|Fw1VjNM*9ME0s~xns0s1Zyzci{4q-*n*3tFLa+!b4StWrSInvl`Ycn z^N{w56$rpyqz8k|88L~Ms@=#I#I9x`N&fgW=P)4mr>!fwo|pwx{izCjp4NcJQ;lX* zApcPPKyH5u6Sh&1W_irrkoI?j5-D_}-m(P*x=x+Fk6I2zFEv?72y|Px5EG1MoH`Nh z7o^$gRVO(gtZ(u?_E(P(uC3|6E2qc_@Ma{eH{3NcP5=HNzdxzL_;+L+vANJakLJ1@%O`!OIoPN@&?w z$|rkJ-eT>Ex%67jTV_zoMFX#{?i{gi`L&M7@U4A;a|yn-&rWi{b?K@*laXJy`hJV; ze; zB1RT48k&D$tPzM&*3w6k?m$E|nj^7E8ckH)7}tKbU!LNUC(Q=G`d?$20E&u=k From 7da394fd3fb62ed999f69d29ee9e003c2f350782 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 12 Oct 2021 13:49:47 +0300 Subject: [PATCH 3/8] core: check MPT node is not requested twice by StateSync module This check prevents infinite loop if something goes wrong with MPT nodes restore process. --- pkg/core/statesync_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/core/statesync_test.go b/pkg/core/statesync_test.go index 9f7ac6bd4..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 From 43ac4e15177174755cfea8c7d236240984df952b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 7 Oct 2021 16:56:27 +0300 Subject: [PATCH 4/8] rpc: implement `findstates` RPC handler --- docs/node-configuration.md | 7 ++ docs/rpc.md | 1 + pkg/config/config.go | 1 + pkg/core/blockchainer/state_root.go | 2 + pkg/core/helper_test.go | 4 + pkg/core/mpt/billet.go | 60 ++++++++++--- pkg/core/mpt/compat_test.go | 26 ++++++ pkg/core/mpt/proof.go | 7 +- pkg/core/mpt/trie.go | 88 ++++++++++++++---- pkg/core/mpt/trie_test.go | 55 ++++++++++++ pkg/core/stateroot/module.go | 10 +++ pkg/core/statesync/module.go | 6 +- pkg/rpc/client/rpc.go | 19 ++++ pkg/rpc/client/rpc_test.go | 24 +++++ pkg/rpc/response/result/findstates.go | 13 +++ pkg/rpc/rpc_config.go | 1 + pkg/rpc/server/server.go | 110 +++++++++++++++++++++-- pkg/rpc/server/server_test.go | 119 ++++++++++++++++++++++++- pkg/rpc/server/testdata/testblocks.acc | Bin 24617 -> 24765 bytes 19 files changed, 515 insertions(+), 38 deletions(-) create mode 100644 pkg/rpc/response/result/findstates.go 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 dce0b0632..49341809e 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -36,6 +36,7 @@ which would yield the response: | Method | | ------- | | `calculatenetworkfee` | +| `findstates` | | `getapplicationlog` | | `getbestblockhash` | | `getblock` | 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 d3b4b76d0..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,7 @@ 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) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index c2ffcbd91..182f88544 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -504,6 +504,10 @@ func initBasicChain(t *testing.T, bc *Blockchain) { // Invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call 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() diff --git a/pkg/core/mpt/billet.go b/pkg/core/mpt/billet.go index b2e19c3c8..4b72a55ca 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{}, 0, 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, offset int, 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,18 +222,45 @@ 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, offset, process, ignoreStorageErr) } - bytes := slice.Copy(curr.Bytes()) - if process(curr, bytes) { - return curr, errStop + if _, ok := curr.(*LeafNode); !ok || len(from) <= offset && (len(from) == 0 || !bytes.Equal(path, from)) { + 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) + if len(from) > offset { + startIndex := from[offset] + for i := startIndex; i < lastChild; i++ { + newOffset := len(from) + if i == startIndex { + newOffset = offset + 1 + } + r, err := b.traverse(n.Children[i], append(path, i), from, newOffset, process, ignoreStorageErr) + if err != nil { + if !errors.Is(err, errStop) { + return nil, err + } + n.Children[i] = r + return n, err + } + n.Children[i] = r + } + return b.tryCollapseBranch(n), nil + } + for i, child := range n.Children { + var newPath []byte + if i == lastChild { + newPath = path + } else { + newPath = append(path, byte(i)) + } + r, err := b.traverse(child, newPath, from, offset, process, ignoreStorageErr) if err != nil { if !errors.Is(err, errStop) { return nil, err @@ -245,12 +272,23 @@ func (b *Billet) traverse(curr Node, process func(node Node, nodeBytes []byte) b } return b.tryCollapseBranch(n), nil case *ExtensionNode: - r, err := b.traverse(n.next, process, ignoreStorageErr) + var newOffset int + if len(from) > offset && bytes.HasPrefix(from[offset:], n.key) { + newOffset = offset + len(n.key) + } else if len(from) <= offset || bytes.Compare(n.key, from[offset:]) > 0 { + newOffset = len(from) + } else { + return b.tryCollapseExtension(n), nil + } + r, err := b.traverse(n.next, append(path, n.key...), from, newOffset, process, ignoreStorageErr) if err != nil && !errors.Is(err, errStop) { return nil, err } n.next = r - return b.tryCollapseExtension(n), err + if err != nil { + return n, err + } + return b.tryCollapseExtension(n), nil default: return nil, ErrNotFound } diff --git a/pkg/core/mpt/compat_test.go b/pkg/core/mpt/compat_test.go index 707b7987e..e69bc2e29 100644 --- a/pkg/core/mpt/compat_test.go +++ b/pkg/core/mpt/compat_test.go @@ -378,3 +378,29 @@ 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("aa30"), 1) + }) + t.Run("non-matching", func(t *testing.T) { + check(t, []byte("aa60"), 0) + }) + }) + t.Run("from is in tree", func(t *testing.T) { + check(t, []byte("aa10"), 1) // without `from` key + }) +} diff --git a/pkg/core/mpt/proof.go b/pkg/core/mpt/proof.go index d3cfb9134..cc8b5876d 100644 --- a/pkg/core/mpt/proof.go +++ b/pkg/core/mpt/proof.go @@ -72,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 678dab26a..efe402233 100644 --- a/pkg/core/mpt/trie.go +++ b/pkg/core/mpt/trie.go @@ -53,49 +53,62 @@ func (t *Trie) Get(key []byte) ([]byte, error) { 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. @@ -511,3 +524,48 @@ 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 path (not including the item at the specified path +// if so). The `max` number of elements is returned at max. +func (t *Trie) Find(prefix, from []byte, max int) ([]storage.KeyValue, error) { + if len(prefix) > MaxKeyLength { + return nil, errors.New("invalid prefix length") + } + if len(from) > MaxKeyLength { + return nil, errors.New("invalid from length") + } + prefixP := toNibbles(prefix) + fromP := []byte{} + if len(from) > 0 { + if !bytes.HasPrefix(from, prefix) { + return nil, errors.New("`from` argument doesn't match specified prefix") + } + fromP = toNibbles(from) + } + _, start, path, err := t.getWithPath(t.root, prefixP, false) + if err != nil { + return nil, fmt.Errorf("failed to determine the start node: %w", err) + } + + var ( + res []storage.KeyValue + count int + ) + b := NewBillet(t.root.Hash(), false, t.Store) + process := func(pathToNode []byte, node Node, _ []byte) bool { + if leaf, ok := node.(*LeafNode); ok { + res = append(res, storage.KeyValue{ + Key: pathToNode, + Value: slice.Copy(leaf.value), + }) + count++ + } + return count >= max + } + _, err = b.traverse(start, path, fromP, len(path), process, false) + if err != nil && !errors.Is(err, errStop) { + return nil, err + } + return res, nil +} 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/stateroot/module.go b/pkg/core/stateroot/module.go index 3447524c2..a96bac1f1 100644 --- a/pkg/core/stateroot/module.go +++ b/pkg/core/stateroot/module.go @@ -61,6 +61,16 @@ func (s *Module) GetState(root util.Uint256, key []byte) ([]byte, error) { 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/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 69559962b..b3cba29dc 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -387,6 +387,25 @@ func (c *Client) GetState(stateroot util.Uint256, historicalContractHash util.Ui 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 ( diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 78ab119d4..bb13cf088 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" @@ -689,6 +690,29 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ }, }, }, + "findstates": { + { + name: "positive", + invoke: func(c *Client) (interface{}, error) { + root, _ := util.Uint256DecodeStringLE("252e9d73d49c95c7618d40650da504e05183a1b2eed0685e42c360413c329170") + cHash, _ := util.Uint160DecodeStringLE("5c9e40a12055c6b9e3f72271c9779958c842135d") + count := 1 + return c.FindStates(root, cHash, []byte("aa"), []byte("aa00"), &count) + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"results":[{"key":"YWExMA==","value":"djI="}],"firstProof":"CAEAAABhYTEwCXIAA5KNHjQ1+LFX4lQBLjMAhaLtTJnfuI86O7WnNdlshsYWBAQEBAQEBAQDTzD7MJp2KW6E8BNVjjjgZMTAjI/GI3ZrTmR2UUOtSeIEBAQEBAPKPqb0qnb4Ywz6gqpNKCUNQsfBmAnKc5p3dxokSQRpwgRSAAQDPplG1wee4KOfkehaF94R5uoKSgvQL1j5gkFTN4ywYaIEBAOhOyI39MZfoKc940g57XeqwRnxh7P62fKjnfEtBzQxHQQEBAQEBAQEBAQEBCkBBgAAAAAAAAM6A1UrwFYZAEMfe6go3jX25xz2sHsovQ2UO/UHqZZOXLIABAOwg7pkXyaTR85yQIvYnoGaG/OVRLRHOj+nhZnXb6dVtAQEBAPnciBUp3uspLQTajKTlAxgrNe+3tlqlbwlNRkz0eNmhQMzoMcWOFi9nCyn+eM5lA6Pq67DxzTQDlHljh8g8kRtJAPq9hxzTgreK0qDTavsethixguZYfV7wDmKfumMglnoqQQEBAQEBAM1x2dVBdf5BJ0Xvw2qqhvpKqxdHb8/HMFWiXkJj1uAAQQEJgEDAQYBA5kV2WLkgey9C5z6gZT69VLKcEuwyY8P853rNtGhT3NeUgAEBAQDiX59K9PuJ5RE7Z1uj7q/QJ8FGf8avLdWM7hwmWkVH2gEBAQEBAQEBAQEBAQD1SubX5XhFHcUOWdUzg1bXmDwWJwt+wpU3FOdFkU1PXBSAAQDHCzfEQyqwOO263EE6HER1vWDrwz8JiEHEOXfZ3kX7NYEBAQDEH++Hy8wBcniKuWVevaAwzHCh60kzncU30E5fDC3gJsEBAQEBAQEBAQEBCUBAgMAA1wt18LbxMKdYcJ+nEDMMWZbRsu550l8HGhcYhpl6DjSBAICdjI=","truncated":true}}`, + result: func(c *Client) interface{} { + proofB, _ := base64.StdEncoding.DecodeString("CAEAAABhYTEwCXIAA5KNHjQ1+LFX4lQBLjMAhaLtTJnfuI86O7WnNdlshsYWBAQEBAQEBAQDTzD7MJp2KW6E8BNVjjjgZMTAjI/GI3ZrTmR2UUOtSeIEBAQEBAPKPqb0qnb4Ywz6gqpNKCUNQsfBmAnKc5p3dxokSQRpwgRSAAQDPplG1wee4KOfkehaF94R5uoKSgvQL1j5gkFTN4ywYaIEBAOhOyI39MZfoKc940g57XeqwRnxh7P62fKjnfEtBzQxHQQEBAQEBAQEBAQEBCkBBgAAAAAAAAM6A1UrwFYZAEMfe6go3jX25xz2sHsovQ2UO/UHqZZOXLIABAOwg7pkXyaTR85yQIvYnoGaG/OVRLRHOj+nhZnXb6dVtAQEBAPnciBUp3uspLQTajKTlAxgrNe+3tlqlbwlNRkz0eNmhQMzoMcWOFi9nCyn+eM5lA6Pq67DxzTQDlHljh8g8kRtJAPq9hxzTgreK0qDTavsethixguZYfV7wDmKfumMglnoqQQEBAQEBAM1x2dVBdf5BJ0Xvw2qqhvpKqxdHb8/HMFWiXkJj1uAAQQEJgEDAQYBA5kV2WLkgey9C5z6gZT69VLKcEuwyY8P853rNtGhT3NeUgAEBAQDiX59K9PuJ5RE7Z1uj7q/QJ8FGf8avLdWM7hwmWkVH2gEBAQEBAQEBAQEBAQD1SubX5XhFHcUOWdUzg1bXmDwWJwt+wpU3FOdFkU1PXBSAAQDHCzfEQyqwOO263EE6HER1vWDrwz8JiEHEOXfZ3kX7NYEBAQDEH++Hy8wBcniKuWVevaAwzHCh60kzncU30E5fDC3gJsEBAQEBAQEBAQEBCUBAgMAA1wt18LbxMKdYcJ+nEDMMWZbRsu550l8HGhcYhpl6DjSBAICdjI=") + proof := &result.ProofWithKey{} + r := io.NewBinReaderFromBuf(proofB) + proof.DecodeBinary(r) + return result.FindStates{ + Results: []result.KeyValue{{Key: []byte("aa10"), Value: []byte("v2")}}, + FirstProof: proof, + Truncated: true, + } + }, + }, + }, "getstateheight": { { 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 b2c42e481..e0cab4356 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -102,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, @@ -1051,6 +1052,108 @@ func (s *Server) getState(ps request.Params) (interface{}, *response.Error) { 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(ps) > 4 { + count, err = ps.Value(4).GetInt() + if err != nil { + return nil, response.WrapErrorWithData(response.ErrInvalidParams, errors.New("invalid count")) + } + if count > s.config.MaxFindResultItems { + count = s.config.MaxFindResultItems + } + } + cs, respErr := s.getHistoricalContractState(root, csHash) + if respErr != nil { + return nil, respErr + } + pKey := makeStorageKey(cs.ID, prefix) + var sKey []byte + if len(key) > 0 { + sKey = makeStorageKey(cs.ID, key) + } + kvs, err := s.chain.GetStateModule().FindStates(root, pKey, sKey, count+1) // +1 to define result truncation + if err != nil { + return nil, response.NewInternalServerError("failed to find historical items", err) + } + res := result.FindStates{} + if len(kvs) == count+1 { + res.Truncated = true + kvs = kvs[:len(kvs)-1] + } + if len(kvs) > 0 { + proof, err := s.chain.GetStateModule().GetStateProof(root, kvs[0].Key) + if err != nil { + return nil, response.NewInternalServerError("failed to get first proof", err) + } + res.FirstProof = &result.ProofWithKey{ + Key: kvs[0].Key, + Proof: proof, + } + } + if len(kvs) > 1 { + proof, err := s.chain.GetStateModule().GetStateProof(root, kvs[len(kvs)-1].Key) + if err != nil { + return nil, response.NewInternalServerError("failed to get first proof", err) + } + res.LastProof = &result.ProofWithKey{ + Key: kvs[len(kvs)-1].Key, + Proof: proof, + } + } + res.Results = make([]result.KeyValue, len(kvs)) + for i, kv := range kvs { + res.Results[i] = result.KeyValue{ + Key: kv.Key[4:], // cut contract ID as it is done in C# + Value: kv.Value, + } + } + return res, nil +} + +func (s *Server) getHistoricalContractState(root util.Uint256, csHash util.Uint160) (*state.Contract, *response.Error) { csKey := makeStorageKey(native.ManagementContractID, native.MakeContractKey(csHash)) csBytes, err := s.chain.GetStateModule().GetState(root, csKey) if err != nil { @@ -1061,12 +1164,7 @@ func (s *Server) getState(ps request.Params) (interface{}, *response.Error) { if err != nil { return nil, response.NewInternalServerError("failed to deserialize historical contract state", err) } - sKey := makeStorageKey(contract.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 + return contract, nil } func (s *Server) getStateHeight(_ request.Params) (interface{}, *response.Error) { diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index c273a2774..767642a6f 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 = "cb17eac9594d7ffa318545ab36e3227eedf30b4d13d76d3b49c94243fb3b2bde" +const deploymentTxHash = "8de63ea12ca8a9c5233ebf8664a442c881ae1bb83708d82da7fa1da2305ecf14" const genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4" const verifyContractHash = "f68822e4ecd93de334bdf1f7c409eda3431bcbd0" @@ -348,6 +348,38 @@ var rpcTestCases = map[string][]rpcTestCase{ 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", @@ -1444,6 +1476,89 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] testGetState(t, params, base64.StdEncoding.EncodeToString([]byte("newtestvalue"))) }) }) + t.Run("findstates", func(t *testing.T) { + testFindStates := func(t *testing.T, p string, root util.Uint256, expected result.FindStates) { + rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "findstates", "params": [%s]}`, p) + body := doRPCCall(rpc, httpSrv.URL, t) + rawRes := checkErrGetResult(t, body, false) + + var actual result.FindStates + require.NoError(t, json.Unmarshal(rawRes, &actual)) + require.Equal(t, expected.Results, actual.Results) + + checkProof := func(t *testing.T, proof *result.ProofWithKey, value []byte) { + rpc = fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "verifyproof", "params": ["%s", "%s"]}`, + root.StringLE(), proof.String()) + body = doRPCCall(rpc, httpSrv.URL, t) + rawRes = checkErrGetResult(t, body, false) + vp := new(result.VerifyProof) + require.NoError(t, json.Unmarshal(rawRes, vp)) + require.Equal(t, value, vp.Value) + } + checkProof(t, actual.FirstProof, actual.Results[0].Value) + if len(actual.Results) > 1 { + checkProof(t, actual.LastProof, actual.Results[len(actual.Results)-1].Value) + } + require.Equal(t, expected.Truncated, actual.Truncated) + } + t.Run("good: no prefix, no limit", func(t *testing.T) { + // pairs for this test where put to the contract storage at block #16 + root, err := e.chain.GetStateModule().GetStateRoot(16) + require.NoError(t, err) + params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa"))) + testFindStates(t, params, root.Root, result.FindStates{ + Results: []result.KeyValue{ + {Key: []byte("aa10"), Value: []byte("v2")}, + {Key: []byte("aa50"), Value: []byte("v3")}, + {Key: []byte("aa"), Value: []byte("v1")}, + }, + Truncated: false, + }) + }) + t.Run("good: with prefix, no limit", func(t *testing.T) { + // pairs for this test where put to the contract storage at block #16 + root, err := e.chain.GetStateModule().GetStateRoot(16) + require.NoError(t, err) + params := fmt.Sprintf(`"%s", "%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")), base64.StdEncoding.EncodeToString([]byte("aa10"))) + testFindStates(t, params, root.Root, result.FindStates{ + Results: []result.KeyValue{ + {Key: []byte("aa50"), Value: []byte("v3")}, + }, + Truncated: false, + }) + }) + t.Run("good: no prefix, with limit", func(t *testing.T) { + for limit := 2; limit < 5; limit++ { + // pairs for this test where put to the contract storage at block #16 + root, err := e.chain.GetStateModule().GetStateRoot(16) + require.NoError(t, err) + params := fmt.Sprintf(`"%s", "%s", "%s", "", %d`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")), limit) + expected := result.FindStates{ + Results: []result.KeyValue{ + {Key: []byte("aa10"), Value: []byte("v2")}, + {Key: []byte("aa50"), Value: []byte("v3")}, + }, + Truncated: limit == 2, + } + if limit != 2 { + expected.Results = append(expected.Results, result.KeyValue{Key: []byte("aa"), Value: []byte("v1")}) + } + testFindStates(t, params, root.Root, expected) + } + }) + t.Run("good: with prefix, with limit", func(t *testing.T) { + // pairs for this test where put to the contract storage at block #16 + root, err := e.chain.GetStateModule().GetStateRoot(16) + require.NoError(t, err) + params := fmt.Sprintf(`"%s", "%s", "%s", "%s", %d`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")), base64.StdEncoding.EncodeToString([]byte("aa00")), 1) + testFindStates(t, params, root.Root, result.FindStates{ + Results: []result.KeyValue{ + {Key: []byte("aa10"), Value: []byte("v2")}, + }, + Truncated: true, + }) + }) + }) t.Run("getrawtransaction", func(t *testing.T) { block, _ := chain.GetBlock(chain.GetHeaderHash(1)) @@ -1708,7 +1823,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "57796933740", + Amount: "57796785740", LastUpdated: 16, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index e025c1364dc77caacde64cf1f6a77ffccdd927ce..7ce922f592c084939459ac961f13d141e4efb313 100644 GIT binary patch delta 5914 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 g|7uh2xNX)iQkwwFHq)$ From 8e7c76827b247f29b9c02e80bf00af8814dc85b5 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 13 Oct 2021 10:22:57 +0300 Subject: [PATCH 5/8] core: optimise (*Billet).Traverse and (*Trie).Find (*Billet).Traverse changes: 1. Get rid of the `offset` argument. We can cut `from` and pass just the part that remains. This implies that node with path matching `from` will also be included in the result, so additional check needs to be added to the callback function. 2. Pass `path` and `from` without search prefix. Append prefix to the result inside the callback. 3. Remove duplicating code. (*Trie).Find changes: 1. Properly prepare `from` argument for traversing function. It closly depends on the `path` argument. --- pkg/core/mpt/billet.go | 53 ++++++++++++++++++------------------------ pkg/core/mpt/trie.go | 34 +++++++++++++++++++++------ 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/pkg/core/mpt/billet.go b/pkg/core/mpt/billet.go index 4b72a55ca..bcabc3f65 100644 --- a/pkg/core/mpt/billet.go +++ b/pkg/core/mpt/billet.go @@ -200,7 +200,7 @@ func (b *Billet) incrementRefAndStore(h util.Uint256, bs []byte) { // 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(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool) error { - r, err := b.traverse(b.root, []byte{}, []byte{}, 0, process, ignoreStorageErr) + 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(pathToNode []byte, node Node, nodeBytes [ return nil } -func (b *Billet) traverse(curr Node, path, from []byte, offset int, process func(pathToNode []byte, 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,9 +222,9 @@ func (b *Billet) traverse(curr Node, path, from []byte, offset int, process func } return nil, err } - return b.traverse(r, path, from, offset, process, ignoreStorageErr) + return b.traverse(r, path, from, process, ignoreStorageErr) } - if _, ok := curr.(*LeafNode); !ok || len(from) <= offset && (len(from) == 0 || !bytes.Equal(path, from)) { + if len(from) == 0 { bytes := slice.Copy(curr.Bytes()) if process(fromNibbles(path), curr, bytes) { return curr, errStop @@ -234,33 +234,25 @@ func (b *Billet) traverse(curr Node, path, from []byte, offset int, process func case *LeafNode: return b.tryCollapseLeaf(n), nil case *BranchNode: - if len(from) > offset { - startIndex := from[offset] - for i := startIndex; i < lastChild; i++ { - newOffset := len(from) - if i == startIndex { - newOffset = offset + 1 - } - r, err := b.traverse(n.Children[i], append(path, i), from, newOffset, process, ignoreStorageErr) - if err != nil { - if !errors.Is(err, errStop) { - return nil, err - } - n.Children[i] = r - return n, err - } - n.Children[i] = r - } - return b.tryCollapseBranch(n), nil + var ( + startIndex byte + endIndex byte = childrenCount + ) + if len(from) != 0 { + endIndex = lastChild + startIndex, from = splitPath(from) } - for i, child := range n.Children { + for i := startIndex; i < endIndex; i++ { var newPath []byte if i == lastChild { newPath = path } else { - newPath = append(path, byte(i)) + newPath = append(path, i) } - r, err := b.traverse(child, newPath, from, offset, process, ignoreStorageErr) + 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 @@ -272,15 +264,14 @@ func (b *Billet) traverse(curr Node, path, from []byte, offset int, process func } return b.tryCollapseBranch(n), nil case *ExtensionNode: - var newOffset int - if len(from) > offset && bytes.HasPrefix(from[offset:], n.key) { - newOffset = offset + len(n.key) - } else if len(from) <= offset || bytes.Compare(n.key, from[offset:]) > 0 { - newOffset = len(from) + 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, newOffset, process, ignoreStorageErr) + 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/trie.go b/pkg/core/mpt/trie.go index efe402233..dc7c670c8 100644 --- a/pkg/core/mpt/trie.go +++ b/pkg/core/mpt/trie.go @@ -541,12 +541,29 @@ func (t *Trie) Find(prefix, from []byte, max int) ([]storage.KeyValue, error) { if !bytes.HasPrefix(from, prefix) { return nil, errors.New("`from` argument doesn't match specified prefix") } - fromP = toNibbles(from) + fromP = toNibbles(from)[len(prefixP):] } _, 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 @@ -555,15 +572,18 @@ func (t *Trie) Find(prefix, from []byte, max int) ([]storage.KeyValue, error) { b := NewBillet(t.root.Hash(), false, t.Store) process := func(pathToNode []byte, node Node, _ []byte) bool { if leaf, ok := node.(*LeafNode); ok { - res = append(res, storage.KeyValue{ - Key: pathToNode, - Value: slice.Copy(leaf.value), - }) - count++ + key := append(prefix, pathToNode...) + if !bytes.Equal(key, from) { // (*Billet).traverse includes `from` path into result if so. Need to filter out manually. + res = append(res, storage.KeyValue{ + Key: key, + Value: slice.Copy(leaf.value), + }) + count++ + } } return count >= max } - _, err = b.traverse(start, path, fromP, len(path), process, false) + _, err = b.traverse(start, path, fromP, process, false) if err != nil && !errors.Is(err, errStop) { return nil, err } From 892eadf86d5a05b41e1e4ddf6fd10892709e5e9b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 13 Oct 2021 11:38:53 +0300 Subject: [PATCH 6/8] core: mandate passing `from` as a subprefix for (*Trie).Find However, we need to distinguish empty subprefix and nil subprefix (no start specified) to match the C# behaviour. --- pkg/core/mpt/compat_test.go | 9 ++++++--- pkg/core/mpt/trie.go | 16 ++++++---------- pkg/rpc/server/server.go | 15 ++++++++++----- pkg/rpc/server/server_test.go | 16 +++++++++++++++- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/pkg/core/mpt/compat_test.go b/pkg/core/mpt/compat_test.go index e69bc2e29..bcd9be184 100644 --- a/pkg/core/mpt/compat_test.go +++ b/pkg/core/mpt/compat_test.go @@ -394,13 +394,16 @@ func TestCompatibility_Find(t *testing.T) { }) t.Run("from is not in tree", func(t *testing.T) { t.Run("matching", func(t *testing.T) { - check(t, []byte("aa30"), 1) + check(t, []byte("30"), 1) }) t.Run("non-matching", func(t *testing.T) { - check(t, []byte("aa60"), 0) + check(t, []byte("60"), 0) }) }) t.Run("from is in tree", func(t *testing.T) { - check(t, []byte("aa10"), 1) // without `from` key + 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/trie.go b/pkg/core/mpt/trie.go index dc7c670c8..ac54fed65 100644 --- a/pkg/core/mpt/trie.go +++ b/pkg/core/mpt/trie.go @@ -526,22 +526,19 @@ func collapse(depth int, node Node) Node { } // Find returns list of storage key-value pairs whose key is prefixed by the specified -// prefix starting from the specified path (not including the item at the specified path -// if so). The `max` number of elements is returned at max. +// 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 { + if len(from) > MaxKeyLength-len(prefix) { return nil, errors.New("invalid from length") } prefixP := toNibbles(prefix) fromP := []byte{} if len(from) > 0 { - if !bytes.HasPrefix(from, prefix) { - return nil, errors.New("`from` argument doesn't match specified prefix") - } - fromP = toNibbles(from)[len(prefixP):] + fromP = toNibbles(from) } _, start, path, err := t.getWithPath(t.root, prefixP, false) if err != nil { @@ -572,10 +569,9 @@ func (t *Trie) Find(prefix, from []byte, max int) ([]storage.KeyValue, error) { b := NewBillet(t.root.Hash(), false, t.Store) process := func(pathToNode []byte, node Node, _ []byte) bool { if leaf, ok := node.(*LeafNode); ok { - key := append(prefix, pathToNode...) - if !bytes.Equal(key, from) { // (*Billet).traverse includes `from` path into result if so. Need to filter out manually. + 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: key, + Key: append(slice.Copy(prefix), pathToNode...), Value: slice.Copy(leaf.value), }) count++ diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index e0cab4356..ab9ac6ce1 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -1095,6 +1095,15 @@ func (s *Server) findStates(ps request.Params) (interface{}, *response.Error) { 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() @@ -1110,11 +1119,7 @@ func (s *Server) findStates(ps request.Params) (interface{}, *response.Error) { return nil, respErr } pKey := makeStorageKey(cs.ID, prefix) - var sKey []byte - if len(key) > 0 { - sKey = makeStorageKey(cs.ID, key) - } - kvs, err := s.chain.GetStateModule().FindStates(root, pKey, sKey, count+1) // +1 to define result truncation + 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) } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 767642a6f..c0fca9a7b 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -1515,6 +1515,20 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] 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) @@ -1527,7 +1541,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] Truncated: false, }) }) - t.Run("good: no prefix, with limit", func(t *testing.T) { + 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) From 360bb220b0b6da3800c3eebf314bcc2622c229fc Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 11 Oct 2021 16:44:22 +0300 Subject: [PATCH 7/8] rpc: remove unnecessary base64 encoding It's default encoding for []byte. --- pkg/rpc/client/rpc.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index b3cba29dc..aca66aba6 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -1,7 +1,6 @@ package client import ( - "encoding/base64" "errors" "fmt" @@ -420,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) { From 495aba4174b667bb801e563ea8b5b3e3ee8bb7f0 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 13 Oct 2021 13:48:18 +0300 Subject: [PATCH 8/8] core: try to collapse traversed Billet part on errStop --- pkg/core/mpt/billet.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/core/mpt/billet.go b/pkg/core/mpt/billet.go index bcabc3f65..05212aafa 100644 --- a/pkg/core/mpt/billet.go +++ b/pkg/core/mpt/billet.go @@ -258,7 +258,7 @@ func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode return nil, err } n.Children[i] = r - return n, err + return b.tryCollapseBranch(n), err } n.Children[i] = r } @@ -276,10 +276,7 @@ func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode return nil, err } n.next = r - if err != nil { - return n, err - } - return b.tryCollapseExtension(n), nil + return b.tryCollapseExtension(n), err default: return nil, ErrNotFound }