From 97a346422a1cc4effb09c9b360a59798be35cdda Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 23 Nov 2023 11:23:06 -0400 Subject: [PATCH 1/7] feat: export block trie state entries --- dot/rpc/modules/state.go | 35 ++++++++++++++ dot/rpc/modules/state_test.go | 89 +++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/dot/rpc/modules/state.go b/dot/rpc/modules/state.go index 742b023f6f..b562295646 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 []trie.Entry + // StateStorageKeysResponse field for storage keys type StateStorageKeysResponse []string @@ -245,6 +252,34 @@ func (sm *StateModule) GetPairs(_ *http.Request, req *StatePairRequest, res *Sta return nil } +func (sm *StateModule) Trie(_ *http.Request, req *StateTrieAtRequest, res *StateTrieResponse) error { + blockHash := sm.blockAPI.BestBlockHash() + if req.At != nil { + blockHash = *req.At + } + + 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([]trie.Entry, 0, len(entries)) + for key, value := range entries { + entriesArr = append(entriesArr, trie.Entry{ + Key: []byte(key), + Value: value, + }) + } + + *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 116d879b42..1f55225ef7 100644 --- a/dot/rpc/modules/state_test.go +++ b/dot/rpc/modules/state_test.go @@ -21,6 +21,7 @@ import ( "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" @@ -31,6 +32,7 @@ import ( "github.com/ChainSafe/gossamer/pkg/scale" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestStateModuleGetPairs(t *testing.T) { @@ -308,6 +310,93 @@ 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}) + + testcases := map[string]struct { + request StateTrieAtRequest + newStateModule func(t *testing.T) *StateModule + expected StateTrieResponse + }{ + "blockhash_parameter_nil": { + request: StateTrieAtRequest{At: nil}, + expected: []trie.Entry{ + {Key: []byte("entry-1"), Value: []byte{0, 1, 2, 3}}, + {Key: []byte("entry-2"), Value: []byte{3, 4, 5, 6}}, + }, + 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: []trie.Entry{ + {Key: []byte("entry-1"), Value: []byte{0, 1, 2, 3}}, + {Key: []byte("entry-2"), Value: []byte{3, 4, 5, 6}}, + }, + newStateModule: func(t *testing.T) *StateModule { + ctrl := gomock.NewController(t) + blockAPIMock := NewMockBlockAPI(ctrl) + + bestBlockHash := common.Hash([32]byte{1, 0, 1, 0, 1}) + 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(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) + require.Equal(t, tt.expected, res) + }) + } +} + func TestStateModuleGetMetadata(t *testing.T) { ctrl := gomock.NewController(t) From fd8056991b6a087ccfe2bf901856eb4bb2ae5219 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 30 Nov 2023 09:27:49 -0400 Subject: [PATCH 2/7] chore: include rpc test that asserts `state_trie` response --- dot/rpc/modules/rpc.go | 1 + dot/rpc/modules/state.go | 15 +++++++++++---- tests/rpc/rpc_05-state_test.go | 31 +++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/dot/rpc/modules/rpc.go b/dot/rpc/modules/rpc.go index 9170d76e06..1a01adabbd 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 b562295646..ad7e9e80d9 100644 --- a/dot/rpc/modules/state.go +++ b/dot/rpc/modules/state.go @@ -117,7 +117,7 @@ type StateStorageResponse string // StatePairResponse is a key values type StatePairResponse []interface{} -type StateTrieResponse []trie.Entry +type StateTrieResponse []string // StateStorageKeysResponse field for storage keys type StateStorageKeysResponse []string @@ -268,12 +268,19 @@ func (sm *StateModule) Trie(_ *http.Request, req *StateTrieAtRequest, res *State return fmt.Errorf("getting entries: %w", err) } - entriesArr := make([]trie.Entry, 0, len(entries)) + entriesArr := make([]string, 0, len(entries)) for key, value := range entries { - entriesArr = append(entriesArr, trie.Entry{ + 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 diff --git a/tests/rpc/rpc_05-state_test.go b/tests/rpc/rpc_05-state_test.go index e40b21e523..9d3573cce8 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) { From 20f0a43529dd46423efa71103d497a7c993621d6 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 30 Nov 2023 09:37:23 -0400 Subject: [PATCH 3/7] chore: solve `state_test` failure --- dot/rpc/modules/state_test.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/dot/rpc/modules/state_test.go b/dot/rpc/modules/state_test.go index 1f55225ef7..c338dea30d 100644 --- a/dot/rpc/modules/state_test.go +++ b/dot/rpc/modules/state_test.go @@ -312,6 +312,15 @@ func TestCall(t *testing.T) { 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 @@ -319,11 +328,8 @@ func TestStateTrie(t *testing.T) { expected StateTrieResponse }{ "blockhash_parameter_nil": { - request: StateTrieAtRequest{At: nil}, - expected: []trie.Entry{ - {Key: []byte("entry-1"), Value: []byte{0, 1, 2, 3}}, - {Key: []byte("entry-2"), Value: []byte{3, 4, 5, 6}}, - }, + request: StateTrieAtRequest{At: nil}, + expected: expectedEncodedSlice, newStateModule: func(t *testing.T) *StateModule { ctrl := gomock.NewController(t) @@ -350,11 +356,8 @@ func TestStateTrie(t *testing.T) { }, }, "blockhash_parameter_not_nil": { - request: StateTrieAtRequest{At: &expecificBlockHash}, - expected: []trie.Entry{ - {Key: []byte("entry-1"), Value: []byte{0, 1, 2, 3}}, - {Key: []byte("entry-2"), Value: []byte{3, 4, 5, 6}}, - }, + request: StateTrieAtRequest{At: &expecificBlockHash}, + expected: expectedEncodedSlice, newStateModule: func(t *testing.T) *StateModule { ctrl := gomock.NewController(t) blockAPIMock := NewMockBlockAPI(ctrl) From 2ab123c1d734f4f5152279aa0c97810da43d24d0 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 30 Nov 2023 09:44:36 -0400 Subject: [PATCH 4/7] chore: use `slices.Sort` to avoid any position diff while comparing --- dot/rpc/modules/state_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dot/rpc/modules/state_test.go b/dot/rpc/modules/state_test.go index c338dea30d..b8747d8016 100644 --- a/dot/rpc/modules/state_test.go +++ b/dot/rpc/modules/state_test.go @@ -18,6 +18,7 @@ package modules import ( "errors" "net/http" + "slices" "testing" wazero_runtime "github.com/ChainSafe/gossamer/lib/runtime/wazero" @@ -395,6 +396,9 @@ func TestStateTrie(t *testing.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) }) } From bca5ca9c81b7849ac96b04acf60f7fa0fb755730 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 30 Nov 2023 10:37:16 -0400 Subject: [PATCH 5/7] chore: improve exported comment at `state_trie` RPC method --- dot/rpc/modules/state.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dot/rpc/modules/state.go b/dot/rpc/modules/state.go index ad7e9e80d9..d69549c8d0 100644 --- a/dot/rpc/modules/state.go +++ b/dot/rpc/modules/state.go @@ -252,6 +252,8 @@ 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 { blockHash := sm.blockAPI.BestBlockHash() if req.At != nil { From cc19521183f0d679afaf48f5b5129ecf95f6c707 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 5 Dec 2023 09:13:51 -0400 Subject: [PATCH 6/7] chore: retrive best block hash when necessary --- dot/rpc/modules/state.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dot/rpc/modules/state.go b/dot/rpc/modules/state.go index d69549c8d0..1cae7e3612 100644 --- a/dot/rpc/modules/state.go +++ b/dot/rpc/modules/state.go @@ -255,9 +255,12 @@ func (sm *StateModule) GetPairs(_ *http.Request, req *StatePairRequest, res *Sta // 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 { - blockHash := sm.blockAPI.BestBlockHash() + var blockHash common.Hash + if req.At != nil { blockHash = *req.At + } else { + blockHash = sm.blockAPI.BestBlockHash() } blockHeader, err := sm.blockAPI.GetHeader(blockHash) From c1e76ca91ebb9fdbbdbdbd47c22db44f0bfdefb0 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 6 Dec 2023 15:44:32 -0400 Subject: [PATCH 7/7] chore: fix unexepected call to a mocked structure --- dot/rpc/modules/state_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/dot/rpc/modules/state_test.go b/dot/rpc/modules/state_test.go index 0325e2eac1..491922804d 100644 --- a/dot/rpc/modules/state_test.go +++ b/dot/rpc/modules/state_test.go @@ -363,9 +363,6 @@ func TestStateTrie(t *testing.T) { ctrl := gomock.NewController(t) blockAPIMock := NewMockBlockAPI(ctrl) - bestBlockHash := common.Hash([32]byte{1, 0, 1, 0, 1}) - 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{})