Skip to content

Commit 355e029

Browse files
authored
feat(ledger): Bittensor on-chain anchor backend — repo-side glue for the off-Worker submitter (#9277) (#9594)
* feat(ledger): Bittensor on-chain anchor backend — repo-side glue for the off-Worker submitter (#9277) The fourth, optional, Gittensor/SN74-audience anchoring backend. The on-chain SUBMISSION never runs in this Worker: a process on the operator's own node infrastructure (where its dedicated anchor-only hotkey lives as an operational secret, never in this repo) fetches the signed checkpoint, commits sha256(signingInput) via the commitments pallet's set_commitment(netuid, Data::Sha256), and reports the outcome back. This lands the validation boundary that report crosses: - GET /v1/public/decision-ledger/anchor-payload: the current tip's freshly signed checkpoint + the exact signingInput bytes to hash. Unauthenticated (it is the same payload Rekor/git already publish externally); no-store, since 'at' is minted per call. - POST /v1/decision-ledger/anchor-attempts: bearer-gated (LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN, fails closed unset -- the isAuthorizedIngest posture). Authentication alone cannot forge corroboration: an ok report must verify against a PUBLISHED anchor key AND its (seq, rowHash) must match the LIVE chain row; a failed report records without those checks, because a broken submitter is exactly what the public attempt log exists to make visible. - decision_ledger_anchors backend CHECK widened to 'bittensor' (migration 0201 rebuild); the public listing filter follows. Rows are byte-identical in shape to Rekor/git attempts. - backendRef carries the FULL historical-retrieval reference {netuid, blockNumber, blockHash, hotkey}: CommitmentOf is overwritten in place on-chain, so a verifier reads archive state at that block, never current chain state -- now documented step-by-step in what-you-can-verify.mdx, still clearly scoped as optional corroboration outside the default two-backend claim. Closes #9277 * feat(openapi): describe the two #9277 anchoring routes and widen the backend filter The route/spec ratchet caught both new routes as undescribed -- correctly: they exist in createApp() with no OpenAPI operation, which is exactly what that check is for. - GET /v1/public/decision-ledger/anchor-payload: the signed checkpoint an off-Worker submitter commits, with the 404 arms (signing unconfigured, empty ledger) described as the honest degrades they are. - POST /v1/decision-ledger/anchor-attempts: the report ingest, with every refusal documented -- notably the 401 covering an unconfigured token (fails closed) and the 422 naming all four unverifiable outcomes, since those are what a submitter integrates against. - The anchors listing's `backend` filter gains 'bittensor', matching the widened CHECK constraint.
1 parent 26071f1 commit 355e029

10 files changed

Lines changed: 560 additions & 11 deletions

File tree

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

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,28 @@ asking LoopOver anything.
127127

128128
<Callout variant="warn">
129129
**Bittensor on-chain anchoring is optional, separate corroboration — not part of this default
130-
check.** A third, Gittensor/SN74-audience-specific backend (tracked, not yet
131-
shipped — [#9277](https://github.com/JSONbored/loopover/issues/9277)) publishes the same checkpoint
132-
as an on-chain commitment, signed by a dedicated hotkey run on the operator's own infrastructure.
133-
It's additive corroboration for that specific audience, never folded into the default two-backend
134-
claim every verifier above is told to check.
130+
check.** A third, Gittensor/SN74-audience-specific
131+
backend ([#9277](https://github.com/JSONbored/loopover/issues/9277)) publishes the same signed
132+
checkpoint as an on-chain commitment. A submitter on the operator's own node infrastructure — never
133+
this Worker, and its dedicated anchor-only hotkey never leaves that infrastructure — fetches
134+
`GET /v1/public/decision-ledger/anchor-payload` and commits `sha256(signingInput)` via the
135+
commitments pallet's `set_commitment(netuid, Data::Sha256)`, then reports the attempt (success
136+
*and* failure, like every other backend) back into the public attempt log, where it appears as
137+
`backend: "bittensor"`. It's additive corroboration for that specific audience, never folded into
138+
the default two-backend claim every verifier above is told to check.
139+
</Callout>
140+
141+
<Callout variant="info">
142+
**Historical retrieval — read the block, not chain state.** The commitments pallet's
143+
`CommitmentOf` map is **overwritten in place**: only the *latest* commitment per (netuid, account)
144+
survives in current chain state. To verify an older anchor, use its `backendRef` from the attempt
145+
log — `{netuid, blockNumber, blockHash, hotkey}` — and query **archive state at that block**
146+
(`state_getStorage` at `blockHash`, or the block's events), not the current tip. Then check the
147+
stored commitment's `Sha256` bytes equal `sha256(signingInput)` of the anchor's own
148+
`payload_json`, and verify the payload's signature against the published anchor keys exactly as in
149+
step (c) above. Any Bittensor archive node can answer this; the operator's own archive is merely
150+
the convenient one — the same trust posture as the git backend's "GitHub hosts it, the mirror
151+
cross-checks it."
135152
</Callout>
136153

137154
### 2. Decision-record authenticity

apps/loopover-ui/public/openapi.json

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20766,7 +20766,8 @@
2076620766
"enum": [
2076720767
"rekor",
2076820768
"git",
20769-
"ots"
20769+
"ots",
20770+
"bittensor"
2077020771
]
2077120772
},
2077220773
"required": false,
@@ -26208,6 +26209,49 @@
2620826209
}
2620926210
}
2621026211
}
26212+
},
26213+
"/v1/public/decision-ledger/anchor-payload": {
26214+
"get": {
26215+
"operationId": "getPublicDecisionLedgerAnchorPayload",
26216+
"tags": [
26217+
"Public"
26218+
],
26219+
"summary": "The current ledger tip as a freshly signed checkpoint, for an external anchoring submitter to commit",
26220+
"responses": {
26221+
"200": {
26222+
"description": "{ signed: { payload, keyId, signature }, signingInput } — `sha256(signingInput)` is the exact 32 bytes an on-chain commitment holds. Never cached: `payload.at` is minted per call"
26223+
},
26224+
"404": {
26225+
"description": "Anchor signing is not configured, or the ledger is empty — nothing is claimed to be anchorable yet"
26226+
}
26227+
}
26228+
}
26229+
},
26230+
"/v1/decision-ledger/anchor-attempts": {
26231+
"post": {
26232+
"operationId": "reportDecisionLedgerAnchorAttempt",
26233+
"tags": [
26234+
"Public"
26235+
],
26236+
"summary": "Report one off-Worker anchoring attempt (success or failure) into the public attempt log",
26237+
"responses": {
26238+
"200": {
26239+
"description": "{ recorded: true, status: 'ok' | 'failed' }"
26240+
},
26241+
"400": {
26242+
"description": "Unparseable body, or a report whose named field failed validation"
26243+
},
26244+
"401": {
26245+
"description": "Missing or wrong bearer token; also returned when no report token is configured (fails closed)"
26246+
},
26247+
"413": {
26248+
"description": "Body exceeded the ingest ceiling"
26249+
},
26250+
"422": {
26251+
"description": "Authenticated but unverifiable: unknown_key, bad_signature, row_not_found, or row_hash_mismatch — an `ok` report must verify against a published key AND match the live chain row"
26252+
}
26253+
}
26254+
}
2621126255
}
2621226256
},
2621326257
"servers": [
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
-- #9277 (epic #9267): widen decision_ledger_anchors' backend CHECK to admit 'bittensor' — the optional,
2+
-- Gittensor/SN74-audience on-chain commitment backend. The submission itself runs on the operator's own node
3+
-- infrastructure (never this Worker); this table records its reported attempts, success AND failure, exactly
4+
-- like the Rekor/git rows (#9271's whole design: a backend that can fail silently is a backend an operator
5+
-- can silently disable). SQLite cannot ALTER a CHECK constraint, so this is the standard rebuild-and-rename.
6+
CREATE TABLE decision_ledger_anchors_new (
7+
id TEXT PRIMARY KEY,
8+
seq INTEGER NOT NULL,
9+
row_hash TEXT NOT NULL,
10+
payload_json TEXT NOT NULL,
11+
signature TEXT NOT NULL,
12+
key_id TEXT NOT NULL,
13+
backend TEXT NOT NULL CHECK (backend IN ('rekor', 'git', 'ots', 'bittensor')),
14+
-- For bittensor: {netuid, blockNumber, blockHash, hotkey} — deliberately the FULL historical-retrieval
15+
-- reference: CommitmentOf is overwritten in place on-chain, so a verifier needs the block, not chain state.
16+
backend_ref TEXT,
17+
proof_r2_key TEXT,
18+
status TEXT NOT NULL CHECK (status IN ('ok', 'failed')),
19+
error TEXT,
20+
created_at TEXT NOT NULL
21+
);
22+
INSERT INTO decision_ledger_anchors_new SELECT * FROM decision_ledger_anchors;
23+
DROP TABLE decision_ledger_anchors;
24+
ALTER TABLE decision_ledger_anchors_new RENAME TO decision_ledger_anchors;
25+
CREATE INDEX IF NOT EXISTS decision_ledger_anchors_created_at ON decision_ledger_anchors (created_at DESC);
26+
CREATE INDEX IF NOT EXISTS decision_ledger_anchors_backend_created_at ON decision_ledger_anchors (backend, created_at DESC);

src/api/routes.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,10 @@ import { getContributorTrustProfile } from "../review/contributor-trust-profile"
305305
import { backfillContributorGateHistory } from "../review/contributor-gate-history-backfill";
306306
import { isFairnessAnalyticsEnabled, resolveFairnessAnalyticsManifestOverride } from "../review/contributor-trust-profile-wire";
307307
import { isRagEnabled } from "../review/rag-wire";
308-
import { loadPublicDecisionRecord, loadPublicLedgerRow, verifyDecisionLedger } from "../review/decision-record";
308+
import { loadDecisionLedgerTip, loadPublicDecisionRecord, loadPublicLedgerRow, verifyDecisionLedger } from "../review/decision-record";
309309
import { buildEvalScoreRecordsFromRulePrecision, filterEvalScoreRecords } from "../review/eval-score-records";
310-
import { currentAnchorKey, parseAnchorPublicKeys } from "../review/ledger-anchor";
310+
import { anchorSigningInput, buildLedgerAnchorPayload, currentAnchorKey, parseAnchorPublicKeys, signLedgerAnchorPayload } from "../review/ledger-anchor";
311+
import { ingestBittensorAnchorReport, parseBittensorAnchorReport } from "../review/ledger-anchor-bittensor";
311312
import { loadPublicLedgerAnchors } from "../review/ledger-anchor-persistence";
312313
import { getPublicStats, isPublicStatsEnabled, resolvePublicStatsManifestOverride } from "../review/public-stats";
313314
import { loadPublicAccuracyTrend } from "../services/public-accuracy-trend";
@@ -1339,7 +1340,7 @@ export function createApp() {
13391340
// Built with spreads, not literal undefined-valued keys: exactOptionalPropertyTypes means an optional
13401341
// filter field must be OMITTED to mean "no filter", not present-with-value-undefined.
13411342
const backendParam = c.req.query("backend");
1342-
const backend = backendParam === "rekor" || backendParam === "git" || backendParam === "ots" ? backendParam : undefined;
1343+
const backend = backendParam === "rekor" || backendParam === "git" || backendParam === "ots" || backendParam === "bittensor" ? backendParam : undefined;
13431344
const before = c.req.query("before");
13441345
const limit = Number(c.req.query("limit")) || undefined;
13451346
const result = await loadPublicLedgerAnchors(c.env, {
@@ -1351,6 +1352,48 @@ export function createApp() {
13511352
return c.json(result);
13521353
});
13531354

1355+
// #9277 (epic #9267): the current tip's SIGNED checkpoint, for the operator's off-Worker Bittensor
1356+
// commitment submitter to fetch and commit on-chain (sha256 of `signingInput` is the exact 32 bytes
1357+
// `Data::Sha256` holds). Unauthenticated like every /v1/public/* sibling: it is the same payload the
1358+
// Rekor/git backends already publish externally on every checkpoint — hashes, a seq, a timestamp and a
1359+
// key id, nothing else. `no-store`: `at` is minted per call, so a cached copy would just make two
1360+
// submitters commit two different payload hashes for the same tip for no reason.
1361+
app.get("/v1/public/decision-ledger/anchor-payload", async (c) => {
1362+
const keys = parseAnchorPublicKeys(c.env.LOOPOVER_LEDGER_ANCHOR_KEYS);
1363+
const current = currentAnchorKey(keys);
1364+
if (!current || !c.env.LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY) return c.json({ error: "anchor_signing_unconfigured" }, 404);
1365+
const tip = await loadDecisionLedgerTip(c.env);
1366+
if (tip.seq === 0) return c.json({ error: "empty_ledger" }, 404);
1367+
const payload = buildLedgerAnchorPayload(tip, nowIso());
1368+
const signed = await signLedgerAnchorPayload(payload, c.env.LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY, current.keyId);
1369+
c.header("Cache-Control", "no-store");
1370+
return c.json({ signed, signingInput: anchorSigningInput(payload) });
1371+
});
1372+
1373+
// #9277 (epic #9267): the operator's off-Worker Bittensor submitter reports each on-chain anchor attempt
1374+
// (success AND failure) back into #9271's public attempt log. Bearer-gated, FAILS CLOSED when the token is
1375+
// unset (isAuthorizedIngest, same posture as /v1/orb/ingest). Authentication alone is deliberately not
1376+
// enough for an `ok` row: the report's signed payload must verify against a PUBLISHED anchor key and its
1377+
// (seq, rowHash) must match the LIVE chain row — the public log asserting on-chain corroboration that a
1378+
// buggy submitter never actually anchored would be worse than no log at all. A `failed` report skips those
1379+
// checks: "the submitter is broken" is exactly what the attempt log exists to make publicly visible.
1380+
app.post("/v1/decision-ledger/anchor-attempts", async (c) => {
1381+
if (!(await isAuthorizedIngest(c.env.LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN, extractBearerToken(c.req.header("authorization"))))) return c.json({ error: "unauthorized" }, 401);
1382+
const body = await readOrbIngestBody(c.req.raw, c.req.header("content-length"));
1383+
if (body === null) return c.json({ error: "payload_too_large" }, 413);
1384+
let raw: unknown;
1385+
try {
1386+
raw = JSON.parse(body || "");
1387+
} catch {
1388+
return c.json({ error: "invalid_json" }, 400);
1389+
}
1390+
const parsed = parseBittensorAnchorReport(raw);
1391+
if ("error" in parsed) return c.json({ error: "invalid_report", detail: parsed.error }, 400);
1392+
const outcome = await ingestBittensorAnchorReport(c.env, parsed.report);
1393+
if (!outcome.recorded) return c.json({ error: outcome.reason }, 422);
1394+
return c.json({ recorded: true, status: outcome.status }, 200);
1395+
});
1396+
13541397
// #9123: the decision record itself was persisted (decision_records) but never published anywhere a
13551398
// contributor or a third party could fetch the full body — the only prior public surface was
13561399
// renderDecisionRecordSection's bounded review-comment summary (12-char digest prefixes, and it omits

src/auth/route-auth.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ export function requiresApiToken(path: string): boolean {
4848
if (path === "/v1/public/decision-ledger/anchor-key") return false;
4949
// #9271: the public anchor-attempt listing, added in the SAME PR as its route.
5050
if (path === "/v1/public/decision-ledger/anchors") return false;
51+
// #9277: the current tip's signed checkpoint, for the operator's off-Worker Bittensor submitter (and
52+
// anyone else — it is the same payload the Rekor/git backends already publish externally). Added in the
53+
// SAME PR as its route, per the #9120 lesson.
54+
if (path === "/v1/public/decision-ledger/anchor-payload") return false;
55+
// #9277: the submitter's report route carries its OWN bearer gate (isAuthorizedIngest against
56+
// LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN, fails closed when unset) — same posture as /v1/orb/ingest below.
57+
if (path === "/v1/decision-ledger/anchor-attempts") return false;
5158
// #9123: the new public decision-record read route — same unauthenticated posture as its ledger-verify
5259
// sibling immediately above, added in the SAME PR so the two can never drift apart the way #9120 did. The
5360
// pull segment matches any non-slash text (not just digits): an invalid pull number is the ROUTE's 400 to

src/env.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,12 @@ declare global {
652652
* chokepoint every other GitHub write in this engine goes through). Unset (alongside the owner/repo
653653
* pair above) means the git backend does not run this tick; Rekor is unaffected. */
654654
LOOPOVER_LEDGER_ANCHOR_GIT_INSTALLATION_ID?: string;
655+
/** External ledger anchoring (#9277, epic #9267): bearer token the operator's OFF-Worker Bittensor
656+
* commitment submitter presents to `POST /v1/decision-ledger/anchor-attempts` when reporting an
657+
* on-chain anchor attempt back into the public attempt log. FAILS CLOSED when unset (the route
658+
* rejects everything, same isAuthorizedIngest posture as ORB_INGEST_TOKEN) — the submitter itself and
659+
* its hotkey live entirely on the operator's node infrastructure, never in this repo or this Worker. */
660+
LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN?: string;
655661
/** Convergence (port): public OAuth draft-submission flow ported from reviewbot. When truthy, the
656662
* /v1/drafts endpoints accept a contributor draft -> GitHub OAuth -> fork PR against the content repo.
657663
* Default OFF — unset/false makes every draft endpoint 404 and writes nothing (byte-identical worker). */

src/openapi/spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1901,11 +1901,36 @@ export function buildOpenApiSpec() {
19011901
operationId: "listPublicDecisionLedgerAnchors",
19021902
tags: ["Public"],
19031903
summary: "Every external anchoring attempt, success and failure, paginated newest-first — anchoring's own health as a public fact",
1904-
request: { query: z.object({ backend: z.enum(["rekor", "git", "ots"]).optional(), before: z.string().optional(), limit: z.string().optional() }) },
1904+
request: { query: z.object({ backend: z.enum(["rekor", "git", "ots", "bittensor"]).optional(), before: z.string().optional(), limit: z.string().optional() }) },
19051905
responses: {
19061906
200: { description: "{ anchors: [{ id, seq, rowHash, keyId, backend, backendRef, status, error, createdAt }], nextBefore } — a failed attempt is returned identically to a successful one, never filtered out or reshaped" },
19071907
},
19081908
});
1909+
registry.registerPath({
1910+
method: "get",
1911+
path: "/v1/public/decision-ledger/anchor-payload",
1912+
operationId: "getPublicDecisionLedgerAnchorPayload",
1913+
tags: ["Public"],
1914+
summary: "The current ledger tip as a freshly signed checkpoint, for an external anchoring submitter to commit",
1915+
responses: {
1916+
200: { description: "{ signed: { payload, keyId, signature }, signingInput } — `sha256(signingInput)` is the exact 32 bytes an on-chain commitment holds. Never cached: `payload.at` is minted per call" },
1917+
404: { description: "Anchor signing is not configured, or the ledger is empty — nothing is claimed to be anchorable yet" },
1918+
},
1919+
});
1920+
registry.registerPath({
1921+
method: "post",
1922+
path: "/v1/decision-ledger/anchor-attempts",
1923+
operationId: "reportDecisionLedgerAnchorAttempt",
1924+
tags: ["Public"],
1925+
summary: "Report one off-Worker anchoring attempt (success or failure) into the public attempt log",
1926+
responses: {
1927+
200: { description: "{ recorded: true, status: 'ok' | 'failed' }" },
1928+
400: { description: "Unparseable body, or a report whose named field failed validation" },
1929+
401: { description: "Missing or wrong bearer token; also returned when no report token is configured (fails closed)" },
1930+
413: { description: "Body exceeded the ingest ceiling" },
1931+
422: { description: "Authenticated but unverifiable: unknown_key, bad_signature, row_not_found, or row_hash_mismatch — an `ok` report must verify against a published key AND match the live chain row" },
1932+
},
1933+
});
19091934
registry.registerPath({
19101935
method: "get",
19111936
path: "/v1/public/decision-records/{owner}/{repo}/{pull}",

0 commit comments

Comments
 (0)