From 01143da6213fd4d757c13195495f7e8b590a5191 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 7 Oct 2021 12:03:37 +0300 Subject: [PATCH] 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 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