Skip to content

Commit

Permalink
feat(dot/rpc) implement author_hasSessionKeys RPC call (ChainSafe#1704
Browse files Browse the repository at this point in the history
)

* chore: add interface for grandpa in rpc pkg

* chore: create roundState rpc call

* chore: coment unused branch

* chore: fix lint

* chore: add test case

* chore: fix lint

* chore: add grandpa subscribe justification rpc call

* chore: add cancel function to stop goroutine when unsuubscribe

* chore: resolve lint

* chore: add tests to subscribe justification call

* remove deps from round state rpc call pr

* chore: remove unecessary changes

* remove inpackage well mock is in mocks folder

* chore: fix lint

* wip: fixing tests

* chore: use channels to control goroutines

* chore: resolve lint

* add time.Duration on structs

* chore: add runtime method and rpc method

* wip: fix runtime response scale decoding

* chore: adding data to tests

* wip: improve test coverage and assertions

* chore: hasSessionKey rpc call done

* chore: change config.toml back

* chore: remove log, add string.EqualFold

* chore: remove unused logs, get back new private key

* update hasSessionKey method to use coreapi insted of runtime api

* chore: resolve lint issues

* chore: change from []byte to []uint8

* chore: fix tests failures

* chore: fix HasSessionKeyResponse comments

Co-authored-by: noot <36753753+noot@users.noreply.github.com>

* chore: fix KeyTypeID comments

Co-authored-by: noot <36753753+noot@users.noreply.github.com>

* chore: update comments, unexport struct and remove qtyCheck

* chore: improve hasSessionKey key check

* chore: update params to represent better data

* chore: improve func name to DecodeKeyPairFromHex

* chore: get back func names to avoid naming conflicts

* chore: update go.sum

* chore: update grandpa_subscribeJustification return response

* chore: fix lint issues

* chore: check the len of the slice of decoded keys

* chore: update exports comments

* chore: update the import style

* chore: improve DecodeKeyPairFromHex export comment

* chore: hasSessionKeys test fixed

* chore: improve export function comment

* chore: group rpc methods string in a unique place

* chore: use chan struct{}

* chore: fix tests that uses wsconn

* chore: fix subscription test

* chore: improve log message

* chore: fix lint issues

* chore: increase the author RPC method qty

* chore: fix deepsource style error

Co-authored-by: Arijit Das <arijitad.in@gmail.com>
Co-authored-by: noot <36753753+noot@users.noreply.github.com>
  • Loading branch information
3 people authored and timwu20 committed Dec 6, 2021
1 parent 2c5ed48 commit a20c2e1
Show file tree
Hide file tree
Showing 21 changed files with 748 additions and 171 deletions.
10 changes: 10 additions & 0 deletions dot/core/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,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
Expand Down
16 changes: 7 additions & 9 deletions dot/rpc/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,13 @@ func (h *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// NewWSConn to create new WebSocket Connection struct
func NewWSConn(conn *websocket.Conn, cfg *HTTPServerConfig) *subscription.WSConn {
c := &subscription.WSConn{
Wsconn: conn,
Subscriptions: make(map[uint]subscription.Listener),
BlockSubChannels: make(map[uint]byte),
StorageSubChannels: make(map[int]byte),
StorageAPI: cfg.StorageAPI,
BlockAPI: cfg.BlockAPI,
CoreAPI: cfg.CoreAPI,
TxStateAPI: cfg.TransactionQueueAPI,
RPCHost: fmt.Sprintf("http://%s:%d/", cfg.Host, cfg.RPCPort),
Wsconn: conn,
Subscriptions: make(map[uint32]subscription.Listener),
StorageAPI: cfg.StorageAPI,
BlockAPI: cfg.BlockAPI,
CoreAPI: cfg.CoreAPI,
TxStateAPI: cfg.TransactionQueueAPI,
RPCHost: fmt.Sprintf("http://%s:%d/", cfg.Host, cfg.RPCPort),
HTTP: &http.Client{
Timeout: time.Second * 30,
},
Expand Down
1 change: 1 addition & 0 deletions dot/rpc/modules/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type CoreAPI interface {
GetRuntimeVersion(bhash *common.Hash) (runtime.Version, error)
HandleSubmittedExtrinsic(types.Extrinsic) error
GetMetadata(bhash *common.Hash) ([]byte, error)
DecodeSessionKeys(enc []byte) ([]byte, error)
}

// RPCAPI is the interface for methods related to RPC service
Expand Down
83 changes: 72 additions & 11 deletions dot/rpc/modules/author.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
74 changes: 70 additions & 4 deletions dot/rpc/modules/author_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,78 @@ 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(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"))
Expand Down Expand Up @@ -202,8 +267,8 @@ func TestAuthorModule_InsertKey(t *testing.T) {
args: args{
req: &KeyInsertRequest{
"babe",
"0xb7e9185065667390d2ad952a5324e8c365c9bf503dcf97c67a5ce861afe97309",
"0x6246ddf254e0b4b4e7dffefc8adf69d212b98ac2b579c362b473fec8c40b4c0a",
"0xdad5131003242c37c227f744f82118dd59a24b949ae264a93d949100738c196c",
},
},
},
Expand All @@ -214,9 +279,10 @@ func TestAuthorModule_InsertKey(t *testing.T) {
coreAPI: mockCoreAPI,
},
args: args{
req: &KeyInsertRequest{"gran",
"0xb7e9185065667390d2ad952a5324e8c365c9bf503dcf97c67a5ce861afe97309b7e9185065667390d2ad952a5324e8c365c9bf503dcf97c67a5ce861afe97309",
"0xb7e9185065667390d2ad952a5324e8c365c9bf503dcf97c67a5ce861afe97309",
req: &KeyInsertRequest{
"gran",
"0xb48004c6e1625282313b07d1c9950935e86894a2e4f21fb1ffee9854d180c781",
"0xa7d6507d59f8871b8f1a0f2c32e219adfacff4c9fcb05b0b2d8ebd6a65c88ee6",
},
},
},
Expand Down
37 changes: 23 additions & 14 deletions dot/rpc/modules/mocks/core_api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dot/rpc/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestNewService(t *testing.T) {
func TestService_Methods(t *testing.T) {
qtySystemMethods := 13
qtyRPCMethods := 1
qtyAuthorMethods := 7
qtyAuthorMethods := 8

rpcService := NewService()
sysMod := modules.NewSystemModule(nil, nil, nil, nil, nil, nil)
Expand Down
Loading

0 comments on commit a20c2e1

Please sign in to comment.