verify-audit works. I ran it against a 502-entry live chain, then built the mutation matrix to establish what "intact" actually covers — because "intact" over an unstated population is the shape of error I published in #9.
The tool's stated contract is accurate and this is not a claim that it lies. The help text says it "fails if any entry was edited, reordered, or inserted" — deletion is not in that list, and that turns out to be exact.
Mutation matrix — DB write access, no master key
Run against a copy of a real store, master key supplied via --key-path:
untampered copy -> intact (exit 0) control: the verifier and key are right
edit one interior field (actor) -> BROKEN at seq 1 exit 1
delete one interior row (seq 201) -> BROKEN at seq 202 exit 1
delete the last row -> intact (exit 0) <-
delete the last 50 rows -> intact (exit 0) <-
truncate, leaving a sqlite_sequence gap (tip 493, sequence 496) -> intact (exit 0) <-
Interior mutations are caught exactly as documented. Tail truncation is not, at any depth, and neither is the sqlite_sequence gap it leaves behind.
This is inherent to a backward-linked chain rather than an implementation slip: every entry binds its predecessor, so a truncated chain is a shorter valid chain. There is no in-band anchor for expected length. An append-only forgery is still impossible without the audit key — truncation is the unique undetectable mutation.
Why I'm filing rather than just noting it
Because it invalidates something I published, and the same reasoning is available to anyone else reading the chain.
I have been treating "zero chain rows were written" as an authoritative discriminator — specifically, for separating the vault adjudicated a credential dead from the vault was unreachable. I called it unfakeable and used it as the tiebreaker over two consumer-side signals, on the grounds that a downed daemon writes nothing by construction.
That holds non-adversarially and fails under an adversary, because absence of rows is exactly what truncation manufactures. Dropping the last two rows of a real death episode makes it read as a vault outage. The property I relied on was "the vault records its decisions", and truncation attacks the record without touching a decision.
Suggestions, cheapest first
-
Say it in the help text. verify-audit's summary lists the three mutations it catches; a reader completes that set as "everything". One clause — "cannot detect truncation of the newest entries; see high-water mark below" — closes the gap between the stated contract and what a reader assumes. This is the whole fix as far as honesty goes.
-
A persisted high-water mark. A chain_hwm row (or a sidecar file) holding the last-known MAX(seq) plus its entry_mac, updated on write, makes truncation below it detectable in-band. Truncating the mark and the tail leaves an inconsistency with sqlite_sequence unless that is also rewritten, so it raises the bar without pretending to be a proof.
-
An external witness, which is what actually saved me here. My monitoring recorders happen to log audit_seq every 30 s to a file outside the DB. That is an unplanned high-water witness, and it holds:
2478 sample lines, audit_seq range 458 -> 502, decreases = 0
live MAX(seq) = 502, witness max = 502 (agree)
planted-decrease control -> detected (the checker can see one)
Worth stating for anyone building the same check: my first pass reported 3 decreases, all false. The regex matched audit_seq= inside the recorder's own arming banners, which echo a historical baseline (v36 audit_seq=458) rather than a current reading. A monotonicity check over a log has to filter to sample lines first, or it compares a historical statement against a live series and reports truncation that never happened.
The generalisable half
verify-audit answers "has the recorded history been altered". It does not answer "is this all of the history", and the second question is the one a reader silently assumes is included. Same shape as the bc counter in #10: the check was honest about what it measured, and its output invited a stronger reading than it supported.
Environment: 560c1b5 binaries, store copied from a rollback backup so the live chain was never mutated.
verify-auditworks. I ran it against a 502-entry live chain, then built the mutation matrix to establish what "intact" actually covers — because "intact" over an unstated population is the shape of error I published in #9.The tool's stated contract is accurate and this is not a claim that it lies. The help text says it "fails if any entry was edited, reordered, or inserted" — deletion is not in that list, and that turns out to be exact.
Mutation matrix — DB write access, no master key
Run against a copy of a real store, master key supplied via
--key-path:Interior mutations are caught exactly as documented. Tail truncation is not, at any depth, and neither is the
sqlite_sequencegap it leaves behind.This is inherent to a backward-linked chain rather than an implementation slip: every entry binds its predecessor, so a truncated chain is a shorter valid chain. There is no in-band anchor for expected length. An append-only forgery is still impossible without the audit key — truncation is the unique undetectable mutation.
Why I'm filing rather than just noting it
Because it invalidates something I published, and the same reasoning is available to anyone else reading the chain.
I have been treating "zero chain rows were written" as an authoritative discriminator — specifically, for separating the vault adjudicated a credential dead from the vault was unreachable. I called it unfakeable and used it as the tiebreaker over two consumer-side signals, on the grounds that a downed daemon writes nothing by construction.
That holds non-adversarially and fails under an adversary, because absence of rows is exactly what truncation manufactures. Dropping the last two rows of a real death episode makes it read as a vault outage. The property I relied on was "the vault records its decisions", and truncation attacks the record without touching a decision.
Suggestions, cheapest first
Say it in the help text.
verify-audit's summary lists the three mutations it catches; a reader completes that set as "everything". One clause — "cannot detect truncation of the newest entries; see high-water mark below" — closes the gap between the stated contract and what a reader assumes. This is the whole fix as far as honesty goes.A persisted high-water mark. A
chain_hwmrow (or a sidecar file) holding the last-knownMAX(seq)plus itsentry_mac, updated on write, makes truncation below it detectable in-band. Truncating the mark and the tail leaves an inconsistency withsqlite_sequenceunless that is also rewritten, so it raises the bar without pretending to be a proof.An external witness, which is what actually saved me here. My monitoring recorders happen to log
audit_seqevery 30 s to a file outside the DB. That is an unplanned high-water witness, and it holds:Worth stating for anyone building the same check: my first pass reported 3 decreases, all false. The regex matched
audit_seq=inside the recorder's own arming banners, which echo a historical baseline (v36 audit_seq=458) rather than a current reading. A monotonicity check over a log has to filter to sample lines first, or it compares a historical statement against a live series and reports truncation that never happened.The generalisable half
verify-auditanswers "has the recorded history been altered". It does not answer "is this all of the history", and the second question is the one a reader silently assumes is included. Same shape as thebccounter in #10: the check was honest about what it measured, and its output invited a stronger reading than it supported.Environment:
560c1b5binaries, store copied from a rollback backup so the live chain was never mutated.