diff --git a/validator/rpc/wallet.go b/validator/rpc/wallet.go index b93234d9af69..c86857a38096 100644 --- a/validator/rpc/wallet.go +++ b/validator/rpc/wallet.go @@ -2,6 +2,7 @@ package rpc import ( "context" + "encoding/hex" "encoding/json" "io/ioutil" "path/filepath" @@ -280,9 +281,48 @@ func (s *Server) ChangePassword(ctx context.Context, req *pb.ChangePasswordReque return &ptypes.Empty{}, nil } -// ImportKeystores -- +// ImportKeystores allows importing new keystores via RPC into the wallet +// which will be decrypted using the specified password . func (s *Server) ImportKeystores( ctx context.Context, req *pb.ImportKeystoresRequest, ) (*pb.ImportKeystoresResponse, error) { - return nil, status.Error(codes.Unimplemented, "Unimplemented") + if s.wallet == nil { + return nil, status.Error(codes.FailedPrecondition, "No wallet initialized") + } + if s.wallet.KeymanagerKind() != v2keymanager.Direct { + return nil, status.Error(codes.FailedPrecondition, "Only Non-HD wallets can import keystores") + } + if req.KeystoresPassword == "" { + return nil, status.Error(codes.InvalidArgument, "Password required for keystores") + } + // Needs to unmarshal the keystores from the requests. + if req.KeystoresImported == nil || len(req.KeystoresImported) < 1 { + return nil, status.Error(codes.InvalidArgument, "No keystores included for import") + } + keystores := make([]*v2keymanager.Keystore, len(req.KeystoresImported)) + importedPubKeys := make([][]byte, len(req.KeystoresImported)) + for i := 0; i < len(req.KeystoresImported); i++ { + encoded := req.KeystoresImported[i] + keystore := &v2keymanager.Keystore{} + if err := json.Unmarshal([]byte(encoded), &keystore); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "Not a valid EIP-2335 keystore JSON file: %v", err) + } + keystores[i] = keystore + pubKey, err := hex.DecodeString(keystore.Pubkey) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "Not a valid BLS public key in keystore file: %v", err) + } + importedPubKeys[i] = pubKey + } + // Import the uploaded accounts. + if err := v2.ImportAccounts(ctx, &v2.ImportAccountsConfig{ + Wallet: s.wallet, + Keystores: keystores, + AccountPassword: req.KeystoresPassword, + }); err != nil { + return nil, err + } + return &pb.ImportKeystoresResponse{ + ImportedPublicKeys: importedPubKeys, + }, nil } diff --git a/validator/rpc/wallet_test.go b/validator/rpc/wallet_test.go index 7686cd761a9f..99f6f2d3198a 100644 --- a/validator/rpc/wallet_test.go +++ b/validator/rpc/wallet_test.go @@ -286,3 +286,131 @@ func TestServer_HasWallet(t *testing.T) { WalletExists: true, }, resp) } + +func TestServer_ImportKeystores_FailedPreconditions_WrongKeymanagerKind(t *testing.T) { + localWalletDir := setupWalletDir(t) + defaultWalletPath = localWalletDir + ctx := context.Background() + strongPass := "29384283xasjasd32%%&*@*#*" + w, err := v2.CreateWalletWithKeymanager(ctx, &v2.CreateWalletConfig{ + WalletCfg: &wallet.Config{ + WalletDir: defaultWalletPath, + KeymanagerKind: v2keymanager.Derived, + WalletPassword: strongPass, + }, + SkipMnemonicConfirm: true, + }) + require.NoError(t, err) + km, err := w.InitializeKeymanager(ctx, true /* skip mnemonic confirm */) + require.NoError(t, err) + ss := &Server{ + wallet: w, + keymanager: km, + } + _, err = ss.ImportKeystores(ctx, &pb.ImportKeystoresRequest{}) + assert.ErrorContains(t, "Only Non-HD wallets can import", err) +} + +func TestServer_ImportKeystores_FailedPreconditions(t *testing.T) { + localWalletDir := setupWalletDir(t) + defaultWalletPath = localWalletDir + ctx := context.Background() + strongPass := "29384283xasjasd32%%&*@*#*" + w, err := v2.CreateWalletWithKeymanager(ctx, &v2.CreateWalletConfig{ + WalletCfg: &wallet.Config{ + WalletDir: defaultWalletPath, + KeymanagerKind: v2keymanager.Direct, + WalletPassword: strongPass, + }, + SkipMnemonicConfirm: true, + }) + require.NoError(t, err) + require.NoError(t, w.SaveHashedPassword(ctx)) + km, err := w.InitializeKeymanager(ctx, true /* skip mnemonic confirm */) + require.NoError(t, err) + ss := &Server{ + keymanager: km, + } + _, err = ss.ImportKeystores(ctx, &pb.ImportKeystoresRequest{}) + assert.ErrorContains(t, "No wallet initialized", err) + ss.wallet = w + _, err = ss.ImportKeystores(ctx, &pb.ImportKeystoresRequest{}) + assert.ErrorContains(t, "Password required for keystores", err) + _, err = ss.ImportKeystores(ctx, &pb.ImportKeystoresRequest{ + KeystoresPassword: strongPass, + }) + assert.ErrorContains(t, "No keystores included for import", err) + _, err = ss.ImportKeystores(ctx, &pb.ImportKeystoresRequest{ + KeystoresPassword: strongPass, + KeystoresImported: []string{"badjson"}, + }) + assert.ErrorContains(t, "Not a valid EIP-2335 keystore", err) +} + +func TestServer_ImportKeystores_OK(t *testing.T) { + localWalletDir := setupWalletDir(t) + defaultWalletPath = localWalletDir + ctx := context.Background() + strongPass := "29384283xasjasd32%%&*@*#*" + w, err := v2.CreateWalletWithKeymanager(ctx, &v2.CreateWalletConfig{ + WalletCfg: &wallet.Config{ + WalletDir: defaultWalletPath, + KeymanagerKind: v2keymanager.Direct, + WalletPassword: strongPass, + }, + SkipMnemonicConfirm: true, + }) + require.NoError(t, err) + require.NoError(t, w.SaveHashedPassword(ctx)) + km, err := w.InitializeKeymanager(ctx, true /* skip mnemonic confirm */) + require.NoError(t, err) + ss := &Server{ + keymanager: km, + wallet: w, + } + + // Create 3 keystores. + encryptor := keystorev4.New() + keystores := make([]string, 3) + pubKeys := make([][]byte, 3) + for i := 0; i < len(keystores); i++ { + privKey := bls.RandKey() + pubKey := fmt.Sprintf("%x", privKey.PublicKey().Marshal()) + id, err := uuid.NewRandom() + require.NoError(t, err) + cryptoFields, err := encryptor.Encrypt(privKey.Marshal(), strongPass) + require.NoError(t, err) + item := &v2keymanager.Keystore{ + Crypto: cryptoFields, + ID: id.String(), + Version: encryptor.Version(), + Pubkey: pubKey, + Name: encryptor.Name(), + } + encodedFile, err := json.MarshalIndent(item, "", "\t") + require.NoError(t, err) + keystores[i] = string(encodedFile) + pubKeys[i] = privKey.PublicKey().Marshal() + } + + // Check the wallet has no accounts to start with. + keys, err := km.FetchValidatingPublicKeys(ctx) + require.NoError(t, err) + assert.Equal(t, 0, len(keys)) + + // Import the 3 keystores and verify the wallet has 3 new accounts. + res, err := ss.ImportKeystores(ctx, &pb.ImportKeystoresRequest{ + KeystoresPassword: strongPass, + KeystoresImported: keystores, + }) + require.NoError(t, err) + assert.DeepEqual(t, &pb.ImportKeystoresResponse{ + ImportedPublicKeys: pubKeys, + }, res) + + km, err = w.InitializeKeymanager(ctx, true /* skip mnemonic confirm */) + require.NoError(t, err) + keys, err = km.FetchValidatingPublicKeys(ctx) + require.NoError(t, err) + assert.Equal(t, 3, len(keys)) +}