diff --git a/dot/rpc/modules/rpc.go b/dot/rpc/modules/rpc.go index 9170d76e063..1a01adabbd0 100644 --- a/dot/rpc/modules/rpc.go +++ b/dot/rpc/modules/rpc.go @@ -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 diff --git a/dot/rpc/modules/state.go b/dot/rpc/modules/state.go index 742b023f6fe..1cae7e36127 100644 --- a/dot/rpc/modules/state.go +++ b/dot/rpc/modules/state.go @@ -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" ) @@ -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 @@ -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 @@ -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 diff --git a/dot/rpc/modules/state_test.go b/dot/rpc/modules/state_test.go index 3d6a6764eb3..491922804d4 100644 --- a/dot/rpc/modules/state_test.go +++ b/dot/rpc/modules/state_test.go @@ -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" @@ -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" ) @@ -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) diff --git a/tests/rpc/rpc_05-state_test.go b/tests/rpc/rpc_05-state_test.go index e40b21e5235..9d3573cce85 100644 --- a/tests/rpc/rpc_05-state_test.go +++ b/tests/rpc/rpc_05-state_test.go @@ -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" @@ -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) {