Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions apps/loopover-ui/content/docs/what-you-can-verify.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,28 @@ asking LoopOver anything.

<Callout variant="warn">
**Bittensor on-chain anchoring is optional, separate corroboration — not part of this default
check.** A third, Gittensor/SN74-audience-specific backend (tracked, not yet
shipped — [#9277](https://github.com/JSONbored/loopover/issues/9277)) publishes the same checkpoint
as an on-chain commitment, signed by a dedicated hotkey run on the operator's own infrastructure.
It's additive corroboration for that specific audience, never folded into the default two-backend
claim every verifier above is told to check.
check.** A third, Gittensor/SN74-audience-specific
backend ([#9277](https://github.com/JSONbored/loopover/issues/9277)) publishes the same signed
checkpoint as an on-chain commitment. A submitter on the operator's own node infrastructure — never
this Worker, and its dedicated anchor-only hotkey never leaves that infrastructure — fetches
`GET /v1/public/decision-ledger/anchor-payload` and commits `sha256(signingInput)` via the
commitments pallet's `set_commitment(netuid, Data::Sha256)`, then reports the attempt (success
*and* failure, like every other backend) back into the public attempt log, where it appears as
`backend: "bittensor"`. It's additive corroboration for that specific audience, never folded into
the default two-backend claim every verifier above is told to check.
</Callout>

<Callout variant="info">
**Historical retrieval — read the block, not chain state.** The commitments pallet's
`CommitmentOf` map is **overwritten in place**: only the *latest* commitment per (netuid, account)
survives in current chain state. To verify an older anchor, use its `backendRef` from the attempt
log — `{netuid, blockNumber, blockHash, hotkey}` — and query **archive state at that block**
(`state_getStorage` at `blockHash`, or the block's events), not the current tip. Then check the
stored commitment's `Sha256` bytes equal `sha256(signingInput)` of the anchor's own
`payload_json`, and verify the payload's signature against the published anchor keys exactly as in
step (c) above. Any Bittensor archive node can answer this; the operator's own archive is merely
the convenient one — the same trust posture as the git backend's "GitHub hosts it, the mirror
cross-checks it."
</Callout>

### 2. Decision-record authenticity
Expand Down
46 changes: 45 additions & 1 deletion apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -20766,7 +20766,8 @@
"enum": [
"rekor",
"git",
"ots"
"ots",
"bittensor"
]
},
"required": false,
Expand Down Expand Up @@ -26208,6 +26209,49 @@
}
}
}
},
"/v1/public/decision-ledger/anchor-payload": {
"get": {
"operationId": "getPublicDecisionLedgerAnchorPayload",
"tags": [
"Public"
],
"summary": "The current ledger tip as a freshly signed checkpoint, for an external anchoring submitter to commit",
"responses": {
"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"
},
"404": {
"description": "Anchor signing is not configured, or the ledger is empty — nothing is claimed to be anchorable yet"
}
}
}
},
"/v1/decision-ledger/anchor-attempts": {
"post": {
"operationId": "reportDecisionLedgerAnchorAttempt",
"tags": [
"Public"
],
"summary": "Report one off-Worker anchoring attempt (success or failure) into the public attempt log",
"responses": {
"200": {
"description": "{ recorded: true, status: 'ok' | 'failed' }"
},
"400": {
"description": "Unparseable body, or a report whose named field failed validation"
},
"401": {
"description": "Missing or wrong bearer token; also returned when no report token is configured (fails closed)"
},
"413": {
"description": "Body exceeded the ingest ceiling"
},
"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"
}
}
}
}
},
"servers": [
Expand Down
26 changes: 26 additions & 0 deletions migrations/0201_ledger_anchor_bittensor.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- #9277 (epic #9267): widen decision_ledger_anchors' backend CHECK to admit 'bittensor' — the optional,
-- Gittensor/SN74-audience on-chain commitment backend. The submission itself runs on the operator's own node
-- infrastructure (never this Worker); this table records its reported attempts, success AND failure, exactly
-- like the Rekor/git rows (#9271's whole design: a backend that can fail silently is a backend an operator
-- can silently disable). SQLite cannot ALTER a CHECK constraint, so this is the standard rebuild-and-rename.
CREATE TABLE decision_ledger_anchors_new (
id TEXT PRIMARY KEY,
seq INTEGER NOT NULL,
row_hash TEXT NOT NULL,
payload_json TEXT NOT NULL,
signature TEXT NOT NULL,
key_id TEXT NOT NULL,
backend TEXT NOT NULL CHECK (backend IN ('rekor', 'git', 'ots', 'bittensor')),
-- For bittensor: {netuid, blockNumber, blockHash, hotkey} — deliberately the FULL historical-retrieval
-- reference: CommitmentOf is overwritten in place on-chain, so a verifier needs the block, not chain state.
backend_ref TEXT,
proof_r2_key TEXT,
status TEXT NOT NULL CHECK (status IN ('ok', 'failed')),
error TEXT,
created_at TEXT NOT NULL
);
INSERT INTO decision_ledger_anchors_new SELECT * FROM decision_ledger_anchors;
DROP TABLE decision_ledger_anchors;
ALTER TABLE decision_ledger_anchors_new RENAME TO decision_ledger_anchors;
CREATE INDEX IF NOT EXISTS decision_ledger_anchors_created_at ON decision_ledger_anchors (created_at DESC);
CREATE INDEX IF NOT EXISTS decision_ledger_anchors_backend_created_at ON decision_ledger_anchors (backend, created_at DESC);
49 changes: 46 additions & 3 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,10 @@ import { getContributorTrustProfile } from "../review/contributor-trust-profile"
import { backfillContributorGateHistory } from "../review/contributor-gate-history-backfill";
import { isFairnessAnalyticsEnabled, resolveFairnessAnalyticsManifestOverride } from "../review/contributor-trust-profile-wire";
import { isRagEnabled } from "../review/rag-wire";
import { loadPublicDecisionRecord, loadPublicLedgerRow, verifyDecisionLedger } from "../review/decision-record";
import { loadDecisionLedgerTip, loadPublicDecisionRecord, loadPublicLedgerRow, verifyDecisionLedger } from "../review/decision-record";
import { buildEvalScoreRecordsFromRulePrecision, filterEvalScoreRecords } from "../review/eval-score-records";
import { currentAnchorKey, parseAnchorPublicKeys } from "../review/ledger-anchor";
import { anchorSigningInput, buildLedgerAnchorPayload, currentAnchorKey, parseAnchorPublicKeys, signLedgerAnchorPayload } from "../review/ledger-anchor";
import { ingestBittensorAnchorReport, parseBittensorAnchorReport } from "../review/ledger-anchor-bittensor";
import { loadPublicLedgerAnchors } from "../review/ledger-anchor-persistence";
import { getPublicStats, isPublicStatsEnabled, resolvePublicStatsManifestOverride } from "../review/public-stats";
import { loadPublicAccuracyTrend } from "../services/public-accuracy-trend";
Expand Down Expand Up @@ -1339,7 +1340,7 @@ export function createApp() {
// Built with spreads, not literal undefined-valued keys: exactOptionalPropertyTypes means an optional
// filter field must be OMITTED to mean "no filter", not present-with-value-undefined.
const backendParam = c.req.query("backend");
const backend = backendParam === "rekor" || backendParam === "git" || backendParam === "ots" ? backendParam : undefined;
const backend = backendParam === "rekor" || backendParam === "git" || backendParam === "ots" || backendParam === "bittensor" ? backendParam : undefined;
const before = c.req.query("before");
const limit = Number(c.req.query("limit")) || undefined;
const result = await loadPublicLedgerAnchors(c.env, {
Expand All @@ -1351,6 +1352,48 @@ export function createApp() {
return c.json(result);
});

// #9277 (epic #9267): the current tip's SIGNED checkpoint, for the operator's off-Worker Bittensor
// commitment submitter to fetch and commit on-chain (sha256 of `signingInput` is the exact 32 bytes
// `Data::Sha256` holds). Unauthenticated like every /v1/public/* sibling: it is the same payload the
// Rekor/git backends already publish externally on every checkpoint — hashes, a seq, a timestamp and a
// key id, nothing else. `no-store`: `at` is minted per call, so a cached copy would just make two
// submitters commit two different payload hashes for the same tip for no reason.
app.get("/v1/public/decision-ledger/anchor-payload", async (c) => {
const keys = parseAnchorPublicKeys(c.env.LOOPOVER_LEDGER_ANCHOR_KEYS);
const current = currentAnchorKey(keys);
if (!current || !c.env.LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY) return c.json({ error: "anchor_signing_unconfigured" }, 404);
const tip = await loadDecisionLedgerTip(c.env);
if (tip.seq === 0) return c.json({ error: "empty_ledger" }, 404);
const payload = buildLedgerAnchorPayload(tip, nowIso());
const signed = await signLedgerAnchorPayload(payload, c.env.LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY, current.keyId);
c.header("Cache-Control", "no-store");
return c.json({ signed, signingInput: anchorSigningInput(payload) });
});

// #9277 (epic #9267): the operator's off-Worker Bittensor submitter reports each on-chain anchor attempt
// (success AND failure) back into #9271's public attempt log. Bearer-gated, FAILS CLOSED when the token is
// unset (isAuthorizedIngest, same posture as /v1/orb/ingest). Authentication alone is deliberately not
// enough for an `ok` row: the report's signed payload must verify against a PUBLISHED anchor key and its
// (seq, rowHash) must match the LIVE chain row — the public log asserting on-chain corroboration that a
// buggy submitter never actually anchored would be worse than no log at all. A `failed` report skips those
// checks: "the submitter is broken" is exactly what the attempt log exists to make publicly visible.
app.post("/v1/decision-ledger/anchor-attempts", async (c) => {
if (!(await isAuthorizedIngest(c.env.LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN, extractBearerToken(c.req.header("authorization"))))) return c.json({ error: "unauthorized" }, 401);
const body = await readOrbIngestBody(c.req.raw, c.req.header("content-length"));
if (body === null) return c.json({ error: "payload_too_large" }, 413);
let raw: unknown;
try {
raw = JSON.parse(body || "");
} catch {
return c.json({ error: "invalid_json" }, 400);
}
const parsed = parseBittensorAnchorReport(raw);
if ("error" in parsed) return c.json({ error: "invalid_report", detail: parsed.error }, 400);
const outcome = await ingestBittensorAnchorReport(c.env, parsed.report);
if (!outcome.recorded) return c.json({ error: outcome.reason }, 422);
return c.json({ recorded: true, status: outcome.status }, 200);
});

// #9123: the decision record itself was persisted (decision_records) but never published anywhere a
// contributor or a third party could fetch the full body — the only prior public surface was
// renderDecisionRecordSection's bounded review-comment summary (12-char digest prefixes, and it omits
Expand Down
7 changes: 7 additions & 0 deletions src/auth/route-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ export function requiresApiToken(path: string): boolean {
if (path === "/v1/public/decision-ledger/anchor-key") return false;
// #9271: the public anchor-attempt listing, added in the SAME PR as its route.
if (path === "/v1/public/decision-ledger/anchors") return false;
// #9277: the current tip's signed checkpoint, for the operator's off-Worker Bittensor submitter (and
// anyone else — it is the same payload the Rekor/git backends already publish externally). Added in the
// SAME PR as its route, per the #9120 lesson.
if (path === "/v1/public/decision-ledger/anchor-payload") return false;
// #9277: the submitter's report route carries its OWN bearer gate (isAuthorizedIngest against
// LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN, fails closed when unset) — same posture as /v1/orb/ingest below.
if (path === "/v1/decision-ledger/anchor-attempts") return false;
// #9123: the new public decision-record read route — same unauthenticated posture as its ledger-verify
// sibling immediately above, added in the SAME PR so the two can never drift apart the way #9120 did. The
// pull segment matches any non-slash text (not just digits): an invalid pull number is the ROUTE's 400 to
Expand Down
6 changes: 6 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,12 @@ declare global {
* chokepoint every other GitHub write in this engine goes through). Unset (alongside the owner/repo
* pair above) means the git backend does not run this tick; Rekor is unaffected. */
LOOPOVER_LEDGER_ANCHOR_GIT_INSTALLATION_ID?: string;
/** External ledger anchoring (#9277, epic #9267): bearer token the operator's OFF-Worker Bittensor
* commitment submitter presents to `POST /v1/decision-ledger/anchor-attempts` when reporting an
* on-chain anchor attempt back into the public attempt log. FAILS CLOSED when unset (the route
* rejects everything, same isAuthorizedIngest posture as ORB_INGEST_TOKEN) — the submitter itself and
* its hotkey live entirely on the operator's node infrastructure, never in this repo or this Worker. */
LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN?: string;
/** Convergence (port): public OAuth draft-submission flow ported from reviewbot. When truthy, the
* /v1/drafts endpoints accept a contributor draft -> GitHub OAuth -> fork PR against the content repo.
* Default OFF — unset/false makes every draft endpoint 404 and writes nothing (byte-identical worker). */
Expand Down
27 changes: 26 additions & 1 deletion src/openapi/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1901,11 +1901,36 @@ export function buildOpenApiSpec() {
operationId: "listPublicDecisionLedgerAnchors",
tags: ["Public"],
summary: "Every external anchoring attempt, success and failure, paginated newest-first — anchoring's own health as a public fact",
request: { query: z.object({ backend: z.enum(["rekor", "git", "ots"]).optional(), before: z.string().optional(), limit: z.string().optional() }) },
request: { query: z.object({ backend: z.enum(["rekor", "git", "ots", "bittensor"]).optional(), before: z.string().optional(), limit: z.string().optional() }) },
responses: {
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" },
},
});
registry.registerPath({
method: "get",
path: "/v1/public/decision-ledger/anchor-payload",
operationId: "getPublicDecisionLedgerAnchorPayload",
tags: ["Public"],
summary: "The current ledger tip as a freshly signed checkpoint, for an external anchoring submitter to commit",
responses: {
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" },
404: { description: "Anchor signing is not configured, or the ledger is empty — nothing is claimed to be anchorable yet" },
},
});
registry.registerPath({
method: "post",
path: "/v1/decision-ledger/anchor-attempts",
operationId: "reportDecisionLedgerAnchorAttempt",
tags: ["Public"],
summary: "Report one off-Worker anchoring attempt (success or failure) into the public attempt log",
responses: {
200: { description: "{ recorded: true, status: 'ok' | 'failed' }" },
400: { description: "Unparseable body, or a report whose named field failed validation" },
401: { description: "Missing or wrong bearer token; also returned when no report token is configured (fails closed)" },
413: { description: "Body exceeded the ingest ceiling" },
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" },
},
});
registry.registerPath({
method: "get",
path: "/v1/public/decision-records/{owner}/{repo}/{pull}",
Expand Down
Loading
Loading