๐ Bitcoin Algorithms
The mathematical and computational algorithms that power the Bitcoin network
SHA-256
Complexity: O(1)Secure Hash Algorithm 256-bit โ the primary hashing function used in Bitcoin. Converts any input (any size) into a fixed 256-bit (32-byte) output. It's deterministic, collision-resistant, and one-way.
import hashlib
# Single SHA-256
hash1 = hashlib.sha256(b"Hello Bitcoin").hexdigest()
# Double SHA-256 (Bitcoin standard)
hash2 = hashlib.sha256(hashlib.sha256(b"Hello Bitcoin").digest()).hexdigest()
print(f"Double SHA-256: {hash2}")
RIPEMD-160
Complexity: O(1)RACE Integrity Primitives Evaluation Message Digest 160-bit โ used to shorten SHA-256 hashes from 256 bits to 160 bits for Bitcoin addresses, making them shorter and easier to use.
import hashlib
# SHA-256 then RIPEMD-160 (HASH160)
sha = hashlib.sha256(b"Hello Bitcoin").digest()
ripemd160 = hashlib.new('ripemd160', sha).hexdigest()
print(f"HASH160: {ripemd160}") # 40 characters (160 bits)
ECDSA (secp256k1)
Complexity: O(1)Elliptic Curve Digital Signature Algorithm โ uses the secp256k1 curve to create cryptographic signatures proving ownership of Bitcoin without revealing the private key.
from ecdsa import SigningKey, SECP256k1
# Generate key pair
sk = SigningKey.generate(curve=SECP256k1)
vk = sk.verifying_key
# Sign a transaction hash
tx_hash = b"transaction_data"
signature = sk.sign(tx_hash)
# Verify signature
is_valid = vk.verify(signature, tx_hash)
print(f"Signature valid: {is_valid}")
HMAC-SHA512
Complexity: O(1)Hash-based Message Authentication Code โ used in HD wallet key derivation (BIP32) to generate child keys from a master seed, creating a hierarchical tree of keys.
import hmac
import hashlib
# Derive child key from parent
master_key = b"master_secret_256_bits"
chain_code = b"chain_code"
index = 0
# HMAC-SHA512
h = hmac.new(chain_code, master_key + index.to_bytes(4, 'big'), hashlib.sha512)
child_key = h.digest()[:32] # First 32 bytes = child key
child_chain = h.digest()[32:] # Next 32 bytes = new chain code
Proof of Work (PoW)
Complexity: O(n) (hash attempts)Proof of Work โ requires miners to expend computational energy to find a nonce that produces a hash below the target. This secures the network, creates new blocks, and makes attacks economically infeasible.
def proof_of_work(block_header, target):
nonce = 0
while True:
attempt = block_header + str(nonce)
block_hash = double_sha256(attempt)
if int(block_hash, 16) < target:
return nonce, block_hash
nonce += 1
# Target determines difficulty (more zeros = harder)
target = 0x0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a9
nonce, block_hash = proof_of_work(header, target)
Nakamoto Consensus
Complexity: O(n) (chain comparison)Nakamoto Consensus โ the longest chain rule that determines which blockchain is the "true" history. Nodes always follow the chain with the most cumulative Proof of Work, solving the Byzantine Generals Problem.
def get_longest_chain(chains):
longest = None
max_work = 0
for chain in chains:
total_work = sum(block.difficulty for block in chain)
if total_work > max_work:
max_work = total_work
longest = chain
return longest
# Nodes always build on the longest valid chain
def choose_best_chain(chain_a, chain_b):
if get_total_work(chain_a) > get_total_work(chain_b):
return chain_a
return chain_b
Difficulty Adjustment
Complexity: O(1)Difficulty Adjustment Algorithm โ automatically adjusts the mining target every 2,016 blocks to maintain a consistent 10-minute block time, regardless of total network hashrate.
def adjust_difficulty(prev_difficulty, actual_time, expected_time=1209600):
# expected_time = 2016 blocks ร 600 seconds = 1,209,600 seconds
ratio = actual_time / expected_time
ratio = max(0.25, min(4.0, ratio)) # Cap at 4x or 0.25x
new_difficulty = prev_difficulty / ratio
return new_difficulty
# Example: Blocks found too fast (600,000 seconds instead of 1,209,600)
new_diff = adjust_difficulty(80_000_000_000, 600000)
print(f"New Difficulty: {new_diff:.0f}") # Difficulty increases
Merkle Tree
Complexity: O(log n) verificationBinary Hash Tree โ summarizes all transactions in a block into a single 32-byte Merkle Root, enabling efficient SPV verification without downloading all transactions.
def build_merkle_root(transactions):
if len(transactions) == 1:
return double_sha256(transactions[0])
next_level = []
for i in range(0, len(transactions), 2):
if i + 1 < len(transactions):
combined = transactions[i] + transactions[i+1]
else:
combined = transactions[i] + transactions[i] # Duplicate odd
next_level.append(double_sha256(combined))
return build_merkle_root(next_level)
# 4 transactions become 1 root
txs = [hash_tx(tx1), hash_tx(tx2), hash_tx(tx3), hash_tx(tx4)]
merkle_root = build_merkle_root(txs)
Merkle Proof
Complexity: O(log n) sizeMerkle Proof (Authentication Path) โ allows a light client to verify that a specific transaction is included in a block using only O(log n) hashes, not the entire block.
def verify_merkle_proof(tx_hash, proof, merkle_root):
current = tx_hash
for sibling, direction in proof:
if direction == 'left':
current = double_sha256(sibling + current)
else:
current = double_sha256(current + sibling)
return current == merkle_root
# For a block with 4,000 transactions, proof is only ~12 hashes (384 bytes)
proof = [("hash_of_sibling", "right"), ("hash_of_parent", "left")]
is_valid = verify_merkle_proof(tx_hash, proof, root)
Base58Check
Complexity: O(n)Base58Check โ encodes binary data (like addresses) into human-readable strings, removing visually ambiguous characters (0, O, I, l) and adding a checksum to prevent typos.
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
# No 0, O, I, l โ prevents visual confusion
def base58_encode(data):
value = int.from_bytes(data, 'big')
encoded = ''
while value > 0:
value, remainder = divmod(value, 58)
encoded = ALPHABET[remainder] + encoded
return encoded
# Base58Check includes 4-byte checksum
def base58check_encode(payload):
checksum = double_sha256(payload)[:4]
return base58_encode(payload + checksum)
Bech32
Complexity: O(n)Bech32 (BIP173) โ newer address format for SegWit addresses (bc1...). Uses a BCH error correction code for better error detection and is entirely lowercase.
# Bech32 address format: bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq
# hrp (human-readable part) = "bc" (Bitcoin mainnet)
# separator = "1"
# data = witness program (variable length)
# checksum = BCH code (6 characters)
# Advantages over Base58Check:
# - Better error detection (can detect up to 4 errors)
# - All lowercase (easier to read/type)
# - More efficient encoding
Gossip Protocol
Complexity: O(nยฒ) propagationGossip (Epidemic) Protocol โ peer-to-peer communication where nodes broadcast transactions and blocks to their connected peers, propagating data globally within seconds.
class Node:
def __init__(self, peers=[]):
self.peers = peers
self.mempool = []
self.seen = set() # Prevent infinite loops
def broadcast_transaction(self, tx):
if tx.id in self.seen:
return
self.seen.add(tx.id)
for peer in self.peers:
peer.receive_transaction(tx)
def receive_transaction(self, tx):
if self.validate_transaction(tx):
self.mempool.append(tx)
self.broadcast_transaction(tx) # Propagate to neighbors
Compact Block Relay (BIP152)
Complexity: O(n)Compact Block Relay (BIP152) โ reduces block propagation bandwidth by sending only short transaction identifiers instead of full transactions that peers already have.
# Instead of sending full block (1-4 MB):
# Send compact block with 6-byte transaction IDs (~10-40 KB)
# Peers reconstruct missing transactions from mempool
# Sender side:
compact_block = {
'header': block_header,
'short_ids': [calculate_short_id(tx) for tx in block_txs],
'missing_txs': [] # Only for txs not in peer's mempool
}
# Receiver side:
missing = [tx for tx in compact_block.short_ids if tx not in mempool]
request_missing(missing) # Only download missing few
UTXO Selection (Coin Selection)
Complexity: O(n) (knapsack)Coin Selection Algorithm โ selects which UTXOs to spend to achieve a target amount while minimizing fees, avoiding dust, and preserving privacy.
def coin_selection(utxos, target, fee_rate):
# Sort by value (smallest first for knapsack)
utxos = sorted(utxos, key=lambda x: x['value'])
# Strategy 1: Use smallest UTXOs first (good for privacy)
selected = []
total = 0
for utxo in utxos:
selected.append(utxo)
total += utxo['value']
if total >= target + estimate_fee(selected, fee_rate):
return selected
# Strategy 2: Use single UTXO (simplest, best for privacy)
for utxo in utxos:
if utxo['value'] >= target + estimate_fee([utxo], fee_rate):
return [utxo]
# Strategy 3: Use largest UTXOs (fallback)
return select_largest_utxos(utxos, target)
Fee Estimation
Complexity: O(n)Fee Estimation Algorithm โ predicts the optimal transaction fee based on mempool congestion and recent block inclusion data to achieve desired confirmation time.
def estimate_fee(target_blocks=6):
# Analyze recent blocks to determine fee rates
recent_fees = []
for block in last_n_blocks(100):
for tx in block.transactions:
recent_fees.append(tx.fee_rate)
recent_fees.sort()
# Calculate fee for desired confirmation time
return {
'fast': recent_fees[int(len(recent_fees) * 0.90)], # 90th percentile
'medium': recent_fees[int(len(recent_fees) * 0.50)], # 50th percentile
'slow': recent_fees[int(len(recent_fees) * 0.10)] # 10th percentile
}
# Mempool-based estimation (more accurate)
def estimate_from_mempool(mempool, target_blocks):
# Simulate block inclusion based on fee rate
sorted_txs = sorted(mempool, key=lambda x: x.fee_rate, reverse=True)
block_size = 0
for tx in sorted_txs:
block_size += tx.vsize
if block_size >= 1_000_000: # 1 MB block target
return tx.fee_rate
SHA-256
How Bitcoin creates unique fingerprints for data
What is SHA-256 in simple terms?
Think of SHA-256 as a unique fingerprint generator for digital data. No matter how long or short your input is, SHA-256 always produces a fixed 64-character "fingerprint" called a hash. Change even one letter, and the fingerprint becomes completely different.
โ Same input always produces the same output (deterministic)
โ Tiny input change = completely different output (avalanche effect)
โ You cannot reverse a hash back to the original input (one-way)
โ No two different inputs should share the same hash (collision resistant)
Why does Bitcoin need SHA-256?
Bitcoin relies on SHA-256 for three critical jobs:
- Linking blocks: Each block stores the hash of the block before it โ creating a chain. Changing one block would break every hash after it, making tampering instantly obvious.
- Mining (proof of work): Miners must find a hash that starts with many zeros. The only way is to guess billions of times โ that effort is what makes Bitcoin secure.
- Transaction IDs: Every transaction gets a unique ID (TXID) derived from a SHA-256 hash, so you can look up any transaction on the blockchain.
Step 1: input preparation (padding)
Before SHA-256 can hash your message, it must prepare it. This preparation is called padding, and it ensures every message is exactly the right size for processing.
1. Add a
1 bit to the end โ marks where the real message ends2. Add
0 bits until total length = 448 bits (512 โ 64, saving room for step 3)3. Add the original message length as a 64-bit number at the very end
Walkthrough with "abc":
Now the message is a perfect 512-bit block, and SHA-256 can begin its work. If your original message were longer than 448 bits, it would simply be split across multiple 512-bit blocks โ each one padded and processed in turn.
Step 2: breaking into chunks
The padded message is split into 512-bit chunks. Each chunk is divided into 16 smaller pieces called words, each exactly 32 bits long.
If your message were longer โ say, a whole paragraph โ it would be split into multiple 512-bit chunks, each processed one after the other.
Step 3: creating 64 words (message schedule)
SHA-256 needs 64 rounds of mixing, but we only have 16 words. So it generates 48 more words from the original 16, giving us 64 words total (W0โW63).
ฯ0 = (W[tโ15] rotate right 7) XOR (W[tโ15] rotate right 18) XOR (W[tโ15] shift right 3)
ฯ1 = (W[tโ2] rotate right 17) XOR (W[tโ2] rotate right 19) XOR (W[tโ2] shift right 10)
W[t] = W[tโ16] + ฯ0 + W[tโ7] + ฯ1
Step 4: initialize working variables
Before the 64 rounds begin, SHA-256 sets up 8 working variables (a through h). These are the "state" that gets shuffled with every round.
These values are fixed and public โ every SHA-256 implementation in the world starts from exactly these numbers.
Step 5: the compression function (64 rounds)
This is the core of SHA-256. For each of the 64 words, the 8 working variables are scrambled through a series of bitwise operations. After 64 rounds, the data is thoroughly mixed.
ฮฃ1 = (e โ 6) XOR (e โ 11) XOR (e โ 25)
Ch = (e AND f) XOR (NOT e AND g)
temp1 = h + ฮฃ1 + Ch + K[t] + W[t]
ฮฃ0 = (a โ 2) XOR (a โ 13) XOR (a โ 22)
Maj = (a AND b) XOR (a AND c) XOR (b AND c)
temp2 = ฮฃ0 + Maj
Then all 8 variables shift:
hโg, gโf, fโe, eโd+temp1, dโc, cโb, bโa, aโtemp1+temp2
Maj (Majority): outputs whichever bit value appears most often among a, b, and c.
These functions introduce non-linearity โ the property that makes the hash impossible to reverse.
Step 6: the round constants K[t]
Each of the 64 rounds uses a unique constant K[t] to ensure every round behaves differently. Without them, multiple rounds could produce the same transformation โ weakening security.
K[0] = 0x428a2f98 (cube root of prime 2)
K[1] = 0x71374491 (cube root of prime 3)
K[2] = 0xb5c0fbcf (cube root of prime 5)
...
K[63] = 0xc67178f2 (cube root of prime 311)
Step 7: producing the final hash
After all 64 rounds, the 8 working variables are added back to the original starting values (from Step 4), then concatenated to form the final 256-bit hash.
Example โ change one letter, get a completely different hash:
The avalanche effect (why it's secure)
The avalanche effect means that flipping even a single bit in the input causes roughly half the output bits to change. This makes SHA-256 completely unpredictable โ you cannot "tweak" a hash by making small changes to the input.
Double SHA-256 (why Bitcoin applies it twice)
Bitcoin doesn't just hash once โ it runs SHA-256 twice in sequence. The output of the first hash becomes the input of the second.
import hashlib
def bitcoin_hash(data: bytes) -> str:
first = hashlib.sha256(data).digest() # first pass โ raw bytes
second = hashlib.sha256(first).hexdigest() # second pass โ hex string
return second
# Used for TXIDs, Block Header hashes, and Merkle tree nodes
ECDSA (secp256k1)
How Bitcoin proves you own your money without revealing your password
What is ECDSA in simple terms?
ECDSA is like a digital wax seal that proves YOU signed a document.
Imagine you have a special stamp (your Private Key). When you stamp a document, you create a unique seal (Signature). Anyone can look at the seal and verify it came from your stamp using your public ID (Public Key). But they can't recreate your stamp just by looking at the seal.
โ Only the Private Key holder can create a valid signature
โ Anyone with the Public Key can verify the signature
โ You cannot reverse-engineer the Private Key from the signature
โ Each signature is unique, even for the same message
Why does Bitcoin need ECDSA?
Bitcoin uses ECDSA for ONE critical purpose:
- ๐ฐ Proving Ownership: When you send Bitcoin, you must prove you own the address. ECDSA creates a mathematical proof that only you can create.
Step 1: understanding the elliptic curve (the "playing field")
ECDSA works on a special mathematical curve called secp256k1. The equation is:
This curve looks like a mirror-image loop. But we're only interested in points where both x and y are whole numbers (integers) modulo a huge prime number.
p = 2ยฒโตโถ - 2ยณยฒ - 977
p = 115792089237316195423570985008687907853269984665640564039457584007913129639747
Step 2: the generator point G (the starting line)
There's a special starting point on the curve called G (the Generator). Everyone in Bitcoin uses the same G.
Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
Step 3: generating your Private Key (your secret)
Your Private Key is simply a random 256-bit number (between 1 and about 1.16 ร 10โทโท).
d = 0x1e99423a4ed27608a15a2616a2b0e9e52ced330ac530edcc32c8ffc6a526aedd
Step 4: generating your Public Key (your identity)
Your Public Key is derived by "multiplying" the Generator Point (G) by your Private Key (d):
This means: start at G, then jump to the next point d times. This is called scalar multiplication.
# Python concept (simplified)
def private_to_public(private_key):
# Start at generator point G
result = G
# "Add" G to itself private_key times
for _ in range(private_key):
result = point_addition(result, G)
return result
# In reality, this is done using a much faster algorithm called
# "double-and-add" โ O(log n) instead of O(n)
Public Key formats (compressed vs uncompressed)
Public Keys can be represented in two ways:
0x04 || X (32 bytes) || Y (32 bytes)
0x02 (if Y is even) or 0x03 (if Y is odd) || X (32 bytes)
# Example: Compressed public key (33 bytes)
03 = prefix (Y is odd)
79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 = X coordinate
# Most modern wallets use compressed public keys for efficiency
Step 5: signing a transaction (creating your "seal")
To prove you own the Bitcoin, you must sign the transaction. Here's how:
- Hash the transaction: Create a unique fingerprint (z) of the transaction using SHA-256
- Pick a random number (k): This is a temporary secret, called the nonce
- Calculate R = k ร G: This gives you a point on the curve
- Calculate r = x-coordinate of R (mod n): Take the x value of that point
- Calculate s = kโปยน ร (z + r ร d) mod n: This is the second part of the signature
Both r and s are 256-bit integers.
def sign(private_key, transaction_hash, k):
# k must be random and NEVER reused!
# Step 1: Calculate R = k ร G
R = scalar_multiply(k, G)
r = R.x % n
# Step 2: Calculate s = kโปยน ร (z + r ร d) mod n
k_inv = modular_inverse(k, n)
s = (k_inv * (transaction_hash + r * private_key)) % n
return (r, s)
Formula if k is reused:
(sโ - sโ) ร k = (zโ - zโ) mod n โ solve for k โ then d = (sโ ร k - zโ) / r
This is how the PlayStation 3's signing key was stolen in 2010.
Step 6: verifying a signature (anyone can do this)
Anyone can verify that you signed the transaction using only your Public Key (Q). No secret information needed.
- Check that r and s are between 1 and n-1 (valid range)
- Calculate w = sโปยน mod n (modular inverse of s)
- Calculate uโ = (z ร w) mod n
- Calculate uโ = (r ร w) mod n
- Calculate P = uโ ร G + uโ ร Q (point addition on the curve)
- Check if the x-coordinate of P equals r
If not โ Signature is INVALID!
def verify(public_key, transaction_hash, signature):
r, s = signature
# Check range
if not (1 <= r < n) or not (1 <= s < n):
return False
# Calculate modular inverse of s
w = modular_inverse(s, n)
# Calculate u1 and u2
u1 = (transaction_hash * w) % n
u2 = (r * w) % n
# Calculate P = u1รG + u2รQ
P = point_addition(
scalar_multiply(u1, G),
scalar_multiply(u2, public_key)
)
# Verify
return P.x == r
Remember: Q = d ร G (Public Key = Private Key ร Generator)
Also: s = kโปยน ร (z + r ร d) โ multiply both sides by k:
s ร k = z + r ร d โ multiply both sides by sโปยน:
k = sโปยน ร (z + r ร d) = uโ + uโ ร d
Then: P = uโ ร G + uโ ร Q = uโ ร G + uโ ร (d ร G) = (uโ + uโ ร d) ร G = k ร G = R
So x-coordinate of P must equal r. The math proves itself!
The order n (the "wrap-around" number)
Every elliptic curve has an order n โ the number of times you can add G to itself before you return to the starting point.
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
Low-S signatures (BIP62)
ECDSA signatures can have two valid representations: (r, s) and (r, n - s). Both are mathematically valid, but this creates transaction malleability โ someone could change your transaction's signature without invalidating it.
This eliminates one of the two possible representations.
def enforce_low_s(r, s, n):
# Ensure s is the "low" form
if s > n // 2:
s = n - s
return (r, s)
# Most wallets now produce low-s signatures by default
Real Bitcoin transaction example
Let's look at the actual signature from the first Bitcoin transaction ever made (Satoshi to Hal Finney):
f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16
3045022100bcbe87cdc8aaf78e89c373a1528abd9fec9f45e78bc699e13b569a4c854b719d02206ffdeb0af05b61e7c81d4b8648339a3edbb6b23598233e7f6ab2aa5415816f23
30 โ DER signature header
45 โ Length of signature (69 bytes)
02 โ Integer marker for r
21 โ Length of r (33 bytes โ includes leading zero)
00bcbe87cdc8aaf78e89c373a1528abd9fec9f45e78bc699e13b569a4c854b719d โ r value
02 โ Integer marker for s
20 โ Length of s (32 bytes)
6ffdeb0af05b61e7c81d4b8648339a3edbb6b23598233e7f6ab2aa5415816f23 โ s value
Why secp256k1? (Satoshi's choice)
Satoshi chose the secp256k1 curve for several important reasons:
- Koblitz curve: More efficient for computation than random curves
- No backdoor concerns: Unlike NIST curves (which were designed by government agencies), secp256k1's parameters are "nothing up my sleeve" โ they come from simple, verifiable constants
- Performance: Faster point multiplication due to the specific form (a=0)
- 2ยฒโตโถ proximity: The field size is extremely close to 2ยฒโตโถ, making operations efficient on 256-bit computers
a = 0
b = 7
p = 2ยฒโตโถ - 2ยณยฒ - 977
n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
sec = Standards for Efficient Cryptography
p = Prime field
256 = 256-bit field
k = Koblitz curve
1 = Version 1
RIPEMD-160
Making Bitcoin addresses shorter and safer
What is RIPEMD-160 in simple terms?
RIPEMD-160 is a hashing algorithm that produces 160-bit (20-byte) outputs โ about 40 hex characters.
Think of it as a "short fingerprint" generator. While SHA-256 produces a 64-character fingerprint, RIPEMD-160 produces a 40-character version. Both uniquely identify data, but the shorter one is much easier for humans to read, write, and share.
โ Fixed 160-bit output (20 bytes / 40 hex chars)
โ One-way function (cannot reverse)
โ Collision-resistant (no two inputs produce same output)
โ Designed as a backup to SHA-1
Why doesn't Bitcoin use SHA-256 alone for addresses?
A SHA-256 hash of a public key is 64 hex characters (256 bits). Bitcoin addresses would look like:
That's far too long for people to use comfortably! By applying RIPEMD-160, we get:
The HASH160 process (how Bitcoin creates addresses)
Bitcoin combines SHA-256 and RIPEMD-160 in a process called HASH160:
Step 2: RIPEMD-160(Result) โ 160-bit result
def hash160(public_key_bytes):
# Step 1: SHA-256
sha256_hash = hashlib.sha256(public_key_bytes).digest()
# Step 2: RIPEMD-160
ripemd160_hash = hashlib.new('ripemd160', sha256_hash).digest()
return ripemd160_hash # 20 bytes (160 bits)
# Example output (this becomes the core of a Bitcoin address)
# 0x00 + hash160 + checksum โ Base58Check โ 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
How RIPEMD-160 works (the "parallel" design)
RIPEMD-160 processes data in 512-bit blocks, similar to SHA-256. However, it has a unique parallel processing design for extra security.
Left line: A, B, C, D, E (5 registers, 32 bits each)
Right line: A', B', C', D', E' (5 registers, 32 bits each)
Total: 10 registers processing in parallel
โ 5 rounds of 16 steps each (80 total steps per line)
โ Each round uses different Boolean functions
โ After all rounds, left and right results are combined
Round 1: F = (X โ Y โ Z) โ XOR (parallel)
Round 2: F = (X โง Y) โจ (ยฌX โง Z) โ selection
Round 3: F = (X โจ ยฌY) โ Z โ majority-like
Round 4: F = (X โง Z) โจ (Y โง ยฌZ) โ selection
Round 5: F = X โ (Y โจ ยฌZ) โ XOR with OR
Step-by-step: how RIPEMD-160 processes data
Like SHA-256, RIPEMD-160 follows a similar structure:
- Padding: Message is padded to a multiple of 512 bits (same as SHA-256)
- Initialization: 5 registers (A,B,C,D,E) are set to initial values
- Parallel processing: The left and right lines run independently through 5 rounds
- Combination: After 80 steps, left and right results are added to the initial values
- Output: The final 160-bit hash is the concatenation of A,B,C,D,E
A = 0x67452301
B = 0xEFCDAB89
C = 0x98BADCFE
D = 0x10325476
E = 0xC3D2E1F0
Security properties and collision resistance
RIPEMD-160 was designed as a backup hash function in case SHA-1 was broken. Here's how it compares:
โ 160-bit output โ 2โธโฐ security against birthday attacks
โ 2โธโฐ operations is currently impossible with classical computers
โ Quantum computers would reduce this to 2โดโฐ (still somewhat secure)
Real Bitcoin address example
Let's trace how Satoshi's public key became a Bitcoin address:
04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac
0f715b7b5e67e0adabfca9b8b6d1e1f2f3e4f5f6f7f8f9fafbfcfdfeff0f1f2
62e907b15cbf27d5425399ebf6f0fb50ebb88f18
0062e907b15cbf27d5425399ebf6f0fb50ebb88f18
0062e907b15cbf27d5425399ebf6f0fb50ebb88f18 + 88f0e1d2
1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa (Satoshi's Genesis address!)
Why doesn't Bitcoin use ONLY RIPEMD-160?
RIPEMD-160 alone would be weaker than the combined HASH160 approach. Here's why:
- Pre-image resistance: SHA-256 provides 256-bit security, RIPEMD-160 provides 160-bit. The combination gives the stronger of the two.
- Collision resistance: Even if one hash function is weakened, the other still provides protection.
- Historical reasons: When Bitcoin was created, SHA-1 was showing weakness. RIPEMD-160 was a conservative backup.
HMAC-SHA512
Creating a family of keys from one master key
What is HMAC in simple terms?
HMAC (Hash-based Message Authentication Code) is like a secret handshake that only you and someone who knows your secret can perform.
It combines a secret key with a message to create a unique, verifiable hash. Only someone with the same secret key can produce the same HMAC.
HMAC(K, m) = H( (K โ opad) || H( (K โ ipad) || m) )
Where:
opad = 0x5c5c5c... (outer padding)
ipad = 0x363636... (inner padding)
โ = XOR operation
Why does Bitcoin need HMAC-SHA512?
Bitcoin uses HMAC-SHA512 for HD Wallets (Hierarchical Deterministic wallets, BIP32). This allows you to generate thousands of addresses from a single seed phrase.
How HMAC works (the "keyed hash" construction)
HMAC prevents length extension attacks โ a vulnerability where an attacker can append data to a hash without knowing the original message.
Step 1: If key is longer than block size, hash it first
Step 2: Pad key to block size (512 bits for SHA-512)
Step 3: XOR key with ipad (0x36 repeated)
Step 4: Append message and hash
Step 5: XOR key with opad (0x5c repeated)
Step 6: Append previous hash and hash again
def hmac_sha512(key, message):
block_size = 128 # SHA-512 block size (128 bytes = 1024 bits)
# Step 1: If key is longer than block size, hash it
if len(key) > block_size:
key = hashlib.sha512(key).digest()
# Step 2: Pad key to block size
key = key.ljust(block_size, b'\x00')
# Step 3: Create inner and outer padded keys
ipad = bytes([0x36] * block_size)
opad = bytes([0x5c] * block_size)
inner_key = bytes([k ^ i for k, i in zip(key, ipad)])
outer_key = bytes([k ^ o for k, o in zip(key, opad)])
# Step 4: Inner hash
inner_hash = hashlib.sha512(inner_key + message).digest()
# Step 5: Outer hash (final HMAC)
return hashlib.sha512(outer_key + inner_hash).digest()
BIP32 key derivation (how child keys are created)
Bitcoin uses HMAC-SHA512 to derive child keys from parent keys in HD wallets:
HMAC-SHA512(chain_code, parent_key || index)
The output is 512 bits (64 bytes), split into two 256-bit halves:
Last 32 bytes (256 bits): New chain code (for further derivation)
def derive_child_key(parent_key, parent_chain, index):
# Combine parent key with index (4 bytes, big-endian)
data = parent_key + index.to_bytes(4, 'big')
# HMAC-SHA512 with parent chain code as key
hmac_result = hmac.new(parent_chain, data, hashlib.sha512).digest()
# Split into child key and new chain code
child_key = hmac_result[:32] # 256-bit child private key
child_chain = hmac_result[32:] # 256-bit new chain code
return child_key, child_chain
Normal vs Hardened derivation (the apostrophe)
BIP32 defines two types of key derivation:
data = parent_public_key || index
data = 0x00 || parent_private_key || index
Hardened derivation uses the parent private key, so it cannot be reversed. Normal derivation uses the parent public key, which allows anyone with the parent public key to derive child public keys.
BIP44 derivation path (the "address formula")
BIP44 defines a standard path for organizing keys across different cryptocurrencies and accounts:
m: Master seed (your 12/24 words)
purpose': 44' for BIP44 (always)
coin_type': 0' for Bitcoin, 60' for Ethereum, etc.
account': 0', 1', 2'... (separate accounts like "savings" vs "checking")
change: 0 for receiving addresses, 1 for change addresses
address_index: 0, 1, 2... (increment for each new address)
Example: m/44'/0'/0'/0/0
โ โ โ โ โ
โ โ โ โ โโโ First address (index 0)
โ โ โ โโโโโ Receiving address (not change)
โ โ โโโโโโโโโ First account (account 0)
โ โโโโโโโโโโโโโ Bitcoin (coin type 0)
โโโโโโโโโโโโโโโโโโ BIP44 purpose
Seed phrase generation (BIP39)
Your 12 or 24-word seed phrase is the human-readable representation of your master seed. Here's how it works:
- Generate 128-256 bits of entropy (randomness)
- Add a checksum (SHA-256 of entropy, take first bits)
- Split into 11-bit chunks (each chunk indexes into a word list)
- Look up words from the BIP39 English word list (2048 words)
Entropy (128-256 bits) โ Add checksum โ Split into 11-bit groups โ 12-24 words
# Example: 12-word seed phrase
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
# This encodes 128 bits of entropy (all zeros) + checksum
From seed phrase to master key (PBKDF2)
The seed phrase is converted into a master seed using PBKDF2 (Password-Based Key Derivation Function 2) with HMAC-SHA512:
from hashlib import pbkdf2_hmac
def mnemonic_to_seed(mnemonic, passphrase=""):
# Salt is "mnemonic" + optional passphrase
salt = "mnemonic" + passphrase
# PBKDF2 with HMAC-SHA512, 2048 iterations
seed = pbkdf2_hmac(
'sha512',
mnemonic.encode('utf-8'),
salt.encode('utf-8'),
iterations=2048,
dklen=64 # 512-bit seed
)
return seed
# The seed then becomes the master key for your HD wallet
Proof of Work (PoW)
The energy-based consensus mechanism that secures Bitcoin
What is Proof of Work in simple terms?
Proof of Work is like a digital lottery where buying tickets requires real computational energy.
Miners compete to find a special number (nonce) that makes the block hash start with many zeros. The only way to find it is to guess billions of times per second. When someone finds it, they prove they did the work, and the network rewards them with Bitcoin.
Hash(Block Header) < Target
If the hash is lower than the target โ VALID BLOCK!
Why does Bitcoin need Proof of Work?
Proof of Work solves two critical problems in decentralized systems:
- ๐ก๏ธ Sybil Attack prevention: Without PoW, an attacker could create millions of fake nodes to overwhelm the network. PoW requires energy, not identities.
- โฑ๏ธ Byzantine Generals Problem: How do distant nodes agree on the truth? PoW creates an objective measure (most work = truth).
- ๐ฐ No free lunch: Creating blocks costs real money, so miners only produce valid blocks.
Step 1: assembling the block header
Miners collect pending transactions and build a block header. The header is exactly 80 bytes โ no more, no less.
โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Field โ Size โ Description โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Version โ 4 bytes โ Software version โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Previous Block Hash โ 32 bytes โ Link to parent block โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Merkle Root โ 32 bytes โ Summary of all transactions โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Timestamp โ 4 bytes โ Mining time (Unix seconds) โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ nBits (Target) โ 4 bytes โ Difficulty target โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Nonce โ 4 bytes โ Random number (the guess) โ
โโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Step 2: calculating the block hash
Miners calculate the double SHA-256 hash of the block header:
This produces a 256-bit number (64 hex characters).
def calculate_block_hash(version, prev_hash, merkle_root, timestamp, bits, nonce):
# Assemble the 80-byte header
header = (
version.to_bytes(4, 'little') +
bytes.fromhex(prev_hash) +
bytes.fromhex(merkle_root) +
timestamp.to_bytes(4, 'little') +
bits.to_bytes(4, 'little') +
nonce.to_bytes(4, 'little')
)
# Double SHA-256
return hashlib.sha256(hashlib.sha256(header).digest()).hexdigest()
Step 3: understanding the target
The target is a 256-bit number that determines how hard mining is. The block hash must be numerically lower than the target.
0x0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a9
0x00000000... (many zeros) = HARD (like today)
0x0000ffff... (fewer zeros) = EASY (like 2009)
The target is stored in 4 bytes using scientific notation.
nBits = 0x17098f96 โ Target = 0x098f96 ร 256^(0x17 - 3)
Step 4: the mining loop (guessing billions of times)
Miners increment the nonce (and extranonce) billions of times per second until they find a valid hash:
def mine_block(header, target):
nonce = 0
extranonce = 0
while True:
# Update extranonce in coinbase transaction
# (changes the merkle root)
header.update_coinbase(extranonce)
header.update_merkle_root()
# Try all 4 billion nonce values
for nonce in range(2**32):
header.nonce = nonce
block_hash = calculate_block_hash(header)
# Convert hash to integer for comparison
hash_int = int(block_hash, 16)
if hash_int < target:
return header, block_hash
# Exhausted 4 billion nonces, try a new extranonce
extranonce += 1
Difficulty โ how hard mining really is
Difficulty is a human-friendly number that represents how hard mining is relative to the first block in 2009.
Maximum Target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
2009: Difficulty 1 (anyone could mine on a CPU)
2013: Difficulty 1 million (ASICs arrived)
2021: Difficulty 20 trillion
2024: Difficulty ~80 trillion (100 million ร harder than 2009!)
P = (Hashrate ร Block Time) / (2ยฒโตโถ / Difficulty)
For a solo miner with 1 TH/s (10ยนยฒ hashes/sec):
P โ 1 / (2ยฒโตโถ / (10ยนยฒ ร 600 ร 80e12)) โ 1 in 10ยฒโธ per block!
Step 5: broadcast and reward
When a miner finds a valid hash, they broadcast the block to the network. Other nodes verify the work (instantly โ one hash calculation) and if valid, the miner receives:
2009: 50 BTC per block
2012: 25 BTC per block
2016: 12.5 BTC per block
2020: 6.25 BTC per block
2024: 3.125 BTC per block
... continues until 2140
Security analysis โ why 6 confirmations?
Satoshi calculated the probability of an attacker catching up from z blocks behind:
where p = honest mining power, q = attacker power
z=1 โ 11% chance
z=2 โ 1.2% chance
z=3 โ 0.13% chance
z=6 โ 0.0024% chance (1 in 42,000)
What about a 51% attack?
If an attacker controls >50% of total hashrate, they can:
- โ Double-spend: Send coins to an exchange, then mine a longer chain without that transaction
- โ Censor transactions: Refuse to include specific transactions
- โ Prevent confirmations: Make the network unresponsive
They CANNOT:
- โ Steal existing coins (requires private keys)
- โ Create new coins (consensus rules prevent this)
- โ Change history beyond the reorganization window
โ $10-15 billion in hardware + ongoing electricity costs
Nakamoto Consensus
The longest chain rule โ solving the Byzantine Generals Problem
What is Nakamoto Consensus in simple terms?
Nakamoto Consensus is the rule that determines which version of the blockchain is the "true" history.
When different nodes see different blocks, they all follow the chain with the most cumulative Proof of Work โ the chain that required the most energy to build. This creates an objective way to agree on truth without a central authority.
Choose the chain with maximum cumulative difficulty
Cumulative Difficulty = ฮฃ( difficulty of each block )
The Byzantine Generals Problem (why consensus is hard)
The Byzantine Generals Problem is a famous computer science dilemma that describes the difficulty of reaching consensus in a decentralized system where some participants may be malicious.
โ Nodes are generals (no central leader)
โ Transactions are attack orders
โ Malicious nodes are traitors (can lie, double-spend)
โ Question: How do honest nodes agree on the truth?
The longest chain rule (how it works)
Nodes always follow the chain with the most cumulative Proof of Work โ not necessarily the most blocks, but the chain that required the most total energy.
Choose chain with maximum work
def get_best_chain(chains):
best_chain = None
max_work = 0
for chain in chains:
# Sum the difficulty of each block (Proof of Work)
total_work = sum(block.difficulty for block in chain)
if total_work > max_work:
max_work = total_work
best_chain = chain
return best_chain
# Nodes always build on the best chain
def choose_best_chain(chain_a, chain_b):
if get_total_work(chain_a) > get_total_work(chain_b):
return chain_a
return chain_b
Fork resolution (when two miners find blocks simultaneously)
Sometimes two miners find valid blocks at nearly the same time. This creates a temporary fork โ two competing chains.
Chain A: Block 100 โ Block 101A โ Block 102A โ Block 103A (wins)
Chain B: Block 100 โ Block 101B โ stops here (loses)
1. Miner X finds Block 101A, broadcasts it
2. Miner Y finds Block 101B at nearly the same time
3. Different nodes see different blocks first
4. Network splits temporarily (a fork)
5. Next block found โ Miner X finds Block 102A
6. Chain A is now longer (102 blocks vs 101)
7. All nodes switch to Chain A
8. Chain B becomes an orphan block (discarded)
Confirmations โ why you wait
Each new block added on top of a transaction is called a confirmation. The more confirmations, the harder it is to reverse.
0 confirmations: Transaction in mempool (risky for large amounts)
1 confirmation: In the latest block (safe for small amounts)
3 confirmations: Buried under 3 blocks (safe for medium amounts)
6 confirmations: Buried under 6 blocks (~1 hour) โ industry standard
Game theory โ why honest mining is rational
Nakamoto Consensus works because of economic incentives. Miners are rewarded for following the rules and penalized for breaking them.
Attack reward: Temporary double-spend (then Bitcoin becomes worthless)
โ Attack: Double-spend (short-term gain, destroys Bitcoin's value)
โ Mine honestly: Collect steady rewards (long-term profit)
Rational miners choose honesty because a destroyed Bitcoin has no value.
Difficulty Adjustment
Maintaining the 10-minute heartbeat
What is Difficulty Adjustment in simple terms?
Difficulty Adjustment is like a thermostat for Bitcoin mining โ it automatically makes mining harder or easier to keep block times at 10 minutes.
As more miners join the network, blocks would be found faster than 10 minutes. The difficulty adjustment makes the puzzle harder, slowing blocks back down. When miners leave, it makes the puzzle easier, speeding blocks back up.
The method: Adjust difficulty every 2,016 blocks (~2 weeks)
Why is difficulty adjustment necessary?
Without difficulty adjustment, block times would vary wildly as miners join or leave:
More miners โ Faster blocks โ Unstable network
Fewer miners โ Slower blocks โ Long wait times
2009: Difficulty 1, block time ~10 minutes
2024: Difficulty ~80 trillion, block time still ~10 minutes
The network has adjusted 100 million ร harder, yet blocks still come every 10 minutes!
The adjustment formula (how it's calculated)
Every 2,016 blocks (~2 weeks), the network calculates:
Expected Time = 2,016 ร 600 seconds = 1,209,600 seconds (exactly 2 weeks)
def adjust_difficulty(old_difficulty, actual_time_seconds):
expected_time = 2016 * 600 # 1,209,600 seconds
# Calculate ratio (how much faster/slower blocks were found)
ratio = actual_time_seconds / expected_time
# Cap the ratio to prevent extreme changes (safety measure)
# Max 4x harder, max 0.25x easier
ratio = max(0.25, min(4.0, ratio))
# Adjust difficulty
new_difficulty = old_difficulty / ratio
return new_difficulty
# Example 1: Blocks found too fast (600,000 seconds vs 1,209,600)
new_diff = adjust_difficulty(80_000_000_000, 600000)
print(f"New Difficulty: {new_diff:.0f}") # ~160,000,000,000 (doubled!)
# Example 2: Blocks found too slow (1,800,000 seconds)
new_diff = adjust_difficulty(80_000_000_000, 1800000)
print(f"New Difficulty: {new_diff:.0f}") # ~53,000,000,000 (reduced by ~34%)
Difficulty vs Target (inverse relationship)
Difficulty and Target are inversely related:
Maximum Target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
Higher Difficulty โ Lower Target โ Fewer valid hashes โ Harder mining
Lower Difficulty โ Higher Target โ More valid hashes โ Easier mining
Difficulty 1 โ Target = 0x00000000FFFF... (easy)
Difficulty 80 trillion โ Target = 0x000000000000... (very small, hard)
Why 2,016 blocks? (the retargeting interval)
Satoshi chose 2,016 blocks because it's approximately 2 weeks at 10 minutes per block.
โ Long enough to smooth out random variance (mining luck)
โ Short enough to respond to changes in network hashrate
โ 2,016 is exactly 2ยนยน ร 2 (easy to calculate in binary)
If adjustment was too infrequent: Network would be unresponsive to large hashrate changes.
Historical difficulty (how it's evolved)
2009: Difficulty 1 (anyone could mine on a CPU)
2010: Difficulty ~100 (GPU mining begins)
2011: Difficulty ~1,000 (FPGA mining)
2012: Difficulty ~1,000,000 (ASICs arrive)
2013: Difficulty ~10,000,000
2016: Difficulty ~100,000,000,000
2021: Difficulty ~20,000,000,000,000
2024: Difficulty ~80,000,000,000,000
Real example: the 2017 hashrate surge
In 2017, Bitcoin's price surged from $1,000 to $20,000, causing massive miner entry. Hashrate increased rapidly.
During surge: Block times dropped to 5-8 minutes
After adjustment: Difficulty increased to ~1 trillion, block time back to 10 minutes
Merkle Tree
Efficiently summarizing thousands of transactions into a single hash
What is a Merkle Tree in simple terms?
A Merkle Tree is like a family tree of hashes โ it summarizes many transactions into a single "root" hash.
Imagine you have 4 transactions. You hash each one (leaves). Then you hash pairs of leaves together (branches). Then you hash the branches together. You end up with one root hash that represents all 4 transactions.
[Merkle Root]
/ \
[Hash AB] [Hash CD]
/ \ / \
[TX1] [TX2] [TX3] [TX4]
Why does Bitcoin need Merkle Trees?
Merkle Trees enable two critical features in Bitcoin:
- ๐ฆ Compact block headers: The block header stores only the Merkle Root (32 bytes), not all transactions. This keeps headers small (80 bytes).
- ๐ฑ SPV wallets (mobile wallets): Your phone can verify a transaction exists using only a Merkle Proof (few hundred bytes) instead of downloading the entire block (1-4 MB).
Step 1: hash individual transactions (leaves)
Each transaction is hashed using double SHA-256 to create the leaves of the tree:
Leafโ = SHA-256( SHA-256( TXโ ) )
Leafโ = SHA-256( SHA-256( TXโ ) )
Leafโ = SHA-256( SHA-256( TXโ ) )
def hash_transaction(tx_data):
# Double SHA-256 (Bitcoin standard)
return hashlib.sha256(hashlib.sha256(tx_data).digest()).hexdigest()
tx1_hash = hash_transaction(b"Alice pays Bob 1 BTC")
tx2_hash = hash_transaction(b"Charlie pays Dave 2 BTC")
tx3_hash = hash_transaction(b"Eve pays Frank 0.5 BTC")
tx4_hash = hash_transaction(b"Grace pays Henry 0.75 BTC")
leaves = [tx1_hash, tx2_hash, tx3_hash, tx4_hash]
Step 2: pair and hash (create branches)
Pairs of adjacent leaves are concatenated and hashed to form parent nodes:
Parentโ = SHA-256( SHA-256( Leafโ + Leafโ ) )
def hash_pair(hash_a, hash_b):
# Concatenate and double hash
combined = hash_a + hash_b
return hashlib.sha256(hashlib.sha256(combined.encode()).digest()).hexdigest()
hash_ab = hash_pair(leaves[0], leaves[1])
hash_cd = hash_pair(leaves[2], leaves[3])
Step 3: repeat until one root remains
The process continues until only one hash remains โ the Merkle Root:
def build_merkle_root(hashes):
"""Build a Merkle tree and return the root"""
if len(hashes) == 1:
return hashes[0]
next_level = []
for i in range(0, len(hashes), 2):
if i + 1 < len(hashes):
combined = hashes[i] + hashes[i+1]
else:
# Odd number of items โ duplicate the last one
combined = hashes[i] + hashes[i]
next_level.append(hash_pair(combined))
return build_merkle_root(next_level)
# Build the full tree
merkle_root = build_merkle_root(leaves)
print(f"Merkle Root: {merkle_root}")
Space savings (why this is revolutionary)
Without Merkle tree: Need to store all 4,000 TXIDs (128 KB)
With Merkle tree: Store only the root (32 bytes)
Space savings: 99.98%!
- Version: 4 bytes
- Previous Block Hash: 32 bytes
- Merkle Root: 32 bytes
- Timestamp: 4 bytes
- nBits: 4 bytes
- Nonce: 4 bytes
Merkle Proof
Proving a transaction exists without downloading the whole block
What is a Merkle Proof in simple terms?
A Merkle Proof is like a "receipt" that proves a specific transaction is in a block without downloading the entire block.
It contains the target transaction hash and the "sibling" hashes needed to recompute the Merkle Root. If the recomputed root matches the block header's root, the transaction is proven to be in that block.
Why does Bitcoin need Merkle Proofs?
Merkle Proofs enable SPV (Simplified Payment Verification) wallets โ mobile wallets that don't download the full blockchain.
- ๐ฑ Mobile wallets: Your phone stores only block headers (80 bytes each). When you receive a payment, you request a Merkle Proof from a full node.
- ๐ Trust-minimized verification: You don't need to trust the full node โ the math proves the transaction exists.
- โก Fast sync: Your wallet can verify payments in seconds instead of downloading years of history.
How a Merkle Proof works (step by step)
To prove TXโ is in the block, you only need:
1. TXโ (the target transaction hash)
2. TXโ (its sibling โ the hash next to it)
3. Hash AB (the sibling of the parent branch)
def verify_merkle_proof(tx_hash, proof, merkle_root):
"""
tx_hash: The transaction hash to verify
proof: List of (sibling_hash, direction) tuples
merkle_root: The root from the block header
"""
current_hash = tx_hash
for sibling, direction in proof:
if direction == 'left':
# Sibling is on the left: Hash(Sibling + Current)
current_hash = hash_pair(sibling, current_hash)
else:
# Sibling is on the right: Hash(Current + Sibling)
current_hash = hash_pair(current_hash, sibling)
return current_hash == merkle_root
# Example proof for TXโ
tx_hash = "hash_of_TX3"
proof = [
("hash_of_TX4", "right"), # sibling of TX3
("hash_of_AB", "left") # sibling of parent
]
merkle_root = "root_from_header"
is_valid = verify_merkle_proof(tx_hash, proof, merkle_root)
print(f"Transaction verified: {is_valid}")
Visual example (proving TXโ exists)
[Merkle Root]
/ \
[Hash AB] [Hash CD] โ sibling of parent
/ \ / \
[TX1] [TX2] [TX3] [TX4] โ sibling of TX3
โ
target
1. Start with TXโ
2. Hash TXโ + TXโ (sibling on right) โ get Hash CD
3. Hash Hash AB (sibling on left) + Hash CD โ get Merkle Root
4. Compare with root from block header
5. Match โ TXโ is in the block!
Proof size (why it's efficient)
2 transactions โ 1 hash in proof
4 transactions โ 2 hashes in proof
8 transactions โ 3 hashes in proof
4,000 transactions โ ~12 hashes in proof (~384 bytes)
1,000,000 transactions โ ~20 hashes in proof (~640 bytes)
Base58Check
Human-readable address encoding with error detection
What is Base58Check in simple terms?
Base58Check is a way to encode Bitcoin addresses so they're easy to read, hard to mistype, and include error detection.
It removes visually confusing characters (0, O, I, l) and adds a checksum that catches typos before you send money to the wrong address.
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
Notice: No 0 (zero), O (capital o), I (capital i), l (lowercase L)
Why does Bitcoin need Base58Check?
Bitcoin addresses are binary data (20 bytes + version + checksum). Base58Check converts this binary into a human-readable string that:
- ๐ Is easy to read and share (no confusing characters)
- โ Has built-in error detection (catches typos)
- ๐ข Is shorter than hex (34 chars vs 40 hex chars)
- ๐ Self-validating (invalid addresses are rejected by wallets)
Raw binary: 25 bytes
Hexadecimal: 50 characters
Base58Check: ~34 characters
How Base58 encoding works
Base58 works like converting a number to a different base (base-58 instead of base-10 or base-16).
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
def base58_encode(data):
"""Encode bytes to Base58 string"""
# Convert bytes to a big integer
value = int.from_bytes(data, 'big')
encoded = ''
while value > 0:
value, remainder = divmod(value, 58)
encoded = ALPHABET[remainder] + encoded
# Add leading '1's for each leading zero byte
for byte in data:
if byte == 0:
encoded = '1' + encoded
else:
break
return encoded
# Example: Encode the number 100
print(base58_encode(b'\x64')) # '2q' (100 in base-58)
- Base-64 includes ambiguous characters (+ and /)
- Base-32 is less efficient (more characters)
- Base-58 is the sweet spot: efficient + unambiguous
The checksum (how errors are detected)
Base58Check adds a 4-byte checksum to the end of the payload:
def base58check_encode(payload):
# Step 1: Add version byte (0x00 for Bitcoin mainnet)
versioned = b'\x00' + payload
# Step 2: Calculate checksum (first 4 bytes of double SHA-256)
checksum = hashlib.sha256(hashlib.sha256(versioned).digest()).digest()[:4]
# Step 3: Append checksum and Base58 encode
return base58_encode(versioned + checksum)
# Example: Creating a Bitcoin address
public_key_hash = bytes.fromhex('62e907b15cbf27d5425399ebf6f0fb50ebb88f18')
address = base58check_encode(public_key_hash)
print(f"Address: {address}") # 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
Bitcoin address example (Genesis address)
1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
- Starts with '1' โ Mainnet P2PKH address
- 34 characters total
- Contains no confusing characters (0, O, I, l)
- Last 6 characters are part of the checksum
1 โ Legacy P2PKH (old addresses)
3 โ P2SH (multi-sig, compatibility)
bc1 โ Bech32 (SegWit, modern addresses)
bc1p โ Bech32m (Taproot, newest)
Bech32
Modern SegWit address encoding with BCH error correction
What is Bech32 in simple terms?
Bech32 is a modern address format for Bitcoin that's all lowercase, easier to read, and can actually correct typos.
Unlike Base58Check (which only detects errors), Bech32 uses a special error-correcting code that can fix typos automatically. Bech32 addresses start with "bc1" for Bitcoin mainnet.
bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq
Why does Bitcoin need Bech32?
Bech32 was introduced with SegWit (Segregated Witness) to support native SegWit addresses with several advantages:
- ๐ง Error correction: Can detect up to 4 errors and correct up to 3
- ๐ค All lowercase: Easier to read, type, and QR code
- ๐ฐ Lower fees: Native SegWit addresses are more efficient (smaller transaction size)
- ๐ No ambiguous characters: Like Base58Check, removes confusing letters
- โ Better checksum: BCH code detects more errors than Base58Check
Base58Check: Only detects errors (you must retype)
Bech32: Can actually CORRECT errors (fixes typos automatically!)
Bech32 address structure (the three parts)
A Bech32 address has three distinct parts:
โโ โโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โโ โ
hrp separator data + checksum
hrp (Human Readable Part): "bc" for Bitcoin mainnet
Separator: Always "1" (never appears in data)
Data: Witness program (variable length, usually 32 or 40 characters)
Checksum: 6 characters (BCH error-correcting code)
Legacy (Base58Check): 26-34 characters
Bech32: 42-62 characters (slightly longer, but more features)
BCH error correction (how it fixes typos)
Bech32 uses a BCH (Bose-Chaudhuri-Hocquenghem) code โ a powerful error-correcting code that can:
Error correction: Up to 3 errors (can fix common typos)
Guaranteed detection: Single character errors (always caught)
# Example: Typo correction in Bech32
# Original: bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq
# Typo: bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5m**dq** (swapped last two chars)
# Wallet can automatically correct this! (Base58Check would just say "invalid")
HRP (Human Readable Part) โ what "bc1" means
"bc" = Bitcoin mainnet (production network)
"tb" = Bitcoin testnet (for testing)
"bcrt" = Bitcoin regtest (for development)
bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8qt4acm9lrq2s5a
Taproot addresses use "bc1p" (p for pay-to-taproot) and Bech32m encoding (slightly different checksum).
Data conversion (8 bits โ 5 bits)
Bech32 uses a 5-bit alphabet (32 possible characters), but data is normally 8-bit bytes. So Bech32 converts 8-bit bytes to 5-bit groups.
8-bit bytes โ convert to 5-bit groups โ map to alphabet
# Bech32 alphabet (32 characters)
BECH32_ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def convert_bits(data, from_bits, to_bits, pad=True):
"""Convert between bit lengths (e.g., 8-bit bytes to 5-bit groups)"""
acc = 0
bits = 0
ret = []
maxv = (1 << to_bits) - 1
max_acc = (1 << (from_bits + to_bits - 1)) - 1
for value in data:
acc = ((acc << from_bits) | value) & max_acc
bits += from_bits
while bits >= to_bits:
bits -= to_bits
ret.append((acc >> bits) & maxv)
if pad and bits:
ret.append((acc << (to_bits - bits)) & maxv)
return ret
# Convert witness program (20 bytes) to 5-bit groups
witness_program = bytes.fromhex('62e907b15cbf27d5425399ebf6f0fb50ebb88f18')
five_bit_data = convert_bits(witness_program, 8, 5, pad=True)
SegWit and Bech32 (why they go together)
Bech32 was specifically designed for SegWit (Segregated Witness) addresses. SegWit separates transaction signatures (witness data) from transaction data, which:
- ๐ฐ Lowers fees: Witness data is discounted (0.75 bytes per byte)
- ๐ง Fixes malleability: Transaction IDs no longer include signatures
- โก Enables Lightning: Malleability fix made Lightning possible
Transaction weight = 4 ร base size + 1 ร witness size
Base data (non-witness) counts 4ร, witness data counts 1ร
Bech32 SegWit addresses use witness data โ lower fees!
# Fee comparison (example)
Legacy address (P2PKH): 1 input, 2 outputs โ 192 vbytes โ fee: 192 ร 10 = 1920 sats
SegWit address (Bech32): 1 input, 2 outputs โ 144 vbytes โ fee: 144 ร 10 = 1440 sats
# Savings: 25% lower fees!
Bech32m (the Taproot upgrade)
Bech32m is a modified version of Bech32 used for Taproot addresses (bc1p...). It fixes a security issue where some Bech32 addresses could be confused with short data.
Bech32: Used for SegWit (bc1q...)
Bech32m: Used for Taproot (bc1p...)
Taproot version = 1 (witness version 1)
- Better privacy (complex scripts look like simple payments)
- Schnorr signatures (smaller, batchable)
- MAST (only reveals the used spending condition)
Gossip Protocol
How transactions spread across the network in seconds
What is Gossip Protocol in simple terms?
Gossip Protocol is how Bitcoin nodes spread information โ like a rumor spreading through a crowd.
When a node hears a new transaction, it tells its neighbors. Those neighbors tell their neighbors, and within seconds, every node in the world knows about it. There's no central broadcaster โ information spreads peer-to-peer.
Why does Bitcoin need Gossip Protocol?
Without a central server, Bitcoin needs a way for all nodes to learn about new transactions and blocks. Gossip Protocol solves this:
- ๐ No central point of failure: If a node goes down, information routes around it
- โก Fast propagation: Information reaches 90% of nodes in 1-2 seconds
- ๐ก๏ธ Resilient to attacks: Even if some nodes are malicious, honest nodes still communicate
- ๐ Scalable: Works with thousands of nodes
Within 1 second: ~50% of nodes
Within 2 seconds: ~90% of nodes
Within 10 seconds: ~99% of nodes
Peer connections (the network mesh)
Each Bitcoin node connects to 8-12 random peers. This creates a mesh network where information can spread quickly.
Node A connects to B, C, D
Node B connects to A, E, F
Node C connects to A, G, H
Node D connects to A, I, J
Information spreads like a rumor through the mesh.
class BitcoinNode:
def __init__(self):
self.peers = [] # List of connected nodes
self.seen_transactions = set() # Avoid infinite loops
self.mempool = [] # Unconfirmed transactions
def add_peer(self, peer):
self.peers.append(peer)
peer.peers.append(self) # Bidirectional connection
def broadcast_transaction(self, tx):
# Prevent infinite loops
if tx.id in self.seen_transactions:
return
self.seen_transactions.add(tx.id)
self.mempool.append(tx)
# Tell all peers about the transaction
for peer in self.peers:
peer.receive_transaction_inv(tx.id)
def receive_transaction_inv(self, tx_id):
# Request the full transaction if we don't have it
if tx_id not in self.seen_transactions:
self.request_transaction(tx_id)
Inventory messages (INV) โ efficient broadcasting
Nodes don't broadcast full transactions immediately. They first send an INV (inventory) message containing just transaction hashes (32 bytes each).
[txidโ, txidโ, txidโ, ...] (each 32 bytes)
# INV message example
inv_message = {
"type": "inv",
"inventory": [
"f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
]
}
# Peer responds with GETDATA for transactions they don't have
if tx_id not in local_mempool:
send_message("getdata", tx_id)
Full transaction: ~250-500 bytes
INV message: 32 bytes per transaction โ 90%+ bandwidth savings!
Anti-spam mechanisms (stopping bad rumors)
To prevent spam and denial-of-service attacks, nodes follow strict rules:
- ๐ Validation first: Don't relay transactions you haven't validated
- ๐ Stop bad rumors: Don't relay invalid transactions (ban the source)
- ๐ฆ Rate limiting: Limit how many messages a peer can send
- โ ๏ธ Ban malicious peers: Disconnect and ban peers sending invalid data
Compact Block Relay (BIP152)
Reducing block propagation bandwidth
What is Compact Block Relay in simple terms?
Compact Block Relay is like sending only the "missing pieces" of a puzzle instead of the whole puzzle.
Most nodes already have most transactions in their mempool. Instead of sending the full block (1-4 MB), nodes send only the short IDs of transactions. The receiving node reconstructs the block using transactions it already has, requesting only the few it's missing.
Full block: 1-4 MB
Compact block: ~10-40 KB
Savings: 90-95%!
Why does Bitcoin need Compact Block Relay?
Before BIP152 (2017), nodes had to download full blocks from peers. This caused problems:
- ๐ก Bandwidth waste: Blocks contain 90%+ transactions already in mempool
- โฑ๏ธ Slow propagation: Large blocks took seconds to transfer, increasing orphan rates
- ๐ Centralization risk: Only large nodes could afford the bandwidth
After BIP152: 1 MB block โ ~20 KB download (98% savings!)
Short transaction IDs (how to identify transactions efficiently)
Instead of sending full 32-byte TXIDs, compact blocks use 6-byte short IDs (48 bits).
Full TXID: 32 bytes
Short ID: 6 bytes
80% smaller!
def make_short_id(tx_hash, nonce):
"""
Create a 6-byte short ID using SipHash
SipHash is fast, non-cryptographic, and collision-resistant
"""
import siphash
# SipHash with 64-bit output
sip = siphash.SipHash_2_4(key=nonce.to_bytes(16, 'little'))
sip.update(tx_hash)
# Take lowest 48 bits (6 bytes)
return sip.raw_value() & 0xffffffffffff
# Collision probability: ~1 in 2โดโธ (very low, safe for Bitcoin)
Block reconstruction (how it works step by step)
# Sender side (miner)
compact_block = {
'header': block_header,
'short_ids': [make_short_id(tx) for tx in block.transactions],
'prefilled_txns': [] # Transactions not in most peers' mempool
}
# Receiver side (node)
def reconstruct_block(compact_block, mempool):
transactions = []
missing = []
# Try to find each transaction in mempool using short ID
for short_id in compact_block.short_ids:
tx = find_tx_by_short_id(mempool, short_id)
if tx:
transactions.append(tx)
else:
missing.append(short_id)
# Request missing transactions (should be very few)
if missing:
requested_txs = request_missing(missing)
transactions.extend(requested_txs)
# Reconstruct full block
return Block(compact_block.header, transactions)
Bandwidth savings (real numbers)
Block size: 1,000,000 bytes
Propagation to 10 peers: 10 MB
Short IDs: 4,000 ร 6 = 24,000 bytes
Missing transactions: ~200 bytes
Total: ~25 KB per peer
Propagation to 10 peers: 250 KB
Savings: 97.5%!
UTXO Selection (Coin Selection)
Choosing which coins to spend
What is UTXO Selection in simple terms?
UTXO Selection is how your wallet decides which coins (UTXOs) to spend when you make a payment.
Your wallet has many UTXOs of different sizes (like having a $10 bill, a $5 bill, and three $1 bills). To pay $7, you could use the $5 + two $1 bills (3 inputs) or the $10 bill (1 input but you get $3 change). The algorithm chooses the best combination.
- Use $10 + $1 + $1 (3 bills) โ best for privacy, no large change
- Use $20 (1 bill) โ simplest, but you get $8 change (creates new UTXO)
- Use $5 + $5 + $1 + $1 (4 bills) โ worst (many inputs = higher fees)
Why does Bitcoin need UTXO selection?
Unlike a bank account (single balance), Bitcoin uses UTXOs โ many individual "coins" of different values. Your wallet must decide which ones to spend:
- ๐ฐ Fee optimization: More inputs = larger transaction = higher fees
- ๐ Privacy: Using certain UTXOs can reveal your transaction history
- ๐งน Dust management: Small UTXOs ("dust") may cost more to spend than they're worth
- โก Speed: More inputs = more signatures = slower to sign
The knapsack problem (why it's non-trivial)
Target: 0.15 BTC payment
A) 0.1 + 0.05 (2 inputs, 0.00 waste) โ optimal
B) 0.5 (1 input, 0.35 waste) โ simple but creates large change
C) 0.1 + 0.01 + 0.05 (3 inputs, 0.01 waste) โ more fees
D) 1.0 (1 input, 0.85 waste) โ terrible, large change
Branch and Bound (B&B) โ the modern algorithm
Modern wallets (Bitcoin Core, Electrum) use Branch and Bound for optimal selection:
def select_utxos_bnb(utxos, target, fee_rate):
"""
Branch and Bound coin selection
Finds the optimal UTXO set minimizing waste
"""
# Sort by value (largest first for efficiency)
utxos = sorted(utxos, key=lambda x: x.value, reverse=True)
best_selection = None
best_waste = float('inf')
def search(selected, selected_value, effective_value, index):
nonlocal best_selection, best_waste
# Calculate waste (excess value + fees)
waste = (selected_value - target) + fee_rate * len(selected)
# Prune branches that can't be better
if waste < 0 or waste >= best_waste:
return # This path is useless, don't explore further
# Found a solution
if selected_value >= target:
if waste < best_waste:
best_waste = waste
best_selection = selected.copy()
return
# No more UTXOs to consider
if index >= len(utxos):
return
# Branch 1: Include this UTXO
selected.append(utxos[index])
search(selected,
selected_value + utxos[index].value,
effective_value + utxos[index].effective_value,
index + 1)
selected.pop()
# Branch 2: Skip this UTXO
search(selected, selected_value, effective_value, index + 1)
search([], 0, 0, 0)
return best_selection
Privacy considerations (how coin selection affects anonymity)
Largest-first: Use biggest UTXOs first (good privacy, may overpay)
Smallest-first: Use smallest UTXOs first (consolidates dust, may reveal patterns)
Random selection: Harder to trace (best privacy, but unpredictable fees)
B&B with privacy: Adds random variation to selection
Fee Estimation
Predicting optimal transaction fees
What is Fee Estimation in simple terms?
Fee Estimation predicts how much you should pay to get your transaction confirmed within a certain time.
Your wallet looks at the current mempool (unconfirmed transactions), analyzes how long transactions with different fees took to confirm recently, and recommends a fee rate (satoshis per vbyte) that will likely get your transaction into a block soon.
Why does Bitcoin need fee estimation?
Bitcoin has limited block space (1-4 MB per block). When many people want to send transactions, they compete by offering higher fees. Fee estimation helps users:
- โฑ๏ธ Avoid overpaying: Don't pay high fees when network is quiet
- โก Avoid delays: Don't pay too little when network is congested
- ๐ฐ Optimize costs: Balance speed vs. cost based on your needs
The mempool (where unconfirmed transactions wait)
All unconfirmed transactions sit in the mempool (memory pool) โ a waiting room for transactions waiting to be mined. Miners pick the highest-fee transactions first.
Fee rate 50+ sat/vB โ Immediate (next block)
Fee rate 20-50 sat/vB โ 1-3 blocks (10-30 min)
Fee rate 10-20 sat/vB โ 3-10 blocks (30-100 min)
Fee rate 5-10 sat/vB โ 10+ blocks (100+ min)
Fee rate 1-5 sat/vB โ May never confirm during high traffic
How fee estimation works (the algorithm)
class FeeEstimator:
def __init__(self):
self.fee_history = [] # Recent blocks' fee rates
def estimate_fee(self, target_blocks):
"""
Estimate fee rate for target confirmation time
target_blocks: 1 = fastest, 6 = standard, 25+ = slowest
"""
# Collect fee rates from recent blocks
fee_rates = []
for block in self.recent_blocks(100):
# Get minimum fee rate that would have been included in this block
min_fee = block.get_minimum_included_fee()
fee_rates.append(min_fee)
# Sort and pick percentile based on target
fee_rates.sort()
# Faster targets require higher percentiles
if target_blocks == 1:
percentile = 90 # Fastest (90th percentile)
elif target_blocks <= 3:
percentile = 75 # Medium (75th percentile)
elif target_blocks <= 6:
percentile = 50 # Standard (50th percentile)
else:
percentile = 25 # Slow (25th percentile)
# Return the fee rate at that percentile
index = int(len(fee_rates) * percentile / 100)
return fee_rates[index]
90th percentile = 90% of recent transactions had lower fees โ very likely to confirm quickly
50th percentile = average fee โ likely to confirm within standard time
Mempool-based estimation (more accurate)
A more accurate method uses the current mempool state directly:
def estimate_from_mempool(mempool, target_blocks):
"""
Estimate fee by simulating block inclusion from current mempool
"""
# Sort mempool by fee rate (highest first)
sorted_txs = sorted(mempool, key=lambda x: x.fee_rate, reverse=True)
block_size = 0
for tx in sorted_txs:
block_size += tx.vsize
# When we've filled enough blocks, return the fee rate
if block_size >= target_blocks * 1_000_000: # 1 MB per block
return tx.fee_rate
# If mempool is empty, return minimum fee
return 1 # 1 sat/vbyte minimum
Current fee rates (real-time examples)
Urgent (next block): 50+ sat/vB
Fast (1-3 blocks): 20-50 sat/vB
Economy (3-10 blocks): 10-20 sat/vB
Slow (10+ blocks): 5-10 sat/vB
Minimum: 1 sat/vB (may never confirm)
$10 transaction (250 vbytes):
- Urgent: 250 ร 50 = 12,500 sats (~$5)
- Economy: 250 ร 10 = 2,500 sats (~$1)
- Lightning: 0-1 sat (~$0.0001)
Highest: Weekdays (US business hours)
Lowest: Weekends, late nights (UTC)
What if you underpay? (RBF and CPFP)
If your fee estimate was too low, you have two options to speed up a stuck transaction:
CPFP (Child-Pays-For-Parent): Create a new transaction spending the stuck UTXO with high fee
# RBF example: Bump fee from 10 to 50 sat/vB
original_tx.fee_rate = 10
bumped_tx = replace_transaction(original_tx, new_fee_rate=50)
# CPFP example: Parent stuck at 5 sat/vB
parent_tx.fee_rate = 5
child_tx = create_child_transaction(parent_tx, child_fee_rate=100)
# Miners see: parent(5) + child(100) = average 52.5 sat/vB โ includes both!