diff --git a/cmd/validator/accounts/backup_test.go b/cmd/validator/accounts/backup_test.go index 7c3fd167bc..5d1d5714cc 100644 --- a/cmd/validator/accounts/backup_test.go +++ b/cmd/validator/accounts/backup_test.go @@ -67,7 +67,7 @@ func TestBackupAccounts_Noninteractive_Derived(t *testing.T) { // Create 2 accounts derivedKM, ok := km.(*derived.Keymanager) require.Equal(t, true, ok) - err = derivedKM.RecoverAccountsFromMnemonic(cliCtx.Context, constant.TestMnemonic, "", 2) + err = derivedKM.RecoverAccountsFromMnemonic(cliCtx.Context, constant.TestMnemonic, "", "", 2) require.NoError(t, err) // Obtain the public keys of the accounts we created diff --git a/cmd/validator/flags/flags.go b/cmd/validator/flags/flags.go index d500d37406..f61ff1140f 100644 --- a/cmd/validator/flags/flags.go +++ b/cmd/validator/flags/flags.go @@ -162,6 +162,11 @@ var ( Name: "mnemonic-file", Usage: "File to retrieve mnemonic for non-interactively passing a mnemonic phrase into wallet recover.", } + // MnemonicLanguageFlag is used to specify the language of the mnemonic. + MnemonicLanguageFlag = &cli.StringFlag{ + Name: "mnemonic-language", + Usage: "Allows specifying mnemonic language. Supported languages are: english|chinese_traditional|chinese_simplified|czech|french|japanese|korean|italian|spanish", + } // ShowDepositDataFlag for accounts. ShowDepositDataFlag = &cli.BoolFlag{ Name: "show-deposit-data", diff --git a/cmd/validator/wallet/create.go b/cmd/validator/wallet/create.go index 3d6ebabf11..03b4c199e8 100644 --- a/cmd/validator/wallet/create.go +++ b/cmd/validator/wallet/create.go @@ -77,6 +77,7 @@ func ConstructCLIManagerOpts(cliCtx *cli.Context, keymanagerKind keymanager.Kind cliOpts = append(cliOpts, accounts.WithWalletPassword(walletPassword)) cliOpts = append(cliOpts, accounts.WithKeymanagerType(keymanagerKind)) cliOpts = append(cliOpts, accounts.WithSkipMnemonicConfirm(cliCtx.Bool(flags.SkipDepositConfirmationFlag.Name))) + cliOpts = append(cliOpts, accounts.WithMnemonicLanguage(cliCtx.String(flags.MnemonicLanguageFlag.Name))) skipMnemonic25thWord := cliCtx.IsSet(flags.SkipMnemonic25thWordCheckFlag.Name) has25thWordFile := cliCtx.IsSet(flags.Mnemonic25thWordFileFlag.Name) diff --git a/validator/accounts/accounts_list_test.go b/validator/accounts/accounts_list_test.go index 8712f17fe6..7a5bc4ea68 100644 --- a/validator/accounts/accounts_list_test.go +++ b/validator/accounts/accounts_list_test.go @@ -349,7 +349,7 @@ func TestListAccounts_DerivedKeymanager(t *testing.T) { require.NoError(t, err) numAccounts := 5 - err = km.RecoverAccountsFromMnemonic(cliCtx.Context, constant.TestMnemonic, "", numAccounts) + err = km.RecoverAccountsFromMnemonic(cliCtx.Context, constant.TestMnemonic, "", "", numAccounts) require.NoError(t, err) rescueStdout := os.Stdout diff --git a/validator/accounts/cli_manager.go b/validator/accounts/cli_manager.go index 4766b46b28..ae603efaf1 100644 --- a/validator/accounts/cli_manager.go +++ b/validator/accounts/cli_manager.go @@ -45,6 +45,7 @@ type AccountsCLIManager struct { privateKeyFile string passwordFilePath string keysDir string + mnemonicLanguage string backupsDir string backupsPassword string filteredPubKeys []bls.PublicKey diff --git a/validator/accounts/cli_options.go b/validator/accounts/cli_options.go index 6563400cac..850f980d8d 100644 --- a/validator/accounts/cli_options.go +++ b/validator/accounts/cli_options.go @@ -131,6 +131,14 @@ func WithSkipMnemonicConfirm(s bool) Option { } } +// WithMnemonicLanguage specifies the language used for the mnemonic passphrase. +func WithMnemonicLanguage(mnemonicLanguage string) Option { + return func(acc *AccountsCLIManager) error { + acc.mnemonicLanguage = mnemonicLanguage + return nil + } +} + // WithPrivateKeyFile specifies the private key path. func WithPrivateKeyFile(privateKeyFile string) Option { return func(acc *AccountsCLIManager) error { diff --git a/validator/accounts/wallet_create.go b/validator/accounts/wallet_create.go index 9404d6b146..36a1eaba07 100644 --- a/validator/accounts/wallet_create.go +++ b/validator/accounts/wallet_create.go @@ -55,6 +55,7 @@ func (acm *AccountsCLIManager) WalletCreate(ctx context.Context) (*wallet.Wallet ctx, w, acm.mnemonic25thWord, + acm.mnemonicLanguage, acm.skipMnemonicConfirm, acm.numAccounts, ); err != nil { @@ -92,6 +93,7 @@ func createDerivedKeymanagerWallet( ctx context.Context, wallet *wallet.Wallet, mnemonicPassphrase string, + mnemonicLanguage string, skipMnemonicConfirm bool, numAccounts int, ) error { @@ -108,11 +110,11 @@ func createDerivedKeymanagerWallet( if err != nil { return errors.Wrap(err, "could not initialize HD keymanager") } - mnemonic, err := derived.GenerateAndConfirmMnemonic(skipMnemonicConfirm) + mnemonic, err := derived.GenerateAndConfirmMnemonic(mnemonicLanguage, skipMnemonicConfirm) if err != nil { return errors.Wrap(err, "could not confirm mnemonic") } - if err := km.RecoverAccountsFromMnemonic(ctx, mnemonic, mnemonicPassphrase, numAccounts); err != nil { + if err := km.RecoverAccountsFromMnemonic(ctx, mnemonic, mnemonicLanguage, mnemonicPassphrase, numAccounts); err != nil { return errors.Wrap(err, "could not recover accounts from mnemonic") } return nil diff --git a/validator/accounts/wallet_recover.go b/validator/accounts/wallet_recover.go index c753910985..ce8ac18259 100644 --- a/validator/accounts/wallet_recover.go +++ b/validator/accounts/wallet_recover.go @@ -45,7 +45,7 @@ func (acm *AccountsCLIManager) WalletRecover(ctx context.Context) (*wallet.Walle if err != nil { return nil, errors.Wrap(err, "could not make keymanager for given phrase") } - if err := km.RecoverAccountsFromMnemonic(ctx, acm.mnemonic, acm.mnemonic25thWord, acm.numAccounts); err != nil { + if err := km.RecoverAccountsFromMnemonic(ctx, acm.mnemonic, acm.mnemonicLanguage, acm.mnemonic25thWord, acm.numAccounts); err != nil { return nil, err } log.WithField("wallet-path", w.AccountsDir()).Infof( diff --git a/validator/client/wait_for_activation_test.go b/validator/client/wait_for_activation_test.go index 4cca04ccdc..9866ae7bb3 100644 --- a/validator/client/wait_for_activation_test.go +++ b/validator/client/wait_for_activation_test.go @@ -345,7 +345,7 @@ func TestWaitForActivation_AccountsChanged(t *testing.T) { ListenForChanges: true, }) require.NoError(t, err) - err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", 1) + err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", "", 1) require.NoError(t, err) validatorClient := mock.NewMockBeaconNodeValidatorClient(ctrl) beaconClient := mock.NewMockBeaconChainClient(ctrl) @@ -390,7 +390,7 @@ func TestWaitForActivation_AccountsChanged(t *testing.T) { go func() { // We add the active key into the keymanager and simulate a key refresh. time.Sleep(time.Second * 1) - err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", 2) + err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", "", 2) require.NoError(t, err) channel <- [][fieldparams.BLSPubkeyLength]byte{} }() diff --git a/validator/keymanager/derived/BUILD.bazel b/validator/keymanager/derived/BUILD.bazel index 9448f52020..413430c8c1 100644 --- a/validator/keymanager/derived/BUILD.bazel +++ b/validator/keymanager/derived/BUILD.bazel @@ -28,6 +28,7 @@ go_library( "@com_github_pkg_errors//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_tyler_smith_go_bip39//:go_default_library", + "@com_github_tyler_smith_go_bip39//wordlists:go_default_library", "@com_github_wealdtech_go_eth2_util//:go_default_library", ], ) @@ -51,6 +52,7 @@ go_test( "//validator/testing:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_tyler_smith_go_bip39//:go_default_library", + "@com_github_tyler_smith_go_bip39//wordlists:go_default_library", "@com_github_wealdtech_go_eth2_util//:go_default_library", ], ) diff --git a/validator/keymanager/derived/eip_test.go b/validator/keymanager/derived/eip_test.go index 2032720d8a..e6d73cdcde 100644 --- a/validator/keymanager/derived/eip_test.go +++ b/validator/keymanager/derived/eip_test.go @@ -16,13 +16,14 @@ import ( func TestDerivationFromMnemonic(t *testing.T) { mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" passphrase := "TREZOR" + lang := "english" seed := "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04" masterSK := "6083874454709270928345386274498605044986640685124978867557563392430687146096" childIndex := 0 childSK := "20397789859736650942317412262472558107875392172444076792671091975210932703118" seedBytes, err := hex.DecodeString(seed) require.NoError(t, err) - derivedSeed, err := seedFromMnemonic(mnemonic, passphrase) + derivedSeed, err := seedFromMnemonic(mnemonic, lang, passphrase) require.NoError(t, err) assert.DeepEqual(t, seedBytes, derivedSeed) diff --git a/validator/keymanager/derived/keymanager.go b/validator/keymanager/derived/keymanager.go index 694548f1e2..b1f22ba899 100644 --- a/validator/keymanager/derived/keymanager.go +++ b/validator/keymanager/derived/keymanager.go @@ -59,9 +59,9 @@ func NewKeymanager( // from a derived seed, encrypt them according to the EIP-2334 JSON standard, and write them // to disk. Then, the mnemonic is never stored nor used by the validator. func (km *Keymanager) RecoverAccountsFromMnemonic( - ctx context.Context, mnemonic, mnemonicPassphrase string, numAccounts int, + ctx context.Context, mnemonic, mnemonicLanguage, mnemonicPassphrase string, numAccounts int, ) error { - seed, err := seedFromMnemonic(mnemonic, mnemonicPassphrase) + seed, err := seedFromMnemonic(mnemonic, mnemonicLanguage, mnemonicPassphrase) if err != nil { return errors.Wrap(err, "could not initialize new wallet seed file") } diff --git a/validator/keymanager/derived/keymanager_test.go b/validator/keymanager/derived/keymanager_test.go index 2308a8945d..ba1de1d5fb 100644 --- a/validator/keymanager/derived/keymanager_test.go +++ b/validator/keymanager/derived/keymanager_test.go @@ -36,7 +36,7 @@ func TestDerivedKeymanager_MnemnonicPassphrase_DifferentResults(t *testing.T) { }) require.NoError(t, err) numAccounts := 5 - err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "mnemonicpass", numAccounts) + err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", "mnemonicpass", numAccounts) require.NoError(t, err) without25thWord, err := km.FetchValidatingPublicKeys(ctx) require.NoError(t, err) @@ -51,7 +51,7 @@ func TestDerivedKeymanager_MnemnonicPassphrase_DifferentResults(t *testing.T) { }) require.NoError(t, err) // No mnemonic passphrase this time. - err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts) + err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", "", numAccounts) require.NoError(t, err) with25thWord, err := km.FetchValidatingPublicKeys(ctx) require.NoError(t, err) @@ -70,14 +70,14 @@ func TestDerivedKeymanager_RecoverSeedRoundTrip(t *testing.T) { require.NoError(t, err) wanted := bip39.NewSeed(mnemonic, "") - got, err := seedFromMnemonic(mnemonic, "" /* no passphrase */) + got, err := seedFromMnemonic(mnemonic, "", "" /* no passphrase */) require.NoError(t, err) // Ensure the derived seed matches. assert.DeepEqual(t, wanted, got) } func TestDerivedKeymanager_FetchValidatingPublicKeys(t *testing.T) { - derivedSeed, err := seedFromMnemonic(constant.TestMnemonic, "") + derivedSeed, err := seedFromMnemonic(constant.TestMnemonic, "", "") require.NoError(t, err) wallet := &mock.Wallet{ Files: make(map[string]map[string][]byte), @@ -91,7 +91,7 @@ func TestDerivedKeymanager_FetchValidatingPublicKeys(t *testing.T) { }) require.NoError(t, err) numAccounts := 5 - err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts) + err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", "", numAccounts) require.NoError(t, err) // Fetch the public keys. @@ -116,7 +116,7 @@ func TestDerivedKeymanager_FetchValidatingPublicKeys(t *testing.T) { } func TestDerivedKeymanager_FetchValidatingPrivateKeys(t *testing.T) { - derivedSeed, err := seedFromMnemonic(constant.TestMnemonic, "") + derivedSeed, err := seedFromMnemonic(constant.TestMnemonic, "", "") require.NoError(t, err) wallet := &mock.Wallet{ Files: make(map[string]map[string][]byte), @@ -130,7 +130,7 @@ func TestDerivedKeymanager_FetchValidatingPrivateKeys(t *testing.T) { }) require.NoError(t, err) numAccounts := 5 - err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts) + err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", "", numAccounts) require.NoError(t, err) // Fetch the private keys. @@ -167,7 +167,7 @@ func TestDerivedKeymanager_Sign(t *testing.T) { }) require.NoError(t, err) numAccounts := 5 - err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts) + err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", "", numAccounts) require.NoError(t, err) pubKeys, err := dr.FetchValidatingPublicKeys(ctx) diff --git a/validator/keymanager/derived/mnemonic.go b/validator/keymanager/derived/mnemonic.go index 4fb76e7a98..dc77e09c39 100644 --- a/validator/keymanager/derived/mnemonic.go +++ b/validator/keymanager/derived/mnemonic.go @@ -8,26 +8,26 @@ import ( "github.com/prysmaticlabs/prysm/v3/crypto/rand" "github.com/prysmaticlabs/prysm/v3/io/prompt" "github.com/tyler-smith/go-bip39" + "github.com/tyler-smith/go-bip39/wordlists" ) const confirmationText = "Confirm you have written down the recovery words somewhere safe (offline) [y|Y]" -// EnglishMnemonicGenerator implements methods for creating +// MnemonicGenerator implements methods for creating // mnemonic seed phrases in english using a given // source of entropy such as a private key. -type EnglishMnemonicGenerator struct { +type MnemonicGenerator struct { skipMnemonicConfirm bool } // GenerateAndConfirmMnemonic requires confirming the generated mnemonics. -func GenerateAndConfirmMnemonic( - skipMnemonicConfirm bool, -) (string, error) { +func GenerateAndConfirmMnemonic(mnemonicLanguage string, skipMnemonicConfirm bool) (string, error) { mnemonicRandomness := make([]byte, 32) if _, err := rand.NewGenerator().Read(mnemonicRandomness); err != nil { return "", errors.Wrap(err, "could not initialize mnemonic source of randomness") } - m := &EnglishMnemonicGenerator{ + setBip39Lang(mnemonicLanguage) + m := &MnemonicGenerator{ skipMnemonicConfirm: skipMnemonicConfirm, } phrase, err := m.Generate(mnemonicRandomness) @@ -42,13 +42,13 @@ func GenerateAndConfirmMnemonic( // Generate a mnemonic seed phrase in english using a source of // entropy given as raw bytes. -func (_ *EnglishMnemonicGenerator) Generate(data []byte) (string, error) { +func (_ *MnemonicGenerator) Generate(data []byte) (string, error) { return bip39.NewMnemonic(data) } // ConfirmAcknowledgement displays the mnemonic phrase to the user // and confirms the user has written down the phrase securely offline. -func (m *EnglishMnemonicGenerator) ConfirmAcknowledgement(phrase string) error { +func (m *MnemonicGenerator) ConfirmAcknowledgement(phrase string) error { log.Info( "Write down the sentence below, as it is your only " + "means of recovering your wallet", @@ -74,9 +74,30 @@ func (m *EnglishMnemonicGenerator) ConfirmAcknowledgement(phrase string) error { // Uses the provided mnemonic seed phrase to generate the // appropriate seed file for recovering a derived wallets. -func seedFromMnemonic(mnemonic, mnemonicPassphrase string) ([]byte, error) { +func seedFromMnemonic(mnemonic, mnemonicLanguage, mnemonicPassphrase string) ([]byte, error) { + setBip39Lang(mnemonicLanguage) if ok := bip39.IsMnemonicValid(mnemonic); !ok { return nil, bip39.ErrInvalidMnemonic } return bip39.NewSeed(mnemonic, mnemonicPassphrase), nil } + +func setBip39Lang(lang string) { + wordlist := wordlists.English + allowedLanguages := map[string][]string{ + "chinese_simplified": wordlists.ChineseSimplified, + "chinese_traditional": wordlists.ChineseTraditional, + "czech": wordlists.Czech, + "english": wordlists.English, + "french": wordlists.French, + "japanese": wordlists.Japanese, + "korean": wordlists.Korean, + "italian": wordlists.Italian, + "spanish": wordlists.Spanish, + } + + if wl, ok := allowedLanguages[lang]; ok { + wordlist = wl + } + bip39.SetWordList(wordlist) +} diff --git a/validator/keymanager/derived/mnemonic_test.go b/validator/keymanager/derived/mnemonic_test.go index 5a6ba0aeca..93f6291f42 100644 --- a/validator/keymanager/derived/mnemonic_test.go +++ b/validator/keymanager/derived/mnemonic_test.go @@ -6,10 +6,11 @@ import ( "github.com/prysmaticlabs/prysm/v3/testing/assert" "github.com/prysmaticlabs/prysm/v3/testing/require" "github.com/tyler-smith/go-bip39" + "github.com/tyler-smith/go-bip39/wordlists" ) func TestMnemonic_Generate_CanRecover(t *testing.T) { - generator := &EnglishMnemonicGenerator{} + generator := &MnemonicGenerator{} data := make([]byte, 32) copy(data, "hello-world") phrase, err := generator.Generate(data) @@ -18,3 +19,28 @@ func TestMnemonic_Generate_CanRecover(t *testing.T) { require.NoError(t, err) assert.DeepEqual(t, data, entropy, "Expected to recover original data") } + +func Test_setBip39Lang(t *testing.T) { + tests := []struct { + lang string + expectedWordlist []string + }{ + {lang: "english", expectedWordlist: wordlists.English}, + {lang: "chinese_traditional", expectedWordlist: wordlists.ChineseTraditional}, + {lang: "chinese_simplified", expectedWordlist: wordlists.ChineseSimplified}, + {lang: "czech", expectedWordlist: wordlists.Czech}, + {lang: "french", expectedWordlist: wordlists.French}, + {lang: "japanese", expectedWordlist: wordlists.Japanese}, + {lang: "korean", expectedWordlist: wordlists.Korean}, + {lang: "italian", expectedWordlist: wordlists.Italian}, + {lang: "spanish", expectedWordlist: wordlists.Spanish}, + {lang: "undefined", expectedWordlist: wordlists.English}, + } + for _, tt := range tests { + t.Run(tt.lang, func(t *testing.T) { + setBip39Lang(tt.lang) + wordlist := bip39.GetWordList() + assert.DeepEqual(t, tt.expectedWordlist, wordlist, "Expected wordlist to match") + }) + } +} diff --git a/validator/rpc/accounts_test.go b/validator/rpc/accounts_test.go index 447ece200c..2748884178 100644 --- a/validator/rpc/accounts_test.go +++ b/validator/rpc/accounts_test.go @@ -64,7 +64,7 @@ func TestServer_ListAccounts(t *testing.T) { numAccounts := 50 dr, ok := km.(*derived.Keymanager) require.Equal(t, true, ok) - err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts) + err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", "", numAccounts) require.NoError(t, err) resp, err := s.ListAccounts(ctx, &pb.ListAccountsRequest{ PageSize: int32(numAccounts), @@ -137,7 +137,7 @@ func TestServer_BackupAccounts(t *testing.T) { numAccounts := 50 dr, ok := km.(*derived.Keymanager) require.Equal(t, true, ok) - err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts) + err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", "", numAccounts) require.NoError(t, err) resp, err := s.ListAccounts(ctx, &pb.ListAccountsRequest{ PageSize: int32(numAccounts), @@ -251,7 +251,7 @@ func TestServer_VoluntaryExit(t *testing.T) { numAccounts := 2 dr, ok := km.(*derived.Keymanager) require.Equal(t, true, ok) - err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts) + err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", "", numAccounts) require.NoError(t, err) pubKeys, err := dr.FetchValidatingPublicKeys(ctx) require.NoError(t, err) diff --git a/validator/rpc/standard_api_test.go b/validator/rpc/standard_api_test.go index bceec33b7b..da2de59d2b 100644 --- a/validator/rpc/standard_api_test.go +++ b/validator/rpc/standard_api_test.go @@ -76,7 +76,7 @@ func TestServer_ListKeystores(t *testing.T) { numAccounts := 50 dr, ok := km.(*derived.Keymanager) require.Equal(t, true, ok) - err = dr.RecoverAccountsFromMnemonic(ctx, mocks.TestMnemonic, "", numAccounts) + err = dr.RecoverAccountsFromMnemonic(ctx, mocks.TestMnemonic, "", "", numAccounts) require.NoError(t, err) expectedKeys, err := dr.FetchValidatingPublicKeys(ctx) require.NoError(t, err) @@ -295,7 +295,7 @@ func TestServer_DeleteKeystores(t *testing.T) { require.NoError(t, er) dr, ok := km.(*derived.Keymanager) require.Equal(t, true, ok) - err := dr.RecoverAccountsFromMnemonic(ctx, mocks.TestMnemonic, "", numAccounts) + err := dr.RecoverAccountsFromMnemonic(ctx, mocks.TestMnemonic, "", "", numAccounts) require.NoError(t, err) publicKeys, err := dr.FetchValidatingPublicKeys(ctx) require.NoError(t, err) @@ -431,7 +431,7 @@ func TestServer_DeleteKeystores_FailedSlashingProtectionExport(t *testing.T) { require.NoError(t, er) dr, ok := km.(*derived.Keymanager) require.Equal(t, true, ok) - err := dr.RecoverAccountsFromMnemonic(ctx, mocks.TestMnemonic, "", numAccounts) + err := dr.RecoverAccountsFromMnemonic(ctx, mocks.TestMnemonic, "", "", numAccounts) require.NoError(t, err) publicKeys, err := dr.FetchValidatingPublicKeys(ctx) require.NoError(t, err)