The Blockchain Chief Bitcoin Book / Part II: How Bitcoin Works
Chapter 05

Addresses & Encoding

How public keys become human-readable addresses: Base58Check, Bech32, address types, and the key_io layer that ties them together.

From Key to Address

A Bitcoin address is simply a human-readable encoding of a script template. The path from a private key to an address looks like this:

Private Key32 random bytes
โ†“ EC multiply by generator G
Public Key33 bytes (compressed)
โ†“ RIPEMD160(SHA256(pubkey)) or SHA256(pubkey)
Public Key Hash20 or 32 bytes
โ†“ Add version prefix, encode
AddressBase58Check or Bech32 string
๐Ÿ’ก Addresses Are Not Keys

An address encodes a hash of a public key (or script), not the key itself. The actual public key is only revealed when spending, providing an extra layer of security against potential future attacks on elliptic curve cryptography.

Base58Check Encoding

The original Bitcoin address format (still used for P2PKH and P2SH). Base58 is like Base64 but removes ambiguous characters:

Removed Characters

Encoding Steps

  1. Take the raw data (e.g., 20-byte pubkey hash)
  2. Prepend a version byte (0x00 for P2PKH mainnet, 0x05 for P2SH)
  3. Compute checksum: first 4 bytes of SHA256d(version + data)
  4. Append checksum to get version + data + checksum
  5. Encode the result in Base58
// Example: P2PKH address creation
pubkey_hash = RIPEMD160(SHA256(public_key))     // 20 bytes
versioned   = 0x00 || pubkey_hash               // 21 bytes
checksum    = SHA256d(versioned)[0:4]            // 4 bytes
address     = Base58(versioned || checksum)      // e.g., "1A1zP1eP..."
โš ๏ธ Base58 Limitations

Base58Check addresses are case-sensitive, don't detect transposition errors reliably, and are slow to decode. These issues led to the development of Bech32.

Bech32 & Bech32m

Bech32 (BIP 173) and Bech32m (BIP 350) are modern address formats designed to fix Base58's shortcomings:

Case-Insensitive

Addresses use only lowercase (or only uppercase). No more confusion between similar characters.

Better Error Detection

Uses a BCH code that detects up to 4 errors and catches all single-character errors.

QR-Code Friendly

When all-uppercase, Bech32 uses the efficient alphanumeric QR encoding mode, producing smaller QR codes.

Format

// Bech32 address structure:
bc1 q r4g5fcm3k0v8vm0smlpv3he5xtfnqmv50f    // P2WPKH
โ”‚   โ”‚ โ””โ”€โ”€ data (witness program in 5-bit groups) + checksum
โ”‚   โ””โ”€โ”€โ”€โ”€ witness version (q = 0)
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ human-readable part (bc = mainnet, tb = testnet)

Bech32 vs. Bech32m

Address Types Summary

P2PKH Starts with 1 ยท Base58Check ยท Legacy single-key
P2SH Starts with 3 ยท Base58Check ยท Script hash (multisig, wrapped SegWit)
P2WPKH Starts with bc1q ยท Bech32 ยท Native SegWit single-key
P2WSH Starts with bc1q (longer) ยท Bech32 ยท Native SegWit script hash
P2TR Starts with bc1p ยท Bech32m ยท Taproot (recommended for new wallets)
โœ… Recommendation

New wallets should default to P2TR (Taproot) addresses. They provide the best combination of privacy, efficiency, and functionality. P2WPKH is also widely used and well-supported.

The key_io Layer

In Bitcoin Core, address encoding/decoding is handled by src/key_io.cpp. Two key functions:

EncodeDestination()

Converts a CTxDestination (internal destination variant) to a string address:

// CTxDestination is a std::variant holding one of:
// - PKHash          โ†’ encodes as P2PKH (1...)
// - ScriptHash      โ†’ encodes as P2SH  (3...)
// - WitnessV0KeyHash โ†’ encodes as P2WPKH (bc1q...)
// - WitnessV0ScriptHash โ†’ encodes as P2WSH (bc1q...)
// - WitnessV1Taproot โ†’ encodes as P2TR  (bc1p...)

std::string address = EncodeDestination(destination);

DecodeDestination()

Parses a string address back into a CTxDestination:

CTxDestination dest = DecodeDestination("bc1p...");
// Automatically detects address type
// Returns CNoDestination if invalid

WIF: Wallet Import Format

Private keys can also be encoded as strings using WIF (Base58Check with a different version byte):

  1. Take the 32-byte private key
  2. Prepend version byte (0x80 for mainnet)
  3. If compressed, append 0x01 flag byte
  4. Apply Base58Check encoding

WIF keys start with 5 (uncompressed) or K/L (compressed) on mainnet. This format is used for importprivkey RPC and backup/restore operations.