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>
43 lines
3.7 KiB
HTML
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>
|