From 38f095d556bfa2c44e35783590586498ada1d4ff Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Tue, 24 Jan 2023 04:05:55 -0600 Subject: [PATCH] prysmctl: changeblstoexecutionaddress ( withdrawals) (#11790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * adding unit test for withdrawal * updating to handle api changes * Update cmd/prysmctl/withdrawal/withdrawal.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/withdrawal/cmd.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/withdrawal/withdrawal.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/withdrawal/cmd.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/withdrawal/withdrawal.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/withdrawal/withdrawal.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/withdrawal/withdrawal.go Co-authored-by: Radosław Kapka * updating based on feedback * rewording prints * Update cmd/prysmctl/withdrawal/withdrawal.go Co-authored-by: Potuz * add error handling when status not ok * addressing some of the feedback, adding new tests * adding unit test with error return * adjusting based on review comments * addressing more comments and fixing unit test * updating prompts * migrating some functions to api/client * removing unused function * adding review feedback * removing unused functions * migrating command, old command under accounts still exist * reverting tos.go * fixing bazel lint * fixing build * resolving unused parameter caught by deepsource * fixing unit tests * Update cmd/prysmctl/validator/cmd.go Co-authored-by: Potuz * Update cmd/prysmctl/validator/cmd.go Co-authored-by: Potuz * adding in website * addressing review comment * adding new verify only flag * Update cmd/prysmctl/validator/withdraw.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/validator/withdraw.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/validator/withdraw.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/validator/withdraw.go Co-authored-by: Radosław Kapka * Update api/client/beacon/client.go Co-authored-by: Radosław Kapka * Update api/client/beacon/client.go Co-authored-by: Radosław Kapka * Update api/client/beacon/client.go Co-authored-by: Radosław Kapka * Update api/client/beacon/client.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/validator/withdraw.go Co-authored-by: Radosław Kapka * addressing comments * fixing unit test * Update cmd/prysmctl/validator/cmd.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/validator/cmd.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/validator/cmd.go Co-authored-by: Radosław Kapka * Update cmd/prysmctl/validator/cmd.go Co-authored-by: Radosław Kapka * address comments * Update cmd/prysmctl/validator/cmd.go Co-authored-by: Radosław Kapka * adjusting object for new provided format * adding omit empty * fixing unit test, and linting * adjusting based on cli changes * fixing test data and metadata property * addressing backwards compatability * reverting based on review comment Co-authored-by: Radosław Kapka Co-authored-by: Potuz Co-authored-by: Raul Jordan Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com> --- api/client/beacon/client.go | 70 +++- cmd/prysmctl/BUILD.bazel | 2 +- cmd/prysmctl/main.go | 4 +- cmd/prysmctl/signing/BUILD.bazel | 17 - cmd/prysmctl/signing/cmd.go | 64 ---- cmd/prysmctl/validator/BUILD.bazel | 41 +++ cmd/prysmctl/validator/cmd.go | 137 ++++++++ .../testdata/change-operations-multiple.json | 1 + .../change-operations-multiple_notfound.json | 1 + .../validator/testdata/change-operations.json | 1 + ...taking-cli-change-operations-multiple.json | 1 + cmd/prysmctl/validator/withdraw.go | 171 ++++++++++ cmd/prysmctl/validator/withdraw_test.go | 305 ++++++++++++++++++ cmd/validator/accounts/accounts.go | 2 +- 14 files changed, 724 insertions(+), 93 deletions(-) delete mode 100644 cmd/prysmctl/signing/BUILD.bazel delete mode 100644 cmd/prysmctl/signing/cmd.go create mode 100644 cmd/prysmctl/validator/BUILD.bazel create mode 100644 cmd/prysmctl/validator/cmd.go create mode 100644 cmd/prysmctl/validator/testdata/change-operations-multiple.json create mode 100644 cmd/prysmctl/validator/testdata/change-operations-multiple_notfound.json create mode 100755 cmd/prysmctl/validator/testdata/change-operations.json create mode 100644 cmd/prysmctl/validator/testdata/staking-cli-change-operations-multiple.json create mode 100644 cmd/prysmctl/validator/withdraw.go create mode 100644 cmd/prysmctl/validator/withdraw_test.go diff --git a/api/client/beacon/client.go b/api/client/beacon/client.go index 1fd85886ed..05f54d7287 100644 --- a/api/client/beacon/client.go +++ b/api/client/beacon/client.go @@ -28,13 +28,14 @@ import ( ) const ( - getSignedBlockPath = "/eth/v2/beacon/blocks" - getBlockRootPath = "/eth/v1/beacon/blocks/{{.Id}}/root" - getForkForStatePath = "/eth/v1/beacon/states/{{.Id}}/fork" - getWeakSubjectivityPath = "/eth/v1/beacon/weak_subjectivity" - getForkSchedulePath = "/eth/v1/config/fork_schedule" - getStatePath = "/eth/v2/debug/beacon/states" - getNodeVersionPath = "/eth/v1/node/version" + getSignedBlockPath = "/eth/v2/beacon/blocks" + getBlockRootPath = "/eth/v1/beacon/blocks/{{.Id}}/root" + getForkForStatePath = "/eth/v1/beacon/states/{{.Id}}/fork" + getWeakSubjectivityPath = "/eth/v1/beacon/weak_subjectivity" + getForkSchedulePath = "/eth/v1/config/fork_schedule" + getStatePath = "/eth/v2/debug/beacon/states" + getNodeVersionPath = "/eth/v1/node/version" + changeBLStoExecutionPath = "/eth/v1/beacon/pool/bls_to_execution_changes" ) // StateOrBlockId represents the block_id / state_id parameters that several of the Eth Beacon API methods accept. @@ -146,7 +147,6 @@ func withSSZEncoding() reqOption { // get is a generic, opinionated GET function to reduce boilerplate amongst the getters in this package. func (c *Client) get(ctx context.Context, path string, opts ...reqOption) ([]byte, error) { u := c.baseURL.ResolveReference(&url.URL{Path: path}) - log.Printf("requesting %s", u.String()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, err @@ -343,6 +343,60 @@ func (c *Client) GetWeakSubjectivity(ctx context.Context) (*WeakSubjectivityData }, nil } +// SubmitChangeBLStoExecution calls a beacon API endpoint to set the withdrawal addresses based on the given signed messages. +// If the API responds with something other than OK there will be failure messages associated to the corresponding request message. +func (c *Client) SubmitChangeBLStoExecution(ctx context.Context, request []*apimiddleware.SignedBLSToExecutionChangeJson) error { + u := c.baseURL.ResolveReference(&url.URL{Path: changeBLStoExecutionPath}) + body, err := json.Marshal(request) + if err != nil { + return errors.Wrap(err, "failed to marshal JSON") + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewBuffer(body)) + if err != nil { + return errors.Wrap(err, "invalid format, failed to create new POST request object") + } + req.Header.Set("Content-Type", "application/json") + resp, err := c.hc.Do(req) + if err != nil { + return err + } + defer func() { + err = resp.Body.Close() + }() + if resp.StatusCode != http.StatusOK { + decoder := json.NewDecoder(resp.Body) + decoder.DisallowUnknownFields() + errorJson := &apimiddleware.IndexedVerificationFailureErrorJson{} + if err := decoder.Decode(errorJson); err != nil { + return errors.Wrapf(err, "failed to decode error JSON for %s", resp.Request.URL) + } + for _, failure := range errorJson.Failures { + w := request[failure.Index].Message + log.WithFields(log.Fields{ + "validator_index": w.ValidatorIndex, + "withdrawal_address": w.ToExecutionAddress, + }).Error(failure.Message) + } + return errors.Errorf("POST error %d: %s", errorJson.Code, errorJson.Message) + } + return nil +} + +// GetBLStoExecutionChanges gets all the set withdrawal messages in the node's operation pool. +// Returns a struct representation of json response. +func (c *Client) GetBLStoExecutionChanges(ctx context.Context) (*apimiddleware.BLSToExecutionChangesPoolResponseJson, error) { + body, err := c.get(ctx, changeBLStoExecutionPath) + if err != nil { + return nil, err + } + poolResponse := &apimiddleware.BLSToExecutionChangesPoolResponseJson{} + err = json.Unmarshal(body, poolResponse) + if err != nil { + return nil, err + } + return poolResponse, nil +} + func non200Err(response *http.Response) error { bodyBytes, err := io.ReadAll(response.Body) var body string diff --git a/cmd/prysmctl/BUILD.bazel b/cmd/prysmctl/BUILD.bazel index 3bf6c57ca2..0cbdd29851 100644 --- a/cmd/prysmctl/BUILD.bazel +++ b/cmd/prysmctl/BUILD.bazel @@ -15,8 +15,8 @@ go_library( "//cmd/prysmctl/db:go_default_library", "//cmd/prysmctl/deprecated:go_default_library", "//cmd/prysmctl/p2p:go_default_library", - "//cmd/prysmctl/signing:go_default_library", "//cmd/prysmctl/testnet:go_default_library", + "//cmd/prysmctl/validator:go_default_library", "//cmd/prysmctl/weaksubjectivity:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", diff --git a/cmd/prysmctl/main.go b/cmd/prysmctl/main.go index b888fbc898..59c9094629 100644 --- a/cmd/prysmctl/main.go +++ b/cmd/prysmctl/main.go @@ -7,8 +7,8 @@ import ( "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/db" "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/deprecated" "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/p2p" - "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/signing" "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/testnet" + "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/validator" "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/weaksubjectivity" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" @@ -36,5 +36,5 @@ func init() { prysmctlCommands = append(prysmctlCommands, p2p.Commands...) prysmctlCommands = append(prysmctlCommands, testnet.Commands...) prysmctlCommands = append(prysmctlCommands, weaksubjectivity.Commands...) - prysmctlCommands = append(prysmctlCommands, signing.Commands...) + prysmctlCommands = append(prysmctlCommands, validator.Commands...) } diff --git a/cmd/prysmctl/signing/BUILD.bazel b/cmd/prysmctl/signing/BUILD.bazel deleted file mode 100644 index 4eba6982c1..0000000000 --- a/cmd/prysmctl/signing/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -load("@prysm//tools/go:def.bzl", "go_library") - -go_library( - name = "go_default_library", - srcs = ["cmd.go"], - importpath = "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/signing", - visibility = ["//visibility:public"], - deps = [ - "//cmd:go_default_library", - "//cmd/validator/accounts:go_default_library", - "//cmd/validator/flags:go_default_library", - "//config/features:go_default_library", - "//runtime/tos:go_default_library", - "@com_github_sirupsen_logrus//:go_default_library", - "@com_github_urfave_cli_v2//:go_default_library", - ], -) diff --git a/cmd/prysmctl/signing/cmd.go b/cmd/prysmctl/signing/cmd.go deleted file mode 100644 index 5d6781922a..0000000000 --- a/cmd/prysmctl/signing/cmd.go +++ /dev/null @@ -1,64 +0,0 @@ -package signing - -import ( - "os" - - "github.com/prysmaticlabs/prysm/v3/cmd" - "github.com/prysmaticlabs/prysm/v3/cmd/validator/accounts" - "github.com/prysmaticlabs/prysm/v3/cmd/validator/flags" - "github.com/prysmaticlabs/prysm/v3/config/features" - "github.com/prysmaticlabs/prysm/v3/runtime/tos" - log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" -) - -var Commands = []*cli.Command{ - { - Name: "sign", - Usage: "signs a message and broadcasts it to the network through the beacon node", - Subcommands: []*cli.Command{ - { - Name: "voluntary-exit", - Description: "Performs a voluntary exit on selected accounts", - Flags: cmd.WrapFlags([]cli.Flag{ - flags.WalletDirFlag, - flags.WalletPasswordFileFlag, - flags.AccountPasswordFileFlag, - flags.VoluntaryExitPublicKeysFlag, - flags.BeaconRPCProviderFlag, - flags.Web3SignerURLFlag, - flags.Web3SignerPublicValidatorKeysFlag, - flags.InteropNumValidators, - flags.InteropStartIndex, - cmd.GrpcMaxCallRecvMsgSizeFlag, - flags.CertFlag, - flags.GrpcHeadersFlag, - flags.GrpcRetriesFlag, - flags.GrpcRetryDelayFlag, - flags.ExitAllFlag, - flags.ForceExitFlag, - features.Mainnet, - features.PraterTestnet, - features.RopstenTestnet, - features.SepoliaTestnet, - cmd.AcceptTosFlag, - }), - Before: func(cliCtx *cli.Context) error { - if err := cmd.LoadFlagsFromConfig(cliCtx, cliCtx.Command.Flags); err != nil { - return err - } - if err := tos.VerifyTosAcceptedOrPrompt(cliCtx); err != nil { - return err - } - return features.ConfigureValidator(cliCtx) - }, - Action: func(cliCtx *cli.Context) error { - if err := accounts.AccountsExit(cliCtx, os.Stdin); err != nil { - log.WithError(err).Fatal("Could not perform voluntary exit") - } - return nil - }, - }, - }, - }, -} diff --git a/cmd/prysmctl/validator/BUILD.bazel b/cmd/prysmctl/validator/BUILD.bazel new file mode 100644 index 0000000000..639ebc00f4 --- /dev/null +++ b/cmd/prysmctl/validator/BUILD.bazel @@ -0,0 +1,41 @@ +load("@prysm//tools/go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "cmd.go", + "withdraw.go", + ], + importpath = "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/validator", + visibility = ["//visibility:public"], + deps = [ + "//api/client/beacon:go_default_library", + "//beacon-chain/rpc/apimiddleware:go_default_library", + "//cmd:go_default_library", + "//cmd/validator/accounts:go_default_library", + "//cmd/validator/flags:go_default_library", + "//config/features:go_default_library", + "//config/fieldparams:go_default_library", + "//runtime/tos:go_default_library", + "@com_github_ethereum_go_ethereum//common:go_default_library", + "@com_github_logrusorgru_aurora//:go_default_library", + "@com_github_pkg_errors//:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + "@com_github_urfave_cli_v2//:go_default_library", + "@io_opencensus_go//trace:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["withdraw_test.go"], + data = glob(["testdata/**"]), + embed = [":go_default_library"], + deps = [ + "//beacon-chain/rpc/apimiddleware:go_default_library", + "//testing/assert:go_default_library", + "//testing/require:go_default_library", + "@com_github_sirupsen_logrus//hooks/test:go_default_library", + "@com_github_urfave_cli_v2//:go_default_library", + ], +) diff --git a/cmd/prysmctl/validator/cmd.go b/cmd/prysmctl/validator/cmd.go new file mode 100644 index 0000000000..e60e9335ce --- /dev/null +++ b/cmd/prysmctl/validator/cmd.go @@ -0,0 +1,137 @@ +package validator + +import ( + "fmt" + "os" + + "github.com/logrusorgru/aurora" + "github.com/prysmaticlabs/prysm/v3/cmd" + "github.com/prysmaticlabs/prysm/v3/cmd/validator/accounts" + "github.com/prysmaticlabs/prysm/v3/cmd/validator/flags" + "github.com/prysmaticlabs/prysm/v3/config/features" + "github.com/prysmaticlabs/prysm/v3/runtime/tos" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +var ( + BeaconHostFlag = &cli.StringFlag{ + Name: "beacon-node-host", + Usage: "host:port for beacon node to query", + Value: "127.0.0.1:3500", + } + PathFlag = &cli.StringFlag{ + Name: "path", + Aliases: []string{"p"}, + Usage: "path to the signed withdrawal messages JSON", + } + ConfirmFlag = &cli.BoolFlag{ + Name: "confirm", + Aliases: []string{"c"}, + Usage: "WARNING: User confirms and accepts responsibility of all input data provided and actions for setting their withdrawal address for their validator key. " + + "This action is not reversible and withdrawal addresses can not be changed once set.", + } + VerifyOnlyFlag = &cli.BoolFlag{ + Name: "verify-only", + Aliases: []string{"vo"}, + Usage: "overrides withdrawal command to only verify whether requests are in the pool and does not submit withdrawal requests", + } +) + +var Commands = []*cli.Command{ + { + Name: "validator", + Aliases: []string{"v", "sign"}, // remove sign command should be depreciated but having as backwards compatability. + Usage: "commands that affect the state of validators such as exiting or withdrawing", + Subcommands: []*cli.Command{ + { + Name: "withdraw", + Aliases: []string{"w"}, + Usage: "Assign Ethereum withdrawal addresses to validator keys. WARNING: once set values are included they can no longer be updated.", + Flags: []cli.Flag{ + BeaconHostFlag, + PathFlag, + ConfirmFlag, + VerifyOnlyFlag, + cmd.ConfigFileFlag, + cmd.AcceptTosFlag, + }, + Before: func(cliCtx *cli.Context) error { + if err := cmd.LoadFlagsFromConfig(cliCtx, cliCtx.Command.Flags); err != nil { + return err + } + au := aurora.NewAurora(true) + if !cliCtx.Bool(cmd.AcceptTosFlag.Name) || !cliCtx.Bool(ConfirmFlag.Name) { + fmt.Println(au.Red("===============IMPORTANT===============")) + fmt.Println(au.Red("Please read the following carefully")) + fmt.Print("This action will allow the partial withdrawal of amounts over the 32 staked ETH in your active validator balance. \n" + + "You will also be entitled to the full withdrawal of the entire validator balance if your validator has exited. \n" + + "Please navigate to our website (https://docs.prylabs.network/) and make sure you understand the full implications of setting your withdrawal address. \n") + fmt.Println(au.Red("THIS ACTION WILL NOT BE REVERSIBLE ONCE INCLUDED. ")) + fmt.Println(au.Red("You will NOT be able to change the address again once changed. ")) + return fmt.Errorf("both the `--%s` and `--%s` flags are required to run this command. \n"+ + "By providing these flags the user has read and accepts the TERMS AND CONDITIONS: https://github.com/prysmaticlabs/prysm/blob/master/TERMS_OF_SERVICE.md "+ + "and confirms the action of setting withdrawals addresses", cmd.AcceptTosFlag.Name, ConfirmFlag.Name) + } else { + return nil + } + }, + Action: func(cliCtx *cli.Context) error { + if cliCtx.Bool(VerifyOnlyFlag.Name) { + if err := verifyWithdrawalsInPool(cliCtx); err != nil { + log.WithError(err).Fatal("Could not verify withdrawal addresses") + } + } else { + if err := setWithdrawalAddresses(cliCtx); err != nil { + log.WithError(err).Fatal("Could not set withdrawal addresses") + } + } + return nil + }, + }, + { + Name: "exit", + Aliases: []string{"e", "voluntary-exit"}, + Usage: "Performs a voluntary exit on selected accounts", + Flags: cmd.WrapFlags([]cli.Flag{ + flags.WalletDirFlag, + flags.WalletPasswordFileFlag, + flags.AccountPasswordFileFlag, + flags.VoluntaryExitPublicKeysFlag, + flags.BeaconRPCProviderFlag, + flags.Web3SignerURLFlag, + flags.Web3SignerPublicValidatorKeysFlag, + flags.InteropNumValidators, + flags.InteropStartIndex, + cmd.GrpcMaxCallRecvMsgSizeFlag, + flags.CertFlag, + flags.GrpcHeadersFlag, + flags.GrpcRetriesFlag, + flags.GrpcRetryDelayFlag, + flags.ExitAllFlag, + flags.ForceExitFlag, + features.Mainnet, + features.PraterTestnet, + features.RopstenTestnet, + features.SepoliaTestnet, + cmd.AcceptTosFlag, + }), + Before: func(cliCtx *cli.Context) error { + if err := cmd.LoadFlagsFromConfig(cliCtx, cliCtx.Command.Flags); err != nil { + return err + } + if err := tos.VerifyTosAcceptedOrPrompt(cliCtx); err != nil { + return err + } + return features.ConfigureValidator(cliCtx) + }, + Action: func(cliCtx *cli.Context) error { + if err := accounts.AccountsExit(cliCtx, os.Stdin); err != nil { + log.WithError(err).Fatal("Could not perform voluntary exit") + } + return nil + }, + }, + }, + }, +} diff --git a/cmd/prysmctl/validator/testdata/change-operations-multiple.json b/cmd/prysmctl/validator/testdata/change-operations-multiple.json new file mode 100644 index 0000000000..345402dbb0 --- /dev/null +++ b/cmd/prysmctl/validator/testdata/change-operations-multiple.json @@ -0,0 +1 @@ +[{"message":{"validator_index":"0","from_bls_pubkey":"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xb6e640f0fc58e9f22585dbf434b6a0e8fc36b98e2f2a963e158716cfc84034141289f7898027de1ec56754937f1a837e01c7b066a6a56af7a379f8aec823d050788a5ecc799e9bc39f73d45b7c389c961cbaace61823e4c7bf2f93bd06c03127"},{"message":{"validator_index":"1","from_bls_pubkey":"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xa97103e15d3dbdaa75fb15cea782e4a11329eea77d155864ec682d7907b3b70c7771960bef7be1b1c4e08fe735888b950c1a22053f6049b35736f48e6dd018392efa3896c9e427ea4e100e86e9131b5ea2673388a4bf188407a630ba405b7dc5"}] \ No newline at end of file diff --git a/cmd/prysmctl/validator/testdata/change-operations-multiple_notfound.json b/cmd/prysmctl/validator/testdata/change-operations-multiple_notfound.json new file mode 100644 index 0000000000..f9ee49d426 --- /dev/null +++ b/cmd/prysmctl/validator/testdata/change-operations-multiple_notfound.json @@ -0,0 +1 @@ +[{"message":{"validator_index":"3","from_bls_pubkey":"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xb6e640f0fc58e9f22585dbf434b6a0e8fc36b98e2f2a963e158716cfc84034141289f7898027de1ec56754937f1a837e01c7b066a6a56af7a379f8aec823d050788a5ecc799e9bc39f73d45b7c389c961cbaace61823e4c7bf2f93bd06c03127"},{"message":{"validator_index":"5","from_bls_pubkey":"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xa97103e15d3dbdaa75fb15cea782e4a11329eea77d155864ec682d7907b3b70c7771960bef7be1b1c4e08fe735888b950c1a22053f6049b35736f48e6dd018392efa3896c9e427ea4e100e86e9131b5ea2673388a4bf188407a630ba405b7dc5"}] \ No newline at end of file diff --git a/cmd/prysmctl/validator/testdata/change-operations.json b/cmd/prysmctl/validator/testdata/change-operations.json new file mode 100755 index 0000000000..219ee2099b --- /dev/null +++ b/cmd/prysmctl/validator/testdata/change-operations.json @@ -0,0 +1 @@ +[{"message":{"validator_index":"1","from_bls_pubkey":"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xa97103e15d3dbdaa75fb15cea782e4a11329eea77d155864ec682d7907b3b70c7771960bef7be1b1c4e08fe735888b950c1a22053f6049b35736f48e6dd018392efa3896c9e427ea4e100e86e9131b5ea2673388a4bf188407a630ba405b7dc5"}] \ No newline at end of file diff --git a/cmd/prysmctl/validator/testdata/staking-cli-change-operations-multiple.json b/cmd/prysmctl/validator/testdata/staking-cli-change-operations-multiple.json new file mode 100644 index 0000000000..019ab3d2ec --- /dev/null +++ b/cmd/prysmctl/validator/testdata/staking-cli-change-operations-multiple.json @@ -0,0 +1 @@ +[{"message":{"validator_index":"0","from_bls_pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","to_execution_address":"a94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"b6e640f0fc58e9f22585dbf434b6a0e8fc36b98e2f2a963e158716cfc84034141289f7898027de1ec56754937f1a837e01c7b066a6a56af7a379f8aec823d050788a5ecc799e9bc39f73d45b7c389c961cbaace61823e4c7bf2f93bd06c03127", "metadata":{ "network_name": "mainnet", "genesis_validators_root": "4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95", "deposit_cli_version": "2.4.0"}},{"message":{"validator_index":"1","from_bls_pubkey":"b89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","to_execution_address":"a94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"a97103e15d3dbdaa75fb15cea782e4a11329eea77d155864ec682d7907b3b70c7771960bef7be1b1c4e08fe735888b950c1a22053f6049b35736f48e6dd018392efa3896c9e427ea4e100e86e9131b5ea2673388a4bf188407a630ba405b7dc5", "metadata":{ "network_name": "mainnet", "genesis_validators_root": "4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95", "deposit_cli_version": "2.4.0"}}] \ No newline at end of file diff --git a/cmd/prysmctl/validator/withdraw.go b/cmd/prysmctl/validator/withdraw.go new file mode 100644 index 0000000000..2b251af0ae --- /dev/null +++ b/cmd/prysmctl/validator/withdraw.go @@ -0,0 +1,171 @@ +package validator + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/logrusorgru/aurora" + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v3/api/client/beacon" + "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware" + fieldparams "github.com/prysmaticlabs/prysm/v3/config/fieldparams" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + "go.opencensus.io/trace" +) + +func setWithdrawalAddresses(c *cli.Context) error { + ctx, span := trace.StartSpan(c.Context, "withdrawal.setWithdrawalAddresses") + defer span.End() + au := aurora.NewAurora(true) + beaconNodeHost := c.String(BeaconHostFlag.Name) + if !c.IsSet(PathFlag.Name) { + return fmt.Errorf("no --%s flag value was provided", PathFlag.Name) + } + setWithdrawalAddressJsons, err := getWithdrawalMessagesFromPathFlag(c) + if err != nil { + return err + } + for _, request := range setWithdrawalAddressJsons { + fmt.Println("SETTING VALIDATOR INDEX " + au.Red(request.Message.ValidatorIndex).String() + " TO WITHDRAWAL ADDRESS " + au.Red(request.Message.ToExecutionAddress).String()) + } + return callWithdrawalEndpoints(ctx, beaconNodeHost, setWithdrawalAddressJsons) +} + +func getWithdrawalMessagesFromPathFlag(c *cli.Context) ([]*apimiddleware.SignedBLSToExecutionChangeJson, error) { + setWithdrawalAddressJsons := make([]*apimiddleware.SignedBLSToExecutionChangeJson, 0) + foundFilePaths, err := findWithdrawalFiles(c.String(PathFlag.Name)) + if err != nil { + return setWithdrawalAddressJsons, errors.Wrap(err, "failed to find withdrawal files") + } + for _, foundFilePath := range foundFilePaths { + b, err := os.ReadFile(filepath.Clean(foundFilePath)) + if err != nil { + return setWithdrawalAddressJsons, errors.Wrap(err, "failed to open file") + } + var to []*apimiddleware.SignedBLSToExecutionChangeJson + if err := json.Unmarshal(b, &to); err != nil { + log.Warnf("provided file: %s, is not a list of signed withdrawal messages", foundFilePath) + continue + } + // verify 0x from file and add if needed + for i, obj := range to { + if len(obj.Message.FromBLSPubkey) == fieldparams.BLSPubkeyLength*2 { + to[i].Message.FromBLSPubkey = fmt.Sprintf("0x%s", obj.Message.FromBLSPubkey) + } + if len(obj.Message.ToExecutionAddress) == common.AddressLength*2 { + to[i].Message.ToExecutionAddress = fmt.Sprintf("0x%s", obj.Message.ToExecutionAddress) + } + if len(obj.Signature) == fieldparams.BLSSignatureLength*2 { + to[i].Signature = fmt.Sprintf("0x%s", obj.Signature) + } + setWithdrawalAddressJsons = append(setWithdrawalAddressJsons, &apimiddleware.SignedBLSToExecutionChangeJson{ + Message: &apimiddleware.BLSToExecutionChangeJson{ + ValidatorIndex: to[i].Message.ValidatorIndex, + FromBLSPubkey: to[i].Message.FromBLSPubkey, + ToExecutionAddress: to[i].Message.ToExecutionAddress, + }, + Signature: to[i].Signature, + }) + } + + } + if len(setWithdrawalAddressJsons) == 0 { + return setWithdrawalAddressJsons, errors.New("the list of signed requests is empty") + } + return setWithdrawalAddressJsons, nil +} + +func callWithdrawalEndpoints(ctx context.Context, host string, request []*apimiddleware.SignedBLSToExecutionChangeJson) error { + client, err := beacon.NewClient(host) + if err != nil { + return err + } + if err := client.SubmitChangeBLStoExecution(ctx, request); err != nil { + return err + } + log.Infof("Successfully published messages to update %d withdrawal addresses.", len(request)) + return checkIfWithdrawsAreInPool(ctx, client, request) +} + +func checkIfWithdrawsAreInPool(ctx context.Context, client *beacon.Client, request []*apimiddleware.SignedBLSToExecutionChangeJson) error { + log.Info("Verifying requested withdrawal messages known to node...") + poolResponse, err := client.GetBLStoExecutionChanges(ctx) + if err != nil { + return err + } + requestMap := make(map[string]string) + for _, w := range request { + requestMap[w.Message.ValidatorIndex] = w.Message.ToExecutionAddress + } + totalMessages := len(requestMap) + for _, resp := range poolResponse.Data { + value, found := requestMap[resp.Message.ValidatorIndex] + if found && value == resp.Message.ToExecutionAddress { + delete(requestMap, resp.Message.ValidatorIndex) + } + } + if len(requestMap) != 0 { + for key, address := range requestMap { + log.WithFields(log.Fields{ + "validator_index": key, + "execution_address:": address, + }).Warn("Set withdrawal address message not found in the node's operations pool.") + } + log.Warn("Please check before resubmitting. Set withdrawal address messages that were not found in the pool may have been already included into a block.") + } else { + log.Infof("All (total:%d) signed withdrawal messages were found in the pool.", totalMessages) + } + return nil +} + +func findWithdrawalFiles(path string) ([]string, error) { + var foundpaths []string + maxdepth := 3 + cleanpath := filepath.Clean(path) + if err := filepath.WalkDir(cleanpath, func(s string, d fs.DirEntry, e error) error { + if e != nil { + return e + } + if d.IsDir() && strings.Count(cleanpath, string(os.PathSeparator)) > maxdepth { + return fs.SkipDir + } + + if filepath.Ext(d.Name()) == ".json" { + foundpaths = append(foundpaths, s) + } + return nil + }); err != nil { + return nil, errors.Wrap(err, "unable to find compatible files") + } + if len(foundpaths) == 0 { + return nil, errors.New("no compatible files were found") + } + log.Infof("found JSON files for setting withdrawals: %v", foundpaths) + return foundpaths, nil +} + +func verifyWithdrawalsInPool(c *cli.Context) error { + ctx, span := trace.StartSpan(c.Context, "withdrawal.verifyWithdrawalsInPool") + defer span.End() + beaconNodeHost := c.String(BeaconHostFlag.Name) + if !c.IsSet(PathFlag.Name) { + return fmt.Errorf("no --%s flag value was provided", PathFlag.Name) + } + client, err := beacon.NewClient(beaconNodeHost) + if err != nil { + return err + } + + request, err := getWithdrawalMessagesFromPathFlag(c) + if err != nil { + return err + } + return checkIfWithdrawsAreInPool(ctx, client, request) +} diff --git a/cmd/prysmctl/validator/withdraw_test.go b/cmd/prysmctl/validator/withdraw_test.go new file mode 100644 index 0000000000..b0ebd94697 --- /dev/null +++ b/cmd/prysmctl/validator/withdraw_test.go @@ -0,0 +1,305 @@ +package validator + +import ( + "encoding/json" + "flag" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware" + "github.com/prysmaticlabs/prysm/v3/testing/assert" + "github.com/prysmaticlabs/prysm/v3/testing/require" + logtest "github.com/sirupsen/logrus/hooks/test" + "github.com/urfave/cli/v2" +) + +func TestCallWithdrawalEndpoint(t *testing.T) { + file := "./testdata/change-operations.json" + baseurl := "127.0.0.1:3500" + l, err := net.Listen("tcp", baseurl) + require.NoError(t, err) + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodGet { + b, err := os.ReadFile(filepath.Clean(file)) + require.NoError(t, err) + var to []*apimiddleware.SignedBLSToExecutionChangeJson + err = json.Unmarshal(b, &to) + require.NoError(t, err) + err = json.NewEncoder(w).Encode(&apimiddleware.BLSToExecutionChangesPoolResponseJson{ + Data: to, + }) + require.NoError(t, err) + } + })) + err = srv.Listener.Close() + require.NoError(t, err) + srv.Listener = l + srv.Start() + defer srv.Close() + hook := logtest.NewGlobal() + + app := cli.App{} + set := flag.NewFlagSet("test", 0) + set.String("beacon-node-host", baseurl, "") + set.String("path", file, "") + set.Bool("confirm", true, "") + set.Bool("accept-terms-of-use", true, "") + assert.NoError(t, set.Set("beacon-node-host", baseurl)) + assert.NoError(t, set.Set("path", file)) + cliCtx := cli.NewContext(&app, set, nil) + + err = setWithdrawalAddresses(cliCtx) + require.NoError(t, err) + + assert.LogsContain(t, hook, "Successfully published") +} + +func TestCallWithdrawalEndpoint_Mutiple(t *testing.T) { + file := "./testdata/change-operations-multiple.json" + baseurl := "127.0.0.1:3500" + l, err := net.Listen("tcp", baseurl) + require.NoError(t, err) + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodGet { + b, err := os.ReadFile(filepath.Clean(file)) + require.NoError(t, err) + var to []*apimiddleware.SignedBLSToExecutionChangeJson + err = json.Unmarshal(b, &to) + require.NoError(t, err) + err = json.NewEncoder(w).Encode(&apimiddleware.BLSToExecutionChangesPoolResponseJson{ + Data: to, + }) + require.NoError(t, err) + } + })) + err = srv.Listener.Close() + require.NoError(t, err) + srv.Listener = l + srv.Start() + defer srv.Close() + hook := logtest.NewGlobal() + + app := cli.App{} + set := flag.NewFlagSet("test", 0) + set.String("beacon-node-host", baseurl, "") + set.String("path", file, "") + set.Bool("confirm", true, "") + set.Bool("accept-terms-of-use", true, "") + assert.NoError(t, set.Set("beacon-node-host", baseurl)) + assert.NoError(t, set.Set("path", file)) + cliCtx := cli.NewContext(&app, set, nil) + + err = setWithdrawalAddresses(cliCtx) + require.NoError(t, err) + assert.LogsContain(t, hook, "Successfully published") + assert.LogsContain(t, hook, "to update 2 withdrawal") + assert.LogsContain(t, hook, "All (total:2) signed withdrawal messages were found in the pool.") + assert.LogsDoNotContain(t, hook, "Set withdrawal address message not found in the node's operations pool.") +} + +func TestCallWithdrawalEndpoint_Mutiple_stakingcli(t *testing.T) { + stakingcliFile := "./testdata/staking-cli-change-operations-multiple.json" + file := "./testdata/change-operations-multiple.json" + baseurl := "127.0.0.1:3500" + l, err := net.Listen("tcp", baseurl) + require.NoError(t, err) + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodGet { + b, err := os.ReadFile(filepath.Clean(file)) + require.NoError(t, err) + var to []*apimiddleware.SignedBLSToExecutionChangeJson + err = json.Unmarshal(b, &to) + require.NoError(t, err) + err = json.NewEncoder(w).Encode(&apimiddleware.BLSToExecutionChangesPoolResponseJson{ + Data: to, + }) + require.NoError(t, err) + } + })) + err = srv.Listener.Close() + require.NoError(t, err) + srv.Listener = l + srv.Start() + defer srv.Close() + hook := logtest.NewGlobal() + + app := cli.App{} + set := flag.NewFlagSet("test", 0) + set.String("beacon-node-host", baseurl, "") + set.String("path", stakingcliFile, "") + set.Bool("confirm", true, "") + set.Bool("accept-terms-of-use", true, "") + assert.NoError(t, set.Set("beacon-node-host", baseurl)) + assert.NoError(t, set.Set("path", stakingcliFile)) + cliCtx := cli.NewContext(&app, set, nil) + + err = setWithdrawalAddresses(cliCtx) + require.NoError(t, err) + assert.LogsContain(t, hook, "Successfully published") + assert.LogsContain(t, hook, "to update 2 withdrawal") + assert.LogsContain(t, hook, "All (total:2) signed withdrawal messages were found in the pool.") + assert.LogsDoNotContain(t, hook, "Set withdrawal address message not found in the node's operations pool.") +} + +func TestCallWithdrawalEndpoint_Mutiple_notfound(t *testing.T) { + respFile := "./testdata/change-operations-multiple_notfound.json" + file := "./testdata/change-operations-multiple.json" + baseurl := "127.0.0.1:3500" + l, err := net.Listen("tcp", baseurl) + require.NoError(t, err) + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodGet { + b, err := os.ReadFile(filepath.Clean(respFile)) + require.NoError(t, err) + var to []*apimiddleware.SignedBLSToExecutionChangeJson + err = json.Unmarshal(b, &to) + require.NoError(t, err) + err = json.NewEncoder(w).Encode(&apimiddleware.BLSToExecutionChangesPoolResponseJson{ + Data: to, + }) + require.NoError(t, err) + } + })) + err = srv.Listener.Close() + require.NoError(t, err) + srv.Listener = l + srv.Start() + defer srv.Close() + hook := logtest.NewGlobal() + + app := cli.App{} + set := flag.NewFlagSet("test", 0) + set.String("beacon-node-host", baseurl, "") + set.String("path", file, "") + set.Bool("confirm", true, "") + set.Bool("accept-terms-of-use", true, "") + assert.NoError(t, set.Set("beacon-node-host", baseurl)) + assert.NoError(t, set.Set("path", file)) + cliCtx := cli.NewContext(&app, set, nil) + + err = setWithdrawalAddresses(cliCtx) + require.NoError(t, err) + assert.LogsContain(t, hook, "Successfully published") + assert.LogsContain(t, hook, "to update 2 withdrawal") + assert.LogsContain(t, hook, "Set withdrawal address message not found in the node's operations pool.") + assert.LogsContain(t, hook, "Please check before resubmitting. Set withdrawal address messages that were not found in the pool may have been already included into a block.") + assert.LogsDoNotContain(t, hook, "Set withdrawal address message found in the node's operations pool.") +} + +func TestCallWithdrawalEndpoint_Empty(t *testing.T) { + baseurl := "127.0.0.1:3500" + content := []byte("[]") + tmpfile, err := os.CreateTemp("./testdata", "*.json") + require.NoError(t, err) + _, err = tmpfile.Write(content) + require.NoError(t, err) + defer func() { + err := os.Remove(tmpfile.Name()) + require.NoError(t, err) + }() + app := cli.App{} + set := flag.NewFlagSet("test", 0) + set.String("beacon-node-host", baseurl, "") + set.String("path", tmpfile.Name(), "") + set.Bool("confirm", true, "") + set.Bool("accept-terms-of-use", true, "") + assert.NoError(t, set.Set("beacon-node-host", baseurl)) + assert.NoError(t, set.Set("path", tmpfile.Name())) + cliCtx := cli.NewContext(&app, set, nil) + err = setWithdrawalAddresses(cliCtx) + assert.ErrorContains(t, "the list of signed requests is empty", err) +} + +func TestCallWithdrawalEndpoint_Errors(t *testing.T) { + file := "./testdata/change-operations.json" + baseurl := "127.0.0.1:3500" + l, err := net.Listen("tcp", baseurl) + require.NoError(t, err) + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(400) + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(&apimiddleware.IndexedVerificationFailureErrorJson{ + Failures: []*apimiddleware.SingleIndexedVerificationFailureJson{ + {Index: 0, Message: "Could not validate SignedBLSToExecutionChange"}, + }, + }) + require.NoError(t, err) + })) + err = srv.Listener.Close() + require.NoError(t, err) + srv.Listener = l + srv.Start() + defer srv.Close() + hook := logtest.NewGlobal() + + app := cli.App{} + set := flag.NewFlagSet("test", 0) + set.String("beacon-node-host", baseurl, "") + set.String("path", file, "") + set.Bool("confirm", true, "") + set.Bool("accept-terms-of-use", true, "") + assert.NoError(t, set.Set("beacon-node-host", baseurl)) + assert.NoError(t, set.Set("path", file)) + cliCtx := cli.NewContext(&app, set, nil) + + err = setWithdrawalAddresses(cliCtx) + assert.ErrorContains(t, "POST error", err) + + assert.LogsContain(t, hook, "Could not validate SignedBLSToExecutionChange") +} + +func TestVerifyWithdrawal_Mutiple(t *testing.T) { + file := "./testdata/change-operations-multiple.json" + baseurl := "127.0.0.1:3500" + l, err := net.Listen("tcp", baseurl) + require.NoError(t, err) + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodGet { + b, err := os.ReadFile(filepath.Clean(file)) + require.NoError(t, err) + var to []*apimiddleware.SignedBLSToExecutionChangeJson + err = json.Unmarshal(b, &to) + require.NoError(t, err) + err = json.NewEncoder(w).Encode(&apimiddleware.BLSToExecutionChangesPoolResponseJson{ + Data: to, + }) + require.NoError(t, err) + } + })) + err = srv.Listener.Close() + require.NoError(t, err) + srv.Listener = l + srv.Start() + defer srv.Close() + hook := logtest.NewGlobal() + + app := cli.App{} + set := flag.NewFlagSet("test", 0) + set.String("beacon-node-host", baseurl, "") + set.String("path", file, "") + set.Bool("confirm", true, "") + set.Bool("accept-terms-of-use", true, "") + set.Bool("verify-only", true, "") + assert.NoError(t, set.Set("beacon-node-host", baseurl)) + assert.NoError(t, set.Set("path", file)) + cliCtx := cli.NewContext(&app, set, nil) + + err = verifyWithdrawalsInPool(cliCtx) + require.NoError(t, err) + assert.LogsContain(t, hook, "All (total:2) signed withdrawal messages were found in the pool.") + assert.LogsDoNotContain(t, hook, "set withdrawal address message not found in the node's operations pool.") +} diff --git a/cmd/validator/accounts/accounts.go b/cmd/validator/accounts/accounts.go index 6aff7166c2..7c6bed8bc6 100644 --- a/cmd/validator/accounts/accounts.go +++ b/cmd/validator/accounts/accounts.go @@ -186,7 +186,7 @@ var Commands = &cli.Command{ return features.ConfigureValidator(cliCtx) }, Action: func(cliCtx *cli.Context) error { - log.Info("This command will be deprecated in the future in favor of `prysmctl sign validator-exit`") + log.Info("This command will be deprecated in the future in favor of `prysmctl validator exit`") if err := AccountsExit(cliCtx, os.Stdin); err != nil { log.WithError(err).Fatal("Could not perform voluntary exit") }