From ccd30ae6e799b2775313188df2592510c8fe35c1 Mon Sep 17 00:00:00 2001 From: olegshmuelov <45327364+olegshmuelov@users.noreply.github.com> Date: Tue, 28 Mar 2023 12:10:36 +0300 Subject: [PATCH] Capella withdrawals (#95) --- cli/cmd/flags.go | 79 ++++++- cli/cmd/wallet/cmd/account/create.go | 10 +- cli/cmd/wallet/cmd/account/credentials.go | 34 +++ .../wallet/cmd/account/credentials_test.go | 217 ++++++++++++++++++ cli/cmd/wallet/cmd/account/deposit-data.go | 6 +- cli/cmd/wallet/cmd/account/flag/create.go | 74 ------ .../wallet/cmd/account/flag/credentials.go | 193 ++++++++++++++++ .../wallet/cmd/account/flag/voluntary_exit.go | 116 ++++++++++ .../cmd/account/handler/handler_create.go | 12 +- .../account/handler/handler_credentials.go | 191 +++++++++++++++ .../account/handler/handler_deposit-data.go | 4 +- .../account/handler/handler_voluntary_exit.go | 202 ++++++++++++++++ cli/cmd/wallet/cmd/account/voluntary_exit.go | 34 +++ .../wallet/cmd/account/voluntary_exit_test.go | 120 ++++++++++ cli/cmd/wallet/cmd/publickey/flag/generate.go | 33 --- cli/cmd/wallet/cmd/publickey/generate.go | 5 +- .../cmd/publickey/handler/handler_generate.go | 6 +- core/networks.go | 21 +- core/validator_account.go | 10 + core/wallet.go | 3 +- eth1_deposit/eth1_deposit.go | 31 +-- go.mod | 2 + go.sum | 30 ++- signer/sign_bls_to_execution_change.go | 39 ++++ signer/sign_bls_to_execution_change_test.go | 94 ++++++++ signer/sign_voluntary_exit.go | 36 +++ signer/sign_voluntary_exit_test.go | 87 +++++++ signer/validator_signer.go | 3 + stores/inmemory/marshalable_test.go | 1 + stores/inmemory/slashing_test.go | 1 + wallets/hd/wallet.go | 21 +- 31 files changed, 1541 insertions(+), 174 deletions(-) create mode 100644 cli/cmd/wallet/cmd/account/credentials.go create mode 100644 cli/cmd/wallet/cmd/account/credentials_test.go create mode 100644 cli/cmd/wallet/cmd/account/flag/credentials.go create mode 100644 cli/cmd/wallet/cmd/account/flag/voluntary_exit.go create mode 100644 cli/cmd/wallet/cmd/account/handler/handler_credentials.go create mode 100644 cli/cmd/wallet/cmd/account/handler/handler_voluntary_exit.go create mode 100644 cli/cmd/wallet/cmd/account/voluntary_exit.go create mode 100644 cli/cmd/wallet/cmd/account/voluntary_exit_test.go delete mode 100644 cli/cmd/wallet/cmd/publickey/flag/generate.go create mode 100644 signer/sign_bls_to_execution_change.go create mode 100644 signer/sign_bls_to_execution_change_test.go create mode 100644 signer/sign_voluntary_exit.go create mode 100644 signer/sign_voluntary_exit_test.go diff --git a/cli/cmd/flags.go b/cli/cmd/flags.go index 4fe1375..2686ea0 100644 --- a/cli/cmd/flags.go +++ b/cli/cmd/flags.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/pkg/errors" "github.com/spf13/cobra" @@ -8,14 +10,42 @@ import ( "github.com/bloxapp/eth2-key-manager/core" ) +// ResponseType represents the network. +type ResponseType string + +// Available response types. +const ( + // StorageResponseType represents the storage response type. + StorageResponseType ResponseType = "storage" + + // ObjectResponseType represents the storage response type. + ObjectResponseType ResponseType = "object" +) + +// ResponseTypeFromString returns response type from the given string value +func ResponseTypeFromString(n string) ResponseType { + switch n { + case string(StorageResponseType): + return StorageResponseType + case string(ObjectResponseType): + return ObjectResponseType + default: + panic(fmt.Sprintf("undefined response type %s", n)) + } +} + // Flag names. const ( - networkFlag = "network" + networkFlag = "network" + accumulateFlag = "accumulate" + seedFlag = "seed" + indexFlag = "index" + responseTypeFlag = "response-type" ) // AddNetworkFlag adds the network flag to the command func AddNetworkFlag(c *cobra.Command) { - cliflag.AddPersistentStringFlag(c, networkFlag, "", "Ethereum network", false) + cliflag.AddPersistentStringFlag(c, networkFlag, "", "Ethereum network", true) } // GetNetworkFlagValue gets the network flag from the command @@ -32,3 +62,48 @@ func GetNetworkFlagValue(c *cobra.Command) (core.Network, error) { return ret, nil } + +// AddAccumulateFlag adds the accumulate flag to the command +func AddAccumulateFlag(c *cobra.Command) { + cliflag.AddPersistentBoolFlag(c, accumulateFlag, false, "accumulate accounts", false) +} + +// GetAccumulateFlagValue gets the accumulate flag from the command +func GetAccumulateFlagValue(c *cobra.Command) (bool, error) { + return c.Flags().GetBool(accumulateFlag) +} + +// AddSeedFlag adds the seed flag to the command +func AddSeedFlag(c *cobra.Command) { + cliflag.AddPersistentStringFlag(c, seedFlag, "", "seed", false) +} + +// GetSeedFlagValue gets the seed flag from the command +func GetSeedFlagValue(c *cobra.Command) (string, error) { + return c.Flags().GetString(seedFlag) +} + +// AddIndexFlag adds the index flag to the command +func AddIndexFlag(c *cobra.Command) { + cliflag.AddPersistentIntFlag(c, indexFlag, 0, "public key index", true) +} + +// GetIndexFlagValue gets the index flag from the command +func GetIndexFlagValue(c *cobra.Command) (int, error) { + return c.Flags().GetInt(indexFlag) +} + +// AddResponseTypeFlag adds the response-type flag to the command +func AddResponseTypeFlag(c *cobra.Command) { + cliflag.AddPersistentStringFlag(c, responseTypeFlag, string(StorageResponseType), "response type", false) +} + +// GetResponseTypeFlagValue gets the response-type flag from the command +func GetResponseTypeFlagValue(c *cobra.Command) (ResponseType, error) { + responseTypeValue, err := c.Flags().GetString(responseTypeFlag) + if err != nil { + return "", err + } + + return ResponseTypeFromString(responseTypeValue), nil +} diff --git a/cli/cmd/wallet/cmd/account/create.go b/cli/cmd/wallet/cmd/account/create.go index ff61998..5477c5f 100644 --- a/cli/cmd/wallet/cmd/account/create.go +++ b/cli/cmd/wallet/cmd/account/create.go @@ -21,15 +21,15 @@ var createCmd = &cobra.Command{ func init() { // Define flags for the command. - flag.AddIndexFlag(createCmd) - flag.AddSeedFlag(createCmd) + rootcmd.AddNetworkFlag(createCmd) + rootcmd.AddSeedFlag(createCmd) + rootcmd.AddIndexFlag(createCmd) + rootcmd.AddAccumulateFlag(createCmd) + rootcmd.AddResponseTypeFlag(createCmd) flag.AddPrivateKeyFlag(createCmd) - flag.AddAccumulateFlag(createCmd) - flag.AddResponseTypeFlag(createCmd) flag.AddHighestSourceFlag(createCmd) flag.AddHighestTargetFlag(createCmd) flag.AddHighestProposalFlag(createCmd) - rootcmd.AddNetworkFlag(createCmd) Command.AddCommand(createCmd) } diff --git a/cli/cmd/wallet/cmd/account/credentials.go b/cli/cmd/wallet/cmd/account/credentials.go new file mode 100644 index 0000000..61c3ea8 --- /dev/null +++ b/cli/cmd/wallet/cmd/account/credentials.go @@ -0,0 +1,34 @@ +package account + +import ( + "github.com/spf13/cobra" + + rootcmd "github.com/bloxapp/eth2-key-manager/cli/cmd" + "github.com/bloxapp/eth2-key-manager/cli/cmd/wallet/cmd/account/flag" + "github.com/bloxapp/eth2-key-manager/cli/cmd/wallet/cmd/account/handler" +) + +// credentialsCmd represents the credentials account command. +var credentialsCmd = &cobra.Command{ + Use: "credentials", + Short: "Sign BLS to execution message", + Long: `This command signing BLS to execution change using seed`, + RunE: func(cmd *cobra.Command, args []string) error { + handler := handler.New(rootcmd.ResultPrinter) + return handler.Credentials(cmd, args) + }, +} + +func init() { + // Define flags for the command. + rootcmd.AddNetworkFlag(credentialsCmd) + rootcmd.AddSeedFlag(credentialsCmd) + rootcmd.AddIndexFlag(credentialsCmd) + rootcmd.AddAccumulateFlag(credentialsCmd) + flag.AddValidatorIndicesFlag(credentialsCmd) + flag.AddValidatorPublicKeysFlag(credentialsCmd) + flag.AddWithdrawalCredentialsFlag(credentialsCmd) + flag.AddToExecutionAddressFlag(credentialsCmd) + + Command.AddCommand(credentialsCmd) +} diff --git a/cli/cmd/wallet/cmd/account/credentials_test.go b/cli/cmd/wallet/cmd/account/credentials_test.go new file mode 100644 index 0000000..ef847ff --- /dev/null +++ b/cli/cmd/wallet/cmd/account/credentials_test.go @@ -0,0 +1,217 @@ +package account_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/bloxapp/eth2-key-manager/cli/cmd" + "github.com/bloxapp/eth2-key-manager/cli/util/printer" +) + +func TestAccountCredentials(t *testing.T) { + t.Run("Successfully handle credentials change at specific index", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "credentials", + "--seed=847d135b3aecac8ae77c3fdfd46dc5849ad3b5bacd30a1b9082b6ff53c77357e923b12fcdc3d02728fd35c3685de1fe1e9c052c48f0d83566b1b2287cf0e54c3", + "--index=1", + "--validator-indices=273230", + "--validator-public-keys=0xa1a593775967bf88bb6c14ac109c12e52dc836fa139bd1ba6ca873d65fe91bb7a0fc79c7b2a7315482a81f31e6b1018a", + "--withdrawal-credentials=0x00202f88c6116d27f5de06eeda3b801d3a4eeab8eb09f879848445fffc8948a5", + "--to-execution-address=0x3e6935b8250Cf9A777862871649E5594bE08779e", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.NotNil(t, actualOutput) + require.NoError(t, err) + }) + + t.Run("Successfully handle accumulated credentials change", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "credentials", + "--seed=847d135b3aecac8ae77c3fdfd46dc5849ad3b5bacd30a1b9082b6ff53c77357e923b12fcdc3d02728fd35c3685de1fe1e9c052c48f0d83566b1b2287cf0e54c3", + "--index=1", + "--accumulate=true", + "--validator-indices=273230,273407", + "--validator-public-keys=0xb2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c,0xa1a593775967bf88bb6c14ac109c12e52dc836fa139bd1ba6ca873d65fe91bb7a0fc79c7b2a7315482a81f31e6b1018a", + "--withdrawal-credentials=0x00d9cdf17e3a79317a4e5cd18580b1d10b1df360bbca5c5f8ac5b79b45c29d15,0x00202f88c6116d27f5de06eeda3b801d3a4eeab8eb09f879848445fffc8948a5", + "--to-execution-address=0x3e6935b8250Cf9A777862871649E5594bE08779e,0x3e6935b8250Cf9A777862871649E5594bE08779e", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.NotNil(t, actualOutput) + require.NoError(t, err) + }) + + t.Run("Only one validator can be specified if accumulate is false", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "credentials", + "--seed=847d135b3aecac8ae77c3fdfd46dc5849ad3b5bacd30a1b9082b6ff53c77357e923b12fcdc3d02728fd35c3685de1fe1e9c052c48f0d83566b1b2287cf0e54c3", + "--index=1", + "--accumulate=false", + "--validator-indices=273230,273407", + "--validator-public-keys=0xb2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c,0xa1a593775967bf88bb6c14ac109c12e52dc836fa139bd1ba6ca873d65fe91bb7a0fc79c7b2a7315482a81f31e6b1018a", + "--withdrawal-credentials=0x00d9cdf17e3a79317a4e5cd18580b1d10b1df360bbca5c5f8ac5b79b45c29d15,0x00202f88c6116d27f5de06eeda3b801d3a4eeab8eb09f879848445fffc8948a5", + "--to-execution-address=0x3e6935b8250Cf9A777862871649E5594bE08779e,0x3e6935b8250Cf9A777862871649E5594bE08779e", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.EqualValues(t, actualOutput, "") + require.Error(t, err) + require.EqualError(t, err, "failed to collect credentials flags: only one validator can be specified if accumulate is false") + }) + + t.Run("Not equal length - should be 2 validator indices", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "credentials", + "--seed=847d135b3aecac8ae77c3fdfd46dc5849ad3b5bacd30a1b9082b6ff53c77357e923b12fcdc3d02728fd35c3685de1fe1e9c052c48f0d83566b1b2287cf0e54c3", + "--index=1", + "--accumulate=true", + "--validator-indices=273230,273407,273407", + "--validator-public-keys=0xb2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c,0xa1a593775967bf88bb6c14ac109c12e52dc836fa139bd1ba6ca873d65fe91bb7a0fc79c7b2a7315482a81f31e6b1018a", + "--withdrawal-credentials=0x00d9cdf17e3a79317a4e5cd18580b1d10b1df360bbca5c5f8ac5b79b45c29d15,0x00202f88c6116d27f5de06eeda3b801d3a4eeab8eb09f879848445fffc8948a5", + "--to-execution-address=0x3e6935b8250Cf9A777862871649E5594bE08779e,0x3e6935b8250Cf9A777862871649E5594bE08779e", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.EqualValues(t, actualOutput, "") + require.Error(t, err) + require.EqualError(t, err, "failed to collect credentials flags: validator indices, public keys, withdrawal credentials and to execution addresses must be of equal length") + }) + + t.Run("Not equal length - should be two public keys", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "credentials", + "--seed=847d135b3aecac8ae77c3fdfd46dc5849ad3b5bacd30a1b9082b6ff53c77357e923b12fcdc3d02728fd35c3685de1fe1e9c052c48f0d83566b1b2287cf0e54c3", + "--index=1", + "--accumulate=true", + "--validator-indices=273230,273407", + "--validator-public-keys=0xa1a593775967bf88bb6c14ac109c12e52dc836fa139bd1ba6ca873d65fe91bb7a0fc79c7b2a7315482a81f31e6b1018a", + "--withdrawal-credentials=0x00d9cdf17e3a79317a4e5cd18580b1d10b1df360bbca5c5f8ac5b79b45c29d15,0x00202f88c6116d27f5de06eeda3b801d3a4eeab8eb09f879848445fffc8948a5", + "--to-execution-address=0x3e6935b8250Cf9A777862871649E5594bE08779e,0x3e6935b8250Cf9A777862871649E5594bE08779e", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.EqualValues(t, actualOutput, "") + require.Error(t, err) + require.EqualError(t, err, "failed to collect credentials flags: validator indices, public keys, withdrawal credentials and to execution addresses must be of equal length") + }) + + t.Run("Not equal length - should be two withdrawal credentials", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "credentials", + "--seed=847d135b3aecac8ae77c3fdfd46dc5849ad3b5bacd30a1b9082b6ff53c77357e923b12fcdc3d02728fd35c3685de1fe1e9c052c48f0d83566b1b2287cf0e54c3", + "--index=1", + "--accumulate=true", + "--validator-indices=273230,273407", + "--validator-public-keys=0xb2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c,0xa1a593775967bf88bb6c14ac109c12e52dc836fa139bd1ba6ca873d65fe91bb7a0fc79c7b2a7315482a81f31e6b1018a", + "--withdrawal-credentials=0x00202f88c6116d27f5de06eeda3b801d3a4eeab8eb09f879848445fffc8948a5", + "--to-execution-address=0x3e6935b8250Cf9A777862871649E5594bE08779e,0x3e6935b8250Cf9A777862871649E5594bE08779e", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.EqualValues(t, actualOutput, "") + require.Error(t, err) + require.EqualError(t, err, "failed to collect credentials flags: validator indices, public keys, withdrawal credentials and to execution addresses must be of equal length") + }) + + t.Run("Not equal length - should be two to execution addresses", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "credentials", + "--seed=847d135b3aecac8ae77c3fdfd46dc5849ad3b5bacd30a1b9082b6ff53c77357e923b12fcdc3d02728fd35c3685de1fe1e9c052c48f0d83566b1b2287cf0e54c3", + "--index=1", + "--accumulate=true", + "--validator-indices=273230,273407", + "--validator-public-keys=0xb2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c,0xa1a593775967bf88bb6c14ac109c12e52dc836fa139bd1ba6ca873d65fe91bb7a0fc79c7b2a7315482a81f31e6b1018a", + "--withdrawal-credentials=0x00d9cdf17e3a79317a4e5cd18580b1d10b1df360bbca5c5f8ac5b79b45c29d15,0x00202f88c6116d27f5de06eeda3b801d3a4eeab8eb09f879848445fffc8948a5", + "--to-execution-address=0x3e6935b8250Cf9A777862871649E5594bE08779e", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.EqualValues(t, actualOutput, "") + require.Error(t, err) + require.EqualError(t, err, "failed to collect credentials flags: validator indices, public keys, withdrawal credentials and to execution addresses must be of equal length") + }) + + t.Run("Derived pub key does not match with the provided", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "credentials", + "--seed=847d135b3aecac8ae77c3fdfd46dc5849ad3b5bacd30a1b9082b6ff53c77357e923b12fcdc3d02728fd35c3685de1fe1e9c052c48f0d83566b1b2287cf0e54c3", + "--index=1", + "--accumulate=true", + "--validator-indices=273230,273407", + "--validator-public-keys=0xb2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7a,0xa1a593775967bf88bb6c14ac109c12e52dc836fa139bd1ba6ca873d65fe91bb7a0fc79c7b2a7315482a81f31e6b1018a", + "--withdrawal-credentials=0x00d9cdf17e3a79317a4e5cd18580b1d10b1df360bbca5c5f8ac5b79b45c29d15,0x00202f88c6116d27f5de06eeda3b801d3a4eeab8eb09f879848445fffc8948a5", + "--to-execution-address=0x3e6935b8250Cf9A777862871649E5594bE08779e,0x3e6935b8250Cf9A777862871649E5594bE08779e", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.EqualValues(t, actualOutput, "") + require.Error(t, err) + require.EqualError(t, err, "derived validator public key: 0xb2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c, does not match with the provided one: 0xb2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7a") + }) + + t.Run("Derived withdrawal credentials does not match with the provided", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "credentials", + "--seed=847d135b3aecac8ae77c3fdfd46dc5849ad3b5bacd30a1b9082b6ff53c77357e923b12fcdc3d02728fd35c3685de1fe1e9c052c48f0d83566b1b2287cf0e54c3", + "--index=1", + "--accumulate=true", + "--validator-indices=273230,273407", + "--validator-public-keys=0xb2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c,0xa1a593775967bf88bb6c14ac109c12e52dc836fa139bd1ba6ca873d65fe91bb7a0fc79c7b2a7315482a81f31e6b1018a", + "--withdrawal-credentials=0x00d9cdf17e3a79317a4e5cd18580b1d10b1df360bbca5c5f8ac5b79b45c29d14,0x00202f88c6116d27f5de06eeda3b801d3a4eeab8eb09f879848445fffc8948a5", + "--to-execution-address=0x3e6935b8250Cf9A777862871649E5594bE08779e,0x3e6935b8250Cf9A777862871649E5594bE08779e", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.EqualValues(t, actualOutput, "") + require.Error(t, err) + require.EqualError(t, err, "derived withdrawal credentials: 0x00d9cdf17e3a79317a4e5cd18580b1d10b1df360bbca5c5f8ac5b79b45c29d15, does not match with the provided one: 0x00d9cdf17e3a79317a4e5cd18580b1d10b1df360bbca5c5f8ac5b79b45c29d14") + }) +} diff --git a/cli/cmd/wallet/cmd/account/deposit-data.go b/cli/cmd/wallet/cmd/account/deposit-data.go index 4bd18a4..bcfacf8 100644 --- a/cli/cmd/wallet/cmd/account/deposit-data.go +++ b/cli/cmd/wallet/cmd/account/deposit-data.go @@ -21,10 +21,10 @@ var depositDataCmd = &cobra.Command{ func init() { // Define flags for the command. - flag.AddIndexFlag(depositDataCmd) - flag.AddSeedFlag(depositDataCmd) - flag.AddPublicKeyFlag(depositDataCmd) rootcmd.AddNetworkFlag(depositDataCmd) + rootcmd.AddSeedFlag(depositDataCmd) + rootcmd.AddIndexFlag(depositDataCmd) + flag.AddPublicKeyFlag(depositDataCmd) Command.AddCommand(depositDataCmd) } diff --git a/cli/cmd/wallet/cmd/account/flag/create.go b/cli/cmd/wallet/cmd/account/flag/create.go index 57d5fb0..b09887b 100644 --- a/cli/cmd/wallet/cmd/account/flag/create.go +++ b/cli/cmd/wallet/cmd/account/flag/create.go @@ -1,7 +1,6 @@ package flag import ( - "fmt" "strconv" "strings" @@ -10,37 +9,9 @@ import ( "github.com/bloxapp/eth2-key-manager/cli/util/cliflag" ) -// ResponseType represents the network. -type ResponseType string - -// Available response types. -const ( - // StorageResponseType represents the storage response type. - StorageResponseType ResponseType = "storage" - - // ObjectResponseType represents the storage response type. - ObjectResponseType ResponseType = "object" -) - -// ResponseTypeFromString returns response type from the given string value -func ResponseTypeFromString(n string) ResponseType { - switch n { - case string(StorageResponseType): - return StorageResponseType - case string(ObjectResponseType): - return ObjectResponseType - default: - panic(fmt.Sprintf("undefined response type %s", n)) - } -} - // Flag names. const ( - indexFlag = "index" - seedFlag = "seed" privateKeyFlag = "private-key" - accumulateFlag = "accumulate" - responseTypeFlag = "response-type" highestKnownSource = "highest-source" highestKnownTarget = "highest-target" highestKnownProposal = "highest-proposal" @@ -61,51 +32,6 @@ func GetPrivateKeyFlagName() string { return privateKeyFlag } -// AddIndexFlag adds the index flag to the command -func AddIndexFlag(c *cobra.Command) { - cliflag.AddPersistentIntFlag(c, indexFlag, 0, "account index", true) -} - -// GetIndexFlagValue gets the index flag from the command -func GetIndexFlagValue(c *cobra.Command) (int, error) { - return c.Flags().GetInt(indexFlag) -} - -// AddAccumulateFlag adds the accumulate flag to the command -func AddAccumulateFlag(c *cobra.Command) { - cliflag.AddPersistentBoolFlag(c, accumulateFlag, false, "accumulate accounts", false) -} - -// GetAccumulateFlagValue gets the accumulate flag from the command -func GetAccumulateFlagValue(c *cobra.Command) (bool, error) { - return c.Flags().GetBool(accumulateFlag) -} - -// AddResponseTypeFlag adds the response-type flag to the command -func AddResponseTypeFlag(c *cobra.Command) { - cliflag.AddPersistentStringFlag(c, responseTypeFlag, string(StorageResponseType), "response type", false) -} - -// GetResponseTypeFlagValue gets the response-type flag from the command -func GetResponseTypeFlagValue(c *cobra.Command) (ResponseType, error) { - responseTypeValue, err := c.Flags().GetString(responseTypeFlag) - if err != nil { - return "", err - } - - return ResponseTypeFromString(responseTypeValue), nil -} - -// AddSeedFlag adds the seed flag to the command -func AddSeedFlag(c *cobra.Command) { - cliflag.AddPersistentStringFlag(c, seedFlag, "", "key-vault seed", false) -} - -// GetSeedFlagValue gets the seed flag from the command -func GetSeedFlagValue(c *cobra.Command) (string, error) { - return c.Flags().GetString(seedFlag) -} - // AddHighestSourceFlag adds the highest source flag to the command func AddHighestSourceFlag(c *cobra.Command) { cliflag.AddPersistentStringFlag(c, highestKnownSource, "", "Array of highest known sources for an array of validators", true) diff --git a/cli/cmd/wallet/cmd/account/flag/credentials.go b/cli/cmd/wallet/cmd/account/flag/credentials.go new file mode 100644 index 0000000..060c02e --- /dev/null +++ b/cli/cmd/wallet/cmd/account/flag/credentials.go @@ -0,0 +1,193 @@ +package flag + +import ( + "encoding/hex" + "strings" + + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + rootcmd "github.com/bloxapp/eth2-key-manager/cli/cmd" + "github.com/bloxapp/eth2-key-manager/cli/util/cliflag" + "github.com/bloxapp/eth2-key-manager/core" +) + +// Flag names. +const ( + validatorIndices = "validator-indices" + validatorPublicKeys = "validator-public-keys" + withdrawalCredentials = "withdrawal-credentials" + toExecutionAddress = "to-execution-address" +) + +// AddValidatorIndicesFlag adds the validator indices flag to the command +func AddValidatorIndicesFlag(c *cobra.Command) { + cliflag.AddPersistentStringFlag(c, validatorIndices, "", "comma separate string of validator indices", true) +} + +// GetValidatorIndicesFlagValue gets the validator indices flag from the command +func GetValidatorIndicesFlagValue(c *cobra.Command) ([]uint64, error) { + str, err := c.Flags().GetString(validatorIndices) + if err != nil { + return nil, err + } + return stringSliceToUint64Slice(str) +} + +// AddValidatorPublicKeysFlag adds the validator public keys flag to the command +func AddValidatorPublicKeysFlag(c *cobra.Command) { + cliflag.AddPersistentStringFlag(c, validatorPublicKeys, "", "comma separate string of validator public keys", true) +} + +// GetValidatorPublicKeysFlagValue gets the validator public keys flag from the command +func GetValidatorPublicKeysFlagValue(c *cobra.Command) ([]phase0.BLSPubKey, error) { + validatorPublicKeyValues, err := c.Flags().GetString(validatorPublicKeys) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the validator public keys flag value") + } + + validatorPubKeys := strings.Split(validatorPublicKeyValues, ",") + validatorBLSPubKeys := make([]phase0.BLSPubKey, 0) + for _, pk := range validatorPubKeys { + validatorPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(pk, "0x")) + if err != nil { + return nil, errors.Wrap(err, "invalid validator public key supplied") + } + if len(validatorPubKeyBytes) != phase0.PublicKeyLength { + return nil, errors.New("invalid length for validator public key") + } + + var validatorBLSPubKey phase0.BLSPubKey + copy(validatorBLSPubKey[:], validatorPubKeyBytes) + validatorBLSPubKeys = append(validatorBLSPubKeys, validatorBLSPubKey) + } + return validatorBLSPubKeys, nil +} + +// AddWithdrawalCredentialsFlag adds withdrawal credentials to the command +func AddWithdrawalCredentialsFlag(c *cobra.Command) { + cliflag.AddPersistentStringFlag(c, withdrawalCredentials, "", "comma separate string of withdrawal credentials", true) +} + +// GetWithdrawalCredentialsFlagValue returns the value of withdrawal credentials +func GetWithdrawalCredentialsFlagValue(c *cobra.Command) ([][]byte, error) { + withdrawalCredentialsValues, err := c.Flags().GetString(withdrawalCredentials) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the withdrawal credentials flag value") + } + + withdrawalCreds := strings.Split(withdrawalCredentialsValues, ",") + withdrawalCredentialsList := make([][]byte, 0) + for _, cred := range withdrawalCreds { + withdrawalCredsBytes, err := hex.DecodeString(strings.TrimPrefix(cred, "0x")) + if err != nil { + return nil, errors.Wrap(err, "invalid withdrawal credentials supplied") + } + if len(withdrawalCredsBytes) != 32 { + return nil, errors.New("invalid length for withdrawal credentials") + } + + if withdrawalCredsBytes[0] != byte(0) { + return nil, errors.New("non-BLS withdrawal credentials supplied") + } + withdrawalCredentialsList = append(withdrawalCredentialsList, withdrawalCredsBytes) + } + return withdrawalCredentialsList, nil +} + +// AddToExecutionAddressFlag adds the validator execution withdrawal address flag to the command +func AddToExecutionAddressFlag(c *cobra.Command) { + cliflag.AddPersistentStringFlag(c, toExecutionAddress, "", "comma separate string of to execution addresses", true) +} + +// GetToExecutionAddressFlagValue gets the validator withdrawal address flag from the command +func GetToExecutionAddressFlagValue(c *cobra.Command) ([]bellatrix.ExecutionAddress, error) { + toExecutionAddressValues, err := c.Flags().GetString(toExecutionAddress) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the withdrawal address flag value") + } + + toExecutionAddresses := strings.Split(toExecutionAddressValues, ",") + toExecutionAddressList := make([]bellatrix.ExecutionAddress, 0) + for _, wa := range toExecutionAddresses { + toExecutionAddressBytes, err := hex.DecodeString(strings.TrimPrefix(wa, "0x")) + if err != nil { + return nil, errors.Wrap(err, "invalid to execution address supplied") + } + if len(toExecutionAddressBytes) != bellatrix.ExecutionAddressLength { + return nil, errors.New("invalid length for to execution address") + } + + var toExecutionAdd bellatrix.ExecutionAddress + copy(toExecutionAdd[:], toExecutionAddressBytes) + toExecutionAddressList = append(toExecutionAddressList, toExecutionAdd) + } + return toExecutionAddressList, nil +} + +// GetValidatorInfoFlagValue gets the validator info flag from the command +func GetValidatorInfoFlagValue(c *cobra.Command) ([]*core.ValidatorInfo, error) { + validatorIndices, err := GetValidatorIndicesFlagValue(c) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse validator indices") + } + + validatorPubKeys, err := GetValidatorPublicKeysFlagValue(c) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse validator public keys") + } + + validatorWithdrawalCredentials, err := GetWithdrawalCredentialsFlagValue(c) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse withdrawal credentials") + } + + toExecutionAddresses, err := GetToExecutionAddressFlagValue(c) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse to execution addresses") + } + + if len(validatorIndices) != len(validatorPubKeys) || len(validatorPubKeys) != len(validatorWithdrawalCredentials) || len(validatorWithdrawalCredentials) != len(toExecutionAddresses) { + return nil, errors.New("validator indices, public keys, withdrawal credentials and to execution addresses must be of equal length") + } + + indexFlagValue, err := rootcmd.GetIndexFlagValue(c) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the index flag value") + } + + accumulate, err := rootcmd.GetAccumulateFlagValue(c) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse accumulate flag") + } + + if accumulate && indexFlagValue+1 != len(validatorIndices) { + return nil, errors.New("index flag value must be one less than the number of validator indices") + } + + if !accumulate && len(validatorIndices) > 1 { + return nil, errors.New("only one validator can be specified if accumulate is false") + } + + validatorInfoList := make([]*core.ValidatorInfo, len(validatorIndices)) + for i := 0; i < len(validatorIndices); i++ { + validatorInfoList[i] = &core.ValidatorInfo{ + Index: phase0.ValidatorIndex(validatorIndices[i]), + Pubkey: validatorPubKeys[i], + WithdrawalCredentials: validatorWithdrawalCredentials[i], + ToExecutionAddress: toExecutionAddresses[i], + } + } + + if accumulate && indexFlagValue+1 != len(validatorInfoList) { + return nil, errors.New("index flag value must be one less than the number of validator info") + } + + if !accumulate && len(validatorInfoList) > 1 { + return nil, errors.New("only one validator info can be structured if accumulate is false") + } + + return validatorInfoList, nil +} diff --git a/cli/cmd/wallet/cmd/account/flag/voluntary_exit.go b/cli/cmd/wallet/cmd/account/flag/voluntary_exit.go new file mode 100644 index 0000000..fcc089f --- /dev/null +++ b/cli/cmd/wallet/cmd/account/flag/voluntary_exit.go @@ -0,0 +1,116 @@ +package flag + +import ( + "encoding/hex" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/bloxapp/eth2-key-manager/cli/util/cliflag" + "github.com/bloxapp/eth2-key-manager/core" +) + +// Flag names. +const ( + validatorPublicKey = "validator-public-key" + validatorIndex = "validator-index" + epochFlag = "epoch" + currentForkVersionFlag = "current-fork-version" +) + +// AddEpochFlag adds the epoch flag to the command +func AddEpochFlag(c *cobra.Command) { + cliflag.AddPersistentIntFlag(c, epochFlag, 0, "epoch", true) +} + +// GetEpochFlagValue gets the epoch flag from the command +func GetEpochFlagValue(c *cobra.Command) (int, error) { + return c.Flags().GetInt(epochFlag) +} + +// AddCurrentForkVersionFlag adds the current fork version flag to the command +func AddCurrentForkVersionFlag(c *cobra.Command) { + cliflag.AddPersistentStringFlag(c, currentForkVersionFlag, "", "current fork version (ForkVersionLength: 4 bytes)", true) +} + +// GetCurrentForkVersionFlagValue gets the current fork version flag from the command +func GetCurrentForkVersionFlagValue(c *cobra.Command) (phase0.Version, error) { + var currentForkVersion phase0.Version + currentForkVersionFlagValue, err := c.Flags().GetString(currentForkVersionFlag) + if err != nil { + return currentForkVersion, err + } + version, err := hex.DecodeString(strings.TrimPrefix(currentForkVersionFlagValue, "0x")) + if err != nil { + return currentForkVersion, errors.Wrap(err, "invalid current fork version supplied") + } + if len(version) != phase0.ForkVersionLength { + return currentForkVersion, errors.New("invalid length for current fork version") + } + + copy(currentForkVersion[:], version) + + return currentForkVersion, nil +} + +// AddValidatorPublicKeyFlag adds the validator public key flag to the command +func AddValidatorPublicKeyFlag(c *cobra.Command) { + cliflag.AddPersistentStringFlag(c, validatorPublicKey, "", "validator public key", true) +} + +// GetValidatorPublicKeyFlagValue gets the validator public key flag from the command +func GetValidatorPublicKeyFlagValue(c *cobra.Command) (phase0.BLSPubKey, error) { + var validatorBLSPubKey phase0.BLSPubKey + validatorPublicKeyValue, err := c.Flags().GetString(validatorPublicKey) + if err != nil { + return validatorBLSPubKey, errors.Wrap(err, "failed to retrieve the validator public key flag value") + } + + validatorPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(validatorPublicKeyValue, "0x")) + if err != nil { + return validatorBLSPubKey, errors.Wrap(err, "invalid validator public key supplied") + } + if len(validatorPubKeyBytes) != phase0.PublicKeyLength { + return validatorBLSPubKey, errors.New("invalid length for validator public key") + } + copy(validatorBLSPubKey[:], validatorPubKeyBytes) + + return validatorBLSPubKey, nil +} + +// AddValidatorIndexFlag adds the validator index flag to the command +func AddValidatorIndexFlag(c *cobra.Command) { + cliflag.AddPersistentIntFlag(c, validatorIndex, 0, "validator index", true) +} + +// GetValidatorIndexFlagValue gets the validator index flag from the command +func GetValidatorIndexFlagValue(c *cobra.Command) (phase0.ValidatorIndex, error) { + str, err := c.Flags().GetInt(validatorIndex) + if err != nil { + return 0, err + } + + return phase0.ValidatorIndex(str), nil +} + +// GetVoluntaryExitInfoFlagValue gets the voluntary exit info flag from the command +func GetVoluntaryExitInfoFlagValue(c *cobra.Command) (*core.ValidatorInfo, error) { + validatorIndex, err := GetValidatorIndexFlagValue(c) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse validator index") + } + + validatorPubKey, err := GetValidatorPublicKeyFlagValue(c) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse validator public key") + } + + validatorInfo := &core.ValidatorInfo{ + Index: validatorIndex, + Pubkey: validatorPubKey, + } + + return validatorInfo, nil +} diff --git a/cli/cmd/wallet/cmd/account/handler/handler_create.go b/cli/cmd/wallet/cmd/account/handler/handler_create.go index c875440..3c7ea7e 100644 --- a/cli/cmd/wallet/cmd/account/handler/handler_create.go +++ b/cli/cmd/wallet/cmd/account/handler/handler_create.go @@ -22,7 +22,7 @@ type CreateAccountFlagValues struct { seedBytes []byte privateKeys [][]byte accumulate bool - responseType flag.ResponseType + responseType rootcmd.ResponseType highestSources []uint64 highestTargets []uint64 highestProposals []uint64 @@ -80,7 +80,7 @@ func (h *Account) BuildAccounts(accountFlags *CreateAccountFlagValues) error { } } - if accountFlags.responseType == flag.StorageResponseType { + if accountFlags.responseType == rootcmd.StorageResponseType { // marshal storage bytes, err := store.MarshalJSON() if err != nil { @@ -142,7 +142,7 @@ func CollectAccountFlags(cmd *cobra.Command) (*CreateAccountFlagValues, error) { } } else { // Get seed flag value. - seedFlagValue, err := flag.GetSeedFlagValue(cmd) + seedFlagValue, err := rootcmd.GetSeedFlagValue(cmd) if err != nil { return nil, errors.Wrap(err, "failed to retrieve the seed flag value") } @@ -156,7 +156,7 @@ func CollectAccountFlags(cmd *cobra.Command) (*CreateAccountFlagValues, error) { accountFlagValues.seedBytes = seedBytes // Get accumulate flag value. - accumulateFlagValue, err := flag.GetAccumulateFlagValue(cmd) + accumulateFlagValue, err := rootcmd.GetAccumulateFlagValue(cmd) if err != nil { return nil, errors.Wrap(err, "failed to retrieve the accumulate flag value") } @@ -164,14 +164,14 @@ func CollectAccountFlags(cmd *cobra.Command) (*CreateAccountFlagValues, error) { } // Get index flag value. - indexFlagValue, err := flag.GetIndexFlagValue(cmd) + indexFlagValue, err := rootcmd.GetIndexFlagValue(cmd) if err != nil { return nil, errors.Wrap(err, "failed to retrieve the index flag value") } accountFlagValues.index = indexFlagValue // Get response-type flag value. - responseType, err := flag.GetResponseTypeFlagValue(cmd) + responseType, err := rootcmd.GetResponseTypeFlagValue(cmd) if err != nil { return nil, errors.Wrap(err, "failed to retrieve the response type value") } diff --git a/cli/cmd/wallet/cmd/account/handler/handler_credentials.go b/cli/cmd/wallet/cmd/account/handler/handler_credentials.go new file mode 100644 index 0000000..9ed8717 --- /dev/null +++ b/cli/cmd/wallet/cmd/account/handler/handler_credentials.go @@ -0,0 +1,191 @@ +package handler + +import ( + "bytes" + "encoding/hex" + + "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "github.com/spf13/cobra" + types "github.com/wealdtech/go-eth2-types/v2" + util "github.com/wealdtech/go-eth2-util" + + eth2keymanager "github.com/bloxapp/eth2-key-manager" + rootcmd "github.com/bloxapp/eth2-key-manager/cli/cmd" + "github.com/bloxapp/eth2-key-manager/cli/cmd/wallet/cmd/account/flag" + "github.com/bloxapp/eth2-key-manager/core" + "github.com/bloxapp/eth2-key-manager/signer" + "github.com/bloxapp/eth2-key-manager/stores/inmemory" +) + +// CredentialsFlagValues keeps all collected values for seed +type CredentialsFlagValues struct { + index int + seedBytes []byte + accumulate bool + validators []*core.ValidatorInfo + network core.Network +} + +var domainBlsToExecutionChange = types.DomainType{0x0a, 0x00, 0x00, 0x00} + +// Credentials creates a new wallet account(s) and prints the storage. +func (h *Account) Credentials(cmd *cobra.Command, args []string) error { + err := core.InitBLS() + if err != nil { + return errors.Wrap(err, "failed to init BLS") + } + + credentialsFlags, err := CollectCredentialsFlags(cmd) + if err != nil { + return errors.Wrap(err, "failed to collect credentials flags") + } + + // Initialize store + store := inmemory.NewInMemStore(credentialsFlags.network) + options := ð2keymanager.KeyVaultOptions{} + options.SetStorage(store) + + // Create new key vault + _, err = eth2keymanager.NewKeyVault(options) + if err != nil { + return errors.Wrap(err, "failed to create key vault") + } + + wallet, err := store.OpenWallet() + if err != nil { + return errors.Wrap(err, "failed to open wallet") + } + + // set the context for wallet to use withdrawal key as primary key + wallet.SetContext(&core.WalletContext{ + Storage: store, + WithdrawalMode: true, + }) + + // Compute domain + genesisValidatorsRoot := store.Network().GenesisValidatorsRoot() + genesisForkVersion := store.Network().GenesisForkVersion() + domainBytes, err := types.ComputeDomain(domainBlsToExecutionChange, genesisForkVersion[:], genesisValidatorsRoot[:]) + if err != nil { + return errors.Wrap(err, "failed to calculate domain") + } + var domain phase0.Domain + copy(domain[:], domainBytes) + + simpleSigner := signer.NewSimpleSigner(wallet, nil, store.Network()) + signedBLSToExecutionChanges := make([]*capella.SignedBLSToExecutionChange, 0) + + for i := 0; i <= credentialsFlags.index; i++ { + var index int + if credentialsFlags.accumulate { + index = i + } else { + index = credentialsFlags.index + } + validator := credentialsFlags.validators[i] + + // Its actually withdrawal account, since the wallet is in withdrawal mode + acc, err := wallet.CreateValidatorAccount(credentialsFlags.seedBytes, &index) + if err != nil { + return errors.Wrap(err, "failed to create withdrawal account") + } + + // Since the wallet is in withdrawal mode the validator account is the withdrawal account + derivedWithdrawalPubKey := acc.ValidatorPublicKey() + derivedValidatorPubKey := acc.WithdrawalPublicKey() + + // validation that the derived validation public key is the same as the one in the validator info + if !bytes.Equal(derivedValidatorPubKey, validator.Pubkey[:]) { + derivedPubKey := "0x" + hex.EncodeToString(derivedValidatorPubKey) + providedPubKey := validator.Pubkey.String() + return errors.Errorf("derived validator public key: %s, does not match with the provided one: %s", derivedPubKey, providedPubKey) + } + + // validation that the derived withdrawal credentials are the same as the one in the validator info + withdrawalCredentials := util.SHA256(derivedWithdrawalPubKey) + withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX + if !bytes.Equal(withdrawalCredentials, validator.WithdrawalCredentials) { + derivedCreds := "0x" + hex.EncodeToString(withdrawalCredentials) + providedCreds := "0x" + hex.EncodeToString(validator.WithdrawalCredentials) + return errors.Errorf("derived withdrawal credentials: %s, does not match with the provided one: %s", derivedCreds, providedCreds) + } + + blsToExecutionChange := &capella.BLSToExecutionChange{ + ValidatorIndex: validator.Index, + ToExecutionAddress: validator.ToExecutionAddress, + } + copy(blsToExecutionChange.FromBLSPubkey[:], derivedWithdrawalPubKey) + + signature, _, err := simpleSigner.SignBLSToExecutionChange(blsToExecutionChange, domain, derivedWithdrawalPubKey) + if err != nil { + return errors.Wrap(err, "failed to sign voluntary exit") + } + + signedBLSToExecutionChange := &capella.SignedBLSToExecutionChange{ + Message: blsToExecutionChange, + } + copy(signedBLSToExecutionChange.Signature[:], signature) + signedBLSToExecutionChanges = append(signedBLSToExecutionChanges, signedBLSToExecutionChange) + + if !credentialsFlags.accumulate { + break + } + } + + err = h.printer.JSON(signedBLSToExecutionChanges) + if err != nil { + return errors.Wrap(err, "failed to print signedBLSToExecutionChanges JSON") + } + + return nil +} + +// CollectCredentialsFlags returns collected flags for seed +func CollectCredentialsFlags(cmd *cobra.Command) (*CredentialsFlagValues, error) { + credentialsFlagValues := CredentialsFlagValues{} + + // Get seed flag value. + seedFlagValue, err := rootcmd.GetSeedFlagValue(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the seed flag value") + } + + // Get seed bytes + seedBytes, err := hex.DecodeString(seedFlagValue) + if err != nil { + return nil, errors.Wrap(err, "failed to HEX decode seed") + } + credentialsFlagValues.seedBytes = seedBytes + + // Get accumulate flag value. + accumulateFlagValue, err := rootcmd.GetAccumulateFlagValue(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the accumulate flag value") + } + credentialsFlagValues.accumulate = accumulateFlagValue + + // Get index flag value. + indexFlagValue, err := rootcmd.GetIndexFlagValue(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the index flag value") + } + credentialsFlagValues.index = indexFlagValue + + // Get network flag value. + network, err := rootcmd.GetNetworkFlagValue(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the network flag value") + } + credentialsFlagValues.network = network + + // Get validators info flag value. + validators, err := flag.GetValidatorInfoFlagValue(cmd) + if err != nil { + return nil, err + } + credentialsFlagValues.validators = validators + + return &credentialsFlagValues, nil +} diff --git a/cli/cmd/wallet/cmd/account/handler/handler_deposit-data.go b/cli/cmd/wallet/cmd/account/handler/handler_deposit-data.go index 9f7c68f..6ef3e9b 100644 --- a/cli/cmd/wallet/cmd/account/handler/handler_deposit-data.go +++ b/cli/cmd/wallet/cmd/account/handler/handler_deposit-data.go @@ -22,13 +22,13 @@ func (h *Account) DepositData(cmd *cobra.Command, _ []string) error { } // Get index flag. - indexFlagValue, err := flag.GetIndexFlagValue(cmd) + indexFlagValue, err := rootcmd.GetIndexFlagValue(cmd) if err != nil { return errors.Wrap(err, "failed to retrieve the index flag value") } // Get seed flag. - seedFlagValue, err := flag.GetSeedFlagValue(cmd) + seedFlagValue, err := rootcmd.GetSeedFlagValue(cmd) if err != nil { return errors.Wrap(err, "failed to retrieve the seed flag value") } diff --git a/cli/cmd/wallet/cmd/account/handler/handler_voluntary_exit.go b/cli/cmd/wallet/cmd/account/handler/handler_voluntary_exit.go new file mode 100644 index 0000000..0ecc5db --- /dev/null +++ b/cli/cmd/wallet/cmd/account/handler/handler_voluntary_exit.go @@ -0,0 +1,202 @@ +package handler + +import ( + "bytes" + "encoding/hex" + "encoding/json" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "github.com/spf13/cobra" + types "github.com/wealdtech/go-eth2-types/v2" + + eth2keymanager "github.com/bloxapp/eth2-key-manager" + rootcmd "github.com/bloxapp/eth2-key-manager/cli/cmd" + "github.com/bloxapp/eth2-key-manager/cli/cmd/wallet/cmd/account/flag" + "github.com/bloxapp/eth2-key-manager/core" + "github.com/bloxapp/eth2-key-manager/signer" + "github.com/bloxapp/eth2-key-manager/stores/inmemory" +) + +// VoluntaryExitFlagValues keeps all collected values for seed +type VoluntaryExitFlagValues struct { + index int + seedBytes []byte + currentForkVersion phase0.Version + epoch int + validator *core.ValidatorInfo + network core.Network + responseType rootcmd.ResponseType +} + +// SignRequestEncoded is the encoded sign request +type SignRequestEncoded struct { + PublicKey []byte `json:"public_key,omitempty"` + SignatureDomain [32]byte `json:"signature_domain,omitempty"` + Data []byte + ObjectType string +} + +// VoluntaryExit creates a new wallet account(s) and prints the storage. +func (h *Account) VoluntaryExit(cmd *cobra.Command, args []string) error { + err := core.InitBLS() + if err != nil { + return errors.Wrap(err, "failed to init BLS") + } + + voluntaryExitFlags, err := CollectVoluntaryExitFlags(cmd) + if err != nil { + return errors.Wrap(err, "failed to collect voluntary exit flags") + } + + // Initialize store + store := inmemory.NewInMemStore(voluntaryExitFlags.network) + options := ð2keymanager.KeyVaultOptions{} + options.SetStorage(store) + + // Create new key vault + _, err = eth2keymanager.NewKeyVault(options) + if err != nil { + return errors.Wrap(err, "failed to create key vault") + } + + wallet, err := store.OpenWallet() + if err != nil { + return errors.Wrap(err, "failed to open wallet") + } + + // Compute domain + genesisValidatorsRoot := store.Network().GenesisValidatorsRoot() + domainBytes, err := types.ComputeDomain(types.DomainVoluntaryExit, voluntaryExitFlags.currentForkVersion[:], genesisValidatorsRoot[:]) + if err != nil { + return errors.Wrap(err, "failed to calculate domain") + } + var domain phase0.Domain + copy(domain[:], domainBytes) + + voluntaryExit := &phase0.VoluntaryExit{ + Epoch: phase0.Epoch(voluntaryExitFlags.epoch), + ValidatorIndex: voluntaryExitFlags.validator.Index, + } + + if voluntaryExitFlags.responseType == rootcmd.ObjectResponseType { + simpleSigner := signer.NewSimpleSigner(wallet, nil, store.Network()) + acc, err := wallet.CreateValidatorAccount(voluntaryExitFlags.seedBytes, &voluntaryExitFlags.index) + if err != nil { + return errors.Wrap(err, "failed to create validator account") + } + + // validation that the derived validation public key is the same as the one in the validator info + if !bytes.Equal(acc.ValidatorPublicKey(), voluntaryExitFlags.validator.Pubkey[:]) { + derivedPubKey := "0x" + hex.EncodeToString(acc.ValidatorPublicKey()) + providedPubKey := voluntaryExitFlags.validator.Pubkey.String() + return errors.Errorf("derived validator public key: %s, does not match with the provided one: %s", derivedPubKey, providedPubKey) + } + + signature, _, err := simpleSigner.SignVoluntaryExit(voluntaryExit, domain, acc.ValidatorPublicKey()) + if err != nil { + return errors.Wrap(err, "failed to sign voluntary exit") + } + + signedVoluntaryExit := &phase0.SignedVoluntaryExit{ + Message: voluntaryExit, + } + copy(signedVoluntaryExit.Signature[:], signature) + + err = h.printer.JSON(signedVoluntaryExit) + if err != nil { + return errors.Wrap(err, "failed to print signed voluntary exit JSON") + } + return nil + } + + // Sign request + marshalSSZ, err := voluntaryExit.MarshalSSZ() + if err != nil { + return errors.Wrap(err, "failed to marshal voluntary exit") + } + + signRequest := &SignRequestEncoded{ + PublicKey: voluntaryExitFlags.validator.Pubkey[:], + SignatureDomain: domain, + Data: marshalSSZ, + ObjectType: "*models.SignRequestVoluntaryExit", + } + + byts, err := json.Marshal(signRequest) + if err != nil { + return errors.Wrap(err, "failed to marshal sign request") + } + + h.printer.Text(hex.EncodeToString(byts)) + + return nil +} + +// CollectVoluntaryExitFlags returns collected flags for seed +func CollectVoluntaryExitFlags(cmd *cobra.Command) (*VoluntaryExitFlagValues, error) { + voluntaryExitFlagValues := VoluntaryExitFlagValues{} + + // Get network flag value. + network, err := rootcmd.GetNetworkFlagValue(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the network flag value") + } + voluntaryExitFlagValues.network = network + + // Get response-type flag value. + responseType, err := rootcmd.GetResponseTypeFlagValue(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the response type value") + } + voluntaryExitFlagValues.responseType = responseType + + if responseType == rootcmd.ObjectResponseType { + // Get seed flag value. + seedFlagValue, err := rootcmd.GetSeedFlagValue(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the seed flag value") + } + + if seedFlagValue == "" { + return nil, errors.New("seed flag is required for object response type") + } + + // Get seed bytes + seedBytes, err := hex.DecodeString(seedFlagValue) + if err != nil { + return nil, errors.Wrap(err, "failed to HEX decode seed") + } + voluntaryExitFlagValues.seedBytes = seedBytes + } + + // Get index flag value. + indexFlagValue, err := rootcmd.GetIndexFlagValue(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the index flag value") + } + voluntaryExitFlagValues.index = indexFlagValue + + // Get current fork version flag value. + currentForkVersionFlagValue, err := flag.GetCurrentForkVersionFlagValue(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the current fork version flag value") + } + voluntaryExitFlagValues.currentForkVersion = currentForkVersionFlagValue + + // Get epoch flag value. + epochFlagValue, err := flag.GetEpochFlagValue(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the index flag value") + } + voluntaryExitFlagValues.epoch = epochFlagValue + + // Get validator info flag value. + validator, err := flag.GetVoluntaryExitInfoFlagValue(cmd) + if err != nil { + return nil, err + } + voluntaryExitFlagValues.validator = validator + + return &voluntaryExitFlagValues, nil +} diff --git a/cli/cmd/wallet/cmd/account/voluntary_exit.go b/cli/cmd/wallet/cmd/account/voluntary_exit.go new file mode 100644 index 0000000..b46af88 --- /dev/null +++ b/cli/cmd/wallet/cmd/account/voluntary_exit.go @@ -0,0 +1,34 @@ +package account + +import ( + "github.com/spf13/cobra" + + rootcmd "github.com/bloxapp/eth2-key-manager/cli/cmd" + "github.com/bloxapp/eth2-key-manager/cli/cmd/wallet/cmd/account/flag" + "github.com/bloxapp/eth2-key-manager/cli/cmd/wallet/cmd/account/handler" +) + +// voluntaryExitCmd represents the voluntary exit account command. +var voluntaryExitCmd = &cobra.Command{ + Use: "voluntary-exit", + Short: "Sign voluntary exit message", + Long: `This command signing voluntary exit message using seed or preparing request for signing using key-vault`, + RunE: func(cmd *cobra.Command, args []string) error { + handler := handler.New(rootcmd.ResultPrinter) + return handler.VoluntaryExit(cmd, args) + }, +} + +func init() { + // Define flags for the command. + rootcmd.AddNetworkFlag(voluntaryExitCmd) + rootcmd.AddSeedFlag(voluntaryExitCmd) + rootcmd.AddIndexFlag(voluntaryExitCmd) + rootcmd.AddResponseTypeFlag(voluntaryExitCmd) + flag.AddCurrentForkVersionFlag(voluntaryExitCmd) + flag.AddValidatorPublicKeyFlag(voluntaryExitCmd) + flag.AddValidatorIndexFlag(voluntaryExitCmd) + flag.AddEpochFlag(voluntaryExitCmd) + + Command.AddCommand(voluntaryExitCmd) +} diff --git a/cli/cmd/wallet/cmd/account/voluntary_exit_test.go b/cli/cmd/wallet/cmd/account/voluntary_exit_test.go new file mode 100644 index 0000000..a9f6df8 --- /dev/null +++ b/cli/cmd/wallet/cmd/account/voluntary_exit_test.go @@ -0,0 +1,120 @@ +package account_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/bloxapp/eth2-key-manager/cli/cmd" + "github.com/bloxapp/eth2-key-manager/cli/util/printer" +) + +func TestAccountVoluntaryExit(t *testing.T) { + t.Run("Successfully sign voluntary exit", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "voluntary-exit", + "--response-type=object", + "--seed=847d135b3aecac8ae77c3fdfd46dc5849ad3b5bacd30a1b9082b6ff53c77357e923b12fcdc3d02728fd35c3685de1fe1e9c052c48f0d83566b1b2287cf0e54c3", + "--current-fork-version=0x02001020", + "--index=0", + "--validator-index=273230", + "--validator-public-key=b2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c", + "--epoch=183797", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.NotNil(t, actualOutput) + require.NoError(t, err) + }) + + t.Run("Successfully prepare sign voluntary exit request for key-vault", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "voluntary-exit", + "--current-fork-version=0x02001020", + "--index=0", + "--validator-index=273230", + "--validator-public-key=b2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c", + "--epoch=183797", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.NotNil(t, actualOutput) + require.NoError(t, err) + }) + + t.Run("Invalid current fork version length", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "voluntary-exit", + "--current-fork-version=0x020000", + "--index=1", + "--validator-index=1", + "--validator-public-key=0xb2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c", + "--epoch=1", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.EqualValues(t, actualOutput, "") + require.Error(t, err) + require.EqualError(t, err, "failed to collect voluntary exit flags: failed to retrieve the current fork version flag value: invalid length for current fork version") + }) + + t.Run("Invalid validator public key", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "voluntary-exit", + "--current-fork-version=0x02001020", + "--index=1", + "--validator-index=1", + "--validator-public-key=0x2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c", + "--epoch=1", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.EqualValues(t, actualOutput, "") + require.Error(t, err) + require.EqualError(t, err, "failed to collect voluntary exit flags: failed to parse validator public key: invalid validator public key supplied: encoding/hex: odd length hex string") + }) + + t.Run("Seed flag is required for object response type", func(t *testing.T) { + var output bytes.Buffer + cmd.ResultPrinter = printer.New(&output) + cmd.RootCmd.SetArgs([]string{ + "wallet", + "account", + "voluntary-exit", + "--response-type=object", + "--seed=", + "--current-fork-version=0x02001020", + "--index=0", + "--validator-index=273230", + "--validator-public-key=b2dc1daa8c9cd104d4503028639e41a41e4f06ee5cc90ebfaeab3c41f43a148ce9afa4ebd1b8be3f54e4d6c15e870c7c", + "--epoch=183797", + "--network=prater", + }) + err := cmd.RootCmd.Execute() + actualOutput := output.String() + require.EqualValues(t, actualOutput, "") + require.Error(t, err) + require.EqualError(t, err, "failed to collect voluntary exit flags: seed flag is required for object response type") + }) +} diff --git a/cli/cmd/wallet/cmd/publickey/flag/generate.go b/cli/cmd/wallet/cmd/publickey/flag/generate.go deleted file mode 100644 index cd1e790..0000000 --- a/cli/cmd/wallet/cmd/publickey/flag/generate.go +++ /dev/null @@ -1,33 +0,0 @@ -package flag - -import ( - "github.com/spf13/cobra" - - "github.com/bloxapp/eth2-key-manager/cli/util/cliflag" -) - -// Flag names. -const ( - indexFlag = "index" - seedFlag = "seed" -) - -// AddIndexFlag adds the index flag to the command -func AddIndexFlag(c *cobra.Command) { - cliflag.AddPersistentIntFlag(c, indexFlag, 0, "public key index", true) -} - -// GetIndexFlagValue gets the index flag from the command -func GetIndexFlagValue(c *cobra.Command) (int, error) { - return c.Flags().GetInt(indexFlag) -} - -// AddSeedFlag adds the seed flag to the command -func AddSeedFlag(c *cobra.Command) { - cliflag.AddPersistentStringFlag(c, seedFlag, "", "key-vault seed", true) -} - -// GetSeedFlagValue gets the seed flag from the command -func GetSeedFlagValue(c *cobra.Command) (string, error) { - return c.Flags().GetString(seedFlag) -} diff --git a/cli/cmd/wallet/cmd/publickey/generate.go b/cli/cmd/wallet/cmd/publickey/generate.go index 648ab1b..f2ee8c3 100644 --- a/cli/cmd/wallet/cmd/publickey/generate.go +++ b/cli/cmd/wallet/cmd/publickey/generate.go @@ -4,7 +4,6 @@ import ( "github.com/spf13/cobra" rootcmd "github.com/bloxapp/eth2-key-manager/cli/cmd" - "github.com/bloxapp/eth2-key-manager/cli/cmd/wallet/cmd/publickey/flag" "github.com/bloxapp/eth2-key-manager/cli/cmd/wallet/cmd/publickey/handler" ) @@ -26,9 +25,9 @@ var generateCmd = &cobra.Command{ func init() { // Define flags for the command. - flag.AddIndexFlag(generateCmd) - flag.AddSeedFlag(generateCmd) rootcmd.AddNetworkFlag(generateCmd) + rootcmd.AddSeedFlag(generateCmd) + rootcmd.AddIndexFlag(generateCmd) Command.AddCommand(generateCmd) } diff --git a/cli/cmd/wallet/cmd/publickey/handler/handler_generate.go b/cli/cmd/wallet/cmd/publickey/handler/handler_generate.go index e13e2f2..1dae6ba 100644 --- a/cli/cmd/wallet/cmd/publickey/handler/handler_generate.go +++ b/cli/cmd/wallet/cmd/publickey/handler/handler_generate.go @@ -3,13 +3,13 @@ package handler import ( "encoding/hex" + rootcmd "github.com/bloxapp/eth2-key-manager/cli/cmd" "github.com/bloxapp/eth2-key-manager/core" "github.com/pkg/errors" "github.com/spf13/cobra" eth2keymanager "github.com/bloxapp/eth2-key-manager" - "github.com/bloxapp/eth2-key-manager/cli/cmd/wallet/cmd/publickey/flag" "github.com/bloxapp/eth2-key-manager/stores/inmemory" ) @@ -21,7 +21,7 @@ func (h *PublicKey) Generate(cmd *cobra.Command, _ []string) error { } // Get index flag. - indexFlagValue, err := flag.GetIndexFlagValue(cmd) + indexFlagValue, err := rootcmd.GetIndexFlagValue(cmd) if err != nil { return errors.Wrap(err, "failed to retrieve the index flag value") } @@ -31,7 +31,7 @@ func (h *PublicKey) Generate(cmd *cobra.Command, _ []string) error { } // Get seed flag. - seedFlagValue, err := flag.GetSeedFlagValue(cmd) + seedFlagValue, err := rootcmd.GetSeedFlagValue(cmd) if err != nil { return errors.Wrap(err, "failed to retrieve the seed flag value") } diff --git a/core/networks.go b/core/networks.go index c9b1220..cd6c79c 100644 --- a/core/networks.go +++ b/core/networks.go @@ -1,6 +1,7 @@ package core import ( + "encoding/hex" "time" "github.com/attestantio/go-eth2-client/spec/phase0" @@ -24,8 +25,8 @@ func NetworkFromString(n string) Network { } } -// ForkVersion returns the fork version of the network. -func (n Network) ForkVersion() phase0.Version { +// GenesisForkVersion returns the genesis fork version of the network. +func (n Network) GenesisForkVersion() phase0.Version { switch n { case PyrmontNetwork: return phase0.Version{0, 0, 32, 9} @@ -39,6 +40,22 @@ func (n Network) ForkVersion() phase0.Version { } } +// GenesisValidatorsRoot returns the genesis validators root of the network. +func (n Network) GenesisValidatorsRoot() phase0.Root { + var genValidatorsRoot phase0.Root + switch n { + case PraterNetwork: + rootBytes, _ := hex.DecodeString("043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb") + copy(genValidatorsRoot[:], rootBytes) + case MainNetwork: + rootBytes, _ := hex.DecodeString("4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95") + copy(genValidatorsRoot[:], rootBytes) + default: + logrus.WithField("network", n).Fatal("undefined network") + } + return genValidatorsRoot +} + // DepositContractAddress returns the deposit contract address of the network. func (n Network) DepositContractAddress() string { switch n { diff --git a/core/validator_account.go b/core/validator_account.go index 066db7a..5cab765 100644 --- a/core/validator_account.go +++ b/core/validator_account.go @@ -1,6 +1,8 @@ package core import ( + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/google/uuid" ) @@ -33,3 +35,11 @@ type ValidatorAccount interface { // SetContext sets the given context SetContext(ctx *WalletContext) } + +// ValidatorInfo represents the information of a validator +type ValidatorInfo struct { + Index phase0.ValidatorIndex + Pubkey phase0.BLSPubKey + WithdrawalCredentials []byte + ToExecutionAddress bellatrix.ExecutionAddress +} diff --git a/core/wallet.go b/core/wallet.go index 9677b62..ac85297 100644 --- a/core/wallet.go +++ b/core/wallet.go @@ -57,5 +57,6 @@ type Wallet interface { // WalletContext represents the wallet's context type type WalletContext struct { - Storage Storage + Storage Storage + WithdrawalMode bool } diff --git a/eth1_deposit/eth1_deposit.go b/eth1_deposit/eth1_deposit.go index 28e2d61..e565b8d 100644 --- a/eth1_deposit/eth1_deposit.go +++ b/eth1_deposit/eth1_deposit.go @@ -3,6 +3,7 @@ package eth1deposit import ( "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" + types "github.com/wealdtech/go-eth2-types/v2" util "github.com/wealdtech/go-eth2-util" "github.com/bloxapp/eth2-key-manager/core" @@ -16,12 +17,6 @@ const ( BLSWithdrawalPrefixByte = byte(0) ) -// GenesisValidatorsRoot genesis validators root of the chain. -var ( - GenesisValidatorsRoot = phase0.Root{} - DomainDeposit = [4]byte{0x03, 0x00, 0x00, 0x00} -) - // IsSupportedDepositNetwork returns true if the given network is supported var IsSupportedDepositNetwork = func(network core.Network) bool { return network == core.PyrmontNetwork || network == core.PraterNetwork || network == core.MainNetwork @@ -44,16 +39,17 @@ func DepositData(validationKey *core.HDKey, withdrawalPubKey []byte, network cor return nil, [32]byte{}, errors.Wrap(err, "failed to determine the root hash of deposit data") } - // Create domain - domain, err := ComputeETHDomain(DomainDeposit, network.ForkVersion(), GenesisValidatorsRoot) + // Compute domain + genesisForkVersion := network.GenesisForkVersion() + domain, err := types.ComputeDomain(types.DomainDeposit, genesisForkVersion[:], types.ZeroGenesisValidatorsRoot) if err != nil { return nil, [32]byte{}, errors.Wrap(err, "failed to calculate domain") } signingData := phase0.SigningData{ ObjectRoot: objRoot, - Domain: domain, } + copy(signingData.Domain[:], domain[:]) root, err := signingData.HashTreeRoot() if err != nil { @@ -88,20 +84,3 @@ func withdrawalCredentialsHash(withdrawalPubKey []byte) []byte { h := util.SHA256(withdrawalPubKey) return append([]byte{BLSWithdrawalPrefixByte}, h[1:]...)[:32] } - -// ComputeETHDomain returns computed domain -func ComputeETHDomain(domain phase0.DomainType, fork phase0.Version, genesisValidatorRoot phase0.Root) (phase0.Domain, error) { - ret := phase0.Domain{} - copy(ret[0:4], domain[:]) - - forkData := phase0.ForkData{ - CurrentVersion: fork, - GenesisValidatorsRoot: genesisValidatorRoot, - } - forkDataRoot, err := forkData.HashTreeRoot() - if err != nil { - return ret, err - } - copy(ret[4:32], forkDataRoot[0:28]) - return ret, nil -} diff --git a/go.mod b/go.mod index 43eaeb8..62f32e5 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/fatih/color v1.13.0 // indirect + github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/goccy/go-yaml v1.9.5 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.1 // indirect @@ -36,6 +37,7 @@ require ( github.com/wealdtech/go-bytesutil v1.1.1 // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 70088fb..86ed47d 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -80,12 +81,15 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-yaml v1.9.2/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0= @@ -176,13 +180,17 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -206,6 +214,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -236,6 +245,9 @@ github.com/prysmaticlabs/gohashtree v0.0.1-alpha.0.20220714111606-acbb2962fb48 h github.com/prysmaticlabs/gohashtree v0.0.1-alpha.0.20220714111606-acbb2962fb48/go.mod h1:4pWaT30XoEx1j8KNJf3TV+E3mQkaufn7mf+jRNb/Fuk= github.com/r3labs/sse/v2 v2.7.4/go.mod h1:hUrYMKfu9WquG9MyI0r6TKiNH+6Sw/QPKm2YbNbU5g8= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -255,6 +267,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= @@ -284,6 +297,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -406,6 +420,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -553,8 +568,9 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/signer/sign_bls_to_execution_change.go b/signer/sign_bls_to_execution_change.go new file mode 100644 index 0000000..6be77ce --- /dev/null +++ b/signer/sign_bls_to_execution_change.go @@ -0,0 +1,39 @@ +package signer + +import ( + "encoding/hex" + + "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// SignBLSToExecutionChange signs the given BLSToExecutionChange. OFFLINE operation +func (signer *SimpleSigner) SignBLSToExecutionChange(blsToExecutionChange *capella.BLSToExecutionChange, domain phase0.Domain, pubKey []byte) ([]byte, []byte, error) { + // Validate the bls to execution change. + if blsToExecutionChange == nil { + return nil, nil, errors.New("bls to execution change is nil") + } + + // Get the account. + if pubKey == nil { + return nil, nil, errors.New("account was not supplied") + } + account, err := signer.wallet.AccountByPublicKey(hex.EncodeToString(pubKey)) + if err != nil { + return nil, nil, err + } + + // Produce the signature. + root, err := ComputeETHSigningRoot(blsToExecutionChange, domain) + if err != nil { + return nil, nil, err + } + + // This is actually withdrawal key + sig, err := account.ValidationKeySign(root[:]) + if err != nil { + return nil, nil, err + } + return sig, root[:], nil +} diff --git a/signer/sign_bls_to_execution_change_test.go b/signer/sign_bls_to_execution_change_test.go new file mode 100644 index 0000000..259d5aa --- /dev/null +++ b/signer/sign_bls_to_execution_change_test.go @@ -0,0 +1,94 @@ +package signer + +import ( + "encoding/hex" + "testing" + + "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +// tested against a real block and sig from the Sepolia testnet (slot 1864649) +func TestSimpleSigner_SignBLSToExecutionChange(t *testing.T) { + signer, err := setupNoSlashingProtectionSK(_byteArray("6c1efcf889f78ec02d6becac5839b656d402d68f1c56723616f2c22d69cb7fc1")) + require.NoError(t, err) + + blsToExecutionChangeMock := &capella.BLSToExecutionChange{ + ValidatorIndex: 1573, + } + hexDecodedExecAdd, err := hex.DecodeString("beefd32838d5762ff55395a7beebef6e8528c64f") + require.NoError(t, err) + hexDecodedBLSPubKey, err := hex.DecodeString("8d8e66062fa5a1e5c4b9b0017d4027c944550a4e096fd8c535de1aa3b0283c5ece23c68c5881a154fe24a9c9377a0a09") + require.NoError(t, err) + + copy(blsToExecutionChangeMock.ToExecutionAddress[:], hexDecodedExecAdd) + copy(blsToExecutionChangeMock.FromBLSPubkey[:], hexDecodedBLSPubKey) + + tests := []struct { + name string + data *capella.BLSToExecutionChange + pubKey []byte + domain [32]byte + expectedError error + sig []byte + }{ + { + name: "simple sign", + data: blsToExecutionChangeMock, + pubKey: blsToExecutionChangeMock.FromBLSPubkey[:], + domain: _byteArray32("0a000000a8fee8ee9978418b64f1140b05f699a49ccd9b3fd666c35d4ae5f79e"), + expectedError: nil, + sig: _byteArray("aae6b0261494230fbf69ec8c1b907763153a5c52a39797f90aa106923d2d0d4752a392642f555520c2bbf54a9191876e0642555afa1c0050b341314c610f5bfd0821eafcc7981d551cc5b4969aff0ede5c229084dd098244706336621809a069"), + }, + { + name: "nil data", + data: nil, + pubKey: blsToExecutionChangeMock.FromBLSPubkey[:], + domain: _byteArray32("0a000000a8fee8ee9978418b64f1140b05f699a49ccd9b3fd666c35d4ae5f79e"), + expectedError: errors.New("bls to execution change is nil"), + sig: _byteArray("a3e966603e64cfd1d091718e3da0e4ed9b13619e7b40d805caf9eadaf84b72dc24fd7f09957a1438f937fbe3e12d6242190dcd5fcbced2b0ef57114ff369c65383eb8561bc56f4ab294ab3a3eba81134e1a90924e85e99e9742009ed4d8f9982"), + }, + { + name: "unknown account, should error", + data: blsToExecutionChangeMock, + pubKey: _byteArray("83e04069ed28b637f113d272a235af3e610401f252860ed2063d87d985931229458e3786e9b331cd73d9fc58863d9e4c"), + domain: _byteArray32("0a000000a8fee8ee9978418b64f1140b05f699a49ccd9b3fd666c35d4ae5f79e"), + expectedError: errors.New("account not found"), + sig: nil, + }, + { + name: "nil account, should error", + data: blsToExecutionChangeMock, + pubKey: nil, + domain: _byteArray32("0a000000a8fee8ee9978418b64f1140b05f699a49ccd9b3fd666c35d4ae5f79e"), + expectedError: errors.New("account was not supplied"), + sig: nil, + }, + { + name: "empty account, should error", + data: blsToExecutionChangeMock, + pubKey: _byteArray(""), + domain: _byteArray32("00000001d7a9bca8823e555db65bb772e1496a26e1a8c5b1c0c7def9c9eaf7f6"), + expectedError: errors.New("account not found"), + sig: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + res, _, err := signer.SignBLSToExecutionChange(test.data, test.domain, test.pubKey) + if test.expectedError != nil { + if err != nil { + require.Equal(t, test.expectedError.Error(), err.Error()) + } else { + t.Errorf("no error returned, expected: %s", test.expectedError.Error()) + } + } else { + // check sign worked + require.NoError(t, err) + require.EqualValues(t, test.sig, res) + } + }) + } +} diff --git a/signer/sign_voluntary_exit.go b/signer/sign_voluntary_exit.go new file mode 100644 index 0000000..f535c8d --- /dev/null +++ b/signer/sign_voluntary_exit.go @@ -0,0 +1,36 @@ +package signer + +import ( + "encoding/hex" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// SignVoluntaryExit signs the given VoluntaryExit. +func (signer *SimpleSigner) SignVoluntaryExit(voluntaryExit *phase0.VoluntaryExit, domain phase0.Domain, pubKey []byte) ([]byte, []byte, error) { + // Validate the voluntary exit. + if voluntaryExit == nil { + return nil, nil, errors.New("voluntary exit data is nil") + } + + // Get the account. + if pubKey == nil { + return nil, nil, errors.New("account was not supplied") + } + account, err := signer.wallet.AccountByPublicKey(hex.EncodeToString(pubKey)) + if err != nil { + return nil, nil, err + } + + // Produce the signature. + root, err := ComputeETHSigningRoot(voluntaryExit, domain) + if err != nil { + return nil, nil, err + } + sig, err := account.ValidationKeySign(root[:]) + if err != nil { + return nil, nil, err + } + return sig, root[:], nil +} diff --git a/signer/sign_voluntary_exit_test.go b/signer/sign_voluntary_exit_test.go new file mode 100644 index 0000000..02a3e56 --- /dev/null +++ b/signer/sign_voluntary_exit_test.go @@ -0,0 +1,87 @@ +package signer + +import ( + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +// tested against a real block and sig from the Prater testnet (slot 5133683) +func TestSimpleSigner_SignVoluntaryExit(t *testing.T) { + signer, err := setupNoSlashingProtectionSK(_byteArray("37247532b925101f094fb0cc877f523859c4a73bcbfc88f3833b05c26bd37cc6")) + require.NoError(t, err) + + voluntaryExitMock := &phase0.VoluntaryExit{ + Epoch: 160427, + ValidatorIndex: 438850, + } + + tests := []struct { + name string + data *phase0.VoluntaryExit + pubKey []byte + domain [32]byte + expectedError error + sig []byte + }{ + { + name: "simple sign", + data: voluntaryExitMock, + pubKey: _byteArray("b5ade10d8cc63646ae7b30588c6fb9e482e51f98e396633a6e157bbde14bcdb771b7d147e5fb8b2bd6ce99323431008e"), + domain: _byteArray32("04000000c2ce3aa85707d491e3dd033a53971deb9bed9d4813d74c99369642f5"), + expectedError: nil, + sig: _byteArray("8b8084ef095af3d0351a4c9308667b7254f3c0e9233e18f7ab59a29a6b6a3abdab9fbe9b7b61d9dd384675c4ed2b721a108890645ee9f69e97e2bccc586a35ddcebeaf20617d9c942fa1562db6814b016b8ebb4ee97d78c8ae27ae4b3dba2653"), + }, + { + name: "nil data", + data: nil, + pubKey: _byteArray("b5ade10d8cc63646ae7b30588c6fb9e482e51f98e396633a6e157bbde14bcdb771b7d147e5fb8b2bd6ce99323431008e"), + domain: _byteArray32("04000000c2ce3aa85707d491e3dd033a53971deb9bed9d4813d74c99369642f5"), + expectedError: errors.New("voluntary exit data is nil"), + sig: _byteArray("a3e966603e64cfd1d091718e3da0e4ed9b13619e7b40d805caf9eadaf84b72dc24fd7f09957a1438f937fbe3e12d6242190dcd5fcbced2b0ef57114ff369c65383eb8561bc56f4ab294ab3a3eba81134e1a90924e85e99e9742009ed4d8f9982"), + }, + { + name: "unknown account, should error", + data: voluntaryExitMock, + pubKey: _byteArray("83e04069ed28b637f113d272a235af3e610401f252860ed2063d87d985931229458e3786e9b331cd73d9fc58863d9e4c"), + domain: _byteArray32("04000000c2ce3aa85707d491e3dd033a53971deb9bed9d4813d74c99369642f5"), + expectedError: errors.New("account not found"), + sig: nil, + }, + { + name: "nil account, should error", + data: voluntaryExitMock, + pubKey: nil, + domain: _byteArray32("04000000c2ce3aa85707d491e3dd033a53971deb9bed9d4813d74c99369642f5"), + expectedError: errors.New("account was not supplied"), + sig: nil, + }, + { + name: "empty account, should error", + data: voluntaryExitMock, + pubKey: _byteArray(""), + domain: _byteArray32("04000000c2ce3aa85707d491e3dd033a53971deb9bed9d4813d74c99369642f5"), + expectedError: errors.New("account not found"), + sig: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + res, _, err := signer.SignVoluntaryExit(test.data, test.domain, test.pubKey) + if test.expectedError != nil { + if err != nil { + require.Equal(t, test.expectedError.Error(), err.Error()) + } else { + t.Errorf("no error returned, expected: %s", test.expectedError.Error()) + } + } else { + // check sign worked + require.NoError(t, err) + require.EqualValues(t, test.sig, res) + } + }) + } +} diff --git a/signer/validator_signer.go b/signer/validator_signer.go index 6fdedae..a4e12b9 100644 --- a/signer/validator_signer.go +++ b/signer/validator_signer.go @@ -6,6 +6,7 @@ import ( "github.com/attestantio/go-eth2-client/api" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/google/uuid" @@ -25,6 +26,8 @@ type ValidatorSigner interface { SignSyncCommitteeSelectionData(data *altair.SyncAggregatorSelectionData, domain phase0.Domain, pubKey []byte) (sig []byte, root []byte, err error) SignSyncCommitteeContributionAndProof(contribAndProof *altair.ContributionAndProof, domain phase0.Domain, pubKey []byte) (sig []byte, root []byte, err error) SignRegistration(registration *api.VersionedValidatorRegistration, domain phase0.Domain, pubKey []byte) (sig []byte, root []byte, err error) + SignVoluntaryExit(voluntaryExit *phase0.VoluntaryExit, domain phase0.Domain, pubKey []byte) (sig []byte, root []byte, err error) + SignBLSToExecutionChange(blsToExecutionChange *capella.BLSToExecutionChange, domain phase0.Domain, pubKey []byte) (sig []byte, root []byte, err error) } // SimpleSigner implements ValidatorSigner interface diff --git a/stores/inmemory/marshalable_test.go b/stores/inmemory/marshalable_test.go index 125f1e7..d20a093 100644 --- a/stores/inmemory/marshalable_test.go +++ b/stores/inmemory/marshalable_test.go @@ -132,6 +132,7 @@ func TestMarshaling(t *testing.T) { prop2, found, err := store.RetrieveHighestProposal(acc.ValidatorPublicKey()) require.NoError(t, err) require.True(t, found) + require.NotNil(t, prop2) require.Equal(t, phase0.Slot(1), prop2) }) } diff --git a/stores/inmemory/slashing_test.go b/stores/inmemory/slashing_test.go index dc203c2..ec3f9be 100644 --- a/stores/inmemory/slashing_test.go +++ b/stores/inmemory/slashing_test.go @@ -98,6 +98,7 @@ func TestSavingHighestProposal(t *testing.T) { proposal, found, err := storage.RetrieveHighestProposal(valPubKey) require.NoError(t, err) require.True(t, found) + require.NotNil(t, proposal) require.EqualValues(t, test.proposal, proposal) }) } diff --git a/wallets/hd/wallet.go b/wallets/hd/wallet.go index 21d0f26..358f33f 100644 --- a/wallets/hd/wallet.go +++ b/wallets/hd/wallet.go @@ -20,11 +20,8 @@ const ( ValidatorKeyPath = WithdrawalKeyPath + "/0" ) -// Predefined errors -var ( - // ErrAccountNotFound is the error when account not found - ErrAccountNotFound = errors.New("account not found") -) +// ErrAccountNotFound is the error when account not found +var ErrAccountNotFound = errors.New("account not found") // Wallet represents hierarchical deterministic wallet type Wallet struct { @@ -91,11 +88,21 @@ func (wallet *Wallet) BuildValidatorAccount(indexPointer *int, key *core.MasterD return nil, err } + var primaryKey *core.HDKey + var secondaryPubKey []byte + if wallet.context.WithdrawalMode { + primaryKey = withdrawalKey + secondaryPubKey = validatorKey.PublicKey().Serialize() + } else { + primaryKey = validatorKey + secondaryPubKey = withdrawalKey.PublicKey().Serialize() + } + // Create ret account ret := wallets.NewValidatorAccount( name, - validatorKey, - withdrawalKey.PublicKey().Serialize(), + primaryKey, + secondaryPubKey, baseAccountPath, wallet.context, )