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 0f48ffd78..e025c1364 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ