github.com/prysmaticlabs/prysm@v1.4.4/validator/rpc/wallet.go (about)

     1  package rpc
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"fmt"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/golang/protobuf/ptypes/empty"
    12  	"github.com/pkg/errors"
    13  	pb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2"
    14  	"github.com/prysmaticlabs/prysm/shared/featureconfig"
    15  	"github.com/prysmaticlabs/prysm/shared/fileutil"
    16  	"github.com/prysmaticlabs/prysm/shared/promptutil"
    17  	"github.com/prysmaticlabs/prysm/shared/rand"
    18  	"github.com/prysmaticlabs/prysm/validator/accounts"
    19  	"github.com/prysmaticlabs/prysm/validator/accounts/iface"
    20  	"github.com/prysmaticlabs/prysm/validator/accounts/wallet"
    21  	"github.com/prysmaticlabs/prysm/validator/keymanager"
    22  	"github.com/prysmaticlabs/prysm/validator/keymanager/imported"
    23  	"github.com/tyler-smith/go-bip39"
    24  	"github.com/tyler-smith/go-bip39/wordlists"
    25  	"google.golang.org/grpc/codes"
    26  	"google.golang.org/grpc/status"
    27  )
    28  
    29  const (
    30  	checkExistsErrMsg   = "Could not check if wallet exists"
    31  	checkValidityErrMsg = "Could not check if wallet is valid"
    32  	invalidWalletMsg    = "Directory does not contain a valid wallet"
    33  )
    34  
    35  // CreateWallet via an API request, allowing a user to save a new
    36  // imported wallet via RPC.
    37  func (s *Server) CreateWallet(ctx context.Context, req *pb.CreateWalletRequest) (*pb.CreateWalletResponse, error) {
    38  	walletDir := s.walletDir
    39  	exists, err := wallet.Exists(walletDir)
    40  	if err != nil {
    41  		return nil, status.Errorf(codes.Internal, "Could not check for existing wallet: %v", err)
    42  	}
    43  	if exists {
    44  		if err := s.initializeWallet(ctx, &wallet.Config{
    45  			WalletDir:      walletDir,
    46  			WalletPassword: req.WalletPassword,
    47  		}); err != nil {
    48  			return nil, err
    49  		}
    50  		keymanagerKind := pb.KeymanagerKind_IMPORTED
    51  		switch s.wallet.KeymanagerKind() {
    52  		case keymanager.Derived:
    53  			keymanagerKind = pb.KeymanagerKind_DERIVED
    54  		case keymanager.Remote:
    55  			keymanagerKind = pb.KeymanagerKind_REMOTE
    56  		}
    57  		return &pb.CreateWalletResponse{
    58  			Wallet: &pb.WalletResponse{
    59  				WalletPath:     walletDir,
    60  				KeymanagerKind: keymanagerKind,
    61  			},
    62  		}, nil
    63  	}
    64  	if req.Keymanager == pb.KeymanagerKind_IMPORTED {
    65  		_, err := accounts.CreateWalletWithKeymanager(ctx, &accounts.CreateWalletConfig{
    66  			WalletCfg: &wallet.Config{
    67  				WalletDir:      walletDir,
    68  				KeymanagerKind: keymanager.Imported,
    69  				WalletPassword: req.WalletPassword,
    70  			},
    71  			SkipMnemonicConfirm: true,
    72  		})
    73  		if err != nil {
    74  			return nil, err
    75  		}
    76  		if err := s.initializeWallet(ctx, &wallet.Config{
    77  			WalletDir:      walletDir,
    78  			KeymanagerKind: keymanager.Imported,
    79  			WalletPassword: req.WalletPassword,
    80  		}); err != nil {
    81  			return nil, err
    82  		}
    83  		if err := writeWalletPasswordToDisk(walletDir, req.WalletPassword); err != nil {
    84  			return nil, status.Error(codes.Internal, "Could not write wallet password to disk")
    85  		}
    86  		return &pb.CreateWalletResponse{
    87  			Wallet: &pb.WalletResponse{
    88  				WalletPath:     walletDir,
    89  				KeymanagerKind: pb.KeymanagerKind_IMPORTED,
    90  			},
    91  		}, nil
    92  	}
    93  	return nil, status.Errorf(codes.InvalidArgument, "Keymanager type %T create wallet not supported through web", req.Keymanager)
    94  }
    95  
    96  // WalletConfig returns the wallet's configuration. If no wallet exists, we return an empty response.
    97  func (s *Server) WalletConfig(ctx context.Context, _ *empty.Empty) (*pb.WalletResponse, error) {
    98  	exists, err := wallet.Exists(s.walletDir)
    99  	if err != nil {
   100  		return nil, status.Errorf(codes.Internal, checkExistsErrMsg)
   101  	}
   102  	if !exists {
   103  		// If no wallet is found, we simply return an empty response.
   104  		return &pb.WalletResponse{}, nil
   105  	}
   106  	valid, err := wallet.IsValid(s.walletDir)
   107  	if errors.Is(err, wallet.ErrNoWalletFound) {
   108  		return &pb.WalletResponse{}, nil
   109  	}
   110  	if err != nil {
   111  		return nil, status.Errorf(codes.Internal, checkValidityErrMsg)
   112  	}
   113  	if !valid {
   114  		return nil, status.Errorf(codes.FailedPrecondition, invalidWalletMsg)
   115  	}
   116  
   117  	if s.wallet == nil || s.keymanager == nil {
   118  		// If no wallet is found, we simply return an empty response.
   119  		return &pb.WalletResponse{}, nil
   120  	}
   121  	var keymanagerKind pb.KeymanagerKind
   122  	switch s.wallet.KeymanagerKind() {
   123  	case keymanager.Derived:
   124  		keymanagerKind = pb.KeymanagerKind_DERIVED
   125  	case keymanager.Imported:
   126  		keymanagerKind = pb.KeymanagerKind_IMPORTED
   127  	case keymanager.Remote:
   128  		keymanagerKind = pb.KeymanagerKind_REMOTE
   129  	}
   130  	return &pb.WalletResponse{
   131  		WalletPath:     s.walletDir,
   132  		KeymanagerKind: keymanagerKind,
   133  	}, nil
   134  }
   135  
   136  // RecoverWallet via an API request, allowing a user to recover a derived.
   137  // Generate the seed from the mnemonic + language + 25th passphrase(optional).
   138  // Create N validator keystores from the seed specified by req.NumAccounts.
   139  // Set the wallet password to req.WalletPassword, then create the wallet from
   140  // the provided Mnemonic and return CreateWalletResponse.
   141  func (s *Server) RecoverWallet(ctx context.Context, req *pb.RecoverWalletRequest) (*pb.CreateWalletResponse, error) {
   142  	numAccounts := int(req.NumAccounts)
   143  	if numAccounts == 0 {
   144  		return nil, status.Error(codes.InvalidArgument, "Must create at least 1 validator account")
   145  	}
   146  
   147  	// Check validate mnemonic with chosen language
   148  	language := strings.ToLower(req.Language)
   149  	allowedLanguages := map[string][]string{
   150  		"chinese_simplified":  wordlists.ChineseSimplified,
   151  		"chinese_traditional": wordlists.ChineseTraditional,
   152  		"czech":               wordlists.Czech,
   153  		"english":             wordlists.English,
   154  		"french":              wordlists.French,
   155  		"japanese":            wordlists.Japanese,
   156  		"korean":              wordlists.Korean,
   157  		"italian":             wordlists.Italian,
   158  		"spanish":             wordlists.Spanish,
   159  	}
   160  	if _, ok := allowedLanguages[language]; !ok {
   161  		return nil, status.Error(codes.InvalidArgument, "input not in the list of supported languages")
   162  	}
   163  	bip39.SetWordList(allowedLanguages[language])
   164  	mnemonic := req.Mnemonic
   165  	if err := accounts.ValidateMnemonic(mnemonic); err != nil {
   166  		return nil, status.Error(codes.InvalidArgument, "invalid mnemonic in request")
   167  	}
   168  
   169  	// Check it is not null and not an empty string.
   170  	if req.Mnemonic25ThWord != "" && strings.TrimSpace(req.Mnemonic25ThWord) == "" {
   171  		return nil, status.Error(codes.InvalidArgument, "mnemonic 25th word cannot be empty")
   172  	}
   173  
   174  	// Web UI is structured to only write to the default wallet directory
   175  	// accounts.Recoverwallet checks if wallet already exists.
   176  	walletDir := s.walletDir
   177  
   178  	// Web UI should check the new and confirmed password are equal.
   179  	walletPassword := req.WalletPassword
   180  	if err := promptutil.ValidatePasswordInput(walletPassword); err != nil {
   181  		return nil, status.Error(codes.InvalidArgument, "password did not pass validation")
   182  	}
   183  
   184  	if _, err := accounts.RecoverWallet(ctx, &accounts.RecoverWalletConfig{
   185  		WalletDir:        walletDir,
   186  		WalletPassword:   walletPassword,
   187  		Mnemonic:         mnemonic,
   188  		NumAccounts:      numAccounts,
   189  		Mnemonic25thWord: req.Mnemonic25ThWord,
   190  	}); err != nil {
   191  		return nil, err
   192  	}
   193  	if err := s.initializeWallet(ctx, &wallet.Config{
   194  		WalletDir:      walletDir,
   195  		KeymanagerKind: keymanager.Derived,
   196  		WalletPassword: walletPassword,
   197  	}); err != nil {
   198  		return nil, err
   199  	}
   200  	if err := writeWalletPasswordToDisk(walletDir, walletPassword); err != nil {
   201  		return nil, status.Error(codes.Internal, "Could not write wallet password to disk")
   202  	}
   203  	return &pb.CreateWalletResponse{
   204  		Wallet: &pb.WalletResponse{
   205  			WalletPath:     walletDir,
   206  			KeymanagerKind: pb.KeymanagerKind_DERIVED,
   207  		},
   208  	}, nil
   209  }
   210  
   211  // GenerateMnemonic creates a new, random bip39 mnemonic phrase.
   212  func (s *Server) GenerateMnemonic(_ context.Context, _ *empty.Empty) (*pb.GenerateMnemonicResponse, error) {
   213  	mnemonicRandomness := make([]byte, 32)
   214  	if _, err := rand.NewGenerator().Read(mnemonicRandomness); err != nil {
   215  		return nil, status.Errorf(
   216  			codes.FailedPrecondition,
   217  			"Could not initialize mnemonic source of randomness: %v",
   218  			err,
   219  		)
   220  	}
   221  	mnemonic, err := bip39.NewMnemonic(mnemonicRandomness)
   222  	if err != nil {
   223  		return nil, status.Errorf(codes.Internal, "Could not generate wallet seed: %v", err)
   224  	}
   225  	return &pb.GenerateMnemonicResponse{
   226  		Mnemonic: mnemonic,
   227  	}, nil
   228  }
   229  
   230  // ImportKeystores allows importing new keystores via RPC into the wallet
   231  // which will be decrypted using the specified password .
   232  func (s *Server) ImportKeystores(
   233  	ctx context.Context, req *pb.ImportKeystoresRequest,
   234  ) (*pb.ImportKeystoresResponse, error) {
   235  	if s.wallet == nil {
   236  		return nil, status.Error(codes.FailedPrecondition, "No wallet initialized")
   237  	}
   238  	km, ok := s.keymanager.(*imported.Keymanager)
   239  	if !ok {
   240  		return nil, status.Error(codes.FailedPrecondition, "Only imported wallets can import more keystores")
   241  	}
   242  	if req.KeystoresPassword == "" {
   243  		return nil, status.Error(codes.InvalidArgument, "Password required for keystores")
   244  	}
   245  	// Needs to unmarshal the keystores from the requests.
   246  	if req.KeystoresImported == nil || len(req.KeystoresImported) < 1 {
   247  		return nil, status.Error(codes.InvalidArgument, "No keystores included for import")
   248  	}
   249  	keystores := make([]*keymanager.Keystore, len(req.KeystoresImported))
   250  	importedPubKeys := make([][]byte, len(req.KeystoresImported))
   251  	for i := 0; i < len(req.KeystoresImported); i++ {
   252  		encoded := req.KeystoresImported[i]
   253  		keystore := &keymanager.Keystore{}
   254  		if err := json.Unmarshal([]byte(encoded), &keystore); err != nil {
   255  			return nil, status.Errorf(codes.InvalidArgument, "Not a valid EIP-2335 keystore JSON file: %v", err)
   256  		}
   257  		keystores[i] = keystore
   258  		pubKey, err := hex.DecodeString(keystore.Pubkey)
   259  		if err != nil {
   260  			return nil, status.Errorf(codes.InvalidArgument, "Not a valid BLS public key in keystore file: %v", err)
   261  		}
   262  		importedPubKeys[i] = pubKey
   263  	}
   264  	// Import the uploaded accounts.
   265  	if err := accounts.ImportAccounts(ctx, &accounts.ImportAccountsConfig{
   266  		Keymanager:      km,
   267  		Keystores:       keystores,
   268  		AccountPassword: req.KeystoresPassword,
   269  	}); err != nil {
   270  		return nil, err
   271  	}
   272  	s.walletInitializedFeed.Send(s.wallet)
   273  	return &pb.ImportKeystoresResponse{
   274  		ImportedPublicKeys: importedPubKeys,
   275  	}, nil
   276  }
   277  
   278  // Initialize a wallet and send it over a global feed.
   279  func (s *Server) initializeWallet(ctx context.Context, cfg *wallet.Config) error {
   280  	// We first ensure the user has a wallet.
   281  	exists, err := wallet.Exists(cfg.WalletDir)
   282  	if err != nil {
   283  		return errors.Wrap(err, wallet.CheckExistsErrMsg)
   284  	}
   285  	if !exists {
   286  		return wallet.ErrNoWalletFound
   287  	}
   288  	valid, err := wallet.IsValid(cfg.WalletDir)
   289  	if errors.Is(err, wallet.ErrNoWalletFound) {
   290  		return wallet.ErrNoWalletFound
   291  	}
   292  	if err != nil {
   293  		return errors.Wrap(err, wallet.CheckValidityErrMsg)
   294  	}
   295  	if !valid {
   296  		return errors.New(wallet.InvalidWalletErrMsg)
   297  	}
   298  
   299  	// We fire an event with the opened wallet over
   300  	// a global feed signifying wallet initialization.
   301  	w, err := wallet.OpenWallet(ctx, &wallet.Config{
   302  		WalletDir:      cfg.WalletDir,
   303  		WalletPassword: cfg.WalletPassword,
   304  	})
   305  	if err != nil {
   306  		return errors.Wrap(err, "could not open wallet")
   307  	}
   308  
   309  	s.walletInitialized = true
   310  	km, err := w.InitializeKeymanager(ctx, iface.InitKeymanagerConfig{ListenForChanges: true})
   311  	if err != nil {
   312  		return errors.Wrap(err, accounts.ErrCouldNotInitializeKeymanager)
   313  	}
   314  	s.keymanager = km
   315  	s.wallet = w
   316  	s.walletDir = cfg.WalletDir
   317  
   318  	// Only send over feed if we have validating keys.
   319  	validatingPublicKeys, err := km.FetchValidatingPublicKeys(ctx)
   320  	if err != nil {
   321  		return errors.Wrap(err, "could not check for validating public keys")
   322  	}
   323  	if len(validatingPublicKeys) > 0 {
   324  		s.walletInitializedFeed.Send(w)
   325  	}
   326  	return nil
   327  }
   328  
   329  func writeWalletPasswordToDisk(walletDir, password string) error {
   330  	if !featureconfig.Get().WriteWalletPasswordOnWebOnboarding {
   331  		return nil
   332  	}
   333  	passwordFilePath := filepath.Join(walletDir, wallet.DefaultWalletPasswordFile)
   334  	if fileutil.FileExists(passwordFilePath) {
   335  		return fmt.Errorf("cannot write wallet password file as it already exists %s", passwordFilePath)
   336  	}
   337  	return fileutil.WriteFile(passwordFilePath, []byte(password))
   338  }