Rust · accounting · cryptographic audit

Bookkeeping you can prove.

An immutable, tamper-evident double-entry bookkeeping engine. One rule core decides every write, money is exact integer arithmetic, and every sealed period commits to balances a third party can verify without seeing your books.

cargo add doubleentry

What it guarantees

Balanced at the write

One rule core decides every entry — over a database, inside the store's write transaction under the ledger's lock. Every entry balances per currency and layer, and audit() decides each one again.

Exact

Money is an i128 at a compile-time scale, each currency declared with its exponent. No floats, no operators, explicit rounding, and every overflow a Result.

Deterministic

An entry's id is derived from its idempotency key, so a retry replays instead of booking twice. The rule core reads no clock and no randomness.

Verifiable

Every record is a leaf of one RFC 6962 SHA-256 log. An auditor holding a tree head gets O(log n) proofs that an entry is included and that the log only grew.

Witnessable

The log head is a C2SP signed checkpoint that stock witnesses — somebody else's — cosign. The crate ships no witness and no HTTP client: requests and replies are bytes.

Audited by replay

audit() replays every record through the rule core and compares every table a store keeps, byte for byte. A superuser's edit is detected, not prevented.

Gross-preserving

Balances keep debit and credit totals, so a zero balance after heavy turnover is told apart from no activity.

Hierarchical

Accounts are a tree of paths and only leaves take postings. A rollup folds a trial balance up the tree and infers the grouping nodes.

Yours to shape

No chart of accounts and no opinion about your domain: paths, classes and reporting axes are yours. In memory, or over SQLite and PostgreSQL.

In practice

use doubleentry::prelude::*;

let mut books = Journal::new(LedgerId::new("example.com/acme")?).as_actor("setup");
books.declare_currency(EUR, 2, date!("2026-01-01"))?;
books.open("Assets:Cash", AccountKind::Asset, date!("2026-01-01"))?;
books.open("Income:Sales", AccountKind::Income, date!("2026-01-01"))?;
books.define_period("2026-03", date!("2026-03-01"), date!("2026-03-31"))?;

let rec = books.record(
    Entry::new("invoice-2026-0001", date!("2026-03-15"))
        .debit("Assets:Cash", amount!("1190.00"), EUR)
        .credit("Income:Sales", amount!("1190.00"), EUR),
)?; // Ok(Recorded) | Err(Error::Rejected(r))

let proof = books.prove_entry(rec.id)?; // verifies against books.head()
assert_eq!((proof.index, proof.size), (rec.position, books.head().size));

A ledger is one append-only RFC 6962 log of typed records — master data, periods, entries, clearings, seals. One pure rule core decides every write, returning the records to append or one Rejection with a stable code. Journal runs it in memory; Ledger runs it inside a store's write transaction (SQLite, PostgreSQL or memory), with the same calls .awaited. The idempotency key is the entry's identity, so recording the same draft again returns the original receipt.

The question an auditor actually asks

Not “does this balance?” — most systems manage that — but “how do I know this record wasn't changed after the fact?”

Inclusion

This entry sits at this index under this root, in O(log n) hashes, revealing nothing else.

Consistency

The earlier log is a prefix of the later one. Not merely linked — provably append-only.

Balance

This account held this balance when the period closed, proven against a tree head and named — without disclosing any other account.

And the one no proof answers alone — “is this the history you showed everybody else?” — is answered by independent witnesses, who refuse to cosign a second history at a size they already vouched for.

How the proofs work →  ·  Transparency →

Where to go next