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

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