Milestone 6 — Cryptographic Ledger
Kernel Milestone: 6 of 8 Previous: Milestone 5 — Schema-Derived Categorical Boundary (v5.26.0) Next: Milestone 7 — Deterministic Infrastructure
System Impact
-------------
Ledger integrity: strengthened — deterministic replay axis assigned to all transactions
Ledger mutation: unchanged
Tenant isolation: unchanged
Financial immutability: unchanged
State machine: unchanged
Breaking changes: none
Replay determinism: formally established — ledger_sequence is the sole replay ordering axis
Invariant Change
----------------
Every posted transaction now has a stable, allocation-order sequence number enforced at the
database layer. Every audit record is linked into a per-organisation linear hash chain. Any
gap, insertion, or reordering of audit records is detectable from persisted state alone.
Why This Matters
A correct ledger and a demonstrably correct ledger are different things. Milestones 1–5 established that the kernel behaves correctly under normal operation — tenant isolation, immutability, monetary precision, and provenance are all enforced. What was missing was the ability to verify that correctness after the fact: after a restore, after a suspected incident, or by an independent auditor.
M6 closes that gap. Two independent verification paths are now active. The replay digest walks all posted transactions in ledger_sequence order and computes a terminal hash — the same hash must emerge on any clean restore of the same data. The audit chain walks all audit records in insertion order and verifies each link — any deletion, insertion, or reordering breaks it. These mechanisms are independent: one covers the transaction ledger, the other covers the audit trail. Both must pass for a restore to be considered verified.
This also eliminates the last Class O audit mechanism. Superuser database operations — which can bypass row-level security and triggers — were previously only governed by operational policy. pgaudit now captures all privileged statements and forwards them to an external log that the database server cannot retroactively modify. Operational discipline is no longer the only defence.
Proof Anchors
Database layer:
ledger_sequence — GENERATED ALWAYS AS IDENTITY on the transactions table.
PostgreSQL rejects any explicit value on INSERT without an override flag.
Unique index enforces strict monotonicity. Migration 082.
audit_trail — new table with canonical_payload and chain_hash columns.
Structural trigger validates JSON format and SHA-256 hex format on every
write. Second trigger seeds the genesis chain head row at organisation
creation time. Migration 082.
pgaudit — statement-level audit logging active on both privileged roles.
Entries forwarded to GCP Cloud Logging (write-once from the database
server's perspective), retained 365 days, alert within 5 minutes of any
out-of-window privileged statement. Migration 082.
Runtime layer:
canonicaliseAuditJson() — single deterministic JSON serialisation function.
Key ordering is lexicographic and stable across calls. Sensitive fields
are stripped before serialisation.
writeAuditRecord() — sole permitted write path for the audit trail.
Acquires a row lock on the per-organisation chain head before each write,
ensuring concurrent writes queue and the chain remains linear. Hard
failure on any missing chain head row — no silent fallback.
ledger-query.ts — sole entry point for ledger replay reads. All queries
use ledger_sequence ordering. All rows pass through a typed schema
parser before leaving the module. BigInt used throughout to prevent
precision loss on large sequence numbers.
Tests layer:
12 property-based tests covering determinism, key-order independence,
idempotence, null/absent distinction, array order preservation, chain
continuity, and redaction correctness.
verify-audit-chain.ts — post-restore chain verification script.
verify-ledger-replay.ts — post-restore replay integrity and audit
completeness verification.
CI scan blocking ORDER BY created_at in any ledger replay path.
verify-axiom-coverage.ts — CI final gate verifying all delivered
Class M axioms have resolving proof anchors.
Canonical Time Axis (AX-LED-001)
accounting.transactions gains a monotonic sequence number assigned by the database, not the application. The database rejects any attempt by the application to supply an explicit value — a long-standing application-override vector is now closed at the schema level.
This column is the sole permitted ordering axis for ledger replay. created_at is retained for user-facing display but is formally excluded from all replay, integrity, and hash computations. A CI scan enforces this across all source files on every build.
Historical transactions were backfilled with a deterministic sequence based on created_at ascending, then id ascending — the best available approximation of commit order for existing data.
Audit Linear Hash Chain (AX-AUD-001 — Class O → Class M)
A new purpose-built audit table stores a cryptographic chain alongside every financial mutation. Each record carries a canonical serialisation of the audit event and a SHA-256 hash linking it to the previous record. The chain is per-organisation and ordered by insertion sequence.
Concurrent writes are serialised via a row lock on a per-organisation chain head table — the chain cannot fork under concurrent load. Hard failure is raised if the chain head row is missing; a database trigger ensures it is created atomically when an organisation is first registered.
A structural trigger validates that every inserted record carries non-empty JSON, a valid 64-character hex hash, and a non-null trace identifier. These are structural checks — semantic correctness (that the payload matches the event that actually occurred) is the runtime layer’s exclusive responsibility.
The chain proves write-path integrity: that records were appended sequentially through the application write path, with no gaps and no reordering. It does not protect against a fabricated payload that is internally self-consistent. The superuser audit stream (AX-GOV-001) closes that remaining vector.
Superuser Audit Stream (AX-GOV-001 — Class O → Class M)
All database operations by privileged roles are now captured at statement level and forwarded to an external log that the database server itself cannot modify. Alerts fire within five minutes of any privileged statement outside a declared maintenance window.
The application role is not audited at this level — application mutations are covered by the audit chain. Auditing the application role would produce unbounded log volume for no additional integrity value.
The 365-day retention of this stream covers the operational privileged-action audit requirement. It does not substitute for the statutory accounting record obligation, which is satisfied by the audit trail table (retained indefinitely) and the nightly backup policy. These are separate obligations with separate mechanisms.
CI Axiom Coverage Gate (AX-KERN-001)
A verification script now runs as the final step in CI. It reads the axiom registry, iterates all delivered Class M axioms, confirms that the minimum independent layer count is satisfied, and resolves every proof anchor reference to an existing file in the repository. If any proof anchor points to a deleted or renamed artefact, the build fails.
This makes the axiom registry machine-verifiable. Proof anchors can no longer silently decay as the codebase evolves.
Threat Closure
| Threat | Status Before | Status After | Enforcement Layer |
|---|---|---|---|
| Non-deterministic ledger replay ordering | Open | Closed (Class M) | Monotonic sequence enforced at database layer |
| Application override of ledger sequence | Open | Closed (Class M) | Database rejects explicit sequence values |
| Audit record deletion or reordering undetected | Open | Closed (Class M) | Linear hash chain — any gap breaks verification |
| Superuser mutations leaving no external trace | Open (Class O) | Closed (Class M) | External append-only privileged statement log |
| Proof anchor decay in axiom registry | Open | Closed (Class M) | CI gate resolves all anchors to existing files |
Kernel Closure Statement
Axioms: AX-LED-001, AX-AUD-001 (O→M), AX-GOV-001 (O→M), AX-KERN-001
Status: CLOSED as of SpeyBooks v5.27.0
Enforcement layers verified:
Database layer — ledger_sequence GENERATED ALWAYS AS IDENTITY,
unique index, structural trigger on audit_trail,
per-org chain head table with row-lock serialisation,
genesis chain head trigger on org creation,
pgaudit on both privileged roles
Runtime layer — canonicaliseAuditJson(), writeAuditRecord(),
ledger-query.ts with Zod schema parse and BigInt mapper
Tests layer — 12 property-based tests, verify-audit-chain.ts,
verify-ledger-replay.ts, ORDER BY created_at CI scan,
verify-axiom-coverage.ts CI final gate
Post-restore verification protocol:
verify-restore.sh — double-entry, domain algebra, RLS
verify-audit-chain.ts — linear hash chain integrity
verify-ledger-replay.ts — replay digest, audit completeness
Residual risk:
Pre-M6 transactions have no audit trail records — they predate the
audit table. The chain is at genesis for all existing organisations.
Going forward, every new financial mutation produces an audit record.
The chain proves write-path integrity, not semantic row integrity.
A fabricated canonical_payload that is self-consistent passes the
database trigger. The runtime write path is the semantic guard.
AX-GOV-001 closes the superuser bypass vector.
Next dependent axiom:
AX-MIG-002 — Migration Replay Determinism (M7)
AX-CRY-001 — External Audit Chain Anchoring (M7)
Security Posture Change
Two invariants move from Class O to Class M. The audit trail, previously enforced by application convention and code review, is now mechanically enforced at three independent layers: database structural trigger, runtime serialisation and chain computation, and a 12-test property suite. The superuser audit obligation, previously met by operational access controls alone, is now backed by a tamper-resistant external log. Neither can be silently bypassed — one produces a detectable chain break, the other produces a detectable log entry.
Verification Record
Pre-flight:
Transaction count confirmed: 148
Existing organisations confirmed: 14
audit_trail table confirmed absent before migration
Migration:
COMMIT
All 5 post-commit invariant validation blocks passed
Post-commit:
V1 ledger_sequence — NOT NULL, unique index, GENERATED ALWAYS AS IDENTITY active
V2 audit_trail chain columns — canonical_payload, chain_hash present
V3 audit_chain_heads — table present, 14 organisations seeded at genesis
V4 trg_audit_canonical_form — trigger active
V5 trg_org_seed_chain_head — trigger active
Verification scripts:
verify-audit-chain.ts — PASS (genesis state, chain trivially valid)
verify-ledger-replay.ts — PASS (142 posted transactions, terminal digest: a7f3c2e1b8d94f2a...)
API health post-deployment:
api: operational (0ms)
database: operational (15ms)
email: operational (146ms)
auth: operational (88ms)
Adversarial review:
Two audit passes across three document versions.
Ten findings resolved (F1–F9, H1–H10) before implementation.
Production Gold — Final.
Deployment defects resolved:
PostgreSQL 17 requires NOT NULL before GENERATED ALWAYS AS IDENTITY —
migration step order corrected from design.
audit_trail required CREATE TABLE not ALTER TABLE — table did not
exist on this instance.
transaction_lines column name confirmed as amount (not amount_pence).
Fastify 5 breaking change — reference type decorators require
getter/setter pattern, corrected in server.ts.
Architectural Context
M6 sits between the correctness foundations (M1–M5) and the infrastructure determinism work (M7–M8). M1–M5 proved that the kernel behaves correctly. M6 makes that correctness demonstrable after the fact. M7 will extend this to migrations and external anchoring. M8 will add end-to-end state reconstruction proof.
The post-restore protocol is now three steps: verify restore integrity, verify audit chain, verify ledger replay. All three must pass before the API can accept traffic after any restore event.
Operational Impact
Restore confidence: Any database restore can now be independently verified. The terminal ledger digest and the terminal chain hash must match across any two consistent snapshots of the same data.
Deployment requirement: PostgreSQL must be restarted with the pgaudit extension loaded before migration 082 is applied. This is a one-time requirement.
Pre-M6 transactions: Existing posted transactions have no audit trail records — they predate the audit table. The orphan check in verify-ledger-replay.ts will report these; they are expected and do not indicate a defect.
Kernel Status
| Milestone | Description | Status |
|---|---|---|
| M1 | Tenant Isolation | Complete |
| M2 | Financial Immutability and State Machines | Complete |
| M3 | Monetary Domain | Complete |
| M4 | Provenance | Complete |
| M5 | Schema-Derived Categorical Boundary | Complete |
| M6 | Cryptographic Ledger | Complete |
| M7 | Deterministic Infrastructure | Pending |
| M8 | State Reproducibility | Pending |
Files Changed
Backend:
api/db/migrations/082-milestone-6-ledger.sql— ledger_sequence, audit_trail, audit_chain_heads, triggers, pgauditapi/src/lib/ledger-query.ts— new: AX-LED-001 compliant ledger replay helperapi/src/lib/audit.ts— extended: canonical JSON serialisation, chain hash computation, sole audit write pathapi/src/server.ts— Fastify 5 decorator compatibility fix (reference type pattern)tests/audit-canonical.test.ts— new: 12 property-based testsscripts/check-ledger-ordering.sh— new: CI scanscripts/verify-audit-chain.ts— new: post-restore chain verificationscripts/verify-ledger-replay.ts— new: post-restore replay integrityscripts/backfill-audit-chain.ts— new: one-time deployment backfillscripts/verify-axiom-coverage.ts— new: AX-KERN-001 CI gatedocs/kernel/axioms.yml— v2.3: M6 complete, four axioms updated
M7 establishes migration replay determinism and external anchor chain, closing the final two infrastructure determinism axes before the reconstruction proof in M8.