igneum/site/verify/verify.js
igneum-labs 26121a138c Light client v0: the browser verifies the latest certified checkpoint
site/verify/core.js recomputes every header hash (keyed BLAKE2b, the
node's field order), checks the parent links from the previous locked
checkpoint, hashes each voter's G1 key to its vote_key_hash, verifies the
BLS aggregate over "igneum-vote-v1/" || chain_id || 0 || index_le64 ||
checkpoint under the vote tag with the bitmap's keys, and applies Q3
(2/3 of active, 17/30 of total). verify.js drives it from /api/checkpoint
with @noble/hashes 2.4.0 and @noble/curves 2.4.0 pinned from jsdelivr and
fills the homepage card; the badge says LIVE only after a pass in the tab.

site/api/checkpoint.mjs ships the data: certificate bytes, voter table
with public keys, header chain. tools/observer stores every certificate a
block carries (new table live_certificates, voter table read at the lock,
selected-chain headers back to the previous lock, one-off backfill of the
newest lock on start) and keeps header nonces exact; the FinalityLock
write no longer fails on a missing votes_seen.

Tested on the igneum-devnet-7 test network: checkpoint 95 verifies in
Chrome in 103 ms; a flipped signature bit, a dropped voter, an altered key,
an altered header and a removed header all fail with the reason named.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 22:21:12 +00:00

66 lines
3.5 KiB
JavaScript

// Igneum light client, version zero, browser driver. Fetches /api/checkpoint and verifies it in this tab with
// core.js, then fills the homepage card. Everything is computed here; the server only ships data.
// Libraries, pinned: BLAKE2b from @noble/hashes 2.4.0, BLS12-381 from @noble/curves 2.4.0 (pure JavaScript,
// served by jsdelivr as ES modules).
import { blake2b } from 'https://cdn.jsdelivr.net/npm/@noble/hashes@2.4.0/blake2.js/+esm';
import { bls12_381 } from 'https://cdn.jsdelivr.net/npm/@noble/curves@2.4.0/bls12-381.js/+esm';
import { verifyCheckpoint } from './core.js';
export const LIBRARIES = { '@noble/hashes': '2.4.0', '@noble/curves': '2.4.0' };
const deps = { blake2b, bls: bls12_381 };
export async function fetchCheckpoint(url = '/api/checkpoint') {
const r = await fetch(url, { cache: 'no-store' });
let body = null;
try { body = await r.json(); } catch { body = null; }
if (!body) return { ok: false, error: `api answered ${r.status}` };
return body;
}
export function verify(data) { return verifyCheckpoint(data, deps); }
// The homepage card. Cells carry data-lc="..." so the layout and classes stay as they are.
const $ = sel => document.querySelector(sel);
const pct = x => `${(x * 100).toFixed(1)}%`;
export function renderCard(result, data) {
const badge = $('[data-lc="badge"]'), badgeText = $('[data-lc="badge-text"]');
const cp = $('[data-lc="checkpoint"]'), proof = $('[data-lc="proof"]'), sys = $('[data-lc="system"]'), tab = $('[data-lc="tab"]');
const line = $('[data-lc="line"]');
if (!cp) return;
if (proof) proof.textContent = 'not yet';
if (sys) sys.textContent = 'BLS aggregate, version 1';
if (result.verified) {
const count = (window.__igneumVerified = (window.__igneumVerified || 0) + 1);
cp.textContent = `${count} verified in this tab`;
cp.title = `checkpoint ${result.index} on ${result.network}, block ${String(result.hash).slice(0, 12)}, ${result.headers_checked} headers checked`;
tab.textContent = `${result.ms} ms · ${pct(result.weight_fraction_total)} of weight`;
tab.title = `${result.signers} of ${result.voters} voters signed: ${pct(result.weight_fraction_active)} of active weight, ${pct(result.weight_fraction_total)} of total${result.weights_exact ? '' : ` (weights read at checkpoint ${result.weights_at_index})`}`;
tab.style.color = '';
if (badgeText) badgeText.textContent = data.source === 'test' ? 'LIVE · test network' : 'LIVE';
if (badge) badge.classList.add('on');
if (line) line.textContent = data.source === 'test'
? `Checkpoint ${result.index} of the test network, certified by ${result.signers} miners. Verified here.`
: `Checkpoint ${result.index}, certified by ${result.signers} miners. Verified here.`;
} else {
cp.textContent = 'could not verify';
cp.title = result.reason || '';
tab.textContent = result.reason ? result.reason : 'could not verify';
tab.style.color = 'var(--molten)';
if (badgeText) badgeText.textContent = 'PREVIEW';
if (badge) badge.classList.remove('on');
}
}
export async function run(url) {
const data = await fetchCheckpoint(url);
const result = verify(data);
window.__igneumLightClient = { result, data };
renderCard(result, data);
return result;
}
if (typeof document !== 'undefined' && document.querySelector('[data-lc="checkpoint"]')) {
const start = () => run().catch(e => renderCard({ verified: false, reason: String(e.message || e) }, {}));
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
}