// Igneum reference apps, the browser driver for /light and /receipt (8 October 2026). Fetches data from the read // service behind rpc.devnet.igneum.network/light and verifies every byte in this tab with core.js. Libraries, pinned: // BLAKE2b and keccak from @noble/hashes 2.4.0, BLS12-381 from @noble/curves 2.4.0 (pure JavaScript, served by jsdelivr // as ES modules). Known-failed first: before the genuine result is shown, a copy of the same proof with one byte // altered is run through the same verifier and must read as refused. import { blake2b } from 'https://cdn.jsdelivr.net/npm/@noble/hashes@2.4.0/blake2.js/+esm'; import { keccak_256 } from 'https://cdn.jsdelivr.net/npm/@noble/hashes@2.4.0/sha3.js/+esm'; import { bls12_381 } from 'https://cdn.jsdelivr.net/npm/@noble/curves@2.4.0/bls12-381.js/+esm'; import { verifyBalance, verifyReceipt, formatIgn } from './core.js'; export const LIBRARIES = { '@noble/hashes': '2.4.0', '@noble/curves': '2.4.0' }; const deps = { blake2b, bls: bls12_381, keccak: keccak_256 }; const q = new URLSearchParams(location.search); export const API = (q.get('api') || 'https://rpc.devnet.igneum.network/light').replace(/\/$/, ''); const $ = s => document.querySelector(s); const esc = s => String(s).replace(/&/g, '&').replace(//g, '>'); const short = h => { const s = String(h).replace(/^0x/, ''); return s.slice(0, 8) + '…' + s.slice(-6); }; const clone = x => JSON.parse(JSON.stringify(x)); const flipHex = (s, at) => { const h = s.replace(/^0x/, ''); const i = Math.min(at, h.length - 1); const d = (parseInt(h[i], 16) ^ 1).toString(16); return (s.startsWith('0x') ? '0x' : '') + h.slice(0, i) + d + h.slice(i + 1); }; async function getJson(url) { const r = await fetch(url, { cache: 'no-store' }); let j = null; try { j = await r.json(); } catch { j = null; } if (!j) throw new Error(`${url.replace(API, '')} answered ${r.status} with no JSON`); if (!j.ok) throw new Error(j.error || `${url.replace(API, '')} answered ${r.status}`); return j; } function setStatus(text, kind = '') { const el = $('[data-status]'); if (!el) return; el.textContent = text; el.dataset.kind = kind; } function stepsHtml(result) { return '
    ' + result.steps.map(s => `
  1. ${s.ok ? 'checked' : 'refused'}${esc(s.name)}${esc(s.detail)}
  2. `).join('') + '
'; } function negativeHtml(name, result) { return `
${result.verified ? 'NOT REFUSED (this is a bug)' : 'refused'} ${esc(name)}${esc(result.reason || 'verified')}
`; } const ago = ms => { const s = Math.max(0, Math.round((Date.now() - Number(ms)) / 1000)); return s < 90 ? `${s} s ago` : s < 5400 ? `${Math.round(s / 60)} min ago` : `${(s / 3600).toFixed(1)} h ago`; }; // ---- /light ------------------------------------------------------------------------------------------------------- export async function runLight(address) { const out = $('[data-result]'); out.innerHTML = ''; setStatus('fetching the latest certified checkpoint…', 'busy'); const cp = await getJson(`${API}/checkpoint`); setStatus(`checkpoint ${cp.index} fetched; fetching the proof for ${short(address)}…`, 'busy'); const proof = await getJson(`${API}/balance?address=${address}&checkpoint=${cp.hash}&index=${cp.index}`); // the service may answer with an earlier certificate (the first checkpoint above the carrier: the smallest proof); verified like any other if (proof.checkpoint_certificate) { Object.assign(cp, proof.checkpoint_certificate); } setStatus('verifying in this tab…', 'busy'); await new Promise(r => setTimeout(r, 20)); // known-failed first: the same proof with one byte of the last account-proof node altered const bad = clone(proof); const last = bad.account.accountProof.length - 1; bad.account.accountProof[last] = flipHex(bad.account.accountProof[last], 30); const neg = verifyBalance(cp, bad, deps); const res = verifyBalance(cp, proof, deps); window.__igneumLight = { cp, proof, neg, res }; const top = res.verified ? `
Proven balance · Devnet 3, no value
${esc(formatIgn(res.balance_wei))} IGN

${esc(res.balance_wei)} wei at chain block ${res.block}, under checkpoint ${res.checkpoint} (locked ${esc(ago(Number(proof.headers[proof.headers.length - 1].timestamp)))}). Verified here in ${res.ms} ms, ${res.headers} headers checked.

` : `
Not verified · Devnet 3, no value

${esc(res.reason)}

`; out.innerHTML = top + negativeHtml('a copy of this proof with one byte of a trie node altered', neg) + stepsHtml(res) + `

Data served by ${esc(API)} (a read service in front of a Devnet 3 node). Nothing it answered was taken on trust: the certificate, every header hash and parent link, the coinbase inclusion, the segment record's signature and the account proof were recomputed in this tab. What is trusted: that the aggregator's statement is the true execution result (every node checks it natively before paying the record; the SP1 proof behind it is verified by nodes, not in this tab yet), and the voter list with weights, which came from the node (spec 10.1).

`; setStatus(res.verified ? 'verified' : 'refused', res.verified ? 'ok' : 'bad'); return res; } // ---- /receipt ----------------------------------------------------------------------------------------------------- export async function runReceipt(tx) { const out = $('[data-result]'); out.innerHTML = ''; setStatus('fetching the latest certified checkpoint…', 'busy'); const cp = await getJson(`${API}/checkpoint`); setStatus(`checkpoint ${cp.index} fetched; fetching the inclusion proof…`, 'busy'); const r = await getJson(`${API}/receipt?tx=${tx}&checkpoint=${cp.hash}&index=${cp.index}`); // the service may answer with an earlier certificate (the first checkpoint above the block: the smallest proof); it is verified like any other const receipt = { format: 'igneum-receipt-v1', issued: new Date().toISOString(), ...r, checkpoint: { ...r.checkpoint, certificate: r.checkpoint.certificate || cp } }; delete receipt.ok; delete receipt.now; setStatus('verifying in this tab…', 'busy'); await new Promise(resolve => setTimeout(resolve, 20)); const bad = clone(receipt); bad.raw_tx_hex = flipHex(bad.raw_tx_hex, 40); const neg = verifyReceipt(bad, deps); const res = verifyReceipt(receipt, deps); window.__igneumReceipt = { receipt, neg, res }; const t = res.tx || {}; const when = new Date(Number(res.block_time || 0)).toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; const top = res.verified ? `
Final · Devnet 3, no value
${esc(formatIgn(t.value || '0'))} IGN
To
${esc(t.to || 'contract creation')}
From
${esc(receipt.tx_as_reported.from)} (as the node reports it; the signature is the chain's check)
Transaction
0x${esc(receipt.tx_hash)}
Block
${esc(res.block)} at ${esc(when)}, DAA ${esc(res.block_daa)}
Finality
checkpoint ${res.checkpoint}, ${esc(res.certificate)}; ${res.headers} headers from the block to the checkpoint, verified here in ${res.ms} ms
${receipt.execution ? `
Execution
status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}, gas ${parseInt(receipt.execution.gas_used, 16)}, ${receipt.execution.logs} log(s) (as the node reports it, not proven here)
` : ''}
` : `
Not verified · Devnet 3, no value

${esc(res.reason)}

`; out.innerHTML = top + negativeHtml('a copy of this receipt with one nibble of the raw transaction altered', neg) + stepsHtml(res) + (res.verified ? `

The file carries the raw transaction, the including block's header and merkle path, every header up to the certified checkpoint, the certificate and the voter table. Anyone re-verifies it offline with the one-file verifier: node verify-receipt.js receipt.json (verify-receipt.js, plain JavaScript, no npm, no network). A tampered file fails there the same way the copy above failed here.

` : ''); const dl = $('[data-download]'); if (dl) dl.addEventListener('click', () => { const blob = new Blob([JSON.stringify(receipt, null, 1)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `igneum-receipt-${receipt.tx_hash.slice(0, 12)}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000); }); setStatus(res.verified ? 'verified' : 'refused', res.verified ? 'ok' : 'bad'); return res; } // ---- wiring --------------------------------------------------------------------------------------------------------- function wire() { const form = $('[data-form]'); if (!form) return; const input = form.querySelector('input'); const kind = form.dataset.form; if (q.get(kind === 'light' ? 'address' : 'tx')) input.value = q.get(kind === 'light' ? 'address' : 'tx'); form.addEventListener('submit', async e => { e.preventDefault(); const v = input.value.trim(); const ok = kind === 'light' ? /^0x[0-9a-fA-F]{40}$/.test(v) : /^0x[0-9a-fA-F]{64}$/.test(v); if (!ok) { setStatus(kind === 'light' ? 'an address is 0x and 40 hex digits' : 'a transaction hash is 0x and 64 hex digits', 'bad'); return; } form.querySelector('button').disabled = true; try { if (kind === 'light') await runLight(v); else await runReceipt(v); } catch (err) { setStatus(String(err.message || err), 'bad'); $('[data-result]').innerHTML = `
Could not fetch · Devnet 3, no value

${esc(String(err.message || err))}

`; } finally { form.querySelector('button').disabled = false; } }); if (input.value) form.requestSubmit(); } if (typeof document !== 'undefined') { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire); else wire(); }