Chapter 1: Private Keys, Public Keys, and Address Encoding#


Understanding Bitcoin’s cryptographic foundation is essential before diving into Taproot. This chapter covers private keys, public keys, and address generation—the building blocks of all transactions.

# Chapter environment (load once, reuse in subsequent code cells)
from btcaaron import Key, TapTree
from bitcoinutils.setup import setup
from bitcoinutils.keys import PrivateKey, P2shAddress, P2wpkhAddress
from bitcoinutils.script import Script

The Cryptographic Hierarchy#

Bitcoin’s security is built on a one-way chain:

Private Key (256-bit) → Public Key (ECDSA point) → Address (encoded hash)

Each layer has a clear purpose:

  • Private keys: Prove ownership, used for signing

  • Public keys: Verify signatures, authorize payments

  • Addresses: Convenient for receiving, while hiding public keys

Private Keys: The Foundation of Ownership#

A Bitcoin private key is a 256-bit random integer used to prove ownership. How large is 2^256? More than all atoms in the observable universe.

Generating Private Keys#

Using btcaaron concise API (this book’s example library):

# Example 1: Generate private key (btcaaron)
# Reference: examples/ch01_keys_and_addresses.py

alice = Key.from_wif("cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT")
print(f"Private Key (WIF): {alice.wif}")

Sample output:

Private Key (HEX): e9873d79c6d87dc0fb6a5778633389dfa5c32fa27f99b5199abf2f9848ee0289
Private Key (WIF): L1aW4aubDFB7yfras2S1mN3bqg9w3KmCPSM3Qh4rQG9E1e84n5Bd

The hexadecimal representation contains 64 characters (4 bits each), totaling 256 bits or 32 bytes. This format is mathematically precise but not ideal for storage or import/export.

Wallet Import Format (WIF)#

Wallet Import Format (WIF) uses Base58Check encoding to make private keys more practical:

  • Adds checksum for error detection

  • Eliminates confusing characters (0, O, I, l)

  • Standardizes import/export across wallets

The WIF encoding process:

  1. Add version prefix: 0x80 for mainnet, 0xEF for testnet

  2. (Optional) Add compression flag: Append 0x01 if the public key will be compressed; this changes the final Base58 prefix

  3. Calculate checksum: Apply SHA256(SHA256(data)) and take first 4 bytes

  4. Apply Base58 encoding: Convert to human-readable string

WIF Encoding Flow

Figure 1-1: WIF encoding converts 32-byte private key to Base58Check string

The resulting WIF string has distinctive prefixes:

  • L or K: Mainnet private keys

  • c: Testnet private keys

Public Keys: Cryptographic Verification Points#

A public key is a point on the secp256k1 elliptic curve, derived from the private key via elliptic curve multiplication. The math is complex but the code is simple.

ECDSA and secp256k1#

Bitcoin uses the secp256k1 curve for ECDSA. The curve is defined by:

y² = x³ + 7

secp256k1 Elliptic Curve

Figure 1-2: secp256k1 elliptic curve used by Bitcoin

Without diving into the math, understand:

  • Each private key k generates a unique point (x, y) on the curve

  • This relationship is computationally irreversible

  • The curve’s mathematical properties ensure security

Compressed vs Uncompressed Public Keys#

Public keys can be represented in two formats:

Uncompressed format (65 bytes):

04 + x-coordinate (32 bytes) + y-coordinate (32 bytes)

Compressed format (33 bytes):

02/03 + x-coordinate (32 bytes)

Why compress? The elliptic curve property: given x-coordinate and y-parity, the full y can be derived:

  • 02 prefix: y-coordinate is even

  • 03 prefix: y-coordinate is odd

# Example 2: Generate public key + X-only (btcaaron)
# Reference: examples/ch01_keys_and_addresses.py

# Compressed (33 bytes) and x-only (32 bytes) public keys
print(f"Compressed (33 bytes): {alice.pubkey}")
print(f"X-only (32 bytes):     {alice.xonly}")
print(f"Verify: x-only = compressed pubkey minus 02/03 prefix → {alice.pubkey[2:]} == {alice.xonly}")

Sample output:

Compressed:   0250be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3
Uncompressed: 0450be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3...

Modern Bitcoin software uses only compressed public keys—smaller and equally secure.

X-Only Public Keys: Taproot’s Innovation#

Taproot introduces x-only public keys—only the x-coordinate (32 bytes), no y-parity. Benefits:

  • Smaller transactions

  • Faster verification

  • Enables key aggregation

# Example 3: Taproot X-Only public key (reusing alice from examples 1/2)
print(f"Taproot X-only public key (32 bytes): {alice.xonly}")

This is key to Taproot’s efficiency gains; we’ll detail it in later chapters.

Address Generation: From Public Key to Payment Destination#

Bitcoin addresses are not public keys—they are encoded hashes of public keys. Benefits:

  • Privacy: Public key hidden until spending

  • Post-quantum resistance: Hash adds extra protection

  • Error detection: Encoding includes checksum

Address Generation Process#

Address generation follows a similar pattern:

  1. Hash the public key: SHA256 then RIPEMD160 (Hash160)

  2. Add version byte: Indicates address type

  3. Add checksum: For input error detection

  4. Encode: To Base58Check or Bech32 string

Legacy Bitcoin Address Flow

Figure 1-3: Traditional Bitcoin address generation flow

# Example 4: Generate different address types
# Legacy/SegWit/P2SH: bitcoinutils | Taproot: btcaaron

setup('mainnet')
priv = PrivateKey()
pub = priv.get_public_key()

# Legacy / SegWit / P2SH-P2WPKH (bitcoin-utils)
legacy_address = pub.get_address()
segwit_native = pub.get_segwit_address()
segwit_p2sh = P2shAddress.from_script(segwit_native.to_script_pub_key())

# Taproot: btcaaron one-liner
program = TapTree(internal_key=Key.from_wif(priv.to_wif())).build()

print(f"Legacy (P2PKH):     {legacy_address.to_string()}")
print(f"SegWit Native:      {segwit_native.to_string()}")
print(f"SegWit P2SH:        {segwit_p2sh.to_string()}")
print(f"Taproot (P2TR):     {program.address}")

Sample output:

Legacy (P2PKH):     1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
SegWit Native:      bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080
SegWit P2SH:        3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy
Taproot (P2TR):     bc1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h  (~62 chars)

Address Types and Encoding Formats#

Base58Check Encoding#

Base58Check for legacy addresses—eliminates confusing characters and includes checksum:

Excluded characters: 0 (zero), O (capital o), I (capital i), l (lowercase L)

P2PKH (Pay-to-Public-Key-Hash):

  • Prefix: 1

  • Format: Base58Check encoding

  • Usage: Original Bitcoin address format

  • Example: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa

P2SH (Pay-to-Script-Hash):

  • Prefix: 3

  • Format: Base58Check encoding

  • Usage: Multi-signature and wrapped SegWit addresses

  • Example: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy

Bech32 Encoding: SegWit’s Innovation#

Bech32 was introduced with SegWit—better error detection:

P2WPKH (Pay-to-Witness-Public-Key-Hash):

  • Prefix: bc1q

  • Format: Bech32 encoding

  • Advantages: Lower fees, better error detection

  • Example: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080

Bech32m Encoding: Taproot’s Enhancement#

Taproot uses Bech32m, an improved Bech32:

P2TR (Pay-to-Taproot):

  • Prefix: bc1p

  • Format: Bech32m encoding

  • Advantages: Enhanced privacy, script flexibility

  • Example: bc1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h(~62 chars)

Address Format Comparison#

Address Type

Encoding

Data Size

Address Length

Prefix

Primary Use Case

P2PKH

Base58Check

25 bytes

~34 chars

1...

Legacy payments

P2SH

Base58Check

25 bytes

~34 chars

3...

Multi-sig, wrapped SegWit

P2WPKH

Bech32

21 bytes

42-46 chars

bc1q...

SegWit payments

P2TR

Bech32m

33 bytes

58-62 chars

bc1p...

Taproot payments

# Example 5: Verify address formats (brief)
# Reference: code/chapter01/05_verify_addresses.py

setup('mainnet')
priv = PrivateKey()
pub = priv.get_public_key()
legacy = pub.get_address()
segwit = pub.get_segwit_address()
taproot = pub.get_taproot_address()
print(f"P2PKH:  {len(legacy.to_string())} chars | P2WPKH: {len(segwit.to_string())} chars | P2TR: {len(taproot.to_string())} chars")
print(f"P2TR ScriptPubKey: OP_1 + 32B x-only = {len(taproot.to_script_pub_key().to_hex())//2} bytes")

Address encoding has many details—version bytes, checksums, different encodings—but one core idea:

👉 Addresses are for humans. They’re just friendly representations of locking scripts (scriptPubKey); the protocol doesn’t need them.

The prefix (1, 3, bc1q, bc1p) reveals the script. Nodes don’t see ‘addresses’—only scripts.

Later chapters focus on what matters: the scriptPubKey behind each address type. That’s where the logic lives—Bitcoin Script and programmability. Infer the script behind an address to understand how it’s spent.

Derivation Model#

Understanding the full derivation from private key to address is important. The diagram shows the flow from private key to on-chain script. Users see addresses; developers must trace each step to understand how Bitcoin verifies ownership and spends.

Key-Public Key-Address Relationship

Figure 1-4: Full derivation model from private key to address

Private Key (k)
    ↓ ECDSA multiplication
Public Key (x, y)
    ↓ SHA256 + RIPEMD160
Public Key Hash (20 bytes)
    ↓ Version + Checksum + Encoding
Address (Base58/Bech32)
  ↓ Decoded by wallet/node
ScriptPubKey (locking script on-chain)

Security properties:

  • Forward: Each step is quick to compute

  • Reverse: Each step is infeasible to reverse

  • Collision resistance: Different pubkeys producing same address is negligible

Practical Implications for Taproot#

As we’ll see, Taproot builds on these foundations:

  • X-only public keys: Save space, enable key aggregation

  • Bech32m encoding: Better error detection

  • Unified address appearance: Multi-sig and single-sig look identical—better privacy

Mastering these encodings and key formats prepares you for Taproot’s advanced features—multiple spending conditions can hide in one address.

Chapter Summary#

This chapter covered Bitcoin’s cryptographic foundation:

  • ✅ Private keys are 256-bit random numbers, WIF-encoded for import/export

  • ✅ Public keys are elliptic curve points; compressed format is standard

  • ✅ Addresses are encoded hashes of public keys, not pubkeys themselves

  • ✅ Different address types use different encodings: Base58Check, Bech32, Bech32m

  • ✅ Taproot introduces x-only pubkeys and Bech32m for greater efficiency

These components—keys, hashes, encodings—are what Bitcoin Script operates on. Next chapter: how they work with Bitcoin Script—the language that defines spending conditions and underpins Taproot.