github.com/prysmaticlabs/prysm@v1.4.4/validator/accounts/accounts_exit.go (about)

     1  package accounts
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"strings"
     9  
    10  	"github.com/ethereum/go-ethereum/common/hexutil"
    11  	"github.com/pkg/errors"
    12  	"github.com/prysmaticlabs/prysm/beacon-chain/core/blocks"
    13  	"github.com/prysmaticlabs/prysm/cmd/validator/flags"
    14  	ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1"
    15  	"github.com/prysmaticlabs/prysm/shared/bytesutil"
    16  	"github.com/prysmaticlabs/prysm/shared/cmd"
    17  	"github.com/prysmaticlabs/prysm/shared/grpcutils"
    18  	"github.com/prysmaticlabs/prysm/shared/params"
    19  	"github.com/prysmaticlabs/prysm/shared/promptutil"
    20  	"github.com/prysmaticlabs/prysm/validator/accounts/iface"
    21  	"github.com/prysmaticlabs/prysm/validator/accounts/prompt"
    22  	"github.com/prysmaticlabs/prysm/validator/accounts/wallet"
    23  	"github.com/prysmaticlabs/prysm/validator/client"
    24  	"github.com/prysmaticlabs/prysm/validator/keymanager"
    25  	"github.com/urfave/cli/v2"
    26  	"google.golang.org/grpc"
    27  )
    28  
    29  // PerformExitCfg for account voluntary exits.
    30  type PerformExitCfg struct {
    31  	ValidatorClient  ethpb.BeaconNodeValidatorClient
    32  	NodeClient       ethpb.NodeClient
    33  	Keymanager       keymanager.IKeymanager
    34  	RawPubKeys       [][]byte
    35  	FormattedPubKeys []string
    36  }
    37  
    38  const exitPassphrase = "Exit my validator"
    39  
    40  // ExitAccountsCli performs a voluntary exit on one or more accounts.
    41  func ExitAccountsCli(cliCtx *cli.Context, r io.Reader) error {
    42  	validatingPublicKeys, kManager, err := prepareWallet(cliCtx)
    43  	if err != nil {
    44  		return err
    45  	}
    46  
    47  	rawPubKeys, trimmedPubKeys, err := interact(cliCtx, r, validatingPublicKeys)
    48  	if err != nil {
    49  		return err
    50  	}
    51  	// User decided to cancel the voluntary exit.
    52  	if rawPubKeys == nil && trimmedPubKeys == nil {
    53  		return nil
    54  	}
    55  
    56  	validatorClient, nodeClient, err := prepareClients(cliCtx)
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	cfg := PerformExitCfg{
    62  		*validatorClient,
    63  		*nodeClient,
    64  		kManager,
    65  		rawPubKeys,
    66  		trimmedPubKeys,
    67  	}
    68  	rawExitedKeys, trimmedExitedKeys, err := PerformVoluntaryExit(cliCtx.Context, cfg)
    69  	if err != nil {
    70  		return err
    71  	}
    72  	displayExitInfo(rawExitedKeys, trimmedExitedKeys)
    73  
    74  	return nil
    75  }
    76  
    77  // PerformVoluntaryExit uses gRPC clients to submit a voluntary exit message to a beacon node.
    78  func PerformVoluntaryExit(
    79  	ctx context.Context, cfg PerformExitCfg,
    80  ) (rawExitedKeys [][]byte, formattedExitedKeys []string, err error) {
    81  	var rawNotExitedKeys [][]byte
    82  	for i, key := range cfg.RawPubKeys {
    83  		if err := client.ProposeExit(ctx, cfg.ValidatorClient, cfg.NodeClient, cfg.Keymanager.Sign, key); err != nil {
    84  			rawNotExitedKeys = append(rawNotExitedKeys, key)
    85  
    86  			msg := err.Error()
    87  			if strings.Contains(msg, blocks.ValidatorAlreadyExitedMsg) ||
    88  				strings.Contains(msg, blocks.ValidatorCannotExitYetMsg) {
    89  				log.Warningf("Could not perform voluntary exit for account %s: %s", cfg.FormattedPubKeys[i], msg)
    90  			} else {
    91  				log.WithError(err).Errorf("voluntary exit failed for account %s", cfg.FormattedPubKeys[i])
    92  			}
    93  		}
    94  	}
    95  
    96  	rawExitedKeys = make([][]byte, 0)
    97  	formattedExitedKeys = make([]string, 0)
    98  	for i, key := range cfg.RawPubKeys {
    99  		found := false
   100  		for _, notExited := range rawNotExitedKeys {
   101  			if bytes.Equal(notExited, key) {
   102  				found = true
   103  				break
   104  			}
   105  		}
   106  		if !found {
   107  			rawExitedKeys = append(rawExitedKeys, key)
   108  			formattedExitedKeys = append(formattedExitedKeys, cfg.FormattedPubKeys[i])
   109  		}
   110  	}
   111  
   112  	return rawExitedKeys, formattedExitedKeys, nil
   113  }
   114  
   115  func prepareWallet(cliCtx *cli.Context) (validatingPublicKeys [][48]byte, km keymanager.IKeymanager, err error) {
   116  	w, err := wallet.OpenWalletOrElseCli(cliCtx, func(cliCtx *cli.Context) (*wallet.Wallet, error) {
   117  		return nil, wallet.ErrNoWalletFound
   118  	})
   119  	if err != nil {
   120  		return nil, nil, errors.Wrap(err, "could not open wallet")
   121  	}
   122  
   123  	km, err = w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false})
   124  	if err != nil {
   125  		return nil, nil, errors.Wrap(err, ErrCouldNotInitializeKeymanager)
   126  	}
   127  	validatingPublicKeys, err = km.FetchValidatingPublicKeys(cliCtx.Context)
   128  	if err != nil {
   129  		return nil, nil, err
   130  	}
   131  	if len(validatingPublicKeys) == 0 {
   132  		return nil, nil, errors.New("wallet is empty, no accounts to perform voluntary exit")
   133  	}
   134  
   135  	return validatingPublicKeys, km, nil
   136  }
   137  
   138  func interact(
   139  	cliCtx *cli.Context,
   140  	r io.Reader,
   141  	validatingPublicKeys [][48]byte,
   142  ) (rawPubKeys [][]byte, formattedPubKeys []string, err error) {
   143  	if !cliCtx.IsSet(flags.ExitAllFlag.Name) {
   144  		// Allow the user to interactively select the accounts to exit or optionally
   145  		// provide them via cli flags as a string of comma-separated, hex strings.
   146  		filteredPubKeys, err := filterPublicKeysFromUserInput(
   147  			cliCtx,
   148  			flags.VoluntaryExitPublicKeysFlag,
   149  			validatingPublicKeys,
   150  			prompt.SelectAccountsVoluntaryExitPromptText,
   151  		)
   152  		if err != nil {
   153  			return nil, nil, errors.Wrap(err, "could not filter public keys for voluntary exit")
   154  		}
   155  		rawPubKeys = make([][]byte, len(filteredPubKeys))
   156  		formattedPubKeys = make([]string, len(filteredPubKeys))
   157  		for i, pk := range filteredPubKeys {
   158  			pubKeyBytes := pk.Marshal()
   159  			rawPubKeys[i] = pubKeyBytes
   160  			formattedPubKeys[i] = fmt.Sprintf("%#x", bytesutil.Trunc(pubKeyBytes))
   161  		}
   162  		allAccountStr := strings.Join(formattedPubKeys, ", ")
   163  		if !cliCtx.IsSet(flags.VoluntaryExitPublicKeysFlag.Name) {
   164  			if len(filteredPubKeys) == 1 {
   165  				promptText := "Are you sure you want to perform a voluntary exit on 1 account? (%s) Y/N"
   166  				resp, err := promptutil.ValidatePrompt(
   167  					r, fmt.Sprintf(promptText, au.BrightGreen(formattedPubKeys[0])), promptutil.ValidateYesOrNo,
   168  				)
   169  				if err != nil {
   170  					return nil, nil, err
   171  				}
   172  				if strings.EqualFold(resp, "n") {
   173  					return nil, nil, nil
   174  				}
   175  			} else {
   176  				promptText := "Are you sure you want to perform a voluntary exit on %d accounts? (%s) Y/N"
   177  				if len(filteredPubKeys) == len(validatingPublicKeys) {
   178  					promptText = fmt.Sprintf(
   179  						"Are you sure you want to perform a voluntary exit on all accounts? Y/N (%s)",
   180  						au.BrightGreen(allAccountStr))
   181  				} else {
   182  					promptText = fmt.Sprintf(promptText, len(filteredPubKeys), au.BrightGreen(allAccountStr))
   183  				}
   184  				resp, err := promptutil.ValidatePrompt(r, promptText, promptutil.ValidateYesOrNo)
   185  				if err != nil {
   186  					return nil, nil, err
   187  				}
   188  				if strings.EqualFold(resp, "n") {
   189  					return nil, nil, nil
   190  				}
   191  			}
   192  		}
   193  	} else {
   194  		rawPubKeys, formattedPubKeys = prepareAllKeys(validatingPublicKeys)
   195  		fmt.Printf("About to perform a voluntary exit of %d accounts\n", len(rawPubKeys))
   196  	}
   197  
   198  	promptHeader := au.Red("===============IMPORTANT===============")
   199  	promptDescription := "Withdrawing funds is not possible in Phase 0 of the system. " +
   200  		"Please navigate to the following website and make sure you understand the current implications " +
   201  		"of a voluntary exit before making the final decision:"
   202  	promptURL := au.Blue("https://docs.prylabs.network/docs/wallet/exiting-a-validator/#withdrawal-delay-warning")
   203  	promptQuestion := "If you still want to continue with the voluntary exit, please input a phrase found at the end " +
   204  		"of the page from the above URL"
   205  	promptText := fmt.Sprintf("%s\n%s\n%s\n%s", promptHeader, promptDescription, promptURL, promptQuestion)
   206  	resp, err := promptutil.ValidatePrompt(r, promptText, func(input string) error {
   207  		return promptutil.ValidatePhrase(input, exitPassphrase)
   208  	})
   209  	if err != nil {
   210  		return nil, nil, err
   211  	}
   212  	if strings.EqualFold(resp, "n") {
   213  		return nil, nil, nil
   214  	}
   215  
   216  	return rawPubKeys, formattedPubKeys, nil
   217  }
   218  
   219  func prepareAllKeys(validatingKeys [][48]byte) (raw [][]byte, formatted []string) {
   220  	raw = make([][]byte, len(validatingKeys))
   221  	formatted = make([]string, len(validatingKeys))
   222  	for i, pk := range validatingKeys {
   223  		raw[i] = make([]byte, len(pk))
   224  		copy(raw[i], pk[:])
   225  		formatted[i] = fmt.Sprintf("%#x", bytesutil.Trunc(pk[:]))
   226  	}
   227  	return
   228  }
   229  
   230  func prepareClients(cliCtx *cli.Context) (*ethpb.BeaconNodeValidatorClient, *ethpb.NodeClient, error) {
   231  	dialOpts := client.ConstructDialOptions(
   232  		cliCtx.Int(cmd.GrpcMaxCallRecvMsgSizeFlag.Name),
   233  		cliCtx.String(flags.CertFlag.Name),
   234  		cliCtx.Uint(flags.GrpcRetriesFlag.Name),
   235  		cliCtx.Duration(flags.GrpcRetryDelayFlag.Name),
   236  	)
   237  	if dialOpts == nil {
   238  		return nil, nil, errors.New("failed to construct dial options")
   239  	}
   240  
   241  	grpcHeaders := strings.Split(cliCtx.String(flags.GrpcHeadersFlag.Name), ",")
   242  	cliCtx.Context = grpcutils.AppendHeaders(cliCtx.Context, grpcHeaders)
   243  
   244  	conn, err := grpc.DialContext(cliCtx.Context, cliCtx.String(flags.BeaconRPCProviderFlag.Name), dialOpts...)
   245  	if err != nil {
   246  		return nil, nil, errors.Wrapf(err, "could not dial endpoint %s", flags.BeaconRPCProviderFlag.Name)
   247  	}
   248  	validatorClient := ethpb.NewBeaconNodeValidatorClient(conn)
   249  	nodeClient := ethpb.NewNodeClient(conn)
   250  	return &validatorClient, &nodeClient, nil
   251  }
   252  
   253  func displayExitInfo(rawExitedKeys [][]byte, trimmedExitedKeys []string) {
   254  	if len(rawExitedKeys) > 0 {
   255  		urlFormattedPubKeys := make([]string, len(rawExitedKeys))
   256  		for i, key := range rawExitedKeys {
   257  			var baseUrl string
   258  			if params.BeaconConfig().ConfigName == params.ConfigNames[params.Pyrmont] {
   259  				baseUrl = "https://pyrmont.beaconcha.in/validator/"
   260  			} else if params.BeaconConfig().ConfigName == params.ConfigNames[params.Prater] {
   261  				baseUrl = "https://prater.beaconcha.in/validator/"
   262  			} else {
   263  				baseUrl = "https://beaconcha.in/validator/"
   264  			}
   265  			// Remove '0x' prefix
   266  			urlFormattedPubKeys[i] = baseUrl + hexutil.Encode(key)[2:]
   267  		}
   268  
   269  		ifaceKeys := make([]interface{}, len(urlFormattedPubKeys))
   270  		for i, k := range urlFormattedPubKeys {
   271  			ifaceKeys[i] = k
   272  		}
   273  
   274  		info := fmt.Sprintf("Voluntary exit was successful for the accounts listed. "+
   275  			"URLs where you can track each validator's exit:\n"+strings.Repeat("%s\n", len(ifaceKeys)), ifaceKeys...)
   276  
   277  		log.WithField("publicKeys", strings.Join(trimmedExitedKeys, ", ")).Info(info)
   278  	} else {
   279  		log.Info("No successful voluntary exits")
   280  	}
   281  }