diff --git a/validator/accounts/BUILD.bazel b/validator/accounts/BUILD.bazel index 719873c2aa..e0eb23a9de 100644 --- a/validator/accounts/BUILD.bazel +++ b/validator/accounts/BUILD.bazel @@ -4,9 +4,11 @@ load("@prysm//tools/go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = [ + "accounts.go", "accounts_backup.go", "accounts_create.go", "accounts_delete.go", + "accounts_enable_disable.go", "accounts_exit.go", "accounts_helper.go", "accounts_import.go", @@ -64,6 +66,7 @@ go_test( "accounts_backup_test.go", "accounts_create_test.go", "accounts_delete_test.go", + "accounts_enable_disable_test.go", "accounts_exit_test.go", "accounts_import_test.go", "accounts_list_test.go", diff --git a/validator/accounts/accounts.go b/validator/accounts/accounts.go new file mode 100644 index 0000000000..f178cfbfd6 --- /dev/null +++ b/validator/accounts/accounts.go @@ -0,0 +1,15 @@ +package accounts + +import ( + "github.com/prysmaticlabs/prysm/validator/accounts/wallet" + "github.com/prysmaticlabs/prysm/validator/keymanager" +) + +// AccountsConfig specifies parameters to run to delete, enable, disable accounts. +type AccountsConfig struct { + Wallet *wallet.Wallet + Keymanager keymanager.IKeymanager + DisablePublicKeys [][]byte + EnablePublicKeys [][]byte + DeletePublicKeys [][]byte +} diff --git a/validator/accounts/accounts_backup.go b/validator/accounts/accounts_backup.go index d14003b9e1..2a72546cbf 100644 --- a/validator/accounts/accounts_backup.go +++ b/validator/accounts/accounts_backup.go @@ -58,7 +58,7 @@ func BackupAccountsCli(cliCtx *cli.Context) error { if err != nil { return errors.Wrap(err, "could not initialize keymanager") } - pubKeys, err := km.FetchValidatingPublicKeys(cliCtx.Context) + pubKeys, err := km.FetchAllValidatingPublicKeys(cliCtx.Context) if err != nil { return errors.Wrap(err, "could not fetch validating public keys") } diff --git a/validator/accounts/accounts_delete.go b/validator/accounts/accounts_delete.go index d855ac0a43..955e642c2b 100644 --- a/validator/accounts/accounts_delete.go +++ b/validator/accounts/accounts_delete.go @@ -18,13 +18,6 @@ import ( "github.com/urfave/cli/v2" ) -// DeleteAccountConfig specifies parameters to run the delete account function. -type DeleteAccountConfig struct { - Wallet *wallet.Wallet - Keymanager keymanager.IKeymanager - PublicKeys [][]byte -} - // DeleteAccountCli deletes the accounts that the user requests to be deleted from the wallet. // This function uses the CLI to extract necessary values. func DeleteAccountCli(cliCtx *cli.Context) error { @@ -40,7 +33,7 @@ func DeleteAccountCli(cliCtx *cli.Context) error { if err != nil { return errors.Wrap(err, "could not initialize keymanager") } - validatingPublicKeys, err := keymanager.FetchValidatingPublicKeys(cliCtx.Context) + validatingPublicKeys, err := keymanager.FetchAllValidatingPublicKeys(cliCtx.Context) if err != nil { return err } @@ -94,10 +87,10 @@ func DeleteAccountCli(cliCtx *cli.Context) error { } } } - if err := DeleteAccount(cliCtx.Context, &DeleteAccountConfig{ - Wallet: w, - Keymanager: keymanager, - PublicKeys: rawPublicKeys, + if err := DeleteAccount(cliCtx.Context, &AccountsConfig{ + Wallet: w, + Keymanager: keymanager, + DeletePublicKeys: rawPublicKeys, }); err != nil { return err } @@ -109,7 +102,7 @@ func DeleteAccountCli(cliCtx *cli.Context) error { } // DeleteAccount deletes the accounts that the user requests to be deleted from the wallet. -func DeleteAccount(ctx context.Context, cfg *DeleteAccountConfig) error { +func DeleteAccount(ctx context.Context, cfg *AccountsConfig) error { switch cfg.Wallet.KeymanagerKind() { case keymanager.Remote: return errors.New("cannot delete accounts for a remote keymanager") @@ -118,12 +111,12 @@ func DeleteAccount(ctx context.Context, cfg *DeleteAccountConfig) error { if !ok { return errors.New("not a imported keymanager") } - if len(cfg.PublicKeys) == 1 { + if len(cfg.DeletePublicKeys) == 1 { log.Info("Deleting account...") } else { log.Info("Deleting accounts...") } - if err := km.DeleteAccounts(ctx, cfg.PublicKeys); err != nil { + if err := km.DeleteAccounts(ctx, cfg.DeletePublicKeys); err != nil { return errors.Wrap(err, "could not delete accounts") } case keymanager.Derived: diff --git a/validator/accounts/accounts_enable_disable.go b/validator/accounts/accounts_enable_disable.go new file mode 100644 index 0000000000..70195b1867 --- /dev/null +++ b/validator/accounts/accounts_enable_disable.go @@ -0,0 +1,275 @@ +package accounts + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/promptutil" + "github.com/prysmaticlabs/prysm/validator/accounts/iface" + "github.com/prysmaticlabs/prysm/validator/accounts/prompt" + "github.com/prysmaticlabs/prysm/validator/accounts/wallet" + "github.com/prysmaticlabs/prysm/validator/flags" + "github.com/prysmaticlabs/prysm/validator/keymanager" + "github.com/prysmaticlabs/prysm/validator/keymanager/imported" + "github.com/urfave/cli/v2" +) + +// DisableAccountsCli disables via CLI the accounts that the user requests to be disabled from the wallet +func DisableAccountsCli(cliCtx *cli.Context) error { + w, err := wallet.OpenWalletOrElseCli(cliCtx, func(cliCtx *cli.Context) (*wallet.Wallet, error) { + return nil, wallet.ErrNoWalletFound + }) + if err != nil { + return errors.Wrap(err, "could not open wallet") + } + keymanager, err := w.InitializeKeymanager(cliCtx.Context, &iface.InitializeKeymanagerConfig{ + SkipMnemonicConfirm: false, + }) + if err != nil { + return errors.Wrap(err, "could not initialize keymanager") + } + validatingPublicKeys, err := keymanager.FetchValidatingPublicKeys(cliCtx.Context) + if err != nil { + return err + } + if len(validatingPublicKeys) == 0 { + return errors.New("wallet is empty, no accounts to disable") + } + // Allow the user to interactively select the accounts to disable or optionally + // provide them via cli flags as a string of comma-separated, hex strings. + filteredPubKeys, err := filterPublicKeysFromUserInput( + cliCtx, + flags.DisablePublicKeysFlag, + validatingPublicKeys, + prompt.SelectAccountsDisablePromptText, + ) + if err != nil { + return errors.Wrap(err, "could not filter public keys for deactivation") + } + rawPublicKeys := make([][]byte, len(filteredPubKeys)) + formattedPubKeys := make([]string, len(filteredPubKeys)) + for i, pk := range filteredPubKeys { + pubKeyBytes := pk.Marshal() + rawPublicKeys[i] = pubKeyBytes + formattedPubKeys[i] = fmt.Sprintf("%#x", bytesutil.Trunc(pubKeyBytes)) + } + allAccountStr := strings.Join(formattedPubKeys, ", ") + if !cliCtx.IsSet(flags.DisablePublicKeysFlag.Name) { + if len(filteredPubKeys) == 1 { + promptText := "Are you sure you want to disable 1 account? (%s) Y/N" + resp, err := promptutil.ValidatePrompt( + os.Stdin, fmt.Sprintf(promptText, au.BrightGreen(formattedPubKeys[0])), promptutil.ValidateYesOrNo, + ) + if err != nil { + return err + } + if strings.ToLower(resp) == "n" { + return nil + } + } else { + promptText := "Are you sure you want to disable %d accounts? (%s) Y/N" + if len(filteredPubKeys) == len(validatingPublicKeys) { + promptText = fmt.Sprintf("Are you sure you want to disable all accounts? Y/N (%s)", au.BrightGreen(allAccountStr)) + } else { + promptText = fmt.Sprintf(promptText, len(filteredPubKeys), au.BrightGreen(allAccountStr)) + } + resp, err := promptutil.ValidatePrompt(os.Stdin, promptText, promptutil.ValidateYesOrNo) + if err != nil { + return err + } + if strings.ToLower(resp) == "n" { + return nil + } + } + } + if err := DisableAccounts(cliCtx.Context, &AccountsConfig{ + Wallet: w, + Keymanager: keymanager, + DisablePublicKeys: rawPublicKeys, + }); err != nil { + return err + } + log.WithField("publicKeys", allAccountStr).Info("Accounts disabled") + return nil +} + +// EnableAccountsCli enables via CLI the accounts that the user requests to be enabled from the wallet +func EnableAccountsCli(cliCtx *cli.Context) error { + w, err := wallet.OpenWalletOrElseCli(cliCtx, func(cliCtx *cli.Context) (*wallet.Wallet, error) { + return nil, wallet.ErrNoWalletFound + }) + if err != nil { + return errors.Wrap(err, "could not open wallet") + } + ikeymanager, err := w.InitializeKeymanager(cliCtx.Context, &iface.InitializeKeymanagerConfig{ + SkipMnemonicConfirm: false, + }) + if err != nil { + return errors.Wrap(err, "could not initialize keymanager") + } + switch w.KeymanagerKind() { + case keymanager.Remote: + return errors.New("cannot enable accounts for a remote keymanager") + case keymanager.Imported: + km, ok := ikeymanager.(*imported.Keymanager) + if !ok { + return errors.New("not a imported keymanager") + } + disabledPublicKeys := km.KeymanagerOpts().DisabledPublicKeys + if len(disabledPublicKeys) == 0 { + return errors.New("No accounts are disabled.") + } + disabledPublicKeys48 := make([][48]byte, len(disabledPublicKeys)) + for i := range disabledPublicKeys { + disabledPublicKeys48[i] = bytesutil.ToBytes48(disabledPublicKeys[i]) + } + + // Allow the user to interactively select the accounts to enable or optionally + // provide them via cli flags as a string of comma-separated, hex strings. + filteredPubKeys, err := filterPublicKeysFromUserInput( + cliCtx, + flags.EnablePublicKeysFlag, + disabledPublicKeys48, + prompt.SelectAccountsEnablePromptText, + ) + if err != nil { + return errors.Wrap(err, "could not filter public keys for activation") + } + rawPublicKeys := make([][]byte, len(filteredPubKeys)) + formattedPubKeys := make([]string, len(filteredPubKeys)) + for i, pk := range filteredPubKeys { + pubKeyBytes := pk.Marshal() + rawPublicKeys[i] = pubKeyBytes + formattedPubKeys[i] = fmt.Sprintf("%#x", bytesutil.Trunc(pubKeyBytes)) + } + allAccountStr := strings.Join(formattedPubKeys, ", ") + if !cliCtx.IsSet(flags.EnablePublicKeysFlag.Name) { + if len(filteredPubKeys) == 1 { + promptText := "Are you sure you want to enable 1 account? (%s) Y/N" + resp, err := promptutil.ValidatePrompt( + os.Stdin, fmt.Sprintf(promptText, au.BrightGreen(formattedPubKeys[0])), promptutil.ValidateYesOrNo, + ) + if err != nil { + return err + } + if strings.ToLower(resp) == "n" { + return nil + } + } else { + promptText := "Are you sure you want to enable %d accounts? (%s) Y/N" + if len(filteredPubKeys) == len(disabledPublicKeys48) { + promptText = fmt.Sprintf("Are you sure you want to enable all accounts? Y/N (%s)", au.BrightGreen(allAccountStr)) + } else { + promptText = fmt.Sprintf(promptText, len(filteredPubKeys), au.BrightGreen(allAccountStr)) + } + resp, err := promptutil.ValidatePrompt(os.Stdin, promptText, promptutil.ValidateYesOrNo) + if err != nil { + return err + } + if strings.ToLower(resp) == "n" { + return nil + } + } + } + if err := EnableAccounts(cliCtx.Context, &AccountsConfig{ + Wallet: w, + Keymanager: ikeymanager, + EnablePublicKeys: rawPublicKeys, + }); err != nil { + return err + } + log.WithField("publicKeys", allAccountStr).Info("Accounts enabled") + return nil + case keymanager.Derived: + return errors.New("cannot enable accounts for a derived keymanager") + default: + return fmt.Errorf("keymanager kind %s not supported", w.KeymanagerKind()) + } +} + +// DisableAccount disables the accounts that the user requests to be disabled from the wallet +func DisableAccounts(ctx context.Context, cfg *AccountsConfig) error { + switch cfg.Wallet.KeymanagerKind() { + case keymanager.Remote: + return errors.New("cannot disable accounts for a remote keymanager") + case keymanager.Imported: + km, ok := cfg.Keymanager.(*imported.Keymanager) + if !ok { + return errors.New("not a imported keymanager") + } + if len(cfg.DisablePublicKeys) == 1 { + log.Info("Disabling account...") + } else { + log.Info("Disabling accounts...") + } + updatedOpts := km.KeymanagerOpts() + // updatedDisabledPubKeys := make([][48]byte, 0) + existingDisabledPubKeys := make(map[[48]byte]bool, len(updatedOpts.DisabledPublicKeys)) + for _, pk := range updatedOpts.DisabledPublicKeys { + existingDisabledPubKeys[bytesutil.ToBytes48(pk)] = true + } + for _, pk := range cfg.DisablePublicKeys { + if _, ok := existingDisabledPubKeys[bytesutil.ToBytes48(pk)]; !ok { + updatedOpts.DisabledPublicKeys = append(updatedOpts.DisabledPublicKeys, pk) + } + } + keymanagerConfig, err := imported.MarshalOptionsFile(ctx, updatedOpts) + if err != nil { + return errors.Wrap(err, "could not marshal keymanager config file") + } + if err := cfg.Wallet.WriteKeymanagerConfigToDisk(ctx, keymanagerConfig); err != nil { + return errors.Wrap(err, "could not write keymanager config to disk") + } + case keymanager.Derived: + return errors.New("cannot disable accounts for a derived keymanager") + default: + return fmt.Errorf("keymanager kind %s not supported", cfg.Wallet.KeymanagerKind()) + } + return nil +} + +// EnableAccounts enables the accounts that the user requests to be enabled from the wallet +func EnableAccounts(ctx context.Context, cfg *AccountsConfig) error { + switch cfg.Wallet.KeymanagerKind() { + case keymanager.Remote: + return errors.New("cannot enable accounts for a remote keymanager") + case keymanager.Imported: + km, ok := cfg.Keymanager.(*imported.Keymanager) + if !ok { + return errors.New("not a imported keymanager") + } + if len(cfg.EnablePublicKeys) == 1 { + log.Info("Enabling account...") + } else { + log.Info("Enabling accounts...") + } + updatedOpts := km.KeymanagerOpts() + updatedDisabledPubKeys := make([][]byte, 0) + setEnablePubKeys := make(map[[48]byte]bool, len(cfg.EnablePublicKeys)) + for _, pk := range cfg.EnablePublicKeys { + setEnablePubKeys[bytesutil.ToBytes48(pk)] = true + } + for _, pk := range updatedOpts.DisabledPublicKeys { + if _, ok := setEnablePubKeys[bytesutil.ToBytes48(pk)]; !ok { + updatedDisabledPubKeys = append(updatedDisabledPubKeys, pk) + } + } + updatedOpts.DisabledPublicKeys = updatedDisabledPubKeys + keymanagerConfig, err := imported.MarshalOptionsFile(ctx, updatedOpts) + if err != nil { + return errors.Wrap(err, "could not marshal keymanager config file") + } + if err := cfg.Wallet.WriteKeymanagerConfigToDisk(ctx, keymanagerConfig); err != nil { + return errors.Wrap(err, "could not write keymanager config to disk") + } + case keymanager.Derived: + return errors.New("cannot enable accounts for a derived keymanager") + default: + return fmt.Errorf("keymanager kind %s not supported", cfg.Wallet.KeymanagerKind()) + } + return nil +} diff --git a/validator/accounts/accounts_enable_disable_test.go b/validator/accounts/accounts_enable_disable_test.go new file mode 100644 index 0000000000..cc996009c6 --- /dev/null +++ b/validator/accounts/accounts_enable_disable_test.go @@ -0,0 +1,143 @@ +package accounts + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "math/big" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/testutil/assert" + "github.com/prysmaticlabs/prysm/shared/testutil/require" + "github.com/prysmaticlabs/prysm/validator/accounts/iface" + "github.com/prysmaticlabs/prysm/validator/accounts/wallet" + "github.com/prysmaticlabs/prysm/validator/keymanager" +) + +func TestDisableAccounts_Noninteractive(t *testing.T) { + walletDir, _, passwordFilePath := setupWalletAndPasswordsDir(t) + randPath, err := rand.Int(rand.Reader, big.NewInt(1000000)) + require.NoError(t, err, "Could not generate random file path") + // Write a directory where we will import keys from. + keysDir := filepath.Join(t.TempDir(), fmt.Sprintf("/%d", randPath), "keysDir") + require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) + + // Create 3 keystore files in the keys directory we can then + // import from in our wallet. + k1, _ := createKeystore(t, keysDir) + time.Sleep(time.Second) + k2, _ := createKeystore(t, keysDir) + time.Sleep(time.Second) + k3, _ := createKeystore(t, keysDir) + generatedPubKeys := []string{k1.Pubkey, k2.Pubkey, k3.Pubkey} + // Only disable keys 0 and 1. + disablePublicKeys := strings.Join(generatedPubKeys[0:2], ",") + // We initialize a wallet with a imported keymanager. + cliCtx := setupWalletCtx(t, &testWalletConfig{ + // Wallet configuration flags. + walletDir: walletDir, + keymanagerKind: keymanager.Imported, + walletPasswordFile: passwordFilePath, + accountPasswordFile: passwordFilePath, + // Flags required for ImportAccounts to work. + keysDir: keysDir, + // Flags required for DisableAccounts to work. + disablePublicKeys: disablePublicKeys, + }) + w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{ + WalletCfg: &wallet.Config{ + WalletDir: walletDir, + KeymanagerKind: keymanager.Imported, + WalletPassword: password, + }, + }) + require.NoError(t, err) + + // We attempt to import accounts. + require.NoError(t, ImportAccountsCli(cliCtx)) + + // We attempt to disable the accounts specified. + require.NoError(t, DisableAccountsCli(cliCtx)) + + keymanager, err := w.InitializeKeymanager(cliCtx.Context, &iface.InitializeKeymanagerConfig{SkipMnemonicConfirm: false}) + require.NoError(t, err) + remainingAccounts, err := keymanager.FetchValidatingPublicKeys(cliCtx.Context) + require.NoError(t, err) + require.Equal(t, len(remainingAccounts), 1) + remainingPublicKey, err := hex.DecodeString(k3.Pubkey) + require.NoError(t, err) + assert.DeepEqual(t, remainingAccounts[0], bytesutil.ToBytes48(remainingPublicKey)) +} + +func TestEnableAccounts_Noninteractive(t *testing.T) { + walletDir, _, passwordFilePath := setupWalletAndPasswordsDir(t) + randPath, err := rand.Int(rand.Reader, big.NewInt(1000000)) + require.NoError(t, err, "Could not generate random file path") + // Write a directory where we will import keys from. + keysDir := filepath.Join(t.TempDir(), fmt.Sprintf("/%d", randPath), "keysDir") + require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) + + // Create 3 keystore files in the keys directory we can then + // import from in our wallet. + k1, _ := createKeystore(t, keysDir) + time.Sleep(time.Second) + k2, _ := createKeystore(t, keysDir) + time.Sleep(time.Second) + k3, _ := createKeystore(t, keysDir) + generatedPubKeys := []string{k1.Pubkey, k2.Pubkey, k3.Pubkey} + // Disable all keys. + disablePublicKeys := strings.Join(generatedPubKeys, ",") + // Only enable keys 0 and 1. + enablePublicKeys := strings.Join(generatedPubKeys[0:2], ",") + // We initialize a wallet with a imported keymanager. + cliCtx := setupWalletCtx(t, &testWalletConfig{ + walletDir: walletDir, + keymanagerKind: keymanager.Imported, + walletPasswordFile: passwordFilePath, + accountPasswordFile: passwordFilePath, + keysDir: keysDir, + disablePublicKeys: disablePublicKeys, + // Flags required for EnableAccounts to work. + enablePublicKeys: enablePublicKeys, + }) + w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{ + WalletCfg: &wallet.Config{ + WalletDir: walletDir, + KeymanagerKind: keymanager.Imported, + WalletPassword: password, + }, + }) + require.NoError(t, err) + + // We attempt to import accounts. + require.NoError(t, ImportAccountsCli(cliCtx)) + + // We attempt to disable the accounts specified. + require.NoError(t, DisableAccountsCli(cliCtx)) + + km, err := w.InitializeKeymanager(cliCtx.Context, &iface.InitializeKeymanagerConfig{SkipMnemonicConfirm: false}) + require.NoError(t, err) + remainingAccounts, err := km.FetchValidatingPublicKeys(cliCtx.Context) + require.NoError(t, err) + require.Equal(t, len(remainingAccounts), 0) + + // We attempt to enable the accounts specified. + require.NoError(t, EnableAccountsCli(cliCtx)) + + km, err = w.InitializeKeymanager(cliCtx.Context, &iface.InitializeKeymanagerConfig{SkipMnemonicConfirm: false}) + require.NoError(t, err) + remainingAccounts, err = km.FetchValidatingPublicKeys(cliCtx.Context) + require.NoError(t, err) + require.Equal(t, len(remainingAccounts), 2) + remainingPublicKey1, err := hex.DecodeString(k1.Pubkey) + require.NoError(t, err) + remainingPublicKey2, err := hex.DecodeString(k2.Pubkey) + require.NoError(t, err) + assert.DeepEqual(t, remainingAccounts[0], bytesutil.ToBytes48(remainingPublicKey1)) + assert.DeepEqual(t, remainingAccounts[1], bytesutil.ToBytes48(remainingPublicKey2)) +} diff --git a/validator/accounts/accounts_list.go b/validator/accounts/accounts_list.go index 0066c28d6b..c467d7b8f8 100644 --- a/validator/accounts/accounts_list.go +++ b/validator/accounts/accounts_list.go @@ -93,7 +93,7 @@ func listImportedKeymanagerAccounts( "by running `validator accounts list --show-deposit-data"), ) - pubKeys, err := keymanager.FetchValidatingPublicKeys(ctx) + pubKeys, err := keymanager.FetchAllValidatingPublicKeys(ctx) if err != nil { return errors.Wrap(err, "could not fetch validating public keys") } @@ -134,7 +134,7 @@ func listDerivedKeymanagerAccounts( au := aurora.NewAurora(true) fmt.Printf("(keymanager kind) %s\n", au.BrightGreen("derived, (HD) hierarchical-deterministic").Bold()) fmt.Printf("(derivation format) %s\n", au.BrightGreen(keymanager.KeymanagerOpts().DerivedPathStructure).Bold()) - validatingPubKeys, err := keymanager.FetchValidatingPublicKeys(ctx) + validatingPubKeys, err := keymanager.FetchAllValidatingPublicKeys(ctx) if err != nil { return errors.Wrap(err, "could not fetch validating public keys") } @@ -226,7 +226,7 @@ func listRemoteKeymanagerAccounts( fmt.Println(" ") fmt.Printf("%s\n", au.BrightGreen("Configuration options").Bold()) fmt.Println(opts) - validatingPubKeys, err := keymanager.FetchValidatingPublicKeys(ctx) + validatingPubKeys, err := keymanager.FetchAllValidatingPublicKeys(ctx) if err != nil { return errors.Wrap(err, "could not fetch validating public keys") } diff --git a/validator/accounts/accounts_list_test.go b/validator/accounts/accounts_list_test.go index f5de93ebba..2d7a21bbd5 100644 --- a/validator/accounts/accounts_list_test.go +++ b/validator/accounts/accounts_list_test.go @@ -33,6 +33,10 @@ func (m *mockRemoteKeymanager) FetchValidatingPublicKeys(_ context.Context) ([][ return m.publicKeys, nil } +func (m *mockRemoteKeymanager) FetchAllValidatingPublicKeys(_ context.Context) ([][48]byte, error) { + return m.publicKeys, nil +} + func (m *mockRemoteKeymanager) Sign(context.Context, *validatorpb.SignRequest) (bls.Signature, error) { return nil, nil } diff --git a/validator/accounts/cmd_accounts.go b/validator/accounts/cmd_accounts.go index ea39654f06..3d0cf943b8 100644 --- a/validator/accounts/cmd_accounts.go +++ b/validator/accounts/cmd_accounts.go @@ -70,6 +70,56 @@ this command outputs a deposit data string which is required to become a validat return nil }, }, + { + Name: "disable", + Description: "Disable the selected accounts from a users wallet.", + Flags: cmd.WrapFlags([]cli.Flag{ + flags.WalletDirFlag, + flags.WalletPasswordFileFlag, + flags.DisablePublicKeysFlag, + featureconfig.ToledoTestnet, + featureconfig.PyrmontTestnet, + cmd.AcceptTosFlag, + }), + Before: func(cliCtx *cli.Context) error { + if err := cmd.LoadFlagsFromConfig(cliCtx, cliCtx.Command.Flags); err != nil { + return err + } + return tos.VerifyTosAcceptedOrPrompt(cliCtx) + }, + Action: func(cliCtx *cli.Context) error { + featureconfig.ConfigureValidator(cliCtx) + if err := DisableAccountsCli(cliCtx); err != nil { + log.Fatalf("Could not disable account: %v", err) + } + return nil + }, + }, + { + Name: "enable", + Description: "Enable the selected accounts from a users wallet.", + Flags: cmd.WrapFlags([]cli.Flag{ + flags.WalletDirFlag, + flags.WalletPasswordFileFlag, + flags.EnablePublicKeysFlag, + featureconfig.ToledoTestnet, + featureconfig.PyrmontTestnet, + cmd.AcceptTosFlag, + }), + Before: func(cliCtx *cli.Context) error { + if err := cmd.LoadFlagsFromConfig(cliCtx, cliCtx.Command.Flags); err != nil { + return err + } + return tos.VerifyTosAcceptedOrPrompt(cliCtx) + }, + Action: func(cliCtx *cli.Context) error { + featureconfig.ConfigureValidator(cliCtx) + if err := EnableAccountsCli(cliCtx); err != nil { + log.Fatalf("Could not enable account: %v", err) + } + return nil + }, + }, { Name: "list", Description: "Lists all validator accounts in a user's wallet directory", diff --git a/validator/accounts/prompt/prompt.go b/validator/accounts/prompt/prompt.go index 1f0332ebbb..0bcccc72ca 100644 --- a/validator/accounts/prompt/prompt.go +++ b/validator/accounts/prompt/prompt.go @@ -27,6 +27,10 @@ const ( SelectAccountsBackupPromptText = "Select the account(s) you wish to backup" // SelectAccountsVoluntaryExitPromptText -- SelectAccountsVoluntaryExitPromptText = "Select the account(s) on which you wish to perform a voluntary exit" + // SelectAccountsDisablePromptText -- + SelectAccountsDisablePromptText = "Select the account(s) you would like to disable" + // SelectAccountsEnablePromptText -- + SelectAccountsEnablePromptText = "Select the account(s) you would like to enable" ) var ( diff --git a/validator/accounts/wallet_create_test.go b/validator/accounts/wallet_create_test.go index 366fe011f2..2e3de86da0 100644 --- a/validator/accounts/wallet_create_test.go +++ b/validator/accounts/wallet_create_test.go @@ -45,6 +45,8 @@ type testWalletConfig struct { backupDir string keysDir string deletePublicKeys string + enablePublicKeys string + disablePublicKeys string voluntaryExitPublicKeys string backupPublicKeys string backupPasswordFile string @@ -66,6 +68,8 @@ func setupWalletCtx( set.String(flags.KeysDirFlag.Name, cfg.keysDir, "") set.String(flags.KeymanagerKindFlag.Name, cfg.keymanagerKind.String(), "") set.String(flags.DeletePublicKeysFlag.Name, cfg.deletePublicKeys, "") + set.String(flags.DisablePublicKeysFlag.Name, cfg.disablePublicKeys, "") + set.String(flags.EnablePublicKeysFlag.Name, cfg.enablePublicKeys, "") set.String(flags.VoluntaryExitPublicKeysFlag.Name, cfg.voluntaryExitPublicKeys, "") set.String(flags.BackupDirFlag.Name, cfg.backupDir, "") set.String(flags.BackupPasswordFile.Name, cfg.backupPasswordFile, "") @@ -85,6 +89,8 @@ func setupWalletCtx( assert.NoError(tb, set.Set(flags.KeysDirFlag.Name, cfg.keysDir)) assert.NoError(tb, set.Set(flags.KeymanagerKindFlag.Name, cfg.keymanagerKind.String())) assert.NoError(tb, set.Set(flags.DeletePublicKeysFlag.Name, cfg.deletePublicKeys)) + assert.NoError(tb, set.Set(flags.DisablePublicKeysFlag.Name, cfg.disablePublicKeys)) + assert.NoError(tb, set.Set(flags.EnablePublicKeysFlag.Name, cfg.enablePublicKeys)) assert.NoError(tb, set.Set(flags.VoluntaryExitPublicKeysFlag.Name, cfg.voluntaryExitPublicKeys)) assert.NoError(tb, set.Set(flags.BackupDirFlag.Name, cfg.backupDir)) assert.NoError(tb, set.Set(flags.BackupPublicKeysFlag.Name, cfg.backupPublicKeys)) diff --git a/validator/client/validator.go b/validator/client/validator.go index 2da00184fd..c4141aaad3 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -477,7 +477,7 @@ func (v *validator) UpdateDuties(ctx context.Context, slot uint64) error { } // RolesAt slot returns the validator roles at the given slot. Returns nil if the -// validator is known to not have a roles at the at slot. Returns UNKNOWN if the +// validator is known to not have a roles at the slot. Returns UNKNOWN if the // validator assignments are unknown. Otherwise returns a valid ValidatorRole map. func (v *validator) RolesAt(ctx context.Context, slot uint64) (map[[48]byte][]ValidatorRole, error) { rolesAt := make(map[[48]byte][]ValidatorRole) diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index 1958758bb8..f94eff9f75 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -61,6 +61,16 @@ func (m *mockKeymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]b return keys, nil } +func (m *mockKeymanager) FetchAllValidatingPublicKeys(ctx context.Context) ([][48]byte, error) { + m.lock.RLock() + defer m.lock.RUnlock() + keys := make([][48]byte, 0) + for pubKey := range m.keysMap { + keys = append(keys, pubKey) + } + return keys, nil +} + func (m *mockKeymanager) Sign(ctx context.Context, req *validatorpb.SignRequest) (bls.Signature, error) { pubKey := [48]byte{} copy(pubKey[:], req.PublicKey) diff --git a/validator/flags/flags.go b/validator/flags/flags.go index 0fa36edc1e..8854060579 100644 --- a/validator/flags/flags.go +++ b/validator/flags/flags.go @@ -186,6 +186,20 @@ var ( Usage: "Comma-separated list of public key hex strings to specify which validator accounts to delete", Value: "", } + // DisablePublicKeysFlag defines a comma-separated list of hex string public keys + // for accounts which a user desires to disable for their wallet. + DisablePublicKeysFlag = &cli.StringFlag{ + Name: "disable-public-keys", + Usage: "Comma-separated list of public key hex strings to specify which validator accounts to disable", + Value: "", + } + // EnablePublicKeysFlag defines a comma-separated list of hex string public keys + // for accounts which a user desires to enable for their wallet. + EnablePublicKeysFlag = &cli.StringFlag{ + Name: "enable-public-keys", + Usage: "Comma-separated list of public key hex strings to specify which validator accounts to enable", + Value: "", + } // BackupPublicKeysFlag defines a comma-separated list of hex string public keys // for accounts which a user desires to backup from their wallet. BackupPublicKeysFlag = &cli.StringFlag{ diff --git a/validator/keymanager/derived/keymanager.go b/validator/keymanager/derived/keymanager.go index 5ab59fcfa4..31bc35e518 100644 --- a/validator/keymanager/derived/keymanager.go +++ b/validator/keymanager/derived/keymanager.go @@ -356,6 +356,11 @@ func (dr *Keymanager) FetchValidatingPublicKeys(_ context.Context) ([][48]byte, return result, nil } +// FetchAllValidatingPublicKeys fetches the list of all public keys (including disabled ones) from the keymanager. +func (dr *Keymanager) FetchAllValidatingPublicKeys(ctx context.Context) ([][48]byte, error) { + return dr.FetchValidatingPublicKeys(ctx) +} + // FetchValidatingPrivateKeys fetches the list of validating private keys from the keymanager. func (dr *Keymanager) FetchValidatingPrivateKeys(ctx context.Context) ([][32]byte, error) { lock.RLock() diff --git a/validator/keymanager/imported/keymanager.go b/validator/keymanager/imported/keymanager.go index d2ca4df84f..f8fdccd425 100644 --- a/validator/keymanager/imported/keymanager.go +++ b/validator/keymanager/imported/keymanager.go @@ -44,8 +44,9 @@ const ( // KeymanagerOpts for a imported keymanager. type KeymanagerOpts struct { - EIPVersion string `json:"direct_eip_version"` - Version string `json:"direct_version"` + EIPVersion string `json:"direct_eip_version"` + Version string `json:"direct_version"` + DisabledPublicKeys [][]byte `json:"disabled_public_keys"` } // Keymanager implementation for imported keystores utilizing EIP-2335. @@ -66,8 +67,9 @@ type AccountStore struct { // DefaultKeymanagerOpts for a imported keymanager implementation. func DefaultKeymanagerOpts() *KeymanagerOpts { return &KeymanagerOpts{ - EIPVersion: eipVersion, - Version: "2", + EIPVersion: eipVersion, + Version: "2", + DisabledPublicKeys: [][]byte{}, } } @@ -111,6 +113,7 @@ func NewKeymanager(ctx context.Context, cfg *SetupConfig) (*Keymanager, error) { func NewInteropKeymanager(_ context.Context, offset, numValidatorKeys uint64) (*Keymanager, error) { k := &Keymanager{ accountsChangedFeed: new(event.Feed), + opts: DefaultKeymanagerOpts(), } if numValidatorKeys == 0 { return k, nil @@ -256,11 +259,33 @@ func (dr *Keymanager) DeleteAccounts(ctx context.Context, publicKeys [][]byte) e return nil } -// FetchValidatingPublicKeys fetches the list of public keys from the imported account keystores. +// FetchValidatingPublicKeys fetches the list of active public keys from the imported account keystores. func (dr *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) { ctx, span := trace.StartSpan(ctx, "keymanager.FetchValidatingPublicKeys") defer span.End() + lock.RLock() + keys := orderedPublicKeys + disabledPublicKeys := dr.KeymanagerOpts().DisabledPublicKeys + result := make([][48]byte, 0) + existingDisabledPubKeys := make(map[[48]byte]bool, len(disabledPublicKeys)) + for _, pk := range disabledPublicKeys { + existingDisabledPubKeys[bytesutil.ToBytes48(pk)] = true + } + for _, pk := range keys { + if _, ok := existingDisabledPubKeys[pk]; !ok { + result = append(result, pk) + } + } + lock.RUnlock() + return result, nil +} + +// FetchAllValidatingPublicKeys fetches the list of all public keys (including disabled ones) from the imported account keystores. +func (dr *Keymanager) FetchAllValidatingPublicKeys(ctx context.Context) ([][48]byte, error) { + ctx, span := trace.StartSpan(ctx, "keymanager.FetchValidatingPublicKeys") + defer span.End() + lock.RLock() keys := orderedPublicKeys result := make([][48]byte, len(keys)) @@ -278,12 +303,19 @@ func (dr *Keymanager) FetchValidatingPrivateKeys(ctx context.Context) ([][32]byt if err != nil { return nil, errors.Wrap(err, "could not retrieve public keys") } + disabledPublicKeys := dr.KeymanagerOpts().DisabledPublicKeys + existingDisabledPubKeys := make(map[[48]byte]bool, len(disabledPublicKeys)) + for _, pk := range disabledPublicKeys { + existingDisabledPubKeys[bytesutil.ToBytes48(pk)] = true + } for i, pk := range pubKeys { - seckey, ok := secretKeysCache[pk] - if !ok { - return nil, errors.New("Could not fetch private key") + if _, ok := existingDisabledPubKeys[pk]; !ok { + seckey, ok := secretKeysCache[pk] + if !ok { + return nil, errors.New("Could not fetch private key") + } + privKeys[i] = bytesutil.ToBytes32(seckey.Marshal()) } - privKeys[i] = bytesutil.ToBytes32(seckey.Marshal()) } return privKeys, nil } diff --git a/validator/keymanager/imported/keymanager_test.go b/validator/keymanager/imported/keymanager_test.go index 6bd7bc626b..8c5d3524a5 100644 --- a/validator/keymanager/imported/keymanager_test.go +++ b/validator/keymanager/imported/keymanager_test.go @@ -28,6 +28,7 @@ func TestImportedKeymanager_RemoveAccounts(t *testing.T) { dr := &Keymanager{ wallet: wallet, accountsStore: &AccountStore{}, + opts: DefaultKeymanagerOpts(), } numAccounts := 5 ctx := context.Background() @@ -78,6 +79,46 @@ func TestImportedKeymanager_FetchValidatingPublicKeys(t *testing.T) { dr := &Keymanager{ wallet: wallet, accountsStore: &AccountStore{}, + opts: DefaultKeymanagerOpts(), + } + // First, generate accounts and their keystore.json files. + ctx := context.Background() + numAccounts := 10 + wantedPubKeys := make([][48]byte, 0) + for i := 0; i < numAccounts; i++ { + privKey, err := bls.RandKey() + require.NoError(t, err) + pubKey := bytesutil.ToBytes48(privKey.PublicKey().Marshal()) + if i == 0 { + // Manually disable the first public key by adding it to the keymanager options + dr.opts.DisabledPublicKeys = append(dr.opts.DisabledPublicKeys, pubKey[:]) + } else { + wantedPubKeys = append(wantedPubKeys, pubKey) + } + dr.accountsStore.PublicKeys = append(dr.accountsStore.PublicKeys, pubKey[:]) + dr.accountsStore.PrivateKeys = append(dr.accountsStore.PrivateKeys, privKey.Marshal()) + } + require.NoError(t, dr.initializeKeysCachesFromKeystore()) + publicKeys, err := dr.FetchValidatingPublicKeys(ctx) + require.NoError(t, err) + assert.Equal(t, numAccounts-1, len(publicKeys)) + // FetchValidatingPublicKeys is also used in generating the output of account list + // therefore the results must be in the same order as the order in which the accounts were derived + for i, key := range wantedPubKeys { + assert.Equal(t, key, publicKeys[i]) + } +} + +func TestImportedKeymanager_FetchAllValidatingPublicKeys(t *testing.T) { + password := "secretPassw0rd$1999" + wallet := &mock.Wallet{ + Files: make(map[string]map[string][]byte), + WalletPassword: password, + } + dr := &Keymanager{ + wallet: wallet, + accountsStore: &AccountStore{}, + opts: DefaultKeymanagerOpts(), } // First, generate accounts and their keystore.json files. ctx := context.Background() @@ -92,10 +133,10 @@ func TestImportedKeymanager_FetchValidatingPublicKeys(t *testing.T) { dr.accountsStore.PrivateKeys = append(dr.accountsStore.PrivateKeys, privKey.Marshal()) } require.NoError(t, dr.initializeKeysCachesFromKeystore()) - publicKeys, err := dr.FetchValidatingPublicKeys(ctx) + publicKeys, err := dr.FetchAllValidatingPublicKeys(ctx) require.NoError(t, err) assert.Equal(t, numAccounts, len(publicKeys)) - // FetchValidatingPublicKeys is also used in generating the output of account list + // FetchAllValidatingPublicKeys is also used in generating the output of account list // therefore the results must be in the same order as the order in which the accounts were derived for i, key := range wantedPubKeys { assert.Equal(t, key, publicKeys[i]) @@ -111,6 +152,7 @@ func TestImportedKeymanager_FetchValidatingPrivateKeys(t *testing.T) { dr := &Keymanager{ wallet: wallet, accountsStore: &AccountStore{}, + opts: DefaultKeymanagerOpts(), } // First, generate accounts and their keystore.json files. ctx := context.Background() @@ -146,6 +188,7 @@ func TestImportedKeymanager_Sign(t *testing.T) { dr := &Keymanager{ wallet: wallet, accountsStore: &AccountStore{}, + opts: DefaultKeymanagerOpts(), } // First, generate accounts and their keystore.json files. diff --git a/validator/keymanager/remote/keymanager.go b/validator/keymanager/remote/keymanager.go index 2a280a0cff..666f0d3013 100644 --- a/validator/keymanager/remote/keymanager.go +++ b/validator/keymanager/remote/keymanager.go @@ -189,6 +189,11 @@ func (k *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, return pubKeys, nil } +// FetchAllValidatingPublicKeys fetches the list of all public keys, including disabled ones. +func (dr *Keymanager) FetchAllValidatingPublicKeys(ctx context.Context) ([][48]byte, error) { + return dr.FetchValidatingPublicKeys(ctx) +} + // Sign signs a message for a validator key via a gRPC request. func (k *Keymanager) Sign(ctx context.Context, req *validatorpb.SignRequest) (bls.Signature, error) { resp, err := k.client.Sign(ctx, req) diff --git a/validator/keymanager/types.go b/validator/keymanager/types.go index 187ed2f8be..f1778137ba 100644 --- a/validator/keymanager/types.go +++ b/validator/keymanager/types.go @@ -10,8 +10,10 @@ import ( // IKeymanager defines a general keymanager interface for Prysm wallets. type IKeymanager interface { - // FetchValidatingKeys fetches the list of public keys that should be used to validate with. + // FetchValidatingKeys fetches the list of active public keys that should be used to validate with. FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) + // FetchAllValidatingKeys fetches the list of all public keys, including disabled ones. + FetchAllValidatingPublicKeys(ctx context.Context) ([][48]byte, error) // Sign signs a message using a validator key. Sign(context.Context, *validatorpb.SignRequest) (bls.Signature, error) } diff --git a/validator/rpc/wallet.go b/validator/rpc/wallet.go index 8abd995bd2..abdaba7f02 100644 --- a/validator/rpc/wallet.go +++ b/validator/rpc/wallet.go @@ -4,7 +4,9 @@ import ( "context" "encoding/hex" "encoding/json" + "fmt" "io/ioutil" + "reflect" "strings" ptypes "github.com/gogo/protobuf/types" @@ -199,10 +201,12 @@ func (s *Server) WalletConfig(ctx context.Context, _ *ptypes.Empty) (*pb.WalletR if err != nil { return nil, status.Errorf(codes.Internal, "Could not parse keymanager config: %v", err) } - var config map[string]string - if err := json.Unmarshal(encoded, &config); err != nil { + kmOpts := &imported.KeymanagerOpts{} + if err := json.Unmarshal(encoded, kmOpts); err != nil { return nil, status.Errorf(codes.Internal, "Could not JSON unmarshal keymanager config: %v", err) } + config := KmOptsToConfig(kmOpts) + return &pb.WalletResponse{ WalletPath: s.walletDir, KeymanagerKind: keymanagerKind, @@ -210,6 +214,28 @@ func (s *Server) WalletConfig(ctx context.Context, _ *ptypes.Empty) (*pb.WalletR }, nil } +// Convert KeymanagerOpts struct to a map[string]string +func KmOptsToConfig(opts *imported.KeymanagerOpts) map[string]string { + val := reflect.ValueOf(opts).Elem() + var config = make(map[string]string, val.NumField()) + for i := 0; i < val.NumField(); i++ { + f := val.Type().Field(i) + v := val.Field(i) + jsonName := strings.Split(f.Tag.Get("json"), ",")[0] // use split to ignore tag "options" like omitempty, etc. + + if keys, ok := v.Interface().([][]byte); ok { + str := make([]string, len(keys)) + for i, key := range keys { + str[i] = fmt.Sprintf("%q", key) + } + config[jsonName] = strings.Join(str, ",") + } else { + config[jsonName] = fmt.Sprint(v) + } + } + return config +} + // GenerateMnemonic creates a new, random bip39 mnemonic phrase. func (s *Server) GenerateMnemonic(_ context.Context, _ *ptypes.Empty) (*pb.GenerateMnemonicResponse, error) { mnemonicRandomness := make([]byte, 32) diff --git a/validator/rpc/wallet_test.go b/validator/rpc/wallet_test.go index b7a35e0e6f..0405546f5a 100644 --- a/validator/rpc/wallet_test.go +++ b/validator/rpc/wallet_test.go @@ -208,10 +208,7 @@ func TestServer_WalletConfig(t *testing.T) { require.NoError(t, err) expectedConfig := imported.DefaultKeymanagerOpts() - enc, err := json.Marshal(expectedConfig) - require.NoError(t, err) - var jsonMap map[string]string - require.NoError(t, json.Unmarshal(enc, &jsonMap)) + jsonMap := KmOptsToConfig(expectedConfig) assert.DeepEqual(t, resp, &pb.WalletResponse{ WalletPath: localWalletDir, KeymanagerKind: pb.KeymanagerKind_IMPORTED,