Wallet recover CLI Manager migration (#11278)

* Wallet recover CLI Manager migration

* bazel run //:gazelle -- fix

* fix lint and build errors

* add TODO to remove duplicate code

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
This commit is contained in:
Mike Neuder
2022-08-24 12:57:03 -04:00
committed by GitHub
parent 606fdd2299
commit e99de7726d
11 changed files with 401 additions and 297 deletions

View File

@@ -4,6 +4,7 @@ go_library(
name = "go_default_library",
srcs = [
"edit.go",
"recover.go",
"wallet.go",
],
importpath = "github.com/prysmaticlabs/prysm/v3/cmd/validator/wallet",
@@ -12,6 +13,7 @@ go_library(
"//cmd:go_default_library",
"//cmd/validator/flags:go_default_library",
"//config/features:go_default_library",
"//io/prompt:go_default_library",
"//runtime/tos:go_default_library",
"//validator/accounts:go_default_library",
"//validator/accounts/userprompt:go_default_library",
@@ -20,13 +22,18 @@ go_library(
"//validator/keymanager/remote:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_tyler_smith_go_bip39//:go_default_library",
"@com_github_tyler_smith_go_bip39//wordlists:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["edit_test.go"],
srcs = [
"edit_test.go",
"recover_test.go",
],
embed = [":go_default_library"],
deps = [
"//cmd/validator/flags:go_default_library",
@@ -34,8 +41,10 @@ go_test(
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"//validator/accounts:go_default_library",
"//validator/accounts/iface:go_default_library",
"//validator/accounts/wallet:go_default_library",
"//validator/keymanager:go_default_library",
"//validator/keymanager/derived:go_default_library",
"//validator/keymanager/remote:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
],

View File

@@ -0,0 +1,175 @@
package wallet
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v3/cmd/validator/flags"
"github.com/prysmaticlabs/prysm/v3/io/prompt"
"github.com/prysmaticlabs/prysm/v3/validator/accounts"
"github.com/prysmaticlabs/prysm/v3/validator/accounts/userprompt"
"github.com/prysmaticlabs/prysm/v3/validator/accounts/wallet"
"github.com/tyler-smith/go-bip39"
"github.com/tyler-smith/go-bip39/wordlists"
"github.com/urfave/cli/v2"
)
const (
// #nosec G101 -- Not sensitive data
mnemonicPassphraseYesNoText = "(Advanced) Do you have an optional '25th word' passphrase for your mnemonic? [y/n]"
// #nosec G101 -- Not sensitive data
mnemonicPassphrasePromptText = "(Advanced) Enter the '25th word' passphrase for your mnemonic"
)
func walletRecover(c *cli.Context) error {
mnemonic, err := inputMnemonic(c)
if err != nil {
return errors.Wrap(err, "could not get mnemonic phrase")
}
opts := []accounts.Option{
accounts.WithMnemonic(mnemonic),
}
skipMnemonic25thWord := c.IsSet(flags.SkipMnemonic25thWordCheckFlag.Name)
has25thWordFile := c.IsSet(flags.Mnemonic25thWordFileFlag.Name)
if !skipMnemonic25thWord && !has25thWordFile {
resp, err := prompt.ValidatePrompt(
os.Stdin, mnemonicPassphraseYesNoText, prompt.ValidateYesOrNo,
)
if err != nil {
return errors.Wrap(err, "could not validate choice")
}
if strings.EqualFold(resp, "y") {
mnemonicPassphrase, err := prompt.InputPassword(
c,
flags.Mnemonic25thWordFileFlag,
mnemonicPassphrasePromptText,
"Confirm mnemonic passphrase",
false, /* Should confirm password */
func(input string) error {
if strings.TrimSpace(input) == "" {
return errors.New("input cannot be empty")
}
return nil
},
)
if err != nil {
return err
}
opts = append(opts, accounts.WithMnemonic25thWord(mnemonicPassphrase))
}
}
walletDir, err := userprompt.InputDirectory(c, userprompt.WalletDirPromptText, flags.WalletDirFlag)
if err != nil {
return err
}
walletPassword, err := prompt.InputPassword(
c,
flags.WalletPasswordFileFlag,
wallet.NewWalletPasswordPromptText,
wallet.ConfirmPasswordPromptText,
true, /* Should confirm password */
prompt.ValidatePasswordInput,
)
if err != nil {
return err
}
numAccounts, err := inputNumAccounts(c)
if err != nil {
return errors.Wrap(err, "could not get number of accounts to recover")
}
opts = append(opts, accounts.WithWalletDir(walletDir))
opts = append(opts, accounts.WithWalletPassword(walletPassword))
opts = append(opts, accounts.WithNumAccounts(int(numAccounts)))
acc, err := accounts.NewCLIManager(opts...)
if err != nil {
return err
}
if _, err = acc.WalletRecover(c.Context); err != nil {
return err
}
log.Infof(
"Successfully recovered HD wallet with accounts and saved configuration to disk",
)
return nil
}
func inputMnemonic(cliCtx *cli.Context) (mnemonicPhrase string, err error) {
if cliCtx.IsSet(flags.MnemonicFileFlag.Name) {
mnemonicFilePath := cliCtx.String(flags.MnemonicFileFlag.Name)
data, err := os.ReadFile(mnemonicFilePath) // #nosec G304 -- ReadFile is safe
if err != nil {
return "", err
}
enteredMnemonic := string(data)
if err := accounts.ValidateMnemonic(enteredMnemonic); err != nil {
return "", errors.Wrap(err, "mnemonic phrase did not pass validation")
}
return enteredMnemonic, nil
}
allowedLanguages := map[string][]string{
"chinese_simplified": wordlists.ChineseSimplified,
"chinese_traditional": wordlists.ChineseTraditional,
"czech": wordlists.Czech,
"english": wordlists.English,
"french": wordlists.French,
"japanese": wordlists.Japanese,
"korean": wordlists.Korean,
"italian": wordlists.Italian,
"spanish": wordlists.Spanish,
}
languages := make([]string, 0)
for k := range allowedLanguages {
languages = append(languages, k)
}
sort.Strings(languages)
selectedLanguage, err := prompt.ValidatePrompt(
os.Stdin,
fmt.Sprintf("Enter the language of your seed phrase: %s", strings.Join(languages, ", ")),
func(input string) error {
if _, ok := allowedLanguages[input]; !ok {
return errors.New("input not in the list of allowed languages")
}
return nil
},
)
if err != nil {
return "", fmt.Errorf("could not get mnemonic language: %w", err)
}
bip39.SetWordList(allowedLanguages[selectedLanguage])
mnemonicPhrase, err = prompt.ValidatePrompt(
os.Stdin,
"Enter the seed phrase for the wallet you would like to recover",
accounts.ValidateMnemonic)
if err != nil {
return "", fmt.Errorf("could not get mnemonic phrase: %w", err)
}
return mnemonicPhrase, nil
}
func inputNumAccounts(cliCtx *cli.Context) (int64, error) {
if cliCtx.IsSet(flags.NumAccountsFlag.Name) {
numAccounts := cliCtx.Int64(flags.NumAccountsFlag.Name)
if numAccounts <= 0 {
return 0, errors.New("must recover at least 1 account")
}
return numAccounts, nil
}
numAccounts, err := prompt.ValidatePrompt(os.Stdin, "Enter how many accounts you would like to generate from the mnemonic", prompt.ValidateNumber)
if err != nil {
return 0, err
}
numAccountsInt, err := strconv.Atoi(numAccounts)
if err != nil {
return 0, err
}
if numAccountsInt <= 0 {
return 0, errors.New("must recover at least 1 account")
}
return int64(numAccountsInt), nil
}

View File

@@ -0,0 +1,113 @@
package wallet
import (
"context"
"flag"
"os"
"path/filepath"
"strconv"
"testing"
"github.com/prysmaticlabs/prysm/v3/cmd/validator/flags"
"github.com/prysmaticlabs/prysm/v3/testing/assert"
"github.com/prysmaticlabs/prysm/v3/testing/require"
"github.com/prysmaticlabs/prysm/v3/validator/accounts/iface"
"github.com/prysmaticlabs/prysm/v3/validator/accounts/wallet"
"github.com/prysmaticlabs/prysm/v3/validator/keymanager"
"github.com/prysmaticlabs/prysm/v3/validator/keymanager/derived"
"github.com/urfave/cli/v2"
)
const (
walletDirName = "wallet"
mnemonicFileName = "mnemonic.txt"
mnemonic = "garage car helmet trade salmon embrace market giant movie wet same champion dawn chair shield drill amazing panther accident puzzle garden mosquito kind arena"
)
type recoverCfgStruct struct {
walletDir string
passwordFilePath string
mnemonicFilePath string
numAccounts int64
}
func setupRecoverCfg(t *testing.T) *recoverCfgStruct {
testDir := t.TempDir()
walletDir := filepath.Join(testDir, walletDirName)
passwordFilePath := filepath.Join(testDir, passwordFileName)
require.NoError(t, os.WriteFile(passwordFilePath, []byte(password), os.ModePerm))
mnemonicFilePath := filepath.Join(testDir, mnemonicFileName)
require.NoError(t, os.WriteFile(mnemonicFilePath, []byte(mnemonic), os.ModePerm))
return &recoverCfgStruct{
walletDir: walletDir,
passwordFilePath: passwordFilePath,
mnemonicFilePath: mnemonicFilePath,
}
}
func createRecoverCliCtx(t *testing.T, cfg *recoverCfgStruct) *cli.Context {
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String(flags.WalletDirFlag.Name, cfg.walletDir, "")
set.String(flags.WalletPasswordFileFlag.Name, cfg.passwordFilePath, "")
set.String(flags.KeymanagerKindFlag.Name, keymanager.Derived.String(), "")
set.String(flags.MnemonicFileFlag.Name, cfg.mnemonicFilePath, "")
set.Bool(flags.SkipMnemonic25thWordCheckFlag.Name, true, "")
set.Int64(flags.NumAccountsFlag.Name, cfg.numAccounts, "")
assert.NoError(t, set.Set(flags.SkipMnemonic25thWordCheckFlag.Name, "true"))
assert.NoError(t, set.Set(flags.WalletDirFlag.Name, cfg.walletDir))
assert.NoError(t, set.Set(flags.WalletPasswordFileFlag.Name, cfg.passwordFilePath))
assert.NoError(t, set.Set(flags.KeymanagerKindFlag.Name, keymanager.Derived.String()))
assert.NoError(t, set.Set(flags.MnemonicFileFlag.Name, cfg.mnemonicFilePath))
assert.NoError(t, set.Set(flags.NumAccountsFlag.Name, strconv.Itoa(int(cfg.numAccounts))))
return cli.NewContext(&app, set, nil)
}
func TestRecoverDerivedWallet(t *testing.T) {
cfg := setupRecoverCfg(t)
cfg.numAccounts = 4
cliCtx := createRecoverCliCtx(t, cfg)
require.NoError(t, walletRecover(cliCtx))
ctx := context.Background()
w, err := wallet.OpenWallet(cliCtx.Context, &wallet.Config{
WalletDir: cfg.walletDir,
WalletPassword: password,
})
assert.NoError(t, err)
km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false})
require.NoError(t, err)
derivedKM, ok := km.(*derived.Keymanager)
if !ok {
t.Fatal("not a derived keymanager")
}
names, err := derivedKM.ValidatingAccountNames(ctx)
assert.NoError(t, err)
require.Equal(t, len(names), int(cfg.numAccounts))
}
// TestRecoverDerivedWallet_OneAccount is a test for regression in cases where the number of accounts recovered is 1
func TestRecoverDerivedWallet_OneAccount(t *testing.T) {
cfg := setupRecoverCfg(t)
cfg.numAccounts = 1
cliCtx := createRecoverCliCtx(t, cfg)
require.NoError(t, walletRecover(cliCtx))
_, err := wallet.OpenWallet(cliCtx.Context, &wallet.Config{
WalletDir: cfg.walletDir,
WalletPassword: password,
})
assert.NoError(t, err)
}
func TestRecoverDerivedWallet_AlreadyExists(t *testing.T) {
cfg := setupRecoverCfg(t)
cfg.numAccounts = 4
cliCtx := createRecoverCliCtx(t, cfg)
require.NoError(t, walletRecover(cliCtx))
// Trying to recover an HD wallet into a directory that already exists should give an error
require.ErrorContains(t, "a wallet already exists at this location", walletRecover(cliCtx))
}

View File

@@ -108,13 +108,13 @@ var Commands = &cli.Command{
if err := cmd.LoadFlagsFromConfig(cliCtx, cliCtx.Command.Flags); err != nil {
return err
}
return tos.VerifyTosAcceptedOrPrompt(cliCtx)
},
Action: func(cliCtx *cli.Context) error {
if err := features.ConfigureValidator(cliCtx); err != nil {
if err := tos.VerifyTosAcceptedOrPrompt(cliCtx); err != nil {
return err
}
if err := accounts.RecoverWalletCli(cliCtx); err != nil {
return features.ConfigureBeaconChain(cliCtx)
},
Action: func(cliCtx *cli.Context) error {
if err := walletRecover(cliCtx); err != nil {
log.WithError(err).Fatal("Could not recover wallet")
}
return nil