Overview
The hash chain lets you check your own log. It does not let someone else check it without database access, and it does not show that the log was left alone after a given date. Checkpoints add that. A checkpoint is a signed tree head, the same idea Certificate Transparency uses (RFC 6962, since obsoleted by RFC 9162). TheAuth builds a Merkle tree over every chained audit row and signs{size, rootHash, timestamp, keyId} with Ed25519. From there:
- An inclusion proof shows one row is a leaf of the tree the checkpoint committed to.
- A consistency proof shows a newer checkpoint extends an older one, with no earlier row changed.
- A receipt bundles a decision, an inclusion proof and the checkpoint into one JSON object.

A signed checkpoint yields a receipt that verifies, and a newer checkpoint extends the older one.
Sign a checkpoint
signer is any object with a keyId and an async sign(bytes), so the private key can live in a KMS. createEd25519Signer(privateKeyJwk, keyId) covers the in-process case. Pass since or until to cover only a time range; both bounds are signed into the checkpoint.
An empty log gives a size 0 checkpoint.
Mint and verify a receipt
createConsistencyProof takes the database because a checkpoint holds only the root, not the tree.
What a receipt proves
- The operator signed a log head with that key at that time.
- A row with the revealed values was a leaf of that log, bound to the row’s chain hash.
- If you hold two checkpoints and a consistency proof, the newer log kept every row of the older one unchanged and in place.
What it does not prove
- That the log is complete. A row that was never written, or was left out of a checkpoint, is invisible. Checkpoints cover rows that exist at signing time.
- That the operator showed everyone the same log. A signer could sign two different heads for two parties. Catching that needs the checkpoint published somewhere many parties read, and someone comparing what they saw.
- That a hidden field is what the operator says. A hidden field is only a commitment. Reveal it with its salt to prove it.
- That the decision was right. It proves the record, not the judgment behind it.
- Time precision. The checkpoint timestamp is the signer’s clock. It is not an independent time stamp.
- Who a pseudonym is. Version 2 rows commit to keyed pseudonyms of user, agent and approver ids, not raw ids. Revealing
idPseudonymsshows the pseudonyms. Someone holding the audit key can confirm an id matches one; a counterparty without it cannot.
Redaction and its limits
Each hashed field of a row gets a commitment: SHA-256 over the field name, a per-field salt and the canonical JSON value. The salt is derived from the audit key and the row hash, so the operator can rebuild it any time and stores nothing extra. A receipt includes the salt only for fields you reveal, plus the commitments for the rest.- Hidden fields stay hidden as long as the audit key stays secret. Anyone with the key can recompute salts and test guesses.
- Revealing a field reveals exactly that value. There is no partial reveal inside
parameters; reveal it whole or not at all. - The set of field names, the row id, the row’s chain hash and its position in the log are visible in every receipt.
Keep checkpoints where a counterparty trusts them
A checkpoint you alone hold is a claim you can quietly replace. Publish each one somewhere the counterparty already trusts and that keeps history, such as a public repository, a transparency feed, an append-only bucket, or a message to the counterparty’s own system. Verifiers should keep the checkpoints they have seen and ask for a consistency proof when a new one arrives.Key rotation
Every checkpoint names itskeyId. Verification functions accept one public key or an array, and pick the key by keyId; a checkpoint whose keyId is not in the set, or whose alg is anything other than Ed25519, is rejected.
To rotate: create a new key pair under a new id, publish the new public key next to the old ones, and sign new checkpoints with the new key. Keep the old public keys published so older receipts still verify. A consistency proof can join checkpoints signed by different keys if you pass both public keys.
If a private key leaks, checkpoints signed after the leak cannot be trusted from that key. Publish a revocation note with the last good checkpoint, and move on to a new key.
Operating notes
- Log order. Leaves are ordered by row timestamp (whole seconds), then row id, across all agents. Rows written later with an earlier or equal timestamp can sort before rows an older checkpoint already covered; then proofs against that checkpoint fail with
LOG_MISMATCH, an alarm and not a missed tamper. Useuntilto checkpoint only rows old enough to be settled. - Unchained rows (written before
tamperEvidentwas on) are not in the tree. - Cleanup and key changes. Deleting old rows with retention cleanup changes the tree, so older checkpoints can no longer be proved against the live database; keep the receipts you issued. Changing
audit.hmacKeychanges every salt, which changes the tree the same way. - Cost. Building a checkpoint or proof reads and hashes every row in range. Run it on a schedule, not per request. Leaf hashes are cached in process per database and reused for rows whose content has not changed (a row is re-hashed whenever any covered field differs), so repeated proofs and receipts cost a read of the range, not a full re-hash.
createCheckpointreturnsINVALID_RANGEfor asinceoruntilthat is not a valid date. - Storage.
CheckpointStoreis an interface with an in-memory implementation. There is no database-backed store yet.
Related
- Tutorial: give a counterparty a receipt they can verify offline
- Tamper evident audit trail: the hash chain that checkpoints sign.
- Agent accountability
- Find and remove unused permissions