diff --git a/.env.example b/.env.example index 1a150b6c..993c5eb4 100644 --- a/.env.example +++ b/.env.example @@ -34,3 +34,7 @@ CHAINALYSIS_KEY= ## badge at a branch/fork while the PET PR is in review. # NEXT_PUBLIC_PET_REFERENCE_URL=https://raw.githubusercontent.com/open-web3-stack/polkadot-ecosystem-tests/master/packages/shared/src/snowbridge/referencePreimages.json # NEXT_PUBLIC_PET_REFERENCE_BLOB_URL=https://github.com/open-web3-stack/polkadot-ecosystem-tests/blob/master/packages/shared/src/snowbridge/referencePreimages.json + +## Governance badge: GitHub API URL used to show "last verified against live chains" +## (last commit to PET's known-good block file, updated by its 6-hourly cron). +# NEXT_PUBLIC_PET_KNOWN_GOOD_URL=https://api.github.com/repos/open-web3-stack/polkadot-ecosystem-tests/commits?path=KNOWN_GOOD_BLOCK_NUMBERS_POLKADOT.env&per_page=1 diff --git a/__tests__/preimageReference.test.ts b/__tests__/preimageReference.test.ts index 498c7fe8..eb3489a7 100644 --- a/__tests__/preimageReference.test.ts +++ b/__tests__/preimageReference.test.ts @@ -1,4 +1,4 @@ -import { compareToReference } from "@/lib/preimageReference"; +import { compareToReference, parseLastVerified } from "@/lib/preimageReference"; import type { ReferenceFile } from "@/lib/preimageReference"; import { describe, expect, test } from "vitest"; @@ -68,3 +68,23 @@ describe("compareToReference", () => { ).toBe("mismatch"); }); }); + +describe("parseLastVerified", () => { + test("extracts the committer date of the first commit", () => { + const d = parseLastVerified([ + { commit: { committer: { date: "2026-07-07T12:00:00Z" } } }, + ]); + expect(d?.toISOString()).toBe("2026-07-07T12:00:00.000Z"); + }); + + test("returns null for an empty array", () => { + expect(parseLastVerified([])).toBeNull(); + }); + + test("returns null for a malformed / missing date", () => { + expect(parseLastVerified([{ commit: { committer: {} } }])).toBeNull(); + expect( + parseLastVerified([{ commit: { committer: { date: "not-a-date" } } }]), + ).toBeNull(); + }); +}); diff --git a/components/governance/HaltBridgeForm.tsx b/components/governance/HaltBridgeForm.tsx index e3db081a..2c44e73e 100644 --- a/components/governance/HaltBridgeForm.tsx +++ b/components/governance/HaltBridgeForm.tsx @@ -782,6 +782,15 @@ function PreimageResult({ ); } +// Relative age of the last live-chain verification, e.g. "3 hours ago". +function formatVerifiedAgo(date: Date): string { + const hours = Math.round((Date.now() - date.getTime()) / 3_600_000); + if (hours < 1) return "less than an hour ago"; + if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`; + const days = Math.round(hours / 24); + return `${days} day${days === 1 ? "" : "s"} ago`; +} + function ReferenceCheck({ operation, result, @@ -842,8 +851,6 @@ function ReferenceCheck({ } if (verdict.kind === "match") { - const bh = verdict.reference.bridgeHubRuntime; - const ah = verdict.reference.assetHubRuntime; return (
@@ -854,11 +861,8 @@ function ReferenceCheck({ These bytes are byte-identical to the canonical full {operation}{" "} pinned in polkadot-ecosystem-tests and re-executed against forked live chains on a schedule. - {ah && bh - ? ` Reference generated against Asset Hub ${ah.specVersion} / Bridge Hub ${bh.specVersion}` + - (verdict.reference.generatedAt - ? ` (${verdict.reference.generatedAt}).` - : ".") + {verdict.lastVerifiedAt + ? ` Last verified against live chains ${formatVerifiedAgo(verdict.lastVerifiedAt)}.` : ""}

{disclaimer} diff --git a/lib/preimageReference.ts b/lib/preimageReference.ts index 6435926a..d147259a 100644 --- a/lib/preimageReference.ts +++ b/lib/preimageReference.ts @@ -25,6 +25,18 @@ export const PET_REFERENCE_BLOB_URL = process.env.NEXT_PUBLIC_PET_REFERENCE_BLOB_URL ?? "https://github.com/open-web3-stack/polkadot-ecosystem-tests/blob/master/packages/shared/src/snowbridge/referencePreimages.json"; +// GitHub API: latest commit that touched PET's known-good block file. That file +// is committed by the 6-hourly `update-known-good` cron only when the whole +// Polkadot suite (our halt/resume test included) passes against fresh live +// blocks, so its commit date is a live "last verified against live chains" +// signal. Derived from CI rather than baked into the reference, so it's always +// current and never goes stale. (Proxy note: it reflects the whole Polkadot +// suite passing, not our test alone; if any Polkadot test fails the date simply +// stops advancing, which is itself a useful staleness signal.) +export const PET_KNOWN_GOOD_COMMITS_URL = + process.env.NEXT_PUBLIC_PET_KNOWN_GOOD_URL ?? + "https://api.github.com/repos/open-web3-stack/polkadot-ecosystem-tests/commits?path=KNOWN_GOOD_BLOCK_NUMBERS_POLKADOT.env&per_page=1"; + export type Operation = "halt" | "resume"; export interface ReferencePreimageEntry { @@ -35,9 +47,6 @@ export interface ReferencePreimageEntry { export interface ReferenceFile { description?: string; - generatedAt?: string; - assetHubRuntime?: { specName: string; specVersion: number }; - bridgeHubRuntime?: { specName: string; specVersion: number }; halt: ReferencePreimageEntry; resume: ReferencePreimageEntry; } @@ -60,7 +69,7 @@ export async function sdkProducesDeterministicPreimage(): Promise { } export type ReferenceVerdict = - | { kind: "match"; reference: ReferenceFile } + | { kind: "match"; reference: ReferenceFile; lastVerifiedAt: Date | null } | { kind: "mismatch"; reference: ReferenceFile; @@ -98,6 +107,38 @@ export async function fetchReference( return (await res.json()) as ReferenceFile; } +/** + * Pure extractor for the last-verified date from the GitHub commits API + * response. Exposed for unit testing. Returns null for an empty/malformed + * response or an unparseable date. + */ +export function parseLastVerified( + commits: Array<{ commit?: { committer?: { date?: string } } }>, +): Date | null { + const iso = commits?.[0]?.commit?.committer?.date; + if (!iso) return null; + const date = new Date(iso); + return Number.isNaN(date.getTime()) ? null : date; +} + +// Fetch the "last verified against live chains" timestamp. Never throws: any +// failure (network, rate limit, shape change) resolves to null so the badge +// simply omits the line rather than breaking the whole check. +export async function fetchLastVerifiedAt( + signal?: AbortSignal, +): Promise { + try { + const res = await fetch(PET_KNOWN_GOOD_COMMITS_URL, { + signal, + headers: { Accept: "application/vnd.github+json" }, + }); + if (!res.ok) return null; + return parseLastVerified(await res.json()); + } catch { + return null; + } +} + export async function verifyAgainstReference( operation: Operation, result: { hash: string; callData: string }, @@ -107,8 +148,14 @@ export async function verifyAgainstReference( return { kind: "unsupported" }; } let reference: ReferenceFile; + let lastVerifiedAt: Date | null = null; try { - reference = await fetchReference(signal); + // fetchLastVerifiedAt never throws, so this only rejects if the reference + // fetch itself fails. + [reference, lastVerifiedAt] = await Promise.all([ + fetchReference(signal), + fetchLastVerifiedAt(signal), + ]); } catch (e) { return { kind: "unavailable", @@ -117,7 +164,7 @@ export async function verifyAgainstReference( } const verdict = compareToReference(operation, result, reference); if (verdict === "match") { - return { kind: "match", reference }; + return { kind: "match", reference, lastVerifiedAt }; } const expected = operation === "halt" ? reference.halt : reference.resume; return { kind: "mismatch", reference, expected };