diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index d7247f973..cbfea1725 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -16,6 +16,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "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/storage" @@ -561,6 +562,12 @@ func (bc *Blockchain) addHeaders(verify bool, headers ...*block.Header) error { return nil } +// GetStateProof returns proof of having key in the MPT with the specified root. +func (bc *Blockchain) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) { + tr := mpt.NewTrie(mpt.NewHashNode(root), false, storage.NewMemCachedStore(bc.dao.Store)) + return tr.GetProof(key) +} + // GetStateRoot returns state root for a given height. func (bc *Blockchain) GetStateRoot(height uint32) (*state.MPTRootState, error) { return bc.dao.GetStateRoot(height) diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 1a1aa94b7..451b7941c 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -46,6 +46,7 @@ type Blockchainer interface { GetValidators() ([]*keys.PublicKey, error) GetStandByCommittee() keys.PublicKeys GetStandByValidators() keys.PublicKeys + GetStateProof(root util.Uint256, key []byte) ([][]byte, error) GetStateRoot(height uint32) (*state.MPTRootState, error) GetStorageItem(id int32, key []byte) *state.StorageItem GetStorageItems(id int32) (map[string]*state.StorageItem, error) diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 135935da4..1c9169ead 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -121,6 +121,9 @@ func (chain testChain) GetStandByValidators() keys.PublicKeys { func (chain testChain) GetEnrollments() ([]state.Validator, error) { panic("TODO") } +func (chain testChain) GetStateProof(util.Uint256, []byte) ([][]byte, error) { + panic("TODO") +} func (chain testChain) GetStateRoot(height uint32) (*state.MPTRootState, error) { panic("TODO") } diff --git a/pkg/rpc/response/result/mpt.go b/pkg/rpc/response/result/mpt.go index 65224e726..4473d7ea3 100644 --- a/pkg/rpc/response/result/mpt.go +++ b/pkg/rpc/response/result/mpt.go @@ -1,7 +1,81 @@ package result +import ( + "encoding/hex" + "encoding/json" + + "github.com/nspcc-dev/neo-go/pkg/io" +) + // StateHeight is a result of getstateheight RPC. type StateHeight struct { BlockHeight uint32 `json:"blockHeight"` StateHeight uint32 `json:"stateHeight"` } + +// ProofWithKey represens key-proof pair. +type ProofWithKey struct { + Key []byte + Proof [][]byte +} + +// GetProof is a result of getproof RPC. +type GetProof struct { + Result ProofWithKey `json:"proof"` + Success bool `json:"success"` +} + +// MarshalJSON implements json.Marshaler. +func (p *ProofWithKey) MarshalJSON() ([]byte, error) { + w := io.NewBufBinWriter() + p.EncodeBinary(w.BinWriter) + if w.Err != nil { + return nil, w.Err + } + return []byte(`"` + hex.EncodeToString(w.Bytes()) + `"`), nil +} + +// EncodeBinary implements io.Serializable. +func (p *ProofWithKey) EncodeBinary(w *io.BinWriter) { + w.WriteVarBytes(p.Key) + w.WriteVarUint(uint64(len(p.Proof))) + for i := range p.Proof { + w.WriteVarBytes(p.Proof[i]) + } +} + +// DecodeBinary implements io.Serializable. +func (p *ProofWithKey) DecodeBinary(r *io.BinReader) { + p.Key = r.ReadVarBytes() + sz := r.ReadVarUint() + for i := uint64(0); i < sz; i++ { + p.Proof = append(p.Proof, r.ReadVarBytes()) + } +} + +// UnmarshalJSON implements json.Unmarshaler. +func (p *ProofWithKey) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + return p.FromString(s) +} + +// String implements fmt.Stringer. +func (p *ProofWithKey) String() string { + w := io.NewBufBinWriter() + p.EncodeBinary(w.BinWriter) + return hex.EncodeToString(w.Bytes()) +} + +// FromString decodes p from hex-encoded string. +func (p *ProofWithKey) FromString(s string) error { + rawProof, err := hex.DecodeString(s) + if err != nil { + return err + } + r := io.NewBinReaderFromBuf(rawProof) + p.DecodeBinary(r) + return r.Err +} diff --git a/pkg/rpc/response/result/mpt_test.go b/pkg/rpc/response/result/mpt_test.go new file mode 100644 index 000000000..3a3497aee --- /dev/null +++ b/pkg/rpc/response/result/mpt_test.go @@ -0,0 +1,57 @@ +package result + +import ( + "encoding/json" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/mpt" + "github.com/nspcc-dev/neo-go/pkg/internal/random" + "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/stretchr/testify/require" +) + +func testProofWithKey() *ProofWithKey { + return &ProofWithKey{ + Key: random.Bytes(10), + Proof: [][]byte{ + random.Bytes(12), + random.Bytes(0), + random.Bytes(34), + }, + } +} + +func TestGetProof_MarshalJSON(t *testing.T) { + t.Run("Good", func(t *testing.T) { + p := &GetProof{ + Result: *testProofWithKey(), + Success: true, + } + testserdes.MarshalUnmarshalJSON(t, p, new(GetProof)) + }) + t.Run("Compatibility", func(t *testing.T) { + js := []byte(`{ + "proof" : "25ddeb9aa1bfc353c9c54e21dffb470f65d9c22a0662616c616e63654f70000000000000000708fd12020020666eaa8a6e75d43a97d76e72b605c7e05189f0c57ec19d84acdb75810f18239d202c83028ce3d7abcf4e4f95d05fbfdfa5e18bde3a8fbb65a57559d6b5ea09425c2090c40d440744a848e3b407a00e4efb692a957245a1efc9cb8496cb05fd328ee620dd2652bf25dfc3ad5fee7b200ccf3e3ae50772ff8ed58907e4dab8e7d4b2489720d8a5d5ed75b5b0f256d0a2cf5c220b4ddae2a228ef0fc0212b689f3811dfa94620342cc0d73fabd2440ed2cc735a9608391a510e1981b321a9f4258682706adc9620ced036e52f39387b9c58ade7bf8c3ca8959b64d8031d36d9b1c62f3f1c51c7cb2031072c7c801b5c1614dae441383a65344acd238f13db28ff0a39c0626e597f002062552d64c616d8b2a6a93d22936055110c0065728aa2b4fbf4d76b108390b474203322d3c93c741674a307cf6455e77c02ceeda307d4ec23fd809a2a420b4243f82052ab92a9cedc6716ad4c66a8a3e423b195b05bdebde456f992bff48f2561e99720e6379995e7053823b8ba8fb8af9623cf48e89f60c989598445df5e711db42a6f20192894ed637e86561ff6a4b8dea4539dee8bddb2fb20bf4ae3499852985c88b120e0005edd09f2335aa6b59ff4723e1262b2192adaa5e3e56f79e662f07041f04c2033577f3e2c5bb0e58746980a07cdfad2f872e2b9a10bcc27b7c678c85576df8420f0f04180d15b6eaa0c43e62380084c75ad773d790700a7120c6c4da1fc51693000fd720100209648e8f10a5ff4c209009b9a09697babbe1b2150d0948c1970a560282a1bfa4720988af8f34859dd8309bffea0b1dff9c8cef0b9b0d6a1852d40786627729ae7be00206ebf4f1b7861bca041cbb8feca75158511ca43a1810d17e1e3017468e8cef0de20cac93064090a7da09f8202c17d1e6cbb9a16eb43afcb032e80719cbf05b3446d2019b76a10b91fb99ec08814e8108e5490b879fb09a190cb2c129dfd98335bd5de000020b1da1198bacacf2adc0d863929d77c285ce3a26e736203d0c0a69a1312255fb2207ee8aa092f49348bd89f9c4bf004b0bee2241a2d0acfe7b3ce08e414b04a5717205b0dda71eac8a4e4cdc6a7b939748c0a78abb54f2547a780e6df67b25530330f000020fc358fb9d1e0d36461e015ac8e35f97072a9f9e750a3c25722a2b1a858fcb82d203c52c9fac6d4694b351390158334a9166bc3478ceb9bea2b0b244915f918239e20d526344a24ff19ee6a9f5c5beb833f4eb6d51191590350e26fa50b138493473f005200000000000000000000002077c404fec0a4265568951dbd096572787d109fab105213f4f292a5f53ce72fca00000020b8d1c7a386eaba83ce83ee0700d4ca9b86e75d147d670ea05123e438231d895000004801250b090a0a010b0f0c0305030c090c05040e02010d0f0f0b0407000f06050d090c02020a0006202af2097cf9d3f42e49f6b3c3dd254e7cbdab3485b029721cbbbf1ad0455a810852000000000000002055170506f4b18bc573a909b51cb21bdd5d303ec511f6cdfb1c6a1ab8d8a1dad020ee774c1b9fe1d8ea8d05823837d959da48af74f384d52f06c42c9d146c5258e300000000000000000072000000204457a6fe530ee953ad1f9caf63daf7f86719c9986df2d0b6917021eb379800f00020406bfc79da4ba6f37452a679d13cca252585d34f7e94a480b047bad9427f233e00000000201ce15a2373d28e0dc5f2000cf308f155d06f72070a29e5af1528c8f05f29d248000000000000004301200601060c0601060e06030605040f0700000000000000000000000000000000072091b83866bbd7450115b462e8d48601af3c3e9a35e7018d2b98a23e107c15c200090307000410a328e800", + "success" : true + }`) + + var p GetProof + require.NoError(t, json.Unmarshal(js, &p)) + require.Equal(t, 8, len(p.Result.Proof)) + for i := range p.Result.Proof { // smoke test that every chunk is correctly encoded node + r := io.NewBinReaderFromBuf(p.Result.Proof[i]) + var n mpt.NodeObject + n.DecodeBinary(r) + require.NoError(t, r.Err) + require.NotNil(t, n.Node) + } + }) +} + +func TestProofWithKey_EncodeString(t *testing.T) { + expected := testProofWithKey() + var actual ProofWithKey + require.NoError(t, actual.FromString(expected.String())) + require.Equal(t, expected, &actual) +} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 3d3362262..4dbcb192f 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/binary" "encoding/hex" "encoding/json" "errors" @@ -98,6 +99,7 @@ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *respon "getnep5balances": (*Server).getNEP5Balances, "getnep5transfers": (*Server).getNEP5Transfers, "getpeers": (*Server).getPeers, + "getproof": (*Server).getProof, "getrawmempool": (*Server).getRawMempool, "getrawtransaction": (*Server).getrawtransaction, "getstateheight": (*Server).getStateHeight, @@ -775,6 +777,46 @@ func (s *Server) contractScriptHashFromParam(param *request.Param) (util.Uint160 return result, nil } +func makeStorageKey(id int32, key []byte) []byte { + skey := make([]byte, 4+len(key)) + binary.LittleEndian.PutUint32(skey, uint32(id)) + copy(skey[4:], key) + return skey +} + +var errKeepOnlyLatestState = errors.New("'KeepOnlyLatestState' setting is enabled") + +func (s *Server) getProof(ps request.Params) (interface{}, *response.Error) { + if s.chain.GetConfig().KeepOnlyLatestState { + return nil, response.NewInvalidRequestError("'getproof' is not supported", errKeepOnlyLatestState) + } + root, err := ps.Value(0).GetUint256() + if err != nil { + return nil, response.ErrInvalidParams + } + sc, err := ps.Value(1).GetUint160FromHex() + if err != nil { + return nil, response.ErrInvalidParams + } + key, err := ps.Value(2).GetBytesHex() + if err != nil { + return nil, response.ErrInvalidParams + } + cs := s.chain.GetContractState(sc) + if cs == nil { + return nil, response.ErrInvalidParams + } + skey := makeStorageKey(cs.ID, key) + proof, err := s.chain.GetStateProof(root, skey) + return &result.GetProof{ + Result: result.ProofWithKey{ + Key: skey, + Proof: proof, + }, + Success: err == nil, + }, nil +} + func (s *Server) getStateHeight(_ request.Params) (interface{}, *response.Error) { var height = s.chain.BlockHeight() var stateHeight uint32 diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index d521d1ebc..527fad726 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -285,6 +285,28 @@ var rpcTestCases = map[string][]rpcTestCase{ check: checkNep5Transfers, }, }, + "getproof": { + { + 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 + `", "notahex"]`, + fail: true, + }, + }, "getstateheight": { { name: "positive",