# 03 — Blocks, Hashes, and History
You can now fingerprint anything and sign anything. The next puzzle: how do you assemble those tools into a history that no one can quietly edit? Take a second to actually consider it — what structure would you reach for?
The answer the field settled on is almost anticlimactic: a linked list, where the pointers are hashes. The delight is in how much that one substitution buys.
## Blocks and transactions
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.
## Hash pointers
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 |
## Merkle roots
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.
### How a light client verifies a payment
Walk through it concretely — this is the question the Merkle machinery exists to answer. First: who is Bob, and how does he get data from the network?
**Nodes.** A **node** is software that participates in the chain — receiving blocks, validating them, talking to other nodes. There is no central server. Thousands of machines run node software and connect to peers over the internet; blocks and transactions **gossip** peer-to-peer ([[01 Replicated State Machines]]'s networking layer). **Bob is a node** — specifically a **light client**: he stores `BlockHeader[]` (the shoebox of index cards) but not full blocks or world state.
| Node type | What it keeps | What it can verify |
|---|---|---|
| **Full node** | headers + full blocks + current state | everything — re-executes each block |
| **Light client (Bob)** | headers only | headers, signatures, Merkle proofs — not full 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*. Wire formats and provider trust are [[08 Nodes, RPCs, and Indexers]]; the verification logic is here.
```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. 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. That's the pattern that makes verification affordable for participants who can't store everything.
## Too big to hold, cheap to verify
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 is the state, exactly?
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>;
```
Two account kinds, same 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 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.)
- 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 and transactions" 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 "How a light client verifies a payment".)*
3. Why can UTXO transactions touching disjoint inputs execute in parallel, while account-model transactions generally can't? *(If unsure, see [[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 the Accounts section above.)*
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 "Too big to hold, cheap to verify".)*
**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]]