# M0 — block reader, schema, reorg rollback

**Status: complete.** 18 tests passing.

## What was built

| Module | Purpose |
|---|---|
| `ribbit/config.py` | Chain params for main/test/regtest; RPC credential loading (conf, then cookie) |
| `ribbit/rpc.py` | JSON-RPC client with **batching**, session reuse, id-based result reordering |
| `ribbit/db.py` | SQLite schema, WAL, and the **undo journal** — `StateDB` records a reversal for every mutation |
| `ribbit/chain.py` | `ChainFollower` — connects blocks, detects forks, rolls back reorgs |
| `ribbit/zmq_listener.py` | ZMQ subscriber for hashblock/rawblock/rawtx, with gap detection |
| `ribbit/regtest.py` | Throwaway regtest node, run as the current user, cookie auth, no root |

## Design decisions made during M0

1. **Mutations are illegal outside a block context.** A mutation with no height
   cannot be rolled back, so it would survive a reorg and corrupt state silently.
   `StateDB` raises rather than allowing it.
2. **Tables must be explicitly registered for journalling.** An unregistered table
   cannot be mutated through `StateDB`. This makes adding protocol state in M2 a
   conscious act rather than something that quietly escapes the journal.
3. **`PRAGMA synchronous=FULL`**, not NORMAL. A torn write after a crash means
   silently wrong protocol state, which is far worse than a slower indexer.
4. **Reorgs are fully resolved before any new block is connected**, so the database
   is never a mixture of two chains.
5. **`max_reorg_depth` (default 500) raises rather than unwinding forever.** A
   disagreement that deep means a wrong network, a corrupt database, or something
   genuinely catastrophic — all three deserve a human, not a retry loop.
6. **ZMQ is a wake-up, never a source of truth.** Pepecoin v1.1.0 has no
   `zmqpubsequence` (Bitcoin 0.19+), so ZMQ cannot report a *disconnected* block.
   Reorg detection stays with `ChainFollower`, comparing hashes over RPC.

## Tests

| File | Covers |
|---|---|
| `tests/test_undo.py` (9) | insert/update/delete reversal, **reverse-order replay**, multi-block rollback, atomic failure, misuse guards |
| `tests/test_chain.py` (6) | connect from activation, idempotency, batching limit, **a real reorg on a live node**, journal hygiene, depth limit |
| `tests/test_zmq.py` (3) | hashblock delivery, rawblock body, sequence monotonicity |

The reorg test is deliberately run against a **real** `pepecoind`, not a mock:
it mines 20 blocks, `invalidateblock`s three below the tip, mines a longer
competing chain, re-syncs, and asserts that state from the orphaned blocks is
**gone** — not merely superseded — and that every height we hold matches the node.

## Known gaps, carried into M1

- `ChainFollower` fetches blocks one at a time. `RpcClient.batch()` exists but is
  not yet used by the follower; wire it in when block volume justifies it.
- No mainnet indexing is possible yet: `MAINNET.activation_height` is `None` by
  design, and `ChainFollower` refuses to start without one (D-004).
