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

     1  package accounts
     2  
     3  import (
     4  	"archive/zip"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/logrusorgru/aurora"
    13  	"github.com/manifoldco/promptui"
    14  	"github.com/pkg/errors"
    15  	"github.com/prysmaticlabs/prysm/cmd/validator/flags"
    16  	"github.com/prysmaticlabs/prysm/shared/bls"
    17  	"github.com/prysmaticlabs/prysm/shared/bytesutil"
    18  	"github.com/prysmaticlabs/prysm/shared/fileutil"
    19  	"github.com/prysmaticlabs/prysm/shared/petnames"
    20  	"github.com/prysmaticlabs/prysm/shared/promptutil"
    21  	"github.com/prysmaticlabs/prysm/validator/accounts/iface"
    22  	"github.com/prysmaticlabs/prysm/validator/accounts/prompt"
    23  	"github.com/prysmaticlabs/prysm/validator/accounts/wallet"
    24  	"github.com/prysmaticlabs/prysm/validator/keymanager"
    25  	"github.com/prysmaticlabs/prysm/validator/keymanager/derived"
    26  	"github.com/prysmaticlabs/prysm/validator/keymanager/imported"
    27  	"github.com/urfave/cli/v2"
    28  )
    29  
    30  var (
    31  	au = aurora.NewAurora(true)
    32  )
    33  
    34  const (
    35  	allAccountsText  = "All accounts"
    36  	archiveFilename  = "backup.zip"
    37  	backupPromptText = "Enter the directory where your backup.zip file will be written to"
    38  )
    39  
    40  // BackupAccountsCli allows users to select validator accounts from their wallet
    41  // and export them as a backup.zip file containing the keys as EIP-2335 compliant
    42  // keystore.json files, which are compatible with importing in other Ethereum consensus clients.
    43  func BackupAccountsCli(cliCtx *cli.Context) error {
    44  	w, err := wallet.OpenWalletOrElseCli(cliCtx, func(cliCtx *cli.Context) (*wallet.Wallet, error) {
    45  		return nil, wallet.ErrNoWalletFound
    46  	})
    47  	if err != nil {
    48  		return errors.Wrap(err, "could not initialize wallet")
    49  	}
    50  	if w.KeymanagerKind() == keymanager.Remote {
    51  		return errors.New(
    52  			"remote wallets cannot backup accounts",
    53  		)
    54  	}
    55  	km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false})
    56  	if err != nil {
    57  		return errors.Wrap(err, ErrCouldNotInitializeKeymanager)
    58  	}
    59  	pubKeys, err := km.FetchValidatingPublicKeys(cliCtx.Context)
    60  	if err != nil {
    61  		return errors.Wrap(err, "could not fetch validating public keys")
    62  	}
    63  
    64  	// Input the directory where they wish to backup their accounts.
    65  	backupDir, err := prompt.InputDirectory(cliCtx, backupPromptText, flags.BackupDirFlag)
    66  	if err != nil {
    67  		return errors.Wrap(err, "could not parse keys directory")
    68  	}
    69  
    70  	// Allow the user to interactively select the accounts to backup or optionally
    71  	// provide them via cli flags as a string of comma-separated, hex strings.
    72  	filteredPubKeys, err := filterPublicKeysFromUserInput(
    73  		cliCtx,
    74  		flags.BackupPublicKeysFlag,
    75  		pubKeys,
    76  		prompt.SelectAccountsBackupPromptText,
    77  	)
    78  	if err != nil {
    79  		return errors.Wrap(err, "could not filter public keys for backup")
    80  	}
    81  
    82  	// Ask the user for their desired password for their backed up accounts.
    83  	backupsPassword, err := promptutil.InputPassword(
    84  		cliCtx,
    85  		flags.BackupPasswordFile,
    86  		"Enter a new password for your backed up accounts",
    87  		"Confirm new password",
    88  		true,
    89  		promptutil.ValidatePasswordInput,
    90  	)
    91  	if err != nil {
    92  		return errors.Wrap(err, "could not determine password for backed up accounts")
    93  	}
    94  
    95  	var keystoresToBackup []*keymanager.Keystore
    96  	switch w.KeymanagerKind() {
    97  	case keymanager.Imported:
    98  		km, ok := km.(*imported.Keymanager)
    99  		if !ok {
   100  			return errors.New("could not assert keymanager interface to concrete type")
   101  		}
   102  		keystoresToBackup, err = km.ExtractKeystores(cliCtx.Context, filteredPubKeys, backupsPassword)
   103  		if err != nil {
   104  			return errors.Wrap(err, "could not backup accounts for imported keymanager")
   105  		}
   106  	case keymanager.Derived:
   107  		km, ok := km.(*derived.Keymanager)
   108  		if !ok {
   109  			return errors.New("could not assert keymanager interface to concrete type")
   110  		}
   111  		keystoresToBackup, err = km.ExtractKeystores(cliCtx.Context, filteredPubKeys, backupsPassword)
   112  		if err != nil {
   113  			return errors.Wrap(err, "could not backup accounts for derived keymanager")
   114  		}
   115  	case keymanager.Remote:
   116  		return errors.New("backing up keys is not supported for a remote keymanager")
   117  	default:
   118  		return fmt.Errorf(errKeymanagerNotSupported, w.KeymanagerKind())
   119  	}
   120  	return zipKeystoresToOutputDir(keystoresToBackup, backupDir)
   121  }
   122  
   123  // Ask user to select accounts via an interactive prompt.
   124  func selectAccounts(selectionPrompt string, pubKeys [][48]byte) (filteredPubKeys []bls.PublicKey, err error) {
   125  	pubKeyStrings := make([]string, len(pubKeys))
   126  	for i, pk := range pubKeys {
   127  		name := petnames.DeterministicName(pk[:], "-")
   128  		pubKeyStrings[i] = fmt.Sprintf(
   129  			"%d | %s | %#x", i, au.BrightGreen(name), au.BrightMagenta(bytesutil.Trunc(pk[:])),
   130  		)
   131  	}
   132  	templates := &promptui.SelectTemplates{
   133  		Label:    "{{ . }}",
   134  		Active:   "\U0001F336 {{ .Name | cyan }}",
   135  		Inactive: "  {{ .Name | cyan }}",
   136  		Selected: "\U0001F336 {{ .Name | red | cyan }}",
   137  		Details: `
   138  --------- Account ----------
   139  {{ "Name:" | faint }}	{{ .Name }}`,
   140  	}
   141  	var result string
   142  	exit := "Done selecting"
   143  	results := make([]int, 0)
   144  	au := aurora.NewAurora(true)
   145  	for result != exit {
   146  		p := promptui.Select{
   147  			Label:        selectionPrompt,
   148  			HideSelected: true,
   149  			Items:        append([]string{exit, allAccountsText}, pubKeyStrings...),
   150  			Templates:    templates,
   151  		}
   152  
   153  		_, result, err = p.Run()
   154  		if err != nil {
   155  			return nil, err
   156  		}
   157  		if result == exit {
   158  			fmt.Printf("%s\n", au.BrightRed("Done with selections").Bold())
   159  			break
   160  		}
   161  		if result == allAccountsText {
   162  			fmt.Printf("%s\n", au.BrightRed("[Selected all accounts]").Bold())
   163  			for i := 0; i < len(pubKeys); i++ {
   164  				results = append(results, i)
   165  			}
   166  			break
   167  		}
   168  		idx := strings.Index(result, " |")
   169  		accountIndexStr := result[:idx]
   170  		accountIndex, err := strconv.Atoi(accountIndexStr)
   171  		if err != nil {
   172  			return nil, err
   173  		}
   174  		results = append(results, accountIndex)
   175  		fmt.Printf("%s %s\n", au.BrightRed("[Selected account]").Bold(), result)
   176  	}
   177  
   178  	// Deduplicate the results.
   179  	seen := make(map[int]bool)
   180  	for i := 0; i < len(results); i++ {
   181  		if _, ok := seen[results[i]]; !ok {
   182  			seen[results[i]] = true
   183  		}
   184  	}
   185  
   186  	// Filter the public keys based on user input.
   187  	filteredPubKeys = make([]bls.PublicKey, 0)
   188  	for selectedIndex := range seen {
   189  		pk, err := bls.PublicKeyFromBytes(pubKeys[selectedIndex][:])
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  		filteredPubKeys = append(filteredPubKeys, pk)
   194  	}
   195  	return filteredPubKeys, nil
   196  }
   197  
   198  // Zips a list of keystore into respective EIP-2335 keystore.json files and
   199  // writes their zipped format into the specified output directory.
   200  func zipKeystoresToOutputDir(keystoresToBackup []*keymanager.Keystore, outputDir string) error {
   201  	if len(keystoresToBackup) == 0 {
   202  		return errors.New("nothing to backup")
   203  	}
   204  	if err := fileutil.MkdirAll(outputDir); err != nil {
   205  		return errors.Wrapf(err, "could not create directory at path: %s", outputDir)
   206  	}
   207  	// Marshal and zip all keystore files together and write the zip file
   208  	// to the specified output directory.
   209  	archivePath := filepath.Join(outputDir, archiveFilename)
   210  	if fileutil.FileExists(archivePath) {
   211  		return errors.Errorf("Zip file already exists in directory: %s", archivePath)
   212  	}
   213  	// We create a new file to store our backup.zip.
   214  	zipfile, err := os.Create(archivePath)
   215  	if err != nil {
   216  		return errors.Wrapf(err, "could not create zip file with path: %s", archivePath)
   217  	}
   218  	defer func() {
   219  		if err := zipfile.Close(); err != nil {
   220  			log.WithError(err).Error("Could not close zipfile")
   221  		}
   222  	}()
   223  	// Using this zip file, we create a new zip writer which we write
   224  	// files to directly from our marshaled keystores.
   225  	writer := zip.NewWriter(zipfile)
   226  	defer func() {
   227  		// We close the zip writer when done.
   228  		if err := writer.Close(); err != nil {
   229  			log.WithError(err).Error("Could not close zip file after writing")
   230  		}
   231  	}()
   232  	for i, k := range keystoresToBackup {
   233  		encodedFile, err := json.MarshalIndent(k, "", "\t")
   234  		if err != nil {
   235  			return errors.Wrap(err, "could not marshal keystore to JSON file")
   236  		}
   237  		f, err := writer.Create(fmt.Sprintf("keystore-%d.json", i))
   238  		if err != nil {
   239  			return errors.Wrap(err, "could not write keystore file to zip")
   240  		}
   241  		if _, err = f.Write(encodedFile); err != nil {
   242  			return errors.Wrap(err, "could not write keystore file contents")
   243  		}
   244  	}
   245  	log.WithField(
   246  		"backup-path", archivePath,
   247  	).Infof("Successfully backed up %d accounts", len(keystoresToBackup))
   248  	return nil
   249  }