seq and by name, plus how many rows predate the chain — receipt
41 was edited and the last nine were deleted are different incidents and you need both. Put it
in the job that already runs after a restore, and after any maintenance that wrote to the
database directly.
The six names
hash_missing exists because of one statement. An UPDATE with no WHERE clearing the hash
column destroyed every independent copy of every hash, and an earlier reader skipped the
comparison it could no longer make and called the chain intact. A check that cannot be evaluated
is not a check that passed.
unchained is never a pass either. Rows from before the chain have no position, so they are read
separately and reported with their count, and the summary says how many of how many were
verified. Folding them into a green count would be the same false green in a new costume.
So a database migrated from before v0.6 makes this command exit non-zero, permanently. That
is the honest answer and not a bug: those receipts were written before there was anything to
verify them against, and nothing can make them verifiable after the fact. Before you put the
command in a scheduled job, run it once and read the unchained count, so you are choosing what to
do about those rows rather than discovering them at three in the morning.
When a receipt cannot be written
If the receipt insert itself fails, the exception reaches you and nothing swallows it. The effect record is already committed, so a retry of that key is refused as a duplicate rather than executed twice; the head is unadvanced and there is no gap inseq. You get an action that ran, an
exception you must handle, and a log with no hole in it.
What this does not do
- It does not tell you who wrote a receipt. Alteration is not authorship, it does not survive an administrator who can rewrite every row including the head, and it vouches for nothing that was never recorded. The receipt chain is where those limits are argued and the threat model says what stays open; this page is what to run and what to do with the answer.
- It does not tell you a break was malicious. A restore from backup, a hand-run
UPDATEand a retention job all produce the same names. The report says what changed, not why. ctrlrun verifydoes not read your store.G11runs against a scratch store verify itself created, because a verification tool with a side effect on the thing it verifies is refused.--verify-chainis the one that reads yours.
T164 — six tamper cases, each asserted on the name and the seq and not
merely on “invalid”, the reordering case included — T165 for the positive control without which
every one of those rows would pass against a detector that always says broken, T167 for
truncation caught by the head and only by the head, T168 for the unchained rows that are never
a pass, and T170 for the failed receipt write that raises and leaves no gap. G11 in ctrlrun verify runs the same detection against a scratch store, with the unaltered chain as its
control.
Next
- The receipt chain: what it proves, and what it does not.
- Operations: where this check belongs in a schedule.
- Get started · Why.