igneum/site/verify/test.html
igneum-labs d4cd91f55f 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

43 lines
3.7 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Igneum light client test</title>
<meta name="robots" content="noindex">
<style>body{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:#0C0C0E;color:#E8E4DA;padding:24px;max-width:960px}h1{font-size:16px}td{padding:4px 10px;border-bottom:1px solid #222;vertical-align:top}.ok{color:#7ED957}.bad{color:#F2541B}</style>
</head>
<body>
<h1>Igneum light client, version zero: genuine and tampered</h1>
<p>Each row verifies in this tab with <code>verify/core.js</code>. Source: <code id="src"></code></p>
<table id="t"><thead><tr><td>case</td><td>verified</td><td>ms</td><td>reason</td><td>signers</td><td>active</td><td>total</td><td>headers</td></tr></thead><tbody></tbody></table>
<pre id="done"></pre>
<script type="module">
import { fetchCheckpoint, verify, LIBRARIES } from './verify.js';
const q = new URLSearchParams(location.search);
const url = q.get('api') || '/api/checkpoint' + (q.get('source') ? `?source=${q.get('source')}` : '');
const data = await fetchCheckpoint(url);
document.getElementById('src').textContent = `${url} (index ${data.index}, ${data.source}, ${data.chain_id}) libraries ${JSON.stringify(LIBRARIES)}`;
const clone = () => JSON.parse(JSON.stringify(data));
const cases = [];
cases.push(['genuine', clone()]);
cases.push(['genuine again (warm)', clone()]);
{ const d = clone(); const s = d.certificate.aggregate_signature_hex; const b = (parseInt(s.slice(20, 22), 16) ^ 1).toString(16).padStart(2, '0'); d.certificate.aggregate_signature_hex = s.slice(0, 20) + b + s.slice(22); cases.push(['signature, one bit flipped', d]); }
{ const d = clone(); const bm = parseInt(d.certificate.bitmap_hex.slice(0, 2), 16); let p = 0; while (!(bm & (1 << p))) p++; d.certificate.bitmap_hex = (bm & ~(1 << p)).toString(16).padStart(2, '0') + d.certificate.bitmap_hex.slice(2); cases.push([`one voter dropped from the bitmap (position ${p})`, d]); }
{ const d = clone(); const i = d.voters.findIndex((v, k) => d.certificate.bitmap_hex && (parseInt(d.certificate.bitmap_hex.slice(0, 2), 16) & (1 << k))); const pk = d.voters[i].pubkey_hex; d.voters[i].pubkey_hex = pk.slice(0, 10) + ((parseInt(pk.slice(10, 12), 16) ^ 4).toString(16).padStart(2, '0')) + pk.slice(12); cases.push(['a signer\'s public key altered', d]); }
{ const d = clone(); d.hash = d.hash.slice(0, 62) + ((parseInt(d.hash.slice(62), 16) ^ 1).toString(16).padStart(2, '0')); d.headers[d.headers.length - 1].hash = d.hash; cases.push(['checkpoint hash altered', d]); }
{ const d = clone(); const h = d.headers[Math.floor(d.headers.length / 2)]; h.nonce = String(BigInt(h.nonce) ^ 1n); cases.push(['a header nonce altered', d]); }
{ const d = clone(); d.headers.splice(Math.floor(d.headers.length / 2), 1); cases.push(['a header removed from the chain', d]); }
{ const d = clone(); const bm = parseInt(d.certificate.bitmap_hex.slice(0, 2), 16); const quiet = d.voters.filter((v, k) => !(bm & (1 << k))); if (quiet.length) { for (const v of quiet) v.weight = Math.round(v.weight * 10); cases.push(['a non-signer\'s weight raised 10x (floor)', d]); } else { for (const v of d.voters) v.weight = 0; cases.push(['every weight zeroed', d]); } }
const tb = document.querySelector('#t tbody');
const out = [];
for (const [name, d] of cases) {
const r = verify(d);
out.push({ name, ...r });
const tr = document.createElement('tr');
tr.innerHTML = `<td>${name}</td><td class="${r.verified ? 'ok' : 'bad'}">${r.verified}</td><td>${r.ms}</td><td>${r.reason || ''}</td><td>${r.signers ?? ''}</td><td>${r.weight_fraction_active ?? ''}</td><td>${r.weight_fraction_total ?? ''}</td><td>${r.headers_checked ?? ''}</td>`;
tb.appendChild(tr);
}
document.getElementById('done').textContent = 'RESULTS ' + JSON.stringify(out);
</script>
</body>
</html>