Files
sneak 02386a6ff1 Rename module to sneak.berlin/go/x and rework bip39wordlist
Move to Go 1.26, embed the wordlist from english.txt, add
ValidateWordlist, NormalizeWordlist, and GenerateMnemonic with tests,
and remove the old root-level CLI that used the previous module path.
2026-10-31 10:03:06 -07:00

144 lines
4.2 KiB
Go

package bip39wordlist_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/x/pkg/bip39wordlist"
)
// vectorRepeats is how many times "abandon" appears in the standard BIP39
// test vector for all-zero 128-bit entropy, which ends with "about".
const vectorRepeats = bip39wordlist.MinWordCount - 1
// vectorMnemonic returns the standard BIP39 test vector for all-zero
// 128-bit entropy.
func vectorMnemonic() string {
return strings.Repeat("abandon ", vectorRepeats) + "about"
}
// messyVectorMnemonic returns vectorMnemonic with mixed case and irregular
// whitespace; it must normalize to vectorMnemonic.
func messyVectorMnemonic() string {
return " Abandon ABANDON\t" + strings.Repeat("abandon ", 4) + "\n" +
strings.Repeat("abandon ", 5) + "About \n"
}
func TestValidWordCount(t *testing.T) {
t.Parallel()
for _, n := range []int{12, 15, 18, 21, 24} {
assert.True(t, bip39wordlist.ValidWordCount(n), "%d words", n)
}
for _, n := range []int{0, 1, 9, 11, 13, 14, 16, 23, 25, 27, 36} {
assert.False(t, bip39wordlist.ValidWordCount(n), "%d words", n)
}
}
func TestValidateWordlistAcceptsValidMnemonics(t *testing.T) {
t.Parallel()
require.NoError(t, bip39wordlist.ValidateWordlist(vectorMnemonic()))
require.NoError(t, bip39wordlist.ValidateWordlist(messyVectorMnemonic()))
}
func TestValidateWordlistRejectsWrongWordCount(t *testing.T) {
t.Parallel()
err := bip39wordlist.ValidateWordlist("")
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
err = bip39wordlist.ValidateWordlist(vectorMnemonic() + " abandon")
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
assert.Contains(t, err.Error(), "got 13")
}
func TestValidateWordlistRejectsUnknownWord(t *testing.T) {
t.Parallel()
words := strings.Fields(vectorMnemonic())
words[4] = "bogus"
err := bip39wordlist.ValidateWordlist(strings.Join(words, " "))
require.ErrorIs(t, err, bip39wordlist.ErrUnknownWord)
assert.Contains(t, err.Error(), `"bogus" at position 5`)
}
func TestValidateWordlistRejectsBadChecksum(t *testing.T) {
t.Parallel()
// Twelve times "abandon" has valid words and count but the checksum
// bits in the last word are wrong.
allAbandon := strings.Repeat("abandon ", bip39wordlist.MinWordCount)
err := bip39wordlist.ValidateWordlist(allAbandon)
require.ErrorIs(t, err, bip39wordlist.ErrInvalidChecksum)
}
func TestNormalizeWordlistCanonicalizes(t *testing.T) {
t.Parallel()
normalized, err := bip39wordlist.NormalizeWordlist(messyVectorMnemonic())
require.NoError(t, err)
assert.Equal(t, vectorMnemonic(), normalized)
normalized, err = bip39wordlist.NormalizeWordlist(vectorMnemonic())
require.NoError(t, err)
assert.Equal(t, vectorMnemonic(), normalized)
}
func TestNormalizeWordlistRefusesInvalid(t *testing.T) {
t.Parallel()
normalized, err := bip39wordlist.NormalizeWordlist("Abandon Bogus about")
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
assert.Empty(t, normalized)
normalized, err = bip39wordlist.NormalizeWordlist(
strings.Repeat("abandon ", bip39wordlist.MinWordCount),
)
require.ErrorIs(t, err, bip39wordlist.ErrInvalidChecksum)
assert.Empty(t, normalized)
}
func TestGenerateMnemonicProducesValidMnemonics(t *testing.T) {
t.Parallel()
for _, wordCount := range []int{12, 15, 18, 21, 24} {
mnemonic, err := bip39wordlist.GenerateMnemonic(wordCount)
require.NoError(t, err)
require.NoError(t, bip39wordlist.ValidateWordlist(mnemonic))
assert.Len(t, strings.Fields(mnemonic), wordCount)
normalized, err := bip39wordlist.NormalizeWordlist(mnemonic)
require.NoError(t, err)
assert.Equal(t, mnemonic, normalized, "generated mnemonic is canonical")
}
}
func TestGenerateMnemonicIsRandom(t *testing.T) {
t.Parallel()
first, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
require.NoError(t, err)
second, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
require.NoError(t, err)
assert.NotEqual(t, first, second)
}
func TestGenerateMnemonicRejectsInvalidWordCount(t *testing.T) {
t.Parallel()
for _, wordCount := range []int{0, 11, 13, 25} {
mnemonic, err := bip39wordlist.GenerateMnemonic(wordCount)
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
assert.Empty(t, mnemonic)
}
}