diff --git a/cmd/validator/accounts/exit.go b/cmd/validator/accounts/exit.go index 168657c195..85143f72fb 100644 --- a/cmd/validator/accounts/exit.go +++ b/cmd/validator/accounts/exit.go @@ -34,7 +34,7 @@ func Exit(c *cli.Context, r io.Reader) error { beaconRPCProvider := c.String(flags.BeaconRPCProviderFlag.Name) if !c.IsSet(flags.Web3SignerURLFlag.Name) && !c.IsSet(flags.WalletDirFlag.Name) && !c.IsSet(flags.InteropNumValidators.Name) { return errors.Errorf("No validators found, please provide a prysm wallet directory via flag --%s "+ - "or a web3signer location with corresponding public keys via flags --%s and --%s ", + "or a remote signer location with corresponding public keys via flags --%s and --%s ", flags.WalletDirFlag.Name, flags.Web3SignerURLFlag.Name, flags.Web3SignerPublicValidatorKeysFlag, @@ -62,7 +62,7 @@ func Exit(c *cli.Context, r io.Reader) error { } config, err := node.Web3SignerConfig(c) if err != nil { - return errors.Wrapf(err, "could not configure web3signer") + return errors.Wrapf(err, "could not configure remote signer") } config.GenesisValidatorsRoot = resp.GenesisValidatorsRoot w, km, err = walletWithWeb3SignerKeymanager(c, config) diff --git a/cmd/validator/accounts/wallet_utils.go b/cmd/validator/accounts/wallet_utils.go index 0e8c45c493..abcccc8370 100644 --- a/cmd/validator/accounts/wallet_utils.go +++ b/cmd/validator/accounts/wallet_utils.go @@ -30,7 +30,7 @@ func walletWithKeymanager(c *cli.Context) (*wallet.Wallet, keymanager.IKeymanage } func walletWithWeb3SignerKeymanager(c *cli.Context, config *remote_web3signer.SetupConfig) (*wallet.Wallet, keymanager.IKeymanager, error) { - w := wallet.NewWalletForWeb3Signer() + w := wallet.NewWalletForWeb3Signer(c) km, err := w.InitializeKeymanager(c.Context, iface.InitKeymanagerConfig{ListenForChanges: false, Web3SignerConfig: config}) if err != nil { return nil, nil, err diff --git a/cmd/validator/flags/flags.go b/cmd/validator/flags/flags.go index d9822cc722..4a13aad669 100644 --- a/cmd/validator/flags/flags.go +++ b/cmd/validator/flags/flags.go @@ -288,18 +288,30 @@ var ( // example:--validators-external-signer-url=http://localhost:9000 // web3signer documentation can be found in Consensys' web3signer project docs Web3SignerURLFlag = &cli.StringFlag{ - Name: "validators-external-signer-url", - Usage: "URL for consensys' web3signer software to use with the Prysm validator client.", - Value: "", + Name: "validators-external-signer-url", + Usage: "URL for consensys' web3signer software to use with the Prysm validator client.", + Value: "", + Aliases: []string{"remote-signer-url"}, } // Web3SignerPublicValidatorKeysFlag defines a comma-separated list of hex string public keys or external url for web3signer to use for validator signing. // example with external url: --validators-external-signer-public-keys= https://web3signer.com/api/v1/eth2/publicKeys // example with public key: --validators-external-signer-public-keys=0xa99a...e44c,0xb89b...4a0b // web3signer documentation can be found in Consensys' web3signer project docs``` Web3SignerPublicValidatorKeysFlag = &cli.StringSliceFlag{ - Name: "validators-external-signer-public-keys", - Usage: "Comma separated list of public keys OR an external url endpoint for the validator to retrieve public keys from for usage with web3signer.", + Name: "validators-external-signer-public-keys", + Usage: "Comma separated list of public keys OR an external url endpoint for the validator to retrieve public keys from for usage with web3signer.", + Aliases: []string{"remote-signer-keys"}, } + + // Web3SignerKeyFileFlag defines a file for keys to persist to. + // example:--validators-external-signer-key-file=./path/to/keys.txt + Web3SignerKeyFileFlag = &cli.StringFlag{ + Name: "validators-external-signer-key-file", + Usage: "A file path used to load remote public validator keys and persist them through restarts.", + Value: "", + Aliases: []string{"remote-signer-keys-file"}, + } + // KeymanagerKindFlag defines the kind of keymanager desired by a user during wallet creation. KeymanagerKindFlag = &cli.StringFlag{ Name: "keymanager-kind", diff --git a/cmd/validator/main.go b/cmd/validator/main.go index f5ca240ee0..c4584f955b 100644 --- a/cmd/validator/main.go +++ b/cmd/validator/main.go @@ -79,6 +79,7 @@ var appFlags = []cli.Flag{ // Consensys' Web3Signer flags flags.Web3SignerURLFlag, flags.Web3SignerPublicValidatorKeysFlag, + flags.Web3SignerKeyFileFlag, flags.SuggestedFeeRecipientFlag, flags.ProposerSettingsURLFlag, flags.ProposerSettingsFlag, diff --git a/cmd/validator/usage.go b/cmd/validator/usage.go index cef63780bb..59f720b2f1 100644 --- a/cmd/validator/usage.go +++ b/cmd/validator/usage.go @@ -127,6 +127,7 @@ var appHelpFlagGroups = []flagGroup{ Flags: []cli.Flag{ flags.Web3SignerURLFlag, flags.Web3SignerPublicValidatorKeysFlag, + flags.Web3SignerKeyFileFlag, }, }, { diff --git a/io/file/BUILD.bazel b/io/file/BUILD.bazel index 80f977fc11..1c8e2a3461 100644 --- a/io/file/BUILD.bazel +++ b/io/file/BUILD.bazel @@ -2,12 +2,16 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["fileutil.go"], + srcs = [ + "fileutil.go", + "log.go", + ], importpath = "github.com/prysmaticlabs/prysm/v5/io/file", visibility = ["//visibility:public"], deps = [ "//config/params:go_default_library", "@com_github_pkg_errors//:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", ], ) diff --git a/io/file/fileutil.go b/io/file/fileutil.go index dfe0294579..19b4ff976c 100644 --- a/io/file/fileutil.go +++ b/io/file/fileutil.go @@ -382,3 +382,27 @@ func DirFiles(dir string) ([]string, error) { } return files, nil } + +// WriteLinesToFile writes a slice of strings to a file, each string on a new line. +func WriteLinesToFile(lines []string, filename string) error { + // Open the file for writing. If the file does not exist, create it, or truncate it if it does. + f, err := os.Create(filepath.Clean(filename)) + if err != nil { + return fmt.Errorf("error creating file: %w", err) + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + log.Error(err.Error()) + } + }(f) + + // Iterate through all lines in the slice and write them to the file + for _, line := range lines { + if _, err := f.WriteString(line + "\n"); err != nil { + return fmt.Errorf("error writing line to file: %w", err) + } + } + + return nil +} diff --git a/io/file/fileutil_test.go b/io/file/fileutil_test.go index 42754b59e0..dba58a192d 100644 --- a/io/file/fileutil_test.go +++ b/io/file/fileutil_test.go @@ -24,6 +24,7 @@ import ( "os/user" "path/filepath" "sort" + "strings" "testing" "github.com/prysmaticlabs/prysm/v5/config/params" @@ -567,3 +568,37 @@ func TestHasReadWritePermissions(t *testing.T) { }) } } + +func TestWriteLinesToFile(t *testing.T) { + filename := filepath.Join(t.TempDir(), "testfile.txt") + t.Run("write to a new file", func(t *testing.T) { + lines := []string{"line1", "line2", "line3"} + require.NoError(t, file.WriteLinesToFile(lines, filename)) + // Check file content + content, err := os.ReadFile(filepath.Clean(filename)) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + // Join lines with newline for comparison + expectedContent := strings.Join(lines, "\n") + "\n" + if string(content) != expectedContent { + t.Errorf("file content = %q, want %q", string(content), expectedContent) + } + }) + t.Run("overwrite existing file", func(t *testing.T) { + lines := []string{"line4", "line5"} + require.NoError(t, file.WriteLinesToFile(lines, filename)) + // Check file content + content, err := os.ReadFile(filepath.Clean(filename)) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + // Join lines with newline for comparison + expectedContent := strings.Join(lines, "\n") + "\n" + if string(content) != expectedContent { + t.Errorf("file content = %q, want %q", string(content), expectedContent) + } + }) +} diff --git a/io/file/log.go b/io/file/log.go new file mode 100644 index 0000000000..2489ef8585 --- /dev/null +++ b/io/file/log.go @@ -0,0 +1,5 @@ +package file + +import "github.com/sirupsen/logrus" + +var log = logrus.WithField("prefix", "fileutil") diff --git a/testing/endtoend/components/validator.go b/testing/endtoend/components/validator.go index 042a49bc7e..117c2ec3cb 100644 --- a/testing/endtoend/components/validator.go +++ b/testing/endtoend/components/validator.go @@ -198,7 +198,7 @@ func (v *ValidatorNode) Start(ctx context.Context) error { beaconRPCPort = e2e.TestParams.Ports.PrysmBeaconNodeRPCPort } - file, err := helpers.DeleteAndCreateFile(e2e.TestParams.LogPath, fmt.Sprintf(e2e.ValidatorLogFileName, index)) + logFile, err := helpers.DeleteAndCreateFile(e2e.TestParams.LogPath, fmt.Sprintf(e2e.ValidatorLogFileName, index)) if err != nil { return err } @@ -223,7 +223,7 @@ func (v *ValidatorNode) Start(ctx context.Context) error { } args := []string{ fmt.Sprintf("--%s=%s/eth2-val-%d", cmdshared.DataDirFlag.Name, e2e.TestParams.TestPath, index), - fmt.Sprintf("--%s=%s", cmdshared.LogFileName.Name, file.Name()), + fmt.Sprintf("--%s=%s", cmdshared.LogFileName.Name, logFile.Name()), fmt.Sprintf("--%s=%s", flags.GraffitiFileFlag.Name, gFile), fmt.Sprintf("--%s=%d", flags.MonitoringPortFlag.Name, e2e.TestParams.Ports.ValidatorMetricsPort+index), fmt.Sprintf("--%s=%d", flags.GRPCGatewayPort.Name, e2e.TestParams.Ports.ValidatorGatewayPort+index), @@ -258,7 +258,16 @@ func (v *ValidatorNode) Start(ctx context.Context) error { // See: https://docs.teku.consensys.net/en/latest/HowTo/External-Signer/Use-External-Signer/ args = append(args, fmt.Sprintf("--%s=http://localhost:%d", flags.Web3SignerURLFlag.Name, Web3RemoteSignerPort), - fmt.Sprintf("--%s=%s", flags.Web3SignerPublicValidatorKeysFlag.Name, strings.Join(validatorHexPubKeys, ","))) + ) + if v.config.UsePersistentKeyFile { + keysPath := filepath.Join(e2e.TestParams.TestPath, "proposer-settings", fmt.Sprintf("validator_%d", index), "keys.txt") + if err := file.WriteLinesToFile(validatorHexPubKeys, keysPath); err != nil { + return err + } + args = append(args, fmt.Sprintf("--%s=%s", flags.Web3SignerKeyFileFlag.Name, keysPath)) + } else { + args = append(args, fmt.Sprintf("--%s=%s", flags.Web3SignerPublicValidatorKeysFlag.Name, strings.Join(validatorHexPubKeys, ","))) + } } else { // When not using remote key signer, use interop keys. args = append(args, diff --git a/testing/endtoend/minimal_scenario_e2e_test.go b/testing/endtoend/minimal_scenario_e2e_test.go index abd029f428..33628927a7 100644 --- a/testing/endtoend/minimal_scenario_e2e_test.go +++ b/testing/endtoend/minimal_scenario_e2e_test.go @@ -20,6 +20,10 @@ func TestEndToEnd_MinimalConfig_Web3Signer(t *testing.T) { e2eMinimal(t, types.InitForkCfg(version.Phase0, version.Deneb, params.E2ETestConfig()), types.WithRemoteSigner()).run() } +func TestEndToEnd_MinimalConfig_Web3Signer_PersistentKeys(t *testing.T) { + e2eMinimal(t, types.InitForkCfg(version.Phase0, version.Deneb, params.E2ETestConfig()), types.WithRemoteSignerAndPersistentKeysFile()).run() +} + func TestEndToEnd_MinimalConfig_ValidatorRESTApi(t *testing.T) { e2eMinimal(t, types.InitForkCfg(version.Phase0, version.Deneb, params.E2ETestConfig()), types.WithCheckpointSync(), types.WithValidatorRESTApi()).run() } diff --git a/testing/endtoend/types/types.go b/testing/endtoend/types/types.go index 3d29af1bca..d5257a76f4 100644 --- a/testing/endtoend/types/types.go +++ b/testing/endtoend/types/types.go @@ -26,6 +26,13 @@ func WithRemoteSigner() E2EConfigOpt { } } +func WithRemoteSignerAndPersistentKeysFile() E2EConfigOpt { + return func(cfg *E2EConfig) { + cfg.UseWeb3RemoteSigner = true + cfg.UsePersistentKeyFile = true + } +} + func WithCheckpointSync() E2EConfigOpt { return func(cfg *E2EConfig) { cfg.TestCheckpointSync = true @@ -58,6 +65,7 @@ type E2EConfig struct { UsePrysmShValidator bool UsePprof bool UseWeb3RemoteSigner bool + UsePersistentKeyFile bool TestDeposits bool UseFixedPeerIDs bool UseValidatorCrossClient bool diff --git a/validator/accounts/iface/wallet.go b/validator/accounts/iface/wallet.go index 19991556ed..3e06a78569 100644 --- a/validator/accounts/iface/wallet.go +++ b/validator/accounts/iface/wallet.go @@ -19,6 +19,8 @@ type InitKeymanagerConfig struct { type Wallet interface { // Methods to retrieve wallet and accounts metadata. AccountsDir() string + // Method to retrieve wallet directory. + Dir() string Password() string // Read methods for important wallet and accounts-related files. ReadFileAtPath(ctx context.Context, filePath string, fileName string) ([]byte, error) @@ -26,4 +28,6 @@ type Wallet interface { WriteFileAtPath(ctx context.Context, pathName string, fileName string, data []byte) (bool, error) // Method for initializing a new keymanager. InitializeKeymanager(ctx context.Context, cfg InitKeymanagerConfig) (keymanager.IKeymanager, error) + // Method for returning keymanager kind. + KeymanagerKind() keymanager.Kind } diff --git a/validator/accounts/testing/mock.go b/validator/accounts/testing/mock.go index 2f1e3d0a98..f1e9560e37 100644 --- a/validator/accounts/testing/mock.go +++ b/validator/accounts/testing/mock.go @@ -29,6 +29,8 @@ type Wallet struct { UnlockAccounts bool lock sync.RWMutex HasWriteFileError bool + WalletDir string + Kind keymanager.Kind } // AccountNames -- @@ -47,6 +49,16 @@ func (w *Wallet) AccountsDir() string { return w.InnerAccountsDir } +// Dir for the wallet. +func (w *Wallet) Dir() string { + return w.WalletDir +} + +// KeymanagerKind -- +func (w *Wallet) KeymanagerKind() keymanager.Kind { + return w.Kind +} + // Exists -- func (w *Wallet) Exists() (bool, error) { return len(w.Directories) > 0, nil diff --git a/validator/accounts/wallet/wallet.go b/validator/accounts/wallet/wallet.go index b8bb3094c9..1ebcab12d4 100644 --- a/validator/accounts/wallet/wallet.go +++ b/validator/accounts/wallet/wallet.go @@ -254,10 +254,11 @@ func OpenOrCreateNewWallet(cliCtx *cli.Context) (*Wallet, error) { } // NewWalletForWeb3Signer returns a new wallet for web3 signer which is temporary and not stored locally. -func NewWalletForWeb3Signer() *Wallet { +func NewWalletForWeb3Signer(cliCtx *cli.Context) *Wallet { + walletDir := cliCtx.String(flags.WalletDirFlag.Name) // wallet is just a temporary wallet for web3 signer used to call initialize keymanager. return &Wallet{ - walletDir: "", + walletDir: walletDir, // it's ok if there's an existing wallet accountsPath: "", keymanagerKind: keymanager.Web3Signer, walletPassword: "", @@ -318,6 +319,11 @@ func (w *Wallet) AccountsDir() string { return w.accountsPath } +// Dir for the wallet. +func (w *Wallet) Dir() string { + return w.walletDir +} + // Password for the wallet. func (w *Wallet) Password() string { return w.walletPassword diff --git a/validator/accounts/wallet/wallet_test.go b/validator/accounts/wallet/wallet_test.go index 586552e330..6b9f17b658 100644 --- a/validator/accounts/wallet/wallet_test.go +++ b/validator/accounts/wallet/wallet_test.go @@ -61,7 +61,11 @@ func Test_IsValid_RandomFiles(t *testing.T) { } func TestWallet_InitializeKeymanager_web3Signer_HappyPath(t *testing.T) { - w := wallet.NewWalletForWeb3Signer() + app := cli.App{} + set := flag.NewFlagSet("test", 0) + newDir := filepath.Join(t.TempDir(), "new") + set.String(flags.WalletDirFlag.Name, newDir, "") + w := wallet.NewWalletForWeb3Signer(cli.NewContext(&app, set, nil)) ctx := context.Background() root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") require.NoError(t, err) @@ -70,7 +74,6 @@ func TestWallet_InitializeKeymanager_web3Signer_HappyPath(t *testing.T) { Web3SignerConfig: &remoteweb3signer.SetupConfig{ BaseEndpoint: "http://localhost:8545", GenesisValidatorsRoot: root, - PublicKeysURL: "http://localhost:8545/public_keys", }, } km, err := w.InitializeKeymanager(ctx, config) @@ -79,7 +82,11 @@ func TestWallet_InitializeKeymanager_web3Signer_HappyPath(t *testing.T) { } func TestWallet_InitializeKeymanager_web3Signer_nilConfig(t *testing.T) { - w := wallet.NewWalletForWeb3Signer() + app := cli.App{} + set := flag.NewFlagSet("test", 0) + newDir := filepath.Join(t.TempDir(), "new") + set.String(flags.WalletDirFlag.Name, newDir, "") + w := wallet.NewWalletForWeb3Signer(cli.NewContext(&app, set, nil)) ctx := context.Background() config := iface.InitKeymanagerConfig{ ListenForChanges: false, diff --git a/validator/client/BUILD.bazel b/validator/client/BUILD.bazel index bbcab6f563..6c9fbce2ac 100644 --- a/validator/client/BUILD.bazel +++ b/validator/client/BUILD.bazel @@ -124,6 +124,7 @@ go_test( "//async/event:go_default_library", "//beacon-chain/core/signing:go_default_library", "//cache/lru:go_default_library", + "//cmd/validator/flags:go_default_library", "//config/features:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", @@ -169,6 +170,7 @@ go_test( "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@com_github_stretchr_testify//mock:go_default_library", "@com_github_tyler_smith_go_bip39//:go_default_library", + "@com_github_urfave_cli_v2//:go_default_library", "@com_github_wealdtech_go_eth2_util//:go_default_library", "@in_gopkg_d4l3k_messagediff_v1//:go_default_library", "@io_bazel_rules_go//go/tools/bazel:go_default_library", diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index d0ebbbfbdd..88214ff284 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -3,9 +3,12 @@ package client import ( "context" "errors" + "flag" "fmt" "io" "math" + "os" + "path/filepath" "strings" "sync" "testing" @@ -15,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/ptypes/empty" "github.com/prysmaticlabs/prysm/v5/async/event" + "github.com/prysmaticlabs/prysm/v5/cmd/validator/flags" "github.com/prysmaticlabs/prysm/v5/config/features" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" @@ -39,6 +43,7 @@ import ( remoteweb3signer "github.com/prysmaticlabs/prysm/v5/validator/keymanager/remote-web3signer" "github.com/sirupsen/logrus" logTest "github.com/sirupsen/logrus/hooks/test" + "github.com/urfave/cli/v2" "go.uber.org/mock/gomock" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -1356,19 +1361,19 @@ func TestValidator_WaitForKeymanagerInitialization_web3Signer(t *testing.T) { copy(root[2:], "a") err := db.SaveGenesisValidatorsRoot(ctx, root) require.NoError(t, err) - w := wallet.NewWalletForWeb3Signer() - decodedKey, err := hexutil.Decode("0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820") - require.NoError(t, err) - keys := [][48]byte{ - bytesutil.ToBytes48(decodedKey), - } + app := cli.App{} + set := flag.NewFlagSet("test", 0) + newDir := filepath.Join(t.TempDir(), "new") + require.NoError(t, os.MkdirAll(newDir, 0700)) + set.String(flags.WalletDirFlag.Name, newDir, "") + w := wallet.NewWalletForWeb3Signer(cli.NewContext(&app, set, nil)) v := validator{ db: db, useWeb: false, wallet: w, web3SignerConfig: &remoteweb3signer.SetupConfig{ BaseEndpoint: "http://localhost:8545", - ProvidedPublicKeys: keys, + ProvidedPublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}, }, } err = v.WaitForKeymanagerInitialization(context.Background()) diff --git a/validator/keymanager/remote-web3signer/BUILD.bazel b/validator/keymanager/remote-web3signer/BUILD.bazel index 3063142ace..d3cc832bb8 100644 --- a/validator/keymanager/remote-web3signer/BUILD.bazel +++ b/validator/keymanager/remote-web3signer/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "keymanager.go", + "log.go", "metrics.go", ], importpath = "github.com/prysmaticlabs/prysm/v5/validator/keymanager/remote-web3signer", @@ -16,18 +17,22 @@ go_library( "//config/fieldparams:go_default_library", "//crypto/bls:go_default_library", "//encoding/bytesutil:go_default_library", + "//io/file:go_default_library", "//proto/prysm/v1alpha1/validator-client:go_default_library", "//validator/accounts/petnames:go_default_library", "//validator/keymanager:go_default_library", "//validator/keymanager/remote-web3signer/internal:go_default_library", "//validator/keymanager/remote-web3signer/v1:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", + "@com_github_fsnotify_fsnotify//:go_default_library", "@com_github_go_playground_validator_v10//:go_default_library", "@com_github_logrusorgru_aurora//:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_prometheus_client_golang//prometheus:go_default_library", "@com_github_prometheus_client_golang//prometheus/promauto:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", + "@io_opencensus_go//trace:go_default_library", + "@org_golang_x_exp//maps:go_default_library", ], ) @@ -38,12 +43,14 @@ go_test( deps = [ "//crypto/bls:go_default_library", "//encoding/bytesutil:go_default_library", + "//io/file:go_default_library", "//proto/prysm/v1alpha1/validator-client:go_default_library", "//testing/require:go_default_library", "//validator/keymanager:go_default_library", "//validator/keymanager/remote-web3signer/internal:go_default_library", "//validator/keymanager/remote-web3signer/v1/mock:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", + "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", ], ) diff --git a/validator/keymanager/remote-web3signer/internal/BUILD.bazel b/validator/keymanager/remote-web3signer/internal/BUILD.bazel index 609734f4b7..8b9e49daf3 100644 --- a/validator/keymanager/remote-web3signer/internal/BUILD.bazel +++ b/validator/keymanager/remote-web3signer/internal/BUILD.bazel @@ -12,7 +12,6 @@ go_library( deps = [ "//config/fieldparams:go_default_library", "//crypto/bls:go_default_library", - "//encoding/bytesutil:go_default_library", "//monitoring/tracing:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_pkg_errors//:go_default_library", diff --git a/validator/keymanager/remote-web3signer/internal/client.go b/validator/keymanager/remote-web3signer/internal/client.go index 572930a3b9..9c9c9fc064 100644 --- a/validator/keymanager/remote-web3signer/internal/client.go +++ b/validator/keymanager/remote-web3signer/internal/client.go @@ -17,7 +17,6 @@ import ( "github.com/pkg/errors" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/crypto/bls" - "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" "github.com/prysmaticlabs/prysm/v5/monitoring/tracing" "github.com/sirupsen/logrus" "go.opencensus.io/trace" @@ -37,7 +36,7 @@ type SignatureResponse struct { // HttpSignerClient defines the interface for interacting with a remote web3signer. type HttpSignerClient interface { Sign(ctx context.Context, pubKey string, request SignRequestJson) (bls.Signature, error) - GetPublicKeys(ctx context.Context, url string) ([][48]byte, error) + GetPublicKeys(ctx context.Context, url string) ([]string, error) } // ApiClient a wrapper object around web3signer APIs. Please refer to the docs from Consensys' web3signer project. @@ -87,8 +86,8 @@ func (client *ApiClient) Sign(ctx context.Context, pubKey string, request SignRe } // GetPublicKeys is a wrapper method around the web3signer publickeys api (this may be removed in the future or moved to another location due to its usage). -func (client *ApiClient) GetPublicKeys(ctx context.Context, url string) ([][fieldparams.BLSPubkeyLength]byte, error) { - resp, err := client.doRequest(ctx, http.MethodGet, url, nil /* no body needed on get request */) +func (client *ApiClient) GetPublicKeys(ctx context.Context, url string) ([]string, error) { + resp, err := client.doRequest(ctx, http.MethodGet, url, http.NoBody) if err != nil { return nil, err } @@ -96,20 +95,19 @@ func (client *ApiClient) GetPublicKeys(ctx context.Context, url string) ([][fiel if err := unmarshalResponse(resp.Body, &publicKeys); err != nil { return nil, err } - decodedKeys := make([][fieldparams.BLSPubkeyLength]byte, len(publicKeys)) - var errorKeyPositions string - for i, value := range publicKeys { - decodedKey, err := hexutil.Decode(value) - if err != nil { - errorKeyPositions += fmt.Sprintf("%v, ", i) - continue - } - decodedKeys[i] = bytesutil.ToBytes48(decodedKey) + if len(publicKeys) == 0 { + return publicKeys, nil } - if errorKeyPositions != "" { - return nil, errors.New("failed to decode from Hex from the following public key index locations: " + errorKeyPositions) + // early check if it's a hex and a public key + // note: a full loop will be conducted in keymanager.go if the quick check passes + b, err := hexutil.Decode(publicKeys[0]) + if err != nil { + return nil, errors.Wrap(err, "unable to decode public key") } - return decodedKeys, nil + if len(b) != fieldparams.BLSPubkeyLength { + return nil, fmt.Errorf("invalid public key length of %v bytes", len(b)) + } + return publicKeys, nil } // ReloadSignerKeys is a wrapper method around the web3signer reload api. diff --git a/validator/keymanager/remote-web3signer/internal/client_test.go b/validator/keymanager/remote-web3signer/internal/client_test.go index 71a482baef..b3048077a8 100644 --- a/validator/keymanager/remote-web3signer/internal/client_test.go +++ b/validator/keymanager/remote-web3signer/internal/client_test.go @@ -153,24 +153,7 @@ func TestClient_GetPublicKeys_HappyPath(t *testing.T) { assert.NotNil(t, resp) assert.Nil(t, err) // we would like them as 48byte base64 without 0x - assert.EqualValues(t, "[162 181 170 173 156 110 254 254 123 185 177 36 58 4 52 4 243 54 41 55 207 182 179 24 51 146 152 51 23 63 71 102 48 234 44 254 176 217 221 241 95 151 202 134 133 148 136 32]", fmt.Sprintf("%v", resp[0][:])) -} - -func TestClient_GetPublicKeys_EncodingError(t *testing.T) { - // public keys are returned hex encoded with 0x - j := `["a2b5aaad9c6efefe7bb9b1243a043404f3362937c","fb6b31833929833173f476630ea2cfe","b0d9ddf15fca8685948820"]` - // create a new reader with that JSON - r := io.NopCloser(bytes.NewReader([]byte(j))) - mock := &mockTransport{mockResponse: &http.Response{ - StatusCode: 200, - Body: r, - }} - u, err := url.Parse("example.com") - assert.NoError(t, err) - cl := internal.ApiClient{BaseURL: u, RestClient: &http.Client{Transport: mock}} - resp, err := cl.GetPublicKeys(context.Background(), "example.com/api/publickeys") - assert.Equal(t, err.Error(), "failed to decode from Hex from the following public key index locations: 0, 1, 2, ") - assert.Nil(t, resp) + require.Equal(t, "[0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820]", fmt.Sprintf("%v", resp)) } // TODO: not really in use, should be revisited diff --git a/validator/keymanager/remote-web3signer/keymanager.go b/validator/keymanager/remote-web3signer/keymanager.go index c213abbb00..0da752d728 100644 --- a/validator/keymanager/remote-web3signer/keymanager.go +++ b/validator/keymanager/remote-web3signer/keymanager.go @@ -1,13 +1,19 @@ package remote_web3signer import ( - "bytes" + "bufio" "context" "encoding/json" "fmt" + "os" "path/filepath" + "slices" + "strings" + "sync" + "time" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/fsnotify/fsnotify" "github.com/go-playground/validator/v10" "github.com/logrusorgru/aurora" "github.com/pkg/errors" @@ -15,18 +21,27 @@ import ( fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/crypto/bls" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v5/io/file" validatorpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1/validator-client" "github.com/prysmaticlabs/prysm/v5/validator/accounts/petnames" "github.com/prysmaticlabs/prysm/v5/validator/keymanager" "github.com/prysmaticlabs/prysm/v5/validator/keymanager/remote-web3signer/internal" web3signerv1 "github.com/prysmaticlabs/prysm/v5/validator/keymanager/remote-web3signer/v1" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" + "go.opencensus.io/trace" + "golang.org/x/exp/maps" +) + +const ( + maxRetries = 60 + retryDelay = 10 * time.Second ) // SetupConfig includes configuration values for initializing. // a keymanager, such as passwords, the wallet, and more. // Web3Signer contains one public keys option. Either through a URL or a static key list. type SetupConfig struct { + KeyFilePath string BaseEndpoint string GenesisValidatorsRoot []byte @@ -38,22 +53,26 @@ type SetupConfig struct { // Either URL or keylist must be set. // a static list of public keys to be passed by the user to determine what accounts should sign. // This will provide a layer of safety against slashing if the web3signer is shared across validators. - ProvidedPublicKeys [][48]byte + ProvidedPublicKeys []string } // Keymanager defines the web3signer keymanager. type Keymanager struct { client internal.HttpSignerClient genesisValidatorsRoot []byte - publicKeysURL string - providedPublicKeys [][48]byte + providedPublicKeys [][48]byte // (source of truth) flag loaded + file loaded + api loaded keys + flagLoadedKeysMap map[string][48]byte // stores what was provided from flag ( as opposed to from file ) accountsChangedFeed *event.Feed validator *validator.Validate - publicKeysUrlCalled bool + retriesRemaining int + keyFilePath string + lock sync.RWMutex } // NewKeymanager instantiates a new web3signer key manager. -func NewKeymanager(_ context.Context, cfg *SetupConfig) (*Keymanager, error) { +func NewKeymanager(ctx context.Context, cfg *SetupConfig) (*Keymanager, error) { + ctx, span := trace.StartSpan(ctx, "remote-keymanager.NewKeymanager") + defer span.End() if cfg.BaseEndpoint == "" || !bytesutil.IsValidRoot(cfg.GenesisValidatorsRoot) { return nil, fmt.Errorf("invalid setup config, one or more configs are empty: BaseEndpoint: %v, GenesisValidatorsRoot: %#x", cfg.BaseEndpoint, cfg.GenesisValidatorsRoot) } @@ -61,31 +80,297 @@ func NewKeymanager(_ context.Context, cfg *SetupConfig) (*Keymanager, error) { if err != nil { return nil, errors.Wrap(err, "could not create apiClient") } - return &Keymanager{ + + km := &Keymanager{ client: internal.HttpSignerClient(client), genesisValidatorsRoot: cfg.GenesisValidatorsRoot, accountsChangedFeed: new(event.Feed), - publicKeysURL: cfg.PublicKeysURL, - providedPublicKeys: cfg.ProvidedPublicKeys, validator: validator.New(), - publicKeysUrlCalled: false, - }, nil + retriesRemaining: maxRetries, + keyFilePath: cfg.KeyFilePath, + } + + keyFileExists := false + if km.keyFilePath != "" { + keyFileExists, err = file.Exists(km.keyFilePath, file.Regular) + if err != nil { + return nil, errors.Wrapf(err, "could not check if remote signer persistent keys exist in %s", km.keyFilePath) + } + if !keyFileExists { + return nil, fmt.Errorf("no file exists in remote signer key file path %s", km.keyFilePath) + } + } + + var ppk []string + // load key values + if cfg.PublicKeysURL != "" { + providedPublicKeys, err := km.client.GetPublicKeys(ctx, cfg.PublicKeysURL) + if err != nil { + erroredResponsesTotal.Inc() + return nil, errors.Wrapf(err, "could not get public keys from remote server URL %v", cfg.PublicKeysURL) + } + ppk = providedPublicKeys + } else if len(cfg.ProvidedPublicKeys) != 0 { + ppk = cfg.ProvidedPublicKeys + } + + // use a map to remove duplicates + flagLoadedKeys := make(map[string][48]byte) + + // Populate the map with existing keys + for _, key := range ppk { + decodedKey, err := hexutil.Decode(key) + if err != nil { + return nil, errors.Wrapf(err, "could not decode public key %s", key) + } + if len(decodedKey) != fieldparams.BLSPubkeyLength { + return nil, fmt.Errorf("public key %s has invalid length (expected %d, got %d)", decodedKey, fieldparams.BLSPubkeyLength, len(decodedKey)) + } + flagLoadedKeys[key] = bytesutil.ToBytes48(decodedKey) + } + km.flagLoadedKeysMap = flagLoadedKeys + + // load from file + if keyFileExists { + log.WithField("file", km.keyFilePath).Info("Loading keys from file") + _, fileKeys, err := km.readKeyFile() + if err != nil { + return nil, errors.Wrap(err, "could not read key file") + } + if len(flagLoadedKeys) != 0 { + log.WithField("flagLoadedKeyCount", len(flagLoadedKeys)).WithField("fileLoadedKeyCount", len(fileKeys)).Info("Combining flag loaded keys and file loaded keys.") + maps.Copy(fileKeys, flagLoadedKeys) + if err = km.savePublicKeysToFile(fileKeys); err != nil { + return nil, errors.Wrap(err, "could not save public keys to file") + } + } + km.lock.Lock() + km.providedPublicKeys = maps.Values(fileKeys) + km.lock.Unlock() + // create a file watcher + go func() { + err = km.refreshRemoteKeysFromFileChangesWithRetry(ctx, retryDelay) + if err != nil { + log.WithError(err).Error("Could not refresh remote keys from file changes") + } + }() + } else { + km.lock.Lock() + km.providedPublicKeys = maps.Values(flagLoadedKeys) + km.lock.Unlock() + } + + return km, nil +} + +func (km *Keymanager) refreshRemoteKeysFromFileChangesWithRetry(ctx context.Context, retryDelay time.Duration) error { + if ctx.Err() != nil { + return ctx.Err() + } + if km.retriesRemaining == 0 { + return errors.New("file check retries remaining exceeded") + } + err := km.refreshRemoteKeysFromFileChanges(ctx) + if err != nil { + km.updatePublicKeys(maps.Values(km.flagLoadedKeysMap)) // update the keys to flag provided defaults + km.retriesRemaining-- + log.WithError(err).Debug("Error occurred on key refresh") + log.WithFields(logrus.Fields{"path": km.keyFilePath, "retriesRemaining": km.retriesRemaining, "retryDelay": retryDelay}).Warnf("Could not refresh keys. Retrying...") + time.Sleep(retryDelay) + return km.refreshRemoteKeysFromFileChangesWithRetry(ctx, retryDelay) + } + return nil +} + +func (km *Keymanager) readKeyFile() ([][48]byte, map[string][48]byte, error) { + km.lock.RLock() + defer km.lock.RUnlock() + + if km.keyFilePath == "" { + return nil, nil, errors.New("no key file path provided") + } + f, err := os.Open(filepath.Clean(km.keyFilePath)) + if err != nil { + return nil, nil, errors.Wrap(err, "could not open remote signer public key file") + } + defer func() { + if err := f.Close(); err != nil { + log.WithError(err).Error("Could not close remote signer public key file") + } + }() + // Use a map to track and skip duplicate lines + seenKeys := make(map[string][48]byte) + scanner := bufio.NewScanner(f) + var keys [][48]byte + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + pubkeyLength := (fieldparams.BLSPubkeyLength * 2) + 2 + if line == "" { + // skip empty line + continue + } + // allow for pubkeys without the 0x + if len(line) == pubkeyLength-2 && !strings.HasPrefix(line, "0x") { + line = "0x" + line + } + if len(line) != pubkeyLength { + log.WithFields(logrus.Fields{ + "filepath": km.keyFilePath, + "key": line, + }).Error("Invalid public key in remote signer key file") + continue + } + if _, found := seenKeys[line]; !found { + // If it's a new line, mark it as seen and process it + pubkey, err := hexutil.Decode(line) + if err != nil { + return nil, nil, errors.Wrapf(err, "could not decode public key %s in remote signer key file", line) + } + bPubkey := bytesutil.ToBytes48(pubkey) + seenKeys[line] = bPubkey + keys = append(keys, bPubkey) + } + } + // Check for scanning errors + if err := scanner.Err(); err != nil { + return nil, nil, errors.Wrap(err, "could not scan remote signer public key file") + } + if len(keys) == 0 { + log.Warn("Remote signer key file: no valid public keys found. Defaulting to flag provided keys if any exist.") + } + return keys, seenKeys, nil +} + +func (km *Keymanager) savePublicKeysToFile(providedPublicKeys map[string][48]byte) error { + if km.keyFilePath == "" { + return errors.New("no key file provided") + } + pubkeys := make([][48]byte, 0) + // Open the file with write and truncate permissions + f, err := os.OpenFile(km.keyFilePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer func(f *os.File) { + err := f.Close() + if err != nil { + log.WithError(err).Error("Could not close file, proceeding without closing the file") + } + }(f) + + // Iterate through all lines in the slice and write them to the file + for key, value := range providedPublicKeys { + if _, err := f.WriteString(key + "\n"); err != nil { + return fmt.Errorf("error writing key %s to file: %w", value, err) + } + pubkeys = append(pubkeys, value) + } + km.updatePublicKeys(pubkeys) + return nil +} + +func (km *Keymanager) arePublicKeysEmpty() bool { + km.lock.RLock() + defer km.lock.RUnlock() + return len(km.providedPublicKeys) == 0 +} + +func (km *Keymanager) refreshRemoteKeysFromFileChanges(ctx context.Context) error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return errors.Wrap(err, "could not initialize file watcher") + } + defer func() { + if err := watcher.Close(); err != nil { + log.WithError(err).Error("Could not close file watcher") + } + }() + initialFileInfo, err := os.Stat(km.keyFilePath) + if err != nil { + return errors.Wrap(err, "could not stat remote signer public key file") + } + initialFileSize := initialFileInfo.Size() + if err := watcher.Add(km.keyFilePath); err != nil { + return errors.Wrap(err, "could not add file to file watcher") + } + log.WithField("path", km.keyFilePath).Info("Successfully initialized file watcher") + km.retriesRemaining = maxRetries // reset retries to default + // reinitialize keys if watcher reinitialized + if km.arePublicKeysEmpty() { + _, fk, err := km.readKeyFile() + if err != nil { + return errors.Wrap(err, "could not read key file") + } + maps.Copy(fk, km.flagLoadedKeysMap) + if err = km.savePublicKeysToFile(fk); err != nil { + return errors.Wrap(err, "could not save public keys to file") + } + km.updatePublicKeys(maps.Values(fk)) + } + for { + select { + case e, ok := <-watcher.Events: + if !ok { // Channel was closed (i.e. Watcher.Close() was called). + log.Info("Closing file watcher") + return nil + } + log.WithFields(logrus.Fields{ + "event": e.Name, + "op": e.Op.String(), + }).Debug("Remote signer key file event triggered") + if e.Has(fsnotify.Remove) { + return errors.New("remote signer key file was removed") + } + currentFileInfo, err := os.Stat(km.keyFilePath) + if err != nil { + return errors.Wrap(err, "could not stat remote signer public key file") + } + if currentFileInfo.Size() != initialFileSize { + log.Info("Remote signer key file updated") + fileKeys, _, err := km.readKeyFile() + if err != nil { + return errors.New("could not read key file") + } + // prioritize file keys over flag keys + if len(fileKeys) == 0 { + log.Warnln("Remote signer key file no longer has keys, defaulting to flag provided keys") + fileKeys = maps.Values(km.flagLoadedKeysMap) + } + currentKeys, err := km.FetchValidatingPublicKeys(ctx) + if err != nil { + return errors.Wrap(err, "could not fetch current keys") + } + if !slices.Equal(currentKeys, fileKeys) { + km.updatePublicKeys(fileKeys) + } + initialFileSize = currentFileInfo.Size() + } + case err, ok := <-watcher.Errors: + if !ok { // Channel was closed (i.e. Watcher.Close() was called). + log.Info("Closing file watcher") + return nil + } + return errors.Wrap(err, "could not watch for file changes") + case <-ctx.Done(): + log.Info("Closing file watcher") + return nil + } + } +} + +func (km *Keymanager) updatePublicKeys(keys [][48]byte) { + km.lock.Lock() + defer km.lock.Unlock() + km.providedPublicKeys = keys + km.accountsChangedFeed.Send(keys) + log.WithField("count", len(km.providedPublicKeys)).Debug("Updated public keys") } // FetchValidatingPublicKeys fetches the validating public keys -// from the remote server or from the provided keys if there are no existing public keys set -// or provides the existing keys in the keymanager. -func (km *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][fieldparams.BLSPubkeyLength]byte, error) { - if km.publicKeysURL != "" && !km.publicKeysUrlCalled { - providedPublicKeys, err := km.client.GetPublicKeys(ctx, km.publicKeysURL) - if err != nil { - erroredResponsesTotal.Inc() - return nil, errors.Wrap(err, fmt.Sprintf("could not get public keys from remote server url: %v", km.publicKeysURL)) - } - // makes sure that if the public keys are deleted the validator does not call URL again. - km.publicKeysUrlCalled = true - km.providedPublicKeys = providedPublicKeys - } +func (km *Keymanager) FetchValidatingPublicKeys(_ context.Context) ([][fieldparams.BLSPubkeyLength]byte, error) { + km.lock.RLock() + defer km.lock.RUnlock() + log.WithField("count", len(km.providedPublicKeys)).Debug("Fetched validating public keys") return km.providedPublicKeys, nil } @@ -96,10 +381,14 @@ func (km *Keymanager) Sign(ctx context.Context, request *validatorpb.SignRequest erroredResponsesTotal.Inc() return nil, err } - + signature, err := km.client.Sign(ctx, hexutil.Encode(request.PublicKey), signRequest) + if err != nil { + erroredResponsesTotal.Inc() + return nil, errors.Wrap(err, "failed to sign the request") + } + log.WithField("publicKey", request.PublicKey).Debug("Successfully signed the request") signRequestsTotal.Inc() - - return km.client.Sign(ctx, hexutil.Encode(request.PublicKey), signRequest) + return signature, nil } // getSignRequestJson returns a json request based on the SignRequest type. @@ -420,10 +709,21 @@ func DisplayRemotePublicKeys(validatingPubKeys [][48]byte) { } // AddPublicKeys imports a list of public keys into the keymanager for web3signer use. Returns status with message. -func (km *Keymanager) AddPublicKeys(pubKeys []string) []*keymanager.KeyStatus { +func (km *Keymanager) AddPublicKeys(pubKeys []string) ([]*keymanager.KeyStatus, error) { importedRemoteKeysStatuses := make([]*keymanager.KeyStatus, len(pubKeys)) + // Using a map to track both existing and new public keys efficiently + combinedKeys := make(map[string][48]byte) + + // Populate the map with existing keys + km.lock.RLock() + originalKeysLen := len(km.providedPublicKeys) + for _, key := range km.providedPublicKeys { + encodedKey := hexutil.Encode(key[:]) + combinedKeys[encodedKey] = key + } + km.lock.RUnlock() + for i, pubkey := range pubKeys { - found := false pubkeyBytes, err := hexutil.Decode(pubkey) if err != nil { importedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ @@ -439,76 +739,103 @@ func (km *Keymanager) AddPublicKeys(pubKeys []string) []*keymanager.KeyStatus { } continue } - for _, key := range km.providedPublicKeys { - if bytes.Equal(key[:], pubkeyBytes) { - found = true - break - } - } - if found { + + encodedPubkey := hexutil.Encode(pubkeyBytes) + if _, exists := combinedKeys[encodedPubkey]; exists { importedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ Status: keymanager.StatusDuplicate, Message: fmt.Sprintf("Duplicate pubkey: %v, already in use", pubkey), } continue } - km.providedPublicKeys = append(km.providedPublicKeys, bytesutil.ToBytes48(pubkeyBytes)) + + // Add the new key to the map + combinedKeys[encodedPubkey] = bytesutil.ToBytes48(pubkeyBytes) importedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ Status: keymanager.StatusImported, Message: fmt.Sprintf("Successfully added pubkey: %v", pubkey), } log.Debug("Added pubkey to keymanager for web3signer", "pubkey", pubkey) } - km.accountsChangedFeed.Send(km.providedPublicKeys) - return importedRemoteKeysStatuses + + if originalKeysLen != len(combinedKeys) { + if km.keyFilePath != "" { + if err := km.savePublicKeysToFile(combinedKeys); err != nil { + return nil, err + } + } else { + km.updatePublicKeys(maps.Values(combinedKeys)) + } + } + + return importedRemoteKeysStatuses, nil } // DeletePublicKeys removes a list of public keys from the keymanager for web3signer use. Returns status with message. -func (km *Keymanager) DeletePublicKeys(pubKeys []string) []*keymanager.KeyStatus { - deletedRemoteKeysStatuses := make([]*keymanager.KeyStatus, len(pubKeys)) - if len(km.providedPublicKeys) == 0 { +func (km *Keymanager) DeletePublicKeys(publicKeys []string) ([]*keymanager.KeyStatus, error) { + deletedRemoteKeysStatuses := make([]*keymanager.KeyStatus, len(publicKeys)) + // Using a map to track both existing and new public keys efficiently + combinedKeys := make(map[string][48]byte) + km.lock.RLock() + originalKeysLen := len(km.providedPublicKeys) + if originalKeysLen == 0 { for i := range deletedRemoteKeysStatuses { deletedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ Status: keymanager.StatusNotFound, Message: "No pubkeys are set in validator", } } - return deletedRemoteKeysStatuses + return deletedRemoteKeysStatuses, nil } - for i, pubkey := range pubKeys { - for in, key := range km.providedPublicKeys { - pubkeyBytes, err := hexutil.Decode(pubkey) - if err != nil { - deletedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ - Status: keymanager.StatusError, - Message: err.Error(), - } - continue - } - if len(pubkeyBytes) != fieldparams.BLSPubkeyLength { - deletedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ - Status: keymanager.StatusError, - Message: fmt.Sprintf("pubkey byte length (%d) did not match bls pubkey byte length (%d)", len(pubkeyBytes), fieldparams.BLSPubkeyLength), - } - continue - } - if bytes.Equal(key[:], pubkeyBytes) { - km.providedPublicKeys = append(km.providedPublicKeys[:in], km.providedPublicKeys[in+1:]...) - deletedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ - Status: keymanager.StatusDeleted, - Message: fmt.Sprintf("Successfully deleted pubkey: %v", pubkey), - } - log.Debug("Deleted pubkey from keymanager for web3signer", "pubkey", pubkey) - break + + // Populate the map with existing keys + for _, key := range km.providedPublicKeys { + encodedKey := hexutil.Encode(key[:]) + combinedKeys[encodedKey] = key + } + km.lock.RUnlock() + + for i, pubkey := range publicKeys { + pubkeyBytes, err := hexutil.Decode(pubkey) + if err != nil { + deletedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ + Status: keymanager.StatusError, + Message: err.Error(), } + continue } - if deletedRemoteKeysStatuses[i] == nil { + if len(pubkeyBytes) != fieldparams.BLSPubkeyLength { + deletedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ + Status: keymanager.StatusError, + Message: fmt.Sprintf("pubkey byte length (%d) did not match bls pubkey byte length (%d)", len(pubkeyBytes), fieldparams.BLSPubkeyLength), + } + continue + } + _, exists := combinedKeys[pubkey] + if !exists { deletedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ Status: keymanager.StatusNotFound, Message: fmt.Sprintf("Pubkey: %v not found", pubkey), } + continue + } + delete(combinedKeys, pubkey) + deletedRemoteKeysStatuses[i] = &keymanager.KeyStatus{ + Status: keymanager.StatusDeleted, + Message: fmt.Sprintf("Successfully deleted pubkey: %v", pubkey), + } + log.WithField("pubkey", pubkey).Debug("Deleted pubkey from keymanager for remote signer") + } + + if originalKeysLen != len(combinedKeys) { + if km.keyFilePath != "" { + if err := km.savePublicKeysToFile(combinedKeys); err != nil { + return nil, err + } + } else { + km.updatePublicKeys(maps.Values(combinedKeys)) } } - km.accountsChangedFeed.Send(km.providedPublicKeys) - return deletedRemoteKeysStatuses + + return deletedRemoteKeysStatuses, nil } diff --git a/validator/keymanager/remote-web3signer/keymanager_test.go b/validator/keymanager/remote-web3signer/keymanager_test.go index aee8a4e515..9d47eda753 100644 --- a/validator/keymanager/remote-web3signer/keymanager_test.go +++ b/validator/keymanager/remote-web3signer/keymanager_test.go @@ -1,20 +1,29 @@ package remote_web3signer import ( + "bytes" "context" - "encoding/hex" + "encoding/json" "fmt" - "strings" + "net/http" + "net/http/httptest" + "os" + "path" + "path/filepath" + "slices" "testing" + "time" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/prysmaticlabs/prysm/v5/crypto/bls" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v5/io/file" validatorpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1/validator-client" "github.com/prysmaticlabs/prysm/v5/testing/require" "github.com/prysmaticlabs/prysm/v5/validator/keymanager" "github.com/prysmaticlabs/prysm/v5/validator/keymanager/remote-web3signer/internal" "github.com/prysmaticlabs/prysm/v5/validator/keymanager/remote-web3signer/v1/mock" + logTest "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" ) @@ -31,19 +40,319 @@ func (mc *MockClient) Sign(_ context.Context, _ string, _ internal.SignRequestJs } return bls.SignatureFromBytes(decoded) } -func (mc *MockClient) GetPublicKeys(_ context.Context, _ string) ([][48]byte, error) { - var keys [][48]byte - for _, pk := range mc.PublicKeys { - decoded, err := hex.DecodeString(strings.TrimPrefix(pk, "0x")) - if err != nil { - return nil, err - } - keys = append(keys, bytesutil.ToBytes48(decoded)) +func (mc *MockClient) GetPublicKeys(_ context.Context, _ string) ([]string, error) { + return mc.PublicKeys, nil +} + +func TestNewKeymanager(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode([]string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}) + require.NoError(t, err) + })) + root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") + if err != nil { + fmt.Printf("error: %v", err) } - if mc.isThrowingError { - return nil, fmt.Errorf("mock error") + tests := []struct { + name string + args *SetupConfig + fileContents []string + want []string + wantErr string + wantLog string + }{ + { + name: "happy path public key url", + args: &SetupConfig{ + BaseEndpoint: "http://prysm.xyz/", + GenesisValidatorsRoot: root, + PublicKeysURL: srv.URL + "/public_keys", + }, + want: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}, + }, + { + name: "bad public key url", + args: &SetupConfig{ + BaseEndpoint: "http://prysm.xyz/", + GenesisValidatorsRoot: root, + PublicKeysURL: "0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69", + }, + wantErr: "could not get public keys from remote server URL", + }, + { + name: "happy path provided public keys", + args: &SetupConfig{ + BaseEndpoint: "http://prysm.xyz/", + GenesisValidatorsRoot: root, + ProvidedPublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}, + }, + want: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}, + }, + { + name: "path provided public keys, some bad key", + args: &SetupConfig{ + BaseEndpoint: "http://prysm.xyz/", + GenesisValidatorsRoot: root, + ProvidedPublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820", "http://prysm.xyz/"}, + }, + wantErr: "could not decode public key", + }, + { + name: "path provided public keys, some bad hex for key", + args: &SetupConfig{ + BaseEndpoint: "http://prysm.xyz/", + GenesisValidatorsRoot: root, + ProvidedPublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937"}, + }, + wantErr: "has invalid length", + }, + { + name: "happy path key file", + args: &SetupConfig{ + BaseEndpoint: "http://prysm.xyz/", + GenesisValidatorsRoot: root, + KeyFilePath: filepath.Join(t.TempDir(), "good_keyfile.txt"), + }, + fileContents: []string{"8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055", "0x800057e262bfe42413c2cfce948ff77f11efeea19721f590c8b5b2f32fecb0e164cafba987c80465878408d05b97c9be"}, + want: []string{"0x8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055", "0x800057e262bfe42413c2cfce948ff77f11efeea19721f590c8b5b2f32fecb0e164cafba987c80465878408d05b97c9be"}, + }, + { + name: "happy path public key url with good keyfile", + args: &SetupConfig{ + BaseEndpoint: "http://prysm.xyz/", + GenesisValidatorsRoot: root, + PublicKeysURL: srv.URL + "/public_keys", + KeyFilePath: filepath.Join(t.TempDir(), "good_keyfile.txt"), + }, + fileContents: []string{"0x8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055", "800057e262bfe42413c2cfce948ff77f11efeea19721f590c8b5b2f32fecb0e164cafba987c80465878408d05b97c9be"}, + want: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820", "0x8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055", "0x800057e262bfe42413c2cfce948ff77f11efeea19721f590c8b5b2f32fecb0e164cafba987c80465878408d05b97c9be"}, + }, + { + name: "happy path provided public keys with good keyfile", + args: &SetupConfig{ + BaseEndpoint: "http://prysm.xyz/", + GenesisValidatorsRoot: root, + ProvidedPublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}, + }, + want: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820", "0x8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055", "0x800057e262bfe42413c2cfce948ff77f11efeea19721f590c8b5b2f32fecb0e164cafba987c80465878408d05b97c9be"}, + }, } - return keys, nil + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logHook := logTest.NewGlobal() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + if tt.args.KeyFilePath != "" && len(tt.fileContents) != 0 { + bytesBuf := new(bytes.Buffer) + for _, content := range tt.fileContents { + _, err := bytesBuf.WriteString(content) // test without 0x + require.NoError(t, err) + _, err = bytesBuf.WriteString("\n") + require.NoError(t, err) + } + err = file.WriteFile(tt.args.KeyFilePath, bytesBuf.Bytes()) + require.NoError(t, err) + } + + km, err := NewKeymanager(ctx, tt.args) + if tt.wantLog != "" { + require.LogsContain(t, logHook, tt.wantLog) + } + if tt.wantErr != "" { + require.ErrorContains(t, tt.wantErr, err) + return + } + keys := make([]string, len(km.providedPublicKeys)) + for i, key := range km.providedPublicKeys { + keys[i] = hexutil.Encode(key[:]) + require.Equal(t, true, slices.Contains(tt.want, keys[i])) + } + }) + } +} + +func TestNewKeyManager_fileMissing(t *testing.T) { + keyFilePath := filepath.Join(t.TempDir(), "keyfile.txt") + root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") + require.NoError(t, err) + _, err = NewKeymanager(context.TODO(), &SetupConfig{ + BaseEndpoint: "http://example.com", + GenesisValidatorsRoot: root, + KeyFilePath: keyFilePath, + ProvidedPublicKeys: []string{"0x800077e04f8d7496099b3d30ac5430aea64873a45e5bcfe004d2095babcbf55e21138ff0d5691abc29da190aa32755c6"}, + }) + require.ErrorContains(t, "no file exists in remote signer key file path", err) +} + +func TestNewKeyManager_ChangingFileCreated(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + keyFilePath := filepath.Join(t.TempDir(), "keyfile.txt") + bytesBuf := new(bytes.Buffer) + _, err := bytesBuf.WriteString("8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055") // test without 0x + require.NoError(t, err) + _, err = bytesBuf.WriteString("\n") + require.NoError(t, err) + _, err = bytesBuf.WriteString("0x800057e262bfe42413c2cfce948ff77f11efeea19721f590c8b5b2f32fecb0e164cafba987c80465878408d05b97c9be") + require.NoError(t, err) + _, err = bytesBuf.WriteString("\n") + require.NoError(t, err) + err = file.WriteFile(keyFilePath, bytesBuf.Bytes()) + require.NoError(t, err) + + root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") + require.NoError(t, err) + km, err := NewKeymanager(ctx, &SetupConfig{ + BaseEndpoint: "http://example.com", + GenesisValidatorsRoot: root, + KeyFilePath: keyFilePath, + ProvidedPublicKeys: []string{"0x800077e04f8d7496099b3d30ac5430aea64873a45e5bcfe004d2095babcbf55e21138ff0d5691abc29da190aa32755c6"}, + }) + require.NoError(t, err) + wantSlice := []string{"0x800077e04f8d7496099b3d30ac5430aea64873a45e5bcfe004d2095babcbf55e21138ff0d5691abc29da190aa32755c6", "0x8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055", "0x800057e262bfe42413c2cfce948ff77f11efeea19721f590c8b5b2f32fecb0e164cafba987c80465878408d05b97c9be"} + keys := make([]string, len(km.providedPublicKeys)) + require.Equal(t, 3, len(km.providedPublicKeys)) + for i, key := range km.providedPublicKeys { + keys[i] = hexutil.Encode(key[:]) + require.Equal(t, slices.Contains(wantSlice, keys[i]), true) + } + // sleep needs to be at the front because of how watching the file works + time.Sleep(1 * time.Second) + + // Open the file for writing, create it if it does not exist, and truncate it if it does. + f, err := os.OpenFile(keyFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + require.NoError(t, err) + + // Write the buffer's contents to the file. + _, err = f.WriteString("0x8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055") + require.NoError(t, err) + require.NoError(t, f.Sync()) + require.NoError(t, f.Close()) + + ks, _, err := km.readKeyFile() + require.NoError(t, err) + require.Equal(t, 1, len(ks)) + require.Equal(t, "0x8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055", hexutil.Encode(ks[0][:])) + + require.Equal(t, 1, len(km.providedPublicKeys)) + require.Equal(t, "0x8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055", hexutil.Encode(km.providedPublicKeys[0][:])) +} + +func TestNewKeyManager_FileAndFlagsWithDifferentKeys(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + logHook := logTest.NewGlobal() + keyFilePath := filepath.Join(t.TempDir(), "keyfile.txt") + bytesBuf := new(bytes.Buffer) + _, err := bytesBuf.WriteString("8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055") // test without 0x + require.NoError(t, err) + err = file.WriteFile(keyFilePath, bytesBuf.Bytes()) + require.NoError(t, err) + + root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") + require.NoError(t, err) + km, err := NewKeymanager(ctx, &SetupConfig{ + BaseEndpoint: "http://example.com", + GenesisValidatorsRoot: root, + KeyFilePath: keyFilePath, + ProvidedPublicKeys: []string{"0x800077e04f8d7496099b3d30ac5430aea64873a45e5bcfe004d2095babcbf55e21138ff0d5691abc29da190aa32755c6"}, + }) + require.NoError(t, err) + wantSlice := []string{"0x800077e04f8d7496099b3d30ac5430aea64873a45e5bcfe004d2095babcbf55e21138ff0d5691abc29da190aa32755c6", + "0x8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055"} + // provided public keys are saved to the file + keys, _, err := km.readKeyFile() + require.NoError(t, err) + for _, key := range keys { + require.Equal(t, slices.Contains(wantSlice, hexutil.Encode(key[:])), true) + } + // wait for reading to be done + time.Sleep(2 * time.Second) + // test fall back by clearing file + go func() { + err = file.WriteFile(keyFilePath, []byte(" ")) + require.NoError(t, err) + }() + // waiting for writing to be done + time.Sleep(2 * time.Second) + require.LogsContain(t, logHook, "Remote signer key file no longer has keys, defaulting to flag provided keys") + + // fall back to flag provided keys + keys, err = km.FetchValidatingPublicKeys(context.TODO()) + require.NoError(t, err) + require.Equal(t, 1, len(keys)) + require.Equal(t, "0x800077e04f8d7496099b3d30ac5430aea64873a45e5bcfe004d2095babcbf55e21138ff0d5691abc29da190aa32755c6", hexutil.Encode(keys[0][:])) +} + +func TestRefreshRemoteKeysFromFileChangesWithRetry(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + logHook := logTest.NewGlobal() + root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") + require.NoError(t, err) + keyFilePath := filepath.Join(t.TempDir(), "keyfile.txt") + + require.NoError(t, err) + km, err := NewKeymanager(ctx, &SetupConfig{ + BaseEndpoint: "http://example.com", + GenesisValidatorsRoot: root, + }) + require.NoError(t, err) + go func() { + km.keyFilePath = keyFilePath + require.NoError(t, km.refreshRemoteKeysFromFileChangesWithRetry(ctx, 1*time.Second)) + }() + // wait for file detection + time.Sleep(1 * time.Second) + require.LogsContain(t, logHook, "Could not refresh keys") + go func() { + bytesBuf := new(bytes.Buffer) + _, err = bytesBuf.WriteString("8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055") // test without 0x + require.NoError(t, err) + err = file.WriteFile(keyFilePath, bytesBuf.Bytes()) + require.NoError(t, err) + }() + // wait for file write to reinitialize + time.Sleep(2 * time.Second) + cancel() + require.LogsContain(t, logHook, "Successfully initialized file watcher") + keys, err := km.FetchValidatingPublicKeys(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(keys)) +} + +func TestReadKeyFile_PathMissing(t *testing.T) { + root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") + require.NoError(t, err) + + require.NoError(t, err) + km, err := NewKeymanager(context.TODO(), &SetupConfig{ + BaseEndpoint: "http://example.com", + GenesisValidatorsRoot: root, + }) + require.NoError(t, err) + _, _, err = km.readKeyFile() + require.ErrorContains(t, "no key file path provided", err) +} + +func TestRefreshRemoteKeysFromFileChangesWithRetry_maxRetryReached(t *testing.T) { + ctx := context.Background() + root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") + require.NoError(t, err) + keyFilePath := filepath.Join(t.TempDir(), "keyfile.txt") + + require.NoError(t, err) + km, err := NewKeymanager(ctx, &SetupConfig{ + BaseEndpoint: "http://example.com", + GenesisValidatorsRoot: root, + }) + require.NoError(t, err) + km.keyFilePath = keyFilePath + km.retriesRemaining = 1 + err = km.refreshRemoteKeysFromFileChangesWithRetry(ctx, 1*time.Millisecond) + require.ErrorContains(t, "file check retries remaining exceeded", err) } func TestKeymanager_Sign(t *testing.T) { @@ -187,20 +496,16 @@ func TestKeymanager_Sign(t *testing.T) { func TestKeymanager_FetchValidatingPublicKeys_HappyPath_WithKeyList(t *testing.T) { ctx := context.Background() decodedKey, err := hexutil.Decode("0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820") - if err != nil { - fmt.Printf("error: %v", err) - } + require.NoError(t, err) keys := [][48]byte{ bytesutil.ToBytes48(decodedKey), } root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") - if err != nil { - fmt.Printf("error: %v", err) - } + require.NoError(t, err) config := &SetupConfig{ BaseEndpoint: "http://example.com", GenesisValidatorsRoot: root, - ProvidedPublicKeys: keys, + ProvidedPublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}, } km, err := NewKeymanager(ctx, config) if err != nil { @@ -217,9 +522,6 @@ func TestKeymanager_FetchValidatingPublicKeys_HappyPath_WithKeyList(t *testing.T func TestKeymanager_FetchValidatingPublicKeys_HappyPath_WithExternalURL(t *testing.T) { ctx := context.Background() - client := &MockClient{ - PublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}, - } decodedKey, err := hexutil.Decode("0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820") if err != nil { fmt.Printf("error: %v", err) @@ -231,31 +533,37 @@ func TestKeymanager_FetchValidatingPublicKeys_HappyPath_WithExternalURL(t *testi if err != nil { fmt.Printf("error: %v", err) } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode([]string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}) + require.NoError(t, err) + })) + defer srv.Close() config := &SetupConfig{ BaseEndpoint: "http://example.com", GenesisValidatorsRoot: root, - PublicKeysURL: "http://example2.com/api/v1/eth2/publicKeys", + PublicKeysURL: srv.URL + "/api/v1/eth2/publicKeys", } km, err := NewKeymanager(ctx, config) if err != nil { fmt.Printf("error: %v", err) } - km.client = client resp, err := km.FetchValidatingPublicKeys(ctx) - if err != nil { - fmt.Printf("error: %v", err) - } + require.NoError(t, err) assert.NotNil(t, resp) - assert.Nil(t, err) assert.EqualValues(t, resp, keys) } func TestKeymanager_FetchValidatingPublicKeys_WithExternalURL_ThrowsError(t *testing.T) { ctx := context.Background() - client := &MockClient{ - PublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}, - isThrowingError: true, - } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + http.Error(w, "mock error", http.StatusInternalServerError) + })) + defer srv.Close() + root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") if err != nil { fmt.Printf("error: %v", err) @@ -263,17 +571,11 @@ func TestKeymanager_FetchValidatingPublicKeys_WithExternalURL_ThrowsError(t *tes config := &SetupConfig{ BaseEndpoint: "http://example.com", GenesisValidatorsRoot: root, - PublicKeysURL: "http://example2.com/api/v1/eth2/publicKeys", + PublicKeysURL: srv.URL + "/api/v1/eth2/publicKeys", } km, err := NewKeymanager(ctx, config) - if err != nil { - fmt.Printf("error: %v", err) - } - km.client = client - resp, err := km.FetchValidatingPublicKeys(ctx) - assert.NotNil(t, err) - assert.Nil(t, resp) - assert.Equal(t, "could not get public keys from remote server url: http://example2.com/api/v1/eth2/publicKeys: mock error", fmt.Sprintf("%v", err)) + require.ErrorContains(t, fmt.Sprintf("could not get public keys from remote server URL %s/api/v1/eth2/publicKeys", srv.URL), err) + assert.Nil(t, km) } func TestKeymanager_AddPublicKeys(t *testing.T) { @@ -291,16 +593,56 @@ func TestKeymanager_AddPublicKeys(t *testing.T) { fmt.Printf("error: %v", err) } publicKeys := []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"} - statuses := km.AddPublicKeys(publicKeys) + statuses, err := km.AddPublicKeys(publicKeys) + require.NoError(t, err) for _, status := range statuses { require.Equal(t, keymanager.StatusImported, status.Status) } - statuses = km.AddPublicKeys(publicKeys) + statuses, err = km.AddPublicKeys(publicKeys) + require.NoError(t, err) for _, status := range statuses { require.Equal(t, keymanager.StatusDuplicate, status.Status) } } +func TestKeymanager_AddPublicKeys_WithFile(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + dir := t.TempDir() + stdOutFile, err := os.Create(filepath.Clean(path.Join(dir, "keyfile.txt"))) + require.NoError(t, err) + require.NoError(t, stdOutFile.Chmod(os.FileMode(0600))) + keyFilePath := filepath.Join(dir, "keyfile.txt") + root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") + if err != nil { + fmt.Printf("error: %v", err) + } + config := &SetupConfig{ + BaseEndpoint: "http://example.com", + GenesisValidatorsRoot: root, + KeyFilePath: keyFilePath, + } + km, err := NewKeymanager(ctx, config) + if err != nil { + fmt.Printf("error: %v", err) + } + publicKeys := []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"} + statuses, err := km.AddPublicKeys(publicKeys) + require.NoError(t, err) + for _, status := range statuses { + require.Equal(t, keymanager.StatusImported, status.Status) + } + statuses, err = km.AddPublicKeys(publicKeys) + require.NoError(t, err) + for _, status := range statuses { + require.Equal(t, keymanager.StatusDuplicate, status.Status) + } + keys, _, err := km.readKeyFile() + require.NoError(t, err) + require.Equal(t, len(keys), len(publicKeys)) + require.Equal(t, hexutil.Encode(keys[0][:]), publicKeys[0]) +} + func TestKeymanager_DeletePublicKeys(t *testing.T) { ctx := context.Background() root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") @@ -316,18 +658,66 @@ func TestKeymanager_DeletePublicKeys(t *testing.T) { fmt.Printf("error: %v", err) } publicKeys := []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"} - statuses := km.AddPublicKeys(publicKeys) + statuses, err := km.AddPublicKeys(publicKeys) + require.NoError(t, err) for _, status := range statuses { require.Equal(t, keymanager.StatusImported, status.Status) } - s := km.DeletePublicKeys(publicKeys) + s, err := km.DeletePublicKeys(publicKeys) + require.NoError(t, err) for _, status := range s { require.Equal(t, keymanager.StatusDeleted, status.Status) } - s = km.DeletePublicKeys(publicKeys) + s, err = km.DeletePublicKeys(publicKeys) + require.NoError(t, err) for _, status := range s { require.Equal(t, keymanager.StatusNotFound, status.Status) } } + +func TestKeymanager_DeletePublicKeys_WithFile(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + dir := t.TempDir() + stdOutFile, err := os.Create(filepath.Clean(path.Join(dir, "keyfile.txt"))) + require.NoError(t, err) + require.NoError(t, stdOutFile.Chmod(os.FileMode(0600))) + keyFilePath := filepath.Join(dir, "keyfile.txt") + root, err := hexutil.Decode("0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69") + if err != nil { + fmt.Printf("error: %v", err) + } + config := &SetupConfig{ + BaseEndpoint: "http://example.com", + GenesisValidatorsRoot: root, + KeyFilePath: keyFilePath, + } + km, err := NewKeymanager(ctx, config) + if err != nil { + fmt.Printf("error: %v", err) + } + publicKeys := []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820", "0x8000a9a6d3f5e22d783eefaadbcf0298146adb5d95b04db910a0d4e16976b30229d0b1e7b9cda6c7e0bfa11f72efe055"} + statuses, err := km.AddPublicKeys(publicKeys) + require.NoError(t, err) + for _, status := range statuses { + require.Equal(t, keymanager.StatusImported, status.Status) + } + + s, err := km.DeletePublicKeys([]string{publicKeys[0]}) + require.NoError(t, err) + for _, status := range s { + require.Equal(t, keymanager.StatusDeleted, status.Status) + } + + s, err = km.DeletePublicKeys([]string{publicKeys[0]}) + require.NoError(t, err) + for _, status := range s { + require.Equal(t, keymanager.StatusNotFound, status.Status) + } + keys, _, err := km.readKeyFile() + require.NoError(t, err) + require.Equal(t, len(keys), 1) + require.Equal(t, hexutil.Encode(keys[0][:]), publicKeys[1]) +} diff --git a/validator/keymanager/remote-web3signer/log.go b/validator/keymanager/remote-web3signer/log.go new file mode 100644 index 0000000000..f579bdb7ae --- /dev/null +++ b/validator/keymanager/remote-web3signer/log.go @@ -0,0 +1,5 @@ +package remote_web3signer + +import "github.com/sirupsen/logrus" + +var log = logrus.WithField("prefix", "remote-keymanager") diff --git a/validator/keymanager/types.go b/validator/keymanager/types.go index f778708247..986e9b2e07 100644 --- a/validator/keymanager/types.go +++ b/validator/keymanager/types.go @@ -61,7 +61,7 @@ type KeyStoreExtractor interface { // PublicKeyAdder allows adding public keys to the keymanager. type PublicKeyAdder interface { - AddPublicKeys(publicKeys []string) []*KeyStatus + AddPublicKeys(publicKeys []string) ([]*KeyStatus, error) } // KeyStatus is a json representation of the status fields for the keymanager apis @@ -85,7 +85,7 @@ const ( // PublicKeyDeleter allows deleting public keys set in keymanager. type PublicKeyDeleter interface { - DeletePublicKeys(publicKeys []string) []*KeyStatus + DeletePublicKeys(publicKeys []string) ([]*KeyStatus, error) } type ListKeymanagerAccountConfig struct { diff --git a/validator/node/BUILD.bazel b/validator/node/BUILD.bazel index 2a76b823f0..253255212a 100644 --- a/validator/node/BUILD.bazel +++ b/validator/node/BUILD.bazel @@ -8,7 +8,6 @@ go_test( deps = [ "//cmd:go_default_library", "//cmd/validator/flags:go_default_library", - "//encoding/bytesutil:go_default_library", "//io/file:go_default_library", "//testing/assert:go_default_library", "//testing/require:go_default_library", @@ -17,7 +16,6 @@ go_test( "//validator/db/kv:go_default_library", "//validator/keymanager:go_default_library", "//validator/keymanager/remote-web3signer:go_default_library", - "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", ], @@ -45,8 +43,6 @@ go_library( "//config/params:go_default_library", "//config/proposer:go_default_library", "//config/proposer/loader:go_default_library", - "//container/slice:go_default_library", - "//encoding/bytesutil:go_default_library", "//io/file:go_default_library", "//monitoring/backup:go_default_library", "//monitoring/prometheus:go_default_library", @@ -67,7 +63,6 @@ go_library( "//validator/keymanager/remote-web3signer:go_default_library", "//validator/rpc:go_default_library", "//validator/web:go_default_library", - "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_gorilla_mux//:go_default_library", "@com_github_grpc_ecosystem_grpc_gateway_v2//runtime:go_default_library", "@com_github_pkg_errors//:go_default_library", diff --git a/validator/node/node.go b/validator/node/node.go index e2a0233616..ed80038020 100644 --- a/validator/node/node.go +++ b/validator/node/node.go @@ -18,7 +18,6 @@ import ( "syscall" "time" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/gorilla/mux" gwruntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/pkg/errors" @@ -32,8 +31,6 @@ import ( "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/config/proposer" "github.com/prysmaticlabs/prysm/v5/config/proposer/loader" - "github.com/prysmaticlabs/prysm/v5/container/slice" - "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" "github.com/prysmaticlabs/prysm/v5/io/file" "github.com/prysmaticlabs/prysm/v5/monitoring/backup" "github.com/prysmaticlabs/prysm/v5/monitoring/prometheus" @@ -259,7 +256,7 @@ func (c *ValidatorClient) initializeFromCLI(cliCtx *cli.Context, router *mux.Rou if !isInteropNumValidatorsSet { // Custom Check For Web3Signer if isWeb3SignerURLFlagSet { - c.wallet = wallet.NewWalletForWeb3Signer() + c.wallet = wallet.NewWalletForWeb3Signer(cliCtx) } else { w, err := wallet.OpenWalletOrElseCli(cliCtx, func(cliCtx *cli.Context) (*wallet.Wallet, error) { return nil, wallet.ErrNoWalletFound @@ -302,7 +299,7 @@ func (c *ValidatorClient) initializeFromCLI(cliCtx *cli.Context, router *mux.Rou func (c *ValidatorClient) initializeForWeb(cliCtx *cli.Context, router *mux.Router) error { if cliCtx.IsSet(flags.Web3SignerURLFlag.Name) { // Custom Check For Web3Signer - c.wallet = wallet.NewWalletForWeb3Signer() + c.wallet = wallet.NewWalletForWeb3Signer(cliCtx) } else { // Read the wallet password file from the cli context. if err := setWalletPasswordFilePath(cliCtx); err != nil { @@ -565,30 +562,20 @@ func Web3SignerConfig(cliCtx *cli.Context) (*remoteweb3signer.SetupConfig, error } if publicKeysSlice := cliCtx.StringSlice(flags.Web3SignerPublicValidatorKeysFlag.Name); len(publicKeysSlice) > 0 { - pks := make([]string, 0) if len(publicKeysSlice) == 1 { pURL, err := url.ParseRequestURI(publicKeysSlice[0]) if err == nil && pURL.Scheme != "" && pURL.Host != "" { web3signerConfig.PublicKeysURL = publicKeysSlice[0] } else { - pks = strings.Split(publicKeysSlice[0], ",") + web3signerConfig.ProvidedPublicKeys = strings.Split(publicKeysSlice[0], ",") } - } else if len(publicKeysSlice) > 1 { - pks = publicKeysSlice - } - if len(pks) > 0 { - pks = slice.Unique[string](pks) - var validatorKeys [][48]byte - for _, key := range pks { - decodedKey, decodeErr := hexutil.Decode(key) - if decodeErr != nil { - return nil, errors.Wrapf(decodeErr, "could not decode public key for web3signer: %s", key) - } - validatorKeys = append(validatorKeys, bytesutil.ToBytes48(decodedKey)) - } - web3signerConfig.ProvidedPublicKeys = validatorKeys + } else { + web3signerConfig.ProvidedPublicKeys = publicKeysSlice } } + if cliCtx.IsSet(flags.Web3SignerKeyFileFlag.Name) { + web3signerConfig.KeyFilePath = cliCtx.String(flags.Web3SignerKeyFileFlag.Name) + } } return web3signerConfig, nil } diff --git a/validator/node/node_test.go b/validator/node/node_test.go index 2b70339d2d..257714c0da 100644 --- a/validator/node/node_test.go +++ b/validator/node/node_test.go @@ -9,10 +9,8 @@ import ( "path/filepath" "testing" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/prysmaticlabs/prysm/v5/cmd" "github.com/prysmaticlabs/prysm/v5/cmd/validator/flags" - "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" "github.com/prysmaticlabs/prysm/v5/io/file" "github.com/prysmaticlabs/prysm/v5/testing/assert" "github.com/prysmaticlabs/prysm/v5/testing/require" @@ -211,17 +209,10 @@ func TestClearDB(t *testing.T) { // TestWeb3SignerConfig tests the web3 signer config returns the correct values. func TestWeb3SignerConfig(t *testing.T) { - pubkey1decoded, err := hexutil.Decode("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c") - require.NoError(t, err) - bytepubkey1 := bytesutil.ToBytes48(pubkey1decoded) - - pubkey2decoded, err := hexutil.Decode("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b") - require.NoError(t, err) - bytepubkey2 := bytesutil.ToBytes48(pubkey2decoded) - type args struct { baseURL string publicKeysOrURLs []string + persistentFile string } tests := []struct { name string @@ -240,9 +231,9 @@ func TestWeb3SignerConfig(t *testing.T) { BaseEndpoint: "http://localhost:8545", GenesisValidatorsRoot: nil, PublicKeysURL: "", - ProvidedPublicKeys: [][48]byte{ - bytepubkey1, - bytepubkey2, + ProvidedPublicKeys: []string{ + "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", }, }, }, @@ -269,25 +260,6 @@ func TestWeb3SignerConfig(t *testing.T) { want: nil, wantErrMsg: "web3signer url 0xa99a76ed7796f7be22d5b7e85deeb7c5677e88, is invalid: parse \"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88,\": invalid URI for request", }, - { - name: "Bad publicKeys", - args: &args{ - baseURL: "http://localhost:8545", - publicKeysOrURLs: []string{"0xa99a76ed7796f7be22c," + - "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b"}, - }, - want: nil, - wantErrMsg: "could not decode public key for web3signer: 0xa99a76ed7796f7be22c: hex string of odd length", - }, - { - name: "Bad publicKeysURL", - args: &args{ - baseURL: "http://localhost:8545", - publicKeysOrURLs: []string{"localhost"}, - }, - want: nil, - wantErrMsg: "could not decode public key for web3signer: localhost: hex string without 0x prefix", - }, { name: "Base URL missing scheme or host", args: &args{ @@ -298,23 +270,15 @@ func TestWeb3SignerConfig(t *testing.T) { wantErrMsg: "web3signer url must be in the format of http(s)://host:port url used: localhost:8545", }, { - name: "Public Keys URL missing scheme or host", + name: "happy path with persistentFile", args: &args{ - baseURL: "http://localhost:8545", - publicKeysOrURLs: []string{"localhost:8545"}, + baseURL: "http://localhost:8545", + persistentFile: "/remote/key/file.txt", }, - want: nil, - wantErrMsg: "could not decode public key for web3signer: localhost:8545: hex string without 0x prefix", - }, - { - name: "incorrect amount of flag calls used with url", - args: &args{ - baseURL: "http://localhost:8545", - publicKeysOrURLs: []string{"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c," + - "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", "http://localhost:8545/api/v1/eth2/publicKeys"}, + want: &remoteweb3signer.SetupConfig{ + BaseEndpoint: "http://localhost:8545", + KeyFilePath: "/remote/key/file.txt", }, - want: nil, - wantErrMsg: "could not decode public key for web3signer", }, } for _, tt := range tests { @@ -322,6 +286,7 @@ func TestWeb3SignerConfig(t *testing.T) { app := cli.App{} set := flag.NewFlagSet(tt.name, 0) set.String("validators-external-signer-url", tt.args.baseURL, "baseUrl") + set.String(flags.Web3SignerKeyFileFlag.Name, "", "") c := &cli.StringSliceFlag{ Name: "validators-external-signer-public-keys", } @@ -331,6 +296,9 @@ func TestWeb3SignerConfig(t *testing.T) { for _, key := range tt.args.publicKeysOrURLs { require.NoError(t, set.Set(flags.Web3SignerPublicValidatorKeysFlag.Name, key)) } + if tt.args.persistentFile != "" { + require.NoError(t, set.Set(flags.Web3SignerKeyFileFlag.Name, tt.args.persistentFile)) + } cliCtx := cli.NewContext(&app, set, nil) got, err := Web3SignerConfig(cliCtx) if tt.wantErrMsg != "" { diff --git a/validator/rpc/BUILD.bazel b/validator/rpc/BUILD.bazel index 5c07cd7a9c..96701e719c 100644 --- a/validator/rpc/BUILD.bazel +++ b/validator/rpc/BUILD.bazel @@ -148,6 +148,7 @@ go_test( "@com_github_pkg_errors//:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@com_github_tyler_smith_go_bip39//:go_default_library", + "@com_github_urfave_cli_v2//:go_default_library", "@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library", "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//metadata:go_default_library", diff --git a/validator/rpc/handlers_keymanager.go b/validator/rpc/handlers_keymanager.go index f5d1c9aeba..236945a248 100644 --- a/validator/rpc/handlers_keymanager.go +++ b/validator/rpc/handlers_keymanager.go @@ -484,7 +484,12 @@ func (s *Server) ImportRemoteKeys(w http.ResponseWriter, r *http.Request) { log.Warnf("Setting the remote signer base url within the request is not supported. The remote signer url can only be set from the --%s flag.", flags.Web3SignerURLFlag.Name) } - httputil.WriteJson(w, &RemoteKeysResponse{Data: adder.AddPublicKeys(remoteKeys)}) + ks, err := adder.AddPublicKeys(remoteKeys) + if err != nil { + httputil.HandleError(w, err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteJson(w, &RemoteKeysResponse{Data: ks}) } // DeleteRemoteKeys deletes a list of public keys defined for web3signer keymanager type. @@ -533,8 +538,12 @@ func (s *Server) DeleteRemoteKeys(w http.ResponseWriter, r *http.Request) { httputil.WriteJson(w, &RemoteKeysResponse{Data: statuses}) return } - - httputil.WriteJson(w, RemoteKeysResponse{Data: deleter.DeletePublicKeys(req.Pubkeys)}) + data, err := deleter.DeletePublicKeys(req.Pubkeys) + if err != nil { + httputil.HandleError(w, err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteJson(w, RemoteKeysResponse{Data: data}) } // ListFeeRecipientByPubkey returns the public key to eth address mapping object to the end user. diff --git a/validator/rpc/handlers_keymanager_test.go b/validator/rpc/handlers_keymanager_test.go index 330340c374..4cb58217df 100644 --- a/validator/rpc/handlers_keymanager_test.go +++ b/validator/rpc/handlers_keymanager_test.go @@ -4,9 +4,12 @@ import ( "bytes" "context" "encoding/json" + "flag" "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" "time" @@ -15,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/gorilla/mux" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/prysmaticlabs/prysm/v5/cmd/validator/flags" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/config/proposer" @@ -40,6 +44,7 @@ import ( remoteweb3signer "github.com/prysmaticlabs/prysm/v5/validator/keymanager/remote-web3signer" "github.com/prysmaticlabs/prysm/v5/validator/slashing-protection-history/format" mocks "github.com/prysmaticlabs/prysm/v5/validator/testing" + "github.com/urfave/cli/v2" "go.uber.org/mock/gomock" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" @@ -342,13 +347,18 @@ func TestServer_ImportKeystores(t *testing.T) { func TestServer_ImportKeystores_WrongKeymanagerKind(t *testing.T) { ctx := context.Background() - w := wallet.NewWalletForWeb3Signer() + app := cli.App{} + set := flag.NewFlagSet("test", 0) + newDir := filepath.Join(t.TempDir(), "new") + require.NoError(t, os.MkdirAll(newDir, 0700)) + set.String(flags.WalletDirFlag.Name, newDir, "") + w := wallet.NewWalletForWeb3Signer(cli.NewContext(&app, set, nil)) root := make([]byte, fieldparams.RootLength) root[0] = 1 km, err := w.InitializeKeymanager(ctx, iface.InitKeymanagerConfig{ListenForChanges: false, Web3SignerConfig: &remoteweb3signer.SetupConfig{ BaseEndpoint: "http://example.com", GenesisValidatorsRoot: root, - PublicKeysURL: "http://example.com/public_keys", + ProvidedPublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}, }}) require.NoError(t, err) vs, err := client.NewValidatorService(ctx, &client.Config{ @@ -620,14 +630,19 @@ func TestServer_DeleteKeystores_FailedSlashingProtectionExport(t *testing.T) { func TestServer_DeleteKeystores_WrongKeymanagerKind(t *testing.T) { ctx := context.Background() - w := wallet.NewWalletForWeb3Signer() + app := cli.App{} + set := flag.NewFlagSet("test", 0) + newDir := filepath.Join(t.TempDir(), "new") + require.NoError(t, os.MkdirAll(newDir, 0700)) + set.String(flags.WalletDirFlag.Name, newDir, "") + w := wallet.NewWalletForWeb3Signer(cli.NewContext(&app, set, nil)) root := make([]byte, fieldparams.RootLength) root[0] = 1 km, err := w.InitializeKeymanager(ctx, iface.InitKeymanagerConfig{ListenForChanges: false, Web3SignerConfig: &remoteweb3signer.SetupConfig{ BaseEndpoint: "http://example.com", GenesisValidatorsRoot: root, - PublicKeysURL: "http://example.com/public_keys", + ProvidedPublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"}, }}) require.NoError(t, err) vs, err := client.NewValidatorService(ctx, &client.Config{ @@ -1312,16 +1327,17 @@ func TestServer_DeleteGasLimit(t *testing.T) { func TestServer_ListRemoteKeys(t *testing.T) { ctx := context.Background() - w := wallet.NewWalletForWeb3Signer() + app := cli.App{} + set := flag.NewFlagSet("test", 0) + newDir := filepath.Join(t.TempDir(), "new") + set.String(flags.WalletDirFlag.Name, newDir, "") + w := wallet.NewWalletForWeb3Signer(cli.NewContext(&app, set, nil)) root := make([]byte, fieldparams.RootLength) root[0] = 1 - bytevalue, err := hexutil.Decode("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a") - require.NoError(t, err) - pubkeys := [][fieldparams.BLSPubkeyLength]byte{bytesutil.ToBytes48(bytevalue)} config := &remoteweb3signer.SetupConfig{ BaseEndpoint: "http://example.com", GenesisValidatorsRoot: root, - ProvidedPublicKeys: pubkeys, + ProvidedPublicKeys: []string{"0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"}, } km, err := w.InitializeKeymanager(ctx, iface.InitKeymanagerConfig{ListenForChanges: false, Web3SignerConfig: config}) require.NoError(t, err) @@ -1357,7 +1373,11 @@ func TestServer_ListRemoteKeys(t *testing.T) { func TestServer_ImportRemoteKeys(t *testing.T) { ctx := context.Background() - w := wallet.NewWalletForWeb3Signer() + app := cli.App{} + set := flag.NewFlagSet("test", 0) + newDir := filepath.Join(t.TempDir(), "new") + set.String(flags.WalletDirFlag.Name, newDir, "") + w := wallet.NewWalletForWeb3Signer(cli.NewContext(&app, set, nil)) root := make([]byte, fieldparams.RootLength) root[0] = 1 config := &remoteweb3signer.SetupConfig{ @@ -1414,17 +1434,18 @@ func TestServer_ImportRemoteKeys(t *testing.T) { func TestServer_DeleteRemoteKeys(t *testing.T) { ctx := context.Background() - w := wallet.NewWalletForWeb3Signer() + app := cli.App{} + set := flag.NewFlagSet("test", 0) + newDir := filepath.Join(t.TempDir(), "new") + set.String(flags.WalletDirFlag.Name, newDir, "") + w := wallet.NewWalletForWeb3Signer(cli.NewContext(&app, set, nil)) root := make([]byte, fieldparams.RootLength) root[0] = 1 pkey := "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" - bytevalue, err := hexutil.Decode(pkey) - require.NoError(t, err) - pubkeys := [][fieldparams.BLSPubkeyLength]byte{bytesutil.ToBytes48(bytevalue)} config := &remoteweb3signer.SetupConfig{ BaseEndpoint: "http://example.com", GenesisValidatorsRoot: root, - ProvidedPublicKeys: pubkeys, + ProvidedPublicKeys: []string{pkey}, } km, err := w.InitializeKeymanager(ctx, iface.InitKeymanagerConfig{ListenForChanges: false, Web3SignerConfig: config}) require.NoError(t, err)