prev_hash, the
hash of receipt n − 1, with seq inside the hashed content. Change what one receipt says,
delete one from the middle, or swap two, and the chain no longer verifies at that point.
ctrlrun receipts --verify-chain reports the break by seq and by name.
What it detects
content_altered where a receipt no longer hashes to its stored hash,
hash_missing where the stored hash is gone and nothing can be compared, link_broken between
two, missing where there is a gap, head_mismatch where the head does not match the last
receipt, and unchained for a receipt written before the chain existed. Each names the seq,
so a reader can go and look at that row rather than being told the log is bad, and
Receipt integrity says what to do about each.
What it does not prove
Alteration is not authorship. Receipts are not signed. The chain says the log was not edited after the fact; it says nothing about who wrote any receipt, and nothing here is evidence of origin. Signing is not in this version. It is not tamper-proof. The head lives in the same database as the receipts, so an administrator who can rewrite every row can recompute the chain and it will verify. What the chain raises is the cost of changing history while keeping it: rewriting one receipt means rewriting every receipt after it and the head. It does not detect erasing the end. Deleting the last receipts and rewinding the head is two statements and leaves a chain that verifies. An anchor outside the database would close that, and there is none. This is asserted by a test rather than argued, so the sentence cannot quietly become optimistic. It does not prove every action wrote a receipt. A receipt whose write failed leaves no gap inseq and is invisible to the chain by construction. The events log is where that is
reconciled.
Where it is checked
ctrlrun receipts --verify-chain, on either store backend, including through--store-url.ctrlrun verify’s G11, an altered receipt is detected — against a scratch store verify creates on your backend, never against the store you run on.- On Postgres,
put_receipttakes the head row’s lock first, so concurrent writers cannot interleave into a chain that then fails to verify.
What to do with it
Treat a break as an incident about the store rather than about CTRLRun: the chain is the alarm, not the lock. If you need evidence of origin rather than of integrity, sign or anchor the receipts outside the database yourself; they are portable JSON, one object per line, for exactly that reason. Receipt integrity in practice is the runbook: when to run the check, what each of the six names means for your store, and what an unchained count does to a scheduled job.Next
- Receipts and evidence: what a receipt holds.
- Threat model: what remains open, in full.
- Get started · Why.