diff --git a/cmd/prysmctl/validator/cmd.go b/cmd/prysmctl/validator/cmd.go index f3e8ab52c459..d1fcb260cec7 100644 --- a/cmd/prysmctl/validator/cmd.go +++ b/cmd/prysmctl/validator/cmd.go @@ -110,6 +110,7 @@ var Commands = []*cli.Command{ flags.GrpcRetryDelayFlag, flags.ExitAllFlag, flags.ForceExitFlag, + flags.VoluntaryExitJSONOutputPath, features.Mainnet, features.PraterTestnet, features.SepoliaTestnet, diff --git a/cmd/validator/accounts/BUILD.bazel b/cmd/validator/accounts/BUILD.bazel index 22d3454ec685..1f8389deaaf0 100644 --- a/cmd/validator/accounts/BUILD.bazel +++ b/cmd/validator/accounts/BUILD.bazel @@ -49,6 +49,7 @@ go_test( ], embed = [":go_default_library"], deps = [ + "//build/bazel:go_default_library", "//cmd/validator/flags:go_default_library", "//config/params:go_default_library", "//crypto/bls:go_default_library", diff --git a/cmd/validator/accounts/accounts.go b/cmd/validator/accounts/accounts.go index f7dfd84cf925..6c907f426bac 100644 --- a/cmd/validator/accounts/accounts.go +++ b/cmd/validator/accounts/accounts.go @@ -166,6 +166,7 @@ var Commands = &cli.Command{ flags.GrpcRetryDelayFlag, flags.ExitAllFlag, flags.ForceExitFlag, + flags.VoluntaryExitJSONOutputPath, features.Mainnet, features.PraterTestnet, features.SepoliaTestnet, diff --git a/cmd/validator/accounts/exit.go b/cmd/validator/accounts/exit.go index 39672b5b4397..ef845a3cd88a 100644 --- a/cmd/validator/accounts/exit.go +++ b/cmd/validator/accounts/exit.go @@ -83,6 +83,7 @@ func AccountsExit(c *cli.Context, r io.Reader) error { accounts.WithBeaconRPCProvider(beaconRPCProvider), accounts.WithBeaconRESTApiProvider(c.String(flags.BeaconRESTApiProviderFlag.Name)), accounts.WithGRPCHeaders(grpcHeaders), + accounts.WithExitJSONOutputPath(c.String(flags.VoluntaryExitJSONOutputPath.Name)), } // Get full set of public keys from the keymanager. validatingPublicKeys, err := km.FetchValidatingPublicKeys(c.Context) diff --git a/cmd/validator/accounts/exit_test.go b/cmd/validator/accounts/exit_test.go index 0f8647ad8c32..587f811afb18 100644 --- a/cmd/validator/accounts/exit_test.go +++ b/cmd/validator/accounts/exit_test.go @@ -3,12 +3,15 @@ package accounts import ( "bytes" "os" + "path" "path/filepath" "sort" "testing" "time" "github.com/golang/mock/gomock" + "github.com/prysmaticlabs/prysm/v4/build/bazel" + "github.com/prysmaticlabs/prysm/v4/io/file" ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v4/testing/assert" "github.com/prysmaticlabs/prysm/v4/testing/require" @@ -305,3 +308,93 @@ func TestExitAccountsCli_OK_ForceExit(t *testing.T) { require.Equal(t, 1, len(formattedExitedKeys)) assert.Equal(t, "0x"+keystore.Pubkey[:12], formattedExitedKeys[0]) } + +func TestExitAccountsCli_WriteJSON_NoBroadcast(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockValidatorClient := validatormock.NewMockValidatorClient(ctrl) + mockNodeClient := validatormock.NewMockNodeClient(ctrl) + + mockValidatorClient.EXPECT(). + ValidatorIndex(gomock.Any(), gomock.Any()). + Return(ðpb.ValidatorIndexResponse{Index: 1}, nil) + + // Any time in the past will suffice + genesisTime := ×tamppb.Timestamp{ + Seconds: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), + } + + mockNodeClient.EXPECT(). + GetGenesis(gomock.Any(), gomock.Any()). + Return(ðpb.Genesis{GenesisTime: genesisTime}, nil) + + mockValidatorClient.EXPECT(). + DomainData(gomock.Any(), gomock.Any()). + Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, 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. + keystore, _ := createKeystore(t, keysDir) + time.Sleep(time.Second) + + // We initialize a wallet with a local keymanager. + cliCtx := setupWalletCtx(t, &testWalletConfig{ + // Wallet configuration flags. + walletDir: walletDir, + keymanagerKind: keymanager.Local, + walletPasswordFile: passwordFilePath, + accountPasswordFile: passwordFilePath, + // Flag required for ImportAccounts to work. + keysDir: keysDir, + // Flag required for ExitAccounts to work. + voluntaryExitPublicKeys: keystore.Pubkey, + }) + opts := []accounts.Option{ + accounts.WithWalletDir(walletDir), + accounts.WithKeymanagerType(keymanager.Local), + accounts.WithWalletPassword(password), + } + acc, err := accounts.NewCLIManager(opts...) + require.NoError(t, err) + _, err = acc.WalletCreate(cliCtx.Context) + require.NoError(t, err) + require.NoError(t, accountsImport(cliCtx)) + + _, km, err := walletWithKeymanager(cliCtx) + require.NoError(t, err) + require.NotNil(t, km) + + validatingPublicKeys, err := km.FetchValidatingPublicKeys(cliCtx.Context) + require.NoError(t, err) + require.NotNil(t, validatingPublicKeys) + + rawPubKeys, formattedPubKeys, err := accounts.FilterExitAccountsFromUserInput( + cliCtx, &bytes.Buffer{}, validatingPublicKeys, true, + ) + require.NoError(t, err) + require.NotNil(t, rawPubKeys) + require.NotNil(t, formattedPubKeys) + + out := path.Join(bazel.TestTmpDir(), "exits") + + cfg := accounts.PerformExitCfg{ + ValidatorClient: mockValidatorClient, + NodeClient: mockNodeClient, + Keymanager: km, + RawPubKeys: rawPubKeys, + FormattedPubKeys: formattedPubKeys, + OutputDirectory: out, + } + rawExitedKeys, formattedExitedKeys, err := accounts.PerformVoluntaryExit(cliCtx.Context, cfg) + require.NoError(t, err) + require.Equal(t, 1, len(rawExitedKeys)) + assert.DeepEqual(t, rawPubKeys[0], rawExitedKeys[0]) + require.Equal(t, 1, len(formattedExitedKeys)) + assert.Equal(t, "0x"+keystore.Pubkey[:12], formattedExitedKeys[0]) + + require.Equal(t, true, file.FileExists(path.Join(out, "validator-exit-1.json")), "Expected file to exist") +} diff --git a/cmd/validator/flags/flags.go b/cmd/validator/flags/flags.go index f8b420a9b76f..11f2ccb2fade 100644 --- a/cmd/validator/flags/flags.go +++ b/cmd/validator/flags/flags.go @@ -230,6 +230,12 @@ var ( Name: "force-exit", Usage: "Exit without displaying the confirmation prompt", } + VoluntaryExitJSONOutputPath = &cli.StringFlag{ + Name: "exit-json-output-dir", + Usage: "The output directory to write voluntary exits as individual unencrypted JSON " + + "files. If this flag is provided, voluntary exits will be written to the provided " + + "directory and will not be broadcasted.", + } // BackupPasswordFile for encrypting accounts a user wishes to back up. BackupPasswordFile = &cli.StringFlag{ Name: "backup-password-file", diff --git a/validator/accounts/BUILD.bazel b/validator/accounts/BUILD.bazel index 9ff7d8de2ca4..45a0ac477fa4 100644 --- a/validator/accounts/BUILD.bazel +++ b/validator/accounts/BUILD.bazel @@ -40,6 +40,7 @@ go_library( "//validator/accounts/userprompt:go_default_library", "//validator/accounts/wallet:go_default_library", "//validator/client:go_default_library", + "//validator/client/beacon-api:go_default_library", "//validator/client/iface:go_default_library", "//validator/client/node-client-factory:go_default_library", "//validator/client/validator-client-factory:go_default_library", @@ -72,12 +73,15 @@ go_test( data = glob(["testdata/**"]), embed = [":go_default_library"], deps = [ + "//beacon-chain/rpc/apimiddleware:go_default_library", + "//build/bazel:go_default_library", "//cmd/validator/flags:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", "//crypto/bls:go_default_library", "//encoding/bytesutil:go_default_library", + "//io/file:go_default_library", "//proto/eth/service:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//testing/assert:go_default_library", diff --git a/validator/accounts/accounts_exit.go b/validator/accounts/accounts_exit.go index f5bcf9a502ac..a4861479f6ef 100644 --- a/validator/accounts/accounts_exit.go +++ b/validator/accounts/accounts_exit.go @@ -3,7 +3,9 @@ package accounts import ( "bytes" "context" + "encoding/json" "fmt" + "path" "strings" "github.com/ethereum/go-ethereum/common/hexutil" @@ -12,7 +14,10 @@ import ( fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" "github.com/prysmaticlabs/prysm/v4/config/params" "github.com/prysmaticlabs/prysm/v4/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v4/io/file" + eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v4/validator/client" + beacon_api "github.com/prysmaticlabs/prysm/v4/validator/client/beacon-api" "github.com/prysmaticlabs/prysm/v4/validator/client/iface" "github.com/prysmaticlabs/prysm/v4/validator/keymanager" "google.golang.org/protobuf/types/known/emptypb" @@ -25,6 +30,7 @@ type PerformExitCfg struct { Keymanager keymanager.IKeymanager RawPubKeys [][]byte FormattedPubKeys []string + OutputDirectory string } // ExitPassphrase exported for use in test. @@ -62,6 +68,7 @@ func (acm *AccountsCLIManager) Exit(ctx context.Context) error { acm.keymanager, acm.rawPubKeys, acm.formattedPubKeys, + acm.exitJSONOutputPath, } rawExitedKeys, trimmedExitedKeys, err := PerformVoluntaryExit(ctx, cfg) if err != nil { @@ -78,7 +85,23 @@ func PerformVoluntaryExit( ) (rawExitedKeys [][]byte, formattedExitedKeys []string, err error) { var rawNotExitedKeys [][]byte for i, key := range cfg.RawPubKeys { - if err := client.ProposeExit(ctx, cfg.ValidatorClient, cfg.NodeClient, cfg.Keymanager.Sign, key); err != nil { + // When output directory is present, only create the signed exit, but do not propose it. + // Otherwise, propose the exit immediately. + if len(cfg.OutputDirectory) > 0 { + sve, err := client.CreateSignedVoluntaryExit(ctx, cfg.ValidatorClient, cfg.NodeClient, cfg.Keymanager.Sign, key) + if err != nil { + rawNotExitedKeys = append(rawNotExitedKeys, key) + msg := err.Error() + if strings.Contains(msg, blocks.ValidatorAlreadyExitedMsg) || + strings.Contains(msg, blocks.ValidatorCannotExitYetMsg) { + log.Warningf("Could not create voluntary exit for account %s: %s", cfg.FormattedPubKeys[i], msg) + } else { + log.WithError(err).Errorf("voluntary exit failed for account %s", cfg.FormattedPubKeys[i]) + } + } else if err := writeSignedVoluntaryExitJSON(ctx, sve, cfg.OutputDirectory); err != nil { + log.WithError(err).Error("failed to write voluntary exit") + } + } else if err := client.ProposeExit(ctx, cfg.ValidatorClient, cfg.NodeClient, cfg.Keymanager.Sign, key); err != nil { rawNotExitedKeys = append(rawNotExitedKeys, key) msg := err.Error() @@ -148,3 +171,24 @@ func displayExitInfo(rawExitedKeys [][]byte, trimmedExitedKeys []string) { log.Info("No successful voluntary exits") } } + +func writeSignedVoluntaryExitJSON(ctx context.Context, sve *eth.SignedVoluntaryExit, outputDirectory string) error { + if err := file.MkdirAll(outputDirectory); err != nil { + return err + } + + jsve := beacon_api.JsonifySignedVoluntaryExits([]*eth.SignedVoluntaryExit{sve})[0] + b, err := json.Marshal(jsve) + if err != nil { + return errors.Wrap(err, "failed to marshal JSON signed voluntary exit") + } + + filepath := path.Join(outputDirectory, fmt.Sprintf("validator-exit-%s.json", jsve.Exit.ValidatorIndex)) + if err := file.WriteFile(filepath, b); err != nil { + return errors.Wrap(err, "failed to write validator exist json") + } + + log.Infof("Wrote signed validator exit JSON to %s", filepath) + + return nil +} diff --git a/validator/accounts/accounts_exit_test.go b/validator/accounts/accounts_exit_test.go index 0e4b3bd597be..632cf0525490 100644 --- a/validator/accounts/accounts_exit_test.go +++ b/validator/accounts/accounts_exit_test.go @@ -1,10 +1,18 @@ package accounts import ( + "context" + "encoding/json" + "fmt" + "path" "testing" + "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/apimiddleware" + "github.com/prysmaticlabs/prysm/v4/build/bazel" fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" "github.com/prysmaticlabs/prysm/v4/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v4/io/file" + eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v4/testing/assert" "github.com/prysmaticlabs/prysm/v4/testing/require" "github.com/sirupsen/logrus/hooks/test" @@ -34,3 +42,26 @@ func TestPrepareAllKeys(t *testing.T) { assert.Equal(t, "0x6b6579310000", formatted[0]) assert.Equal(t, "0x6b6579320000", formatted[1]) } + +func TestWriteSignedVoluntaryExitJSON(t *testing.T) { + sve := ð.SignedVoluntaryExit{ + Exit: ð.VoluntaryExit{ + Epoch: 5, + ValidatorIndex: 300, + }, + Signature: []byte{0x01, 0x02}, + } + + output := path.Join(bazel.TestTmpDir(), "TestWriteSignedVoluntaryExitJSON") + require.NoError(t, writeSignedVoluntaryExitJSON(context.Background(), sve, output)) + + b, err := file.ReadFileAsBytes(path.Join(output, "validator-exit-300.json")) + require.NoError(t, err) + + svej := &apimiddleware.SignedVoluntaryExitJson{} + require.NoError(t, json.Unmarshal(b, svej)) + + require.Equal(t, fmt.Sprintf("%d", sve.Exit.Epoch), svej.Exit.Epoch) + require.Equal(t, fmt.Sprintf("%d", sve.Exit.ValidatorIndex), svej.Exit.ValidatorIndex) + require.Equal(t, "0x0102", svej.Signature) +} diff --git a/validator/accounts/cli_manager.go b/validator/accounts/cli_manager.go index 99740b6e9274..6ac89fd58dd6 100644 --- a/validator/accounts/cli_manager.go +++ b/validator/accounts/cli_manager.go @@ -56,6 +56,7 @@ type AccountsCLIManager struct { filteredPubKeys []bls.PublicKey rawPubKeys [][]byte formattedPubKeys []string + exitJSONOutputPath string walletDir string walletPassword string mnemonic string diff --git a/validator/accounts/cli_options.go b/validator/accounts/cli_options.go index 8c58dc76200c..a464ba29cd2f 100644 --- a/validator/accounts/cli_options.go +++ b/validator/accounts/cli_options.go @@ -205,6 +205,13 @@ func WithFormattedPubKeys(formattedPubKeys []string) Option { } } +func WithExitJSONOutputPath(outputPath string) Option { + return func(acc *AccountsCLIManager) error { + acc.exitJSONOutputPath = outputPath + return nil + } +} + // WithWalletDir specifies the password for backups. func WithWalletDir(walletDir string) Option { return func(acc *AccountsCLIManager) error { diff --git a/validator/client/beacon-api/beacon_block_json_helpers.go b/validator/client/beacon-api/beacon_block_json_helpers.go index 724d44aca8ce..2154b583eabf 100644 --- a/validator/client/beacon-api/beacon_block_json_helpers.go +++ b/validator/client/beacon-api/beacon_block_json_helpers.go @@ -97,7 +97,8 @@ func jsonifyProposerSlashings(proposerSlashings []*ethpb.ProposerSlashing) []*ap return jsonProposerSlashings } -func jsonifySignedVoluntaryExits(voluntaryExits []*ethpb.SignedVoluntaryExit) []*apimiddleware.SignedVoluntaryExitJson { +// JsonifySignedVoluntaryExits converts an array of voluntary exit structs to a JSON hex string compatible format. +func JsonifySignedVoluntaryExits(voluntaryExits []*ethpb.SignedVoluntaryExit) []*apimiddleware.SignedVoluntaryExitJson { jsonSignedVoluntaryExits := make([]*apimiddleware.SignedVoluntaryExitJson, len(voluntaryExits)) for index, signedVoluntaryExit := range voluntaryExits { jsonSignedVoluntaryExit := &apimiddleware.SignedVoluntaryExitJson{ diff --git a/validator/client/beacon-api/beacon_block_json_helpers_test.go b/validator/client/beacon-api/beacon_block_json_helpers_test.go index 1c04dfb1cd0c..68ac90142e8c 100644 --- a/validator/client/beacon-api/beacon_block_json_helpers_test.go +++ b/validator/client/beacon-api/beacon_block_json_helpers_test.go @@ -508,7 +508,7 @@ func TestBeaconBlockJsonHelpers_JsonifySignedVoluntaryExits(t *testing.T) { }, } - result := jsonifySignedVoluntaryExits(input) + result := JsonifySignedVoluntaryExits(input) assert.DeepEqual(t, expectedResult, result) } diff --git a/validator/client/beacon-api/propose_beacon_block.go b/validator/client/beacon-api/propose_beacon_block.go index 59fa45fffca0..b8c77c22501d 100644 --- a/validator/client/beacon-api/propose_beacon_block.go +++ b/validator/client/beacon-api/propose_beacon_block.go @@ -127,7 +127,7 @@ func marshallBeaconBlockPhase0(block *ethpb.SignedBeaconBlock) ([]byte, error) { Graffiti: hexutil.Encode(block.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(block.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(block.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), }, ParentRoot: hexutil.Encode(block.Block.ParentRoot), ProposerIndex: uint64ToString(block.Block.ProposerIndex), @@ -155,7 +155,7 @@ func marshallBeaconBlockAltair(block *ethpb.SignedBeaconBlockAltair) ([]byte, er Graffiti: hexutil.Encode(block.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(block.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(block.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), SyncAggregate: &apimiddleware.SyncAggregateJson{ SyncCommitteeBits: hexutil.Encode(block.Block.Body.SyncAggregate.SyncCommitteeBits), SyncCommitteeSignature: hexutil.Encode(block.Block.Body.SyncAggregate.SyncCommitteeSignature), @@ -183,7 +183,7 @@ func marshallBeaconBlockBellatrix(block *ethpb.SignedBeaconBlockBellatrix) ([]by Graffiti: hexutil.Encode(block.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(block.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(block.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), SyncAggregate: &apimiddleware.SyncAggregateJson{ SyncCommitteeBits: hexutil.Encode(block.Block.Body.SyncAggregate.SyncCommitteeBits), SyncCommitteeSignature: hexutil.Encode(block.Block.Body.SyncAggregate.SyncCommitteeSignature), @@ -227,7 +227,7 @@ func marshallBeaconBlockBlindedBellatrix(block *ethpb.SignedBlindedBeaconBlockBe Graffiti: hexutil.Encode(block.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(block.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(block.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), SyncAggregate: &apimiddleware.SyncAggregateJson{ SyncCommitteeBits: hexutil.Encode(block.Block.Body.SyncAggregate.SyncCommitteeBits), SyncCommitteeSignature: hexutil.Encode(block.Block.Body.SyncAggregate.SyncCommitteeSignature), @@ -271,7 +271,7 @@ func marshallBeaconBlockCapella(block *ethpb.SignedBeaconBlockCapella) ([]byte, Graffiti: hexutil.Encode(block.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(block.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(block.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), SyncAggregate: &apimiddleware.SyncAggregateJson{ SyncCommitteeBits: hexutil.Encode(block.Block.Body.SyncAggregate.SyncCommitteeBits), SyncCommitteeSignature: hexutil.Encode(block.Block.Body.SyncAggregate.SyncCommitteeSignature), @@ -317,7 +317,7 @@ func marshallBeaconBlockBlindedCapella(block *ethpb.SignedBlindedBeaconBlockCape Graffiti: hexutil.Encode(block.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(block.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(block.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(block.Block.Body.VoluntaryExits), SyncAggregate: &apimiddleware.SyncAggregateJson{ SyncCommitteeBits: hexutil.Encode(block.Block.Body.SyncAggregate.SyncCommitteeBits), SyncCommitteeSignature: hexutil.Encode(block.Block.Body.SyncAggregate.SyncCommitteeSignature), diff --git a/validator/client/beacon-api/propose_beacon_block_altair_test.go b/validator/client/beacon-api/propose_beacon_block_altair_test.go index c2c07c1eb35c..2596e53d77c7 100644 --- a/validator/client/beacon-api/propose_beacon_block_altair_test.go +++ b/validator/client/beacon-api/propose_beacon_block_altair_test.go @@ -41,7 +41,7 @@ func TestProposeBeaconBlock_Altair(t *testing.T) { Graffiti: hexutil.Encode(altairBlock.Altair.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(altairBlock.Altair.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(altairBlock.Altair.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(altairBlock.Altair.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(altairBlock.Altair.Block.Body.VoluntaryExits), SyncAggregate: &apimiddleware.SyncAggregateJson{ SyncCommitteeBits: hexutil.Encode(altairBlock.Altair.Block.Body.SyncAggregate.SyncCommitteeBits), SyncCommitteeSignature: hexutil.Encode(altairBlock.Altair.Block.Body.SyncAggregate.SyncCommitteeSignature), diff --git a/validator/client/beacon-api/propose_beacon_block_bellatrix_test.go b/validator/client/beacon-api/propose_beacon_block_bellatrix_test.go index 697c992a0726..1739ca349fa0 100644 --- a/validator/client/beacon-api/propose_beacon_block_bellatrix_test.go +++ b/validator/client/beacon-api/propose_beacon_block_bellatrix_test.go @@ -42,7 +42,7 @@ func TestProposeBeaconBlock_Bellatrix(t *testing.T) { Graffiti: hexutil.Encode(bellatrixBlock.Bellatrix.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(bellatrixBlock.Bellatrix.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(bellatrixBlock.Bellatrix.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(bellatrixBlock.Bellatrix.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(bellatrixBlock.Bellatrix.Block.Body.VoluntaryExits), SyncAggregate: &apimiddleware.SyncAggregateJson{ SyncCommitteeBits: hexutil.Encode(bellatrixBlock.Bellatrix.Block.Body.SyncAggregate.SyncCommitteeBits), SyncCommitteeSignature: hexutil.Encode(bellatrixBlock.Bellatrix.Block.Body.SyncAggregate.SyncCommitteeSignature), diff --git a/validator/client/beacon-api/propose_beacon_block_blinded_bellatrix_test.go b/validator/client/beacon-api/propose_beacon_block_blinded_bellatrix_test.go index f7cc9dd929c9..0efad719291d 100644 --- a/validator/client/beacon-api/propose_beacon_block_blinded_bellatrix_test.go +++ b/validator/client/beacon-api/propose_beacon_block_blinded_bellatrix_test.go @@ -43,7 +43,7 @@ func TestProposeBeaconBlock_BlindedBellatrix(t *testing.T) { Graffiti: hexutil.Encode(blindedBellatrixBlock.BlindedBellatrix.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(blindedBellatrixBlock.BlindedBellatrix.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(blindedBellatrixBlock.BlindedBellatrix.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(blindedBellatrixBlock.BlindedBellatrix.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(blindedBellatrixBlock.BlindedBellatrix.Block.Body.VoluntaryExits), SyncAggregate: &apimiddleware.SyncAggregateJson{ SyncCommitteeBits: hexutil.Encode(blindedBellatrixBlock.BlindedBellatrix.Block.Body.SyncAggregate.SyncCommitteeBits), SyncCommitteeSignature: hexutil.Encode(blindedBellatrixBlock.BlindedBellatrix.Block.Body.SyncAggregate.SyncCommitteeSignature), diff --git a/validator/client/beacon-api/propose_beacon_block_blinded_capella_test.go b/validator/client/beacon-api/propose_beacon_block_blinded_capella_test.go index d3d70ff7dd40..68ba5a0dec85 100644 --- a/validator/client/beacon-api/propose_beacon_block_blinded_capella_test.go +++ b/validator/client/beacon-api/propose_beacon_block_blinded_capella_test.go @@ -43,7 +43,7 @@ func TestProposeBeaconBlock_BlindedCapella(t *testing.T) { Graffiti: hexutil.Encode(blindedCapellaBlock.BlindedCapella.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(blindedCapellaBlock.BlindedCapella.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(blindedCapellaBlock.BlindedCapella.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(blindedCapellaBlock.BlindedCapella.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(blindedCapellaBlock.BlindedCapella.Block.Body.VoluntaryExits), SyncAggregate: &apimiddleware.SyncAggregateJson{ SyncCommitteeBits: hexutil.Encode(blindedCapellaBlock.BlindedCapella.Block.Body.SyncAggregate.SyncCommitteeBits), SyncCommitteeSignature: hexutil.Encode(blindedCapellaBlock.BlindedCapella.Block.Body.SyncAggregate.SyncCommitteeSignature), diff --git a/validator/client/beacon-api/propose_beacon_block_capella_test.go b/validator/client/beacon-api/propose_beacon_block_capella_test.go index 0632286a4c05..90296aaa87ed 100644 --- a/validator/client/beacon-api/propose_beacon_block_capella_test.go +++ b/validator/client/beacon-api/propose_beacon_block_capella_test.go @@ -42,7 +42,7 @@ func TestProposeBeaconBlock_Capella(t *testing.T) { Graffiti: hexutil.Encode(capellaBlock.Capella.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(capellaBlock.Capella.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(capellaBlock.Capella.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(capellaBlock.Capella.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(capellaBlock.Capella.Block.Body.VoluntaryExits), SyncAggregate: &apimiddleware.SyncAggregateJson{ SyncCommitteeBits: hexutil.Encode(capellaBlock.Capella.Block.Body.SyncAggregate.SyncCommitteeBits), SyncCommitteeSignature: hexutil.Encode(capellaBlock.Capella.Block.Body.SyncAggregate.SyncCommitteeSignature), diff --git a/validator/client/beacon-api/propose_beacon_block_phase0_test.go b/validator/client/beacon-api/propose_beacon_block_phase0_test.go index 4b1b47cc44b9..c0c1a6263fa2 100644 --- a/validator/client/beacon-api/propose_beacon_block_phase0_test.go +++ b/validator/client/beacon-api/propose_beacon_block_phase0_test.go @@ -41,7 +41,7 @@ func TestProposeBeaconBlock_Phase0(t *testing.T) { Graffiti: hexutil.Encode(phase0Block.Phase0.Block.Body.Graffiti), ProposerSlashings: jsonifyProposerSlashings(phase0Block.Phase0.Block.Body.ProposerSlashings), RandaoReveal: hexutil.Encode(phase0Block.Phase0.Block.Body.RandaoReveal), - VoluntaryExits: jsonifySignedVoluntaryExits(phase0Block.Phase0.Block.Body.VoluntaryExits), + VoluntaryExits: JsonifySignedVoluntaryExits(phase0Block.Phase0.Block.Body.VoluntaryExits), }, }, } diff --git a/validator/client/propose.go b/validator/client/propose.go index 9147fd7f9fd7..13499f3352be 100644 --- a/validator/client/propose.go +++ b/validator/client/propose.go @@ -205,13 +205,39 @@ func ProposeExit( ctx, span := trace.StartSpan(ctx, "validator.ProposeExit") defer span.End() + signedExit, err := CreateSignedVoluntaryExit(ctx, validatorClient, nodeClient, signer, pubKey) + if err != nil { + return errors.Wrap(err, "failed to create signed voluntary exit") + } + exitResp, err := validatorClient.ProposeExit(ctx, signedExit) + if err != nil { + return errors.Wrap(err, "failed to propose voluntary exit") + } + + span.AddAttributes( + trace.StringAttribute("exitRoot", fmt.Sprintf("%#x", exitResp.ExitRoot)), + ) + + return nil +} + +func CreateSignedVoluntaryExit( + ctx context.Context, + validatorClient iface.ValidatorClient, + nodeClient iface.NodeClient, + signer iface.SigningFunc, + pubKey []byte, +) (*ethpb.SignedVoluntaryExit, error) { + ctx, span := trace.StartSpan(ctx, "validator.CreateSignedVoluntaryExit") + defer span.End() + indexResponse, err := validatorClient.ValidatorIndex(ctx, ðpb.ValidatorIndexRequest{PublicKey: pubKey}) if err != nil { - return errors.Wrap(err, "gRPC call to get validator index failed") + return nil, errors.Wrap(err, "gRPC call to get validator index failed") } genesisResponse, err := nodeClient.GetGenesis(ctx, &emptypb.Empty{}) if err != nil { - return errors.Wrap(err, "gRPC call to get genesis time failed") + return nil, errors.Wrap(err, "gRPC call to get genesis time failed") } totalSecondsPassed := prysmTime.Now().Unix() - genesisResponse.GenesisTime.Seconds currentEpoch := primitives.Epoch(uint64(totalSecondsPassed) / uint64(params.BeaconConfig().SlotsPerEpoch.Mul(params.BeaconConfig().SecondsPerSlot))) @@ -219,20 +245,10 @@ func ProposeExit( exit := ðpb.VoluntaryExit{Epoch: currentEpoch, ValidatorIndex: indexResponse.Index} sig, err := signVoluntaryExit(ctx, validatorClient, signer, pubKey, exit, currentSlot) if err != nil { - return errors.Wrap(err, "failed to sign voluntary exit") + return nil, errors.Wrap(err, "failed to sign voluntary exit") } - signedExit := ðpb.SignedVoluntaryExit{Exit: exit, Signature: sig} - exitResp, err := validatorClient.ProposeExit(ctx, signedExit) - if err != nil { - return errors.Wrap(err, "failed to propose voluntary exit") - } - - span.AddAttributes( - trace.StringAttribute("exitRoot", fmt.Sprintf("%#x", exitResp.ExitRoot)), - ) - - return nil + return ðpb.SignedVoluntaryExit{Exit: exit, Signature: sig}, nil } // Sign randao reveal with randao domain and private key.