Addresses, WIF, Base58Check & Bech32
A public key is a point on a curve; a private key is a 256-bit number. Neither is something you’d want to read aloud, type, or paste into a payment field. Encodings turn these raw values into compact, copy-pasteable strings — and, critically, bake in a checksum so a single mistyped character is rejected instead of silently sending your coins somewhere unspendable. This page is about that translation layer and why it matters more than it looks.
What an address actually encodes
Section titled “What an address actually encodes”An address is not your public key, and definitely not your private key. It is a short representation of a spending condition — the rule a future spender must satisfy. For the common case, that rule is “provide a public key that hashes to this value, plus a valid signature.” So the pipeline is:
public key ──hash──► hash160 (or other commitment) ──encode──► ADDRESS you shareHashing the public key before publishing it adds a layer: the world sees only a fingerprint until you actually spend, when the real public key is revealed. (See hash functions for why these fingerprints are one-way.) What kind of script the address commits to — legacy, P2SH-wrapped, SegWit, Taproot — is covered in the anatomy of a transaction; here we focus on the encoding that wraps it.
Why checksums exist: the void problem
Section titled “Why checksums exist: the void problem”Bitcoin has no “undo” and no customer-support desk. If you send to a well-formed but unintended
address, the coins land in a UTXO whose key nobody holds — gone forever. Humans transcribe strings
wrong constantly: a swapped character, a dropped digit, an O read as 0. So every Bitcoin encoding
embeds a checksum: extra characters derived from the data itself.
[ payload ] ──► hash/derive ──► [ checksum ] concatenate: [ payload ][ checksum ] ──► encode ──► final string
wallet decodes ► recomputes checksum from payload ► compares match → accept mismatch → REJECT (don't broadcast)The wallet recomputes the checksum from what you typed and refuses to proceed unless it matches. A one-character typo changes the payload, so the recomputed checksum won’t match, and the address is rejected before any coins move. The checksum’s whole purpose is to convert silent catastrophe into a loud, harmless error.
Base58Check: the legacy encoding
Section titled “Base58Check: the legacy encoding”The original Bitcoin encoding is Base58Check. Two ideas in the name:
- Base58 — a 58-character alphabet that deliberately omits the visually ambiguous characters
0(zero),O(capital o),I(capital i), andl(lowercase L). Fewer look-alikes means fewer transcription errors. - Check — a 4-byte checksum computed as the first 4 bytes of
SHA256(SHA256(payload)), appended before encoding.
The structure is:
[ version byte ][ payload (e.g. hash160) ][ 4-byte checksum ] ──Base58──► stringThe version byte is what makes the result start with a recognizable prefix. A mainnet legacy
address (version 0x00) renders starting with 1; a P2SH address (version 0x05) starts with
3. So the leading character is not cosmetic — it encodes what type of thing the string is.
WIF: encoding a private key
Section titled “WIF: encoding a private key”The same Base58Check machinery encodes private keys for backup and import, in a format called WIF (Wallet Import Format). It’s a private key wrapped with a version byte, a checksum, and an optional “compressed” flag:
[ 0x80 version ][ 32-byte private key ][ 0x01 if compressed ][ 4-byte checksum ] ──Base58──► WIFMainnet WIF keys start with 5 (uncompressed) or K/L (compressed). The point is the
same as for addresses: a typo in a WIF string fails the checksum rather than importing a different,
wrong key.
Bech32 and Bech32m: SegWit and Taproot
Section titled “Bech32 and Bech32m: SegWit and Taproot”SegWit introduced a better encoding: Bech32 (BIP173), and Taproot refined it to Bech32m
(BIP350). You recognize these by the human-readable prefix and a separator: mainnet uses bc1.
[ "bc" HRP ][ "1" separator ][ data + 6-char BCH checksum ] all-lowercase native SegWit v0 ► bc1q… Taproot v1 ► bc1p…Why bother replacing Base58Check? Bech32 is strictly better at its one job:
- All-lowercase (never mixed-case), so it’s unambiguous to read aloud — and because the whole string is one case, it can be uppercased inside QR codes to use their more compact alphanumeric mode.
- A far stronger BCH error-detection code that doesn’t just catch errors but can pinpoint which characters are wrong — and is mathematically guaranteed to catch any few-character mistake.
- Built-in error location is why some wallets can suggest corrections.
Putting the prefixes together
Section titled “Putting the prefixes together”| Starts with | Encoding | Type |
|---|---|---|
1… | Base58Check (v0x00) | Legacy P2PKH |
3… | Base58Check (v0x05) | P2SH (incl. wrapped SegWit) |
bc1q… | Bech32 | Native SegWit v0 |
bc1p… | Bech32m | Taproot (SegWit v1) |
5…, K…, L… | Base58Check (WIF) | Private key — keep secret |
(All strings above are illustrative prefixes, not real addresses.)
The architect’s lens
Section titled “The architect’s lens”Step back from the prefixes and checksums and ask the five questions that turn an implementer into an architect:
- Why does it exist? To translate raw 256-bit keys and curve points into compact, copy-pasteable strings — and to bake a checksum into them so a single mistyped character is rejected rather than silently sending coins to an unspendable UTXO.
- What problem does it solve? The void problem: Bitcoin has no undo and no support desk, so the encoding must catch a swapped character or an
O-read-as-0before any value moves. Base58 also strips the four visually ambiguous characters (0,O,I,l) to cut transcription errors at the source. - What are the trade-offs? A 4-byte (32-bit) Base58Check checksum only catches ~all-but-1-in-4-billion random corruptions and can’t say which character is wrong; mixed case and a bespoke alphabet hurt QR density and read-aloud. Bech32’s BCH code fixes both — guaranteed detection of up to 4-character errors, all-lowercase — at the cost of a second, deliberately incompatible variant (Bech32m) once Taproot arrived.
- When is this the wrong design? When the string never leaves software — an internal key reference, an
xpubpassed between your own programs — a human-facing checksum buys nothing. And you’d never invent your own scheme: the value of an encoding is that every wallet agrees on the prefix-to-type mapping (1,3,bc1q,bc1p). - What breaks if I remove it? Fat-fingered addresses would broadcast as valid transactions to keys no one holds — irreversible loss — and the leading character (address types) would stop signaling legacy vs SegWit vs Taproot, so wallets couldn’t tell what kind of script a string commits to.
The thread
Section titled “The thread”How does this help untrusting strangers agree on one ledger? The ledger is global and unforgiving: once you broadcast, anyone in the world will faithfully execute exactly what you signed. Encodings with checksums are the safety rail at that cliff edge — they let a stranger hand you a string, and let your wallet independently verify the string is well-formed before committing irreversible value to it. No trusted intermediary catches your typo; the math built into the address does. That keeps the shared ledger usable by ordinary humans who fat-finger keys.
Check your understanding
Section titled “Check your understanding”- What does an address actually encode — and why isn’t it simply your public key?
- Walk through how a checksum turns a single mistyped character into a rejection instead of lost coins.
- Why does Base58 omit
0,O,I, andl? - What is WIF, and why is it as dangerous to expose as a seed phrase?
- Give two concrete reasons Bech32 improves on Base58Check, and explain why Bech32m was introduced.
Show answers
- An address encodes a spending condition — for the common case, “provide a public key that hashes to this value, plus a valid signature.” It isn’t simply your public key: the key is hashed first, so the world sees only a fingerprint (
public key → hash160 → encode) until you actually spend and reveal the real key. - The encoding appends a checksum derived from the payload itself. On entry the wallet recomputes the checksum from what you typed and compares: a one-character typo changes the payload so the recomputed checksum won’t match, and the address is rejected before any coins move — converting silent, irreversible loss into a loud, harmless error.
- To eliminate visually ambiguous characters —
0(zero),O(capital o),I(capital i), andl(lowercase L) — so fewer look-alikes means fewer transcription errors when humans read or copy a string. - WIF (Wallet Import Format) is a private key encoded with Base58Check (version byte, optional compressed flag, checksum) for backup/import. It looks like an address but is your secret — anyone who reads it can spend everything it controls — so, like a seed phrase, never paste it into a website, screenshot, or email.
- Any two of: it’s all-lowercase with no mixed case (unambiguous to read aloud, and can be uppercased in QR codes for a more compact encoding); it uses a far stronger BCH error-detection code that’s mathematically guaranteed to catch any few-character mistake; and it offers error location so wallets can suggest corrections. Bech32m was introduced because the original Bech32 checksum had a subtle weakness for some lengths, so witness v1+ (Taproot,
bc1p…) uses Bech32m while v0 (bc1q…) stays on Bech32 — deliberately incompatible so the two can’t be silently confused.