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) | |
|---|---|---|
| State | Set of unspent outputs | Map: address → balance + nonce |
| Tx input | References specific outputs | References sender address |
| Parallelism | Easier (disjoint UTXOs) | Harder (shared balance cells) |
| Expressiveness | Script (limited) | EVM (Turing complete) |
| Mental model | Cash & coins | Bank 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 balExtensions 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
- No mempool dedup / replacement (no "replace-by-fee").
- No Merkle-Patricia state trie — balance is recomputed by replay.
- SHA-256 instead of Keccak, strings instead of RLP encoding.
- No P2P; single-node. Add Phase 3's gossip mesh.
Quiz
Q. Why does the nonce exist in an Ethereum transaction?
- To pay for gas
- To prevent replay of the same signed transaction and to enforce tx ordering per sender
- To identify the smart contract being called
- It's a random number for mining
Without per-account nonce, any signed tx could be broadcast infinitely, draining the sender. Nonce also forces tx #8 to land before #9.