Integrity
Tamper-evidence: a per-account hash chain plus signed checkpoints make any altered entry detectable, atop conservation and no-overdraft invariants.
Source src/chain.tssrc/integrity.tssrc/worker/checkpoint.ts
The idea
The ledger is append-only, but "append-only" is a policy, not a guarantee. Anyone who reaches the table can still edit a database row in place. Integrity is what makes such an edit detectable.
Every posting that touches an account is hashed together with that account's previous hash, so each entry commits to the whole history before it. The background worker then periodically folds every account's current head into one signed root.
Those two layers fail loudly in two different ways. Alter one old entry and its hash no longer re-derives, which breaks the next link and every link after it. Rewrite the whole chain to hide that, and the signed root no longer matches. The check that exposes both is the chainIntact flag in a ProveReport.
Why it exists
You can balance a tampered ledger. Conservation and no-overdraft — two of the proof's five flags — are properties of sums, not of the rows behind them.
Change two offsetting amounts, or move a credit from one account's history to another, and conserved stays true while the audit trail is now a lie. Integrity covers the gap those sum-based checks structurally cannot see. It attests that the individual rows are the ones that were actually posted, in the order they were posted.
This matters most for double-entry bookkeeping presented as a system of record. The point of an append-only audit trail is that you can replay it, and that value drops to zero if history can be quietly rewritten between the write and the read.
The invariant
For every account, walking its postings from genesis re-derives every stored hash, and each entry's recorded "previous head" equals the head reached so far. When that holds for every account, the Merkle root over all current heads equals the root in the latest signed checkpoint, and that checkpoint's signature verifies.
How it's enforced
Two layers catch two different attacks. The per-account hash chain catches a single edited row that still balances.
The signed checkpoint catches a wholesale re-seal — rewriting history and recomputing every chain so it looks intact. The re-sealed root differs from the one already signed, and the signature can't be forged without the key.
The per-account hash chain
Each account carries its own chain. When a posting is appended, advanceHeads computes one new head per distinct account the posting touches — a posting that moves three accounts advances three chains by one link each — by hashing that account's legs and metadata onto its prior head. The result is a ChainLink that records the head before and after. An account's first posting starts from a genesis value of 64 zero hex characters.
proveChain re-checks every chain. It sorts accounts by id char by char, so the same tampering is reported identically across runtimes and databases. Then it walks each account's postings from genesis, recomputes the head at each step with the same hash function the write path used, and stops at the first mismatch.
The failure it returns is a ChainBreak that names the offending account and transaction, with a reason of one of two kinds:
reason | What it means |
|---|---|
broken-link | The stored "previous head" does not match the head reached by walking the chain so far — the chain is not continuous. |
tampered-hash | Re-hashing the stored entry and metadata no longer produces the recorded head — the contents were altered after the fact. |
The signed checkpoint
An attacker who also recomputes the chain can defeat it on its own, so the second layer anchors the whole ledger at a moment. The worker's checkpoint job calls sealCheckpoint, which folds every account's head into one merkleRoot and signs it.
The Merkle construction is reproducible on any machine and changes if any head changes. It uses RFC 6962 domain tags — a 0x00 byte for leaves, 0x01 for internal nodes — so a leaf can never be reinterpreted as an interior node. It sorts leaves by account id, and hashes each pair left-then-right.
Sealing proves before it signs. recordCheckpoint runs proveChain first; if the chain no longer re-derives, it throws a non-retryable CHAIN_BROKEN fault and persists no checkpoint. So a signed root never attests to a tampered ledger.
The signature is Ed25519, produced by the injected Signer. The key's public half is published, so an auditor holding only a saved checkpoint and that public key can recompute the root and verify the signature independently.
A second worker job, checkpointVerify, runs reverifyCheckpoint before checkpoint each cycle. It re-derives the root over current heads via verifyCheckpoint and compares it to the latest signed checkpoint. A plain mismatch is recorded and logged at error level for an operator, not thrown — only a corrupt stored row or a storage outage throws and routes through the retry/dead-letter split.
Two provers
The full replay is not cheap, so the surface splits the work. proveEconomy — the thorough prover behind make prove and the proof — always recomputes the entire chain via proveChain; there is no latest-hash-only path.
The in-process read.prove() runs a lighter prover. Its chainIntact is only a shape check on each account's latest head (64 lowercase hex characters), not a replay. The two agree on a healthy ledger, and the thorough one is what locates an altered entry.
allInvariantsHold rolls the five flags — conserved, backed, noOverdraft, chainIntact, consistent — into a single boolean. Integrity owns the fourth.