Day 19 · Proof-of-Work
Day 18 gave every transaction a way to prove it’s legitimate. But legitimacy isn’t agreement. Picture two honest miners on opposite sides of the planet, each assembling a perfectly valid next block at the same instant. Both are valid. Both point at the current tip. Which one is the real next block? With no central server to break the tie, you need a rule that’s objective, that nobody can game by simply asserting it, and that makes rewriting history expensive. That rule is Proof-of-Work, and building it is the moment the whole chain finally answers its founding question: how do strangers converge on one history?
The idea: make blocks costly to produce
Section titled “The idea: make blocks costly to produce”Proof-of-Work attaches a price tag to every block — not in money, but in computation. To publish a block
you must find a nonce (a throwaway number in the header) such that the block’s hash, read as a 256-bit
number, lands below a target. Because double-SHA-256 is effectively random and unpredictable (Day 16),
there is no clever way to find such a nonce — you can only guess, hash, check, repeat. Finding one is proof
you did, on average, a known amount of work. Verifying it is a single hash. That asymmetry — hard to
produce, trivial to check — is the entire trick.
target for bits = 16 (the top 16 bits must be zero): 0000_0000 0000_0000 1111_1111 … 1111_1111 (2^240 − 1) └──── must be 0 ────┘└─ anything below the line passes ─┘
mining: keep changing the nonce, re-hash, until hash ≤ target.Target and difficulty
Section titled “Target and difficulty”The target is a 256-bit threshold; difficulty is how low you set it. The lower the target, the more
leading zeros a passing hash needs, the more guesses it takes. In rust/btcmini/src/pow.rs we keep this as
plain as possible — bits is literally the number of required leading zero bits:
pub fn target_from_bits(bits: u32) -> Hash256 { // the largest 256-bit value with `bits` leading zeros, all lower bits set}
pub fn meets_target(hash: &Hash256, target: &Hash256) -> bool { hash.0 <= target.0 // big-endian: a [u8; 32]'s natural Ord IS numeric comparison}That last line is a small, satisfying piece of Rust: because the bytes are stored big-endian (most
significant first), the array’s built-in lexicographic Ord is exactly numeric comparison of the
256-bit values, so “does this hash meet the target?” is just hash ≤ target. No big-integer library, no
hand-rolled comparison.
The mining loop
Section titled “The mining loop”Mining is the least glamorous loop in the crate, and that’s the point — its security comes from how many
times it runs, not from cleverness. From rust/btcmini/src/block.rs:
pub fn mine(header: &mut BlockHeader) -> u64 { let target = target_from_bits(header.bits); let mut attempts = 0; loop { attempts += 1; if meets_target(&header.hash(), &target) { return attempts; // found a winning nonce } header.nonce = header.nonce.wrapping_add(1); }}Increment the nonce, hash the header, check against the target, repeat until it passes. The attempts
count is the literal work done. The most_nonces_fail_the_target test in block.rs shows the flip side:
scan thousands of nonces at difficulty 16 and the overwhelming majority fail — which is precisely why
finding one that passes is evidence of effort.
The most-work chain rule
Section titled “The most-work chain rule”Now the payoff. When honest nodes see two competing valid blocks, they don’t vote, negotiate, or ask an
authority. Each node simply keeps building on the chain it saw first, and the network resolves the tie the
moment one branch gets the next block — because every node then prefers the branch carrying the most
cumulative Proof-of-Work. Our Blockchain measures and compares exactly that, in
rust/btcmini/src/chain.rs:
pub fn total_work(&self) -> u128 { self.blocks.iter().map(|b| block_work(b.header.bits)).sum()}A node adopts a competing chain only if it’s valid and has strictly more work than its own (the
maybe_adopt logic you’ll wire up on Day 20). This is why a fork is
temporary: branches race, one pulls ahead in accumulated work, and everyone converges on it; the
transactions in the abandoned branch fall back to the mempool to be mined again.
┌── block A4 ──╴ (orphaned: less total work) …─ A3 ───────┤ └── block B4 ── B5 ──╴ (adopted: most work wins)Under the hood — why “longest” really means “most work”
Section titled “Under the hood — why “longest” really means “most work””You’ll often hear “the longest chain wins,” but that’s a shortcut. The rule is most accumulated work, because difficulty changes over time: a short chain of very-hard blocks can embody more total work than a longer chain of easy ones, and the hard one is the legitimate winner. In our toy, difficulty is fixed, so more blocks does mean more work and “longest” and “most-work” coincide — but the code sums work, not block count, on purpose, so the rule stays correct the day difficulty varies. The security assumption underneath all of it: an honest majority of hash power. If one party controls more than half, it can consistently out-work everyone and rewrite recent history — the “51% attack.” Proof-of-Work doesn’t make attacks impossible; it makes them as expensive as outspending the rest of the world combined.
This is the same bargain as the playbook’s thread, viewed from the chain side. What does building this force you to understand? That Bitcoin doesn’t replace trust with magic — it replaces “trust this person” with “trust that no one will burn more electricity than everyone else to lie to you.” Rust strikes the parallel bargain: it doesn’t trust the programmer, it makes the unsafe program fail to compile. Both buy safety by making the bad outcome costly-to-impossible rather than merely forbidden.
Build it
Section titled “Build it”In rust/btcmini:
- Read
src/pow.rsandsrc/block.rs. Runcargo test -- pow blockand watchmining_finds_a_nonce_that_meets_the_targetandmost_nonces_fail_the_target. - Run
cargo run -- demoat itsbits = 12, then bump it to 20 indemo()and feel mining take measurably longer — difficulty is exponential, live. - In
chain.rs, readconnect: note the order of checks — links to the tip, then PoW meets the target, then merkle root, then the transactions and coinbase amount. Then readtotal_workand themost_work_chain_beats_a_shorter_onetest.
Where this leaves us
Section titled “Where this leaves us”The four pillars now stand: a tamper-evident chain, a UTXO ledger, cryptographic ownership, and
Proof-of-Work with the most-work rule. But there’s one last gap between “a chain in one program” and “a
network.” Mining and the most-work rule only converge strangers if those strangers can actually exchange
blocks and transactions. Tomorrow we put the chain on the wire: a std::net TCP node with a mempool, where
one node mines a block, broadcasts it, and a second node — running in a different process — adopts it and
ends up on the exact same chain. The theory becomes two computers agreeing.
→ Next: Day 20 · A P2P Node · Prev: Day 18 · Keys and Signatures · Back to the Project 5 overview
Check your understanding
Section titled “Check your understanding”- What asymmetry makes Proof-of-Work useful, and how does the mining loop exploit it? What is the
noncefor? - What is the relationship between the target and difficulty? In our crate, what does
bitsliterally mean, and how does that differ from real Bitcoin? - Why is
meets_targetjusthash.0 <= target.0? What property of the byte layout makes that correct? - Explain the most-work chain rule. Why does the code sum work rather than count blocks, and what is the honest-majority assumption it rests on?
- What did the March 2013 fork reveal about the phrase “the longest chain wins”? Why couldn’t Proof-of-Work alone resolve it?
Show answers
- Proof-of-Work is hard to produce, trivial to verify: finding a nonce whose block hash is below the
target takes on average
2^bitsrandom guesses (no shortcut, because the hash is unpredictable), but checking it is a single hash. The mining loop just increments thenonce, re-hashes the header, and testshash ≤ targetuntil it passes; thenonceis the throwaway number it varies to search. - The target is a 256-bit threshold and difficulty is how low you set it — a lower target needs more
leading zeros, hence more guesses. In our crate
bitsis literally the count of required leading zero bits; real Bitcoin uses a compact mantissa/exponent target and auto-adjusts difficulty every 2016 blocks to hold ~10-minute blocks. - Because the hash bytes are stored big-endian (most-significant byte first), a
[u8; 32]’s built-in lexicographic ordering is identical to numeric comparison of the 256-bit value — sohash ≤ targetas arrays ishash ≤ targetas numbers, no big-integer math needed. - A node adopts another chain only if it is valid and has strictly more cumulative Proof-of-Work. The code sums work (not block count) because difficulty can vary, and a short chain of hard blocks can outweigh a long chain of easy ones — summing work keeps the rule correct in that case. It assumes an honest majority of hash power; a party with over 50% can out-work everyone and rewrite recent history (the 51% attack).
- It revealed that the rule is really “the most-work valid chain,” and that validity is defined by the rules each node enforces. In March 2013, v0.8 and v0.7 disagreed about whether a block was valid, so they built different histories; Proof-of-Work couldn’t choose between them because the two sides didn’t agree on the validity rules in the first place. It was fixed socially, by miners downgrading to reconverge (BIP 50).