- verify_audit_chain(): rows with entry_hash IS NULL are only tolerated
BEFORE the first hashed row (documented legacy prefix). A NULL hash
AFTER the chain started is tampering and fails verification at that
row id — previously any NULL row anywhere was silently skipped, so an
attacker with UPDATE could erase intermediate evidence undetected.
- migration 0004: audit_log becomes append-only. SQLite: BEFORE UPDATE/
DELETE triggers RAISE(ABORT). Postgres: BEFORE triggers RAISE EXCEPTION.
Closes the hole for the table owner and direct SQL connections.
- monitor.verify_chain_against_anchors(): compares the chain tail against
anchors published to Nostr. Explicit statuses: no_anchors (never
anchored / notes missing on relays), local_only (no fetcher), ok
(relay note attests the tail or the last local anchor), mismatch.
- tests: NULL-before-genesis ok, NULL-in-middle fails at that id, NULL
late fails, UPDATE rejected, DELETE rejected, INSERT still allowed,
no-anchors explicit, local-only explicit, mismatch detected, match ok.
Gate: ruff 0 · 81 unit + 1 PG integration · alembic 0001→0004 on PG ·
gitleaks clean · make check exit 0.