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

Screenshot of the project "ledger-core: double entry and a hash-chained journal" at 1280 pixels wide
Desktop, 1280 px
Screenshot of the project "ledger-core: double entry and a hash-chained journal" on a phone screen, 390 pixels wide
Phone, 390 px

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:8115

04 · Code

The repository is private

The source is in a private repository. Message me on Telegram and I will open access.

Request the code