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)