mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
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:
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user