mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
Add Ability to Specify All Public Keys When Exiting Validators (#8399)
* add programmatic voluntary exit * add exit all flag * test * lint * add multiple exits test * fix test Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
This commit is contained in:
@@ -99,54 +99,64 @@ func interact(
|
||||
r io.Reader,
|
||||
validatingPublicKeys [][48]byte,
|
||||
) (rawPubKeys [][]byte, formattedPubKeys []string, err error) {
|
||||
// Allow the user to interactively select the accounts to exit or optionally
|
||||
// provide them via cli flags as a string of comma-separated, hex strings.
|
||||
filteredPubKeys, err := filterPublicKeysFromUserInput(
|
||||
cliCtx,
|
||||
flags.VoluntaryExitPublicKeysFlag,
|
||||
validatingPublicKeys,
|
||||
prompt.SelectAccountsVoluntaryExitPromptText,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "could not filter public keys for voluntary exit")
|
||||
}
|
||||
rawPubKeys = make([][]byte, len(filteredPubKeys))
|
||||
formattedPubKeys = make([]string, len(filteredPubKeys))
|
||||
for i, pk := range filteredPubKeys {
|
||||
pubKeyBytes := pk.Marshal()
|
||||
rawPubKeys[i] = pubKeyBytes
|
||||
formattedPubKeys[i] = fmt.Sprintf("%#x", bytesutil.Trunc(pubKeyBytes))
|
||||
}
|
||||
allAccountStr := strings.Join(formattedPubKeys, ", ")
|
||||
if !cliCtx.IsSet(flags.VoluntaryExitPublicKeysFlag.Name) {
|
||||
if len(filteredPubKeys) == 1 {
|
||||
promptText := "Are you sure you want to perform a voluntary exit on 1 account? (%s) Y/N"
|
||||
resp, err := promptutil.ValidatePrompt(
|
||||
r, fmt.Sprintf(promptText, au.BrightGreen(formattedPubKeys[0])), promptutil.ValidateYesOrNo,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if strings.EqualFold(resp, "n") {
|
||||
return nil, nil, nil
|
||||
}
|
||||
} else {
|
||||
promptText := "Are you sure you want to perform a voluntary exit on %d accounts? (%s) Y/N"
|
||||
if len(filteredPubKeys) == len(validatingPublicKeys) {
|
||||
promptText = fmt.Sprintf(
|
||||
"Are you sure you want to perform a voluntary exit on all accounts? Y/N (%s)",
|
||||
au.BrightGreen(allAccountStr))
|
||||
if !cliCtx.IsSet(flags.ExitAllFlag.Name) {
|
||||
// Allow the user to interactively select the accounts to exit or optionally
|
||||
// provide them via cli flags as a string of comma-separated, hex strings.
|
||||
filteredPubKeys, err := filterPublicKeysFromUserInput(
|
||||
cliCtx,
|
||||
flags.VoluntaryExitPublicKeysFlag,
|
||||
validatingPublicKeys,
|
||||
prompt.SelectAccountsVoluntaryExitPromptText,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "could not filter public keys for voluntary exit")
|
||||
}
|
||||
rawPubKeys = make([][]byte, len(filteredPubKeys))
|
||||
formattedPubKeys = make([]string, len(filteredPubKeys))
|
||||
for i, pk := range filteredPubKeys {
|
||||
pubKeyBytes := pk.Marshal()
|
||||
rawPubKeys[i] = pubKeyBytes
|
||||
formattedPubKeys[i] = fmt.Sprintf("%#x", bytesutil.Trunc(pubKeyBytes))
|
||||
}
|
||||
allAccountStr := strings.Join(formattedPubKeys, ", ")
|
||||
if !cliCtx.IsSet(flags.VoluntaryExitPublicKeysFlag.Name) {
|
||||
if len(filteredPubKeys) == 1 {
|
||||
promptText := "Are you sure you want to perform a voluntary exit on 1 account? (%s) Y/N"
|
||||
resp, err := promptutil.ValidatePrompt(
|
||||
r, fmt.Sprintf(promptText, au.BrightGreen(formattedPubKeys[0])), promptutil.ValidateYesOrNo,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if strings.EqualFold(resp, "n") {
|
||||
return nil, nil, nil
|
||||
}
|
||||
} else {
|
||||
promptText = fmt.Sprintf(promptText, len(filteredPubKeys), au.BrightGreen(allAccountStr))
|
||||
}
|
||||
resp, err := promptutil.ValidatePrompt(r, promptText, promptutil.ValidateYesOrNo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if strings.EqualFold(resp, "n") {
|
||||
return nil, nil, nil
|
||||
promptText := "Are you sure you want to perform a voluntary exit on %d accounts? (%s) Y/N"
|
||||
if len(filteredPubKeys) == len(validatingPublicKeys) {
|
||||
promptText = fmt.Sprintf(
|
||||
"Are you sure you want to perform a voluntary exit on all accounts? Y/N (%s)",
|
||||
au.BrightGreen(allAccountStr))
|
||||
} else {
|
||||
promptText = fmt.Sprintf(promptText, len(filteredPubKeys), au.BrightGreen(allAccountStr))
|
||||
}
|
||||
resp, err := promptutil.ValidatePrompt(r, promptText, promptutil.ValidateYesOrNo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if strings.EqualFold(resp, "n") {
|
||||
return nil, nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rawPubKeys = make([][]byte, len(validatingPublicKeys))
|
||||
formattedPubKeys = make([]string, len(validatingPublicKeys))
|
||||
for i, pk := range validatingPublicKeys {
|
||||
rawPubKeys[i] = pk[:]
|
||||
formattedPubKeys[i] = fmt.Sprintf("%#x", bytesutil.Trunc(pk[:]))
|
||||
}
|
||||
fmt.Printf("About to perform a voluntary exit of %d accounts\n", len(rawPubKeys))
|
||||
}
|
||||
|
||||
promptHeader := au.Red("===============IMPORTANT===============")
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -20,7 +21,7 @@ import (
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestExitAccountsCli_Ok(t *testing.T) {
|
||||
func TestExitAccountsCli_OK(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockValidatorClient := mock.NewMockBeaconNodeValidatorClient(ctrl)
|
||||
@@ -106,6 +107,107 @@ func TestExitAccountsCli_Ok(t *testing.T) {
|
||||
assert.Equal(t, "0x"+keystore.Pubkey[:12], formattedExitedKeys[0])
|
||||
}
|
||||
|
||||
func TestExitAccountsCli_OK_AllPublicKeys(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockValidatorClient := mock.NewMockBeaconNodeValidatorClient(ctrl)
|
||||
mockNodeClient := mock.NewMockNodeClient(ctrl)
|
||||
|
||||
mockValidatorClient.EXPECT().
|
||||
ValidatorIndex(gomock.Any(), gomock.Any()).
|
||||
Return(ðpb.ValidatorIndexResponse{Index: 0}, nil)
|
||||
|
||||
mockValidatorClient.EXPECT().
|
||||
ValidatorIndex(gomock.Any(), gomock.Any()).
|
||||
Return(ðpb.ValidatorIndexResponse{Index: 1}, nil)
|
||||
|
||||
// Any time in the past will suffice
|
||||
genesisTime := &types.Timestamp{
|
||||
Seconds: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
|
||||
}
|
||||
|
||||
mockNodeClient.EXPECT().
|
||||
GetGenesis(gomock.Any(), gomock.Any()).
|
||||
Times(2).
|
||||
Return(ðpb.Genesis{GenesisTime: genesisTime}, nil)
|
||||
|
||||
mockValidatorClient.EXPECT().
|
||||
DomainData(gomock.Any(), gomock.Any()).
|
||||
Times(2).
|
||||
Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil)
|
||||
|
||||
mockValidatorClient.EXPECT().
|
||||
ProposeExit(gomock.Any(), gomock.AssignableToTypeOf(ðpb.SignedVoluntaryExit{})).
|
||||
Times(2).
|
||||
Return(ðpb.ProposeExitResponse{}, nil)
|
||||
|
||||
walletDir, _, passwordFilePath := setupWalletAndPasswordsDir(t)
|
||||
// Write a directory where we will import keys from.
|
||||
keysDir := filepath.Join(t.TempDir(), "keysDir")
|
||||
require.NoError(t, os.MkdirAll(keysDir, os.ModePerm))
|
||||
|
||||
// Create keystore file in the keys directory we can then import from in our wallet.
|
||||
keystore1, _ := createKeystore(t, keysDir)
|
||||
time.Sleep(time.Second)
|
||||
keystore2, _ := createKeystore(t, keysDir)
|
||||
time.Sleep(time.Second)
|
||||
|
||||
// We initialize a wallet with a imported keymanager.
|
||||
cliCtx := setupWalletCtx(t, &testWalletConfig{
|
||||
// Wallet configuration flags.
|
||||
walletDir: walletDir,
|
||||
keymanagerKind: keymanager.Imported,
|
||||
walletPasswordFile: passwordFilePath,
|
||||
accountPasswordFile: passwordFilePath,
|
||||
// Flag required for ImportAccounts to work.
|
||||
keysDir: keysDir,
|
||||
// Exit all public keys.
|
||||
exitAll: true,
|
||||
})
|
||||
_, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{
|
||||
WalletCfg: &wallet.Config{
|
||||
WalletDir: walletDir,
|
||||
KeymanagerKind: keymanager.Imported,
|
||||
WalletPassword: password,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, ImportAccountsCli(cliCtx))
|
||||
|
||||
validatingPublicKeys, keymanager, err := prepareWallet(cliCtx)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, validatingPublicKeys)
|
||||
require.NotNil(t, keymanager)
|
||||
|
||||
// Prepare user input for final confirmation step
|
||||
var stdin bytes.Buffer
|
||||
stdin.Write([]byte(exitPassphrase))
|
||||
rawPubKeys, formattedPubKeys, err := interact(cliCtx, &stdin, validatingPublicKeys)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rawPubKeys)
|
||||
require.NotNil(t, formattedPubKeys)
|
||||
|
||||
cfg := performExitCfg{
|
||||
mockValidatorClient,
|
||||
mockNodeClient,
|
||||
keymanager,
|
||||
rawPubKeys,
|
||||
formattedPubKeys,
|
||||
}
|
||||
rawExitedKeys, formattedExitedKeys, err := performExit(cliCtx, cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(rawExitedKeys))
|
||||
assert.DeepEqual(t, rawPubKeys, rawExitedKeys)
|
||||
require.Equal(t, 2, len(formattedExitedKeys))
|
||||
wantedFormatted := []string{
|
||||
"0x" + keystore1.Pubkey[:12],
|
||||
"0x" + keystore2.Pubkey[:12],
|
||||
}
|
||||
sort.Strings(wantedFormatted)
|
||||
sort.Strings(formattedExitedKeys)
|
||||
require.DeepEqual(t, wantedFormatted, formattedExitedKeys)
|
||||
}
|
||||
|
||||
func TestPrepareWallet_EmptyWalletReturnsError(t *testing.T) {
|
||||
imported.ResetCaches()
|
||||
walletDir, _, passwordFilePath := setupWalletAndPasswordsDir(t)
|
||||
|
||||
@@ -142,6 +142,7 @@ var AccountCommands = &cli.Command{
|
||||
flags.GrpcHeadersFlag,
|
||||
flags.GrpcRetriesFlag,
|
||||
flags.GrpcRetryDelayFlag,
|
||||
flags.ExitAllFlag,
|
||||
featureconfig.Mainnet,
|
||||
featureconfig.PyrmontTestnet,
|
||||
featureconfig.ToledoTestnet,
|
||||
|
||||
@@ -37,23 +37,24 @@ func init() {
|
||||
}
|
||||
|
||||
type testWalletConfig struct {
|
||||
walletDir string
|
||||
passwordsDir string
|
||||
backupDir string
|
||||
keysDir string
|
||||
deletePublicKeys string
|
||||
enablePublicKeys string
|
||||
disablePublicKeys string
|
||||
voluntaryExitPublicKeys string
|
||||
backupPublicKeys string
|
||||
backupPasswordFile string
|
||||
walletPasswordFile string
|
||||
accountPasswordFile string
|
||||
privateKeyFile string
|
||||
grpcHeaders string
|
||||
exitAll bool
|
||||
skipDepositConfirm bool
|
||||
numAccounts int64
|
||||
keymanagerKind keymanager.Kind
|
||||
numAccounts int64
|
||||
grpcHeaders string
|
||||
privateKeyFile string
|
||||
accountPasswordFile string
|
||||
walletPasswordFile string
|
||||
backupPasswordFile string
|
||||
backupPublicKeys string
|
||||
voluntaryExitPublicKeys string
|
||||
disablePublicKeys string
|
||||
enablePublicKeys string
|
||||
deletePublicKeys string
|
||||
keysDir string
|
||||
backupDir string
|
||||
passwordsDir string
|
||||
walletDir string
|
||||
}
|
||||
|
||||
func setupWalletCtx(
|
||||
@@ -77,6 +78,7 @@ func setupWalletCtx(
|
||||
set.Int64(flags.NumAccountsFlag.Name, cfg.numAccounts, "")
|
||||
set.Bool(flags.SkipDepositConfirmationFlag.Name, cfg.skipDepositConfirm, "")
|
||||
set.Bool(flags.SkipMnemonic25thWordCheckFlag.Name, true, "")
|
||||
set.Bool(flags.ExitAllFlag.Name, cfg.exitAll, "")
|
||||
set.String(flags.GrpcHeadersFlag.Name, cfg.grpcHeaders, "")
|
||||
|
||||
if cfg.privateKeyFile != "" {
|
||||
@@ -98,6 +100,7 @@ func setupWalletCtx(
|
||||
assert.NoError(tb, set.Set(flags.AccountPasswordFileFlag.Name, cfg.accountPasswordFile))
|
||||
assert.NoError(tb, set.Set(flags.NumAccountsFlag.Name, strconv.Itoa(int(cfg.numAccounts))))
|
||||
assert.NoError(tb, set.Set(flags.SkipDepositConfirmationFlag.Name, strconv.FormatBool(cfg.skipDepositConfirm)))
|
||||
assert.NoError(tb, set.Set(flags.ExitAllFlag.Name, strconv.FormatBool(cfg.exitAll)))
|
||||
assert.NoError(tb, set.Set(flags.GrpcHeadersFlag.Name, cfg.grpcHeaders))
|
||||
return cli.NewContext(&app, set, nil)
|
||||
}
|
||||
|
||||
@@ -214,6 +214,12 @@ var (
|
||||
"a voluntary exit",
|
||||
Value: "",
|
||||
}
|
||||
// ExitAllFlag allows stakers to select all validating keys for exit. This will still require the staker
|
||||
// to confirm a prompt for this action given it is a dangerous one.
|
||||
ExitAllFlag = &cli.BoolFlag{
|
||||
Name: "exit-all",
|
||||
Usage: "Exit all validators. This will still require the staker to confirm a prompt for the action",
|
||||
}
|
||||
// BackupPasswordFile for encrypting accounts a user wishes to back up.
|
||||
BackupPasswordFile = &cli.StringFlag{
|
||||
Name: "backup-password-file",
|
||||
|
||||
Reference in New Issue
Block a user