diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 69b76c6026..11185af6d0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -132,6 +132,7 @@ jobs: name: Run stable tests run: | docker run chainsafe/gossamer:test sh -c "make it-stable" + docker-rpc-tests: runs-on: ubuntu-latest steps: @@ -155,6 +156,7 @@ jobs: name: Run rpc tests run: | docker run chainsafe/gossamer:test sh -c "make it-rpc" + docker-stress-tests: runs-on: ubuntu-latest steps: @@ -178,6 +180,7 @@ jobs: name: Run stress run: | docker run chainsafe/gossamer:test sh -c "make it-stress" + docker-grandpa-tests: runs-on: ubuntu-latest steps: diff --git a/dot/core/interface.go b/dot/core/interface.go index c9760ae0ce..83fe0ab21f 100644 --- a/dot/core/interface.go +++ b/dot/core/interface.go @@ -60,6 +60,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 diff --git a/dot/core/mocks/block_producer.go b/dot/core/mocks/block_producer.go index 341cc96b25..16c549ab1a 100644 --- a/dot/core/mocks/block_producer.go +++ b/dot/core/mocks/block_producer.go @@ -1,6 +1,6 @@ // Code generated by mockery v2.8.0. DO NOT EDIT. -package core +package mocks import ( types "github.com/ChainSafe/gossamer/dot/types" diff --git a/dot/core/mocks/block_state.go b/dot/core/mocks/block_state.go new file mode 100644 index 0000000000..7686c23dab --- /dev/null +++ b/dot/core/mocks/block_state.go @@ -0,0 +1,443 @@ +// Code generated by mockery v2.8.0. DO NOT EDIT. + +package mocks + +import ( + big "math/big" + + common "github.com/ChainSafe/gossamer/lib/common" + + mock "github.com/stretchr/testify/mock" + + runtime "github.com/ChainSafe/gossamer/lib/runtime" + + storage "github.com/ChainSafe/gossamer/lib/runtime/storage" + + types "github.com/ChainSafe/gossamer/dot/types" +) + +// MockBlockState is an autogenerated mock type for the BlockState type +type MockBlockState struct { + mock.Mock +} + +// AddBlock provides a mock function with given fields: _a0 +func (_m *MockBlockState) AddBlock(_a0 *types.Block) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*types.Block) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// BestBlock provides a mock function with given fields: +func (_m *MockBlockState) BestBlock() (*types.Block, error) { + ret := _m.Called() + + var r0 *types.Block + if rf, ok := ret.Get(0).(func() *types.Block); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Block) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BestBlockHash provides a mock function with given fields: +func (_m *MockBlockState) BestBlockHash() common.Hash { + ret := _m.Called() + + var r0 common.Hash + if rf, ok := ret.Get(0).(func() common.Hash); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + return r0 +} + +// BestBlockHeader provides a mock function with given fields: +func (_m *MockBlockState) BestBlockHeader() (*types.Header, error) { + ret := _m.Called() + + var r0 *types.Header + if rf, ok := ret.Get(0).(func() *types.Header); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Header) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BestBlockNumber provides a mock function with given fields: +func (_m *MockBlockState) BestBlockNumber() (*big.Int, error) { + ret := _m.Called() + + var r0 *big.Int + if rf, ok := ret.Get(0).(func() *big.Int); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BestBlockStateRoot provides a mock function with given fields: +func (_m *MockBlockState) BestBlockStateRoot() (common.Hash, error) { + ret := _m.Called() + + var r0 common.Hash + if rf, ok := ret.Get(0).(func() common.Hash); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GenesisHash provides a mock function with given fields: +func (_m *MockBlockState) GenesisHash() common.Hash { + ret := _m.Called() + + var r0 common.Hash + if rf, ok := ret.Get(0).(func() common.Hash); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + return r0 +} + +// GetAllBlocksAtDepth provides a mock function with given fields: hash +func (_m *MockBlockState) GetAllBlocksAtDepth(hash common.Hash) []common.Hash { + ret := _m.Called(hash) + + var r0 []common.Hash + if rf, ok := ret.Get(0).(func(common.Hash) []common.Hash); ok { + r0 = rf(hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]common.Hash) + } + } + + return r0 +} + +// GetBlockBody provides a mock function with given fields: hash +func (_m *MockBlockState) GetBlockBody(hash common.Hash) (*types.Body, error) { + ret := _m.Called(hash) + + var r0 *types.Body + if rf, ok := ret.Get(0).(func(common.Hash) *types.Body); ok { + r0 = rf(hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Body) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Hash) error); ok { + r1 = rf(hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetBlockByHash provides a mock function with given fields: _a0 +func (_m *MockBlockState) GetBlockByHash(_a0 common.Hash) (*types.Block, error) { + ret := _m.Called(_a0) + + var r0 *types.Block + if rf, ok := ret.Get(0).(func(common.Hash) *types.Block); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Block) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Hash) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetFinalisedHash provides a mock function with given fields: _a0, _a1 +func (_m *MockBlockState) GetFinalisedHash(_a0 uint64, _a1 uint64) (common.Hash, error) { + ret := _m.Called(_a0, _a1) + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(uint64, uint64) common.Hash); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(uint64, uint64) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetFinalisedHeader provides a mock function with given fields: _a0, _a1 +func (_m *MockBlockState) GetFinalisedHeader(_a0 uint64, _a1 uint64) (*types.Header, error) { + ret := _m.Called(_a0, _a1) + + var r0 *types.Header + if rf, ok := ret.Get(0).(func(uint64, uint64) *types.Header); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Header) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(uint64, uint64) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRuntime provides a mock function with given fields: _a0 +func (_m *MockBlockState) GetRuntime(_a0 *common.Hash) (runtime.Instance, error) { + ret := _m.Called(_a0) + + var r0 runtime.Instance + if rf, ok := ret.Get(0).(func(*common.Hash) runtime.Instance); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(runtime.Instance) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*common.Hash) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSlotForBlock provides a mock function with given fields: _a0 +func (_m *MockBlockState) GetSlotForBlock(_a0 common.Hash) (uint64, error) { + ret := _m.Called(_a0) + + var r0 uint64 + if rf, ok := ret.Get(0).(func(common.Hash) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Hash) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// HandleRuntimeChanges provides a mock function with given fields: newState, in, bHash +func (_m *MockBlockState) HandleRuntimeChanges(newState *storage.TrieState, in runtime.Instance, bHash common.Hash) error { + ret := _m.Called(newState, in, bHash) + + var r0 error + if rf, ok := ret.Get(0).(func(*storage.TrieState, runtime.Instance, common.Hash) error); ok { + r0 = rf(newState, in, bHash) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// HighestCommonAncestor provides a mock function with given fields: a, b +func (_m *MockBlockState) HighestCommonAncestor(a common.Hash, b common.Hash) (common.Hash, error) { + ret := _m.Called(a, b) + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(common.Hash, common.Hash) common.Hash); ok { + r0 = rf(a, b) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Hash, common.Hash) error); ok { + r1 = rf(a, b) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RegisterFinalizedChannel provides a mock function with given fields: ch +func (_m *MockBlockState) RegisterFinalizedChannel(ch chan<- *types.FinalisationInfo) (byte, error) { + ret := _m.Called(ch) + + var r0 byte + if rf, ok := ret.Get(0).(func(chan<- *types.FinalisationInfo) byte); ok { + r0 = rf(ch) + } else { + r0 = ret.Get(0).(byte) + } + + var r1 error + if rf, ok := ret.Get(1).(func(chan<- *types.FinalisationInfo) error); ok { + r1 = rf(ch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RegisterImportedChannel provides a mock function with given fields: ch +func (_m *MockBlockState) RegisterImportedChannel(ch chan<- *types.Block) (byte, error) { + ret := _m.Called(ch) + + var r0 byte + if rf, ok := ret.Get(0).(func(chan<- *types.Block) byte); ok { + r0 = rf(ch) + } else { + r0 = ret.Get(0).(byte) + } + + var r1 error + if rf, ok := ret.Get(1).(func(chan<- *types.Block) error); ok { + r1 = rf(ch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetFinalisedHash provides a mock function with given fields: _a0, _a1, _a2 +func (_m *MockBlockState) SetFinalisedHash(_a0 common.Hash, _a1 uint64, _a2 uint64) error { + ret := _m.Called(_a0, _a1, _a2) + + var r0 error + if rf, ok := ret.Get(0).(func(common.Hash, uint64, uint64) error); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// StoreRuntime provides a mock function with given fields: _a0, _a1 +func (_m *MockBlockState) StoreRuntime(_a0 common.Hash, _a1 runtime.Instance) { + _m.Called(_a0, _a1) +} + +// SubChain provides a mock function with given fields: start, end +func (_m *MockBlockState) SubChain(start common.Hash, end common.Hash) ([]common.Hash, error) { + ret := _m.Called(start, end) + + var r0 []common.Hash + if rf, ok := ret.Get(0).(func(common.Hash, common.Hash) []common.Hash); ok { + r0 = rf(start, end) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]common.Hash) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Hash, common.Hash) error); ok { + r1 = rf(start, end) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnregisterFinalisedChannel provides a mock function with given fields: id +func (_m *MockBlockState) UnregisterFinalisedChannel(id byte) { + _m.Called(id) +} + +// UnregisterImportedChannel provides a mock function with given fields: id +func (_m *MockBlockState) UnregisterImportedChannel(id byte) { + _m.Called(id) +} diff --git a/dot/core/mocks/digest_handler.go b/dot/core/mocks/digest_handler.go index 77c8e1a76c..95dd03c168 100644 --- a/dot/core/mocks/digest_handler.go +++ b/dot/core/mocks/digest_handler.go @@ -1,6 +1,6 @@ // Code generated by mockery v2.8.0. DO NOT EDIT. -package core +package mocks import ( types "github.com/ChainSafe/gossamer/dot/types" diff --git a/dot/core/mocks/network.go b/dot/core/mocks/network.go index 573cb7ac78..314c19b80a 100644 --- a/dot/core/mocks/network.go +++ b/dot/core/mocks/network.go @@ -1,6 +1,6 @@ // Code generated by mockery v2.8.0. DO NOT EDIT. -package core +package mocks import ( network "github.com/ChainSafe/gossamer/dot/network" diff --git a/dot/core/service.go b/dot/core/service.go index f4db0e7fdb..1bac4a4f26 100644 --- a/dot/core/service.go +++ b/dot/core/service.go @@ -40,6 +40,9 @@ var ( logger log.Logger = log.New("pkg", "core") ) +// QueryKeyValueChanges represents the key-value data inside a block storage +type QueryKeyValueChanges map[string]string + // Service is an overhead layer that allows communication between the runtime, // BABE session, and network service. It deals with the validation of transactions // and blocks by calling their respective validation functions in the runtime. @@ -461,6 +464,16 @@ func (s *Service) HasKey(pubKeyStr, keyType string) (bool, error) { return keystore.HasKey(pubKeyStr, keyType, s.keys.Acco) } +// DecodeSessionKeys executes the runtime DecodeSessionKeys and return the scale encoded keys +func (s *Service) DecodeSessionKeys(enc []byte) ([]byte, error) { + rt, err := s.blockState.GetRuntime(nil) + if err != nil { + return nil, err + } + + return rt.DecodeSessionKeys(enc) +} + // GetRuntimeVersion gets the current RuntimeVersion func (s *Service) GetRuntimeVersion(bhash *common.Hash) (runtime.Version, error) { var stateRootHash *common.Hash @@ -550,3 +563,58 @@ func (s *Service) GetMetadata(bhash *common.Hash) ([]byte, error) { rt.SetContextStorage(ts) return rt.Metadata() } + +// 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, to common.Hash, keys ...string) (map[common.Hash]QueryKeyValueChanges, error) { + if to == common.EmptyHash { + to = s.blockState.BestBlockHash() + } + + blocksToQuery, err := s.blockState.SubChain(from, to) + if err != nil { + return nil, err + } + + queries := make(map[common.Hash]QueryKeyValueChanges) + + 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) (QueryKeyValueChanges, error) { + stateRootHash, err := s.storageState.GetStateRootFromBlock(&block) + if err != nil { + return nil, err + } + + changes := make(QueryKeyValueChanges) + 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 +} diff --git a/dot/core/service_test.go b/dot/core/service_test.go index d417b6557f..e5ef9188d8 100644 --- a/dot/core/service_test.go +++ b/dot/core/service_test.go @@ -17,6 +17,7 @@ package core import ( + "errors" "io/ioutil" "math/big" "os" @@ -24,6 +25,7 @@ import ( "testing" "time" + "github.com/ChainSafe/gossamer/dot/core/mocks" coremocks "github.com/ChainSafe/gossamer/dot/core/mocks" "github.com/ChainSafe/gossamer/dot/network" "github.com/ChainSafe/gossamer/dot/state" @@ -33,10 +35,14 @@ import ( "github.com/ChainSafe/gossamer/lib/keystore" "github.com/ChainSafe/gossamer/lib/runtime" "github.com/ChainSafe/gossamer/lib/runtime/extrinsic" + runtimemocks "github.com/ChainSafe/gossamer/lib/runtime/mocks" + "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/mock" "github.com/stretchr/testify/require" ) @@ -613,3 +619,203 @@ 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, common.Hash{}, keys...) + require.NoError(t, err) + require.Len(t, data, 3) + + require.Equal(t, data[firstBlock.Header.Hash()], QueryKeyValueChanges( + 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()], QueryKeyValueChanges( + map[string]string{ + common.BytesToHex(secondKey): common.BytesToHex(secondValue), + }, + )) + require.Equal(t, data[thirdBlock.Header.Hash()], QueryKeyValueChanges( + 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 +} + +func TestDecodeSessionKeys(t *testing.T) { + mockInstance := new(runtimemocks.MockInstance) + mockInstance.On("DecodeSessionKeys", mock.AnythingOfType("[]uint8")).Return([]byte{}, nil).Once() + + mockBlockState := new(mocks.MockBlockState) + mockBlockState.On("GetRuntime", mock.AnythingOfType("*common.Hash")).Return(mockInstance, nil).Once() + + coreservice := new(Service) + coreservice.blockState = mockBlockState + + b, err := coreservice.DecodeSessionKeys([]byte{}) + + mockBlockState.AssertCalled(t, "GetRuntime", mock.AnythingOfType("*common.Hash")) + mockInstance.AssertCalled(t, "DecodeSessionKeys", []uint8{}) + + require.NoError(t, err) + require.Equal(t, b, []byte{}) +} + +func TestDecodeSessionKeys_WhenGetRuntimeReturnError(t *testing.T) { + mockBlockState := new(mocks.MockBlockState) + mockBlockState.On("GetRuntime", mock.AnythingOfType("*common.Hash")).Return(nil, errors.New("problems")).Once() + + coreservice := new(Service) + coreservice.blockState = mockBlockState + + b, err := coreservice.DecodeSessionKeys([]byte{}) + + mockBlockState.AssertCalled(t, "GetRuntime", mock.AnythingOfType("*common.Hash")) + require.Error(t, err, "problems") + require.Nil(t, b) +} diff --git a/dot/network/host.go b/dot/network/host.go index fe0f73ce1c..2082095a2c 100644 --- a/dot/network/host.go +++ b/dot/network/host.go @@ -368,6 +368,42 @@ func (h *host) peers() []peer.ID { return h.h.Network().Peers() } +// addReservedPeers adds the peers `addrs` to the protected peers list and connects to them +func (h *host) addReservedPeers(addrs ...string) error { + for _, addr := range addrs { + maddr, err := ma.NewMultiaddr(addr) + if err != nil { + return err + } + + addinfo, err := peer.AddrInfoFromP2pAddr(maddr) + if err != nil { + return err + } + + h.h.ConnManager().Protect(addinfo.ID, "") + if err := h.connect(*addinfo); err != nil { + return err + } + } + + return nil +} + +// removeReservedPeers will remove the given peers from the protected peers list +func (h *host) removeReservedPeers(ids ...string) error { + for _, id := range ids { + peerID, err := peer.Decode(id) + if err != nil { + return err + } + + h.h.ConnManager().Unprotect(peerID, "") + } + + return nil +} + // supportsProtocol checks if the protocol is supported by peerID // returns an error if could not get peer protocols func (h *host) supportsProtocol(peerID peer.ID, protocol protocol.ID) (bool, error) { diff --git a/dot/network/host_test.go b/dot/network/host_test.go index 4cb3895008..41c50ca53a 100644 --- a/dot/network/host_test.go +++ b/dot/network/host_test.go @@ -406,3 +406,73 @@ func Test_PeerSupportsProtocol(t *testing.T) { require.Equal(t, test.expect, output) } } + +func Test_AddReservedPeers(t *testing.T) { + basePathA := utils.NewTestBasePath(t, "nodeA") + configA := &Config{ + BasePath: basePathA, + Port: 7001, + NoBootstrap: true, + NoMDNS: true, + } + + nodeA := createTestService(t, configA) + nodeA.noGossip = true + + basePathB := utils.NewTestBasePath(t, "nodeB") + configB := &Config{ + BasePath: basePathB, + Port: 7002, + NoBootstrap: true, + NoMDNS: true, + } + + nodeB := createTestService(t, configB) + nodeB.noGossip = true + + nodeBPeerAddr := nodeB.host.multiaddrs()[0].String() + err := nodeA.host.addReservedPeers(nodeBPeerAddr) + require.NoError(t, err) + + isProtected := nodeA.host.h.ConnManager().IsProtected(nodeB.host.addrInfo().ID, "") + require.True(t, isProtected) +} + +func Test_RemoveReservedPeers(t *testing.T) { + basePathA := utils.NewTestBasePath(t, "nodeA") + configA := &Config{ + BasePath: basePathA, + Port: 7001, + NoBootstrap: true, + NoMDNS: true, + } + + nodeA := createTestService(t, configA) + nodeA.noGossip = true + + basePathB := utils.NewTestBasePath(t, "nodeB") + configB := &Config{ + BasePath: basePathB, + Port: 7002, + NoBootstrap: true, + NoMDNS: true, + } + + nodeB := createTestService(t, configB) + nodeB.noGossip = true + + nodeBPeerAddr := nodeB.host.multiaddrs()[0].String() + err := nodeA.host.addReservedPeers(nodeBPeerAddr) + require.NoError(t, err) + + pID := nodeB.host.addrInfo().ID.String() + + err = nodeA.host.removeReservedPeers(pID) + require.NoError(t, err) + + isProtected := nodeA.host.h.ConnManager().IsProtected(nodeB.host.addrInfo().ID, "") + require.False(t, isProtected) + + err = nodeA.host.removeReservedPeers("failing peer ID") + require.Error(t, err) +} diff --git a/dot/network/service.go b/dot/network/service.go index c85a682bff..e6db0e0bd5 100644 --- a/dot/network/service.go +++ b/dot/network/service.go @@ -304,9 +304,16 @@ func (s *Service) collectNetworkMetrics() { } func (s *Service) logPeerCount() { + ticker := time.NewTicker(time.Second * 30) + defer ticker.Stop() + for { - logger.Debug("peer count", "num", s.host.peerCount(), "min", s.cfg.MinPeers, "max", s.cfg.MaxPeers) - time.Sleep(time.Second * 30) + select { + case <-ticker.C: + logger.Debug("peer count", "num", s.host.peerCount(), "min", s.cfg.MinPeers, "max", s.cfg.MaxPeers) + case <-s.ctx.Done(): + return + } } } @@ -698,6 +705,16 @@ func (s *Service) Peers() []common.PeerInfo { return peers } +// AddReservedPeers insert new peers to the peerstore with PermanentAddrTTL +func (s *Service) AddReservedPeers(addrs ...string) error { + return s.host.addReservedPeers(addrs...) +} + +// RemoveReservedPeers closes all connections with the target peers and remove it from the peerstore +func (s *Service) RemoveReservedPeers(addrs ...string) error { + return s.host.removeReservedPeers(addrs...) +} + // NodeRoles Returns the roles the node is running as. func (s *Service) NodeRoles() byte { return s.cfg.Roles diff --git a/dot/rpc/modules/api.go b/dot/rpc/modules/api.go index cedd8e8773..1184b13251 100644 --- a/dot/rpc/modules/api.go +++ b/dot/rpc/modules/api.go @@ -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" @@ -54,6 +55,8 @@ type NetworkAPI interface { IsStopped() bool HighestBlock() int64 StartingBlock() int64 + AddReservedPeers(addrs ...string) error + RemoveReservedPeers(addrs ...string) error } // BlockProducerAPI is the interface for BlockProducer methods @@ -79,6 +82,8 @@ type CoreAPI interface { GetRuntimeVersion(bhash *common.Hash) (runtime.Version, error) HandleSubmittedExtrinsic(types.Extrinsic) error GetMetadata(bhash *common.Hash) ([]byte, error) + QueryStorage(from, to common.Hash, keys ...string) (map[common.Hash]core.QueryKeyValueChanges, error) + DecodeSessionKeys(enc []byte) ([]byte, error) } // RPCAPI is the interface for methods related to RPC service diff --git a/dot/rpc/modules/author.go b/dot/rpc/modules/author.go index c0dd36079d..51394a2bab 100644 --- a/dot/rpc/modules/author.go +++ b/dot/rpc/modules/author.go @@ -17,13 +17,14 @@ package modules import ( - "fmt" + "errors" "net/http" - "reflect" + "strings" "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/lib/common" "github.com/ChainSafe/gossamer/lib/keystore" + "github.com/ChainSafe/gossamer/pkg/scale" log "github.com/ChainSafe/log15" ) @@ -35,8 +36,17 @@ type AuthorModule struct { txStateAPI TransactionStateAPI } +// HasSessionKeyRequest is used to receive the rpc data +type HasSessionKeyRequest struct { + PublicKeys string +} + // KeyInsertRequest is used as model for the JSON -type KeyInsertRequest []string +type KeyInsertRequest struct { + Type string + Seed string + PublicKey string +} // Extrinsic represents a hex-encoded extrinsic type Extrinsic struct { @@ -64,6 +74,18 @@ type RemoveExtrinsicsResponse []common.Hash // KeyRotateResponse is a byte array used to rotate type KeyRotateResponse []byte +// HasSessionKeyResponse is the response to the RPC call author_hasSessionKeys +type HasSessionKeyResponse bool + +// KeyTypeID represents the key type of a session key +type keyTypeID [4]uint8 + +// DecodedKey is the representation of a scaled decoded public key +type decodedKey struct { + Data []uint8 + Type keyTypeID +} + // ExtrinsicStatus holds the actual valid statuses type ExtrinsicStatus struct { IsFuture bool @@ -94,27 +116,66 @@ func NewAuthorModule(logger log.Logger, coreAPI CoreAPI, txStateAPI TransactionS } } -// InsertKey inserts a key into the keystore -func (am *AuthorModule) InsertKey(r *http.Request, req *KeyInsertRequest, res *KeyInsertResponse) error { - keyReq := *req +// HasSessionKeys checks if the keystore has private keys for the given session public keys. +func (am *AuthorModule) HasSessionKeys(r *http.Request, req *HasSessionKeyRequest, res *HasSessionKeyResponse) error { + pubKeysBytes, err := common.HexToBytes(req.PublicKeys) + if err != nil { + return err + } + + pkeys, err := scale.Marshal(pubKeysBytes) + if err != nil { + return err + } - pkDec, err := common.HexToBytes(keyReq[1]) + data, err := am.coreAPI.DecodeSessionKeys(pkeys) if err != nil { + *res = false return err } - privateKey, err := keystore.DecodePrivateKey(pkDec, keystore.DetermineKeyType(keyReq[0])) + var decodedKeys *[]decodedKey + err = scale.Unmarshal(data, &decodedKeys) + if err != nil { + return err + } + + if decodedKeys == nil || len(*decodedKeys) < 1 { + *res = false + return nil + } + + for _, key := range *decodedKeys { + encType := keystore.Name(key.Type[:]) + ok, err := am.coreAPI.HasKey(common.BytesToHex(key.Data), string(encType)) + + if err != nil || !ok { + *res = false + return err + } + } + + *res = true + return nil +} + +// InsertKey inserts a key into the keystore +func (am *AuthorModule) InsertKey(r *http.Request, req *KeyInsertRequest, res *KeyInsertResponse) error { + keyReq := *req + + keyBytes, err := common.HexToBytes(req.Seed) if err != nil { return err } - keyPair, err := keystore.PrivateKeyToKeypair(privateKey) + keyPair, err := keystore.DecodeKeyPairFromHex(keyBytes, keystore.DetermineKeyType(keyReq.Type)) if err != nil { return err } - if !reflect.DeepEqual(keyPair.Public().Hex(), keyReq[2]) { - return fmt.Errorf("generated public key does not equal provide public key") + //strings.EqualFold compare using case-insensitivity. + if !strings.EqualFold(keyPair.Public().Hex(), keyReq.PublicKey) { + return errors.New("generated public key does not equal provide public key") } am.coreAPI.InsertKey(keyPair) diff --git a/dot/rpc/modules/author_test.go b/dot/rpc/modules/author_test.go index 4de36c2b33..2d84d779ec 100644 --- a/dot/rpc/modules/author_test.go +++ b/dot/rpc/modules/author_test.go @@ -1,6 +1,7 @@ package modules import ( + "errors" "fmt" "net/http" "testing" @@ -8,13 +9,160 @@ import ( apimocks "github.com/ChainSafe/gossamer/dot/rpc/modules/mocks" "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/crypto/sr25519" "github.com/ChainSafe/gossamer/lib/keystore" + "github.com/ChainSafe/gossamer/lib/runtime" + "github.com/ChainSafe/gossamer/lib/runtime/wasmer" "github.com/ChainSafe/gossamer/lib/transaction" log "github.com/ChainSafe/log15" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) +func TestAuthorModule_HasSessionKey_WhenScaleDataEmptyOrNil(t *testing.T) { + keys := "0x00" + runtimeInstance := wasmer.NewTestInstance(t, runtime.NODE_RUNTIME) + + coremockapi := new(apimocks.MockCoreAPI) + + decodeSessionKeysMock := coremockapi.On("DecodeSessionKeys", mock.AnythingOfType("[]uint8")) + decodeSessionKeysMock.Run(func(args mock.Arguments) { + b := args.Get(0).([]byte) + dec, err := runtimeInstance.DecodeSessionKeys(b) + decodeSessionKeysMock.ReturnArguments = []interface{}{dec, err} + }) + + module := &AuthorModule{ + coreAPI: coremockapi, + logger: log.New("service", "RPC", "module", "author"), + } + + req := &HasSessionKeyRequest{ + PublicKeys: keys, + } + + var res HasSessionKeyResponse + err := module.HasSessionKeys(nil, req, &res) + require.NoError(t, err) + require.False(t, bool(res)) + + coremockapi.AssertCalled(t, "DecodeSessionKeys", mock.AnythingOfType("[]uint8")) +} + +func TestAuthorModule_HasSessionKey_WhenRuntimeFails(t *testing.T) { + coremockapi := new(apimocks.MockCoreAPI) + coremockapi.On("DecodeSessionKeys", mock.AnythingOfType("[]uint8")).Return(nil, errors.New("problems with runtime")) + + module := &AuthorModule{ + coreAPI: coremockapi, + logger: log.New("service", "RPC", "module", "author"), + } + + req := &HasSessionKeyRequest{ + PublicKeys: "0x00", + } + + var res HasSessionKeyResponse + err := module.HasSessionKeys(nil, req, &res) + require.Error(t, err, "problems with runtime") + require.False(t, bool(res)) +} + +func TestAuthorModule_HasSessionKey_WhenThereIsNoKeys(t *testing.T) { + keys := "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d34309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc3852042602634309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc3852042602634309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc38520426026" + runtimeInstance := wasmer.NewTestInstance(t, runtime.NODE_RUNTIME) + + coremockapi := new(apimocks.MockCoreAPI) + coremockapi.On("HasKey", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(false, nil) + + decodeSessionKeysMock := coremockapi.On("DecodeSessionKeys", mock.AnythingOfType("[]uint8")) + decodeSessionKeysMock.Run(func(args mock.Arguments) { + b := args.Get(0).([]byte) + dec, err := runtimeInstance.DecodeSessionKeys(b) + decodeSessionKeysMock.ReturnArguments = []interface{}{dec, err} + }) + + module := &AuthorModule{ + coreAPI: coremockapi, + logger: log.New("service", "RPC", "module", "author"), + } + + req := &HasSessionKeyRequest{ + PublicKeys: keys, + } + + var res HasSessionKeyResponse + err := module.HasSessionKeys(nil, req, &res) + require.NoError(t, err) + require.False(t, bool(res)) + + coremockapi.AssertCalled(t, "DecodeSessionKeys", mock.AnythingOfType("[]uint8")) + coremockapi.AssertCalled(t, "HasKey", mock.AnythingOfType("string"), mock.AnythingOfType("string")) + coremockapi.AssertNumberOfCalls(t, "HasKey", 1) +} + +func TestAuthorModule_HasSessionKey(t *testing.T) { + globalStore := keystore.NewGlobalKeystore() + + coremockapi := new(apimocks.MockCoreAPI) + mockInsertKey := coremockapi.On("InsertKey", mock.AnythingOfType("*sr25519.Keypair")) + mockInsertKey.Run(func(args mock.Arguments) { + kp := args.Get(0).(*sr25519.Keypair) + globalStore.Acco.Insert(kp) + }) + + mockHasKey := coremockapi.On("HasKey", mock.AnythingOfType("string"), mock.AnythingOfType("string")) + mockHasKey.Run(func(args mock.Arguments) { + pubKeyHex := args.Get(0).(string) + keyType := args.Get(1).(string) + + ok, err := keystore.HasKey(pubKeyHex, keyType, globalStore.Acco) + mockHasKey.ReturnArguments = []interface{}{ok, err} + }) + + keys := "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d34309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc3852042602634309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc3852042602634309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc38520426026" + runtimeInstance := wasmer.NewTestInstance(t, runtime.NODE_RUNTIME) + + decodeSessionKeysMock := coremockapi.On("DecodeSessionKeys", mock.AnythingOfType("[]uint8")) + decodeSessionKeysMock.Run(func(args mock.Arguments) { + b := args.Get(0).([]byte) + dec, err := runtimeInstance.DecodeSessionKeys(b) + decodeSessionKeysMock.ReturnArguments = []interface{}{dec, err} + }) + + module := &AuthorModule{ + coreAPI: coremockapi, + logger: log.New("service", "RPC", "module", "author"), + } + + req := &HasSessionKeyRequest{ + PublicKeys: keys, + } + + err := module.InsertKey(nil, &KeyInsertRequest{ + Type: "babe", + Seed: "0xfec0f475b818470af5caf1f3c1b1558729961161946d581d2755f9fb566534f8", + PublicKey: "0x34309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc38520426026", + }, nil) + coremockapi.AssertCalled(t, "InsertKey", mock.AnythingOfType("*sr25519.Keypair")) + require.NoError(t, err) + require.Equal(t, 1, globalStore.Acco.Size()) + + err = module.InsertKey(nil, &KeyInsertRequest{ + Type: "babe", + Seed: "0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a", + PublicKey: "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + }, nil) + require.NoError(t, err) + require.Equal(t, 2, globalStore.Acco.Size()) + + var res HasSessionKeyResponse + err = module.HasSessionKeys(nil, req, &res) + require.NoError(t, err) + require.True(t, bool(res)) +} + func TestAuthorModule_SubmitExtrinsic(t *testing.T) { errMockCoreAPI := &apimocks.MockCoreAPI{} errMockCoreAPI.On("HandleSubmittedExtrinsic", mock.AnythingOfType("types.Extrinsic")).Return(fmt.Errorf("some error")) @@ -202,8 +350,8 @@ func TestAuthorModule_InsertKey(t *testing.T) { args: args{ req: &KeyInsertRequest{ "babe", - "0xb7e9185065667390d2ad952a5324e8c365c9bf503dcf97c67a5ce861afe97309", "0x6246ddf254e0b4b4e7dffefc8adf69d212b98ac2b579c362b473fec8c40b4c0a", + "0xdad5131003242c37c227f744f82118dd59a24b949ae264a93d949100738c196c", }, }, }, @@ -214,9 +362,10 @@ func TestAuthorModule_InsertKey(t *testing.T) { coreAPI: mockCoreAPI, }, args: args{ - req: &KeyInsertRequest{"gran", - "0xb7e9185065667390d2ad952a5324e8c365c9bf503dcf97c67a5ce861afe97309b7e9185065667390d2ad952a5324e8c365c9bf503dcf97c67a5ce861afe97309", - "0xb7e9185065667390d2ad952a5324e8c365c9bf503dcf97c67a5ce861afe97309", + req: &KeyInsertRequest{ + "gran", + "0xb48004c6e1625282313b07d1c9950935e86894a2e4f21fb1ffee9854d180c781", + "0xa7d6507d59f8871b8f1a0f2c32e219adfacff4c9fcb05b0b2d8ebd6a65c88ee6", }, }, }, diff --git a/dot/rpc/modules/mocks/core_api.go b/dot/rpc/modules/mocks/core_api.go index 47c1f6c5c5..da382dbda6 100644 --- a/dot/rpc/modules/mocks/core_api.go +++ b/dot/rpc/modules/mocks/core_api.go @@ -3,7 +3,9 @@ package mocks import ( + core "github.com/ChainSafe/gossamer/dot/core" common "github.com/ChainSafe/gossamer/lib/common" + crypto "github.com/ChainSafe/gossamer/lib/crypto" mock "github.com/stretchr/testify/mock" @@ -18,6 +20,29 @@ type MockCoreAPI struct { mock.Mock } +// DecodeSessionKeys provides a mock function with given fields: enc +func (_m *MockCoreAPI) DecodeSessionKeys(enc []byte) ([]byte, error) { + ret := _m.Called(enc) + + var r0 []byte + if rf, ok := ret.Get(0).(func([]byte) []byte); ok { + r0 = rf(enc) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(enc) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetMetadata provides a mock function with given fields: bhash func (_m *MockCoreAPI) GetMetadata(bhash *common.Hash) ([]byte, error) { ret := _m.Called(bhash) @@ -103,3 +128,33 @@ func (_m *MockCoreAPI) HasKey(pubKeyStr string, keyType string) (bool, error) { func (_m *MockCoreAPI) InsertKey(kp crypto.Keypair) { _m.Called(kp) } + +// QueryStorage provides a mock function with given fields: from, to, keys +func (_m *MockCoreAPI) QueryStorage(from common.Hash, to common.Hash, keys ...string) (map[common.Hash]core.QueryKeyValueChanges, error) { + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, from, to) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 map[common.Hash]core.QueryKeyValueChanges + if rf, ok := ret.Get(0).(func(common.Hash, common.Hash, ...string) map[common.Hash]core.QueryKeyValueChanges); ok { + r0 = rf(from, to, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[common.Hash]core.QueryKeyValueChanges) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Hash, common.Hash, ...string) error); ok { + r1 = rf(from, to, keys...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/dot/rpc/modules/mocks/network_api.go b/dot/rpc/modules/mocks/network_api.go index 7802cdf6dd..7d8360dc5f 100644 --- a/dot/rpc/modules/mocks/network_api.go +++ b/dot/rpc/modules/mocks/network_api.go @@ -12,6 +12,26 @@ type MockNetworkAPI struct { mock.Mock } +// AddReservedPeers provides a mock function with given fields: addrs +func (_m *MockNetworkAPI) AddReservedPeers(addrs ...string) error { + _va := make([]interface{}, len(addrs)) + for _i := range addrs { + _va[_i] = addrs[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(...string) error); ok { + r0 = rf(addrs...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Health provides a mock function with given fields: func (_m *MockNetworkAPI) Health() common.Health { ret := _m.Called() @@ -98,6 +118,26 @@ func (_m *MockNetworkAPI) Peers() []common.PeerInfo { return r0 } +// RemoveReservedPeers provides a mock function with given fields: addrs +func (_m *MockNetworkAPI) RemoveReservedPeers(addrs ...string) error { + _va := make([]interface{}, len(addrs)) + for _i := range addrs { + _va[_i] = addrs[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(...string) error); ok { + r0 = rf(addrs...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Start provides a mock function with given fields: func (_m *MockNetworkAPI) Start() error { ret := _m.Called() diff --git a/dot/rpc/modules/state.go b/dot/rpc/modules/state.go index ee22373ca5..f301b4e8ac 100644 --- a/dot/rpc/modules/state.go +++ b/dot/rpc/modules/state.go @@ -18,6 +18,7 @@ package modules import ( "encoding/hex" + "errors" "fmt" "net/http" "strings" @@ -85,9 +86,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"` + EndBlock common.Hash `json:"block"` } // StateStorageKeysQuery field to store storage keys @@ -129,8 +130,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 @@ -387,8 +388,32 @@ 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 { + if req.StartBlock == common.EmptyHash { + return errors.New("the start block hash cannot be an empty value") + } + + changesByBlock, err := sm.coreAPI.QueryStorage(req.StartBlock, req.EndBlock, req.Keys...) + if err != nil { + return err + } + + response := make([]StorageChangeSetResponse, 0, len(changesByBlock)) + + 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 } diff --git a/dot/rpc/modules/state_test.go b/dot/rpc/modules/state_test.go index ca8661e250..19a844d80e 100644 --- a/dot/rpc/modules/state_test.go +++ b/dot/rpc/modules/state_test.go @@ -17,6 +17,7 @@ package modules import ( "encoding/hex" + "errors" "fmt" "io/ioutil" "math/big" @@ -24,8 +25,11 @@ import ( "strings" "testing" + "github.com/ChainSafe/gossamer/dot/core" + "github.com/ChainSafe/gossamer/dot/rpc/modules/mocks" "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/lib/common" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -315,6 +319,60 @@ func TestStateModule_GetStorageSize(t *testing.T) { } } +func TestStateModule_QueryStorage(t *testing.T) { + t.Run("When starting block is empty", func(t *testing.T) { + module := new(StateModule) + req := new(StateStorageQueryRangeRequest) + + var res []StorageChangeSetResponse + err := module.QueryStorage(nil, req, &res) + require.Error(t, err, "the start block hash cannot be an empty value") + }) + + t.Run("When coreAPI QueryStorage returns error", func(t *testing.T) { + coreapimock := new(mocks.MockCoreAPI) + coreapimock.On("QueryStorage", mock.AnythingOfType("common.Hash"), mock.AnythingOfType("common.Hash")).Return(nil, errors.New("problem while querying")) + + module := new(StateModule) + module.coreAPI = coreapimock + + req := new(StateStorageQueryRangeRequest) + req.StartBlock = common.NewHash([]byte{1, 2}) + + var res []StorageChangeSetResponse + err := module.QueryStorage(nil, req, &res) + require.Error(t, err) + coreapimock.AssertCalled(t, "QueryStorage", mock.AnythingOfType("common.Hash"), mock.AnythingOfType("common.Hash")) + }) + + t.Run("When QueryStorage returns data", func(t *testing.T) { + blockhash := common.NewHash([]byte{123}) + + changes := map[common.Hash]core.QueryKeyValueChanges{ + blockhash: core.QueryKeyValueChanges(map[string]string{ + "0x80": "value", + "0x90": "another value", + }), + } + coreapimock := new(mocks.MockCoreAPI) + coreapimock.On("QueryStorage", mock.AnythingOfType("common.Hash"), mock.AnythingOfType("common.Hash"), "0x90", "0x80").Return(changes, nil) + + module := new(StateModule) + module.coreAPI = coreapimock + + req := new(StateStorageQueryRangeRequest) + req.StartBlock = common.NewHash([]byte{1, 2}) + req.Keys = []string{"0x90", "0x80"} + + var res []StorageChangeSetResponse + err := module.QueryStorage(nil, req, &res) + require.NoError(t, err) + require.Len(t, res, 1) + + coreapimock.AssertCalled(t, "QueryStorage", mock.AnythingOfType("common.Hash"), mock.AnythingOfType("common.Hash"), "0x90", "0x80") + }) +} + func TestStateModule_GetMetadata(t *testing.T) { t.Skip() // TODO: update expected_metadata sm, hash, _ := setupStateModule(t) diff --git a/dot/rpc/modules/system.go b/dot/rpc/modules/system.go index 80a6a46006..de70e93af9 100644 --- a/dot/rpc/modules/system.go +++ b/dot/rpc/modules/system.go @@ -21,6 +21,7 @@ import ( "errors" "math/big" "net/http" + "strings" "github.com/ChainSafe/gossamer/lib/common" "github.com/ChainSafe/gossamer/lib/crypto" @@ -280,3 +281,21 @@ func (sm *SystemModule) LocalPeerId(r *http.Request, req *EmptyRequest, res *str *res = base58.Encode([]byte(netstate.PeerID)) return nil } + +// AddReservedPeer adds a reserved peer. The string parameter should encode a p2p multiaddr. +func (sm *SystemModule) AddReservedPeer(r *http.Request, req *StringRequest, res *[]byte) error { + if strings.TrimSpace(req.String) == "" { + return errors.New("cannot add an empty reserved peer") + } + + return sm.networkAPI.AddReservedPeers(req.String) +} + +// RemoveReservedPeer remove a reserved peer. The string should encode only the PeerId +func (sm *SystemModule) RemoveReservedPeer(r *http.Request, req *StringRequest, res *[]byte) error { + if strings.TrimSpace(req.String) == "" { + return errors.New("cannot remove an empty reserved peer") + } + + return sm.networkAPI.RemoveReservedPeers(req.String) +} diff --git a/dot/rpc/modules/system_test.go b/dot/rpc/modules/system_test.go index 7519925fd7..afd04255f3 100644 --- a/dot/rpc/modules/system_test.go +++ b/dot/rpc/modules/system_test.go @@ -402,7 +402,7 @@ func TestLocalListenAddresses(t *testing.T) { } mockNetAPI := new(mocks.MockNetworkAPI) - mockNetAPI.On("NetworkState").Return(mockedNetState) + mockNetAPI.On("NetworkState").Return(mockedNetState).Once() res := make([]string, 0) @@ -414,6 +414,10 @@ func TestLocalListenAddresses(t *testing.T) { require.Len(t, res, 1) require.Equal(t, res[0], ma.String()) + + mockNetAPI.On("NetworkState").Return(common.NetworkState{Multiaddrs: []multiaddr.Multiaddr{}}).Once() + err = sysmodule.LocalListenAddresses(nil, nil, &res) + require.Error(t, err, "multiaddress list is empty") } func TestLocalPeerId(t *testing.T) { @@ -441,3 +445,57 @@ func TestLocalPeerId(t *testing.T) { err = sysmodules.LocalPeerId(nil, nil, &res) require.Error(t, err) } + +func TestAddReservedPeer(t *testing.T) { + t.Run("Test Add and Remove reserved peers with success", func(t *testing.T) { + networkMock := new(mocks.MockNetworkAPI) + networkMock.On("AddReservedPeers", mock.AnythingOfType("string")).Return(nil).Once() + networkMock.On("RemoveReservedPeers", mock.AnythingOfType("string")).Return(nil).Once() + + multiAddrPeer := "/ip4/198.51.100.19/tcp/30333/p2p/QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV" + sysModule := &SystemModule{ + networkAPI: networkMock, + } + + var b *[]byte + err := sysModule.AddReservedPeer(nil, &StringRequest{String: multiAddrPeer}, b) + require.NoError(t, err) + require.Nil(t, b) + + peerID := "QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV" + err = sysModule.RemoveReservedPeer(nil, &StringRequest{String: peerID}, b) + require.NoError(t, err) + require.Nil(t, b) + }) + + t.Run("Test Add and Remove reserved peers without success", func(t *testing.T) { + networkMock := new(mocks.MockNetworkAPI) + networkMock.On("AddReservedPeers", mock.AnythingOfType("string")).Return(errors.New("some problems")).Once() + networkMock.On("RemoveReservedPeers", mock.AnythingOfType("string")).Return(errors.New("other problems")).Once() + + sysModule := &SystemModule{ + networkAPI: networkMock, + } + + var b *[]byte + err := sysModule.AddReservedPeer(nil, &StringRequest{String: ""}, b) + require.Error(t, err, "cannot add an empty reserved peer") + require.Nil(t, b) + + multiAddrPeer := "/ip4/198.51.100.19/tcp/30333/p2p/QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV" + err = sysModule.AddReservedPeer(nil, &StringRequest{String: multiAddrPeer}, b) + require.Error(t, err, "some problems") + require.Nil(t, b) + + peerID := "QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV" + err = sysModule.RemoveReservedPeer(nil, &StringRequest{String: peerID}, b) + require.Error(t, err, "other problems") + require.Nil(t, b) + }) + + t.Run("Test trying to add or remove peers with empty or white space request", func(t *testing.T) { + sysModule := &SystemModule{} + require.Error(t, sysModule.AddReservedPeer(nil, &StringRequest{String: ""}, nil)) + require.Error(t, sysModule.RemoveReservedPeer(nil, &StringRequest{String: " "}, nil)) + }) +} diff --git a/dot/rpc/service_test.go b/dot/rpc/service_test.go index 64e95f4ee8..4f943a7443 100644 --- a/dot/rpc/service_test.go +++ b/dot/rpc/service_test.go @@ -33,9 +33,9 @@ func TestNewService(t *testing.T) { } func TestService_Methods(t *testing.T) { - qtySystemMethods := 13 + qtySystemMethods := 15 qtyRPCMethods := 1 - qtyAuthorMethods := 7 + qtyAuthorMethods := 8 rpcService := NewService() sysMod := modules.NewSystemModule(nil, nil, nil, nil, nil, nil) diff --git a/dot/rpc/subscription/websocket_test.go b/dot/rpc/subscription/websocket_test.go index 9f5d9d74af..f3475376c2 100644 --- a/dot/rpc/subscription/websocket_test.go +++ b/dot/rpc/subscription/websocket_test.go @@ -2,10 +2,7 @@ package subscription import ( "fmt" - "log" "math/big" - "net/http" - "os" "testing" "time" @@ -20,47 +17,15 @@ import ( "github.com/stretchr/testify/require" ) -var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, -} - -var wsconn = &WSConn{ - Subscriptions: make(map[uint32]Listener), -} - -func handler(w http.ResponseWriter, r *http.Request) { - c, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Print("upgrade:", err) - return - } - defer c.Close() - - wsconn.Wsconn = c - wsconn.HandleComm() -} - -func TestMain(m *testing.M) { - http.HandleFunc("/", handler) - - go func() { - err := http.ListenAndServe("localhost:8546", nil) - if err != nil { - log.Fatal("error", err) - } - }() - time.Sleep(time.Millisecond * 100) +func TestWSConn_HandleComm(t *testing.T) { + wsconn, c, cancel := setupWSConn(t) + wsconn.Subscriptions = make(map[uint32]Listener) + defer cancel() - // Start all tests - os.Exit(m.Run()) -} + go wsconn.HandleComm() + time.Sleep(time.Second * 2) -func TestWSConn_HandleComm(t *testing.T) { - c, _, err := websocket.DefaultDialer.Dial("ws://localhost:8546", nil) //nolint - if err != nil { - log.Fatal("dial:", err) - } - defer c.Close() + fmt.Println("ws defined") // test storageChangeListener res, err := wsconn.initStorageChangeListener(1, nil) diff --git a/go.sum b/go.sum index c11637e1b0..91273a38f8 100644 --- a/go.sum +++ b/go.sum @@ -274,7 +274,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= diff --git a/lib/crypto/sr25519/sr25519.go b/lib/crypto/sr25519/sr25519.go index 157faa4dec..a9beb284a3 100644 --- a/lib/crypto/sr25519/sr25519.go +++ b/lib/crypto/sr25519/sr25519.go @@ -83,9 +83,13 @@ func NewKeypairFromPrivate(priv *PrivateKey) (*Keypair, error) { } // NewKeypairFromSeed returns a new sr25519 Keypair given a seed -func NewKeypairFromSeed(seed []byte) (*Keypair, error) { +func NewKeypairFromSeed(keystr []byte) (*Keypair, error) { + if len(keystr) != SeedLength { + return nil, errors.New("cannot generate key from seed: seed is not 32 bytes long") + } + buf := [SeedLength]byte{} - copy(buf[:], seed) + copy(buf[:], keystr) msc, err := sr25519.NewMiniSecretKeyFromRaw(buf) if err != nil { return nil, err diff --git a/lib/crypto/sr25519/sr25519_test.go b/lib/crypto/sr25519/sr25519_test.go index 4ab9add81e..04d468b1e8 100644 --- a/lib/crypto/sr25519/sr25519_test.go +++ b/lib/crypto/sr25519/sr25519_test.go @@ -34,6 +34,13 @@ func TestNewKeypairFromSeed(t *testing.T) { require.NoError(t, err) require.NotNil(t, kp.public) require.NotNil(t, kp.private) + + seed = make([]byte, 20) + _, err = rand.Read(seed) + require.NoError(t, err) + kp, err = NewKeypairFromSeed(seed) + require.Nil(t, kp) + require.Error(t, err, "cannot generate key from seed: seed is not 32 bytes long") } func TestSignAndVerify(t *testing.T) { diff --git a/lib/keystore/helpers.go b/lib/keystore/helpers.go index 0cc862ff29..576be2b930 100644 --- a/lib/keystore/helpers.go +++ b/lib/keystore/helpers.go @@ -64,6 +64,20 @@ func DecodePrivateKey(in []byte, keytype crypto.KeyType) (priv crypto.PrivateKey return priv, err } +// DecodeKeyPairFromHex turns an hex-encoded private key into a keypair +func DecodeKeyPairFromHex(keystr []byte, keytype crypto.KeyType) (kp crypto.Keypair, err error) { + switch keytype { + case crypto.Sr25519Type: + kp, err = sr25519.NewKeypairFromSeed(keystr) + case crypto.Ed25519Type: + kp, err = ed25519.NewKeypairFromSeed(keystr) + default: + return nil, errors.New("cannot decode key: invalid key type") + } + + return kp, err +} + // GenerateKeypair create a new keypair with the corresponding type and saves // it to basepath/keystore/[public key].key in json format encrypted using the // specified password and returns the resulting filepath of the new key diff --git a/lib/keystore/helpers_test.go b/lib/keystore/helpers_test.go index 6d23200dc6..71192a3eba 100644 --- a/lib/keystore/helpers_test.go +++ b/lib/keystore/helpers_test.go @@ -26,7 +26,10 @@ import ( "strings" "testing" + "github.com/ChainSafe/gossamer/lib/common" "github.com/ChainSafe/gossamer/lib/crypto" + "github.com/ChainSafe/gossamer/lib/crypto/ed25519" + "github.com/ChainSafe/gossamer/lib/crypto/sr25519" "github.com/ChainSafe/gossamer/lib/utils" "github.com/stretchr/testify/require" @@ -369,3 +372,32 @@ func TestImportRawPrivateKey_Secp256k1(t *testing.T) { require.Equal(t, "secp256k1", kscontents.Type) require.Equal(t, "0x03409094a319b2961660c3ebcc7d206266182c1b3e60d341b5fb17e6851865825c", kscontents.PublicKey) } + +func TestDecodeKeyPairFromHex(t *testing.T) { + keytype := DetermineKeyType("babe") + seed := "0xfec0f475b818470af5caf1f3c1b1558729961161946d581d2755f9fb566534f8" + + keyBytes, err := common.HexToBytes(seed) + require.NoError(t, err) + + kp, err := DecodeKeyPairFromHex(keyBytes, keytype) + require.NoError(t, err) + require.IsType(t, &sr25519.Keypair{}, kp) + + expectedPublic := "0x34309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc38520426026" + require.Equal(t, kp.Public().Hex(), expectedPublic) + + keytype = DetermineKeyType("gran") + keyBytes, err = common.HexToBytes("0x9d96ebdb66b7b6851d529f0c366393782baeba71732a59ce201ea80760d4d66c") + require.NoError(t, err) + + kp, err = DecodeKeyPairFromHex(keyBytes, keytype) + require.NoError(t, err) + require.IsType(t, &ed25519.Keypair{}, kp) + + expectedPublic = "0xd3db685ed1f94c195dc3e72803fa3d8549df45381388e313fa8170f0b397895c" + require.Equal(t, kp.Public().Hex(), expectedPublic) + + _, err = DecodeKeyPairFromHex(nil, "") + require.Error(t, err, "cannot decode key: invalid key type") +} diff --git a/lib/runtime/constants.go b/lib/runtime/constants.go index 1a889a5d70..a691a7fc67 100644 --- a/lib/runtime/constants.go +++ b/lib/runtime/constants.go @@ -72,6 +72,8 @@ var ( BlockBuilderApplyExtrinsic = "BlockBuilder_apply_extrinsic" // BlockBuilderFinalizeBlock is the runtime API call BlockBuilder_finalize_block BlockBuilderFinalizeBlock = "BlockBuilder_finalize_block" + // DecodeSessionKeys is the runtime API call SessionKeys_decode_session_keys + DecodeSessionKeys = "SessionKeys_decode_session_keys" ) // GrandpaAuthoritiesKey is the location of GRANDPA authority data in the storage trie for LEGACY_NODE_RUNTIME and NODE_RUNTIME diff --git a/lib/runtime/interface.go b/lib/runtime/interface.go index 5b9500e64f..8ace2d1e9a 100644 --- a/lib/runtime/interface.go +++ b/lib/runtime/interface.go @@ -47,6 +47,7 @@ type Instance interface { ApplyExtrinsic(data types.Extrinsic) ([]byte, error) FinalizeBlock() (*types.Header, error) ExecuteBlock(block *types.Block) ([]byte, error) + DecodeSessionKeys(enc []byte) ([]byte, error) // TODO: parameters and return values for these are undefined in the spec CheckInherents() diff --git a/lib/runtime/life/exports.go b/lib/runtime/life/exports.go index cc04df8112..b0ef1b38f3 100644 --- a/lib/runtime/life/exports.go +++ b/lib/runtime/life/exports.go @@ -150,6 +150,11 @@ func (in *Instance) ExecuteBlock(block *types.Block) ([]byte, error) { return in.Exec(runtime.CoreExecuteBlock, bdEnc) } +// DecodeSessionKeys decodes the given public session keys. Returns a list of raw public keys including their key type. +func (in *Instance) DecodeSessionKeys(enc []byte) ([]byte, error) { + return in.Exec(runtime.DecodeSessionKeys, enc) +} + func (in *Instance) CheckInherents() {} //nolint func (in *Instance) RandomSeed() {} //nolint func (in *Instance) OffchainWorker() {} //nolint diff --git a/lib/runtime/mocks/instance.go b/lib/runtime/mocks/instance.go new file mode 100644 index 0000000000..43f873ce82 --- /dev/null +++ b/lib/runtime/mocks/instance.go @@ -0,0 +1,431 @@ +// Code generated by mockery v2.8.0. DO NOT EDIT. + +package mocks + +import ( + common "github.com/ChainSafe/gossamer/lib/common" + keystore "github.com/ChainSafe/gossamer/lib/keystore" + + mock "github.com/stretchr/testify/mock" + + runtime "github.com/ChainSafe/gossamer/lib/runtime" + + transaction "github.com/ChainSafe/gossamer/lib/transaction" + + types "github.com/ChainSafe/gossamer/dot/types" +) + +// MockInstance is an autogenerated mock type for the Instance type +type MockInstance struct { + mock.Mock +} + +// ApplyExtrinsic provides a mock function with given fields: data +func (_m *MockInstance) ApplyExtrinsic(data types.Extrinsic) ([]byte, error) { + ret := _m.Called(data) + + var r0 []byte + if rf, ok := ret.Get(0).(func(types.Extrinsic) []byte); ok { + r0 = rf(data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(types.Extrinsic) error); ok { + r1 = rf(data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BabeConfiguration provides a mock function with given fields: +func (_m *MockInstance) BabeConfiguration() (*types.BabeConfiguration, error) { + ret := _m.Called() + + var r0 *types.BabeConfiguration + if rf, ok := ret.Get(0).(func() *types.BabeConfiguration); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.BabeConfiguration) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CheckInherents provides a mock function with given fields: +func (_m *MockInstance) CheckInherents() { + _m.Called() +} + +// CheckRuntimeVersion provides a mock function with given fields: _a0 +func (_m *MockInstance) CheckRuntimeVersion(_a0 []byte) (runtime.Version, error) { + ret := _m.Called(_a0) + + var r0 runtime.Version + if rf, ok := ret.Get(0).(func([]byte) runtime.Version); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(runtime.Version) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DecodeSessionKeys provides a mock function with given fields: enc +func (_m *MockInstance) DecodeSessionKeys(enc []byte) ([]byte, error) { + ret := _m.Called(enc) + + var r0 []byte + if rf, ok := ret.Get(0).(func([]byte) []byte); ok { + r0 = rf(enc) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(enc) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Exec provides a mock function with given fields: function, data +func (_m *MockInstance) Exec(function string, data []byte) ([]byte, error) { + ret := _m.Called(function, data) + + var r0 []byte + if rf, ok := ret.Get(0).(func(string, []byte) []byte); ok { + r0 = rf(function, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, []byte) error); ok { + r1 = rf(function, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExecuteBlock provides a mock function with given fields: block +func (_m *MockInstance) ExecuteBlock(block *types.Block) ([]byte, error) { + ret := _m.Called(block) + + var r0 []byte + if rf, ok := ret.Get(0).(func(*types.Block) []byte); ok { + r0 = rf(block) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*types.Block) error); ok { + r1 = rf(block) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FinalizeBlock provides a mock function with given fields: +func (_m *MockInstance) FinalizeBlock() (*types.Header, error) { + ret := _m.Called() + + var r0 *types.Header + if rf, ok := ret.Get(0).(func() *types.Header); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Header) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GenerateSessionKeys provides a mock function with given fields: +func (_m *MockInstance) GenerateSessionKeys() { + _m.Called() +} + +// GetCodeHash provides a mock function with given fields: +func (_m *MockInstance) GetCodeHash() common.Hash { + ret := _m.Called() + + var r0 common.Hash + if rf, ok := ret.Get(0).(func() common.Hash); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + return r0 +} + +// GrandpaAuthorities provides a mock function with given fields: +func (_m *MockInstance) GrandpaAuthorities() ([]*types.Authority, error) { + ret := _m.Called() + + var r0 []*types.Authority + if rf, ok := ret.Get(0).(func() []*types.Authority); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.Authority) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InherentExtrinsics provides a mock function with given fields: data +func (_m *MockInstance) InherentExtrinsics(data []byte) ([]byte, error) { + ret := _m.Called(data) + + var r0 []byte + if rf, ok := ret.Get(0).(func([]byte) []byte); ok { + r0 = rf(data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InitializeBlock provides a mock function with given fields: header +func (_m *MockInstance) InitializeBlock(header *types.Header) error { + ret := _m.Called(header) + + var r0 error + if rf, ok := ret.Get(0).(func(*types.Header) error); ok { + r0 = rf(header) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Keystore provides a mock function with given fields: +func (_m *MockInstance) Keystore() *keystore.GlobalKeystore { + ret := _m.Called() + + var r0 *keystore.GlobalKeystore + if rf, ok := ret.Get(0).(func() *keystore.GlobalKeystore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*keystore.GlobalKeystore) + } + } + + return r0 +} + +// Metadata provides a mock function with given fields: +func (_m *MockInstance) Metadata() ([]byte, error) { + ret := _m.Called() + + var r0 []byte + if rf, ok := ret.Get(0).(func() []byte); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NetworkService provides a mock function with given fields: +func (_m *MockInstance) NetworkService() runtime.BasicNetwork { + ret := _m.Called() + + var r0 runtime.BasicNetwork + if rf, ok := ret.Get(0).(func() runtime.BasicNetwork); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(runtime.BasicNetwork) + } + } + + return r0 +} + +// NodeStorage provides a mock function with given fields: +func (_m *MockInstance) NodeStorage() runtime.NodeStorage { + ret := _m.Called() + + var r0 runtime.NodeStorage + if rf, ok := ret.Get(0).(func() runtime.NodeStorage); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(runtime.NodeStorage) + } + + return r0 +} + +// OffchainWorker provides a mock function with given fields: +func (_m *MockInstance) OffchainWorker() { + _m.Called() +} + +// RandomSeed provides a mock function with given fields: +func (_m *MockInstance) RandomSeed() { + _m.Called() +} + +// SetContextStorage provides a mock function with given fields: s +func (_m *MockInstance) SetContextStorage(s runtime.Storage) { + _m.Called(s) +} + +// Stop provides a mock function with given fields: +func (_m *MockInstance) Stop() { + _m.Called() +} + +// UpdateRuntimeCode provides a mock function with given fields: _a0 +func (_m *MockInstance) UpdateRuntimeCode(_a0 []byte) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func([]byte) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ValidateTransaction provides a mock function with given fields: e +func (_m *MockInstance) ValidateTransaction(e types.Extrinsic) (*transaction.Validity, error) { + ret := _m.Called(e) + + var r0 *transaction.Validity + if rf, ok := ret.Get(0).(func(types.Extrinsic) *transaction.Validity); ok { + r0 = rf(e) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*transaction.Validity) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(types.Extrinsic) error); ok { + r1 = rf(e) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Validator provides a mock function with given fields: +func (_m *MockInstance) Validator() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Version provides a mock function with given fields: +func (_m *MockInstance) Version() (runtime.Version, error) { + ret := _m.Called() + + var r0 runtime.Version + if rf, ok := ret.Get(0).(func() runtime.Version); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(runtime.Version) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/lib/runtime/wasmer/exports.go b/lib/runtime/wasmer/exports.go index bb23594b90..48368eafe9 100644 --- a/lib/runtime/wasmer/exports.go +++ b/lib/runtime/wasmer/exports.go @@ -174,6 +174,11 @@ func (in *Instance) ExecuteBlock(block *types.Block) ([]byte, error) { return in.exec(runtime.CoreExecuteBlock, bdEnc) } +// DecodeSessionKeys decodes the given public session keys. Returns a list of raw public keys including their key type. +func (in *Instance) DecodeSessionKeys(enc []byte) ([]byte, error) { + return in.exec(runtime.DecodeSessionKeys, enc) +} + func (in *Instance) CheckInherents() {} //nolint func (in *Instance) RandomSeed() {} //nolint func (in *Instance) OffchainWorker() {} //nolint diff --git a/lib/runtime/wasmer/exports_test.go b/lib/runtime/wasmer/exports_test.go index 4ed2523a5f..96e2198b83 100644 --- a/lib/runtime/wasmer/exports_test.go +++ b/lib/runtime/wasmer/exports_test.go @@ -1010,6 +1010,29 @@ func TestInstance_ExecuteBlock_PolkadotBlock1089328(t *testing.T) { require.NoError(t, err) } +func TestInstance_DecodeSessionKeys(t *testing.T) { + keys := "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d34309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc3852042602634309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc3852042602634309a9d2a24213896ff06895db16aade8b6502f3a71cf56374cc38520426026" + pubkeys, err := common.HexToBytes(keys) + require.NoError(t, err) + + pukeysBytes, err := scale.Marshal(pubkeys) + require.NoError(t, err) + + instance := NewTestInstance(t, runtime.NODE_RUNTIME_v098) + decoded, err := instance.DecodeSessionKeys(pukeysBytes) + require.NoError(t, err) + + var decodedKeys *[]struct { + Data []uint8 + Type [4]uint8 + } + + err = scale.Unmarshal(decoded, &decodedKeys) + require.NoError(t, err) + + require.Len(t, *decodedKeys, 4) +} + func newTrieFromPairs(t *testing.T, filename string) *trie.Trie { data, err := ioutil.ReadFile(filename) require.NoError(t, err) diff --git a/lib/runtime/wasmtime/exports.go b/lib/runtime/wasmtime/exports.go index e2574f53c1..dc6f3a6f61 100644 --- a/lib/runtime/wasmtime/exports.go +++ b/lib/runtime/wasmtime/exports.go @@ -150,6 +150,11 @@ func (in *Instance) ExecuteBlock(block *types.Block) ([]byte, error) { return in.exec(runtime.CoreExecuteBlock, bdEnc) } +// DecodeSessionKeys decodes the given public session keys. Returns a list of raw public keys including their key type. +func (in *Instance) DecodeSessionKeys(enc []byte) ([]byte, error) { + return in.exec(runtime.DecodeSessionKeys, enc) +} + func (in *Instance) CheckInherents() {} //nolint func (in *Instance) RandomSeed() {} //nolint func (in *Instance) OffchainWorker() {} //nolint diff --git a/tests/rpc/rpc_05-state_test.go b/tests/rpc/rpc_05-state_test.go index acf4769266..fc8e147796 100644 --- a/tests/rpc/rpc_05-state_test.go +++ b/tests/rpc/rpc_05-state_test.go @@ -88,8 +88,12 @@ func TestStateRPCResponseValidation(t *testing.T) { { description: "Test state_queryStorage", method: "state_queryStorage", - params: `[["0xf2794c22e353e9a839f12faab03a911bf68967d635641a7087e53f2bff1ecad3c6756fee45ec79ead60347fffb770bcdf0ec74da701ab3d6495986fe1ecc3027"], "0xa32c60dee8647b07435ae7583eb35cee606209a595718562dd4a486a07b6de15", null]`, - expected: modules.StorageChangeSetResponse{}, + params: fmt.Sprintf(`[["0xf2794c22e353e9a839f12faab03a911bf68967d635641a7087e53f2bff1ecad3c6756fee45ec79ead60347fffb770bcdf0ec74da701ab3d6495986fe1ecc3027"], "%s", null]`, blockHash), + expected: modules.StorageChangeSetResponse{ + Block: &blockHash, + Changes: [][]string{}, + }, + skip: true, }, { description: "Test valid block hash state_getRuntimeVersion",