Support non english mnemonics for wallet creation (#11543)

* add option to log rejected gossip message

* add bip39 supported mnemonic languages

* Revert "add option to log rejected gossip message"

This reverts commit 9a3d4486f6.

* Add mnemonic language flag

* Update go.mod

* Simplify language mapping

* Add test for setBip39Lang

* Update go.mod

* Improve language matching

Co-authored-by: Preston Van Loon <preston@prysmaticlabs.com>

* Run gazelle + fix maligned struct

Co-authored-by: Preston Van Loon <preston@prysmaticlabs.com>
Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
This commit is contained in:
Sammy Rosso
2022-10-26 23:04:00 +02:00
committed by GitHub
parent 1572c530b5
commit a15e0797e4
17 changed files with 101 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,6 +45,7 @@ type AccountsCLIManager struct {
privateKeyFile string
passwordFilePath string
keysDir string
mnemonicLanguage string
backupsDir string
backupsPassword string
filteredPubKeys []bls.PublicKey

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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