# 03 — Blocks, Hashes, and History You can fingerprint anything now. You can sign a claim. But a single signed note is not yet a **history** — and history is what the whole machine is selling. So here is the next puzzle. How do you assemble fingerprints and signatures into a record that **no one can quietly edit** — one that strangers can walk backward, link by link, and check for themselves? The field's answer is almost anticlimactic: a linked list, where the pointers are hashes. The delight is in how much that one substitution buys. --- ## Blocks as log segments A block is two things: a small **header** everyone can afford to keep, and a **body** of transactions that does the actual work. TypeScript is a convenient way to write down the shapes: ```typescript type Hash = string; type Address = string; interface BlockHeader { parentHash: Hash; // hash of the previous block's header stateRoot: Hash; // commitment to all account state after this block runs transactionsRoot: Hash; // Merkle root over this block's transactions timestamp: number; // + consensus fields (PoW nonce, PoS attestations, etc.) } interface Block { header: BlockHeader; transactions: Transaction[]; // the body — ordered list, hashed into transactionsRoot } ``` The header is the index card ([[#Too big to hold, cheap to verify|later]]). The body is the book. Two hash fields in the header do most of the structural work: | Field | Commits to | |---|---| | `parentHash` | the previous block — chains history | | `transactionsRoot` | this block's transaction list — what got ordered and included | | `stateRoot` | world state *after* executing those transactions — what changed | `parentHash` and `transactionsRoot` are commitments you can verify without re-running anything. `stateRoot` requires execution ([[05 Execution and Smart Contracts]]) — but every node must reach the same one, or the chain forks. ### What a transaction looks like A **transaction** is a signed message that asks the network to change state. On Ethereum-style chains, the shape is: ```typescript interface Transaction { nonce: number; // per-sender counter — replay protection to: Address | null; // null = deploy a new contract value: bigint; // native currency (ETH), in wei — not ERC-20 tokens data: Uint8Array; // empty for a plain transfer; calldata for contract calls gasLimit: bigint; maxFeePerGas: bigint; signature: Signature; // proves sender authorized this — see Module 02 } // recovered from signature verification (not a separate field in the raw bytes): // from: Address ``` Walk one through. Alice sends Bob 1 ETH: ```typescript const payment: Transaction = { nonce: 42, // Alice's 43rd transaction from this address to: bob, value: 1_000_000_000_000_000_000n, // 1 ETH in wei data: new Uint8Array(), // no contract call — just move native currency gasLimit: 21_000n, maxFeePerGas: 20_000_000_000n, signature: aliceSigns({ ... }), // binds all fields above; tampering breaks verify() }; ``` Every node receives the same bytes, verifies `signature`, checks `nonce`, deducts gas, and applies the balance change. That application produces an updated account map — which `stateRoot` in the header commits to. A **contract call** is the same interface with non-empty `data` — encoded function name and arguments: ```typescript const approve: Transaction = { nonce: 43, to: usdcContract, // call the USDC contract account value: 0n, // no ETH moving data: encode("approve", spender, amount), gasLimit: 50_000n, maxFeePerGas: 20_000_000_000n, signature: aliceSigns({ ... }), }; ``` Native `value` and token balances are different layers. `value` is ETH in the transaction envelope; USDC lives in contract storage and changes only when `data` triggers the contract's code ([[05 Execution and Smart Contracts]]). ### Inside one block A block body is an **ordered array** of transactions. Order matters — especially when txs depend on each other: ```typescript const block_18_000_000: Block = { header: { parentHash: "0xabc...", stateRoot: "0xdef...", // state *after* executing everything below transactionsRoot: "0x789...", // MerkleRoot(hash(tx₀), hash(tx₁), ...) timestamp: 1690000000, }, transactions: [ tx_alice_pays_bob, // leaf 0 in the Merkle tree tx_bob_approves_marketplace, // leaf 1 tx_carol_deploys_contract, // leaf 2 — to: null // ... ], }; ``` Each leaf in the Merkle tree is `hash(serialized transaction bytes)`. The header's `transactionsRoot` commits to this exact list in this exact order — swap two entries, the root changes. Bitcoin uses a different transaction shape — **UTXO** instead of accounts — but the block structure is the same idea: header + ordered transaction list + Merkle root over it. The UTXO transaction interface: ```typescript interface TxOutput { address: Address; amount: bigint; } interface TxInput { previousTxId: Hash; previousOutputIndex: number; signature: Signature; // proves you can spend that output } interface UtxoTransaction { inputs: TxInput[]; // consume prior outputs outputs: TxOutput[]; // create new ones } ``` No `nonce`, no `to` field — validity means every input references an unspent output, and signatures match. More on why chains pick one model or the other in [[UTXO vs Account Model]] and below. --- ## Position claimed, not assigned Each block points to its parent: ```text B0 <- B1 <- B2 <- B3 ``` Before the tamper-evidence story, notice what this structure *is*, in the terms [[01 Replicated State Machines]] set up. On one machine, order came from a synchronous xid counter — one process deciding "next." Raft re-centralized that counter behind an elected leader. Here there is no counter and no leader. A block's position is not *assigned* by any authority — it is **claimed**, by the block itself, through `parentHash`: "I come after that one." Sequence numbers have been replaced by pointers. That changes how you learn the order. With a counter, order is handed to you: entry 41, entry 52, entry 60. With pointers, order is **gradually discoverable** — you hold one block, it names its parent, you fetch that, it names its parent, and the history assembles itself backward as you walk. A node joining the network with nothing but a tip hash can reconstruct the entire canonical log this way, verifying each link as it goes. Nobody has to hand over "the log" whole or be trusted about its contents; the structure carries its own order, and every step of the walk is checkable ([[02 Cryptographic Primitives]]). Two consequences fall out immediately: - **Anyone can claim.** Nothing stops two miners from both publishing a block whose `parentHash` names `B3`. The structure permits competing claims — that's a fork, and *choosing* between claims is consensus's job ([[04 Consensus]]). The counter never had this problem; it also couldn't exist without a trusted owner. - **Claims are binding.** Because the pointer is a hash, a block's claimed position is welded to the exact content of everything before it. You cannot claim "after B3" while quietly disagreeing about what B3 was. Now the tamper-evidence story, which is the binding property doing its work. Try to modify `B1`. Its hash changes — that's the avalanche property from [[02 Cryptographic Primitives]] doing its job. But `B2.header.parentHash` recorded the old hash, so `B2` no longer matches, so `B2` must change too, which breaks `B3`, and so on. Tampering with any block means rewriting every block after it. This is what "append-only" really means here. It's not that edits are forbidden — it's that edits are *loud*. A hash chain converts silent corruption into visible divergence. (Whether anyone must also *pay* to produce the divergent version is a separate question, and it belongs to consensus — [[04 Consensus]]. Structure makes tampering evident; economics makes it expensive. You need both.) The through-line from module 01, completed: | | Who decides "next" | How you learn the order | |---|---|---| | Single-node DB | xid counter, one process | the counter already sorted it | | Raft / Paxos | elected leader assigns log slots | leader streams you the log | | Blockchain | nobody — blocks claim positions via `parentHash` | walk the pointers; verify each link | --- ## Commitments at block scale Here's a subtlety the interfaces already hint at: the **header** doesn't embed the transactions — it carries `transactionsRoot`, a [[Merkle Trees|Merkle root]] committing to them. The full `Transaction[]` lives in the block body, retrievable on demand. ```text transactionsRoot (in header) | root ──┴── / \ h12 h34 / \ / \ tx0 tx1 tx2 tx3 ← each leaf = hash(serialized Transaction) ``` Why go to the trouble? Because inclusion proofs become cheap: - You can prove a transaction is in a block without downloading the block. - A **light client** — a node that stores only block headers, not full state — can verify inclusion via Merkle proofs rather than re-executing everything. - Ethereum extends the trick to its whole state: a **Merkle-Patricia trie** commits to every account and storage slot in one 32-byte root. You need the idea (one root, inclusion proofs), not the trie construction — "Patricia" is just an optimization for sparse key spaces. ### What is the network? The **network** is not a server. It is the set of machines running the same chain software, connected **peer-to-peer** over the internet — each machine a **node**, each node talking to a handful of **peers**. Blocks and signed transactions **gossip**: you hear something from one peer, verify it locally, forward it to others. There is no central inbox, no operator who "is" the chain. ```text Alice signs tx → gossips to peers → mempools fill → proposer includes in block → block gossips ``` "Submit to the network" means: broadcast a signed transaction and hope a block proposer eventually includes it. "The network accepted my payment" means: the transaction landed in a block on the history your software follows — not that some API returned 200. A **node** is software that participates in that gossip — receiving blocks, validating them, forwarding what checks out ([[01 Replicated State Machines]]'s replicas, now on the open internet). Thousands of independent operators run nodes; correctness does not require trusting any one of them, because every node checks signatures, hashes, and execution rules locally. | Node type | What it keeps | What it can verify | |---|---|---| | **Full node** | headers + full blocks + current state | everything — re-executes each block | | **Light client** | headers only | headers, signatures, Merkle proofs — not full state | Node types, RPC endpoints, and indexers are [[08 Nodes, RPCs, and Indexers]]. Here: the network is peers plus protocol; verification is math you run yourself on whatever data a peer hands you. ### How a light client verifies a payment Walk through it concretely — this is the question the Merkle machinery exists to answer. **Bob is a node** — specifically a **light client** from the table above: he stores `BlockHeader[]` (the shoebox of index cards) but not full blocks or world state. Bob cannot store the whole library. He still wants to check a payment. So he **asks a full node** — any peer on the network that has the full blocks — for the missing pieces. The request is ordinary networking: Bob's software sends a message, a full node's software responds. Apps often use HTTP/JSON (**RPC**) for this convenience; the underlying idea is the same: *request data from a peer, verify it yourself*. ```typescript // Bob's light client → any full node peer interface GetTransactionProofRequest { blockNumber: number; transactionHash: Hash; } interface GetTransactionProofResponse { transaction: Transaction; header: BlockHeader; // Bob may already have this from header sync inclusionProof: Hash[]; // sibling hashes: leaf → transactionsRoot } ``` **Why "any full node"?** Because Bob does not trust the response — he **checks** it. A peer can lie; the math catches lies. Fake transaction bytes fail signature verification. A forged Merkle path fails against `header.transactionsRoot` Bob already holds. A block from the wrong fork fails the `parentHash` walk. Wrong data is useless; only data that passes all four checks counts. Bob can query three peers and take the proof that verifies — or reject all three if none do. Alice says: "I paid you — look at transaction `tx` in block 18,000,000." Bob's light client sends `GetTransactionProofRequest` to a full node (or an RPC endpoint — same shape). Back come the three pieces: ```text transaction — the payment Transaction bytes header — BlockHeader for that block (or Bob already synced it) inclusionProof — sibling hashes on the path from tx → transactionsRoot ``` Then he runs four checks locally. Each one uses a primitive from [[02 Cryptographic Primitives]]: **1. Authorization — did Alice actually sign this?** ```text verify(Alice_pubkey, tx, signature) == true ``` A signature check. Without it, inclusion in a block means nothing — someone could have fabricated the transaction bytes. **2. Inclusion — is this tx really in that block?** Bob holds `header.transactionsRoot`. He recomputes upward from the transaction leaf: ```text root ← must equal header.transactionsRoot / \ h12 h34 ← inclusion_proof provides the siblings Bob is missing / \ / \ tx0 tx1 tx2 tx3 ^ Alice's payment (leaf Bob has) ``` Hash the serialized `Transaction` with its sibling to get the parent, repeat until one root remains. If `computedRoot == header.transactionsRoot`, the header **commits** to this transaction at this position in the block. **3. Canonical history — is this block in the chain Bob follows?** Bob walks `header.parentHash` links back through the headers he stored. The block must connect to genesis (or his trusted checkpoint) through a chain that passes consensus rules — proof-of-work, proof-of-stake, whatever the network uses ([[04 Consensus]]). Structure alone doesn't pick between two valid forks; consensus does. But once Bob has the header chain he accepts, Merkle inclusion ties the payment to *that* history. **4. Payment — did this transaction actually pay Bob?** Bob parses the `Transaction` fields: for a plain transfer, check `to === bob` and `value >= expected`. For UTXO, check an output address and amount. Inclusion proved the bytes are in the block; this step proves they're the payment he cares about. ```text What Bob is: a light client node (headers only) What Bob asks: a full node peer — request/response over the network What Bob receives: transaction + inclusionProof (+ maybe header) What Bob verifies: signature → Merkle path → header chain → payment fields What Bob avoids: storing full blocks, re-executing every transaction ``` **What this does and doesn't prove.** Bob has credible evidence that Alice signed a payment included in a block on the chain he follows. He did not re-execute the block or store full state. On Bitcoin-style SPV, he still cannot fully verify that Alice's inputs weren't already spent elsewhere — that requires full-node validation. On Ethereum, proving a balance *changed correctly* can require state proofs against `stateRoot`, not just transaction inclusion. For "did this signed payment land in canonical history?", the four steps above are the core walkthrough — and the exercise at the end of this module is literally implementing step 2. Recall from [[02 Cryptographic Primitives]]: a commitment collapsed many possible futures into one digest — a causal knot strangers could audit later. A Merkle root is the same move at block scale. ### Before the block: the mempool Blocks arrive every ~12 seconds (Ethereum) or ~10 minutes (Bitcoin). Transactions arrive all the time. You saw the gossip path above — between blocks, signed `Transaction`s wait in each node's **mempool** — short for *memory pool*: the local set of transactions a node has heard about but not yet seen included in a block. ```text Alice signs tx → gossips to peers → each node stores it in its mempool | v block proposer picks a subset → seals Block { transactions: [...] } ``` No central inbox. Every full node gossips what it receives; every node keeps its **own** mempool — similar across the network, but not identical. There is no global "the queue." [[06 Transaction Lifecycle and MEV]] treats that fuzziness properly; for now: pending transactions live in mempools, and whoever proposes the next block **chooses** which ones to include and in what order. ```typescript // each full node, locally — not one shared object on the network type Mempool = Transaction[]; ``` While transactions sit in mempools, many futures are still open: many conceivable subsets and orderings for the next block. Dependencies trim that space — if `tx_B` spends an output `tx_A` creates, only orderings with `A` before `B` can execute cleanly. Still, until a proposer publishes a header, nothing in the public record fixes *which* valid subset and *which* valid ordering this block chose. ```text many possible (tx set, ordering) pairs ← mempools across the network | | seal block: transactionsRoot = MerkleRoot([tx₀, tx₁, ...]) v one frozen root ----must match----> this exact set, this leaf order ``` The root does not replace execution rules — dependents still have to sort correctly inside the block. But it **binds** the proposer to one branch: these transactions, in this arrangement, in this block. Swap two leaves, drop a tx, or insert one that wasn't there — the root changes, and any header or proof that cited the old root breaks. Together with `parentHash` chaining blocks in order, commitments stack: each header trims the possibility space of what history could have been, and dependent transactions inherit those trims — you cannot claim `B` happened before `A` in a block whose Merkle tree has `A` at an earlier leaf than `B`. Seen this way, a header is a small bundle of commitments, with everything heavy hanging off it — retrievable and verifiable on demand. --- ## The library and the index card You just walked through this literally, with Bob and a payment. Here's the same shape, as something you can actually picture holding. **The library and the index card.** A block, with every transaction inside it, is a book — bulky, and expensive to print correctly. The whole chain, every block ever produced plus the state you get from replaying them, is a library that keeps growing: hundreds of gigabytes and climbing. No phone holds that library. But a **header** is a few hundred bytes — small enough to fit on an index card. `parentHash` is one fingerprint: "here's the summary of the block before me." `transactionsRoot` and `stateRoot` are two more: "here's the summary of every transaction inside me" and "here's the summary of state after running them." A light client's entire sync is: keep a shoebox of these cards, one per block. **Checking a piece without the whole.** When the librarian (a full node) hands you a book and claims your receipt is glued inside it on some page, you don't read the book. You ask for a short trail of pairing numbers, walk them up to the summary already written on your card, and redo that small bit of arithmetic yourself. Match — the receipt is really there. No match — caught, without reading another page. That's exactly what Bob did with the Merkle path above; the card catalog is just a picture for it. **Why forging is a different kind of hard.** Writing tomorrow's card is trivial: one new fingerprint, derived from what's already sitting in the box. But quietly swapping a page in a book from three years ago changes that book's fingerprint — which no longer matches its card — which breaks every card printed since, years of them. Fixing it invisibly means reprinting the whole run, and anyone still holding an old shoebox notices the disagreement instantly. Forward is cheap. Backward-without-getting-caught is not — that's the one-way asymmetry. **No single shoebox.** Here's the part worth sitting with, because it's the one that usually stays abstract: there is no one library anywhere, no special back room with unusual machines. Thousands of ordinary computers hold copies of the library; millions of phones hold copies of the shoebox. Nobody's copy *is* the blockchain — the blockchain is the fact that everyone's cards, checked against each other, tell the same story. You're not trusting a place, or a GPU farm, or anyone's word. You're trusting a recipe you could run yourself, right now, on the one card in your hand. --- ## What the log actually edits Transactions don't change state in the abstract — they change **accounts**. The chain maintains a map; each block's `Transaction[]` is a batch of edits to it. Two dominant shapes for that map. Full comparison: [[UTXO vs Account Model]]. **UTXO (Bitcoin).** No balances — only unspent outputs. A `UtxoTransaction` consumes inputs and creates outputs (defined above). Validity means every input traces to a real unspent output with a valid signature. Parallel-friendly, awkward for rich application state. **Accounts (Ethereum).** A mutable map from address to account: ```typescript interface Account { nonce: number; // increments with each tx from this address — replay protection balance: bigint; // native currency (ETH), in wei codeHash: Hash; // empty for EOAs; bytecode hash for contracts storageRoot: Hash; // Merkle root over contract storage (empty for EOAs) } type WorldState = Record<Address, Account>; ``` ### What is a balance? "How much money does Alice have?" sounds like a wallet question. On-chain, it is a **read** against world state — and you should meet it here, before wallets enter the picture. For native currency (ETH on Ethereum), the answer is one field: ```typescript const aliceEth: bigint = worldState[aliceAddress].balance; // wei ``` That number is not stored in Alice's phone, not inside the signed transaction, not in the block header by itself. It is **derived state**: there is no separate authoritative "balance table" apart from running the log through `apply`. In principle, `worldState` is whatever falls out when you replay every committed transaction in order from a known starting point. ```text write path: signed Transaction { value } → block executes → Account.balance fields change read path: obtain current WorldState → look up WorldState[address].balance ``` **Do you replay the whole world every time someone checks a balance?** In principle, state *is* the replay — that's what "derived" means. In practice, nobody recomputes eighteen million blocks on every wallet open. | How you get `WorldState` | What actually happens | |---|---| | **Full node (normal)** | Replayed once while syncing, then **incremental**: each new block applies only `execute(old_state, block)` on top of the state already held. Balance lookup is a field read from disk. | | **Checkpoint / snapshot** | Start from a recent `stateRoot` someone already computed, replay only blocks since then — same definition, shorter prefix. | | **Light client** | Holds headers, not full state. Can verify a **state proof** — a Merkle path from `Account.balance` up to `header.stateRoot` — without replaying everything. Still trusts the header chain ([[04 Consensus]]). | | **RPC / wallet app** | Calls `eth_getBalance` on someone else's full node. Fast, convenient; you are trusting their answer ([[08 Nodes, RPCs, and Indexers]]). | | **Indexer** | A service replayed the chain (or subscribed to events) and wrote balances into Postgres. Fast queries; same trust question as RPC. | So the honest answer is two-layered. **Conceptually**, yes — a balance only means something because the entire history of valid transitions got you there; change any past block and the number can change. **Operationally**, you read the current map from a node that has been maintaining it incrementally, or you verify one field against a `stateRoot` you accept, or you trust an API. The chain's design is what makes those shortcuts *checkable in principle*; most products skip the check and take the shortcut anyway. Two paths, same source of truth. Wallets ([[07 Wallets and Account Abstraction]]) mostly handle the **write** path — hold the key, build the transaction, sign, broadcast. Showing "you have 1.4 ETH" is the **read** path: one of the rows in the table above. The coins never lived in the wallet; the wallet is showing you a number from chain state. Module 07 is where that UX lives; the thing being displayed is defined here. **Not every asset is `Account.balance`.** ETH sits on the account record. USDC, NFTs, and most "tokens" live in **contract storage** — separate maps, same replay idea, different field ([[05 Execution and Smart Contracts]]). Confusing those two is how people think tokens are "in" a wallet when they're entries in a contract's storage. **Public by default.** On a public chain like Ethereum, yes — in a sense stronger than most people expect. `WorldState` is replicated openly; any full node has the whole map, and anyone can query any `Account.balance` or any contract storage slot. There is no permission gate on "how much does address X have?" The network's transparency *is* that everyone can verify everyone's state — that's what makes strangers able to audit without a bank. What the chain does **not** publish is your name. An address is a pseudonym — derived from a public key, not from a government ID. `0xabc…` is visible; "Alice Smith" is not, unless someone links them off-chain (exchange KYC, a public ENS name, heuristics on transfer patterns). So the accurate statement is: **everyone can see every address's balances and every transfer between addresses, forever** — not necessarily "everyone knows it's *you*." ```text public: WorldState[0xabc].balance = 1.4 ETH USDC.storage[0xabc] = 500 every Transfer event, every contract call, every mempool gossip (before inclusion) not public: who 0xabc is in the real world (unless linked elsewhere) ``` Verification and surveillance share the same data. [[12 Security and Incentive Design]] treats privacy protocols and ZK as attempts to recover selective hiding without giving up auditability — but the default account model is a glass ledger. Two account kinds, same `Account` interface: ```typescript // EOA — externally owned account: controlled by a private key ([[02 Cryptographic Primitives]]) // Can initiate transactions. codeHash and storageRoot are empty. // Contract account — controlled by bytecode at codeHash // Only changes when called by a transaction's `data` field ``` A `Transaction` names `to`, `value`, `data`, and carries a `signature` proving the sender holds the key for some `from` address. The `nonce` must match `Account.nonce` for that sender — without it, anyone could rebroadcast an old signed transaction. After execution, `WorldState` updates and the new map is what `stateRoot` commits to. *(Wallet apps — software that holds keys and submits transactions for you — are the usual way humans interact with EOAs, but they are not part of chain state. Module [[07 Wallets and Account Abstraction]] treats them properly; here, "Alice" means an EOA address plus whoever can sign for it.)* The database framing: UTXO is an append-only ledger of immutable output records; accounts are a key-value store mutated in place. One parallelizes and expresses little; the other expresses anything and serializes everything. ## Learning outcomes Questions worth trying on: - Could you explain what a mempool is, and why there is no single global one? - Could you explain what "the network" is — and why it is not a server you trust? - Could you write out the `Block`, `BlockHeader`, and `Transaction` interfaces from memory — and say what each header hash field commits to? - Could you explain why changing one old transaction requires rewriting every block after it? - Could you explain how a new node with only a tip hash discovers the whole history — and why "position is claimed, not assigned" makes forks possible in a way an xid counter never allowed? - Could you walk through how a light client, holding only headers, verifies that a payment happened? - In one paragraph: why is the chain "too big to hold" but "cheap to verify" — and what does "one-way" mean for hash pointer history? - Given a use case — payments, a game, an exchange — could you argue for one state model over the other, and name what it costs? - What does the `nonce` in an account prevent? (If it's not obvious, imagine what happens when someone re-broadcasts a signed transaction.) - Where does "Alice has X ETH" live — in her wallet, in a transaction, or in `WorldState`? How would you read it without trusting anyone's word? - On a public chain, can anyone look up any address's balance? What is public, and what is only pseudonymous? - And the thread to pull forward: the structure is now tamper-evident. But when two perfectly valid histories exist side by side, structure alone can't choose between them. So who decides? ## Checkpoint 1. A block's `stateRoot` and `transactionsRoot` — what does each commit to, and who checks each? *(If unsure, revisit "Blocks as log segments" and [[Merkle Trees]].)* 2. Walk through the four checks a light client runs to verify a payment (signature, inclusion, header chain, payment fields). What does each step rule out? Who is Bob, and why can he ask any full node? *(If unsure, revisit "Commitments at block scale".)* 3. Why can UTXO transactions touching disjoint inputs execute in parallel, while account-model transactions generally can't? *(If unsure, see "What the log actually edits" and [[UTXO vs Account Model]].)* 4. What stops someone from re-broadcasting your signed transaction and draining you twice? *(Hint: the account nonce — an incrementing counter per sender. If unsure, revisit "What the log actually edits".)* 5. In your own words: what's the index card, what's the library, and why can't an old book be swapped quietly? *(If unsure, revisit "The library and the index card".)* **Exercise.** Extend your hash chain from module 02: give each entry a batch of toy transactions and a Merkle root over them. Then write the inclusion-proof verifier — it takes a leaf, a path, and a root, and answers yes or no. It's about fifteen lines, and after writing it, light clients stop being mysterious. ## Resources - **Bitcoin whitepaper** — the sections on timestamping and simplified payment verification are this module in nine pages. - **Ethereum.org docs: Blocks, Accounts, Transactions** — the canonical modern reference for the account model. - **Mastering Ethereum** — the chapters on keys, addresses, and transactions; the best developer-level treatment. --- Next: two valid chains, one choice. Who decides which history is real? → [[04 Consensus]]