diff --git a/chain/dev/config.toml b/chain/dev/config.toml index 8c96c07e9e..f19fa63578 100644 --- a/chain/dev/config.toml +++ b/chain/dev/config.toml @@ -35,5 +35,5 @@ enabled = true ws = true port = 8545 host = "localhost" -modules = ["system", "author", "chain", "state", "rpc", "grandpa"] +modules = ["system", "author", "chain", "state", "rpc", "grandpa", "offchain", "childstate"] ws-port = 8546 diff --git a/chain/dev/defaults.go b/chain/dev/defaults.go index 38b6fa7111..b6db6ec7e9 100644 --- a/chain/dev/defaults.go +++ b/chain/dev/defaults.go @@ -87,7 +87,7 @@ var ( // DefaultRPCHTTPPort rpc port DefaultRPCHTTPPort = uint32(8545) // DefaultRPCModules rpc modules - DefaultRPCModules = []string{"system", "author", "chain", "state", "rpc", "grandpa"} + DefaultRPCModules = []string{"system", "author", "chain", "state", "rpc", "grandpa", "offchain", "childstate"} // DefaultRPCWSPort rpc websocket port DefaultRPCWSPort = uint32(8546) // DefaultRPCEnabled enables the RPC server diff --git a/chain/gssmr/config.toml b/chain/gssmr/config.toml index 893fd41a1d..6419e8cec1 100644 --- a/chain/gssmr/config.toml +++ b/chain/gssmr/config.toml @@ -35,5 +35,5 @@ discovery-interval = 10 enabled = false port = 8545 host = "localhost" -modules = ["system", "author", "chain", "state", "rpc", "grandpa"] +modules = ["system", "author", "chain", "state", "rpc", "grandpa", "offchain", "childstate"] ws-port = 8546 diff --git a/chain/gssmr/defaults.go b/chain/gssmr/defaults.go index b537f6b1a9..0cdc3d2d35 100644 --- a/chain/gssmr/defaults.go +++ b/chain/gssmr/defaults.go @@ -92,7 +92,7 @@ var ( // DefaultRPCHTTPPort rpc port DefaultRPCHTTPPort = uint32(8545) // DefaultRPCModules rpc modules - DefaultRPCModules = []string{"system", "author", "chain", "state", "rpc", "grandpa"} + DefaultRPCModules = []string{"system", "author", "chain", "state", "rpc", "grandpa", "offchain", "childstate"} // DefaultRPCWSPort rpc websocket port DefaultRPCWSPort = uint32(8546) ) diff --git a/chain/kusama/config.toml b/chain/kusama/config.toml index 941df4b6e6..dd3261fde6 100644 --- a/chain/kusama/config.toml +++ b/chain/kusama/config.toml @@ -35,7 +35,7 @@ enabled = false external = false port = 8545 host = "localhost" -modules = ["system", "author", "chain", "state", "rpc", "grandpa"] +modules = ["system", "author", "chain", "state", "rpc", "grandpa", "offchain", "childstate"] ws-port = 8546 ws = false ws-external = false diff --git a/chain/kusama/defaults.go b/chain/kusama/defaults.go index e4c2e2657a..8902a46297 100644 --- a/chain/kusama/defaults.go +++ b/chain/kusama/defaults.go @@ -83,7 +83,7 @@ var ( // DefaultRPCHTTPPort rpc port DefaultRPCHTTPPort = uint32(8545) // DefaultRPCModules rpc modules - DefaultRPCModules = []string{"system", "author", "chain", "state", "rpc", "grandpa"} + DefaultRPCModules = []string{"system", "author", "chain", "state", "rpc", "grandpa", "offchain", "childstate"} // DefaultRPCWSPort rpc websocket port DefaultRPCWSPort = uint32(8546) ) diff --git a/chain/polkadot/config.toml b/chain/polkadot/config.toml index 6ec7c85783..98f3291e1b 100644 --- a/chain/polkadot/config.toml +++ b/chain/polkadot/config.toml @@ -34,5 +34,5 @@ nomdns = false enabled = false port = 8545 host = "localhost" -modules = ["system", "author", "chain", "state", "rpc", "grandpa"] +modules = ["system", "author", "chain", "state", "rpc", "grandpa", "offchain", "childstate"] ws-port = 8546 \ No newline at end of file diff --git a/chain/polkadot/defaults.go b/chain/polkadot/defaults.go index 4af96caf0e..dbc0dd8e2b 100644 --- a/chain/polkadot/defaults.go +++ b/chain/polkadot/defaults.go @@ -84,7 +84,7 @@ var ( // DefaultRPCHTTPPort rpc port DefaultRPCHTTPPort = uint32(8545) // DefaultRPCModules rpc modules - DefaultRPCModules = []string{"system", "author", "chain", "state", "rpc", "grandpa"} + DefaultRPCModules = []string{"system", "author", "chain", "state", "rpc", "grandpa", "offchain", "childstate"} // DefaultRPCWSPort rpc websocket port DefaultRPCWSPort = uint32(8546) ) diff --git a/dot/rpc/http.go b/dot/rpc/http.go index a7d109f426..76fa52b06d 100644 --- a/dot/rpc/http.go +++ b/dot/rpc/http.go @@ -127,6 +127,8 @@ func (h *HTTPServer) RegisterModules(mods []string) { srvc = modules.NewDevModule(h.serverConfig.BlockProducerAPI, h.serverConfig.NetworkAPI) case "offchain": srvc = modules.NewOffchainModule(h.serverConfig.NodeStorage) + case "childstate": + srvc = modules.NewChildStateModule(h.serverConfig.StorageAPI, h.serverConfig.BlockAPI) default: h.logger.Warn("Unrecognised module", "module", mod) continue diff --git a/dot/rpc/http_test.go b/dot/rpc/http_test.go index 34397ab682..b2dfb50b01 100644 --- a/dot/rpc/http_test.go +++ b/dot/rpc/http_test.go @@ -38,6 +38,27 @@ import ( "github.com/stretchr/testify/require" ) +func TestRegisterModules(t *testing.T) { + rpcapiMocks := new(mocks.MockRPCAPI) + + mods := []string{"system", "author", "chain", "state", "rpc", "grandpa", "offchain", "childstate"} + + for _, modName := range mods { + rpcapiMocks.On("BuildMethodNames", mock.Anything, modName).Once() + } + + cfg := &HTTPServerConfig{ + Modules: mods, + RPCAPI: rpcapiMocks, + } + + NewHTTPServer(cfg) + + for _, modName := range mods { + rpcapiMocks.AssertCalled(t, "BuildMethodNames", mock.Anything, modName) + } +} + func TestNewHTTPServer(t *testing.T) { coreAPI := core.NewTestService(t, nil) si := &types.SystemInfo{ diff --git a/dot/rpc/modules/api.go b/dot/rpc/modules/api.go index febd9dac80..8812b09969 100644 --- a/dot/rpc/modules/api.go +++ b/dot/rpc/modules/api.go @@ -12,11 +12,13 @@ import ( "github.com/ChainSafe/gossamer/lib/grandpa" "github.com/ChainSafe/gossamer/lib/runtime" "github.com/ChainSafe/gossamer/lib/transaction" + "github.com/ChainSafe/gossamer/lib/trie" ) // StorageAPI is the interface for the storage state type StorageAPI interface { GetStorage(root *common.Hash, key []byte) ([]byte, error) + GetStorageChild(root *common.Hash, keyToChild []byte) (*trie.Trie, error) GetStorageByBlockHash(bhash common.Hash, key []byte) ([]byte, error) Entries(root *common.Hash) (map[string][]byte, error) GetStateRootFromBlock(bhash *common.Hash) (*common.Hash, error) diff --git a/dot/rpc/modules/childstate.go b/dot/rpc/modules/childstate.go new file mode 100644 index 0000000000..1dad74b39c --- /dev/null +++ b/dot/rpc/modules/childstate.go @@ -0,0 +1,70 @@ +// Copyright 2019 ChainSafe Systems (ON) Corp. +// This file is part of gossamer. +// +// The gossamer library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The gossamer library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the gossamer library. If not, see . + +package modules + +import ( + "net/http" + + "github.com/ChainSafe/gossamer/lib/common" +) + +// GetKeysRequest represents the request to retrieve the keys of a child storage +type GetKeysRequest struct { + Key []byte + Prefix []byte + Hash common.Hash +} + +// ChildStateModule is the module responsible to implement all the childstate RPC calls +type ChildStateModule struct { + storageAPI StorageAPI + blockAPI BlockAPI +} + +// NewChildStateModule returns a new ChildStateModule +func NewChildStateModule(s StorageAPI, b BlockAPI) *ChildStateModule { + return &ChildStateModule{ + storageAPI: s, + blockAPI: b, + } +} + +// GetKeys returns the keys from the specified child storage. The keys can also be filtered based on a prefix. +func (cs *ChildStateModule) GetKeys(_ *http.Request, req *GetKeysRequest, res *[]string) error { + if req.Hash == common.EmptyHash { + req.Hash = cs.blockAPI.BestBlockHash() + } + + stateRoot, err := cs.storageAPI.GetStateRootFromBlock(&req.Hash) + if err != nil { + return err + } + + trie, err := cs.storageAPI.GetStorageChild(stateRoot, req.Key) + if err != nil { + return err + } + + keys := trie.GetKeysWithPrefix(req.Prefix) + hexKeys := make([]string, len(keys)) + for idx, k := range keys { + hexKeys[idx] = common.BytesToHex(k) + } + + *res = hexKeys + return nil +} diff --git a/dot/rpc/modules/childstate_test.go b/dot/rpc/modules/childstate_test.go new file mode 100644 index 0000000000..5ea9885f4e --- /dev/null +++ b/dot/rpc/modules/childstate_test.go @@ -0,0 +1,112 @@ +// Copyright 2019 ChainSafe Systems (ON) Corp. +// This file is part of gossamer. +// +// The gossamer library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The gossamer library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the gossamer library. If not, see . + +package modules + +import ( + "math/big" + "testing" + + "github.com/ChainSafe/gossamer/dot/types" + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/trie" + "github.com/stretchr/testify/require" +) + +func TestChildStateGetKeys(t *testing.T) { + childStateModule, currBlockHash := setupChildStateStorage(t) + + req := &GetKeysRequest{ + Key: []byte(":child_storage_key"), + Prefix: []byte{}, + Hash: common.EmptyHash, + } + + res := make([]string, 0) + err := childStateModule.GetKeys(nil, req, &res) + require.NoError(t, err) + require.Len(t, res, 3) + + for _, r := range res { + b, dErr := common.HexToBytes(r) + require.NoError(t, dErr) + require.Contains(t, []string{ + ":child_first", ":child_second", ":another_child", + }, string(b)) + } + + req = &GetKeysRequest{ + Key: []byte(":child_storage_key"), + Prefix: []byte(":child_"), + Hash: currBlockHash, + } + + err = childStateModule.GetKeys(nil, req, &res) + require.NoError(t, err) + require.Len(t, res, 2) + + for _, r := range res { + b, err := common.HexToBytes(r) + require.NoError(t, err) + require.Contains(t, []string{ + ":child_first", ":child_second", + }, string(b)) + } +} + +func setupChildStateStorage(t *testing.T) (*ChildStateModule, common.Hash) { + t.Helper() + + st := newTestStateService(t) + + tr, err := st.Storage.TrieState(nil) + require.NoError(t, err) + + tr.Set([]byte(":first_key"), []byte(":value1")) + tr.Set([]byte(":second_key"), []byte(":second_value")) + + childTr := trie.NewEmptyTrie() + childTr.Put([]byte(":child_first"), []byte(":child_first_value")) + childTr.Put([]byte(":child_second"), []byte(":child_second_value")) + childTr.Put([]byte(":another_child"), []byte("value")) + + err = tr.SetChild([]byte(":child_storage_key"), childTr) + require.NoError(t, err) + + stateRoot, err := tr.Root() + require.NoError(t, err) + + bb, err := st.Block.BestBlock() + require.NoError(t, err) + + err = st.Storage.StoreTrie(tr, nil) + require.NoError(t, err) + + b := &types.Block{ + Header: types.Header{ + ParentHash: bb.Header.Hash(), + Number: big.NewInt(0).Add(big.NewInt(1), bb.Header.Number), + StateRoot: stateRoot, + }, + Body: []byte{}, + } + + err = st.Block.AddBlock(b) + require.NoError(t, err) + + hash, _ := st.Block.GetBlockHash(b.Header.Number) + return NewChildStateModule(st.Storage, st.Block), hash +} diff --git a/dot/rpc/modules/mocks/rpcapi.go b/dot/rpc/modules/mocks/rpcapi.go new file mode 100644 index 0000000000..6b3640f4df --- /dev/null +++ b/dot/rpc/modules/mocks/rpcapi.go @@ -0,0 +1,31 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// MockRPCAPI is an autogenerated mock type for the RPCAPI type +type MockRPCAPI struct { + mock.Mock +} + +// BuildMethodNames provides a mock function with given fields: rcvr, name +func (_m *MockRPCAPI) BuildMethodNames(rcvr interface{}, name string) { + _m.Called(rcvr, name) +} + +// Methods provides a mock function with given fields: +func (_m *MockRPCAPI) Methods() []string { + ret := _m.Called() + + var r0 []string + if rf, ok := ret.Get(0).(func() []string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + return r0 +} diff --git a/dot/rpc/modules/mocks/storage_api.go b/dot/rpc/modules/mocks/storage_api.go index db3bb22a4e..d61fd3e2a1 100644 --- a/dot/rpc/modules/mocks/storage_api.go +++ b/dot/rpc/modules/mocks/storage_api.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.8.0. DO NOT EDIT. +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. package mocks @@ -7,6 +7,8 @@ import ( mock "github.com/stretchr/testify/mock" state "github.com/ChainSafe/gossamer/dot/state" + + trie "github.com/ChainSafe/gossamer/lib/trie" ) // MockStorageAPI is an autogenerated mock type for the StorageAPI type @@ -129,6 +131,29 @@ func (_m *MockStorageAPI) GetStorageByBlockHash(bhash common.Hash, key []byte) ( return r0, r1 } +// GetStorageChild provides a mock function with given fields: root, keyToChild +func (_m *MockStorageAPI) GetStorageChild(root *common.Hash, keyToChild []byte) (*trie.Trie, error) { + ret := _m.Called(root, keyToChild) + + var r0 *trie.Trie + if rf, ok := ret.Get(0).(func(*common.Hash, []byte) *trie.Trie); ok { + r0 = rf(root, keyToChild) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*trie.Trie) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*common.Hash, []byte) error); ok { + r1 = rf(root, keyToChild) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // RegisterStorageObserver provides a mock function with given fields: observer func (_m *MockStorageAPI) RegisterStorageObserver(observer state.Observer) { _m.Called(observer)