Case · Backend · Rust
ledger-core: double entry and a hash-chained journal
A double-entry accounting core in Rust: accounts, postings, an immutable journal with a hash chain, an integrity check and a trial balance. Money is integers only (i64, minor units), a transaction sums to exactly 0, arithmetic is checked. Each record stores prev_hash and a SHA-256 of its fields; GET /v1/verify recomputes the chain and names the tampered record. Idempotent by Idempotency-Key.
- Rust
- axum
- SQLite
- SHA-256
- proptest
- musl
- distroless
- Docker
- cargo-audit
- GitHub Actions
01 · How it looks
Screenshots


02 · About
What it is and why
The task: show how to keep accounting invariants at the service level rather than "hoping the client behaves": money without floats, a posting that always nets to zero, a journal that cannot be quietly rewritten.
The solution: Rust (axum + SQLite). Amounts are i64 in minor units, arithmetic is checked; every record stores prev_hash and a SHA-256 over all its fields with length prefixes, and GET /v1/verify recomputes the chain and names the tampered record. SQLite triggers forbid UPDATE and DELETE.
Limitations
The chain catches an edit or deletion in the middle but not tail truncation: head_hash has to be stored outside.
Whoever has the database file can drop the triggers: the defence is the chain check.
Numbers above 2^53 lose precision in JavaScript clients; there is no reversal entity.
The project runs on a small VPS with demo data: no redundancy or backups.
What is inside
- The posting invariant: lines sum to exactly 0, 2 to 100 lines, zero amounts forbidden, one currency; overflow gives 422, not a panic
- Idempotency by Idempotency-Key: the same key and body return the original record, a different body gives 409
- The hash chain and /v1/verify: numbering continuity, prev_hash and a recomputed hash of every record
- A trial balance; the debit and credit totals must match
- 19 tests; property tests: a posting always sums to 0, any change to an amount, memo or account is caught and points at the right record
- Result: an 11.4 MB image, 3.5 MiB idle and 4.5 MiB after 200 requests (100 parallel postings and 100 reads), the chain stayed intact
03 · How to run
Run it locally
git clone <repository on request> ledger-core
cd ledger-core
DEMO_SEED=1 cargo run --release
# http://localhost:811504 · Code
The repository is private
The source is in a private repository. Message me on Telegram and I will open access.
Request the code