cryptography
Why the Last Word of a BIP39 Seed Phrase Isn't Random
A 12-word BIP39 mnemonic has only 128 valid final words out of 2048, and a 24-word one has 8. Here is the checksum math, the error-detection rate, and what the checksum cannot catch.
Take a valid 12-word seed phrase, change the last word to any other word from the list, and there’s a 15-in-16 chance the wallet rejects it. Change it to one of 128 specific words and the wallet accepts it as a perfectly valid — and completely different — wallet. The last word of a BIP39 mnemonic isn’t twelve-twelfths of your entropy. It’s mostly a checksum, and that single design decision explains the word count, the 2048-word list, and why a wallet can tell you that you mistyped something without knowing what you meant.
Everything below was executed locally against @scure/bip39 2.2.0 and the official English wordlist on Node 22.18.0.
12 words is not 12 × 11 bits
Each word encodes an 11-bit index into a 2048-word list (2¹¹ = 2048), so a 12-word sentence carries 132 bits. But BIP-39 only lets you start with entropy that is “between 128-256 bits in multiples of 32,” and defines the rest:
A checksum is generated by taking the first
ENT / 32bits of its SHA256 hash.
with CS = ENT / 32 and MS = (ENT + CS) / 11. The whole table is just that arithmetic:
| Words (MS) | Entropy ENT | Checksum CS | Total bits | Checksum lives in |
|---|---|---|---|---|
| 12 | 128 | 4 | 132 | last 4 bits of word 12 |
| 15 | 160 | 5 | 165 | last 5 bits of word 15 |
| 18 | 192 | 6 | 198 | last 6 bits of word 18 |
| 21 | 224 | 7 | 231 | last 7 bits of word 21 |
| 24 | 256 | 8 | 264 | last 8 bits of word 24 |
The word counts aren’t chosen for looks. They’re the only lengths where entropy-plus-checksum divides evenly by 11.
Doing the checksum by hand
Concatenate the 11-bit indices, split off the trailing CS bits, SHA-256 the remaining entropy bytes, and compare against the first CS bits of the digest:
const bits = words.map(w => wordlist.indexOf(w).toString(2).padStart(11, '0')).join('');
const ENT = (words.length * 11 * 32) / 33; // 128 for 12 words
const CS = words.length * 11 - ENT; // 4
const entropy = Buffer.from(bits.slice(0, ENT).match(/.{8}/g).map(b => parseInt(b, 2)));
const expected = createHash('sha256').update(entropy).digest()[0]
.toString(2).padStart(8, '0').slice(0, CS);
expected === bits.slice(ENT); // true for a valid mnemonic
Run against a freshly generated phrase:
subway giraffe expose sea smile chef range sword small skull second rare
ENT = 128, CS = 4, checksum bits in the mnemonic = 0000
SHA-256(entropy) starts 0e49... -> first 4 bits = 0000 ✓
execute mom found shiver raccoon scene naive grab access ramp enemy amateur
still aisle advance canyon sibling bid flee juice practice venue song blame
ENT = 256, CS = 8, checksum bits = 10111001
SHA-256(entropy) starts b920... -> first 8 bits = 10111001 ✓
The famous all-zeros phrase is the same math with nothing up its sleeve: 128 zero bits of entropy, SHA-256(0x00 × 16) starting with the nibble 0011, giving index 00000000011 = 3 = about:
abandon abandon abandon abandon abandon abandon abandon abandon
abandon abandon abandon about
With 256 zero bits, the same construction ends in art instead.
How many last words are actually valid
If the checksum occupies the final CS bits, the last word carries 11 − CS free bits, so exactly 2^(11−CS) of the 2048 words complete any given prefix. Measured by brute force — take a real mnemonic, try all 2048 candidates in the final slot, count acceptances:
| Words | Checksum bits | Free bits in last word | Valid last words | Measured |
|---|---|---|---|---|
| 12 | 4 | 7 | 128 | 128 ✓ |
| 15 | 5 | 6 | 64 | 64 ✓ |
| 18 | 6 | 5 | 32 | 32 ✓ |
| 21 | 7 | 4 | 16 | 16 ✓ |
| 24 | 8 | 3 | 8 | 8 ✓ |
For the 24-word phrase above, the complete set of valid final words is blame chimney extend kangaroo one powder shadow useless — eight words out of 2048. This is why “I remember 23 of my 24 words” is a recoverable situation and “I remember 11 of my 12” is much less so: in the 24-word case only 8 candidates survive the checksum, but in the 12-word case 128 do, and each has to be tried against the blockchain.
It also means a randomly assembled sentence is valid 1 time in 16. Generating 20,000 random 12-word sentences from the list produced 1,283 that validated — 6.42%, against the predicted 6.25%.
What the checksum catches, and what it doesn’t
The checksum is error-detecting, not error-correcting. It knows something is wrong; it never knows what you meant.
- Single mistyped word, 12-word phrase: I substituted every other word from the list into every position — all 24,564 possibilities — and 23,034 were rejected. That’s 93.77%, i.e. the predicted 15/16. Roughly one wrong word in sixteen sails straight through as a valid phrase for someone else’s wallet.
- 24 words: the same argument gives 255/256, about 99.6%.
- Swapped words: transposing any two adjacent words changed the entropy in all 11 trials and every one was rejected — but that’s the 15/16 die roll again, not a guarantee.
- Wrong passphrase: never detected. More below.
That 15/16 figure is the honest answer to “will the wallet tell me if I got a word wrong?” Usually. Not always. Check the phrase before you rely on it, and check it against the wordlist rather than your memory — the list has engineered properties for exactly this: 2048 sorted words, no confusable pairs like “build/built”, and “it’s enough to type the first four letters to unambiguously identify the word.” Verified against the English list: all 2048 words are unique in their first four characters, the list is sorted, and word lengths run from 3 to 8 characters.
The passphrase is outside the checksum
Going from mnemonic to seed doesn’t involve the checksum at all. BIP-39 specifies PBKDF2 with HMAC-SHA512, 2048 iterations, the mnemonic as the password and the string "mnemonic" plus the optional passphrase as the salt, producing 512 bits.
"abandon ... about" + passphrase "" -> 5eb00bbddcf069084889a8ab91555681...
"abandon ... about" + passphrase "hunter2" -> c8f670df773583d2502c0bc50adc0ec4...
"abandon ... about" + passphrase "hunterZ" -> 3a8c2b40561df0c8bfb705a8ba424feb...
"abandon ... about" + passphrase "TREZOR" -> c55257c360c07c72029aebc1b53c05ed... (matches the BIP-39 test vector)
Every one of those is a valid seed for a valid, empty, different wallet. There is no “wrong passphrase” error, because there is nothing to check it against — which is the point of the feature (plausible deniability) and the reason a typo in a passphrase is unrecoverable in a way that a typo in a word is not.
Those 2048 iterations are worth reading for what they are: BIP-39’s key stretching is weak by modern standards, and a short, guessable passphrase adds far less protection than its length suggests. See password entropy explained for why “how long is it” is the wrong question and how bcrypt works for what deliberate key stretching looks like.
Normalization: where valid phrases get rejected
The spec requires UTF-8 NFKD normalization of both the mnemonic and the passphrase before hashing. That single rule quietly covers several real failure modes: the Japanese wordlist uses ideographic spaces, accented characters in the French and Spanish lists have multiple Unicode spellings, and a phrase that round-trips through a word processor may come back composed differently.
Reference implementations are strict about the rest. Raw @scure/bip39 rejects ABANDON ABANDON ... and rejects a phrase with double spaces. Our BIP39 generator lowercases and collapses runs of whitespace before validating, so all three of these pass:
ABANDON ABANDON ... ABOUT -> valid
abandon abandon ... about -> valid (double spaces)
" abandon ... about " -> valid (padding)
That’s friendlier for checking a phrase you copied out of a PDF, and it’s worth knowing it’s our leniency rather than the spec’s.
Using the checksum to find the typo
“Invalid” is a useless answer, because the two ways a phrase can fail are completely different problems. A word that isn’t in the list is a typo you can fix. A phrase where every word is in the list but the checksum fails is a wrong or reordered word that the checksum, by construction, cannot localize.
So the validator reports them separately: it names each word that isn’t in the wordlist, with its position, and suggests corrections — ranked by shared four-letter prefix first (the property the list was designed around), then by edit distance. And when exactly one word is unknown, every candidate is run back through the checksum, which usually leaves one standing:
abandon … abandon abuot -> Word 12: abuot — did you mean about? (fits the checksum)
abndon abandon … about -> Word 1: abndon — did you mean abandon? [no candidate fits]
abandon … abandon abandon -> Checksum doesn't match: every word is real, one is wrong
Measured over 1,926 single-character typos injected at random positions in random 12-word phrases:
| Result | |
|---|---|
| Typo produced a word outside the list (so it can be named) | 1,945 of 1,957 — 99.4% |
| Suggestion list was already a single word | 486 of 1,926 — 25% |
| Multiple candidates, narrowed to one by the checksum | 1,218 of 1,440 — 85% |
| Top suggestion was the word that had been mangled | 1,881 of 1,926 — 97.7% |
The dozen cases that slip through are the interesting ones: a typo can land on another real wordlist word, and then there is nothing to name — you get a checksum error and a 12-position search. That’s the 15/16 rule from earlier showing up as a user experience.
If the invisible characters in a pasted phrase are the suspect, invisible Unicode characters covers how to find them.
The one rule
Generate seed phrases offline, on a device you trust, and never paste a phrase that protects real funds into any website — including ours. The BIP39 generator runs entirely in your browser and uploads nothing, which is a statement about our code, not a reason to trust any web page with a live key. Use it to learn the format, to generate throwaway phrases for testing, or to validate a phrase for a wallet that holds nothing.