Skip to content

Seed Phrases and Private Keys

💡
Before you start

You need nothing but Python 3 and a terminal. It is already installed on macOS and Linux; on Windows, install it from python.org and tick “Add python.exe to PATH”. Confirm it is there by opening a terminal and running python3 --version, which should print something like Python 3.12.3. No extra libraries are needed — everything below uses Python’s built-in hashlib.

Do not type your own recovery phrase into any of this. The exercise uses a phrase published in the BIP-39 standard as a test case, so your output will match this page line for line. A real phrase typed into any computer is a real phrase that has been exposed — and the whole point of the next fifteen minutes is to understand why.

What is a Seed Phrase?

A seed phrase (also called a recovery phrase or mnemonic) is a sequence of 12 or 24 ordinary English words that encodes all the information needed to reconstruct your entire wallet. It is the human-readable form of your master private key.

Example of what a seed phrase looks like (do NOT use this):

abandon ability able about above absent absorb abstract absurd abuse access accident

These 12 words, in this exact order, would generate specific private keys and addresses. Change a single word or swap the order, and entirely different keys are produced.

How Seed Phrases Work (BIP-39)

Seed phrases follow the BIP-39 standard (Bitcoin Improvement Proposal 39):

  • The wallet generates a large random number (128 bits for 12 words, 256 bits for 24 words)
  • This random data is converted into words from a standardized list of 2,048 English words
  • The final word includes a checksum that verifies the phrase was recorded correctly
  • From this seed, a master key is derived, and from that master key, an unlimited number of private keys and addresses can be generated deterministically
💡
One seed, many accounts

A single seed phrase can generate addresses for Bitcoin, Ethereum, and many other cryptocurrencies simultaneously. This is why losing your seed phrase can mean losing access to all your crypto assets at once.

Seed Phrases vs Private Keys

Seed Phrase The master backup. From it, all private keys can be re-derived. Losing it means losing everything. It is the root of your entire wallet.
Private Key A specific key for a specific address. Each address has its own private key. All of these are derived from the seed phrase. Exporting a single private key exposes only that one address.

Why 12 or 24 Words?

The number of words determines the entropy (randomness) of your seed:

  • 12 words = 128 bits of entropy = 2128 possible combinations (more than the number of atoms in the observable universe)
  • 24 words = 256 bits of entropy = 2256 possible combinations (astronomically more secure)

Both are considered secure against brute-force attacks with current and foreseeable technology. 24-word phrases provide additional margin against future advances in computing.

The Rules of Seed Phrase Security

⚠️
These rules are absolute

Breaking any of these rules can result in permanent, irrecoverable loss of all cryptocurrency associated with the seed phrase.

  • NEVER share your seed phrase with anyone. No legitimate service, support team, or software will ever ask for it. Anyone who asks is trying to steal your funds.
  • NEVER type it into a computer, phone, or website. Malware, keyloggers, and phishing sites can capture it instantly.
  • NEVER photograph it or screenshot it. Photos are synced to cloud services, backed up automatically, and accessible to malware.
  • NEVER store it in a password manager, email, or cloud note. If that service is compromised, your crypto is gone.
  • NEVER enter it into any software that is not your own verified wallet. Fake wallet apps and browser extensions exist specifically to harvest seed phrases.

How to Store Your Seed Phrase

  • Write it on paper using a pen (not pencil, which fades). Store in a waterproof bag in a secure location.
  • Stamp it on metal using a steel seed phrase backup kit. Resistant to fire, water, and corrosion. The most durable option.
  • Store in a secure location such as a home safe, bank safety deposit box, or other tamper-evident container.
  • Consider geographic distribution — store copies in two physically separate locations to protect against localized disasters.

What Happens If Someone Gets Your Seed Phrase

If anyone obtains your seed phrase, they can:

  • Reconstruct your entire wallet on their own device
  • Access every cryptocurrency address derived from that seed
  • Transfer all your funds to their own addresses instantly
  • This is irreversible — there is no way to undo it or recover the funds

If you ever suspect your seed phrase has been compromised, immediately transfer all funds to a new wallet with a freshly generated seed phrase.

Now Run the Check Your Wallet Runs, in Five Steps

Everything above describes BIP-39 from the outside. In the next fifteen minutes you will implement the part of it that matters most — the checksum — and use it to answer a question nobody usually gets a straight answer to: if I copy one word wrong, will anything warn me? You will find that the answer is “usually, but not always”, measure exactly how often “not always” is, and finish by turning a phrase into the master secret it stands for. Every number printed below came from running these exact files.

1
Write the twenty lines that are the whole of BIP-39’s bookkeeping

Go: open a terminal in a folder you can write to, for example cd ~/Desktop (Windows: cd %USERPROFILE%\Desktop).

Do: save this as bip39.py. It does not print anything yet — it is the toolbox the next four steps use. A recovery phrase is a list of numbers dressed up as words, so the code works in numbers and skips the dressing-up.

import hashlib

def to_numbers(entropy: bytes):
    """Turn raw entropy into the word numbers a recovery phrase encodes."""
    bits = "".join(f"{b:08b}" for b in entropy)
    checksum_len = len(entropy) * 8 // 32
    checksum = f"{hashlib.sha256(entropy).digest()[0]:08b}"[:checksum_len]
    full = bits + checksum
    return [int(full[i:i + 11], 2) for i in range(0, len(full), 11)]

def checksum_ok(numbers):
    """Re-run the check your wallet runs when you type a recovery phrase."""
    full = "".join(f"{n:011b}" for n in numbers)
    ent_len = len(full) * 32 // 33
    entropy = int(full[:ent_len], 2).to_bytes(ent_len // 8, "big")
    want = f"{hashlib.sha256(entropy).digest()[0]:08b}"[:len(full) - ent_len]
    return full[ent_len:] == want

You should see: nothing at all. Run python3 bip39.py and it should return you straight to the prompt with no output and no error. That silence is the pass condition — it means the file parses.

If not: IndentationError means the four-space indents inside the functions were lost in copying; every line under a def must be indented. SyntaxError: invalid syntax on the f"..." lines means you are on Python 2 — check python3 --version and use python3 explicitly.

2
Watch 128 random bits become twelve word numbers

Go: same folder, next to bip39.py.

Do: save this as step2.py and run python3 step2.py. The entropy is fixed rather than random so that your output matches this page exactly.

from bip39 import to_numbers, checksum_ok

entropy = bytes.fromhex("0c1e24e5917779d297e14d45f14e1a1a")
numbers = to_numbers(entropy)
print("entropy (128 bits):", entropy.hex())
print("word numbers      :", numbers)
print("checksum valid    :", checksum_ok(numbers))

You should see: twelve numbers, each between 0 and 2047, and a passing check:

entropy (128 bits): 0c1e24e5917779d297e14d45f14e1a1a
word numbers      : [96, 1929, 459, 279, 956, 1866, 764, 333, 559, 1107, 1076, 423]
checksum valid    : True

Those twelve numbers are the phrase. Your wallet looks each one up in the same published list of 2,048 English words that every BIP-39 wallet uses, and shows you the word instead of the number. Nothing else happened: 128 bits of randomness went in, twelve look-up numbers came out.

If not: ModuleNotFoundError: No module named 'bip39' means step2.py is not in the same folder as bip39.py — run ls (Windows: dir) and confirm both names are there. If the numbers differ, one character of the hex string was mistyped; it must be exactly 32 characters.

3
Prove the code is right by checking it against the published standard

Go: same folder. Never trust an implementation you have not tested — including this one.

Do: save this as step3.py and run it. BIP-39 publishes test cases, and the simplest is entropy of all zeros, whose phrase is the famous “abandon” × 11 then “about”. In the official word list abandon is number 0 and about is number 3.

from bip39 import to_numbers
print("all-zero entropy ->", to_numbers(bytes(16)))

You should see: eleven zeros and a three — exactly the phrase the standard says it should be:

all-zero entropy -> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3]

Notice where the 3 came from: the last word carries the leftover entropy bits plus the four checksum bits. That is why the final word of a recovery phrase is never freely chosen — it is partly a check digit, like the last digit of a credit card number.

If not: if the last number is not 3, the checksum slice in to_numbers is wrong — compare that line against step 1 character by character. If you get twelve zeros, the checksum is not being appended at all.

4
Mistype one word, then measure how often the mistake slips through

Go: same folder. This is the step that changes how you handle paper.

Do: save this as step4.py and run it. It swaps a single word for its neighbour in the list — the exact error a tired person makes reading their own handwriting — and then repeats that mistake ten thousand times at random.

import random
from bip39 import checksum_ok

good = [96, 1929, 459, 279, 956, 1866, 764, 333, 559, 1107, 1076, 423]
typo = list(good)
typo[4] = 957
print("phrase as written :", checksum_ok(good))
print("word 5 mistyped   :", checksum_ok(typo))

random.seed(7)
accepted = 0
for _ in range(10000):
    trial = list(good)
    trial[random.randrange(12)] = random.randrange(2048)
    accepted += checksum_ok(trial)
print(f"{accepted} of 10000 single-word errors were NOT caught")
print(f"that is 1 in {10000 // accepted}")

You should see: the typo caught — and then the uncomfortable number:

phrase as written : True
word 5 mistyped   : False
628 of 10000 single-word errors were NOT caught
that is 1 in 15

The checksum is four bits, so roughly one wrong phrase in sixteen passes it by chance, and the measured result lands exactly there. Read what that means: about 94% of the time your wallet will say “invalid recovery phrase” and save you. The other 6% of the time it will open a perfectly valid wallet that is not yours, show you a zero balance, and say nothing was wrong. “The wallet accepted it” is therefore not proof that your backup is correct.

If not: if accepted comes out as 0 you will get a ZeroDivisionError on the last line — that means checksum_ok is rejecting everything, so re-check it against step 1. A slightly different count than 628 means your random.seed(7) line is missing; the proportion should still be close to one in sixteen.

5
Turn the phrase into the master secret it actually stands for

Go: same folder. This is the last conversion in the chain, and it is what your wallet does the moment you finish typing.

Do: save this as step5.py and run it. The words are stirred 2,048 times with PBKDF2 to produce 512 bits — the seed every key in the wallet grows from.

import hashlib, unicodedata

phrase = " ".join(["abandon"] * 11 + ["about"])
seed = hashlib.pbkdf2_hmac("sha512",
                           unicodedata.normalize("NFKD", phrase).encode(),
                           b"mnemonic", 2048)
print("phrase :", phrase)
print("seed   :", seed.hex()[:64])
print("        ", seed.hex()[64:])

You should see: the standard’s own published answer for this test phrase:

phrase : abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
seed   : 5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc1
         9a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4

Now the whole picture is in front of you: twelve words → twelve numbers → 128 bits of entropy → one 512-bit seed → every address you will ever use. There is no account, no server and no password reset anywhere in that chain. It also explains why any wallet, from any maker, can restore the same coins from the same words: they all run the computation you just ran.

If not: a different seed means the phrase string differs — it needs single spaces between words and no trailing space. If you see TypeError: pbkdf2_hmac() argument 'salt' must be bytes, the b before "mnemonic" is missing.

🎉
Check yourself before moving on

Without scrolling up: your friend restores a wallet, the app accepts the phrase without complaint, and the balance shows zero. Name the two possibilities, and the one test that tells them apart. Answer: either the phrase is correct and the coins were moved (or were never there), or the phrase is one of the roughly one-in-sixteen wrong phrases that still passes the checksum, and the app has opened a different empty wallet. The test is to compare a receiving address from the restored wallet against an address they know belongs to the original — if the addresses differ, the phrase is wrong, not the balance. Doing that comparison on the day you write the phrase down, not on the day you need it, is the entire discipline.

Now do it without the page: pick any twelve numbers between 0 and 2047 out of your head, run them through checksum_ok, and keep adjusting only the last number until it returns True. Exactly 128 of the 2,048 possible last words work, so you will hit one roughly every sixteen tries. You have just hand-forged a valid recovery phrase — which is the clearest possible proof that validity says nothing about whose wallet it opens.

Summary

  • A seed phrase is the master backup for your entire crypto wallet
  • It follows the BIP-39 standard and can derive unlimited private keys
  • 12-word and 24-word phrases are both secure against brute force
  • Never share, photograph, or digitally store your seed phrase
  • Store it on paper or metal in a physically secure location
  • If compromised, move all funds to a new wallet immediately
🎉
You understand seed phrase security!

Next, learn how to build a robust backup strategy to protect your crypto keys against all scenarios.