Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dot/rpc/modules) implement state_queryStorage rpc method #1707

Merged
merged 30 commits into from
Aug 6, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dae9318
wip: state query storage rpc
EclesioMeloJunior Jul 19, 2021
1fa9a96
chore: query keys from block stateRoot
EclesioMeloJunior Jul 26, 2021
49a92d5
chore: implemented rpc method and format response correctly
EclesioMeloJunior Jul 27, 2021
8d02937
chore: resolve conflicts, check value is nil
EclesioMeloJunior Jul 28, 2021
5062c5d
chore: check starting block hash is nil
EclesioMeloJunior Jul 28, 2021
729fb22
chore: use GetStorage instead of TrieState
EclesioMeloJunior Jul 28, 2021
86b1d6f
chore: remove logs
EclesioMeloJunior Jul 28, 2021
3ba79fe
chore: resolve lint issues
EclesioMeloJunior Jul 28, 2021
50a8fba
chore: preallocate slice, use varidic param
EclesioMeloJunior Jul 28, 2021
59189ea
chore: update variable name
EclesioMeloJunior Jul 28, 2021
6cb2465
Merge branch 'development' into eclesio/rpc-state-query-storage
EclesioMeloJunior Aug 1, 2021
91b371c
chore: resolve conflicts
EclesioMeloJunior Aug 3, 2021
08bfddc
Merge branch 'eclesio/rpc-state-query-storage' of github.com:ChainSaf…
EclesioMeloJunior Aug 3, 2021
6e7d83b
chore: made from not nulable
EclesioMeloJunior Aug 3, 2021
ade00e4
chore: get best block hash if to is nil
EclesioMeloJunior Aug 3, 2021
0845fd0
chore: resolve conflicts between development
EclesioMeloJunior Aug 4, 2021
04fdfbe
Merge branch 'development' into eclesio/rpc-state-query-storage
EclesioMeloJunior Aug 4, 2021
ea91284
Merge branch 'development' into eclesio/rpc-state-query-storage
EclesioMeloJunior Aug 5, 2021
5f3f69a
chore: remove pointer and compare with EmptyHash
EclesioMeloJunior Aug 5, 2021
a709595
Merge branch 'development' into eclesio/rpc-state-query-storage
EclesioMeloJunior Aug 6, 2021
0364347
chore: fix tests, remove write to nil reference
EclesioMeloJunior Aug 6, 2021
5002961
Merge branch 'eclesio/rpc-state-query-storage' of github.com:ChainSaf…
EclesioMeloJunior Aug 6, 2021
65d034c
chore: fix deepsource rule
EclesioMeloJunior Aug 6, 2021
0754903
chore: fix rpc test
EclesioMeloJunior Aug 6, 2021
59cbcc2
chore: uncomment conditional test
EclesioMeloJunior Aug 6, 2021
d8047ba
chore: improve testing
EclesioMeloJunior Aug 6, 2021
d46afba
Merge branch 'development' into eclesio/rpc-state-query-storage
EclesioMeloJunior Aug 6, 2021
4f0ac85
chore: change from null to a empty array
EclesioMeloJunior Aug 6, 2021
27b96d3
chore: skip state_queryStorage RPC method test
EclesioMeloJunior Aug 6, 2021
d4264b4
chore: remove unconsitent test check
EclesioMeloJunior Aug 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dot/core/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type StorageState interface {
TrieState(root *common.Hash) (*rtstorage.TrieState, error)
StoreTrie(*rtstorage.TrieState, *types.Header) error
GetStateRootFromBlock(bhash *common.Hash) (*common.Hash, error)
GetStorage(root *common.Hash, key []byte) ([]byte, error)
}

// TransactionState is the interface for transaction state methods
Expand Down
64 changes: 64 additions & 0 deletions dot/core/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package core

import (
"context"
"errors"
"math/big"
"os"
"sync"
Expand Down Expand Up @@ -550,3 +551,66 @@ func (s *Service) GetMetadata(bhash *common.Hash) ([]byte, error) {
rt.SetContextStorage(ts)
return rt.Metadata()
}

type (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we declare this at the top or somewhere in lib/common?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I move to the top

// Changes represents the key-value data inside a block storage
Changes map[string]string
)

// QueryStorage returns the key-value data by block based on `keys` params
// on every block starting `from` until `to` block, if `to` is not nil
func (s *Service) QueryStorage(from *common.Hash, to *common.Hash, keys []string) (map[common.Hash]Changes, error) {
if from == nil {
return nil, errors.New("cannot query data without a starting block hash")
}

var err error
blocksToQuery := []common.Hash{*from}
if to != nil {
if blocksToQuery, err = s.blockState.SubChain(*from, *to); err != nil {
return nil, err
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if there is no to provided, you should set it to the BestBlockHash, the spec implies if no to is provided then you query every block from to until the head of the chain. can you double check what substrate returns?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok! I will take a look at substrate node implementation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update to use the BestBlockHash if to is nil


queries := make(map[common.Hash]Changes)
for _, hash := range blocksToQuery {
changes, err := s.tryQueryStorage(hash, keys)
if err != nil {
return nil, err
}

queries[hash] = changes
}

return queries, nil
}

// tryQueryStorage will try to get all the `keys` inside the block's current state
func (s *Service) tryQueryStorage(block common.Hash, keys []string) (Changes, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (s *Service) tryQueryStorage(block common.Hash, keys []string) (Changes, error) {
func (s *Service) QueryStorage(block common.Hash, keys ...string) (Changes, error) {

It's assumed you are making a best effort to retrieve the Changes. Variadic parameter would be nice for single keys.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did we need to change the method's name?

stateRootHash, err := s.storageState.GetStateRootFromBlock(&block)
if err != nil {
return nil, err
}

changes := make(map[string]string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better if we make this slice initially instead of a map?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
changes := make(map[string]string)
changes := make(Changes)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as a map, this function can return the data as key -> value format, I don't know if a slice will be better for represent the data inside the block


for _, k := range keys {
keyBytes, err := common.HexToBytes(k)
if err != nil {
return nil, err
}

storedData, err := s.storageState.GetStorage(stateRootHash, keyBytes)
if err != nil {
return nil, err
}

if storedData == nil {
continue
}

changes[k] = common.BytesToHex(storedData)
}

return changes, nil
}
170 changes: 170 additions & 0 deletions dot/core/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ import (
"github.com/ChainSafe/gossamer/lib/keystore"
"github.com/ChainSafe/gossamer/lib/runtime"
"github.com/ChainSafe/gossamer/lib/runtime/extrinsic"
"github.com/ChainSafe/gossamer/lib/runtime/storage"
"github.com/ChainSafe/gossamer/lib/runtime/wasmer"
"github.com/ChainSafe/gossamer/lib/transaction"
"github.com/ChainSafe/gossamer/lib/trie"
"github.com/ChainSafe/gossamer/lib/utils"
log "github.com/ChainSafe/log15"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -614,3 +616,171 @@ func TestService_HandleRuntimeChangesAfterCodeSubstitutes(t *testing.T) {

require.NotEqualf(t, codeHashBefore, rt.GetCodeHash(), "expected different code hash after runtime update") // codeHash should change after runtime change
}

func TestTryQueryStore_WhenThereIsDataToRetrieve(t *testing.T) {
s := NewTestService(t, nil)
storageStateTrie, err := storage.NewTrieState(trie.NewTrie(nil))

testKey, testValue := []byte("to"), []byte("0x1723712318238AB12312")
storageStateTrie.Set(testKey, testValue)
require.NoError(t, err)

header, err := types.NewHeader(s.blockState.GenesisHash(), storageStateTrie.MustRoot(),
common.Hash{}, big.NewInt(1), nil)
require.NoError(t, err)

err = s.storageState.StoreTrie(storageStateTrie, header)
require.NoError(t, err)

testBlock := &types.Block{
Header: header,
Body: types.NewBody([]byte{}),
}

err = s.blockState.AddBlock(testBlock)
require.NoError(t, err)

blockhash := testBlock.Header.Hash()
hexKey := common.BytesToHex(testKey)
keys := []string{hexKey}

changes, err := s.tryQueryStorage(blockhash, keys)
require.NoError(t, err)

require.Equal(t, changes[hexKey], common.BytesToHex(testValue))
}

func TestTryQueryStore_WhenDoesNotHaveDataToRetrieve(t *testing.T) {
s := NewTestService(t, nil)
storageStateTrie, err := storage.NewTrieState(trie.NewTrie(nil))
require.NoError(t, err)

header, err := types.NewHeader(s.blockState.GenesisHash(), storageStateTrie.MustRoot(),
common.Hash{}, big.NewInt(1), nil)
require.NoError(t, err)

err = s.storageState.StoreTrie(storageStateTrie, header)
require.NoError(t, err)

testBlock := &types.Block{
Header: header,
Body: types.NewBody([]byte{}),
}

err = s.blockState.AddBlock(testBlock)
require.NoError(t, err)

testKey := []byte("to")
blockhash := testBlock.Header.Hash()
hexKey := common.BytesToHex(testKey)
keys := []string{hexKey}

changes, err := s.tryQueryStorage(blockhash, keys)
require.NoError(t, err)

require.Empty(t, changes)
}

func TestTryQueryState_WhenDoesNotHaveStateRoot(t *testing.T) {
s := NewTestService(t, nil)

header, err := types.NewHeader(s.blockState.GenesisHash(), common.Hash{}, common.Hash{}, big.NewInt(1), nil)
require.NoError(t, err)

testBlock := &types.Block{
Header: header,
Body: types.NewBody([]byte{}),
}

err = s.blockState.AddBlock(testBlock)
require.NoError(t, err)

testKey := []byte("to")
blockhash := testBlock.Header.Hash()
hexKey := common.BytesToHex(testKey)
keys := []string{hexKey}

changes, err := s.tryQueryStorage(blockhash, keys)
require.Error(t, err)
require.Nil(t, changes)
}

func TestQueryStorate_WhenBlocksHasData(t *testing.T) {
keys := []string{
common.BytesToHex([]byte("transfer.to")),
common.BytesToHex([]byte("transfer.from")),
common.BytesToHex([]byte("transfer.value")),
}

s := NewTestService(t, nil)
firstKey, firstValue := []byte("transfer.to"), []byte("some-address-herer")

firstBlock := createNewBlockAndStoreDataAtBlock(
t, s, firstKey, firstValue, s.blockState.GenesisHash(), 1,
)

secondKey, secondValue := []byte("transfer.from"), []byte("another-address-here")
secondBlock := createNewBlockAndStoreDataAtBlock(
t, s, secondKey, secondValue, firstBlock.Header.Hash(), 2,
)

thirdKey, thirdValue := []byte("transfer.value"), []byte("value-gigamegablaster")
thirdBlock := createNewBlockAndStoreDataAtBlock(
t, s, thirdKey, thirdValue, secondBlock.Header.Hash(), 3,
)

from := firstBlock.Header.Hash()

data, err := s.QueryStorage(&from, nil, keys)
require.NoError(t, err)
require.Len(t, data, 1)

require.Equal(t, data[firstBlock.Header.Hash()], Changes(
map[string]string{
common.BytesToHex(firstKey): common.BytesToHex(firstValue),
},
))

from = secondBlock.Header.Hash()
to := thirdBlock.Header.Hash()

data, err = s.QueryStorage(&from, &to, keys)
require.NoError(t, err)
require.Len(t, data, 2)

require.Equal(t, data[secondBlock.Header.Hash()], Changes(
map[string]string{
common.BytesToHex(secondKey): common.BytesToHex(secondValue),
},
))
require.Equal(t, data[thirdBlock.Header.Hash()], Changes(
map[string]string{
common.BytesToHex(thirdKey): common.BytesToHex(thirdValue),
},
))
}

func createNewBlockAndStoreDataAtBlock(t *testing.T, s *Service, key, value []byte, parentHash common.Hash, number int64) *types.Block {
t.Helper()

storageStateTrie, err := storage.NewTrieState(trie.NewTrie(nil))
storageStateTrie.Set(key, value)
require.NoError(t, err)

header, err := types.NewHeader(parentHash, storageStateTrie.MustRoot(),
common.Hash{}, big.NewInt(number), nil)
require.NoError(t, err)

err = s.storageState.StoreTrie(storageStateTrie, header)
require.NoError(t, err)

testBlock := &types.Block{
Header: header,
Body: types.NewBody([]byte{}),
}

err = s.blockState.AddBlock(testBlock)
require.NoError(t, err)

return testBlock
}
2 changes: 2 additions & 0 deletions dot/rpc/modules/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package modules
import (
"math/big"

"github.com/ChainSafe/gossamer/dot/core"
"github.com/ChainSafe/gossamer/dot/state"
"github.com/ChainSafe/gossamer/dot/types"
"github.com/ChainSafe/gossamer/lib/common"
Expand Down Expand Up @@ -76,6 +77,7 @@ type CoreAPI interface {
GetRuntimeVersion(bhash *common.Hash) (runtime.Version, error)
HandleSubmittedExtrinsic(types.Extrinsic) error
GetMetadata(bhash *common.Hash) ([]byte, error)
QueryStorage(from *common.Hash, to *common.Hash, keys []string) (map[common.Hash]core.Changes, error)
}

// RPCAPI is the interface for methods related to RPC service
Expand Down
27 changes: 19 additions & 8 deletions dot/rpc/modules/mocks/core_api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 27 additions & 7 deletions dot/rpc/modules/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ type StateStorageRequest struct {

// StateStorageQueryRangeRequest holds json fields
type StateStorageQueryRangeRequest struct {
Keys []*common.Hash `json:"keys" validate:"required"`
StartBlock *common.Hash `json:"startBlock" validate:"required"`
Block *common.Hash `json:"block"`
Keys []string `json:"keys" validate:"required"`
StartBlock *common.Hash `json:"startBlock" validate:"required"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is required, can you change it to common.Hash?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

EndBlock *common.Hash `json:"block"`
}

// StateStorageKeysQuery field to store storage keys
Expand Down Expand Up @@ -129,8 +129,8 @@ type StateMetadataResponse string

// StorageChangeSetResponse is the struct that holds the block and changes
type StorageChangeSetResponse struct {
Block *common.Hash
Changes []KeyValueOption
Block *common.Hash `json:"block"`
Changes [][]string `json:"changes"`
}

// KeyValueOption struct holds json fields
Expand Down Expand Up @@ -387,8 +387,28 @@ func (sm *StateModule) GetStorageSize(r *http.Request, req *StateStorageSizeRequ
}

// QueryStorage isn't implemented properly yet.
func (sm *StateModule) QueryStorage(r *http.Request, req *StateStorageQueryRangeRequest, res *StorageChangeSetResponse) error {
// TODO implement change storage trie so that block hash parameter works (See issue #834)
func (sm *StateModule) QueryStorage(r *http.Request, req *StateStorageQueryRangeRequest, res *[]StorageChangeSetResponse) error {
changesByBlock, err := sm.coreAPI.QueryStorage(req.StartBlock, req.EndBlock, req.Keys)
if err != nil {
return err
}

var response []StorageChangeSetResponse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-allocate this slice.

response := make([]StorageChangeSetResponse, 0, len(changesByBlock))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


for block, c := range changesByBlock {
var changes [][]string

for key, value := range c {
changes = append(changes, []string{key, value})
}

response = append(response, StorageChangeSetResponse{
Block: &block,
Changes: changes,
})
}

*res = response
return nil
}

Expand Down