Repository navigation
v0.7.0
Added
-
notary/— a paid countersigning witness, billed over
x402. A separate distribution (lethe-notary); nothing
inlethedepends on it andlethegains no payment dependency.It sells the one thing Lethe structurally cannot give itself. A certificate
is self-attestation —docs/anchoring.mdsays nothing in the artifact brings
in a party the operator does not control — so the notary is that party: an
independent clock, an independent signature, and a countersigned record of
the audit head. That last one is the product, because tip-truncation is
undetectable from inside the chain.The evidence is the receipt, which the operator holds and the notary cannot
repudiate — not the notary's own log, which is why that log is not
hash-chained (chaining it would only let the notary prove things to itself).
/witnessreads the heads back during a dispute; it is a convenience, and
notary/README.mdsays so.A receipt claims only what the notary can support: that a certificate was
presented at a time, is internally valid, and named this head. Not the
presenter's identity, and not that the deletion happened.The controller pays, for evidence about their own compliance — nothing sits
between a data subject and their erasure (GDPR Art. 12(5)), and witness
retrieval, the query run during a dispute, is permanently free. A certificate
that fails verification is never charged for, nor is one already witnessed.Settlement is verified on-chain — 0.01 USDC moved by EIP-3009
TransferWithAuthorizationin block 46487830 — on Base Sepolia. Mainnet
needs a facilitator that settles it, and the first mainnet payment deserves
the same scrutiny: check the chain, not the200.
Fixed
-
A
forgetthat outruns the audit chain now says the rows are gone. If
the completion append exhausts its retries under contention, the deletion has
already happened — but a bareAuditContentionreads as "the forget
failed", and an operator would retry a deletion that completed, find nothing,
and conclude nothing was ever deleted.Lethe.forgetnow raises
ForgetRecordedIncompletely, which states that the deletion completed, names
the record count and theforget_startedhead, says not to re-run it, and
carries the certificate — at that point the only record of the run outside
the chain. -
Concurrent audit appends no longer fork the chain.
AuditLog.append
reads the chain tip, hashes it, and inserts. Two writers that read the same
tip both committed, leaving two entries claiming one predecessor — and
verify_chain()then reported the operator's own log as tampered with, at
exactly the moment they were trying to prove it had not been.This was not hypothetical and not MCP-specific. Every CLI command opens its
own connection, anddocs/anchoring.mdtells operators to runlethe anchor
hourly on a timer, so a scheduled anchor overlapping alethe forgetis the
documented deployment. Measured before the fix: 8 concurrent appends across
separate connections produced 8 entries with 5 distinctprev_hashvalues
andverify_chain()returningFalse.prev_hashnow carries aUNIQUEindex. In a linear chain every entry links
to a distinct predecessor, so uniqueness is the chain invariant; stating it
as a database constraint makes a fork impossible rather than unlikely. The
losing writer is rejected, re-reads the tip, and links onto the winner's
entry. This is the only guard that works across processes — an in-process
lock cannot help two CLI invocations.Retries back off with full jitter. Without it, writers that collide once
collide again in lockstep: measured at 24 concurrent writers, 5 exhausted
their attempts and their entries were never recorded. The chain stayed
intact, but an unrecorded forget is its own kind of hole. With backoff, 40
concurrent writers across separate connections all record, chain intact,
stable across repeated runs.Known limitation, now documented: this makes concurrent connections
safe, not one connection shared by concurrent threads — there the transaction
is shared, so a rollback in one thread aborts another's work. The MCP server
holds a single connection and serializes tool bodies for exactly this reason. -
lethe verify-auditnames a fork instead of only sayingINVALID.
INVALIDcovers a fork, a tampered entry and a truncated tail — three
situations calling for very different responses. An operator upgrading a
chain written before this release, readingINVALIDas "we were breached"
when concurrent appends were the cause, has been told the wrong thing. The
command now printsFORK: rows [n, m] all claim predecessor …for each
split, and says that concurrency explains it.AuditLog.forks()is the
underlying query. -
init_schemaupgrades an existing table in place, adding the index to
chains written by earlier versions. If such a chain already contains a fork
the index cannot be built, and rather than a raw Postgres error this raises
AuditChainForkednaming the duplicated hashes and the rows that share them.
The rows are left untouched — they are evidence, not something to clean up
silently.AuditLog.forks()reports the same thing on demand.
Not changed, deliberately
- MCP tool calls stay serialized. Now that the chain is safe at the
database, the remaining reason for the lock is the single shared psycopg
connection. Adding a connection pool would buy throughput that a
DSAR-deletion workload does not need, in the subsystem where correctness is
the product.lethe/mcp.pyrecords the reasoning.