Contents

Integrity

Tamper-evidence: a per-account hash chain plus signed checkpoints make any altered entry detectable, atop conservation and no-overdraft invariants.

A “hash” is a short fingerprint of a piece of data: change even one character and the fingerprint comes out completely different. That single property is what this page is built on — it is how an altered record is made to give itself away.

Source src/chain.tssrc/integrity.tssrc/worker/checkpoint.ts

API allInvariantsHold

The idea

“Append-only” is a policy, not a guarantee: anyone who reaches the table can edit a row in place. Integrity 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 attests the rows are the ones actually posted, in the order they were posted.

An append-only audit trail is useful because you can replay it, which only holds if history cannot 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.

ACCOUNT A0000…a1b2…c3d4…e5f6…ACCOUNT B0000…9a8b…7c6d…each link = hash(account's legs + metadata + prior head); the first starts from genesis (64 zeros)Merkle rootfolds every headsigned checkpointEd25519 signatureTamper a row → its hash stops re-deriving. Re-seal the chain → the new root ≠ the signed checkpoint.
Two layers, two attacks. The per-account hash chain catches an edited row that still balances; the signed Merkle checkpoint catches a wholesale re-seal. Both surface as chainIntact in a ProveReport.

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, hashing that account’s legs and metadata onto its prior head (a posting that moves three accounts advances three chains by one link each). 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:

reasonWhat it means
broken-linkThe stored “previous head” does not match the head reached by walking the chain so far. The chain is not continuous.
tampered-hashRe-hashing the stored entry and metadata no longer produces the recorded head. The contents were altered after the fact.

The signed checkpoint

On its own, the per-account chain can be defeated by an attacker who recomputes it, so the second layer anchors the whole ledger to a moment in time. 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. Only a corrupt stored row or a storage outage throws and routes through the retry/dead-letter split.

The rolling re-proof

Seals and verified reads cover the history a handler touches; the reproof sweep covers everything else. Each run re-derives a bounded page of stored chain links from their own content and advances a persistent cursor, wrapping around forever — so a cold account’s old rows, metadata included, get re-hashed on an explicit cadence instead of never. That cadence bounds how long an in-place edit of old rows can sit unnoticed: rotatedAt, the moment the last complete pass finished, is the verified-through watermark, and economy.capacity() reports its age. A link that no longer re-derives throws a non-retryable CHAIN.BROKEN and leaves the cursor in place, so every later tick re-reports the same break until an operator intervenes.

The same construction is what lets proved history leave hot storage: the archive sweep re-verifies a sealed prefix, signs per-account archive heads under a Merkle sum-root, and only then moves and deletes. Archival owns that boundary.

Two provers

Two provers re-check the chain, and the proof covers how the light read.health() and the thorough make prove replay divide the work. Of the proof’s five flags, integrity supplies chainIntact; allInvariantsHold rolls all five into a single boolean.

The light prover is one read away at any moment:

const report = await economy.read.health();
// → { conserved: true, backed: true, noOverdraft: true, chainIntact: true, consistent: true }

See also