· 2 min read

A hash-chained journal: what it catches and what it does not

How ledger-core builds a SHA-256 chain and checks it through /v1/verify, why tampering in the middle shows but tail truncation does not.

Project: ledger-core

In ledger-core (Rust, SQLite) the posting journal is immutable: triggers forbid UPDATE and DELETE. But anyone with the database file can drop a trigger. So the real defence is the chain check.

How the chain works

Each record stores prev_hash and its own hash = SHA-256(version, prev_hash, seq, key, time, currency, memo, postings). Fields carry a length prefix, so "ab"+"c" and "a"+"bc" give different bytes and concatenation is never ambiguous.

GET /v1/verify recomputes everything: numbering continuity, that prev_hash matches the previous record, and the hash of every record from its fields. If a record was altered, the check names exactly that one.

What is caught

Property tests (proptest) change an amount, a memo or an account and require the check to name the altered record. Deleting a record in the middle, the triggers, idempotency and overflow are tested separately.

What is not caught

Tail truncation. Delete the last few records and the remaining chain stays valid: every record still points at its predecessor. From inside the journal this cannot be seen. You need an anchor outside: store the head_hash of the last record somewhere else periodically and compare on verification.

Anyone reading this as "a blockchain protects against everything" should remember: a chain proves the integrity of what is left, not completeness.

Measured

An 11.4 MB image; 3.5 MiB RAM idle and 4.5 MiB after 200 requests (100 parallel postings and 100 reads), the chain stayed intact. 19 tests.