diff --git a/cmd/validator/accounts/BUILD.bazel b/cmd/validator/accounts/BUILD.bazel index aa99b73d58..8b810949a4 100644 --- a/cmd/validator/accounts/BUILD.bazel +++ b/cmd/validator/accounts/BUILD.bazel @@ -7,6 +7,7 @@ go_library( "backup.go", "delete.go", "exit.go", + "import.go", "list.go", "wallet_utils.go", ], @@ -36,6 +37,7 @@ go_test( "backup_test.go", "delete_test.go", "exit_test.go", + "import_test.go", ], embed = [":go_default_library"], deps = [ diff --git a/cmd/validator/accounts/accounts.go b/cmd/validator/accounts/accounts.go index f7f8636be3..5df0f3de48 100644 --- a/cmd/validator/accounts/accounts.go +++ b/cmd/validator/accounts/accounts.go @@ -7,7 +7,6 @@ import ( "github.com/prysmaticlabs/prysm/cmd/validator/flags" "github.com/prysmaticlabs/prysm/config/features" "github.com/prysmaticlabs/prysm/runtime/tos" - "github.com/prysmaticlabs/prysm/validator/accounts" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -139,13 +138,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.ImportAccountsCli(cliCtx); err != nil { + return features.ConfigureValidator(cliCtx) + }, + Action: func(cliCtx *cli.Context) error { + if err := accountsImport(cliCtx); err != nil { log.Fatalf("Could not import accounts: %v", err) } return nil diff --git a/cmd/validator/accounts/backup_test.go b/cmd/validator/accounts/backup_test.go index 8d131dea59..dd7cccd1f3 100644 --- a/cmd/validator/accounts/backup_test.go +++ b/cmd/validator/accounts/backup_test.go @@ -181,7 +181,7 @@ func TestBackupAccounts_Noninteractive_Imported(t *testing.T) { // We attempt to import accounts we wrote to the keys directory // into our newly created wallet. - require.NoError(t, accounts.ImportAccountsCli(cliCtx)) + require.NoError(t, accountsImport(cliCtx)) // Next, we attempt to backup the accounts. require.NoError(t, accountsBackup(cliCtx)) diff --git a/cmd/validator/accounts/delete_test.go b/cmd/validator/accounts/delete_test.go index 416aa75c26..807cb86f1b 100644 --- a/cmd/validator/accounts/delete_test.go +++ b/cmd/validator/accounts/delete_test.go @@ -83,6 +83,7 @@ type testWalletConfig struct { deletePublicKeys string keysDir string backupDir string + passwordsDir string walletDir string } @@ -169,7 +170,7 @@ func TestDeleteAccounts_Noninteractive(t *testing.T) { require.NoError(t, err) // We attempt to import accounts. - require.NoError(t, accounts.ImportAccountsCli(cliCtx)) + require.NoError(t, accountsImport(cliCtx)) // We attempt to delete the accounts specified. require.NoError(t, accountsDelete(cliCtx)) diff --git a/cmd/validator/accounts/exit_test.go b/cmd/validator/accounts/exit_test.go index 95447577bf..1b876b09d7 100644 --- a/cmd/validator/accounts/exit_test.go +++ b/cmd/validator/accounts/exit_test.go @@ -75,7 +75,7 @@ func TestExitAccountsCli_OK(t *testing.T) { }, }) require.NoError(t, err) - require.NoError(t, accounts.ImportAccountsCli(cliCtx)) + require.NoError(t, accountsImport(cliCtx)) _, keymanager, err := walletWithKeymanager(cliCtx) require.NoError(t, err) @@ -175,7 +175,7 @@ func TestExitAccountsCli_OK_AllPublicKeys(t *testing.T) { }, }) require.NoError(t, err) - require.NoError(t, accounts.ImportAccountsCli(cliCtx)) + require.NoError(t, accountsImport(cliCtx)) _, keymanager, err := walletWithKeymanager(cliCtx) require.NoError(t, err) diff --git a/cmd/validator/accounts/import.go b/cmd/validator/accounts/import.go new file mode 100644 index 0000000000..8d56dc38d2 --- /dev/null +++ b/cmd/validator/accounts/import.go @@ -0,0 +1,113 @@ +package accounts + +import ( + "strings" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/cmd" + "github.com/prysmaticlabs/prysm/cmd/validator/flags" + "github.com/prysmaticlabs/prysm/validator/accounts" + "github.com/prysmaticlabs/prysm/validator/accounts/iface" + "github.com/prysmaticlabs/prysm/validator/accounts/userprompt" + "github.com/prysmaticlabs/prysm/validator/accounts/wallet" + "github.com/prysmaticlabs/prysm/validator/client" + "github.com/prysmaticlabs/prysm/validator/keymanager" + "github.com/urfave/cli/v2" +) + +func accountsImport(c *cli.Context) error { + w, err := walletImport(c) + if err != nil { + return errors.Wrap(err, "could not initialize wallet") + } + km, err := w.InitializeKeymanager(c.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) + if err != nil { + return err + } + + dialOpts := client.ConstructDialOptions( + c.Int(cmd.GrpcMaxCallRecvMsgSizeFlag.Name), + c.String(flags.CertFlag.Name), + c.Uint(flags.GrpcRetriesFlag.Name), + c.Duration(flags.GrpcRetryDelayFlag.Name), + ) + grpcHeaders := strings.Split(c.String(flags.GrpcHeadersFlag.Name), ",") + + opts := []accounts.Option{ + accounts.WithWallet(w), + accounts.WithKeymanager(km), + accounts.WithGRPCDialOpts(dialOpts), + accounts.WithBeaconRPCProvider(c.String(flags.BeaconRPCProviderFlag.Name)), + accounts.WithGRPCHeaders(grpcHeaders), + } + + opts = append(opts, accounts.WithImportPrivateKeys(c.IsSet(flags.ImportPrivateKeyFileFlag.Name))) + opts = append(opts, accounts.WithPrivateKeyFile(c.String(flags.ImportPrivateKeyFileFlag.Name))) + opts = append(opts, accounts.WithReadPasswordFile(c.IsSet(flags.AccountPasswordFileFlag.Name))) + opts = append(opts, accounts.WithPasswordFilePath(c.String(flags.AccountPasswordFileFlag.Name))) + + keysDir, err := userprompt.InputDirectory(c, userprompt.ImportKeysDirPromptText, flags.KeysDirFlag) + if err != nil { + return errors.Wrap(err, "could not parse keys directory") + } + opts = append(opts, accounts.WithKeysDir(keysDir)) + + acc, err := accounts.NewCLIManager(opts...) + if err != nil { + return err + } + return acc.Import(c.Context) +} + +func walletImport(c *cli.Context) (*wallet.Wallet, error) { + return wallet.OpenWalletOrElseCli(c, func(cliCtx *cli.Context) (*wallet.Wallet, error) { + walletDir, err := userprompt.InputDirectory(cliCtx, userprompt.WalletDirPromptText, flags.WalletDirFlag) + if err != nil { + return nil, err + } + exists, err := wallet.Exists(walletDir) + if err != nil { + return nil, errors.Wrap(err, wallet.CheckExistsErrMsg) + } + if exists { + isValid, err := wallet.IsValid(walletDir) + if err != nil { + return nil, errors.Wrap(err, wallet.CheckValidityErrMsg) + } + if !isValid { + return nil, errors.New(wallet.InvalidWalletErrMsg) + } + walletPassword, err := wallet.InputPassword( + cliCtx, + flags.WalletPasswordFileFlag, + wallet.PasswordPromptText, + false, /* Do not confirm password */ + wallet.ValidateExistingPass, + ) + if err != nil { + return nil, err + } + return wallet.OpenWallet(cliCtx.Context, &wallet.Config{ + WalletDir: walletDir, + WalletPassword: walletPassword, + }) + } + + cfg, err := accounts.ExtractWalletCreationConfigFromCli(cliCtx, keymanager.Local) + if err != nil { + return nil, err + } + w := wallet.New(&wallet.Config{ + KeymanagerKind: cfg.WalletCfg.KeymanagerKind, + WalletDir: cfg.WalletCfg.WalletDir, + WalletPassword: cfg.WalletCfg.WalletPassword, + }) + if err = accounts.CreateLocalKeymanagerWallet(cliCtx.Context, w); err != nil { + return nil, errors.Wrap(err, "could not create keymanager") + } + log.WithField("wallet-path", cfg.WalletCfg.WalletDir).Info( + "Successfully created new wallet", + ) + return w, nil + }) +} diff --git a/cmd/validator/accounts/import_test.go b/cmd/validator/accounts/import_test.go new file mode 100644 index 0000000000..6b67afdda6 --- /dev/null +++ b/cmd/validator/accounts/import_test.go @@ -0,0 +1,262 @@ +package accounts + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/google/uuid" + "github.com/prysmaticlabs/prysm/crypto/bls" + "github.com/prysmaticlabs/prysm/testing/assert" + "github.com/prysmaticlabs/prysm/testing/require" + "github.com/prysmaticlabs/prysm/validator/accounts" + "github.com/prysmaticlabs/prysm/validator/accounts/iface" + "github.com/prysmaticlabs/prysm/validator/accounts/wallet" + "github.com/prysmaticlabs/prysm/validator/keymanager" + "github.com/prysmaticlabs/prysm/validator/keymanager/local" + keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" +) + +func TestImport_Noninteractive(t *testing.T) { + local.ResetCaches() + walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t) + keysDir := filepath.Join(t.TempDir(), "keysDir") + require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) + + cliCtx := setupWalletCtx(t, &testWalletConfig{ + walletDir: walletDir, + passwordsDir: passwordsDir, + keysDir: keysDir, + keymanagerKind: keymanager.Local, + walletPasswordFile: passwordFilePath, + accountPasswordFile: passwordFilePath, + }) + w, err := accounts.CreateWalletWithKeymanager(cliCtx.Context, &accounts.CreateWalletConfig{ + WalletCfg: &wallet.Config{ + WalletDir: walletDir, + KeymanagerKind: keymanager.Local, + WalletPassword: password, + }, + }) + require.NoError(t, err) + keymanager, err := local.NewKeymanager( + cliCtx.Context, + &local.SetupConfig{ + Wallet: w, + ListenForChanges: false, + }, + ) + require.NoError(t, err) + + // Make sure there are no accounts at the start. + accounts, err := keymanager.ValidatingAccountNames() + require.NoError(t, err) + assert.Equal(t, len(accounts), 0) + + // Create 2 keys. + createKeystore(t, keysDir) + time.Sleep(time.Second) + createKeystore(t, keysDir) + + require.NoError(t, accountsImport(cliCtx)) + + w, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{ + WalletDir: walletDir, + WalletPassword: password, + }) + require.NoError(t, err) + km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) + require.NoError(t, err) + keys, err := km.FetchValidatingPublicKeys(cliCtx.Context) + require.NoError(t, err) + + assert.Equal(t, 2, len(keys)) +} + +// TestImport_DuplicateKeys is a regression test that ensures correction function if duplicate keys are being imported +func TestImport_DuplicateKeys(t *testing.T) { + local.ResetCaches() + walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t) + keysDir := filepath.Join(t.TempDir(), "keysDir") + require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) + + cliCtx := setupWalletCtx(t, &testWalletConfig{ + walletDir: walletDir, + passwordsDir: passwordsDir, + keysDir: keysDir, + keymanagerKind: keymanager.Local, + walletPasswordFile: passwordFilePath, + accountPasswordFile: passwordFilePath, + }) + w, err := accounts.CreateWalletWithKeymanager(cliCtx.Context, &accounts.CreateWalletConfig{ + WalletCfg: &wallet.Config{ + WalletDir: walletDir, + KeymanagerKind: keymanager.Local, + WalletPassword: password, + }, + }) + require.NoError(t, err) + + // Create a key and then copy it to create a duplicate + _, keystorePath := createKeystore(t, keysDir) + time.Sleep(time.Second) + input, err := os.ReadFile(keystorePath) + require.NoError(t, err) + keystorePath2 := filepath.Join(keysDir, "copyOfKeystore.json") + err = os.WriteFile(keystorePath2, input, os.ModePerm) + require.NoError(t, err) + + require.NoError(t, accountsImport(cliCtx)) + + _, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{ + WalletDir: walletDir, + WalletPassword: password, + }) + require.NoError(t, err) + km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) + require.NoError(t, err) + keys, err := km.FetchValidatingPublicKeys(cliCtx.Context) + require.NoError(t, err) + + // There should only be 1 account as the duplicate keystore was ignored + assert.Equal(t, 1, len(keys)) +} + +func TestImport_Noninteractive_RandomName(t *testing.T) { + local.ResetCaches() + walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t) + keysDir := filepath.Join(t.TempDir(), "keysDir") + require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) + + cliCtx := setupWalletCtx(t, &testWalletConfig{ + walletDir: walletDir, + passwordsDir: passwordsDir, + keysDir: keysDir, + keymanagerKind: keymanager.Local, + walletPasswordFile: passwordFilePath, + accountPasswordFile: passwordFilePath, + }) + w, err := accounts.CreateWalletWithKeymanager(cliCtx.Context, &accounts.CreateWalletConfig{ + WalletCfg: &wallet.Config{ + WalletDir: walletDir, + KeymanagerKind: keymanager.Local, + WalletPassword: password, + }, + }) + require.NoError(t, err) + keymanager, err := local.NewKeymanager( + cliCtx.Context, + &local.SetupConfig{ + Wallet: w, + ListenForChanges: false, + }, + ) + require.NoError(t, err) + + // Make sure there are no accounts at the start. + accounts, err := keymanager.ValidatingAccountNames() + require.NoError(t, err) + assert.Equal(t, len(accounts), 0) + + // Create 2 keys. + createRandomNameKeystore(t, keysDir) + time.Sleep(time.Second) + createRandomNameKeystore(t, keysDir) + + require.NoError(t, accountsImport(cliCtx)) + + w, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{ + WalletDir: walletDir, + WalletPassword: password, + }) + require.NoError(t, err) + km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) + require.NoError(t, err) + keys, err := km.FetchValidatingPublicKeys(cliCtx.Context) + require.NoError(t, err) + + assert.Equal(t, 2, len(keys)) +} + +// Returns the fullPath to the newly created keystore file. +func createRandomNameKeystore(t *testing.T, path string) (*keymanager.Keystore, string) { + validatingKey, err := bls.RandKey() + require.NoError(t, err) + encryptor := keystorev4.New() + cryptoFields, err := encryptor.Encrypt(validatingKey.Marshal(), password) + require.NoError(t, err) + id, err := uuid.NewRandom() + require.NoError(t, err) + keystoreFile := &keymanager.Keystore{ + Crypto: cryptoFields, + ID: id.String(), + Pubkey: fmt.Sprintf("%x", validatingKey.PublicKey().Marshal()), + Version: encryptor.Version(), + Name: encryptor.Name(), + } + encoded, err := json.MarshalIndent(keystoreFile, "", "\t") + require.NoError(t, err) + // Write the encoded keystore to disk with the timestamp appended + random, err := rand.Int(rand.Reader, big.NewInt(1000000)) + require.NoError(t, err) + fullPath := filepath.Join(path, fmt.Sprintf("test-%d-keystore", random.Int64())) + require.NoError(t, os.WriteFile(fullPath, encoded, os.ModePerm)) + return keystoreFile, fullPath +} + +func TestImport_Noninteractive_Filepath(t *testing.T) { + local.ResetCaches() + walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t) + keysDir := filepath.Join(t.TempDir(), "keysDir") + require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) + + _, keystorePath := createKeystore(t, keysDir) + cliCtx := setupWalletCtx(t, &testWalletConfig{ + walletDir: walletDir, + passwordsDir: passwordsDir, + keysDir: keystorePath, + keymanagerKind: keymanager.Local, + walletPasswordFile: passwordFilePath, + accountPasswordFile: passwordFilePath, + }) + w, err := accounts.CreateWalletWithKeymanager(cliCtx.Context, &accounts.CreateWalletConfig{ + WalletCfg: &wallet.Config{ + WalletDir: walletDir, + KeymanagerKind: keymanager.Local, + WalletPassword: password, + }, + }) + require.NoError(t, err) + keymanager, err := local.NewKeymanager( + cliCtx.Context, + &local.SetupConfig{ + Wallet: w, + ListenForChanges: false, + }, + ) + require.NoError(t, err) + + // Make sure there are no accounts at the start. + accounts, err := keymanager.ValidatingAccountNames() + require.NoError(t, err) + assert.Equal(t, len(accounts), 0) + + require.NoError(t, accountsImport(cliCtx)) + + w, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{ + WalletDir: walletDir, + WalletPassword: password, + }) + require.NoError(t, err) + km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) + require.NoError(t, err) + keys, err := km.FetchValidatingPublicKeys(cliCtx.Context) + require.NoError(t, err) + + assert.Equal(t, 1, len(keys)) +} diff --git a/validator/accounts/accounts_import.go b/validator/accounts/accounts_import.go index 83488acabb..96bde76015 100644 --- a/validator/accounts/accounts_import.go +++ b/validator/accounts/accounts_import.go @@ -14,17 +14,13 @@ import ( "github.com/google/uuid" "github.com/pkg/errors" - "github.com/prysmaticlabs/prysm/cmd/validator/flags" "github.com/prysmaticlabs/prysm/crypto/bls" "github.com/prysmaticlabs/prysm/encoding/bytesutil" "github.com/prysmaticlabs/prysm/io/file" "github.com/prysmaticlabs/prysm/io/prompt" ethpbservice "github.com/prysmaticlabs/prysm/proto/eth/service" - "github.com/prysmaticlabs/prysm/validator/accounts/iface" - "github.com/prysmaticlabs/prysm/validator/accounts/userprompt" "github.com/prysmaticlabs/prysm/validator/accounts/wallet" "github.com/prysmaticlabs/prysm/validator/keymanager" - "github.com/urfave/cli/v2" keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" ) @@ -81,93 +77,31 @@ type ImportAccountsConfig struct { // ImportAccountsCli can import external, EIP-2335 compliant keystore.json files as // new accounts into the Prysm validator wallet. This uses the CLI to extract // values necessary to run the function. -func ImportAccountsCli(cliCtx *cli.Context) error { - w, err := wallet.OpenWalletOrElseCli(cliCtx, func(cliCtx *cli.Context) (*wallet.Wallet, error) { - walletDir, err := userprompt.InputDirectory(cliCtx, userprompt.WalletDirPromptText, flags.WalletDirFlag) - if err != nil { - return nil, err - } - exists, err := wallet.Exists(walletDir) - if err != nil { - return nil, errors.Wrap(err, wallet.CheckExistsErrMsg) - } - if exists { - isValid, err := wallet.IsValid(walletDir) - if err != nil { - return nil, errors.Wrap(err, wallet.CheckValidityErrMsg) - } - if !isValid { - return nil, errors.New(wallet.InvalidWalletErrMsg) - } - walletPassword, err := wallet.InputPassword( - cliCtx, - flags.WalletPasswordFileFlag, - wallet.PasswordPromptText, - false, /* Do not confirm password */ - wallet.ValidateExistingPass, - ) - if err != nil { - return nil, err - } - return wallet.OpenWallet(cliCtx.Context, &wallet.Config{ - WalletDir: walletDir, - WalletPassword: walletPassword, - }) - } - - cfg, err := extractWalletCreationConfigFromCli(cliCtx, keymanager.Local) - if err != nil { - return nil, err - } - w := wallet.New(&wallet.Config{ - KeymanagerKind: cfg.WalletCfg.KeymanagerKind, - WalletDir: cfg.WalletCfg.WalletDir, - WalletPassword: cfg.WalletCfg.WalletPassword, - }) - if err = createLocalKeymanagerWallet(cliCtx.Context, w); err != nil { - return nil, errors.Wrap(err, "could not create keymanager") - } - log.WithField("wallet-path", cfg.WalletCfg.WalletDir).Info( - "Successfully created new wallet", - ) - return w, nil - }) - if err != nil { - return errors.Wrap(err, "could not initialize wallet") - } - - km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) - if err != nil { - return err - } - k, ok := km.(keymanager.Importer) +func (acm *AccountsCLIManager) Import(ctx context.Context) error { + k, ok := acm.keymanager.(keymanager.Importer) if !ok { return errors.New("keymanager cannot import keystores") } // Check if the user wishes to import a one-off, private key directly // as an account into the Prysm validator. - if cliCtx.IsSet(flags.ImportPrivateKeyFileFlag.Name) { - return importPrivateKeyAsAccount(cliCtx, w, k) + if acm.importPrivateKeys { + return importPrivateKeyAsAccount(ctx, acm.wallet, k, acm.privateKeyFile) } - keysDir, err := userprompt.InputDirectory(cliCtx, userprompt.ImportKeysDirPromptText, flags.KeysDirFlag) - if err != nil { - return errors.Wrap(err, "could not parse keys directory") - } // Consider that the keysDir might be a path to a specific file and handle accordingly. - isDir, err := file.HasDir(keysDir) + isDir, err := file.HasDir(acm.keysDir) if err != nil { return errors.Wrap(err, "could not determine if path is a directory") } keystoresImported := make([]*keymanager.Keystore, 0) if isDir { - files, err := os.ReadDir(keysDir) + files, err := os.ReadDir(acm.keysDir) if err != nil { return errors.Wrap(err, "could not read dir") } if len(files) == 0 { - return fmt.Errorf("directory %s has no files, cannot import from it", keysDir) + return fmt.Errorf("directory %s has no files, cannot import from it", acm.keysDir) } filesInDir := make([]string, 0) for i := 0; i < len(files); i++ { @@ -180,7 +114,7 @@ func ImportAccountsCli(cliCtx *cli.Context) error { // specify this value in their filename. sort.Sort(byDerivationPath(filesInDir)) for _, name := range filesInDir { - keystore, err := readKeystoreFile(cliCtx.Context, filepath.Join(keysDir, name)) + keystore, err := readKeystoreFile(ctx, filepath.Join(acm.keysDir, name)) if err != nil && strings.Contains(err.Error(), "could not decode keystore json") { continue } else if err != nil { @@ -189,7 +123,7 @@ func ImportAccountsCli(cliCtx *cli.Context) error { keystoresImported = append(keystoresImported, keystore) } } else { - keystore, err := readKeystoreFile(cliCtx.Context, keysDir) + keystore, err := readKeystoreFile(ctx, acm.keysDir) if err != nil { return errors.Wrap(err, "could not import keystore") } @@ -197,9 +131,8 @@ func ImportAccountsCli(cliCtx *cli.Context) error { } var accountsPassword string - if cliCtx.IsSet(flags.AccountPasswordFileFlag.Name) { - passwordFilePath := cliCtx.String(flags.AccountPasswordFileFlag.Name) - data, err := os.ReadFile(passwordFilePath) // #nosec G304 + if acm.readPasswordFile { + data, err := os.ReadFile(acm.passwordFilePath) // #nosec G304 if err != nil { return err } @@ -213,7 +146,7 @@ func ImportAccountsCli(cliCtx *cli.Context) error { } } fmt.Println("Importing accounts, this may take a while...") - statuses, err := ImportAccounts(cliCtx.Context, &ImportAccountsConfig{ + statuses, err := ImportAccounts(ctx, &ImportAccountsConfig{ Importer: k, Keystores: keystoresImported, AccountPassword: accountsPassword, @@ -265,8 +198,7 @@ func ImportAccounts(ctx context.Context, cfg *ImportAccountsConfig) ([]*ethpbser // Imports a one-off file containing a private key as a hex string into // the Prysm validator's accounts. -func importPrivateKeyAsAccount(cliCtx *cli.Context, wallet *wallet.Wallet, importer keymanager.Importer) error { - privKeyFile := cliCtx.String(flags.ImportPrivateKeyFileFlag.Name) +func importPrivateKeyAsAccount(ctx context.Context, wallet *wallet.Wallet, importer keymanager.Importer, privKeyFile string) error { fullPath, err := file.ExpandPath(privKeyFile) if err != nil { return errors.Wrapf(err, "could not expand file path for %s", privKeyFile) @@ -297,7 +229,7 @@ func importPrivateKeyAsAccount(cliCtx *cli.Context, wallet *wallet.Wallet, impor return errors.Wrap(err, "could not encrypt private key into a keystore file") } statuses, err := ImportAccounts( - cliCtx.Context, + ctx, &ImportAccountsConfig{ Importer: importer, AccountPassword: wallet.Password(), diff --git a/validator/accounts/accounts_import_test.go b/validator/accounts/accounts_import_test.go index 2630fd8a01..c5d2df2d1e 100644 --- a/validator/accounts/accounts_import_test.go +++ b/validator/accounts/accounts_import_test.go @@ -2,15 +2,12 @@ package accounts import ( "context" - "crypto/rand" "encoding/json" "fmt" - "math/big" "os" "path/filepath" "sort" "testing" - "time" "github.com/google/uuid" "github.com/prysmaticlabs/prysm/config/params" @@ -27,111 +24,6 @@ import ( keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" ) -func TestImport_Noninteractive(t *testing.T) { - local.ResetCaches() - walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t) - keysDir := filepath.Join(t.TempDir(), "keysDir") - require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) - - cliCtx := setupWalletCtx(t, &testWalletConfig{ - walletDir: walletDir, - passwordsDir: passwordsDir, - keysDir: keysDir, - keymanagerKind: keymanager.Local, - walletPasswordFile: passwordFilePath, - accountPasswordFile: passwordFilePath, - }) - w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{ - WalletCfg: &wallet.Config{ - WalletDir: walletDir, - KeymanagerKind: keymanager.Local, - WalletPassword: password, - }, - }) - require.NoError(t, err) - keymanager, err := local.NewKeymanager( - cliCtx.Context, - &local.SetupConfig{ - Wallet: w, - ListenForChanges: false, - }, - ) - require.NoError(t, err) - - // Make sure there are no accounts at the start. - accounts, err := keymanager.ValidatingAccountNames() - require.NoError(t, err) - assert.Equal(t, len(accounts), 0) - - // Create 2 keys. - createKeystore(t, keysDir) - time.Sleep(time.Second) - createKeystore(t, keysDir) - - require.NoError(t, ImportAccountsCli(cliCtx)) - - w, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{ - WalletDir: walletDir, - WalletPassword: password, - }) - require.NoError(t, err) - km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) - require.NoError(t, err) - keys, err := km.FetchValidatingPublicKeys(cliCtx.Context) - require.NoError(t, err) - - assert.Equal(t, 2, len(keys)) -} - -// TestImport_DuplicateKeys is a regression test that ensures correction function if duplicate keys are being imported -func TestImport_DuplicateKeys(t *testing.T) { - local.ResetCaches() - walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t) - keysDir := filepath.Join(t.TempDir(), "keysDir") - require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) - - cliCtx := setupWalletCtx(t, &testWalletConfig{ - walletDir: walletDir, - passwordsDir: passwordsDir, - keysDir: keysDir, - keymanagerKind: keymanager.Local, - walletPasswordFile: passwordFilePath, - accountPasswordFile: passwordFilePath, - }) - w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{ - WalletCfg: &wallet.Config{ - WalletDir: walletDir, - KeymanagerKind: keymanager.Local, - WalletPassword: password, - }, - }) - require.NoError(t, err) - - // Create a key and then copy it to create a duplicate - _, keystorePath := createKeystore(t, keysDir) - time.Sleep(time.Second) - input, err := os.ReadFile(keystorePath) - require.NoError(t, err) - keystorePath2 := filepath.Join(keysDir, "copyOfKeystore.json") - err = os.WriteFile(keystorePath2, input, os.ModePerm) - require.NoError(t, err) - - require.NoError(t, ImportAccountsCli(cliCtx)) - - _, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{ - WalletDir: walletDir, - WalletPassword: password, - }) - require.NoError(t, err) - km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) - require.NoError(t, err) - keys, err := km.FetchValidatingPublicKeys(cliCtx.Context) - require.NoError(t, err) - - // There should only be 1 account as the duplicate keystore was ignored - assert.Equal(t, 1, len(keys)) -} - func TestImportAccounts_NoPassword(t *testing.T) { local.ResetCaches() walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t) @@ -166,115 +58,6 @@ func TestImportAccounts_NoPassword(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(resp)) require.Equal(t, resp[0].Status, ethpbservice.ImportedKeystoreStatus_ERROR) - -} - -func TestImport_Noninteractive_RandomName(t *testing.T) { - local.ResetCaches() - walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t) - keysDir := filepath.Join(t.TempDir(), "keysDir") - require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) - - cliCtx := setupWalletCtx(t, &testWalletConfig{ - walletDir: walletDir, - passwordsDir: passwordsDir, - keysDir: keysDir, - keymanagerKind: keymanager.Local, - walletPasswordFile: passwordFilePath, - accountPasswordFile: passwordFilePath, - }) - w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{ - WalletCfg: &wallet.Config{ - WalletDir: walletDir, - KeymanagerKind: keymanager.Local, - WalletPassword: password, - }, - }) - require.NoError(t, err) - keymanager, err := local.NewKeymanager( - cliCtx.Context, - &local.SetupConfig{ - Wallet: w, - ListenForChanges: false, - }, - ) - require.NoError(t, err) - - // Make sure there are no accounts at the start. - accounts, err := keymanager.ValidatingAccountNames() - require.NoError(t, err) - assert.Equal(t, len(accounts), 0) - - // Create 2 keys. - createRandomNameKeystore(t, keysDir) - time.Sleep(time.Second) - createRandomNameKeystore(t, keysDir) - - require.NoError(t, ImportAccountsCli(cliCtx)) - - w, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{ - WalletDir: walletDir, - WalletPassword: password, - }) - require.NoError(t, err) - km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) - require.NoError(t, err) - keys, err := km.FetchValidatingPublicKeys(cliCtx.Context) - require.NoError(t, err) - - assert.Equal(t, 2, len(keys)) -} - -func TestImport_Noninteractive_Filepath(t *testing.T) { - local.ResetCaches() - walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t) - keysDir := filepath.Join(t.TempDir(), "keysDir") - require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) - - _, keystorePath := createKeystore(t, keysDir) - cliCtx := setupWalletCtx(t, &testWalletConfig{ - walletDir: walletDir, - passwordsDir: passwordsDir, - keysDir: keystorePath, - keymanagerKind: keymanager.Local, - walletPasswordFile: passwordFilePath, - accountPasswordFile: passwordFilePath, - }) - w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{ - WalletCfg: &wallet.Config{ - WalletDir: walletDir, - KeymanagerKind: keymanager.Local, - WalletPassword: password, - }, - }) - require.NoError(t, err) - keymanager, err := local.NewKeymanager( - cliCtx.Context, - &local.SetupConfig{ - Wallet: w, - ListenForChanges: false, - }, - ) - require.NoError(t, err) - - // Make sure there are no accounts at the start. - accounts, err := keymanager.ValidatingAccountNames() - require.NoError(t, err) - assert.Equal(t, len(accounts), 0) - - require.NoError(t, ImportAccountsCli(cliCtx)) - - w, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{ - WalletDir: walletDir, - WalletPassword: password, - }) - require.NoError(t, err) - km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) - require.NoError(t, err) - keys, err := km.FetchValidatingPublicKeys(cliCtx.Context) - require.NoError(t, err) - - assert.Equal(t, 1, len(keys)) } func TestImport_SortByDerivationPath(t *testing.T) { @@ -378,7 +161,7 @@ func Test_importPrivateKeyAsAccount(t *testing.T) { }, ) require.NoError(t, err) - assert.NoError(t, importPrivateKeyAsAccount(cliCtx, wallet, keymanager)) + assert.NoError(t, importPrivateKeyAsAccount(cliCtx.Context, wallet, keymanager, privKeyFileName)) // We re-instantiate the keymanager and check we now have 1 public key. keymanager, err = local.NewKeymanager( @@ -419,29 +202,3 @@ func createKeystore(t *testing.T, path string) (*keymanager.Keystore, string) { require.NoError(t, os.WriteFile(fullPath, encoded, os.ModePerm)) return keystoreFile, fullPath } - -// Returns the fullPath to the newly created keystore file. -func createRandomNameKeystore(t *testing.T, path string) (*keymanager.Keystore, string) { - validatingKey, err := bls.RandKey() - require.NoError(t, err) - encryptor := keystorev4.New() - cryptoFields, err := encryptor.Encrypt(validatingKey.Marshal(), password) - require.NoError(t, err) - id, err := uuid.NewRandom() - require.NoError(t, err) - keystoreFile := &keymanager.Keystore{ - Crypto: cryptoFields, - ID: id.String(), - Pubkey: fmt.Sprintf("%x", validatingKey.PublicKey().Marshal()), - Version: encryptor.Version(), - Name: encryptor.Name(), - } - encoded, err := json.MarshalIndent(keystoreFile, "", "\t") - require.NoError(t, err) - // Write the encoded keystore to disk with the timestamp appended - random, err := rand.Int(rand.Reader, big.NewInt(1000000)) - require.NoError(t, err) - fullPath := filepath.Join(path, fmt.Sprintf("test-%d-keystore", random.Int64())) - require.NoError(t, os.WriteFile(fullPath, encoded, os.ModePerm)) - return keystoreFile, fullPath -} diff --git a/validator/accounts/cli_manager.go b/validator/accounts/cli_manager.go index 5f3d1f7510..2941336446 100644 --- a/validator/accounts/cli_manager.go +++ b/validator/accounts/cli_manager.go @@ -32,10 +32,15 @@ type AccountsCLIManager struct { showPrivateKeys bool listValidatorIndices bool deletePublicKeys bool + importPrivateKeys bool + readPasswordFile bool dialOpts []grpc.DialOption grpcHeaders []string beaconRPCProvider string walletKeyCount int + privateKeyFile string + passwordFilePath string + keysDir string backupsDir string backupsPassword string filteredPubKeys []bls.PublicKey diff --git a/validator/accounts/cli_options.go b/validator/accounts/cli_options.go index 09f531d143..eb439e3718 100644 --- a/validator/accounts/cli_options.go +++ b/validator/accounts/cli_options.go @@ -90,6 +90,46 @@ func WithDeletePublicKeys(deletePublicKeys bool) Option { } } +// WithReadPasswordFile indicates whether to read the password from a file. +func WithReadPasswordFile(readPasswordFile bool) Option { + return func(acc *AccountsCLIManager) error { + acc.readPasswordFile = readPasswordFile + return nil + } +} + +// WithImportPrivateKeys indicates whether to import private keys as accounts. +func WithImportPrivateKeys(importPrivateKeys bool) Option { + return func(acc *AccountsCLIManager) error { + acc.importPrivateKeys = importPrivateKeys + return nil + } +} + +// WithPrivateKeyFile specifies the private key path. +func WithPrivateKeyFile(privateKeyFile string) Option { + return func(acc *AccountsCLIManager) error { + acc.privateKeyFile = privateKeyFile + return nil + } +} + +// WithKeysDir specifies the directory keys are read from. +func WithKeysDir(keysDir string) Option { + return func(acc *AccountsCLIManager) error { + acc.keysDir = keysDir + return nil + } +} + +// WithPasswordFilePath specifies where the password is stored. +func WithPasswordFilePath(passwordFilePath string) Option { + return func(acc *AccountsCLIManager) error { + acc.passwordFilePath = passwordFilePath + return nil + } +} + // WithBackupDir specifies the directory backups are written to. func WithBackupsDir(backupsDir string) Option { return func(acc *AccountsCLIManager) error { diff --git a/validator/accounts/wallet_create.go b/validator/accounts/wallet_create.go index 2f5d52e5d2..3abd848240 100644 --- a/validator/accounts/wallet_create.go +++ b/validator/accounts/wallet_create.go @@ -40,7 +40,7 @@ func CreateAndSaveWalletCli(cliCtx *cli.Context) (*wallet.Wallet, error) { if err != nil { return nil, err } - createWalletConfig, err := extractWalletCreationConfigFromCli(cliCtx, keymanagerKind) + createWalletConfig, err := ExtractWalletCreationConfigFromCli(cliCtx, keymanagerKind) if err != nil { return nil, err } @@ -72,7 +72,7 @@ func CreateWalletWithKeymanager(ctx context.Context, cfg *CreateWalletConfig) (* var err error switch w.KeymanagerKind() { case keymanager.Local: - if err = createLocalKeymanagerWallet(ctx, w); err != nil { + if err = CreateLocalKeymanagerWallet(ctx, w); err != nil { return nil, errors.Wrap(err, "could not initialize wallet") } // TODO(#9883) - Remove this when we have a better way to handle this. should be safe to use for now. @@ -131,7 +131,8 @@ func extractKeymanagerKindFromCli(cliCtx *cli.Context) (keymanager.Kind, error) return inputKeymanagerKind(cliCtx) } -func extractWalletCreationConfigFromCli(cliCtx *cli.Context, keymanagerKind keymanager.Kind) (*CreateWalletConfig, error) { +// ExtractWalletCreationConfigFromCli prompts the user for wallet creation input. +func ExtractWalletCreationConfigFromCli(cliCtx *cli.Context, keymanagerKind keymanager.Kind) (*CreateWalletConfig, error) { walletDir, err := userprompt.InputDirectory(cliCtx, userprompt.WalletDirPromptText, flags.WalletDirFlag) if err != nil { return nil, err @@ -204,7 +205,7 @@ func extractWalletCreationConfigFromCli(cliCtx *cli.Context, keymanagerKind keym return createWalletConfig, nil } -func createLocalKeymanagerWallet(_ context.Context, wallet *wallet.Wallet) error { +func CreateLocalKeymanagerWallet(_ context.Context, wallet *wallet.Wallet) error { if wallet == nil { return errors.New("nil wallet") } diff --git a/validator/accounts/wallet_create_test.go b/validator/accounts/wallet_create_test.go index 74ee1944ef..7c763c040a 100644 --- a/validator/accounts/wallet_create_test.go +++ b/validator/accounts/wallet_create_test.go @@ -119,7 +119,7 @@ func TestCreateOrOpenWallet(t *testing.T) { walletPasswordFile: walletPasswordFile, }) createLocalWallet := func(cliCtx *cli.Context) (*wallet.Wallet, error) { - cfg, err := extractWalletCreationConfigFromCli(cliCtx, keymanager.Local) + cfg, err := ExtractWalletCreationConfigFromCli(cliCtx, keymanager.Local) if err != nil { return nil, err } @@ -128,7 +128,7 @@ func TestCreateOrOpenWallet(t *testing.T) { WalletDir: cfg.WalletCfg.WalletDir, WalletPassword: cfg.WalletCfg.WalletPassword, }) - if err = createLocalKeymanagerWallet(cliCtx.Context, w); err != nil { + if err = CreateLocalKeymanagerWallet(cliCtx.Context, w); err != nil { return nil, errors.Wrap(err, "could not create keymanager") } log.WithField("wallet-path", cfg.WalletCfg.WalletDir).Info(