diff --git a/dot/rpc/modules/state.go b/dot/rpc/modules/state.go index f000c2b139..6266dfcd8e 100644 --- a/dot/rpc/modules/state.go +++ b/dot/rpc/modules/state.go @@ -76,6 +76,12 @@ type StateStorageQueryRangeRequest struct { EndBlock common.Hash `json:"block"` } +// StateStorageQueryAtRequest holds json fields +type StateStorageQueryAtRequest struct { + Keys []string `json:"keys" validate:"required"` + At common.Hash `json:"at"` +} + // StateStorageKeysQuery field to store storage keys type StateStorageKeysQuery [][]byte @@ -467,10 +473,13 @@ func (sm *StateModule) QueryStorage( var hexValue *string if len(value) > 0 { hexValue = stringPtr(common.BytesToHex(value)) + } else if value != nil { // empty byte slice value + hexValue = stringPtr("0x") } differentValueEncountered := i == startBlockNumber || lastValue[j] == nil && hexValue != nil || + lastValue[j] != nil && hexValue == nil || lastValue[j] != nil && *lastValue[j] != *hexValue if differentValueEncountered { changes = append(changes, [2]*string{stringPtr(key), hexValue}) @@ -489,6 +498,40 @@ func (sm *StateModule) QueryStorage( return nil } +// QueryStorageAt queries historical storage entries (by key) at the block hash given or +// the best block if the given block hash is nil +func (sm *StateModule) QueryStorageAt( + _ *http.Request, request *StateStorageQueryAtRequest, response *[]StorageChangeSetResponse) error { + atBlockHash := request.At + if atBlockHash.IsEmpty() { + atBlockHash = sm.blockAPI.BestBlockHash() + } + + changes := make([][2]*string, len(request.Keys)) + + for i, key := range request.Keys { + value, err := sm.storageAPI.GetStorageByBlockHash(&atBlockHash, common.MustHexToBytes(key)) + if err != nil { + return fmt.Errorf("getting value by block hash: %w", err) + } + var hexValue *string + if len(value) > 0 { + hexValue = stringPtr(common.BytesToHex(value)) + } else if value != nil { // empty byte slice value + hexValue = stringPtr("0x") + } + + changes[i] = [2]*string{stringPtr(key), hexValue} + } + + *response = []StorageChangeSetResponse{{ + Block: &atBlockHash, + Changes: changes, + }} + + return nil +} + func stringPtr(s string) *string { return &s } // SubscribeRuntimeVersion initialised a runtime version subscription and returns the current version diff --git a/dot/rpc/modules/state_test.go b/dot/rpc/modules/state_test.go index 7959625171..792e4edc0e 100644 --- a/dot/rpc/modules/state_test.go +++ b/dot/rpc/modules/state_test.go @@ -936,11 +936,11 @@ func TestStateModuleQueryStorage(t *testing.T) { storageAPIBuilder: func(ctrl *gomock.Controller) StorageAPI { mockStorageAPI := NewMockStorageAPI(ctrl) mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{1}, []byte{1, 2, 4}). - Return([]byte{}, nil) - mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{2}, []byte{1, 2, 4}). Return([]byte{1, 1, 1}, nil) + mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{2}, []byte{1, 2, 4}). + Return([]byte(nil), nil) mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{3}, []byte{1, 2, 4}). - Return([]byte{2, 2, 2}, nil) + Return([]byte{}, nil) mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{4}, []byte{1, 2, 4}). Return([]byte{3, 3, 3}, nil) return mockStorageAPI @@ -968,19 +968,19 @@ func TestStateModuleQueryStorage(t *testing.T) { { Block: &common.Hash{1}, Changes: [][2]*string{ - {stringPtr("0x010204"), nil}, + makeChange("0x010204", "0x010101"), }, }, { Block: &common.Hash{2}, Changes: [][2]*string{ - makeChange("0x010204", "0x010101"), + {stringPtr("0x010204"), nil}, }, }, { Block: &common.Hash{3}, Changes: [][2]*string{ - makeChange("0x010204", "0x020202"), + makeChange("0x010204", "0x"), }, }, { @@ -1129,3 +1129,139 @@ func TestStateModuleQueryStorage(t *testing.T) { }) } } +func TestStateModuleQueryStorageAt(t *testing.T) { + t.Parallel() + errTest := errors.New("test error") + + type fields struct { + storageAPIBuilder func(ctrl *gomock.Controller) *MockStorageAPI + blockAPIBuilder func(ctrl *gomock.Controller) *MockBlockAPI + } + + tests := map[string]struct { + fields fields + request *StateStorageQueryAtRequest + expectedError error + expectedResponse []StorageChangeSetResponse + }{ + "missing_start_block": { + fields: fields{ + storageAPIBuilder: func(ctrl *gomock.Controller) *MockStorageAPI { + mockStorageAPI := NewMockStorageAPI(ctrl) + mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{2}, []byte{1, 2, 3}). + Return([]byte{1, 1, 1}, nil) + return mockStorageAPI + }, + blockAPIBuilder: func(ctrl *gomock.Controller) *MockBlockAPI { + mockBlockAPI := NewMockBlockAPI(ctrl) + mockBlockAPI.EXPECT().BestBlockHash().Return(common.Hash{2}) + return mockBlockAPI + }}, + request: &StateStorageQueryAtRequest{ + Keys: []string{"0x010203"}, + }, + expectedResponse: []StorageChangeSetResponse{ + { + Block: &common.Hash{2}, + Changes: [][2]*string{ + makeChange("0x010203", "0x010101"), + }, + }, + }, + }, + "start_block_not_found_error": { + fields: fields{ + storageAPIBuilder: func(ctrl *gomock.Controller) *MockStorageAPI { + mockStorageAPI := NewMockStorageAPI(ctrl) + mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{1}, []byte{1, 2, 3}).Return(nil, errTest) + return mockStorageAPI + }, + blockAPIBuilder: func(ctrl *gomock.Controller) *MockBlockAPI { + return NewMockBlockAPI(ctrl) + }}, + request: &StateStorageQueryAtRequest{ + Keys: []string{"0x010203"}, + At: common.Hash{1}, + }, + expectedResponse: []StorageChangeSetResponse{}, + expectedError: errors.New("getting value by block hash: test error"), + }, + "start_block/multi_keys": { + fields: fields{ + storageAPIBuilder: func(ctrl *gomock.Controller) *MockStorageAPI { + mockStorageAPI := NewMockStorageAPI(ctrl) + mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{2}, []byte{8, 8, 8}). + Return([]byte{8, 8, 8, 8}, nil) + mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{2}, []byte{1, 2, 4}). + Return([]byte{}, nil) + mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{2}, []byte{9, 9, 9}). + Return([]byte(nil), nil) + return mockStorageAPI + }, + blockAPIBuilder: func(ctrl *gomock.Controller) *MockBlockAPI { + return NewMockBlockAPI(ctrl) + }}, + request: &StateStorageQueryAtRequest{ + Keys: []string{"0x080808", "0x010204", "0x090909"}, + At: common.Hash{2}, + }, + expectedResponse: []StorageChangeSetResponse{ + { + Block: &common.Hash{2}, + Changes: [][2]*string{ + makeChange("0x080808", "0x08080808"), + makeChange("0x010204", "0x"), + {stringPtr("0x090909"), nil}, + }, + }, + }, + }, + "missing_start_block/multi_keys": { + fields: fields{ + storageAPIBuilder: func(ctrl *gomock.Controller) *MockStorageAPI { + mockStorageAPI := NewMockStorageAPI(ctrl) + mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{2}, []byte{1, 2, 4}). + Return([]byte{1, 1, 1}, nil) + mockStorageAPI.EXPECT().GetStorageByBlockHash(&common.Hash{2}, []byte{9, 9, 9}). + Return([]byte{9, 9, 9, 9}, nil) + return mockStorageAPI + }, + blockAPIBuilder: func(ctrl *gomock.Controller) *MockBlockAPI { + mockBlockAPI := NewMockBlockAPI(ctrl) + mockBlockAPI.EXPECT().BestBlockHash().Return(common.Hash{2}) + return mockBlockAPI + }}, + request: &StateStorageQueryAtRequest{ + Keys: []string{"0x010204", "0x090909"}, + }, + expectedResponse: []StorageChangeSetResponse{ + { + Block: &common.Hash{2}, + Changes: [][2]*string{ + makeChange("0x010204", "0x010101"), + makeChange("0x090909", "0x09090909"), + }, + }, + }, + }, + } + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + sm := &StateModule{ + storageAPI: tt.fields.storageAPIBuilder(ctrl), + blockAPI: tt.fields.blockAPIBuilder(ctrl), + } + response := []StorageChangeSetResponse{} + err := sm.QueryStorageAt(nil, tt.request, &response) + if tt.expectedError != nil { + assert.EqualError(t, err, tt.expectedError.Error()) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expectedResponse, response) + }) + } +}