Skip to content

Commit

Permalink
feat(dot/rpc): export block trie state entries for a block hash (#3607)
Browse files Browse the repository at this point in the history
  • Loading branch information
EclesioMeloJunior authored and timwu20 committed Apr 18, 2024
1 parent 8f28c20 commit 07472c2
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 2 deletions.
1 change: 1 addition & 0 deletions dot/rpc/modules/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var (
"state_getPairs",
"state_getKeysPaged",
"state_queryStorage",
"state_trie",
}

// AliasesMethods is a map that links the original methods to their aliases
Expand Down
47 changes: 47 additions & 0 deletions dot/rpc/modules/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/ChainSafe/gossamer/lib/common"
"github.com/ChainSafe/gossamer/lib/runtime"
"github.com/ChainSafe/gossamer/lib/trie"
"github.com/ChainSafe/gossamer/pkg/scale"
)

Expand Down Expand Up @@ -82,6 +83,10 @@ type StateStorageQueryAtRequest struct {
At common.Hash `json:"at"`
}

type StateTrieAtRequest struct {
At *common.Hash `json:"at"`
}

// StateStorageKeysQuery field to store storage keys
type StateStorageKeysQuery [][]byte

Expand Down Expand Up @@ -112,6 +117,8 @@ type StateStorageResponse string
// StatePairResponse is a key values
type StatePairResponse []interface{}

type StateTrieResponse []string

// StateStorageKeysResponse field for storage keys
type StateStorageKeysResponse []string

Expand Down Expand Up @@ -245,6 +252,46 @@ func (sm *StateModule) GetPairs(_ *http.Request, req *StatePairRequest, res *Sta
return nil
}

// Trie RPC method returns a list of scale encoded trie.Entry{Key byte, Value byte} representing
// all the entries in a trie for a block hash, if no block hash is given then it uses the best block hash
func (sm *StateModule) Trie(_ *http.Request, req *StateTrieAtRequest, res *StateTrieResponse) error {
var blockHash common.Hash

if req.At != nil {
blockHash = *req.At
} else {
blockHash = sm.blockAPI.BestBlockHash()
}

blockHeader, err := sm.blockAPI.GetHeader(blockHash)
if err != nil {
return fmt.Errorf("getting header: %w", err)
}

entries, err := sm.storageAPI.Entries(&blockHeader.StateRoot)
if err != nil {
return fmt.Errorf("getting entries: %w", err)
}

entriesArr := make([]string, 0, len(entries))
for key, value := range entries {
entry := trie.Entry{
Key: []byte(key),
Value: value,
}

encodedEntry, err := scale.Marshal(entry)
if err != nil {
return fmt.Errorf("scale encoding entry: %w", err)
}

entriesArr = append(entriesArr, common.BytesToHex(encodedEntry))
}

*res = entriesArr
return nil
}

// Call makes a call to the runtime.
func (sm *StateModule) Call(_ *http.Request, req *StateCallRequest, res *StateCallResponse) error {
var blockHash common.Hash
Expand Down
93 changes: 93 additions & 0 deletions dot/rpc/modules/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ package modules
import (
"errors"
"net/http"
"slices"
"testing"

wazero_runtime "github.com/ChainSafe/gossamer/lib/runtime/wazero"
"github.com/ChainSafe/gossamer/lib/trie"

"github.com/ChainSafe/gossamer/dot/rpc/modules/mocks"
testdata "github.com/ChainSafe/gossamer/dot/rpc/modules/test_data"
Expand All @@ -30,6 +32,7 @@ import (
"github.com/ChainSafe/gossamer/lib/runtime"
"github.com/ChainSafe/gossamer/pkg/scale"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)

Expand Down Expand Up @@ -308,6 +311,96 @@ func TestCall(t *testing.T) {
assert.NotEmpty(t, res)
}

func TestStateTrie(t *testing.T) {
expecificBlockHash := common.Hash([32]byte{6, 6, 6, 6, 6, 6})
var expectedEncodedSlice []string
entries := []trie.Entry{
{Key: []byte("entry-1"), Value: []byte{0, 1, 2, 3}},
{Key: []byte("entry-2"), Value: []byte{3, 4, 5, 6}},
}

for _, entry := range entries {
expectedEncodedSlice = append(expectedEncodedSlice, common.BytesToHex(scale.MustMarshal(entry)))
}

testcases := map[string]struct {
request StateTrieAtRequest
newStateModule func(t *testing.T) *StateModule
expected StateTrieResponse
}{
"blockhash_parameter_nil": {
request: StateTrieAtRequest{At: nil},
expected: expectedEncodedSlice,
newStateModule: func(t *testing.T) *StateModule {
ctrl := gomock.NewController(t)

bestBlockHash := common.Hash([32]byte{1, 0, 1, 0, 1})
blockAPIMock := NewMockBlockAPI(ctrl)
blockAPIMock.EXPECT().BestBlockHash().Return(bestBlockHash)

fakeStateRoot := common.Hash([32]byte{5, 5, 5, 5, 5})
fakeBlockHeader := types.NewHeader(common.EmptyHash, fakeStateRoot,
common.EmptyHash, 1, scale.VaryingDataTypeSlice{})

blockAPIMock.EXPECT().GetHeader(bestBlockHash).Return(fakeBlockHeader, nil)

fakeEntries := map[string][]byte{
"entry-1": {0, 1, 2, 3},
"entry-2": {3, 4, 5, 6},
}
storageAPIMock := NewMockStorageAPI(ctrl)
storageAPIMock.EXPECT().Entries(&fakeStateRoot).
Return(fakeEntries, nil)

sm := NewStateModule(nil, storageAPIMock, nil, blockAPIMock)
return sm
},
},
"blockhash_parameter_not_nil": {
request: StateTrieAtRequest{At: &expecificBlockHash},
expected: expectedEncodedSlice,
newStateModule: func(t *testing.T) *StateModule {
ctrl := gomock.NewController(t)
blockAPIMock := NewMockBlockAPI(ctrl)

fakeStateRoot := common.Hash([32]byte{5, 5, 5, 5, 5})
fakeBlockHeader := types.NewHeader(common.EmptyHash, fakeStateRoot,
common.EmptyHash, 1, scale.VaryingDataTypeSlice{})

blockAPIMock.EXPECT().GetHeader(expecificBlockHash).
Return(fakeBlockHeader, nil)

fakeEntries := map[string][]byte{
"entry-1": {0, 1, 2, 3},
"entry-2": {3, 4, 5, 6},
}
storageAPIMock := NewMockStorageAPI(ctrl)
storageAPIMock.EXPECT().Entries(&fakeStateRoot).
Return(fakeEntries, nil)

sm := NewStateModule(nil, storageAPIMock, nil, blockAPIMock)
return sm
},
},
}

for tname, tt := range testcases {
tt := tt

t.Run(tname, func(t *testing.T) {
sm := tt.newStateModule(t)

var res StateTrieResponse
err := sm.Trie(nil, &tt.request, &res)
require.NoError(t, err)

slices.Sort(tt.expected)
slices.Sort(res)
require.Equal(t, tt.expected, res)
})
}
}

func TestStateModuleGetMetadata(t *testing.T) {
ctrl := gomock.NewController(t)

Expand Down
31 changes: 29 additions & 2 deletions tests/rpc/rpc_05-state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import (
"time"

"github.com/ChainSafe/gossamer/dot/rpc/modules"
"github.com/ChainSafe/gossamer/lib/runtime"

"github.com/ChainSafe/gossamer/lib/common"
"github.com/ChainSafe/gossamer/lib/runtime"
"github.com/ChainSafe/gossamer/lib/trie"
libutils "github.com/ChainSafe/gossamer/lib/utils"
"github.com/ChainSafe/gossamer/pkg/scale"
"github.com/ChainSafe/gossamer/tests/utils/config"
"github.com/ChainSafe/gossamer/tests/utils/node"
"github.com/ChainSafe/gossamer/tests/utils/rpc"
Expand All @@ -33,6 +34,32 @@ func TestStateRPCResponseValidation(t *testing.T) { //nolint:tparallel
getBlockHashCancel()
require.NoError(t, err)

t.Run("state_trie", func(t *testing.T) {
t.Parallel()
const westendDevGenesisHash = "0x276bfa91f70859348285599321ea96afd3ae681f0be47d36196bac8075ea32e8"
const westendDevStateRoot = "0x953044ba4386a72ae434d2a2fbdfca77640a28ac3841a924674cbfe7a8b9a81c"
params := fmt.Sprintf(`["%s"]`, westendDevGenesisHash)

var response modules.StateTrieResponse
fetchWithTimeout(ctx, t, "state_trie", params, &response)

entries := make(map[string]string, len(response))
for _, encodedEntry := range response {
bytesEncodedEntry := common.MustHexToBytes(encodedEntry)

entry := trie.Entry{}
err := scale.Unmarshal(bytesEncodedEntry, &entry)
require.NoError(t, err)
entries[common.BytesToHex(entry.Key)] = common.BytesToHex(entry.Value)
}

newTrie, err := trie.LoadFromMap(entries)
require.NoError(t, err)

trieHash := newTrie.MustHash(trie.V0.MaxInlineValue())
require.Equal(t, westendDevStateRoot, trieHash.String())
})

// TODO: Improve runtime tests
// https://github.com/ChainSafe/gossamer/issues/3234
t.Run("state_call", func(t *testing.T) {
Expand Down

0 comments on commit 07472c2

Please sign in to comment.