Skip to content

Commit 4164e09

Browse files
authored
docs(ledger): update the verification contract now that anchoring has shipped (#9420)
Row 1 of what-you-can-verify.mdx and the decision-ledger's own doc comments still said external anchoring was tracked-but-not-built (#9122's honest-limit framing), but #9269-9274 landed it. Precisely restate what's actually closed (a wholesale rewrite before an already- published anchor now requires forging its signature or fabricating matching evidence at an external mirror) and what still isn't (a rewrite made since the last checkpoint, followed by ordinary appends, still gets silently absorbed into every future anchor -- anchoring bounds how far back an undetected rewrite could reach, it doesn't make every row checkable in real time). Publish the end-to-end verifier walkthrough as runnable commands, and present the optional Bittensor backend (#9277, not yet shipped) as its own clearly-labeled corroboration, never folded into the default two-backend claim. Closes #9275.
1 parent 71aad1a commit 4164e09

3 files changed

Lines changed: 109 additions & 18 deletions

File tree

apps/loopover-ui/content/docs/what-you-can-verify.mdx

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ the assumptions and the gaps are what let you decide whether the guarantee is wo
1818

1919
### 1. Gate-decision integrity
2020

21-
**Claim:** the sequence of decisions was not silently reordered, deleted, or rewritten.
21+
**Claim:** the sequence of decisions was not silently reordered, deleted, or rewritten — and, since
22+
external anchoring shipped, that a *wholesale* chain rewrite is independently catchable too, not
23+
only tampering within it.
2224

2325
Every persisted verdict appends to a hash-chained ledger — each row's hash covers the previous row's
2426
hash, so any edit to history breaks the chain at a point you can locate.
@@ -31,11 +33,89 @@ Returns `{ ok, checked, nextAfterSeq, tipSeq, tipHash, totalCount }`, and a `bre
3133
`409` status if the chain is inconsistent. No API key — **anyone** can run it.
3234

3335
<Callout variant="note">
34-
**Trust assumption: tamper-evident, not tamper-proof.** This detects sequence gaps, predecessor
35-
and row-hash mismatches, truncated tails, and records whose content no longer matches their
36-
chained digest. It does **not** detect an operator deleting the chain wholesale and re-chaining
37-
from genesis — catching that requires an external anchor the operator does not control, which is
38-
tracked but **not built today**.
36+
**Trust assumption: tamper-evident, externally anchored.** The check above still catches sequence
37+
gaps, predecessor and row-hash mismatches, truncated tails, and content drift exactly as it always
38+
has. A scheduled job additionally publishes a *signed*, self-describing checkpoint of the chain's
39+
tip — hourly, or every 256 new rows, whichever comes first — to two places the operator does not
40+
control: a Sigstore Rekor transparency log, and a git commit cross-mirrored by GH Archive and
41+
Software Heritage the moment it's pushed. Rewriting history *back past* an anchor already
42+
published before the tamper now means forging that signature or fabricating matching evidence at
43+
an external mirror too — not just editing this repo's own tables. **What this does not close:** a
44+
rewrite made *since* the last anchor, followed by ordinary appends afterward, gets silently
45+
absorbed into every anchor published from then on — see "What you cannot verify" below for the
46+
precise boundary. Anchoring bounds how far back an undetected rewrite could reach; it does not make
47+
every row individually external-checkable in real time.
48+
</Callout>
49+
50+
**Verify an anchor end to end**
51+
52+
a. Fetch the most recent anchor and the currently-published signing key:
53+
54+
```bash
55+
curl -s "https://api.loopover.ai/v1/public/decision-ledger/anchors?limit=1" | jq '.anchors[0]' > anchor.json
56+
curl -s "https://api.loopover.ai/v1/public/decision-ledger/anchor-key" | jq -c '.keys' > anchor-keys.json
57+
```
58+
59+
b. Fetch the actual signed payload the anchor committed to. For the git-commit backend,
60+
`anchor.json`'s `backendRef` names the exact commit — the JSONL line it appended *is* the signed
61+
artifact:
62+
63+
```bash
64+
OWNER=$(jq -r '.backendRef.owner' anchor.json)
65+
REPO=$(jq -r '.backendRef.repo' anchor.json)
66+
SHA=$(jq -r '.backendRef.sha' anchor.json)
67+
FILE_PATH=$(jq -r '.backendRef.path' anchor.json)
68+
curl -s "https://raw.githubusercontent.com/$OWNER/$REPO/$SHA/$FILE_PATH" | tail -n 1 > anchor-signed.json
69+
```
70+
71+
For the Rekor backend, `backendRef` instead names a transparency-log entry (`shardBaseUrl`, `uuid`)
72+
— verify with `rekor-cli verify --rekor_server "$SHARD" --uuid "$UUID"`, or fetch the entry directly
73+
and decode its `hashedRekordRequestV002` body.
74+
75+
c. Verify the signature offline, against the published key — zero contact with LoopOver required:
76+
77+
```bash
78+
npx tsx -e '
79+
import { readFileSync } from "node:fs";
80+
import { verifyLedgerAnchorSignature, anchorKeyById, parseAnchorPublicKeys } from "./src/review/ledger-anchor.ts";
81+
(async () => {
82+
const signed = JSON.parse(readFileSync("anchor-signed.json", "utf8"));
83+
const keys = parseAnchorPublicKeys(readFileSync("anchor-keys.json", "utf8"));
84+
const key = anchorKeyById(keys, signed.keyId);
85+
console.log(key && (await verifyLedgerAnchorSignature(signed, key.publicKeySpki)) ? "signature OK" : "SIGNATURE INVALID");
86+
})();
87+
'
88+
```
89+
90+
d. Bind the anchor back to the *live* chain: fetch the row at the anchored `seq` and recompute its
91+
hash yourself:
92+
93+
```bash
94+
SEQ=$(jq -r '.payload.seq' anchor-signed.json)
95+
curl -s "https://api.loopover.ai/v1/public/decision-ledger/row/$SEQ" | jq > row.json
96+
npx tsx -e '
97+
import { readFileSync } from "node:fs";
98+
import { ledgerRowHash } from "./src/review/decision-record.ts";
99+
(async () => {
100+
const row = JSON.parse(readFileSync("row.json", "utf8"));
101+
const signed = JSON.parse(readFileSync("anchor-signed.json", "utf8"));
102+
const recomputed = await ledgerRowHash(row.prevHash, { seq: row.seq, recordId: row.recordId, recordDigest: row.recordDigest, createdAt: row.createdAt });
103+
console.log(recomputed === row.rowHash && row.rowHash === signed.payload.rowHash ? "row hash OK -- anchor matches the live chain" : "MISMATCH");
104+
})();
105+
'
106+
```
107+
108+
A mismatch here — a live row whose hash no longer matches what was anchored — is exactly what a
109+
wholesale rewrite before this checkpoint would produce. It's public, and anyone can check it, without
110+
asking LoopOver anything.
111+
112+
<Callout variant="warn">
113+
**Bittensor on-chain anchoring is optional, separate corroboration — not part of this default
114+
check.** A third, Gittensor/SN74-audience-specific backend (tracked, not yet
115+
shipped — [#9277](https://github.com/JSONbored/loopover/issues/9277)) publishes the same checkpoint
116+
as an on-chain commitment, signed by a dedicated hotkey run on the operator's own infrastructure.
117+
It's additive corroboration for that specific audience, never folded into the default two-backend
118+
claim every verifier above is told to check.
39119
</Callout>
40120

41121
### 2. Decision-record authenticity
@@ -114,7 +194,7 @@ Stated plainly, because a boundary you discover later is worse than one publishe
114194
| --- | --- | --- |
115195
| **Live gate execution** | The merge/close calls acting on your PR are not attested. Putting an attestation service in the live request path trades real availability for a proof that replay already provides more cheaply. | Standing design, not a pending gap. Revisited only if a tenant contractually requires attested live decisions. |
116196
| **Ground-truth honesty** | Accuracy numbers are scored against recorded human-override events on maintainer infrastructure. Attestation proves *computation*, not *data provenance* — a perfectly attested run over cherry-picked labels is still cherry-picked. | Provenance at capture time (receipts verifiable by the humans whose overrides they record). Different mechanism entirely. |
117-
| **Wholesale ledger replacement** | Row 1's limit — self-operated chains are tamper-evident against everyone except the operator. | External anchoring: signed checkpoints, a transparency log, or an on-chain commitment. |
197+
| **A rewrite made since the last anchor, then re-anchored consistently** | An anchor proves *a* checkpoint existed at *a* time — it has no independent way to know what the tip *would have been* absent tampering. A rewrite to rows since the last published anchor, followed by ordinary appends afterward, gets silently absorbed into every anchor published from then on. Row 1's walkthrough only catches a rewrite that reaches *back past* an anchor already published *before* the tamper happened. | Tighter cadence (currently hourly or every 256 rows, whichever first) shrinks the window of opportunity; it cannot close it fully — that would mean anchoring every single write, at which point it stops being periodic checkpointing at all. |
118198
| **Model behavior** | Row 5's limit — no artifact makes a non-deterministic model reproducible. | Nothing planned; this is a property of the models, not of our record-keeping. |
119199

120200
## Self-hosting versus hosted

migrations/0180_decision_ledger.sql

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@
66
-- Every persistDecisionRecord write appends (including latest-finalize-wins rewrites of the same record id
77
-- -- supersessions are deliberately VISIBLE history, not silent replacement).
88
--
9-
-- HONEST LIMIT (module header repeats this): a self-operated chain is tamper-EVIDENT, not tamper-PROOF --
10-
-- the operator can still rewrite wholesale. External anchoring (signed checkpoints / witness cosigning) is
11-
-- the tracked follow-up once tenants exist, per the epic's sequencing. That gap does not reduce the value
12-
-- against every OTHER actor, or against accidental corruption.
9+
-- HONEST LIMIT (module header repeats this; see migrations/0195_decision_ledger_anchors.sql, #9267): a
10+
-- self-operated chain is tamper-EVIDENT against every actor except the operator, on its own. As of #9267, a
11+
-- scheduled job (src/review/ledger-anchor-scheduler.ts) additionally publishes a SIGNED, self-describing
12+
-- checkpoint of this chain's tip -- hourly, or every 256 new rows, whichever comes first -- to two places the
13+
-- operator does not control: a Sigstore Rekor transparency log and a git commit (cross-mirrored by GH Archive
14+
-- / Software Heritage the moment it's pushed). Rewriting history before the oldest still-referenced anchor
15+
-- now requires forging that signature or fabricating matching evidence at an external mirror too, not just
16+
-- editing this table. What remains open: the UNANCHORED TAIL since the last checkpoint is exactly as
17+
-- tamper-evident-only as before anchoring existed -- anchoring bounds how far back an undetected rewrite
18+
-- could reach, it does not make every row individually external-checkable in real time.
1319
CREATE TABLE IF NOT EXISTS decision_ledger (
1420
seq INTEGER PRIMARY KEY, -- explicit, contiguous (verified); NOT autoincrement -- gaps are breaks
1521
record_id TEXT NOT NULL, -- decision_records.id at append time

src/review/decision-record.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
1313
// replay" is structurally impossible.
1414
//
1515
// HONEST LIMIT (#9122, mirrored from migrations/0180_decision_ledger.sql's own header): the hash-chained
16-
// ledger below makes this instance's history tamper-EVIDENT, not tamper-PROOF — an operator with direct DB
17-
// access can still rewrite the chain wholesale (delete every row, recompute a fresh one from genesis) and
18-
// nothing here can detect that from first principles. External anchoring (a signed checkpoint published
19-
// somewhere the operator does not control — a git commit, a transparency log, an on-chain commitment) is the
20-
// tracked follow-up once tenants exist, not yet built. That gap does not reduce the value against every OTHER
21-
// actor (a maintainer quietly deleting one disputed decision, or an unprivileged bug), or against accidental
16+
// ledger below makes this instance's history tamper-EVIDENT against every actor except an operator with
17+
// direct DB access, on its own — such an operator could still rewrite the chain wholesale (delete every row,
18+
// recompute a fresh one from genesis) and nothing INTERNAL to this table can detect that from first
19+
// principles. As of #9267, external anchoring closes most of that gap: a scheduled job (ledger-anchor-
20+
// scheduler.ts) publishes a signed checkpoint of the tip to a Rekor transparency log and a git commit (cross-
21+
// mirrored by GH Archive / Software Heritage) that the operator does not control — rewriting history before
22+
// the oldest still-referenced anchor now means forging that signature or fabricating matching external
23+
// evidence too. The gap that remains: the unanchored tail since the last checkpoint is exactly as tamper-
24+
// evident-only as before anchoring existed. None of this reduces the value against every OTHER actor (a
25+
// maintainer quietly deleting one disputed decision, or an unprivileged bug), or against accidental
2226
// corruption — both of which the chain below still catches deterministically.
2327
//
2428
// #9124 (v4): three of the four commitments this record makes did not commit to what actually decided the
@@ -395,7 +399,8 @@ export type LedgerBreak =
395399
* record (see below) — and the cursor for the next window. Always returns the CURRENT global tip
396400
* (`tipSeq`/`tipHash`) and total row count, regardless of where this window's pagination stopped, so a
397401
* third-party checkpoint-keeper can compare it against whatever tip it last observed (#9122 — the exact shape
398-
* a future external-anchoring job would need). #9078: also reconciles each row against `decision_records` —
402+
* the scheduled anchoring job, #9274, now consumes via {@link loadDecisionLedgerTip}). #9078: also reconciles
403+
* each row against `decision_records` —
399404
* recomputing `contentDigest(JSON.parse(record_json))` and comparing it to the digest the chain itself
400405
* committed to, so a rewrite of `record_json` that left `decision_records.record_digest` untouched (or vice
401406
* versa) is caught here instead of only being provable by an external challenger who happens to still have the

0 commit comments

Comments
 (0)