web3.path

PHASE 04 Blockchain · ~4 hours

Blockchain Fundamentals

Combine the last three phases: a hash-linked append-only log, replicated across untrusting peers, with a consensus rule for "what's the tip?". That's a blockchain.

Goal — build a 200-line Python blockchain with mining, signatures, and a balance-model ledger. Understand the trade-off between UTXO and accounts.

1. A block, concretely

block = {
  "index":       42,
  "prev_hash":   "0xabc…",   // hash of previous block header
  "timestamp":   1714000000,
  "txs":         [tx1, tx2, …],
  "tx_root":     "0xdef…",   // merkle root of txs
  "nonce":       8213773,    // PoW proof
  "hash":        "0x0000…"   // hash of the above (must be < target)
}
Analogy — it's a linked list of git commits. Each commit hashes the previous one, so changing an old commit cascades and invalidates every descendant. Mining is just "find a commit message whose hash starts with 20 zeros".

2. UTXO vs Account model

UTXO (Bitcoin)Account (Ethereum)
StateSet of unspent outputsMap: address → balance + nonce
Tx inputReferences specific outputsReferences sender address
ParallelismEasier (disjoint UTXOs)Harder (shared balance cells)
ExpressivenessScript (limited)EVM (Turing complete)
Mental modelCash & coinsBank account
Analogy — UTXO is paying in cash: you hand over a $20 bill and get $7 change back as a new bill. Account is Venmo: a single counter goes down, another goes up. Venmo is easier to reason about; cash is easier to parallelize.

3. Proof of Work in 15 lines

import hashlib, time
def mine(prev_hash, txs, difficulty=4):
    nonce = 0
    prefix = "0" * difficulty
    while True:
        header = f"{prev_hash}{txs}{nonce}".encode()
        h = hashlib.sha256(header).hexdigest()
        if h.startswith(prefix):
            return nonce, h
        nonce += 1

Increase difficulty → exponentially more hashes. Bitcoin auto-adjusts this to keep block time ≈10 minutes.

4. Forks and the longest-chain rule

Two miners can find a block at the same time. The network temporarily splits; whichever side grows longer first wins, and the loser's block becomes an uncle/orphan. Bitcoin: "6 confirmations" = 6 blocks built on top, making rollback economically absurd.

… ─► B10 ─► B11 ─► B12a └─► B12b ─► B13b ← this branch wins (longer)

5. Transactions — what's in one

tx = {
  "from": "0x1a…",   // derived from signature on Ethereum
  "to":   "0x2b…",
  "value": 1000000000000000000,   // wei (1 ETH)
  "nonce": 7,                     // prevents replay
  "gas_limit": 21000,
  "gas_price": 20_000_000_000,
  "data": "0x",
  "signature": { v, r, s }
}

The nonce is your per-account sequence number. It's why you can't double-spend: a tx with nonce 7 is valid exactly once.

Analogy — like an idempotency key on a Stripe charge, except mandatory and sequential. Miss a nonce (skip from 7 to 9) and the chain refuses #9 until #8 lands.

6. Project — build pychain

A minimal but honest blockchain in ~200 lines of Python.

Scaffold
import hashlib, json, time
from ecdsa import SigningKey, SECP256k1, VerifyingKey

class Tx:
    def __init__(self, sender, to, amount, sig=None):
        self.sender, self.to, self.amount, self.sig = sender, to, amount, sig
    def payload(self):
        return json.dumps({"s":self.sender,"t":self.to,"a":self.amount}, sort_keys=True).encode()
    def sign(self, sk): self.sig = sk.sign(self.payload()).hex()
    def valid(self):
        if self.sender == "COINBASE": return True
        vk = VerifyingKey.from_string(bytes.fromhex(self.sender), curve=SECP256k1)
        try: return vk.verify(bytes.fromhex(self.sig), self.payload())
        except: return False

class Chain:
    def __init__(self, difficulty=4):
        self.diff = difficulty
        self.chain = [self._block("0"*64, [])]
        self.mempool = []
    def _block(self, prev, txs):
        b = {"prev":prev,"txs":[t.__dict__ for t in txs],"ts":int(time.time()),"nonce":0}
        while True:
            b["hash"] = hashlib.sha256(json.dumps(b, sort_keys=True).encode()).hexdigest()
            if b["hash"].startswith("0"*self.diff): return b
            b["nonce"] += 1
    def add_tx(self, tx):
        if tx.valid(): self.mempool.append(tx)
    def mine(self, miner_pub):
        reward = Tx("COINBASE", miner_pub, 50)
        txs = [reward] + self.mempool
        self.chain.append(self._block(self.chain[-1]["hash"], txs))
        self.mempool = []
    def balance(self, addr):
        bal = 0
        for b in self.chain:
            for t in b["txs"]:
                if t["to"] == addr: bal += t["a"]
                if t["s"] == addr: bal -= t["a"]
        return bal
Extensions to try — (1) add a difficulty retarget every 10 blocks; (2) reject txs where balance(sender) < amount; (3) sync two instances over HTTP and resolve forks with longest-chain.

7. Why this toy is not production

Quiz

Q. Why does the nonce exist in an Ethereum transaction?
Without per-account nonce, any signed tx could be broadcast infinitely, draining the sender. Nonce also forces tx #8 to land before #9.
← Phase 3Phase 5: Ethereum Deep Dive →