github.com/prysmaticlabs/prysm@v1.4.4/validator/accounts/accounts_import.go (about)

     1  package accounts
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"path/filepath"
    10  	"regexp"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/google/uuid"
    16  	"github.com/pkg/errors"
    17  	"github.com/prysmaticlabs/prysm/cmd/validator/flags"
    18  	"github.com/prysmaticlabs/prysm/shared/bls"
    19  	"github.com/prysmaticlabs/prysm/shared/bytesutil"
    20  	"github.com/prysmaticlabs/prysm/shared/fileutil"
    21  	"github.com/prysmaticlabs/prysm/shared/promptutil"
    22  	"github.com/prysmaticlabs/prysm/validator/accounts/iface"
    23  	"github.com/prysmaticlabs/prysm/validator/accounts/prompt"
    24  	"github.com/prysmaticlabs/prysm/validator/accounts/wallet"
    25  	"github.com/prysmaticlabs/prysm/validator/keymanager"
    26  	"github.com/prysmaticlabs/prysm/validator/keymanager/imported"
    27  	"github.com/urfave/cli/v2"
    28  	keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
    29  )
    30  
    31  var derivationPathRegex = regexp.MustCompile(`m_12381_3600_(\d+)_(\d+)_(\d+)`)
    32  
    33  // byDerivationPath implements sort.Interface based on a
    34  // derivation path present in a keystore filename, if any. This
    35  // will allow us to sort filenames such as keystore-m_12381_3600_1_0_0.json
    36  // in a directory and import them nicely in order of the derivation path.
    37  type byDerivationPath []string
    38  
    39  // Len is the number of elements in the collection.
    40  func (fileNames byDerivationPath) Len() int { return len(fileNames) }
    41  
    42  // Less reports whether the element with index i must sort before the element with index j.
    43  func (fileNames byDerivationPath) Less(i, j int) bool {
    44  	// We check if file name at index i has a derivation path
    45  	// in the filename. If it does not, then it is not less than j, and
    46  	// we should swap it towards the end of the sorted list.
    47  	if !derivationPathRegex.MatchString(fileNames[i]) {
    48  		return false
    49  	}
    50  	derivationPathA := derivationPathRegex.FindString(fileNames[i])
    51  	derivationPathB := derivationPathRegex.FindString(fileNames[j])
    52  	if derivationPathA == "" {
    53  		return false
    54  	}
    55  	if derivationPathB == "" {
    56  		return true
    57  	}
    58  	a, err := strconv.Atoi(accountIndexFromFileName(derivationPathA))
    59  	if err != nil {
    60  		return false
    61  	}
    62  	b, err := strconv.Atoi(accountIndexFromFileName(derivationPathB))
    63  	if err != nil {
    64  		return false
    65  	}
    66  	return a < b
    67  }
    68  
    69  // Swap swaps the elements with indexes i and j.
    70  func (fileNames byDerivationPath) Swap(i, j int) {
    71  	fileNames[i], fileNames[j] = fileNames[j], fileNames[i]
    72  }
    73  
    74  // ImportAccountsConfig defines values to run the import accounts function.
    75  type ImportAccountsConfig struct {
    76  	Keystores       []*keymanager.Keystore
    77  	Keymanager      *imported.Keymanager
    78  	AccountPassword string
    79  }
    80  
    81  // ImportAccountsCli can import external, EIP-2335 compliant keystore.json files as
    82  // new accounts into the Prysm validator wallet. This uses the CLI to extract
    83  // values necessary to run the function.
    84  func ImportAccountsCli(cliCtx *cli.Context) error {
    85  	w, err := wallet.OpenWalletOrElseCli(cliCtx, func(cliCtx *cli.Context) (*wallet.Wallet, error) {
    86  		walletDir, err := prompt.InputDirectory(cliCtx, prompt.WalletDirPromptText, flags.WalletDirFlag)
    87  		if err != nil {
    88  			return nil, err
    89  		}
    90  		exists, err := wallet.Exists(walletDir)
    91  		if err != nil {
    92  			return nil, errors.Wrap(err, wallet.CheckExistsErrMsg)
    93  		}
    94  		if exists {
    95  			isValid, err := wallet.IsValid(walletDir)
    96  			if err != nil {
    97  				return nil, errors.Wrap(err, wallet.CheckValidityErrMsg)
    98  			}
    99  			if !isValid {
   100  				return nil, errors.New(wallet.InvalidWalletErrMsg)
   101  			}
   102  			walletPassword, err := wallet.InputPassword(
   103  				cliCtx,
   104  				flags.WalletPasswordFileFlag,
   105  				wallet.PasswordPromptText,
   106  				false, /* Do not confirm password */
   107  				wallet.ValidateExistingPass,
   108  			)
   109  			if err != nil {
   110  				return nil, err
   111  			}
   112  			return wallet.OpenWallet(cliCtx.Context, &wallet.Config{
   113  				WalletDir:      walletDir,
   114  				WalletPassword: walletPassword,
   115  			})
   116  		}
   117  
   118  		cfg, err := extractWalletCreationConfigFromCli(cliCtx, keymanager.Imported)
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  		w := wallet.New(&wallet.Config{
   123  			KeymanagerKind: cfg.WalletCfg.KeymanagerKind,
   124  			WalletDir:      cfg.WalletCfg.WalletDir,
   125  			WalletPassword: cfg.WalletCfg.WalletPassword,
   126  		})
   127  		if err = createImportedKeymanagerWallet(cliCtx.Context, w); err != nil {
   128  			return nil, errors.Wrap(err, "could not create keymanager")
   129  		}
   130  		log.WithField("wallet-path", cfg.WalletCfg.WalletDir).Info(
   131  			"Successfully created new wallet",
   132  		)
   133  		return w, nil
   134  	})
   135  	if err != nil {
   136  		return errors.Wrap(err, "could not initialize wallet")
   137  	}
   138  
   139  	km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false})
   140  	if err != nil {
   141  		return err
   142  	}
   143  	k, ok := km.(*imported.Keymanager)
   144  	if !ok {
   145  		return errors.New("only imported wallets can import more keystores")
   146  	}
   147  
   148  	// Check if the user wishes to import a one-off, private key directly
   149  	// as an account into the Prysm validator.
   150  	if cliCtx.IsSet(flags.ImportPrivateKeyFileFlag.Name) {
   151  		return importPrivateKeyAsAccount(cliCtx, w, k)
   152  	}
   153  
   154  	keysDir, err := prompt.InputDirectory(cliCtx, prompt.ImportKeysDirPromptText, flags.KeysDirFlag)
   155  	if err != nil {
   156  		return errors.Wrap(err, "could not parse keys directory")
   157  	}
   158  	// Consider that the keysDir might be a path to a specific file and handle accordingly.
   159  	isDir, err := fileutil.HasDir(keysDir)
   160  	if err != nil {
   161  		return errors.Wrap(err, "could not determine if path is a directory")
   162  	}
   163  	keystoresImported := make([]*keymanager.Keystore, 0)
   164  	if isDir {
   165  		files, err := ioutil.ReadDir(keysDir)
   166  		if err != nil {
   167  			return errors.Wrap(err, "could not read dir")
   168  		}
   169  		if len(files) == 0 {
   170  			return fmt.Errorf("directory %s has no files, cannot import from it", keysDir)
   171  		}
   172  		filesInDir := make([]string, 0)
   173  		for i := 0; i < len(files); i++ {
   174  			if files[i].IsDir() {
   175  				continue
   176  			}
   177  			filesInDir = append(filesInDir, files[i].Name())
   178  		}
   179  		// Sort the imported keystores by derivation path if they
   180  		// specify this value in their filename.
   181  		sort.Sort(byDerivationPath(filesInDir))
   182  		for _, name := range filesInDir {
   183  			keystore, err := readKeystoreFile(cliCtx.Context, filepath.Join(keysDir, name))
   184  			if err != nil && strings.Contains(err.Error(), "could not decode keystore json") {
   185  				continue
   186  			} else if err != nil {
   187  				return errors.Wrapf(err, "could not import keystore at path: %s", name)
   188  			}
   189  			keystoresImported = append(keystoresImported, keystore)
   190  		}
   191  	} else {
   192  		keystore, err := readKeystoreFile(cliCtx.Context, keysDir)
   193  		if err != nil {
   194  			return errors.Wrap(err, "could not import keystore")
   195  		}
   196  		keystoresImported = append(keystoresImported, keystore)
   197  	}
   198  
   199  	var accountsPassword string
   200  	if cliCtx.IsSet(flags.AccountPasswordFileFlag.Name) {
   201  		passwordFilePath := cliCtx.String(flags.AccountPasswordFileFlag.Name)
   202  		data, err := ioutil.ReadFile(passwordFilePath)
   203  		if err != nil {
   204  			return err
   205  		}
   206  		accountsPassword = string(data)
   207  	} else {
   208  		accountsPassword, err = promptutil.PasswordPrompt(
   209  			"Enter the password for your imported accounts", promptutil.NotEmpty,
   210  		)
   211  		if err != nil {
   212  			return fmt.Errorf("could not read account password: %w", err)
   213  		}
   214  	}
   215  	fmt.Println("Importing accounts, this may take a while...")
   216  	if err := ImportAccounts(cliCtx.Context, &ImportAccountsConfig{
   217  		Keymanager:      k,
   218  		Keystores:       keystoresImported,
   219  		AccountPassword: accountsPassword,
   220  	}); err != nil {
   221  		return err
   222  	}
   223  	fmt.Printf(
   224  		"Successfully imported %s accounts, view all of them by running `accounts list`\n",
   225  		au.BrightMagenta(strconv.Itoa(len(keystoresImported))),
   226  	)
   227  	return nil
   228  }
   229  
   230  // ImportAccounts can import external, EIP-2335 compliant keystore.json files as
   231  // new accounts into the Prysm validator wallet.
   232  func ImportAccounts(ctx context.Context, cfg *ImportAccountsConfig) error {
   233  	return cfg.Keymanager.ImportKeystores(
   234  		ctx,
   235  		cfg.Keystores,
   236  		cfg.AccountPassword,
   237  	)
   238  }
   239  
   240  // Imports a one-off file containing a private key as a hex string into
   241  // the Prysm validator's accounts.
   242  func importPrivateKeyAsAccount(cliCtx *cli.Context, wallet *wallet.Wallet, km *imported.Keymanager) error {
   243  	privKeyFile := cliCtx.String(flags.ImportPrivateKeyFileFlag.Name)
   244  	fullPath, err := fileutil.ExpandPath(privKeyFile)
   245  	if err != nil {
   246  		return errors.Wrapf(err, "could not expand file path for %s", privKeyFile)
   247  	}
   248  	if !fileutil.FileExists(fullPath) {
   249  		return fmt.Errorf("file %s does not exist", fullPath)
   250  	}
   251  	privKeyHex, err := ioutil.ReadFile(fullPath)
   252  	if err != nil {
   253  		return errors.Wrapf(err, "could not read private key file at path %s", fullPath)
   254  	}
   255  	privKeyString := string(privKeyHex)
   256  	if len(privKeyString) > 2 && strings.Contains(privKeyString, "0x") {
   257  		privKeyString = privKeyString[2:] // Strip the 0x prefix, if any.
   258  	}
   259  	privKeyBytes, err := hex.DecodeString(strings.TrimRight(privKeyString, "\r\n"))
   260  	if err != nil {
   261  		return errors.Wrap(
   262  			err, "could not decode file as hex string, does the file contain a valid hex string?",
   263  		)
   264  	}
   265  	privKey, err := bls.SecretKeyFromBytes(privKeyBytes)
   266  	if err != nil {
   267  		return errors.Wrap(err, "not a valid BLS private key")
   268  	}
   269  	keystore, err := createKeystoreFromPrivateKey(privKey, wallet.Password())
   270  	if err != nil {
   271  		return errors.Wrap(err, "could not encrypt private key into a keystore file")
   272  	}
   273  	if err := ImportAccounts(
   274  		cliCtx.Context,
   275  		&ImportAccountsConfig{
   276  			Keymanager:      km,
   277  			AccountPassword: wallet.Password(),
   278  			Keystores:       []*keymanager.Keystore{keystore},
   279  		},
   280  	); err != nil {
   281  		return errors.Wrap(err, "could not import keystore into wallet")
   282  	}
   283  	fmt.Printf(
   284  		"Imported account with public key %#x, view all accounts by running `accounts list`\n",
   285  		au.BrightMagenta(bytesutil.Trunc(privKey.PublicKey().Marshal())),
   286  	)
   287  	return nil
   288  }
   289  
   290  func readKeystoreFile(_ context.Context, keystoreFilePath string) (*keymanager.Keystore, error) {
   291  	keystoreBytes, err := ioutil.ReadFile(keystoreFilePath)
   292  	if err != nil {
   293  		return nil, errors.Wrap(err, "could not read keystore file")
   294  	}
   295  	keystoreFile := &keymanager.Keystore{}
   296  	if err := json.Unmarshal(keystoreBytes, keystoreFile); err != nil {
   297  		return nil, errors.Wrap(err, "could not decode keystore json")
   298  	}
   299  	if keystoreFile.Pubkey == "" {
   300  		return nil, errors.New("could not decode keystore json")
   301  	}
   302  	return keystoreFile, nil
   303  }
   304  
   305  func createKeystoreFromPrivateKey(privKey bls.SecretKey, walletPassword string) (*keymanager.Keystore, error) {
   306  	encryptor := keystorev4.New()
   307  	id, err := uuid.NewRandom()
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  	cryptoFields, err := encryptor.Encrypt(privKey.Marshal(), walletPassword)
   312  	if err != nil {
   313  		return nil, errors.Wrapf(
   314  			err,
   315  			"could not encrypt private key with public key %#x",
   316  			privKey.PublicKey().Marshal(),
   317  		)
   318  	}
   319  	return &keymanager.Keystore{
   320  		Crypto:  cryptoFields,
   321  		ID:      id.String(),
   322  		Version: encryptor.Version(),
   323  		Pubkey:  fmt.Sprintf("%x", privKey.PublicKey().Marshal()),
   324  		Name:    encryptor.Name(),
   325  	}, nil
   326  }
   327  
   328  // Extracts the account index, j, from a derivation path in a file name
   329  // with the format m_12381_3600_j_0_0.
   330  func accountIndexFromFileName(derivationPath string) string {
   331  	derivationPath = derivationPath[13:]
   332  	accIndexEnd := strings.Index(derivationPath, "_")
   333  	return derivationPath[:accIndexEnd]
   334  }