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:
Raul Jordan
2021-02-11 10:50:16 -06:00
committed by GitHub
parent 7d2f7ae9e1
commit e236dedc32
5 changed files with 183 additions and 61 deletions

View File

@@ -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===============")

View File

@@ -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(&ethpb.ValidatorIndexResponse{Index: 0}, nil)
mockValidatorClient.EXPECT().
ValidatorIndex(gomock.Any(), gomock.Any()).
Return(&ethpb.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(&ethpb.Genesis{GenesisTime: genesisTime}, nil)
mockValidatorClient.EXPECT().
DomainData(gomock.Any(), gomock.Any()).
Times(2).
Return(&ethpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil)
mockValidatorClient.EXPECT().
ProposeExit(gomock.Any(), gomock.AssignableToTypeOf(&ethpb.SignedVoluntaryExit{})).
Times(2).
Return(&ethpb.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)

View File

@@ -142,6 +142,7 @@ var AccountCommands = &cli.Command{
flags.GrpcHeadersFlag,
flags.GrpcRetriesFlag,
flags.GrpcRetryDelayFlag,
flags.ExitAllFlag,
featureconfig.Mainnet,
featureconfig.PyrmontTestnet,
featureconfig.ToledoTestnet,

View File

@@ -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)
}

View File

@@ -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",