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
122 changes: 121 additions & 1 deletion apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -26362,7 +26362,127 @@
{
"OrbBearer": []
}
]
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"signed": {
"type": "object",
"properties": {
"payload": {
"type": "object",
"properties": {
"v": {
"type": "number",
"enum": [
1
]
},
"ledger": {
"type": "string",
"enum": [
"loopover.decision_ledger"
]
},
"seq": {
"type": "integer",
"minimum": 0,
"exclusiveMinimum": true
},
"rowHash": {
"type": "string",
"pattern": "^[0-9a-fA-F]{64}$"
},
"totalCount": {
"type": "integer",
"minimum": 0,
"exclusiveMinimum": true
},
"at": {
"type": "string",
"minLength": 1,
"maxLength": 40
}
},
"required": [
"v",
"ledger",
"seq",
"rowHash",
"totalCount",
"at"
]
},
"keyId": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"signature": {
"type": "string",
"minLength": 1,
"maxLength": 512
}
},
"required": [
"payload",
"keyId",
"signature"
]
},
"status": {
"type": "string",
"enum": [
"ok",
"failed"
]
},
"backendRef": {
"type": "object",
"properties": {
"netuid": {
"type": "integer",
"minimum": 0,
"maximum": 65535
},
"blockNumber": {
"type": "integer",
"minimum": 0,
"exclusiveMinimum": true
},
"blockHash": {
"type": "string",
"pattern": "^0x[0-9a-fA-F]{64}$"
},
"hotkey": {
"type": "string",
"minLength": 1,
"maxLength": 64
}
},
"required": [
"netuid",
"blockNumber",
"blockHash",
"hotkey"
]
},
"error": {
"type": "string",
"minLength": 1
}
},
"required": [
"signed",
"status"
]
}
}
}
}
}
},
"/v1/public/repos/{owner}/{repo}/proof": {
Expand Down
13 changes: 11 additions & 2 deletions src/openapi/internal-and-public-route-specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import type { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
import { z } from "zod";
import { registerRouteSpec, type RouteAuth, type RouteMethod } from "./define-route";
import { BittensorAnchorReportRequestSchema } from "../review/ledger-anchor-bittensor";

type SpecEntry = {
method: RouteMethod;
Expand All @@ -20,8 +21,11 @@ type SpecEntry = {
tags: [string, ...string[]];
summary: string;
auth: RouteAuth;
/** Narrower path parameters than the derived string ones; only for a closed-set segment (#9707). */
request?: { params?: z.ZodObject };
/** Narrower path parameters than the derived string ones; only for a closed-set segment (#9707), plus the
* request body for an operation that publishes one (#9770). Both are already honoured by
* registerRouteSpec -- `body` was reachable through RouteSpecOptions but not through THIS local type, so
* an entry here could not describe its own body even though the seam knew how to render it. */
request?: { params?: z.ZodObject; body?: z.ZodTypeAny };
/** `schema` is optional: most entries here describe a status and nothing more, but an operation that
* already published a response body must not lose it on the way through the seam (#9707). */
responses: Record<number, { description: string; schema?: z.ZodTypeAny }>;
Expand Down Expand Up @@ -492,6 +496,11 @@ const CREDENTIAL_GATED: SpecEntry[] = [
// `orb`, not `token`: the gate is LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN, an ingest bearer that is not a
// LoopOver API token -- the same posture the `orb` level already exists for.
auth: "orb",
// #9770: this endpoint exists to be called by a submitter this repo deliberately does not ship, so the
// OpenAPI document is its only reference -- and it published responses with no body at all, leaving the
// whole shape discoverable only by reading the TypeScript that rejects it. The schema lives beside
// parseBittensorAnchorReport, which remains the runtime authority; a cross-check test holds them together.
request: { body: BittensorAnchorReportRequestSchema },
responses: {
200: { description: "{ recorded: true, status: 'ok' | 'failed' }" },
400: { description: "Unparseable body, or a report whose named field failed validation" },
Expand Down
67 changes: 65 additions & 2 deletions src/review/ledger-anchor-bittensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
//
// Scope (#9277's own framing): optional, Gittensor/SN74-audience corroboration. Rekor + git remain the
// default every verifier is told to check; nothing here is a required verification step.
import { z } from "zod";
import type { LedgerAnchorPayload, SignedLedgerAnchor } from "./ledger-anchor";
import { anchorKeyById, parseAnchorPublicKeys, verifyLedgerAnchorSignature } from "./ledger-anchor";
import { LEDGER_ANCHOR_LEDGER_ID, LEDGER_ANCHOR_PAYLOAD_VERSION } from "./ledger-anchor";
Expand All @@ -41,8 +42,70 @@ export type BittensorAnchorReport = {
signed: SignedLedgerAnchor;
} & ({ status: "ok"; backendRef: BittensorAnchorRef } | { status: "failed"; error: string });

const HEX_32_BYTES = /^0x[0-9a-f]{64}$/i;
const ROW_HASH = /^[0-9a-f]{64}$/i;
// Case-insensitivity is spelled into the character class rather than carried as an `i` FLAG, because these
// regexes are now published as JSON Schema `pattern`s (#9770) and JSON Schema has no flags concept: a
// flagged regex serializes its source with the flag appended, so `^[0-9a-f]{64}$/i` reaches a generated
// client as a pattern requiring a literal "/i" suffix -- rejecting every valid hash. Behaviourally identical
// to the flagged form for these two inputs, and safe to publish.
const HEX_32_BYTES = /^0x[0-9a-fA-F]{64}$/;
const ROW_HASH = /^[0-9a-fA-F]{64}$/;

/**
* The published request shape for `POST /v1/decision-ledger/anchor-attempts` (#9770).
*
* This endpoint exists to be called by a submitter this repo deliberately does NOT ship (see the module
* header): a process on the operator's own node infrastructure. Its only reference is the OpenAPI document,
* which registered responses and no body at all — so the contract's entire shape lived in the TypeScript of
* the thing rejecting it, and an implementer had to read source to discover the bounds.
*
* It lives HERE, immediately beside {@link parseBittensorAnchorReport}, rather than in the OpenAPI spec file,
* because the two are one contract described twice and must be edited together. `parseBittensorAnchorReport`
* stays the runtime authority — it is not replaced by this schema — because each of its arms returns a NAMED
* rejection naming the exact field it refused, which is what makes a submitter bug diagnosable from the 400
* body alone; a generic zod issue list would be a downgrade for the caller this is built for. The two are
* held together by a cross-check test that runs the same corpus of payloads through both and asserts they
* agree on accept/reject, so neither can drift without failing CI.
*/
export const BittensorAnchorReportRequestSchema = z
.object({
signed: z.object({
payload: z.object({
v: z.literal(LEDGER_ANCHOR_PAYLOAD_VERSION),
ledger: z.literal(LEDGER_ANCHOR_LEDGER_ID),
/** 1-based row number of the ledger tip being anchored. */
seq: z.number().int().positive(),
/** The tip row's hash — 64 hex chars, case-insensitive. */
rowHash: z.string().regex(ROW_HASH),
/** Total rows in the ledger at the time of signing. */
totalCount: z.number().int().positive(),
/** When the checkpoint was signed. Bounded rather than format-validated, matching the runtime check. */
at: z.string().min(1).max(40),
}),
/** Which published key signed this checkpoint; must appear in LOOPOVER_LEDGER_ANCHOR_KEYS. */
keyId: z.string().min(1).max(64),
/** base64 ECDSA P-256 signature over `anchorSigningInput(payload)`. */
signature: z.string().min(1).max(512),
}),
status: z.enum(["ok", "failed"]),
/** Required when `status` is "ok". The on-chain coordinates a verifier needs to retrieve the commitment
* from archive state, since `CommitmentOf` is overwritten in place. */
backendRef: z
.object({
netuid: z.number().int().min(0).max(65535),
blockNumber: z.number().int().positive(),
/** 0x-prefixed 32-byte block hash, case-insensitive; normalized to lowercase on ingest. */
blockHash: z.string().regex(HEX_32_BYTES),
/** ss58 account of the anchor hotkey — a public on-chain identity, never key material. */
hotkey: z.string().trim().min(1).max(64),
})
.optional(),
/** Required when `status` is "failed". Recorded verbatim (trimmed, truncated to 500) so a submitter that
* could not commit is still visible in the public attempt log rather than silently absent. */
error: z.string().trim().min(1).optional(),
})
.refine((body) => (body.status === "ok" ? body.backendRef !== undefined : body.error !== undefined), {
message: 'backendRef is required when status is "ok"; error is required when status is "failed"',
});

/** Parse + bound one submitter report. Returns a typed report or a NAMED rejection reason — every arm names
* the exact field it refused so a submitter bug is diagnosable from the 400 body alone. PURE. */
Expand Down
64 changes: 64 additions & 0 deletions test/unit/ledger-anchor-bittensor.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import {
BittensorAnchorReportRequestSchema,
ingestBittensorAnchorReport,
parseBittensorAnchorReport,
type BittensorAnchorReport,
Expand Down Expand Up @@ -242,3 +243,66 @@ describe("#9277 routes — the submitter's two HTTP touchpoints", () => {
}
});
});

// #9770: the published request schema and the runtime validator are one contract described twice --
// parseBittensorAnchorReport stays the authority (its per-field named rejections are what make a submitter
// bug diagnosable from the 400 body), and the zod schema exists so the OpenAPI document actually describes
// the shape a caller must send. This suite is the mechanism that stops them drifting: the SAME payloads go
// through both, and the two must agree on accept/reject.
describe("BittensorAnchorReportRequestSchema agrees with parseBittensorAnchorReport (#9770)", () => {
const okBody = () => ({
signed: {
payload: {
v: LEDGER_ANCHOR_PAYLOAD_VERSION,
ledger: LEDGER_ANCHOR_LEDGER_ID,
seq: 7,
rowHash: "a".repeat(64),
totalCount: 7,
at: "2026-07-29T00:00:00.000Z",
},
keyId: "k1",
signature: "c2ln",
},
status: "ok" as const,
backendRef: { netuid: 42, blockNumber: 1234, blockHash: `0x${"b".repeat(64)}`, hotkey: "5Fabc" },
});
const failedBody = () => ({ signed: okBody().signed, status: "failed" as const, error: "commitment reverted" });

/** Every case below is run through BOTH. `accepted` is what they must agree on. */
const cases: Array<{ name: string; body: unknown; accepted: boolean }> = [
{ name: "a well-formed ok report", body: okBody(), accepted: true },
{ name: "a well-formed failed report", body: failedBody(), accepted: true },
{ name: "an UPPERCASE rowHash / blockHash (case-insensitive by class, not by flag)", body: { ...okBody(), signed: { ...okBody().signed, payload: { ...okBody().signed.payload, rowHash: "A".repeat(64) } }, backendRef: { ...okBody().backendRef, blockHash: `0x${"B".repeat(64)}` } }, accepted: true },
{ name: "a non-object body", body: "nope", accepted: false },
{ name: "a missing signed block", body: { status: "ok" }, accepted: false },
{ name: "a wrong payload version", body: { ...okBody(), signed: { ...okBody().signed, payload: { ...okBody().signed.payload, v: 2 } } }, accepted: false },
{ name: "a wrong ledger id", body: { ...okBody(), signed: { ...okBody().signed, payload: { ...okBody().signed.payload, ledger: "someone.else" } } }, accepted: false },
{ name: "a zero seq", body: { ...okBody(), signed: { ...okBody().signed, payload: { ...okBody().signed.payload, seq: 0 } } }, accepted: false },
{ name: "a non-integer seq", body: { ...okBody(), signed: { ...okBody().signed, payload: { ...okBody().signed.payload, seq: 1.5 } } }, accepted: false },
{ name: "a short rowHash", body: { ...okBody(), signed: { ...okBody().signed, payload: { ...okBody().signed.payload, rowHash: "abc" } } }, accepted: false },
{ name: "an empty at", body: { ...okBody(), signed: { ...okBody().signed, payload: { ...okBody().signed.payload, at: "" } } }, accepted: false },
{ name: "an over-long at", body: { ...okBody(), signed: { ...okBody().signed, payload: { ...okBody().signed.payload, at: "x".repeat(41) } } }, accepted: false },
{ name: "an empty keyId", body: { ...okBody(), signed: { ...okBody().signed, keyId: "" } }, accepted: false },
{ name: "an over-long keyId", body: { ...okBody(), signed: { ...okBody().signed, keyId: "k".repeat(65) } }, accepted: false },
{ name: "an over-long signature", body: { ...okBody(), signed: { ...okBody().signed, signature: "s".repeat(513) } }, accepted: false },
{ name: "an unknown status", body: { ...okBody(), status: "maybe" }, accepted: false },
{ name: "an ok report with no backendRef", body: { signed: okBody().signed, status: "ok" }, accepted: false },
{ name: "a failed report with no error", body: { signed: okBody().signed, status: "failed" }, accepted: false },
{ name: "a failed report with a blank error", body: { ...failedBody(), error: " " }, accepted: false },
{ name: "a netuid above the u16 ceiling", body: { ...okBody(), backendRef: { ...okBody().backendRef, netuid: 65_536 } }, accepted: false },
{ name: "a negative netuid", body: { ...okBody(), backendRef: { ...okBody().backendRef, netuid: -1 } }, accepted: false },
{ name: "a zero blockNumber", body: { ...okBody(), backendRef: { ...okBody().backendRef, blockNumber: 0 } }, accepted: false },
{ name: "a blockHash missing its 0x prefix", body: { ...okBody(), backendRef: { ...okBody().backendRef, blockHash: "b".repeat(64) } }, accepted: false },
{ name: "a blank hotkey", body: { ...okBody(), backendRef: { ...okBody().backendRef, hotkey: " " } }, accepted: false },
{ name: "an over-long hotkey", body: { ...okBody(), backendRef: { ...okBody().backendRef, hotkey: "h".repeat(65) } }, accepted: false },
];

it.each(cases)("$name", ({ body, accepted }) => {
const runtime = !("error" in parseBittensorAnchorReport(body));
const published = BittensorAnchorReportRequestSchema.safeParse(body).success;
expect(runtime).toBe(accepted);
// The load-bearing assertion: the document never promises a shape the endpoint refuses, and never
// refuses one the document promises.
expect(published).toBe(runtime);
});
});
Loading