Say why a mnemonic is refused (closes #76)
check / check (push) Waiting to run

Every way a mnemonic could be invalid was reported as a checksum
failure, including an empty one. It is now refused as empty, as having
a word count other than 12, 15, 18, 21 or 24, as having a word that is
not on the BIP-39 word list, or as failing the checksum. A word not on
the list is named by its position, never by the word itself.

Model: opus-5-5
This commit is contained in:
2026-10-14 19:34:21 +00:00
committed by sneak
parent 44cb5215b0
commit 727fa8977b
3 changed files with 108 additions and 9 deletions
+49 -5
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"os"
"os/exec"
"slices"
"strings"
"golang.org/x/term"
@@ -29,6 +30,28 @@ var ErrMissing = errors.New(
"no mnemonic given and standard input is not a terminal",
)
// The lengths a BIP-39 mnemonic may have.
const (
twelve = 12
fifteen = 15
eighteen = 18
twentyOne = 21
twentyFour = 24
)
// ErrEmpty is returned for a mnemonic with no words in it, such as the
// output of a mnemonic command that printed nothing.
var ErrEmpty = errors.New("the mnemonic is empty")
// ErrWordCount is returned for a mnemonic with a number of words that
// BIP-39 does not allow.
var ErrWordCount = errors.New("a mnemonic has 12, 15, 18, 21 or 24 words")
// ErrUnknownWord is returned for a mnemonic with a word that is not on
// the BIP-39 English word list. The error gives the word's position,
// never the word, so the mnemonic stays out of the message.
var ErrUnknownWord = errors.New("not on the BIP-39 word list")
// ErrChecksum is returned for a mnemonic that fails the BIP-39
// checksum.
var ErrChecksum = errors.New("the mnemonic fails its BIP-39 checksum")
@@ -126,13 +149,34 @@ func ask() (string, error) {
// checked joins the words with single spaces, whatever whitespace
// separated them, since the seed is computed over the string itself,
// and refuses a mnemonic that does not pass the BIP-39 checksum.
func checked(words string) (string, error) {
words = strings.Join(strings.Fields(words), " ")
// and refuses a mnemonic that is not a valid BIP-39 mnemonic, with an
// error that says what is wrong with it.
func checked(text string) (string, error) {
words := strings.Fields(text)
if !bip39.IsMnemonicValid(words) {
switch len(words) {
case 0:
return "", ErrEmpty
case twelve, fifteen, eighteen, twentyOne, twentyFour:
default:
return "", fmt.Errorf("%w, not %d", ErrWordCount, len(words))
}
wordList := bip39.English()
for i, word := range words {
if !slices.Contains(wordList, word) {
return "", fmt.Errorf(
"word %d of the mnemonic is %w", i+1, ErrUnknownWord,
)
}
}
joined := strings.Join(words, " ")
if !bip39.IsMnemonicValid(joined) {
return "", ErrChecksum
}
return words, nil
return joined, nil
}