prysmctl: changeblstoexecutionaddress ( withdrawals) (#11790)

* adding unit test for withdrawal

* updating to handle api changes

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* updating based on feedback

* rewording prints

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Potuz <potuz@prysmaticlabs.com>

* 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 <potuz@prysmaticlabs.com>

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Potuz <potuz@prysmaticlabs.com>

* adding in website

* addressing review comment

* adding new verify only flag

* Update cmd/prysmctl/validator/withdraw.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/withdraw.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/withdraw.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/withdraw.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update api/client/beacon/client.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update api/client/beacon/client.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update api/client/beacon/client.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update api/client/beacon/client.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/withdraw.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* addressing comments

* fixing unit test

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* address comments

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* 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 <rkapka@wp.pl>
Co-authored-by: Potuz <potuz@prysmaticlabs.com>
Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
This commit is contained in:
james-prysm
2023-01-24 04:05:55 -06:00
committed by GitHub
parent 04010d45c8
commit 38f095d556
14 changed files with 724 additions and 93 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"}]

View File

@@ -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"}]

View File

@@ -0,0 +1 @@
[{"message":{"validator_index":"1","from_bls_pubkey":"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xa97103e15d3dbdaa75fb15cea782e4a11329eea77d155864ec682d7907b3b70c7771960bef7be1b1c4e08fe735888b950c1a22053f6049b35736f48e6dd018392efa3896c9e427ea4e100e86e9131b5ea2673388a4bf188407a630ba405b7dc5"}]

View File

@@ -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"}}]

View File

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

View File

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

View File

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