Reference apps (8 Oct 2026): the light wallet (/light), the payment receipt (/receipt) and the Sepolia state oracle page (/oracle), each on Devnet 3; site/light/core.js verifies certificate, header path, coinbase merkle inclusion, segment record (aggregator BLS) and the MPT account proof in the browser; the read service tools/reference-apps/light-service (behind rpc.devnet.igneum.network/light); the one-file offline receipt verifier (site/receipt/verify-receipt.js); /api/checkpoint?source=dn3; the negative cases in verify.test.mjs and /light/test; nav routes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 10:26:55 +00:00
parent eaa0c22f2b
commit 32bfed13e6
50 changed files with 7912 additions and 36 deletions

View file

@ -88,6 +88,9 @@ main{flex:1}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -120,6 +123,9 @@ main{flex:1}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -136,6 +136,9 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -168,6 +171,9 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -6,6 +6,7 @@
//
// ?source=live the live devnet (default; falls back to the test network while the live chain has no lock yet)
// ?source=test tonight's finality test network (igneum-devnet-7)
// ?source=dn3 Devnet 3 (igneum-devnet-3, the dn3_ tables the Devnet 3 observer writes; the reference apps read it)
//
// Zero dependencies: Neon's HTTP SQL endpoint over Node's built-in fetch, like live.mjs.
@ -72,6 +73,16 @@ function shape(row, source) {
};
}
/** The latest certified checkpoint of `want` ('live', 'test' or 'dn3') through `sql`, shaped for the verifier, or null. */
export async function readCheckpoint(sql, want = 'live') {
let row = null, source = null;
if (want === 'dn3') { row = await latest(sql, 'dn3_live_certificates'); if (row) source = 'dn3'; }
if (!row && want !== 'test' && want !== 'dn3') { row = await latest(sql, 'live_certificates'); if (row) source = 'live'; }
if (!row && want !== 'dn3') { row = await latest(sql, 'fintest_live_certificates'); if (row) source = 'test'; }
return row ? shape(row, source) : null;
}
export { neon };
export default async function handler(req, res) {
res.setHeader('Cache-Control', 'public, max-age=5');
res.setHeader('Access-Control-Allow-Origin', '*');
@ -82,14 +93,12 @@ export default async function handler(req, res) {
try {
const sql = neon();
const want = String((req.query && req.query.source) || 'live');
let row = null, source = null;
if (want !== 'test') { row = await latest(sql, 'live_certificates'); if (row) source = 'live'; }
if (!row) { row = await latest(sql, 'fintest_live_certificates'); if (row) source = 'test'; }
if (!row) {
const cp = await readCheckpoint(sql, want);
if (!cp) {
res.setHeader('Cache-Control', 'no-store');
return res.status(404).json({ ok: false, error: 'no certified checkpoint stored yet' });
}
return res.status(200).json({ ok: true, now: new Date().toISOString(), ...shape(row, source) });
return res.status(200).json({ ok: true, now: new Date().toISOString(), ...cp });
} catch (e) {
res.setHeader('Cache-Control', 'no-store');
return res.status(500).json({ ok: false, error: String(e.message || e) });

View file

@ -110,6 +110,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -142,6 +145,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

File diff suppressed because one or more lines are too long

View file

@ -136,6 +136,9 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -168,6 +171,9 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -393,7 +393,9 @@ const PAGES = [['index.html', ''], ['download.html', 'download'], ['income.html'
// the DAG explorer (5 Oct 2026): /explorer, /block/<hash> and /address/<addr> (vercel.json rewrites the last two)
['explorer.html', 'live'], ['block.html', 'live'], ['address.html', 'live'],
// the scenes lab (6 Oct 2026): three animated views of the live feed, unlinked, noindex
['scenes.html', '']];
['scenes.html', ''],
// the reference apps (8 Oct 2026): the light wallet, the payment receipt, the Sepolia state oracle
['light.html', 'light'], ['receipt.html', 'receipt'], ['oracle.html', 'oracle']];
for (const [file, active] of PAGES) {
const p = join(here, file);
if (!existsSync(p)) throw new Error(`missing page ${file}`);

View file

@ -114,6 +114,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -146,6 +149,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -114,6 +114,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -146,6 +149,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -104,6 +104,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -136,6 +139,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
@ -235,16 +241,16 @@
<div><h3>Ember for Linux</h3><div class="sub">one tarball, the miner and the node inside</div></div>
<div class="body">
<p>Unpack it, run <code>igneum</code> with your payout address. The same signed manifest as the desktop apps. NVIDIA through CUDA, AMD through OpenCL.</p>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><span class="osmark bare" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v0.3.23 · 28.8 MB</span></a>
<div class="sha"><b>sha256</b> <span data-dl-sha="miner-hive">0d716ed92560b6b2eccb63ca1bd7a4d9eb64c9b334e8370e1263f0344cee294e</span></div>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><span class="osmark bare" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v0.3.22 · 28.8 MB</span></a>
<div class="sha"><b>sha256</b> <span data-dl-sha="miner-hive">8ad6dcef9edc57dcd33e8d5a97cbef5cdda0e384a6e56d3f62f8903029c4c764</span></div>
</div>
</article>
<article class="dl-card" id="hive">
<span class="osmark" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>
<div><h3>Ember on HiveOS</h3><div class="sub">for the rig people, the same tarball</div></div>
<div class="body">
<div class="hive-sheet"><b>Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-0.3.23.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x&lt;your 40-hex payout address&gt;.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn"><span class="osmark bare" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v0.3.23 · 28.8 MB</span></a>
<div class="hive-sheet"><b>Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-0.3.22.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x&lt;your 40-hex payout address&gt;.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn"><span class="osmark bare" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v0.3.22 · 28.8 MB</span></a>
</div>
</article>
</div>

View file

@ -125,6 +125,9 @@ td.mono{font-family:var(--f-mono);font-size:12.5px;min-width:180px}td.iv{color:v
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence" aria-current="page"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -157,6 +160,9 @@ td.mono{font-family:var(--f-mono);font-size:12.5px;min-width:180px}td.iv{color:v
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence" aria-current="page"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -135,6 +135,9 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -167,6 +170,9 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -117,6 +117,9 @@ dt{color:var(--ash)}dd{margin:0;font-family:var(--f-mono);font-size:14px;overflo
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -149,6 +152,9 @@ dt{color:var(--ash)}dd{margin:0;font-family:var(--f-mono);font-size:14px;overflo
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -74,6 +74,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -106,6 +109,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -106,6 +106,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -138,6 +141,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -114,6 +114,9 @@
<a href="/journey" data-nav="journey" aria-current="page"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -146,6 +149,9 @@
<a href="/journey" data-nav="journey" aria-current="page"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
@ -212,7 +218,7 @@
<section class="page-hero">
<div class="container">
<div class="breadcrumb"><a href="/">Igneum</a><span>/</span><span>The journey</span></div>
<div class="page-heading"><div><div class="eyebrow"><span class="line"></span>Six phases · updated 7 Oct 2026</div>
<div class="page-heading"><div><div class="eyebrow"><span class="line"></span>Six phases · updated 8 Oct 2026</div>
<h1>The journey.</h1>
<p class="lead">Six phases from the specification to a fair launch. Each closes at its gate, a measurement published whether it passes or fails, so there is no date to slip. Below the phases, the latest entries of the engineering log.</p></div></div>
</div>
@ -226,7 +232,8 @@
<div class="milestone next" id="phase-5"><div class="when">Armed: opens on the go word</div><div><span class="badge">Next</span><h3>5. Public testnet</h3><p>Three seed nodes and the public RPC are up; the one-click miner app on Windows, macOS and Linux, HiveOS, pools, the first rollup as a proving customer, no coin yet</p><p class="gate"><b>Gate:</b> 1,000 independent miners run 30 days and rollup proofs are delivered on time</p></div></div>
<div class="milestone next" id="phase-6"><div class="when">After the testnet has passed its gate (1,000 independent miners for 30 days, rollup proofs on time) and one proving customer has signed: a customer paying for proofs at a published rate, or a letter of intent with a volume</div><div><span class="badge">Next</span><h3>6. Mainnet fair launch</h3><p>Genesis with no premine, 30-day ramp. No listing is arranged, promised or sought by the project</p></div></div></div>
<div class="section-head" style="margin-top:100px"><div><div class="eyebrow"><span class="line"></span>The log, newest first</div><h2>Lately, in the&nbsp;log.</h2></div><div class="searchbar" style="min-width:min(100%,360px)"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/></svg><label for="log-q" class="visually-hidden">Filter the log</label><input id="log-q" data-filter-input="log" placeholder="Filter: a card, a date, a word" autocomplete="off" spellcheck="false"><span class="small" data-filter-count="log"></span></div></div>
<div class="record-count" data-filter-count="log"></div><div class="record-grid" data-filter-list="log"><article class="record"><div class="record-top"><time datetime="2026-10-07">7 Oct 2026</time><span class="badge">log</span></div><h3>The first 16 GB card</h3><p>The first 16 GB card: an RTX 5060 Ti in a Thunderbolt enclosure on the RTX 5090 Windows rig</p></article>
<div class="record-count" data-filter-count="log"></div><div class="record-grid" data-filter-list="log"><article class="record"><div class="record-top"><time datetime="2026-10-08">8 Oct 2026</time><span class="badge">log</span></div><h3>The hot-table packs on the RTX 5090</h3><p>The hot-table packs on the RTX 5090: ld.global.cs against the plain load, unlocked and at the 1,300 MHz lock</p></article>
<article class="record"><div class="record-top"><time datetime="2026-10-07">7 Oct 2026</time><span class="badge">log</span></div><h3>The first 16 GB card</h3><p>The first 16 GB card: an RTX 5060 Ti in a Thunderbolt enclosure on the RTX 5090 Windows rig</p></article>
<article class="record"><div class="record-top"><time datetime="2026-10-06">6 Oct 2026</time><span class="badge">log</span></div><h3>Counter ASIC 3.0</h3><p>Counter ASIC 3.0: the class v4 rehearsal</p></article>
<article class="record"><div class="record-top"><time datetime="2026-10-06">6 Oct 2026</time><span class="badge">log</span></div><h3>16:01Z: Ember run 6 on the three-card Windows rig</h3></article>
<article class="record"><div class="record-top"><time datetime="2026-10-06">6 Oct 2026</time><span class="badge">log</span></div><h3>Counter ASIC 3.0 item 2</h3><p>Counter ASIC 3.0 item 2: the per-day derivation</p></article>
@ -259,13 +266,12 @@
<article class="record"><div class="record-top"><time datetime="2026-10-05">5 Oct 2026</time><span class="badge">log</span></div><h3>Live devnet: real transactions, the first non-empty shard proven and…</h3><p>Live devnet: real transactions, the first non-empty shard proven and paid, and the exporter's block structure fixed</p></article>
<article class="record"><div class="record-top"><time datetime="2026-10-05">5 Oct 2026</time><span class="badge">log</span></div><h3>The program id split</h3><p>The program id split: why the Apple M5 Max rejected the RTX 5090 Windows rig's proofs, and the verifier at 114 s</p></article>
<article class="record"><div class="record-top"><time datetime="2026-10-05">5 Oct 2026</time><span class="badge">log</span></div><h3>Live devnet: the first shards proven, verified and paid</h3></article>
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>Devnet v4 live: generator v2, two-thirds floor, fresh chain</h3><p>Devnet v4 cut-over: generator v2, 2/3 floor, three nodes and a seed on a fresh chain</p></article>
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>One-click Windows workers</h3><p>One-click Windows workers: what the Apple M5 Max could measure</p></article>
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>First live hourly swap: no pause on Mac, NVIDIA or AMD</h3><p>First hourly program swap on the live devnet: compile-ahead, no pause, two cards</p></article>
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>Proving: devnet v4 shards on the Apple M5 Max CPU, loaded machine</h3></article>
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>First live finality lock: 77.4% of weight, 17 voters</h3><p>First finality lock on the live devnet: checkpoint 242 at 77.4% of all weight, two hours after genesis</p></article>
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>The gfx1036 worker fault and what the Apple M5 Max could and could…</h3><p>The gfx1036 worker fault and what the Apple M5 Max could and could not reproduce</p></article>
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>A node 60 s behind the clock is silently dead</h3></article>
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>First machine on the one-click app: a 5090 at 118 MH/s</h3><p>First machine on the Igneum Miner app: the RTX 5090 Windows rig's RTX 5090 at 118 MH/s, via Setup.exe</p></article></div>
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>The gfx1036 worker fault and what the Apple M5 Max could and could…</h3><p>The gfx1036 worker fault and what the Apple M5 Max could and could not reproduce</p></article></div>
<p class="empty-state" data-filter-empty="log" hidden>Nothing in the log matches that.</p>
<p class="note">Every entry is a dated heading of <a href="/bench">the engineering log</a>, where the commands and the hardware are. The phases and their gates are the litepaper’s <a href="/litepaper#roadmap">roadmap</a>.</p>
<div class="onward"><a href="/bench" class="btn">The engineering log</a><a href="/evidence" class="btn">Every claim and its status</a><a href="/live" class="btn">The live devnet</a></div>

View file

@ -1,5 +1,5 @@
{
"updated": "2026-10-07",
"updated": "2026-10-08",
"stage": "phase-3",
"phases": [
{
@ -50,6 +50,11 @@
}
],
"log": [
{
"date": "2026-10-08",
"text": "The hot-table packs on the RTX 5090: ld.global.cs against the plain load, unlocked and at the 1,300 MHz lock",
"short": "The hot-table packs on the RTX 5090"
},
{
"date": "2026-10-07",
"text": "The first 16 GB card: an RTX 5060 Ti in a Thunderbolt enclosure on the RTX 5090 Windows rig",
@ -215,6 +220,11 @@
"text": "Live devnet: the first shards proven, verified and paid",
"short": "Live devnet: the first shards proven, verified and paid"
},
{
"date": "2026-10-04",
"text": "Devnet v4 cut-over: generator v2, 2/3 floor, three nodes and a seed on a fresh chain",
"short": "Devnet v4 live: generator v2, two-thirds floor, fresh chain"
},
{
"date": "2026-10-04",
"text": "One-click Windows workers: what the Apple M5 Max could measure",
@ -239,16 +249,6 @@
"date": "2026-10-04",
"text": "The gfx1036 worker fault and what the Apple M5 Max could and could not reproduce",
"short": "The gfx1036 worker fault and what the Apple M5 Max could and could…"
},
{
"date": "2026-10-04",
"text": "A node 60 s behind the clock is silently dead",
"short": "A node 60 s behind the clock is silently dead"
},
{
"date": "2026-10-04",
"text": "First machine on the Igneum Miner app: the RTX 5090 Windows rig's RTX 5090 at 118 MH/s, via Setup.exe",
"short": "First machine on the one-click app: a 5090 at 118 MH/s"
}
]
}

View file

@ -89,7 +89,7 @@ blockquote{margin:10px 0;padding:10px 14px;border-left:3px solid var(--line-2);c
<div class="nav-groups" id="nav-groups" role="list">
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-mine" data-group="mine" aria-expanded="false" aria-controls="nav-panel-mine" aria-haspopup="true">Mine<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-network" data-group="network" aria-expanded="false" aria-controls="nav-panel-network" aria-haspopup="true">Network<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-learn" data-group="learn" aria-expanded="false" aria-controls="nav-panel-learn" aria-haspopup="true">Learn<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-learn" data-group="learn" data-active aria-expanded="false" aria-controls="nav-panel-learn" aria-haspopup="true">Learn<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
</div>
<div class="nav-controls">
<button class="btn icon-btn" type="button" data-theme-toggle aria-label="Switch to light theme"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg></button>
@ -118,6 +118,9 @@ blockquote{margin:10px 0;padding:10px 14px;border-left:3px solid var(--line-2);c
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -150,11 +153,14 @@ blockquote{margin:10px 0;padding:10px 14px;border-left:3px solid var(--line-2);c
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/ledger" data-nav="ledger" aria-current="page"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
@ -1426,7 +1432,7 @@ blockquote{margin:10px 0;padding:10px 14px;border-left:3px solid var(--line-2);c
<a href="https://discord.gg/igneum" target="_blank" rel="noopener" aria-label="The Igneum Discord"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19.6 5.3A17 17 0 0 0 15.4 4l-.2.4a15.6 15.6 0 0 1 3.9 1.9 13.6 13.6 0 0 0-14.2 0A15.6 15.6 0 0 1 8.8 4.4L8.6 4a17 17 0 0 0-4.2 1.3C1.8 9.2 1.1 13 1.4 16.7a17.1 17.1 0 0 0 5.2 2.6l1.1-1.8a10.8 10.8 0 0 1-1.7-.8l.4-.3a12.2 12.2 0 0 0 11.2 0l.4.3-1.7.8 1.1 1.8a17 17 0 0 0 5.2-2.6c.4-4.3-.7-8-3-11.4zM8.7 14.4c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1zm6.6 0c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1z"></path></svg></a>
<a data-social="reddit" href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener" aria-label="Igneum on Reddit"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M22 12.1a2.2 2.2 0 0 0-3.7-1.6 10.8 10.8 0 0 0-5.8-1.8l1-4.6 3.2.7a1.5 1.5 0 1 0 .2-.9l-3.6-.8a.5.5 0 0 0-.5.4l-1.1 5.2a10.8 10.8 0 0 0-5.9 1.8A2.2 2.2 0 1 0 3.4 14a4.3 4.3 0 0 0 0 .7c0 3.4 3.9 6.1 8.6 6.1s8.6-2.7 8.6-6.1a4.3 4.3 0 0 0 0-.7 2.2 2.2 0 0 0 1.4-1.9zM7 13.6a1.5 1.5 0 1 1 1.5 1.5A1.5 1.5 0 0 1 7 13.6zm8.6 4.1a5.7 5.7 0 0 1-3.6 1.1 5.7 5.7 0 0 1-3.6-1.1.4.4 0 0 1 .6-.6 4.9 4.9 0 0 0 3 .9 4.9 4.9 0 0 0 3-.9.4.4 0 0 1 .6.6zm-.3-2.6a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5z"></path></svg></a>
</div>
<div class="footer-dl" aria-label="Download Ember"><a href="/download#windows"><span data-os="windows" class="mini"></span>Windows</a><a href="/download#mac"><span data-os="mac" class="mini"></span>macOS</a><a href="/download#linux"><span data-os="linux" class="mini"></span>Linux</a><a href="/download#hive"><span data-os="hive" class="mini"></span>HiveOS</a></div>
<div class="footer-dl" aria-label="Download Ember"><a href="/download#windows"><span class="osmark mini" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Windows</a><a href="/download#mac"><span class="osmark mini" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>macOS</a><a href="/download#linux"><span class="osmark mini" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Linux</a><a href="/download#hive"><span class="osmark mini" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>HiveOS</a></div>
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" class="text-link">Specification and test vectors<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 18 18 6M6 6h12v12"/></svg></a>
</div>
<div class="footer-column"><h4>Run</h4><div>

344
site/light.html Normal file
View file

@ -0,0 +1,344 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Igneum light wallet</title>
<meta name="description" content="A Devnet 3 balance your browser proves itself: certificate, headers, coinbase, segment record and account proof, all recomputed in the tab. Devnet 3, no value.">
<link rel="canonical" href="https://igneum.network/light">
<meta name="theme-color" content="#0C0C0E">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Igneum">
<meta property="og:title" content="Igneum light wallet">
<meta property="og:description" content="A Devnet 3 balance your browser proves itself: certificate, headers, coinbase, segment record and account proof, all recomputed in the tab. Devnet 3, no value.">
<meta property="og:url" content="https://igneum.network/light">
<meta property="og:image" content="https://igneum.network/og.png?v=3">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Igneum light wallet">
<meta name="twitter:description" content="A Devnet 3 balance your browser proves itself: certificate, headers, coinbase, segment record and account proof, all recomputed in the tab. Devnet 3, no value.">
<meta name="twitter:image" content="https://igneum.network/og.png?v=3">
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192">
<link rel="icon" href="/icon-512.png" type="image/png" sizes="512x512">
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<link rel="manifest" href="/site.webmanifest">
<!-- head:start -->
<link rel="preload" href="/fonts/unbounded-900.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/plex-sans-400.woff2" as="font" type="font/woff2" crossorigin>
<meta name="color-scheme" content="dark light">
<link rel="stylesheet" href="/site.css">
<style>
/* Shared by every page: the head partial, injected by the build between the head markers. Edit the partial, not the page. */
/* Fonts, self-hosted (latin subsets, OFL): Unbounded 500/700/900, IBM Plex Sans 400/500/600, IBM Plex Mono 400/500. Fallbacks carry size and ascent overrides so the swap does not move the layout. The tokens, type and components are in /site.css (docs/plans/site-ui-3.md). */
@font-face{font-family:'Unbounded';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/unbounded-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/unbounded-700.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(/fonts/unbounded-900.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-sans-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-sans-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-sans-600.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-mono-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded Fallback';src:local('Arial Black'),local('Arial-Black'),local('Impact');size-adjust:114%;ascent-override:87.5%;descent-override:21.5%;line-gap-override:0%}
@font-face{font-family:'Plex Sans Fallback';src:local('Arial'),local('Helvetica Neue'),local('Helvetica');size-adjust:100.5%;ascent-override:102%;descent-override:27.4%;line-gap-override:0%}
@font-face{font-family:'Plex Mono Fallback';src:local('Courier New'),local('Menlo');size-adjust:100%;ascent-override:102.5%;descent-override:27.5%;line-gap-override:0%}
</style>
<script>
/* theme before first paint: ?theme=light|dark for one view (the capture tool), else the stored choice under igneum-theme, else the system setting; ?motion=off marks the page data-motion=off so every .reveal is drawn at once (a headless render never scrolls) */
(function(){var t=null;try{t=new URLSearchParams(location.search).get("theme");}catch(e){}if(t!=="light"&&t!=="dark"){try{t=localStorage.getItem("igneum-theme");}catch(e){t=null;}}if(t!=="light"&&t!=="dark")t=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";document.documentElement.setAttribute("data-theme",t);try{if(new URLSearchParams(location.search).get("motion")==="off")document.documentElement.setAttribute("data-motion","off");}catch(e){}})();
</script>
<!-- head:end -->
<style>
/* reference apps (8 Oct 2026): the verify form, the proven headline, the step list; tokens and components are in /site.css */
.vform{display:grid;gap:10px;margin:0}
.vform label{font:500 var(--t-sm)/1.4 var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ash)}
.vform .row{display:flex;gap:10px;flex-wrap:wrap}.vform input{flex:1 1 320px;font-family:var(--f-mono);font-size:14px}
.vstatus{font:400 13px/1.5 var(--f-mono);color:var(--ash);min-height:20px}.vstatus[data-kind="ok"]{color:var(--ember-text)}.vstatus[data-kind="bad"]{color:var(--molten-text)}
.headline{background:var(--graphite);border:1px solid var(--line);border-radius:var(--r-card);padding:var(--s-5);margin:var(--s-5) 0 var(--s-4)}
.headline.bad{border-color:var(--molten)}
.headline .big{font:900 clamp(32px,6vw,56px)/1.05 var(--f-display);color:var(--bone);margin:10px 0 8px;overflow-wrap:anywhere}.headline .big span{font-size:.45em;color:var(--ash);font-weight:700}
.headline .kv{margin-top:var(--s-3)}.headline .kv small{color:var(--ash);display:block}.headline .mono{font-family:var(--f-mono);font-size:13px;overflow-wrap:anywhere}
.neg{border-left:2px solid var(--ember);background:var(--row);border-radius:0 var(--r-row) var(--r-row) 0;padding:12px var(--s-4);margin:0 0 var(--s-3);font-size:14px;color:var(--ink-2)}
.neg b{font:500 11px/1 var(--f-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ember-text);margin-right:8px}.neg.wrong{border-color:var(--molten)}.neg.wrong b{color:var(--molten-text)}
.neg small{display:block;color:var(--ash);font-family:var(--f-mono);font-size:12px;margin-top:4px;overflow-wrap:anywhere}
.steps{list-style:none;margin:0;padding:0;display:grid;gap:8px}
.steps li{display:grid;grid-template-columns:84px 1fr;gap:4px 12px;background:var(--row);border:1px solid var(--line);border-radius:var(--r-row);padding:10px 14px;font-size:14px;color:var(--ink-2)}
.steps li b{font:500 11px/1.6 var(--f-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ember-text)}.steps li.bad b{color:var(--molten-text)}
.steps li small{grid-column:2;color:var(--ash);font-family:var(--f-mono);font-size:12px;overflow-wrap:anywhere}
.why{border:1px solid var(--line-2);border-radius:var(--r-card);padding:var(--s-4) var(--s-5);background:var(--obsidian);margin:var(--s-5) 0}
.why p{margin:0;font-size:15px;color:var(--ink-2)}.why p+p{margin-top:8px}
.meta{display:grid;gap:6px;font:400 13px/1.6 var(--f-mono);color:var(--ash);margin:var(--s-4) 0 0}.meta code{overflow-wrap:anywhere}
@media (max-width:640px){.steps li{grid-template-columns:1fr}.steps li small{grid-column:1}}
</style>
</head>
<body>
<!-- nav:start -->
<a class="skip" href="#main">Skip to content</a>
<nav class="nav" aria-label="Main" data-nav-version="5">
<div class="container nav-row">
<a href="/" class="logo" aria-label="Igneum home">
<svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg>
<span class="word">IGNEUM</span>
</a>
<span class="devnet" id="nav-devnet" title="igneum-devnet-3, chain id 4463. Coins have no value. The chain may reset."><span class="dot off" id="foot-dot"></span><span id="foot-state">devnet</span></span>
<div class="nav-groups" id="nav-groups" role="list">
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-mine" data-group="mine" aria-expanded="false" aria-controls="nav-panel-mine" aria-haspopup="true">Mine<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-network" data-group="network" data-active aria-expanded="false" aria-controls="nav-panel-network" aria-haspopup="true">Network<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-learn" data-group="learn" aria-expanded="false" aria-controls="nav-panel-learn" aria-haspopup="true">Learn<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
</div>
<div class="nav-controls">
<button class="btn icon-btn" type="button" data-theme-toggle aria-label="Switch to light theme"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg></button>
<a href="/download" class="btn primary small cta" data-nav="download">Download</a>
<button class="burger" id="nav-burger" type="button" aria-expanded="false" aria-controls="nav-sheet" aria-label="Open the menu" aria-haspopup="true"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path class="bars" d="M4 6h16M4 12h16M4 18h16"/><path class="cross" d="m6 6 12 12M6 18 18 6"/></svg></button>
</div>
</div>
<div class="panels" id="nav-panels">
<div class="panel-wrap container">
<div class="nav-panel" id="nav-panel-mine" data-group="mine" role="region" aria-labelledby="nav-btn-mine" hidden>
<div class="panel-list">
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-network" data-group="network" role="region" aria-labelledby="nav-btn-network" hidden>
<div class="panel-list">
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light" aria-current="page"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
<div class="panel-list">
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
</div>
</div>
</div>
</div>
<div class="nav-sheet" id="nav-sheet" role="dialog" aria-modal="true" aria-label="Menu" hidden>
<div class="sheet-scroll">
<div class="sheet-group"><div class="sheet-head">Mine</div>
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light" aria-current="page"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
</div>
<div class="sheet-foot">
<a href="/download" class="btn primary big" data-nav="download">Download</a>
<div class="sheet-social"><a href="https://discord.gg/igneum" target="_blank" rel="noopener">Discord</a><a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener">Source</a><a href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener">Reddit</a></div>
</div>
</div>
</div>
</nav>
<script>
(function(){
// the slim bar (site-ui-5, 7 Oct 2026): three group buttons open one panel each; the burger opens the sheet on phones.
// Keyboard: Enter or Space opens a group, Escape closes it and returns focus to its button, arrows move between the
// group buttons, Tab walks the open panel's links, a click outside closes. The theme toggle flips data-theme and stores it.
var n=document.querySelector('.nav');if(!n)return;
var btns=[].slice.call(n.querySelectorAll('.group-btn')),panels=[].slice.call(n.querySelectorAll('.nav-panel')),wrap=n.querySelector('.panels');
var burger=document.getElementById('nav-burger'),sheet=document.getElementById('nav-sheet');
function panelOf(g){return panels.filter(function(p){return p.getAttribute('data-group')===g;})[0];}
function closeAll(focusBack){var was=null;btns.forEach(function(b){if(b.getAttribute('aria-expanded')==='true')was=b;b.setAttribute('aria-expanded','false');});panels.forEach(function(p){p.hidden=true;});n.classList.remove('panel-open');if(focusBack&&was)was.focus();}
function open(g){closeAll(false);var b=btns.filter(function(x){return x.getAttribute('data-group')===g;})[0],p=panelOf(g);if(!b||!p)return;b.setAttribute('aria-expanded','true');p.hidden=false;n.classList.add('panel-open');anchor(b,p);}
// the panel sits under its button; a panel that would run past the container's right edge aligns its right edge to the button's right edge instead (never past the viewport)
function anchor(b,p){var pw=n.querySelector('.panel-wrap');if(!pw)return;var cr=pw.getBoundingClientRect(),br=b.getBoundingClientRect(),w=p.offsetWidth,left=br.left-cr.left;if(left+w>cr.width)left=(br.right-cr.left)-w;if(left<0)left=0;p.style.setProperty('--panel-left',Math.round(left)+'px');}
window.addEventListener('resize',function(){var o=btns.filter(function(x){return x.getAttribute('aria-expanded')==='true';})[0];if(o)anchor(o,panelOf(o.getAttribute('data-group')));}); btns.forEach(function(b,i){
b.addEventListener('click',function(){var on=b.getAttribute('aria-expanded')==='true';if(on)closeAll(false);else open(b.getAttribute('data-group'));});
b.addEventListener('keydown',function(e){
var k=e.key;
if(k==='ArrowRight'||k==='ArrowLeft'){e.preventDefault();var j=(i+(k==='ArrowRight'?1:btns.length-1))%btns.length;btns[j].focus();if(n.classList.contains('panel-open'))open(btns[j].getAttribute('data-group'));}
else if(k==='ArrowDown'){e.preventDefault();open(b.getAttribute('data-group'));var f=panelOf(b.getAttribute('data-group')).querySelector('a');if(f)f.focus();}
else if(k==='Escape'){closeAll(true);}
});
});
if(wrap)wrap.addEventListener('keydown',function(e){if(e.key==='Escape'){e.stopPropagation();closeAll(true);}});
document.addEventListener('click',function(e){if(!n.classList.contains('panel-open'))return;if(e.target.closest('.group-btn')||e.target.closest('.nav-panel'))return;closeAll(false);});
document.addEventListener('focusin',function(e){if(!n.classList.contains('panel-open'))return;if(n.contains(e.target))return;closeAll(false);});
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&n.classList.contains('panel-open'))closeAll(true);});
// the sheet on phones
function setSheet(o){if(!burger||!sheet)return;n.classList.toggle('open',o);sheet.hidden=!o;burger.setAttribute('aria-expanded',o?'true':'false');burger.setAttribute('aria-label',o?'Close the menu':'Open the menu');document.documentElement.classList.toggle('nav-locked',o);if(o){var f=sheet.querySelector('a');if(f)f.focus();}}
if(burger&&sheet){
burger.addEventListener('click',function(){setSheet(!n.classList.contains('open'));});
sheet.addEventListener('click',function(e){if(e.target.closest('a'))setSheet(false);});
sheet.addEventListener('keydown',function(e){
if(e.key==='Escape'){setSheet(false);burger.focus();return;}
if(e.key!=='Tab')return;var f=sheet.querySelectorAll('a,button');if(!f.length)return;var first=f[0],last=f[f.length-1];
if(e.shiftKey&&document.activeElement===first){e.preventDefault();burger.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();burger.focus();}
});
burger.addEventListener('keydown',function(e){if(e.key==='Tab'&&!e.shiftKey&&n.classList.contains('open')){e.preventDefault();var f=sheet.querySelector('a');if(f)f.focus();}});
var mq=window.matchMedia('(min-width:861px)');if(mq.addEventListener)mq.addEventListener('change',function(){setSheet(false);});
}
// the theme toggle
var t=n.querySelector('[data-theme-toggle]');
function paintToggle(){var dark=document.documentElement.getAttribute('data-theme')!=='light';t.setAttribute('aria-label',dark?'Switch to light theme':'Switch to dark theme');t.innerHTML=dark?'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg>':'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.7 13.3A8.5 8.5 0 0 1 10.7 3.3 8.5 8.5 0 1 0 20.7 13.3Z"/></svg>';}
if(t){t.addEventListener('click',function(){var v=document.documentElement.getAttribute('data-theme')==='light'?'dark':'light';document.documentElement.setAttribute('data-theme',v);try{localStorage.setItem('igneum-theme',v);}catch(e){}document.dispatchEvent(new CustomEvent('igneum-theme'));});paintToggle();document.addEventListener('igneum-theme',paintToggle);}
})();
</script>
<!-- nav:end -->
<main id="main" class="wrap">
<div class="head">
<div class="breadcrumb"><a href="/">Igneum</a><span>/</span><span>Light wallet</span></div>
<div class="eyebrow"><span class="line"></span>Reference app 1 · Devnet 3, no value</div>
<h1>A balance your browser proves<span class="accent">.</span></h1>
</div>
<p class="note">Paste a Devnet 3 address. This tab fetches the latest finality certificate, the header chain, the carrier block's coinbase, the segment record and an account proof from a read service, then checks every byte itself: BLS over the checkpoint, every header hash and parent link, the merkle path, the aggregator's signature, the Merkle Patricia proof under the proven state root. The service cannot lie about the balance. It can only refuse to answer.</p>
<div class="why">
<p><b>Why this only works on a proven chain.</b> The balance rests on a state root that an aggregator signed inside a block, after proving the segment's execution, and that every node checks against its own execution before paying the record. On a chain without proofs a light client has only a node's word for a balance.</p>
<p>Source: <code>site/light/core.js</code> (the checks), <code>site/light/app.js</code> (this page), <code>tools/reference-apps/light-service/serve.mjs</code> (the read service), fork branch <code>light-apps-node</code> (<code>eth_getProof</code> on the node). Test with the negative cases: <a href="/light/test">/light/test</a> in the browser, <code>tools/reference-apps/light-service/verify.test.mjs</code> under Node.</p>
</div>
<section class="card" aria-labelledby="verify">
<h2 id="verify">Verify a balance</h2>
<form class="vform" data-form="light" novalidate>
<label for="address">Devnet 3 address</label>
<div class="row"><input id="address" name="address" inputmode="text" autocomplete="off" spellcheck="false" placeholder="0x…" pattern="^0x[0-9a-fA-F]{40}$"><button class="btn primary" type="submit">Verify in this tab</button></div>
<div class="vstatus" data-status role="status" aria-live="polite">Devnet 3, no value. Nothing here is a wallet that sends. It reads and proves.</div>
</form>
<div data-result></div>
</section>
<h2 id="checked">What is checked, in order</h2>
<ol>
<li>The certificate: the aggregate BLS signature of the signers over <code>igneum-vote-v1/igneum-devnet-3 || index || checkpoint hash</code>, the canonical voter order, the rule (two thirds of active weight, 17/30 of total), and the header chain from the previous lock.</li>
<li>The header path from the carrier block up to the certified checkpoint: every header hash recomputes (BLAKE2b-256 keyed BlockHash over the header fields), every next header names the previous as a direct parent.</li>
<li>The coinbase transaction is in the carrier block: its hash recomputes (BLAKE2b-256 keyed TransactionHash over the serialized transaction) and the merkle path reaches the header's <code>hash_merkle_root</code>.</li>
<li>The segment record in the coinbase extra data: the aggregator's BLS signature over the record, the aggregator is a voter with weight, the statement names the chain and the block whose state it commits.</li>
<li>The account proof: a keccak-keyed Merkle Patricia proof under the record's <code>post_root</code>, every node hashed on the way down, the balance read from the leaf.</li>
</ol>
<p class="note">What is trusted and said so on the result: the aggregator's statement being the true execution result (nodes check it natively before paying; the SP1 proof behind it is verified by nodes, not in this tab yet), and the voter table with weights, which comes from the node (spec 10.1). The read service names the node it reads.</p>
<p class="asof">Devnet 3, no value. The chain may reset. Pages like this one are demonstrations of the verification path, not a product.</p>
</main>
<script type="module" src="/light/app.js"></script>
<!-- footer:start -->
<footer class="site-footer">
<div class="container">
<div class="footer-grid">
<div class="footer-brand">
<a href="/" class="logo" aria-label="Igneum home"><svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg><span class="word">IGNEUM</span><span class="stop">.</span></a>
<p>Mined by GPUs. Proven by fire.</p>
<div class="social" aria-label="Igneum elsewhere">
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" aria-label="Igneum source: the spec, the vectors and the issues"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M8.6 17.6 3 12l5.6-5.6 1.4 1.4L5.8 12l4.2 4.2zm6.8 0-1.4-1.4L18.2 12 14 7.8l1.4-1.4L21 12z"></path></svg></a>
<a href="https://discord.gg/igneum" target="_blank" rel="noopener" aria-label="The Igneum Discord"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19.6 5.3A17 17 0 0 0 15.4 4l-.2.4a15.6 15.6 0 0 1 3.9 1.9 13.6 13.6 0 0 0-14.2 0A15.6 15.6 0 0 1 8.8 4.4L8.6 4a17 17 0 0 0-4.2 1.3C1.8 9.2 1.1 13 1.4 16.7a17.1 17.1 0 0 0 5.2 2.6l1.1-1.8a10.8 10.8 0 0 1-1.7-.8l.4-.3a12.2 12.2 0 0 0 11.2 0l.4.3-1.7.8 1.1 1.8a17 17 0 0 0 5.2-2.6c.4-4.3-.7-8-3-11.4zM8.7 14.4c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1zm6.6 0c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1z"></path></svg></a>
<a data-social="reddit" href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener" aria-label="Igneum on Reddit"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M22 12.1a2.2 2.2 0 0 0-3.7-1.6 10.8 10.8 0 0 0-5.8-1.8l1-4.6 3.2.7a1.5 1.5 0 1 0 .2-.9l-3.6-.8a.5.5 0 0 0-.5.4l-1.1 5.2a10.8 10.8 0 0 0-5.9 1.8A2.2 2.2 0 1 0 3.4 14a4.3 4.3 0 0 0 0 .7c0 3.4 3.9 6.1 8.6 6.1s8.6-2.7 8.6-6.1a4.3 4.3 0 0 0 0-.7 2.2 2.2 0 0 0 1.4-1.9zM7 13.6a1.5 1.5 0 1 1 1.5 1.5A1.5 1.5 0 0 1 7 13.6zm8.6 4.1a5.7 5.7 0 0 1-3.6 1.1 5.7 5.7 0 0 1-3.6-1.1.4.4 0 0 1 .6-.6 4.9 4.9 0 0 0 3 .9 4.9 4.9 0 0 0 3-.9.4.4 0 0 1 .6.6zm-.3-2.6a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5z"></path></svg></a>
</div>
<div class="footer-dl" aria-label="Download Ember"><a href="/download#windows"><span class="osmark mini" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Windows</a><a href="/download#mac"><span class="osmark mini" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>macOS</a><a href="/download#linux"><span class="osmark mini" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Linux</a><a href="/download#hive"><span class="osmark mini" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>HiveOS</a></div>
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" class="text-link">Specification and test vectors<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 18 18 6M6 6h12v12"/></svg></a>
</div>
<div class="footer-column"><h4>Run</h4><div>
<a href="/download">Download Ember</a>
<a href="/miner">The miner</a>
<a href="/app">The app</a>
<a href="/wallet">The wallet</a>
<a href="/miners">GPU bench table</a>
<a href="/dev-fee">The dev fee</a>
<a href="/faucet">Testnet faucet</a>
<a href="/metamask">Add Igneum to MetaMask</a>
</div></div>
<div class="footer-column"><h4>Read</h4><div>
<a href="/litepaper">Litepaper</a>
<a href="/evidence">Evidence</a>
<a href="/bench">Engineering log</a>
<a href="/ledger">Ledger: every criticism</a>
<a href="/claims">What Igneum does not claim</a>
<a href="/randomx">Igneum vs RandomX</a>
<a href="/provenance">Built on the shoulders</a>
</div></div>
<div class="footer-column"><h4>Explore</h4><div>
<a href="/live">Live devnet</a>
<a href="/explorer">Explorer</a>
<a href="/journey">Journey</a>
<a href="/download">Downloads: devnet build</a>
<a href="https://discord.gg/igneum" target="_blank" rel="noopener">Community Discord</a>
<a href="mailto:hello@igneum.network">hello@igneum.network</a>
<a href="https://git.igneum.network/igneum-network/spec/issues" rel="noopener">An issue on the spec</a>
</div></div>
</div>
<div class="footer-bottom">
<p>Igneum Labs LTD · Unit IH-00-01-01-OF-01, Level 01, Innovation One, Dubai International Financial Centre. Experimental software. Nothing on this site is an offer to sell anything.<br>Report a flaw by email or by an issue on the spec. Nobody from Igneum will ask for your seed. © 2026 Igneum · not an offer to sell anything</p>
<div class="theme-group" role="group" aria-label="Theme"><button type="button" data-theme-set="system" aria-pressed="true">System</button><button type="button" data-theme-set="light" aria-pressed="false">Light</button><button type="button" data-theme-set="dark" aria-pressed="false">Dark</button></div>
</div>
</div>
</footer>
<script>
(function(){
// theme control: stores light or dark under igneum-theme; system clears it and follows the OS. The head applied the value before paint.
var bs=document.querySelectorAll('[data-theme-set]');if(!bs.length)return;
var media=window.matchMedia('(prefers-color-scheme: light)');
function stored(){try{var t=localStorage.getItem('igneum-theme');return t==='light'||t==='dark'?t:'system';}catch(e){return 'system';}}
function apply(){var c=stored();document.documentElement.setAttribute('data-theme',c==='system'?(media.matches?'light':'dark'):c);bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});document.dispatchEvent(new CustomEvent('igneum-theme'));}
bs.forEach(function(b){b.addEventListener('click',function(){var v=b.getAttribute('data-theme-set');try{if(v==='system')localStorage.removeItem('igneum-theme');else localStorage.setItem('igneum-theme',v);}catch(e){}apply();});});
if(media.addEventListener)media.addEventListener('change',function(){if(stored()==='system')apply();});
document.addEventListener('igneum-theme',function(){var c=stored();bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});});
bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===stored()?'true':'false');});
// the network state word in the status bar: one read of the observer per page view
var fs=document.getElementById('foot-state'),fd=document.getElementById('foot-dot');if(fs&&fd){fetch('/api/live?window=30',{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){var on=d&&d.ok&&d.state&&!d.state.stale;fs.textContent=on?'devnet live':'devnet, observer stale';fd.className='dot '+(on?'live':'off');}).catch(function(){fs.textContent='devnet';});}
})();
</script>
<!-- footer:end -->
<script>
(function(){
// one request per button; the wallet shows the chain to the user and decides. No key, no seed, no signature here.
var CHAINS = {
testnet: { chainId: '0x116e', chainName: 'Igneum Testnet', nativeCurrency: { name: 'Igneum', symbol: 'IGN', decimals: 18 }, rpcUrls: ['https://rpc.testnet.igneum.network'], blockExplorerUrls: [] },
devnet: { chainId: '0x116f', chainName: 'Igneum Devnet (local node)', nativeCurrency: { name: 'Igneum', symbol: 'IGN', decimals: 18 }, rpcUrls: ['http://127.0.0.1:26790'], blockExplorerUrls: [] }
};
function status(name, text) { var el = document.querySelector('[data-status="' + name + '"]'); if (el) el.textContent = text; }
Array.prototype.forEach.call(document.querySelectorAll('[data-add]'), function (btn) {
btn.addEventListener('click', function () {
var name = btn.getAttribute('data-add'); var chain = CHAINS[name];
if (!window.ethereum || !window.ethereum.request) { status(name, 'No Ethereum wallet found in this browser. Install MetaMask, or add the network by hand (below).'); return; }
status(name, 'Waiting for the wallet...');
window.ethereum.request({ method: 'wallet_addEthereumChain', params: [chain] })
.then(function () { status(name, 'Added. The wallet is on ' + chain.chainName + ' (chain id ' + parseInt(chain.chainId, 16) + ').'); })
.catch(function (e) { status(name, e && e.code === 4001 ? 'Cancelled in the wallet.' : 'The wallet refused: ' + ((e && e.message) || 'unknown error') + '. Add it by hand (below).'); });
});
});
})();
</script>
</body>
</html>

116
site/light/app.js Normal file
View file

@ -0,0 +1,116 @@
// 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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 '<ol class="steps">' + result.steps.map(s => `<li class="${s.ok ? 'ok' : 'bad'}"><b>${s.ok ? 'checked' : 'refused'}</b><span>${esc(s.name)}</span><small>${esc(s.detail)}</small></li>`).join('') + '</ol>';
}
function negativeHtml(name, result) {
return `<div class="neg ${result.verified ? 'wrong' : 'right'}"><b>${result.verified ? 'NOT REFUSED (this is a bug)' : 'refused'}</b> <span>${esc(name)}</span><small>${esc(result.reason || 'verified')}</small></div>`;
}
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}`);
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
? `<div class="headline"><div class="eyebrow ember">Proven balance · Devnet 3, no value</div><div class="big">${esc(formatIgn(res.balance_wei))} <span>IGN</span></div>
<p class="pt">${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.</p></div>`
: `<div class="headline bad"><div class="eyebrow">Not verified · Devnet 3, no value</div><p class="pt">${esc(res.reason)}</p></div>`;
out.innerHTML = top + negativeHtml('a copy of this proof with one byte of a trie node altered', neg) + stepsHtml(res)
+ `<p class="note">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).</p>`;
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}`);
const receipt = { format: 'igneum-receipt-v1', issued: new Date().toISOString(), ...r, checkpoint: { ...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
? `<div class="headline"><div class="eyebrow ember">Final · Devnet 3, no value</div><div class="big">${esc(formatIgn(t.value || '0'))} <span>IGN</span></div>
<div class="kv"><div class="k">To</div><div class="mono">${esc(t.to || 'contract creation')}</div><div class="k">From</div><div class="mono">${esc(receipt.tx_as_reported.from)} <small>(as the node reports it; the signature is the chain's check)</small></div>
<div class="k">Transaction</div><div class="mono">0x${esc(receipt.tx_hash)}</div><div class="k">Block</div><div class="mono">${esc(res.block)} <small>at ${esc(when)}, DAA ${esc(res.block_daa)}</small></div>
<div class="k">Finality</div><div>checkpoint ${res.checkpoint}, ${esc(res.certificate)}; ${res.headers} headers from the block to the checkpoint, verified here in ${res.ms} ms</div>
${receipt.execution ? `<div class="k">Execution</div><div>status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}, gas ${parseInt(receipt.execution.gas_used, 16)}, ${receipt.execution.logs} log(s) <small>(as the node reports it, not proven here)</small></div>` : ''}</div></div>`
: `<div class="headline bad"><div class="eyebrow">Not verified · Devnet 3, no value</div><p class="pt">${esc(res.reason)}</p></div>`;
out.innerHTML = top + negativeHtml('a copy of this receipt with one nibble of the raw transaction altered', neg) + stepsHtml(res)
+ (res.verified ? `<p><button class="btn" type="button" data-download>Download the receipt (JSON, ${Math.round(JSON.stringify(receipt).length / 1024)} KB)</button></p>
<p class="note">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: <code>node verify-receipt.js receipt.json</code> (<a href="/receipt/verify-receipt.js" download>verify-receipt.js</a>, plain JavaScript, no npm, no network). A tampered file fails there the same way the copy above failed here.</p>` : '');
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 = `<div class="headline bad"><div class="eyebrow">Could not fetch · Devnet 3, no value</div><p class="pt">${esc(String(err.message || err))}</p></div>`; }
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(); }

378
site/light/core.js Normal file
View file

@ -0,0 +1,378 @@
// Igneum reference apps, the checks (no I/O, no DOM). 8 October 2026. Used by the light wallet (/light), the receipt
// page (/receipt) and the one-file offline receipt verifier. Everything here recomputes; nothing is taken on trust
// from the node that served the data. `deps` is { blake2b, bls, keccak } (@noble/hashes blake2b and keccak_256,
// @noble/curves bls12_381), injected so the browser, Node and the bundled verifier share one file.
//
// What a balance proof chains together (every link recomputed here):
// certificate ──signs──> checkpoint header ──parents──> ... ──parents──> carrier header
// carrier header.hash_merkle_root ──merkle path──> coinbase transaction hash ──payload──> IGNS segment record
// segment record ──BLS signature by a voter──> public values ──post_root──> MPT account proof ──> balance
// What a receipt proof chains together: certificate ──> checkpoint ──parents──> including block ──merkle──> raw tx.
//
// Formats, from the node's own code (vendor/igneum-node-light at 5b673577 plus e6081dd6): consensus/core/src/hashing
// (header, transaction), crypto/merkle (the body root), consensus/core/src/proving.rs (IGNS, SegmentRecord,
// BlockStatement), consensus/core/src/finality.rs (the coinbase extra data and IGNF), igneum/exec/src/state.rs
// (the keccak-keyed MPT in reth's layout, eth_getProof).
import { headerHash, voteKeyHash, verifyCheckpoint, hexToBytes, bytesToHex } from '../verify/core.js';
export { headerHash, voteKeyHash, verifyCheckpoint, hexToBytes, bytesToHex };
export const DST_SEGMENT = 'IGNEUM_SEGMENT_RECORD_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_';
export const SEGMENT_RECORD_LEN = 2 + 8 + 8 + 32 + 48 + 20 + 340 + 32 + 96;
export const ZERO32 = new Uint8Array(32);
const te = new TextEncoder();
const strip = s => String(s).replace(/^0x/i, '');
const u64le = v => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(v), true); return b; };
const u16le = v => { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, Number(v), true); return b; };
const u32le = v => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, Number(v), true); return b; };
function concat(parts) { const n = parts.reduce((a, p) => a + p.length, 0); const m = new Uint8Array(n); let o = 0; for (const p of parts) { m.set(p, o); o += p.length; } return m; }
const eq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
const bigOf = b => { let v = 0n; for (const x of b) v = (v << 8n) | BigInt(x); return v; };
export const keyed = (blake2b, key) => data => blake2b(data, { dkLen: 32, key: te.encode(key) });
// ---- the body merkle root (crypto/merkle/src/lib.rs): leaves padded to a power of two, a missing right child is the
// zero hash, inner node = BLAKE2b-256 keyed "MerkleBranchHash" over left || right; one leaf is its own root --------
export function merkleRoot(leaves, blake2b) {
const H = keyed(blake2b, 'MerkleBranchHash');
if (leaves.length === 0) return ZERO32;
let level = leaves.slice();
while (level.length > 1) {
const next = [];
for (let i = 0; i < level.length; i += 2) next.push(H(concat([level[i], level[i + 1] || ZERO32])));
level = next;
}
return level[0];
}
export function merklePath(leaves, index) {
const path = []; let level = leaves.slice(); let i = index;
while (level.length > 1) {
const sib = i % 2 === 0 ? (level[i + 1] || ZERO32) : level[i - 1];
path.push(sib);
const next = []; for (let k = 0; k < level.length; k += 2) next.push(new Uint8Array(0));
// only the shape matters here; the hashes are recomputed by the verifier
level = next; i = i >> 1;
}
return path;
}
export function merkleRootFromPath(leaf, index, siblings, blake2b) {
const H = keyed(blake2b, 'MerkleBranchHash');
let h = leaf, i = index;
for (const s of siblings) { h = i % 2 === 0 ? H(concat([h, s])) : H(concat([s, h])); i = i >> 1; }
return h;
}
// ---- the coinbase transaction hash (consensus/core/src/hashing/tx.rs, FULL flags) from the RPC's JSON ------------
export function coinbaseTxHash(tx, blake2b, amountWireLen = 8) {
const version = Number(tx.version || 0);
const parts = [u16le(version), u64le((tx.inputs || []).length)];
for (const inp of tx.inputs || []) {
parts.push(hexToBytes(strip(inp.previousOutpoint.transactionId)), u32le(inp.previousOutpoint.index));
const sig = hexToBytes(strip(inp.signatureScript || ''));
parts.push(u64le(sig.length), sig);
if (version < 1) parts.push(new Uint8Array([Number(inp.sigOpCount || 0)]));
parts.push(u64le(inp.sequence));
if (version >= 1) parts.push(u16le(inp.computeBudget || 0));
}
parts.push(u64le((tx.outputs || []).length));
for (const out of tx.outputs || []) {
const v = new Uint8Array(amountWireLen); let x = BigInt(out.value);
for (let i = 0; i < amountWireLen; i++) { v[i] = Number(x & 0xffn); x >>= 8n; }
const spk = hexToBytes(strip(out.scriptPublicKey)); // version u16 LE || script, as the RPC serialises it
parts.push(v, spk.subarray(0, 2), u64le(spk.length - 2), spk.subarray(2));
if (version >= 1) parts.push(new Uint8Array([out.covenant ? 1 : 0]));
}
const payload = hexToBytes(strip(tx.payload || ''));
parts.push(u64le(tx.lockTime || 0), hexToBytes(strip(tx.subnetworkId)), u64le(tx.gas || 0), u64le(payload.length), payload);
const mass = BigInt(tx.mass || 0);
if (version < 1) { if (mass > 0n) parts.push(u64le(mass)); } else parts.push(u64le(mass));
return keyed(blake2b, 'TransactionHash')(concat(parts));
}
// ---- the coinbase payload: blue_score u64 || subsidy || script version u16 || script len u8 || script || extra; the
// extra data ends with nested sections `items || len_le32 || TAG`: IGNF last, IGNP before it, IGNS before that ----
export function coinbaseExtraData(payload) {
if (payload.length < 19) return new Uint8Array(0);
const scriptLen = payload[18];
return payload.subarray(19 + scriptLen);
}
function takeSection(extra, tag) {
const n = extra.length;
if (n < 8) return { items: null, before: extra };
const t = String.fromCharCode(...extra.subarray(n - 4));
if (t !== tag) return { items: null, before: extra };
const len = new DataView(extra.buffer, extra.byteOffset + n - 8, 4).getUint32(0, true);
if (len + 8 > n) return { items: null, before: extra };
return { items: extra.subarray(n - 8 - len, n - 8), before: extra.subarray(0, n - 8 - len) };
}
export function segmentRecordsOf(payload) {
const extra = coinbaseExtraData(payload);
const f = takeSection(extra, 'IGNF');
const p = takeSection(f.before, 'IGNP');
const s = takeSection(p.before, 'IGNS');
const out = [];
if (!s.items) return out;
for (let o = 0; o + SEGMENT_RECORD_LEN <= s.items.length; o += SEGMENT_RECORD_LEN) out.push(parseSegmentRecord(s.items.subarray(o, o + SEGMENT_RECORD_LEN)));
return out;
}
export function parseSegmentRecord(b) {
if (b.length !== SEGMENT_RECORD_LEN) throw new Error(`segment record is ${b.length} bytes, not ${SEGMENT_RECORD_LEN}`);
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
let o = 0;
const version = dv.getUint16(o, true); o += 2;
const first = dv.getBigUint64(o, true); o += 8;
const last = dv.getBigUint64(o, true); o += 8;
const block = b.subarray(o, o + 32); o += 32;
const pubkey = b.subarray(o, o + 48); o += 48;
const payout = b.subarray(o, o + 20); o += 20;
const publicValues = b.subarray(o, o + 340); o += 340;
const proofHash = b.subarray(o, o + 32); o += 32;
const signature = b.subarray(o, o + 96);
return { bytes: b, version, first, last, block, pubkey, payout, publicValues, proofHash, signature, statement: parseBlockStatement(publicValues) };
}
export function parseBlockStatement(b) {
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
const h = o => bytesToHex(b.subarray(o, o + 32));
return {
chain_id: dv.getBigUint64(0), number: dv.getBigUint64(8), block_hash: h(16), parent_hash: h(48), shard_count: dv.getUint32(80),
tx_commitment: h(84), pre_root: h(116), post_root: h(148), receipts: h(180), gas_used: dv.getBigUint64(212), pgas_used: dv.getBigUint64(220),
executed: dv.getUint32(228), skipped: dv.getUint32(232), provers: h(236), shard_vk: h(268), agg_vk: h(300), chain_len: dv.getBigUint64(332),
};
}
export function segmentRecordMessage(chainId, r) {
return concat([te.encode('igneum-segment-record-v1/' + chainId), new Uint8Array([0]), u64le(r.first), u64le(r.last), r.block, r.payout, r.publicValues, r.proofHash]);
}
export function verifySegmentRecordSignature(chainId, r, bls) {
const L = bls.longSignatures;
return L.verify(r.signature, L.hash(segmentRecordMessage(chainId, r), DST_SEGMENT), r.pubkey);
}
// ---- RLP and the Merkle Patricia trie (keccak-keyed, Ethereum's layout) ----------------------------------------
export function rlpDecode(b) {
const [item, rest] = rlpItem(b, 0);
if (rest !== b.length) throw new Error('rlp: trailing bytes');
return item;
}
function rlpItem(b, o) {
if (o >= b.length) throw new Error('rlp: short');
const x = b[o];
if (x < 0x80) return [b.subarray(o, o + 1), o + 1];
if (x < 0xb8) { const n = x - 0x80; return [b.subarray(o + 1, o + 1 + n), o + 1 + n]; }
if (x < 0xc0) { const ll = x - 0xb7; const n = Number(bigOf(b.subarray(o + 1, o + 1 + ll))); return [b.subarray(o + 1 + ll, o + 1 + ll + n), o + 1 + ll + n]; }
let n, start;
if (x < 0xf8) { n = x - 0xc0; start = o + 1; } else { const ll = x - 0xf7; n = Number(bigOf(b.subarray(o + 1, o + 1 + ll))); start = o + 1 + ll; }
const end = start + n; if (end > b.length) throw new Error('rlp: list overruns');
const items = []; let p = start;
while (p < end) { const [it, q] = rlpItem(b, p); items.push(it); p = q; }
return [items, end];
}
export function rlpEncode(item) {
if (item instanceof Uint8Array) {
if (item.length === 1 && item[0] < 0x80) return item;
return concat([rlpLen(item.length, 0x80), item]);
}
const body = concat(item.map(rlpEncode));
return concat([rlpLen(body.length, 0xc0), body]);
}
function rlpLen(n, base) {
if (n < 56) return new Uint8Array([base + n]);
const bytes = []; let x = n; while (x > 0) { bytes.unshift(x & 0xff); x = Math.floor(x / 256); }
return new Uint8Array([base + 55 + bytes.length, ...bytes]);
}
export const trimBig = v => { v = BigInt(v); if (v === 0n) return new Uint8Array(0); let h = v.toString(16); if (h.length % 2) h = '0' + h; return hexToBytes(h); };
const nibbles = b => { const out = []; for (const x of b) { out.push(x >> 4, x & 15); } return out; };
function hpDecode(b) { // hex-prefix: returns [nibbles, isLeaf]
const n = nibbles(b); const flag = n[0];
const body = flag & 1 ? n.slice(1) : n.slice(2);
return [body, (flag & 2) !== 0];
}
// Walks the proof from the root to the key; returns the value bytes (Uint8Array) or null for a proven absence.
// Throws on any node that does not hash to its reference, any malformed node, or a path that does not reach a verdict.
export function mptVerify(root, key, proofNodes, keccak) {
const path = nibbles(key); let want = root; let pos = 0;
const byHash = new Map(); for (const n of proofNodes) byHash.set(bytesToHex(keccak(n)), n);
if (proofNodes.length === 0) { if (eq(root, keccak(rlpEncode(new Uint8Array(0))))) return null; throw new Error('mpt: no proof nodes for a non-empty root'); }
let node = byHash.get(bytesToHex(want));
if (!node) throw new Error('mpt: the first proof node is not the root');
for (let guard = 0; guard < 128; guard++) {
const items = rlpDecode(node);
if (!Array.isArray(items)) throw new Error('mpt: node is not a list');
let next;
if (items.length === 17) {
if (pos === path.length) { const v = items[16]; return v.length ? v : null; }
next = items[path[pos]]; pos += 1;
} else if (items.length === 2) {
const [np, leaf] = hpDecode(items[0]);
for (let i = 0; i < np.length; i++) if (path[pos + i] !== np[i]) return null; // the key diverges: proven absent
pos += np.length;
if (leaf) { if (pos !== path.length) return null; return items[1]; }
next = items[1];
} else throw new Error('mpt: node with ' + items.length + ' items');
if (next instanceof Uint8Array) {
if (next.length === 0) return null; // an empty slot: proven absent
if (next.length !== 32) throw new Error('mpt: bad reference');
node = byHash.get(bytesToHex(next));
if (!node) throw new Error('mpt: a referenced node is missing from the proof');
want = next;
} else {
node = rlpEncode(next); // an embedded node (under 32 bytes)
}
}
throw new Error('mpt: path too deep');
}
// Verifies an eth_getProof account proof against `stateRoot`; returns {exists, nonce, balance, storageRoot, codeHash}.
export function verifyAccountProof(stateRoot, address, proofHex, keccak) {
const key = keccak(hexToBytes(strip(address)));
const v = mptVerify(stateRoot, key, proofHex.map(h => hexToBytes(strip(h))), keccak);
if (v === null) return { exists: false, nonce: 0n, balance: 0n, storageRoot: null, codeHash: null };
const items = rlpDecode(v);
if (!Array.isArray(items) || items.length !== 4) throw new Error('mpt: account leaf is not [nonce, balance, storageRoot, codeHash]');
return { exists: true, nonce: bigOf(items[0]), balance: bigOf(items[1]), storageRoot: bytesToHex(items[2]), codeHash: bytesToHex(items[3]) };
}
export function verifyStorageProof(storageRoot, slot, proofHex, keccak) {
const key = keccak(hexToBytes(strip(slot).padStart(64, '0')));
const v = mptVerify(storageRoot, key, proofHex.map(h => hexToBytes(strip(h))), keccak);
if (v === null) return 0n;
const item = rlpDecode(v);
if (Array.isArray(item)) throw new Error('mpt: storage leaf is a list');
return bigOf(item);
}
// ---- the header path: headers[0] is the oldest, each next header names the previous as a direct parent ------------
export function verifyHeaderPath(headers, blake2b, fromHash, toHash) {
if (!headers.length) throw new Error('no headers');
for (let i = 0; i < headers.length; i++) {
const h = headers[i];
const got = headerHash(h, blake2b);
if (got !== strip(h.hash)) throw new Error(`header ${i} (${strip(h.hash).slice(0, 12)}) does not recompute: ${got.slice(0, 12)}`);
if (i > 0) {
const direct = (h.parents_by_level && h.parents_by_level[0]) || [];
if (!direct.includes(strip(headers[i - 1].hash))) throw new Error(`header ${i} does not name header ${i - 1} as a direct parent`);
}
}
if (fromHash && strip(headers[0].hash) !== strip(fromHash)) throw new Error('the first header is not the block the proof starts from');
if (toHash && strip(headers[headers.length - 1].hash) !== strip(toHash)) throw new Error('the last header is not the certified checkpoint');
return headers.length;
}
// ---- the balance proof ------------------------------------------------------------------------------------------
// `cp` is the /api/checkpoint body (certificate, voters, headers to the previous lock); `proof` is the /balance body:
// { address, chain_id, checkpoint: {hash, index}, headers: [carrier .. checkpoint], carrier: { coinbase: <RpcTransaction>,
// evm_tx_hashes: [hex...], leaf_index: 0, merkle_siblings: [hex...] }, segment_record_hex, account: eth_getProof result }.
// Returns { verified, steps: [{name, ok, detail}], balance, ... } and stops at the first failing step.
export function verifyBalance(cp, proof, deps) {
const { blake2b, bls, keccak } = deps;
const steps = []; const t0 = now();
const step = (name, fn) => { try { const d = fn(); steps.push({ name, ok: true, detail: d }); return d; } catch (e) { steps.push({ name, ok: false, detail: String(e.message || e) }); throw e; } };
const done = extra => ({ ...extra, steps, ms: Math.round((now() - t0) * 10) / 10 });
try {
const cert = step('certificate: BLS aggregate over the checkpoint, 2/3 of active and 17/30 of total weight', () => {
const r = verifyCheckpoint(cp, { blake2b, bls });
if (!r.verified) throw new Error(r.reason);
return `checkpoint ${r.index}, ${r.signers} of ${r.voters} voters, ${(r.weight_fraction_total * 100).toFixed(1)}% of total weight, ${r.headers_checked} headers to the previous lock`;
});
step('the proof names the certified checkpoint', () => {
if (strip(proof.checkpoint.hash) !== strip(cp.hash) || Number(proof.checkpoint.index) !== Number(cp.index)) throw new Error('the balance proof is for another checkpoint than the certificate');
if (proof.chain_id !== cp.chain_id) throw new Error(`chain ${proof.chain_id} is not ${cp.chain_id}`);
return `${cp.chain_id}, checkpoint ${cp.index}`;
});
const n = step('header chain from the carrier block up to the checkpoint (every hash recomputed, every parent link checked)', () =>
verifyHeaderPath(proof.headers, blake2b, proof.headers[0].hash, cp.hash));
const carrier = proof.headers[0];
const record = step('the coinbase transaction is in the carrier block (hash recomputed, merkle path to hash_merkle_root)', () => {
const cb = coinbaseTxHash(proof.carrier.coinbase, blake2b, proof.carrier.amount_wire_len || 8);
const sibs = proof.carrier.merkle_siblings.map(s => hexToBytes(strip(s)));
const root = merkleRootFromPath(cb, Number(proof.carrier.leaf_index), sibs, blake2b);
if (bytesToHex(root) !== strip(carrier.hash_merkle_root)) throw new Error('the merkle path does not reach the carrier\'s hash_merkle_root');
const recs = segmentRecordsOf(hexToBytes(strip(proof.carrier.coinbase.payload)));
const want = strip(proof.segment_record_hex);
const rec = recs.find(r => bytesToHex(r.bytes) === want);
if (!rec) throw new Error(`the carrier's coinbase carries ${recs.length} segment record(s), none is the named one`);
return `coinbase ${bytesToHex(cb).slice(0, 12)}, leaf ${proof.carrier.leaf_index} of ${proof.carrier.evm_tx_hashes.length + 1}, record for blocks ${rec.first} to ${rec.last}`;
}) && segmentRecordsOf(hexToBytes(strip(proof.carrier.coinbase.payload))).find(r => bytesToHex(r.bytes) === strip(proof.segment_record_hex));
const agg = step('the segment record is signed by its aggregator (BLS) and the aggregator is a voter with weight', () => {
if (!verifySegmentRecordSignature(cp.chain_id, record, bls)) throw new Error('the aggregator\'s signature does not verify');
const kh = voteKeyHash(record.pubkey, blake2b);
const voter = (cp.voters || []).find(v => (v.vote_key_hash || voteKeyHash(hexToBytes(v.pubkey_hex), blake2b)) === kh);
if (!voter) throw new Error('the aggregator is not in the certified voter set');
const total = (cp.voters || []).reduce((a, v) => a + Number(v.weight), 0);
return `aggregator ${kh.slice(0, 12)}, weight ${voter.weight} of ${total} (${(100 * Number(voter.weight) / total).toFixed(2)}%)`;
});
const st = record.statement;
step('the record\'s statement names the chain and the block whose state it commits', () => {
if (Number(st.number) !== Number(record.last)) throw new Error('the statement is not for the segment\'s last block');
if (bytesToHex(record.block) !== st.block_hash) throw new Error('the record\'s block hash is not the statement\'s');
if (Number(st.chain_id) !== Number(proof.account.evm_chain_id)) throw new Error(`statement chain id ${st.chain_id} is not the proof's ${proof.account.evm_chain_id}`);
return `chain id ${st.chain_id}, chain block ${st.number}, post_root ${st.post_root.slice(0, 12)}, ${st.executed} executed, chain_len ${st.chain_len}`;
});
const acct = step('account proof under post_root (keccak-keyed Merkle Patricia trie, every node hashed)', () => {
if (Number(proof.account.blockNumber) !== Number(st.number)) throw new Error('the account proof is for another block than the statement');
if (strip(proof.account.stateRoot) !== st.post_root) throw new Error('the node\'s state root is not the proven post_root');
const a = verifyAccountProof(hexToBytes(st.post_root), proof.address, proof.account.accountProof, keccak);
if (a.exists && a.balance !== BigInt(proof.account.balance)) throw new Error('the proven balance is not the balance the node reported');
if (!a.exists && BigInt(proof.account.balance) !== 0n) throw new Error('the node reports a balance for an account the trie does not hold');
return `${proof.account.accountProof.length} nodes, ${a.exists ? 'account present' : 'account absent (exclusion proof)'}`;
});
const balance = acct.exists ? acct.balance : 0n;
return done({ verified: true, balance, balance_wei: balance.toString(), nonce: acct.nonce.toString(), block: Number(st.number), post_root: st.post_root, checkpoint: Number(cp.index), headers: n, aggregator: agg, certificate: cert });
} catch (e) {
return done({ verified: false, reason: String(e.message || e) });
}
}
// ---- the receipt proof ------------------------------------------------------------------------------------------
// `receipt` = { chain_id, tx_hash, raw_tx_hex, checkpoint: {hash, index, certificate: <the /api/checkpoint body>},
// including_block: { header, evm_tx_hashes, leaf_index, merkle_siblings }, headers: [including .. checkpoint],
// execution (optional, as the node reports it): { chain_block, status, gas_used, ... } }
export function verifyReceipt(receipt, deps) {
const { blake2b, bls, keccak } = deps;
const steps = []; const t0 = now();
const step = (name, fn) => { try { const d = fn(); steps.push({ name, ok: true, detail: d }); return d; } catch (e) { steps.push({ name, ok: false, detail: String(e.message || e) }); throw e; } };
const done = extra => ({ ...extra, steps, ms: Math.round((now() - t0) * 10) / 10 });
try {
const cp = receipt.checkpoint.certificate;
const cert = step('certificate: BLS aggregate over the checkpoint, 2/3 of active and 17/30 of total weight', () => {
const r = verifyCheckpoint(cp, { blake2b, bls });
if (!r.verified) throw new Error(r.reason);
if (strip(cp.hash) !== strip(receipt.checkpoint.hash) || receipt.chain_id !== cp.chain_id) throw new Error('the receipt names another checkpoint or chain than its certificate');
return `checkpoint ${r.index} on ${cp.chain_id}, ${r.signers} of ${r.voters} voters, ${(r.weight_fraction_total * 100).toFixed(1)}% of total weight`;
});
const tx = step('the transaction hash is keccak256 of the raw signed transaction', () => {
const raw = hexToBytes(strip(receipt.raw_tx_hex));
const h = bytesToHex(keccak(raw));
if (h !== strip(receipt.tx_hash)) throw new Error(`the raw bytes hash to ${h.slice(0, 12)}, not ${strip(receipt.tx_hash).slice(0, 12)}`);
return parseTx(raw);
});
const n = step('header chain from the including block up to the checkpoint (every hash recomputed, every parent link checked)', () =>
verifyHeaderPath(receipt.headers, blake2b, receipt.including_block.header.hash, cp.hash));
step('the transaction is a leaf of the including block\'s hash_merkle_root', () => {
const ib = receipt.including_block;
const sibs = ib.merkle_siblings.map(s => hexToBytes(strip(s)));
const root = merkleRootFromPath(hexToBytes(strip(receipt.tx_hash)), Number(ib.leaf_index), sibs, blake2b);
if (bytesToHex(root) !== strip(receipt.headers[0].hash_merkle_root)) throw new Error('the merkle path does not reach the including block\'s hash_merkle_root');
return `leaf ${ib.leaf_index} of ${ib.leaf_count}`;
});
return done({ verified: true, tx, headers: n, checkpoint: Number(cp.index), certificate: cert, block: strip(receipt.headers[0].hash), block_daa: receipt.headers[0].daa_score, block_time: receipt.headers[0].timestamp });
} catch (e) {
return done({ verified: false, reason: String(e.message || e) });
}
}
// A typed (EIP-1559 type 2, EIP-2930 type 1) or legacy raw transaction: the fields a receipt shows. No signature
// recovery here (the node's `from` is shown as reported; the chain's own check of the signature is what put the
// transaction in a block).
export function parseTx(raw) {
const type = raw[0] <= 0x7f ? raw[0] : 0;
const body = rlpDecode(type ? raw.subarray(1) : raw);
const hex = b => '0x' + bytesToHex(b);
if (type === 2) return { type, chain_id: bigOf(body[0]).toString(), nonce: bigOf(body[1]).toString(), to: body[5].length ? hex(body[5]) : null, value: bigOf(body[6]).toString(), data: hex(body[7]), gas: bigOf(body[4]).toString() };
if (type === 1) return { type, chain_id: bigOf(body[0]).toString(), nonce: bigOf(body[1]).toString(), to: body[4].length ? hex(body[4]) : null, value: bigOf(body[5]).toString(), data: hex(body[6]), gas: bigOf(body[3]).toString() };
return { type: 0, nonce: bigOf(body[0]).toString(), to: body[3].length ? hex(body[3]) : null, value: bigOf(body[4]).toString(), data: hex(body[5]), gas: bigOf(body[2]).toString() };
}
export function formatIgn(wei, decimals = 18) {
const v = BigInt(wei); const base = 10n ** BigInt(decimals);
const whole = v / base; let frac = (v % base).toString().padStart(decimals, '0').replace(/0+$/, '');
if (frac.length > 6) frac = frac.slice(0, 6);
return whole.toString() + (frac ? '.' + frac : '');
}
const now = () => (typeof performance !== 'undefined' ? performance : Date).now();

85
site/light/test.html Normal file
View file

@ -0,0 +1,85 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Igneum reference apps test: genuine and tampered</title>
<meta name="robots" content="noindex">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>body{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:#0C0C0E;color:#E8E4DA;padding:24px;max-width:1100px}h1{font-size:16px}h2{font-size:14px;margin-top:28px}table{display:block;overflow-x:auto;max-width:100%}code{overflow-wrap:anywhere}td{padding:4px 10px;border-bottom:1px solid #222;vertical-align:top}.ok{color:#7ED957}.bad{color:#F2541B}p{max-width:90ch}</style>
</head>
<body>
<h1>Igneum reference apps: the negative cases, in this tab (Devnet 3, no value)</h1>
<p>Each row runs <code>site/light/core.js</code> on live Devnet 3 data from <code id="api"></code>. A tampered case must read <b>refused</b>; the genuine case must read <b>verified</b>. The same cases run under Node in <code>tools/reference-apps/light-service/verify.test.mjs</code>. Parameters: <code>?api=</code> (the read service), <code>?address=</code> (the balance to prove; default: the miner of the latest block), <code>?tx=</code> (a transaction; default: one in a recent block).</p>
<h2>Receipt</h2>
<table id="r"><thead><tr><td>case</td><td>result</td><td>ms</td><td>reason</td></tr></thead><tbody></tbody></table>
<h2>Balance</h2>
<table id="b"><thead><tr><td>case</td><td>result</td><td>ms</td><td>reason</td></tr></thead><tbody></tbody></table>
<pre id="done"></pre>
<script type="module">
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 } from './core.js';
const deps = { blake2b, bls: bls12_381, keccak: keccak_256 };
const q = new URLSearchParams(location.search);
const API = (q.get('api') || 'https://rpc.devnet.igneum.network/light').replace(/\/$/, '');
const RPC = q.get('rpc') || 'https://rpc.devnet.igneum.network';
document.getElementById('api').textContent = API;
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); };
const get = async u => { const r = await fetch(u, { cache: 'no-store' }); const j = await r.json(); if (!j.ok) throw new Error(j.error || r.status); return j; };
const rpc = async (method, params) => { const r = await fetch(RPC, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) }); const j = await r.json(); if (j.error) throw new Error(j.error.message); return j.result; };
const out = [];
function row(table, name, r, want) {
const ok = r.verified === want; out.push({ table, name, want, ...r, behaved: ok });
const tr = document.createElement('tr');
tr.innerHTML = `<td>${name}</td><td class="${ok ? 'ok' : 'bad'}">${r.verified ? 'verified' : 'refused'}${ok ? '' : ' (WRONG)'}</td><td>${r.ms}</td><td>${r.reason || ''}</td>`;
document.querySelector('#' + table + ' tbody').appendChild(tr);
}
try {
const cp = await get(`${API}/checkpoint`);
// a transaction to prove: the first in a chain block below the checkpoint
let tx = q.get('tx');
if (!tx) { const cb = await rpc('eth_getBlockByHash', ['0x' + cp.hash, false]); const n = parseInt(cb.number, 16); for (let k = n - 1; k > n - 200 && !tx; k--) { const b = await rpc('eth_getBlockByNumber', ['0x' + k.toString(16), false]); if (b && b.transactions.length) tx = b.transactions[0]; } }
const rr = await get(`${API}/receipt?tx=${tx}&checkpoint=${cp.hash}&index=${cp.index}`);
const receipt = { format: 'igneum-receipt-v1', ...rr, checkpoint: { ...rr.checkpoint, certificate: cp } };
const R = [
['raw transaction altered by one nibble', d => { d.raw_tx_hex = flipHex(d.raw_tx_hex, 40); }],
['transaction hash altered', d => { d.tx_hash = flipHex(d.tx_hash, 10); }],
['merkle leaf index moved', d => { d.including_block.leaf_index = d.including_block.leaf_index === 1 ? 2 : 1; }],
['a merkle sibling altered', d => { d.including_block.merkle_siblings[0] = flipHex(d.including_block.merkle_siblings[0], 3); }],
['a header removed from the path', d => { if (d.headers.length > 2) d.headers.splice(Math.floor(d.headers.length / 2), 1); else d.headers[0].nonce = String(BigInt(d.headers[0].nonce) ^ 1n); }],
['a header nonce altered', d => { const h = d.headers[Math.floor(d.headers.length / 2)]; h.nonce = String(BigInt(h.nonce) ^ 1n); }],
['certificate signature altered', d => { d.checkpoint.certificate.certificate.aggregate_signature_hex = flipHex(d.checkpoint.certificate.certificate.aggregate_signature_hex, 20); }],
['receipt names another checkpoint than its certificate', d => { d.checkpoint.hash = flipHex(d.checkpoint.hash, 60); }],
];
for (const [name, mutate] of R) { const d = clone(receipt); mutate(d); row('r', name, verifyReceipt(d, deps), false); }
row('r', `genuine receipt for ${tx.slice(0, 14)} (${receipt.headers.length} headers, checkpoint ${cp.index})`, verifyReceipt(receipt, deps), true);
// a balance to prove
let address = q.get('address');
if (!address) { const b = await rpc('eth_getBlockByNumber', ['latest', false]); address = b.igneum.rewards[0].miner; }
let proof = null;
try { proof = await get(`${API}/balance?address=${address}&checkpoint=${cp.hash}&index=${cp.index}`); } catch (e) { document.querySelector('#b tbody').innerHTML = `<tr><td colspan="4" class="bad">balance proof not available: ${e.message}</td></tr>`; }
if (proof) {
const B = [
['account proof: a node altered', d => { const i = d.account.accountProof.length - 1; d.account.accountProof[i] = flipHex(d.account.accountProof[i], 30); }],
['reported balance raised by one wei', d => { d.account.balance = '0x' + (BigInt(d.account.balance) + 1n).toString(16); }],
['state root altered', d => { d.account.stateRoot = flipHex(d.account.stateRoot, 8); }],
['post_root altered inside the segment record', d => { d.segment_record_hex = flipHex(d.segment_record_hex, 2 * (2 + 8 + 8 + 32 + 48 + 20 + 148) + 3); }],
['aggregator signature altered', d => { d.segment_record_hex = flipHex(d.segment_record_hex, d.segment_record_hex.length - 10); }],
['coinbase payload altered', d => { d.carrier.coinbase.payload = flipHex(d.carrier.coinbase.payload, 30); }],
['a merkle sibling altered', d => { d.carrier.merkle_siblings[0] = flipHex(d.carrier.merkle_siblings[0], 1); }],
['a header removed from the path', d => { if (d.headers.length > 2) d.headers.splice(1, 1); else d.headers[0].nonce = String(BigInt(d.headers[0].nonce) ^ 1n); }],
['another address with this proof', d => { d.address = '0x' + '11'.repeat(20); }],
];
for (const [name, mutate] of B) { const d = clone(proof); mutate(d); row('b', name, verifyBalance(cp, d, deps), false); }
{ const c = clone(cp); c.certificate.aggregate_signature_hex = flipHex(c.certificate.aggregate_signature_hex, 20); row('b', 'certificate signature altered', verifyBalance(c, proof, deps), false); }
const g = verifyBalance(cp, proof, deps);
row('b', `genuine balance of ${address.slice(0, 12)} at chain block ${proof.segment.last} (${proof.headers.length} headers, checkpoint ${cp.index})`, g, true);
}
} catch (e) { document.getElementById('done').textContent = 'ERROR ' + (e.message || e); }
const bad = out.filter(x => !x.behaved).length;
document.getElementById('done').textContent += (bad ? `\nFAILED ${bad} case(s)` : `\nRESULT every case behaved (${out.length} cases)`) + '\n' + JSON.stringify(out.map(x => ({ table: x.table, name: x.name, verified: x.verified, ms: x.ms, reason: x.reason || null })));
</script>
</body>
</html>

View file

@ -171,6 +171,9 @@ body.all .pager{display:none}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -203,6 +206,9 @@ body.all .pager{display:none}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper" aria-current="page"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -255,6 +255,9 @@ details.tablebar summary{display:flex;align-items:center}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -287,6 +290,9 @@ details.tablebar summary{display:flex;align-items:center}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -116,6 +116,9 @@ ol{margin:0 0 14px;padding-left:22px}li{margin-bottom:6px}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -148,6 +151,9 @@ ol{margin:0 0 14px;padding-left:22px}li{margin-bottom:6px}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -111,6 +111,9 @@ pre b{color:var(--molten-text);font-weight:500}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -143,6 +146,9 @@ pre b{color:var(--molten-text);font-weight:500}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
@ -268,9 +274,9 @@ pre b{color:var(--molten-text);font-weight:500}
<div class="platform-body" id="panel-hive" role="tabpanel" aria-labelledby="tab-hive" data-platform-panel="hive" hidden>
<div class="download-platform"><span class="osmark" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span><div><h3>Ember for Linux and HiveOS</h3><p>a tarball for rigs and Hive flight sheets</p></div></div>
<p class="download-note">For the rig people. One tarball, the miner and the node inside, the same signed manifest as the desktop apps.</p>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download the tarball <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v0.3.23 · 28.8 MB</span></a>
<div class="hive-sheet" id="hive"><b>HiveOS Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-0.3.23.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x&lt;your 40-hex payout address&gt;.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
<p class="sha">sha256 <span data-dl-sha="miner-hive">0d716ed92560b6b2eccb63ca1bd7a4d9eb64c9b334e8370e1263f0344cee294e</span></p>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download the tarball <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v0.3.22 · 28.8 MB</span></a>
<div class="hive-sheet" id="hive"><b>HiveOS Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-0.3.22.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x&lt;your 40-hex payout address&gt;.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
<p class="sha">sha256 <span data-dl-sha="miner-hive">8ad6dcef9edc57dcd33e8d5a97cbef5cdda0e384a6e56d3f62f8903029c4c764</span></p>
</div>
</div>
<div>

View file

@ -115,6 +115,9 @@ table{min-width:560px}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -147,6 +150,9 @@ table{min-width:560px}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

342
site/oracle.html Normal file
View file

@ -0,0 +1,342 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Igneum state oracle on Sepolia</title>
<meta name="description" content="A Sepolia contract that stores Devnet 3 finality certificates and proven state roots so other contracts read a Devnet 3 balance. Devnet 3, no value.">
<link rel="canonical" href="https://igneum.network/oracle">
<meta name="theme-color" content="#0C0C0E">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Igneum">
<meta property="og:title" content="Igneum state oracle on Sepolia">
<meta property="og:description" content="A Sepolia contract that stores Devnet 3 finality certificates and proven state roots so other contracts read a Devnet 3 balance. Devnet 3, no value.">
<meta property="og:url" content="https://igneum.network/oracle">
<meta property="og:image" content="https://igneum.network/og.png?v=3">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Igneum state oracle on Sepolia">
<meta name="twitter:description" content="A Sepolia contract that stores Devnet 3 finality certificates and proven state roots so other contracts read a Devnet 3 balance. Devnet 3, no value.">
<meta name="twitter:image" content="https://igneum.network/og.png?v=3">
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192">
<link rel="icon" href="/icon-512.png" type="image/png" sizes="512x512">
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<link rel="manifest" href="/site.webmanifest">
<!-- head:start -->
<link rel="preload" href="/fonts/unbounded-900.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/plex-sans-400.woff2" as="font" type="font/woff2" crossorigin>
<meta name="color-scheme" content="dark light">
<link rel="stylesheet" href="/site.css">
<style>
/* Shared by every page: the head partial, injected by the build between the head markers. Edit the partial, not the page. */
/* Fonts, self-hosted (latin subsets, OFL): Unbounded 500/700/900, IBM Plex Sans 400/500/600, IBM Plex Mono 400/500. Fallbacks carry size and ascent overrides so the swap does not move the layout. The tokens, type and components are in /site.css (docs/plans/site-ui-3.md). */
@font-face{font-family:'Unbounded';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/unbounded-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/unbounded-700.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(/fonts/unbounded-900.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-sans-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-sans-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-sans-600.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-mono-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded Fallback';src:local('Arial Black'),local('Arial-Black'),local('Impact');size-adjust:114%;ascent-override:87.5%;descent-override:21.5%;line-gap-override:0%}
@font-face{font-family:'Plex Sans Fallback';src:local('Arial'),local('Helvetica Neue'),local('Helvetica');size-adjust:100.5%;ascent-override:102%;descent-override:27.4%;line-gap-override:0%}
@font-face{font-family:'Plex Mono Fallback';src:local('Courier New'),local('Menlo');size-adjust:100%;ascent-override:102.5%;descent-override:27.5%;line-gap-override:0%}
</style>
<script>
/* theme before first paint: ?theme=light|dark for one view (the capture tool), else the stored choice under igneum-theme, else the system setting; ?motion=off marks the page data-motion=off so every .reveal is drawn at once (a headless render never scrolls) */
(function(){var t=null;try{t=new URLSearchParams(location.search).get("theme");}catch(e){}if(t!=="light"&&t!=="dark"){try{t=localStorage.getItem("igneum-theme");}catch(e){t=null;}}if(t!=="light"&&t!=="dark")t=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";document.documentElement.setAttribute("data-theme",t);try{if(new URLSearchParams(location.search).get("motion")==="off")document.documentElement.setAttribute("data-motion","off");}catch(e){}})();
</script>
<!-- head:end -->
<style>
/* reference apps (8 Oct 2026): the verify form, the proven headline, the step list; tokens and components are in /site.css */
.vform{display:grid;gap:10px;margin:0}
.vform label{font:500 var(--t-sm)/1.4 var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ash)}
.vform .row{display:flex;gap:10px;flex-wrap:wrap}.vform input{flex:1 1 320px;font-family:var(--f-mono);font-size:14px}
.vstatus{font:400 13px/1.5 var(--f-mono);color:var(--ash);min-height:20px}.vstatus[data-kind="ok"]{color:var(--ember-text)}.vstatus[data-kind="bad"]{color:var(--molten-text)}
.headline{background:var(--graphite);border:1px solid var(--line);border-radius:var(--r-card);padding:var(--s-5);margin:var(--s-5) 0 var(--s-4)}
.headline.bad{border-color:var(--molten)}
.headline .big{font:900 clamp(32px,6vw,56px)/1.05 var(--f-display);color:var(--bone);margin:10px 0 8px;overflow-wrap:anywhere}.headline .big span{font-size:.45em;color:var(--ash);font-weight:700}
.headline .kv{margin-top:var(--s-3)}.headline .kv small{color:var(--ash);display:block}.headline .mono{font-family:var(--f-mono);font-size:13px;overflow-wrap:anywhere}
.neg{border-left:2px solid var(--ember);background:var(--row);border-radius:0 var(--r-row) var(--r-row) 0;padding:12px var(--s-4);margin:0 0 var(--s-3);font-size:14px;color:var(--ink-2)}
.neg b{font:500 11px/1 var(--f-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ember-text);margin-right:8px}.neg.wrong{border-color:var(--molten)}.neg.wrong b{color:var(--molten-text)}
.neg small{display:block;color:var(--ash);font-family:var(--f-mono);font-size:12px;margin-top:4px;overflow-wrap:anywhere}
.steps{list-style:none;margin:0;padding:0;display:grid;gap:8px}
.steps li{display:grid;grid-template-columns:84px 1fr;gap:4px 12px;background:var(--row);border:1px solid var(--line);border-radius:var(--r-row);padding:10px 14px;font-size:14px;color:var(--ink-2)}
.steps li b{font:500 11px/1.6 var(--f-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ember-text)}.steps li.bad b{color:var(--molten-text)}
.steps li small{grid-column:2;color:var(--ash);font-family:var(--f-mono);font-size:12px;overflow-wrap:anywhere}
.why{border:1px solid var(--line-2);border-radius:var(--r-card);padding:var(--s-4) var(--s-5);background:var(--obsidian);margin:var(--s-5) 0}
.why p{margin:0;font-size:15px;color:var(--ink-2)}.why p+p{margin-top:8px}
.meta{display:grid;gap:6px;font:400 13px/1.6 var(--f-mono);color:var(--ash);margin:var(--s-4) 0 0}.meta code{overflow-wrap:anywhere}
@media (max-width:640px){.steps li{grid-template-columns:1fr}.steps li small{grid-column:1}}
</style>
</head>
<body>
<!-- nav:start -->
<a class="skip" href="#main">Skip to content</a>
<nav class="nav" aria-label="Main" data-nav-version="5">
<div class="container nav-row">
<a href="/" class="logo" aria-label="Igneum home">
<svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg>
<span class="word">IGNEUM</span>
</a>
<span class="devnet" id="nav-devnet" title="igneum-devnet-3, chain id 4463. Coins have no value. The chain may reset."><span class="dot off" id="foot-dot"></span><span id="foot-state">devnet</span></span>
<div class="nav-groups" id="nav-groups" role="list">
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-mine" data-group="mine" aria-expanded="false" aria-controls="nav-panel-mine" aria-haspopup="true">Mine<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-network" data-group="network" data-active aria-expanded="false" aria-controls="nav-panel-network" aria-haspopup="true">Network<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-learn" data-group="learn" aria-expanded="false" aria-controls="nav-panel-learn" aria-haspopup="true">Learn<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
</div>
<div class="nav-controls">
<button class="btn icon-btn" type="button" data-theme-toggle aria-label="Switch to light theme"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg></button>
<a href="/download" class="btn primary small cta" data-nav="download">Download</a>
<button class="burger" id="nav-burger" type="button" aria-expanded="false" aria-controls="nav-sheet" aria-label="Open the menu" aria-haspopup="true"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path class="bars" d="M4 6h16M4 12h16M4 18h16"/><path class="cross" d="m6 6 12 12M6 18 18 6"/></svg></button>
</div>
</div>
<div class="panels" id="nav-panels">
<div class="panel-wrap container">
<div class="nav-panel" id="nav-panel-mine" data-group="mine" role="region" aria-labelledby="nav-btn-mine" hidden>
<div class="panel-list">
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-network" data-group="network" role="region" aria-labelledby="nav-btn-network" hidden>
<div class="panel-list">
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle" aria-current="page"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
<div class="panel-list">
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
</div>
</div>
</div>
</div>
<div class="nav-sheet" id="nav-sheet" role="dialog" aria-modal="true" aria-label="Menu" hidden>
<div class="sheet-scroll">
<div class="sheet-group"><div class="sheet-head">Mine</div>
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle" aria-current="page"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
</div>
<div class="sheet-foot">
<a href="/download" class="btn primary big" data-nav="download">Download</a>
<div class="sheet-social"><a href="https://discord.gg/igneum" target="_blank" rel="noopener">Discord</a><a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener">Source</a><a href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener">Reddit</a></div>
</div>
</div>
</div>
</nav>
<script>
(function(){
// the slim bar (site-ui-5, 7 Oct 2026): three group buttons open one panel each; the burger opens the sheet on phones.
// Keyboard: Enter or Space opens a group, Escape closes it and returns focus to its button, arrows move between the
// group buttons, Tab walks the open panel's links, a click outside closes. The theme toggle flips data-theme and stores it.
var n=document.querySelector('.nav');if(!n)return;
var btns=[].slice.call(n.querySelectorAll('.group-btn')),panels=[].slice.call(n.querySelectorAll('.nav-panel')),wrap=n.querySelector('.panels');
var burger=document.getElementById('nav-burger'),sheet=document.getElementById('nav-sheet');
function panelOf(g){return panels.filter(function(p){return p.getAttribute('data-group')===g;})[0];}
function closeAll(focusBack){var was=null;btns.forEach(function(b){if(b.getAttribute('aria-expanded')==='true')was=b;b.setAttribute('aria-expanded','false');});panels.forEach(function(p){p.hidden=true;});n.classList.remove('panel-open');if(focusBack&&was)was.focus();}
function open(g){closeAll(false);var b=btns.filter(function(x){return x.getAttribute('data-group')===g;})[0],p=panelOf(g);if(!b||!p)return;b.setAttribute('aria-expanded','true');p.hidden=false;n.classList.add('panel-open');anchor(b,p);}
// the panel sits under its button; a panel that would run past the container's right edge aligns its right edge to the button's right edge instead (never past the viewport)
function anchor(b,p){var pw=n.querySelector('.panel-wrap');if(!pw)return;var cr=pw.getBoundingClientRect(),br=b.getBoundingClientRect(),w=p.offsetWidth,left=br.left-cr.left;if(left+w>cr.width)left=(br.right-cr.left)-w;if(left<0)left=0;p.style.setProperty('--panel-left',Math.round(left)+'px');}
window.addEventListener('resize',function(){var o=btns.filter(function(x){return x.getAttribute('aria-expanded')==='true';})[0];if(o)anchor(o,panelOf(o.getAttribute('data-group')));}); btns.forEach(function(b,i){
b.addEventListener('click',function(){var on=b.getAttribute('aria-expanded')==='true';if(on)closeAll(false);else open(b.getAttribute('data-group'));});
b.addEventListener('keydown',function(e){
var k=e.key;
if(k==='ArrowRight'||k==='ArrowLeft'){e.preventDefault();var j=(i+(k==='ArrowRight'?1:btns.length-1))%btns.length;btns[j].focus();if(n.classList.contains('panel-open'))open(btns[j].getAttribute('data-group'));}
else if(k==='ArrowDown'){e.preventDefault();open(b.getAttribute('data-group'));var f=panelOf(b.getAttribute('data-group')).querySelector('a');if(f)f.focus();}
else if(k==='Escape'){closeAll(true);}
});
});
if(wrap)wrap.addEventListener('keydown',function(e){if(e.key==='Escape'){e.stopPropagation();closeAll(true);}});
document.addEventListener('click',function(e){if(!n.classList.contains('panel-open'))return;if(e.target.closest('.group-btn')||e.target.closest('.nav-panel'))return;closeAll(false);});
document.addEventListener('focusin',function(e){if(!n.classList.contains('panel-open'))return;if(n.contains(e.target))return;closeAll(false);});
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&n.classList.contains('panel-open'))closeAll(true);});
// the sheet on phones
function setSheet(o){if(!burger||!sheet)return;n.classList.toggle('open',o);sheet.hidden=!o;burger.setAttribute('aria-expanded',o?'true':'false');burger.setAttribute('aria-label',o?'Close the menu':'Open the menu');document.documentElement.classList.toggle('nav-locked',o);if(o){var f=sheet.querySelector('a');if(f)f.focus();}}
if(burger&&sheet){
burger.addEventListener('click',function(){setSheet(!n.classList.contains('open'));});
sheet.addEventListener('click',function(e){if(e.target.closest('a'))setSheet(false);});
sheet.addEventListener('keydown',function(e){
if(e.key==='Escape'){setSheet(false);burger.focus();return;}
if(e.key!=='Tab')return;var f=sheet.querySelectorAll('a,button');if(!f.length)return;var first=f[0],last=f[f.length-1];
if(e.shiftKey&&document.activeElement===first){e.preventDefault();burger.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();burger.focus();}
});
burger.addEventListener('keydown',function(e){if(e.key==='Tab'&&!e.shiftKey&&n.classList.contains('open')){e.preventDefault();var f=sheet.querySelector('a');if(f)f.focus();}});
var mq=window.matchMedia('(min-width:861px)');if(mq.addEventListener)mq.addEventListener('change',function(){setSheet(false);});
}
// the theme toggle
var t=n.querySelector('[data-theme-toggle]');
function paintToggle(){var dark=document.documentElement.getAttribute('data-theme')!=='light';t.setAttribute('aria-label',dark?'Switch to light theme':'Switch to dark theme');t.innerHTML=dark?'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg>':'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.7 13.3A8.5 8.5 0 0 1 10.7 3.3 8.5 8.5 0 1 0 20.7 13.3Z"/></svg>';}
if(t){t.addEventListener('click',function(){var v=document.documentElement.getAttribute('data-theme')==='light'?'dark':'light';document.documentElement.setAttribute('data-theme',v);try{localStorage.setItem('igneum-theme',v);}catch(e){}document.dispatchEvent(new CustomEvent('igneum-theme'));});paintToggle();document.addEventListener('igneum-theme',paintToggle);}
})();
</script>
<!-- nav:end -->
<main id="main" class="wrap">
<div class="head">
<div class="breadcrumb"><a href="/">Igneum</a><span>/</span><span>State oracle on Sepolia</span></div>
<div class="eyebrow"><span class="line"></span>Reference app 3 · Devnet 3, no value</div>
<h1>Devnet 3 balances, read from Sepolia<span class="accent">.</span></h1>
</div>
<p class="note">A contract on Ethereum Sepolia stores Igneum Devnet 3 finality certificates that anyone submits, checked by a shared certificate verifier, and state roots bound to those checkpoints through the header chain, the coinbase and the segment record. Other contracts then read a proven Devnet 3 balance or storage slot with an ordinary Merkle Patricia proof. No relayer is trusted: a wrong certificate or a wrong proof reverts.</p>
<div class="why">
<p><b>Why this only works on a proven chain.</b> Another chain can only use Igneum state if a contract can check it without running an Igneum node: a weighted certificate over a checkpoint and a state root proven under it are small enough to verify in a few hundred thousand gas. A chain whose state is only "what most nodes say" has nothing a contract can check.</p>
<p>Source: <code>tools/reference-apps/oracle/contracts/</code> (IgneumStateOracle.sol and its libraries), <code>tools/reference-apps/oracle/demo.mjs</code> (the demo that reads a Devnet 3 balance from Sepolia), <code>tools/reference-apps/oracle/test.mjs</code> (the negative cases through eth_call). The shared certificate verifier is the DEX lane's <code>contracts/bridge/src/IgneumCertificateVerifier.sol</code>.</p>
</div>
<section class="card" aria-labelledby="deployed">
<h2 id="deployed">On Sepolia</h2>
<div class="kv" data-oracle-kv>
<div class="k">Oracle</div><div class="mono" data-oracle-address>deploying today; the address lands here with its first stored checkpoint</div>
<div class="k">Verifier</div><div class="mono" data-verifier-address>the shared IgneumCertificateVerifier (DEX lane), or the stand-in until it lands</div>
<div class="k">Chain</div><div>Sepolia, chain id 11155111; Devnet 3 (igneum-devnet-3), no value</div>
</div>
<p class="note">How to read a balance from another contract: <code>IIgneumStateOracle(oracle).provenBalance(number, account, accountProof)</code>, where <code>number</code> is a Devnet 3 chain block whose state root the oracle holds and <code>accountProof</code> is the <code>eth_getProof</code> account proof at that block. A storage slot: <code>provenStorage(number, account, slot, accountProof, storageProof)</code>. Both revert on any mismatch.</p>
</section>
<h2 id="checked">What the contract checks</h2>
<ol>
<li>The certificate, through the shared verifier: the aggregate BLS signature over the checkpoint under the installed voter table and the weight rule.</li>
<li>The header path from the carrier block up to the certified checkpoint: BLAKE2b-256 keyed BlockHash through the precompile, every parent link.</li>
<li>The coinbase transaction under the carrier's <code>hash_merkle_root</code>, and the segment record parsed out of its extra data: the record's <code>post_root</code> for its block is stored under the certificate's index.</li>
<li>Every read: a keccak-keyed Merkle Patricia proof against the stored root, verified on chain.</li>
</ol>
<p class="note">Said plainly, on the contract too (<code>trust()</code>): the aggregator's BLS signature on the record is not checked on chain in this slice, and the voter table the verifier holds is installed by its deployer. The negative cases in <code>test.mjs</code> are a flipped proof node, a wrong block number, a header removed from the path, a merkle sibling altered, a record with its <code>post_root</code> altered, a header with its nonce altered: each reverts.</p>
<p class="asof">Devnet 3, no value. Sepolia, no value. A demonstration of the verification path, not a product.</p>
</main>
<!-- footer:start -->
<footer class="site-footer">
<div class="container">
<div class="footer-grid">
<div class="footer-brand">
<a href="/" class="logo" aria-label="Igneum home"><svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg><span class="word">IGNEUM</span><span class="stop">.</span></a>
<p>Mined by GPUs. Proven by fire.</p>
<div class="social" aria-label="Igneum elsewhere">
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" aria-label="Igneum source: the spec, the vectors and the issues"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M8.6 17.6 3 12l5.6-5.6 1.4 1.4L5.8 12l4.2 4.2zm6.8 0-1.4-1.4L18.2 12 14 7.8l1.4-1.4L21 12z"></path></svg></a>
<a href="https://discord.gg/igneum" target="_blank" rel="noopener" aria-label="The Igneum Discord"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19.6 5.3A17 17 0 0 0 15.4 4l-.2.4a15.6 15.6 0 0 1 3.9 1.9 13.6 13.6 0 0 0-14.2 0A15.6 15.6 0 0 1 8.8 4.4L8.6 4a17 17 0 0 0-4.2 1.3C1.8 9.2 1.1 13 1.4 16.7a17.1 17.1 0 0 0 5.2 2.6l1.1-1.8a10.8 10.8 0 0 1-1.7-.8l.4-.3a12.2 12.2 0 0 0 11.2 0l.4.3-1.7.8 1.1 1.8a17 17 0 0 0 5.2-2.6c.4-4.3-.7-8-3-11.4zM8.7 14.4c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1zm6.6 0c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1z"></path></svg></a>
<a data-social="reddit" href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener" aria-label="Igneum on Reddit"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M22 12.1a2.2 2.2 0 0 0-3.7-1.6 10.8 10.8 0 0 0-5.8-1.8l1-4.6 3.2.7a1.5 1.5 0 1 0 .2-.9l-3.6-.8a.5.5 0 0 0-.5.4l-1.1 5.2a10.8 10.8 0 0 0-5.9 1.8A2.2 2.2 0 1 0 3.4 14a4.3 4.3 0 0 0 0 .7c0 3.4 3.9 6.1 8.6 6.1s8.6-2.7 8.6-6.1a4.3 4.3 0 0 0 0-.7 2.2 2.2 0 0 0 1.4-1.9zM7 13.6a1.5 1.5 0 1 1 1.5 1.5A1.5 1.5 0 0 1 7 13.6zm8.6 4.1a5.7 5.7 0 0 1-3.6 1.1 5.7 5.7 0 0 1-3.6-1.1.4.4 0 0 1 .6-.6 4.9 4.9 0 0 0 3 .9 4.9 4.9 0 0 0 3-.9.4.4 0 0 1 .6.6zm-.3-2.6a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5z"></path></svg></a>
</div>
<div class="footer-dl" aria-label="Download Ember"><a href="/download#windows"><span class="osmark mini" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Windows</a><a href="/download#mac"><span class="osmark mini" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>macOS</a><a href="/download#linux"><span class="osmark mini" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Linux</a><a href="/download#hive"><span class="osmark mini" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>HiveOS</a></div>
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" class="text-link">Specification and test vectors<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 18 18 6M6 6h12v12"/></svg></a>
</div>
<div class="footer-column"><h4>Run</h4><div>
<a href="/download">Download Ember</a>
<a href="/miner">The miner</a>
<a href="/app">The app</a>
<a href="/wallet">The wallet</a>
<a href="/miners">GPU bench table</a>
<a href="/dev-fee">The dev fee</a>
<a href="/faucet">Testnet faucet</a>
<a href="/metamask">Add Igneum to MetaMask</a>
</div></div>
<div class="footer-column"><h4>Read</h4><div>
<a href="/litepaper">Litepaper</a>
<a href="/evidence">Evidence</a>
<a href="/bench">Engineering log</a>
<a href="/ledger">Ledger: every criticism</a>
<a href="/claims">What Igneum does not claim</a>
<a href="/randomx">Igneum vs RandomX</a>
<a href="/provenance">Built on the shoulders</a>
</div></div>
<div class="footer-column"><h4>Explore</h4><div>
<a href="/live">Live devnet</a>
<a href="/explorer">Explorer</a>
<a href="/journey">Journey</a>
<a href="/download">Downloads: devnet build</a>
<a href="https://discord.gg/igneum" target="_blank" rel="noopener">Community Discord</a>
<a href="mailto:hello@igneum.network">hello@igneum.network</a>
<a href="https://git.igneum.network/igneum-network/spec/issues" rel="noopener">An issue on the spec</a>
</div></div>
</div>
<div class="footer-bottom">
<p>Igneum Labs LTD · Unit IH-00-01-01-OF-01, Level 01, Innovation One, Dubai International Financial Centre. Experimental software. Nothing on this site is an offer to sell anything.<br>Report a flaw by email or by an issue on the spec. Nobody from Igneum will ask for your seed. © 2026 Igneum · not an offer to sell anything</p>
<div class="theme-group" role="group" aria-label="Theme"><button type="button" data-theme-set="system" aria-pressed="true">System</button><button type="button" data-theme-set="light" aria-pressed="false">Light</button><button type="button" data-theme-set="dark" aria-pressed="false">Dark</button></div>
</div>
</div>
</footer>
<script>
(function(){
// theme control: stores light or dark under igneum-theme; system clears it and follows the OS. The head applied the value before paint.
var bs=document.querySelectorAll('[data-theme-set]');if(!bs.length)return;
var media=window.matchMedia('(prefers-color-scheme: light)');
function stored(){try{var t=localStorage.getItem('igneum-theme');return t==='light'||t==='dark'?t:'system';}catch(e){return 'system';}}
function apply(){var c=stored();document.documentElement.setAttribute('data-theme',c==='system'?(media.matches?'light':'dark'):c);bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});document.dispatchEvent(new CustomEvent('igneum-theme'));}
bs.forEach(function(b){b.addEventListener('click',function(){var v=b.getAttribute('data-theme-set');try{if(v==='system')localStorage.removeItem('igneum-theme');else localStorage.setItem('igneum-theme',v);}catch(e){}apply();});});
if(media.addEventListener)media.addEventListener('change',function(){if(stored()==='system')apply();});
document.addEventListener('igneum-theme',function(){var c=stored();bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});});
bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===stored()?'true':'false');});
// the network state word in the status bar: one read of the observer per page view
var fs=document.getElementById('foot-state'),fd=document.getElementById('foot-dot');if(fs&&fd){fetch('/api/live?window=30',{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){var on=d&&d.ok&&d.state&&!d.state.stale;fs.textContent=on?'devnet live':'devnet, observer stale';fd.className='dot '+(on?'live':'off');}).catch(function(){fs.textContent='devnet';});}
})();
</script>
<!-- footer:end -->
<script>
(function(){
// one request per button; the wallet shows the chain to the user and decides. No key, no seed, no signature here.
var CHAINS = {
testnet: { chainId: '0x116e', chainName: 'Igneum Testnet', nativeCurrency: { name: 'Igneum', symbol: 'IGN', decimals: 18 }, rpcUrls: ['https://rpc.testnet.igneum.network'], blockExplorerUrls: [] },
devnet: { chainId: '0x116f', chainName: 'Igneum Devnet (local node)', nativeCurrency: { name: 'Igneum', symbol: 'IGN', decimals: 18 }, rpcUrls: ['http://127.0.0.1:26790'], blockExplorerUrls: [] }
};
function status(name, text) { var el = document.querySelector('[data-status="' + name + '"]'); if (el) el.textContent = text; }
Array.prototype.forEach.call(document.querySelectorAll('[data-add]'), function (btn) {
btn.addEventListener('click', function () {
var name = btn.getAttribute('data-add'); var chain = CHAINS[name];
if (!window.ethereum || !window.ethereum.request) { status(name, 'No Ethereum wallet found in this browser. Install MetaMask, or add the network by hand (below).'); return; }
status(name, 'Waiting for the wallet...');
window.ethereum.request({ method: 'wallet_addEthereumChain', params: [chain] })
.then(function () { status(name, 'Added. The wallet is on ' + chain.chainName + ' (chain id ' + parseInt(chain.chainId, 16) + ').'); })
.catch(function (e) { status(name, e && e.code === 4001 ? 'Cancelled in the wallet.' : 'The wallet refused: ' + ((e && e.message) || 'unknown error') + '. Add it by hand (below).'); });
});
});
})();
</script>
</body>
</html>

View file

@ -38,6 +38,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -70,6 +73,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -115,6 +115,9 @@ table{min-width:560px}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -147,6 +150,9 @@ table{min-width:560px}
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -114,6 +114,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -146,6 +149,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

345
site/receipt.html Normal file
View file

@ -0,0 +1,345 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Igneum payment receipt</title>
<meta name="description" content="A Devnet 3 payment receipt proven against the finality certificate, downloadable as JSON and re-verified offline by anyone with a one-file verifier. Devnet 3, no value.">
<link rel="canonical" href="https://igneum.network/receipt">
<meta name="theme-color" content="#0C0C0E">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Igneum">
<meta property="og:title" content="Igneum payment receipt">
<meta property="og:description" content="A Devnet 3 payment receipt proven against the finality certificate, downloadable as JSON and re-verified offline by anyone with a one-file verifier. Devnet 3, no value.">
<meta property="og:url" content="https://igneum.network/receipt">
<meta property="og:image" content="https://igneum.network/og.png?v=3">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Igneum payment receipt">
<meta name="twitter:description" content="A Devnet 3 payment receipt proven against the finality certificate, downloadable as JSON and re-verified offline by anyone with a one-file verifier. Devnet 3, no value.">
<meta name="twitter:image" content="https://igneum.network/og.png?v=3">
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192">
<link rel="icon" href="/icon-512.png" type="image/png" sizes="512x512">
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<link rel="manifest" href="/site.webmanifest">
<!-- head:start -->
<link rel="preload" href="/fonts/unbounded-900.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/plex-sans-400.woff2" as="font" type="font/woff2" crossorigin>
<meta name="color-scheme" content="dark light">
<link rel="stylesheet" href="/site.css">
<style>
/* Shared by every page: the head partial, injected by the build between the head markers. Edit the partial, not the page. */
/* Fonts, self-hosted (latin subsets, OFL): Unbounded 500/700/900, IBM Plex Sans 400/500/600, IBM Plex Mono 400/500. Fallbacks carry size and ascent overrides so the swap does not move the layout. The tokens, type and components are in /site.css (docs/plans/site-ui-3.md). */
@font-face{font-family:'Unbounded';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/unbounded-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/unbounded-700.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(/fonts/unbounded-900.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-sans-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-sans-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-sans-600.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-mono-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded Fallback';src:local('Arial Black'),local('Arial-Black'),local('Impact');size-adjust:114%;ascent-override:87.5%;descent-override:21.5%;line-gap-override:0%}
@font-face{font-family:'Plex Sans Fallback';src:local('Arial'),local('Helvetica Neue'),local('Helvetica');size-adjust:100.5%;ascent-override:102%;descent-override:27.4%;line-gap-override:0%}
@font-face{font-family:'Plex Mono Fallback';src:local('Courier New'),local('Menlo');size-adjust:100%;ascent-override:102.5%;descent-override:27.5%;line-gap-override:0%}
</style>
<script>
/* theme before first paint: ?theme=light|dark for one view (the capture tool), else the stored choice under igneum-theme, else the system setting; ?motion=off marks the page data-motion=off so every .reveal is drawn at once (a headless render never scrolls) */
(function(){var t=null;try{t=new URLSearchParams(location.search).get("theme");}catch(e){}if(t!=="light"&&t!=="dark"){try{t=localStorage.getItem("igneum-theme");}catch(e){t=null;}}if(t!=="light"&&t!=="dark")t=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";document.documentElement.setAttribute("data-theme",t);try{if(new URLSearchParams(location.search).get("motion")==="off")document.documentElement.setAttribute("data-motion","off");}catch(e){}})();
</script>
<!-- head:end -->
<style>
/* reference apps (8 Oct 2026): the verify form, the proven headline, the step list; tokens and components are in /site.css */
.vform{display:grid;gap:10px;margin:0}
.vform label{font:500 var(--t-sm)/1.4 var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ash)}
.vform .row{display:flex;gap:10px;flex-wrap:wrap}.vform input{flex:1 1 320px;font-family:var(--f-mono);font-size:14px}
.vstatus{font:400 13px/1.5 var(--f-mono);color:var(--ash);min-height:20px}.vstatus[data-kind="ok"]{color:var(--ember-text)}.vstatus[data-kind="bad"]{color:var(--molten-text)}
.headline{background:var(--graphite);border:1px solid var(--line);border-radius:var(--r-card);padding:var(--s-5);margin:var(--s-5) 0 var(--s-4)}
.headline.bad{border-color:var(--molten)}
.headline .big{font:900 clamp(32px,6vw,56px)/1.05 var(--f-display);color:var(--bone);margin:10px 0 8px;overflow-wrap:anywhere}.headline .big span{font-size:.45em;color:var(--ash);font-weight:700}
.headline .kv{margin-top:var(--s-3)}.headline .kv small{color:var(--ash);display:block}.headline .mono{font-family:var(--f-mono);font-size:13px;overflow-wrap:anywhere}
.neg{border-left:2px solid var(--ember);background:var(--row);border-radius:0 var(--r-row) var(--r-row) 0;padding:12px var(--s-4);margin:0 0 var(--s-3);font-size:14px;color:var(--ink-2)}
.neg b{font:500 11px/1 var(--f-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ember-text);margin-right:8px}.neg.wrong{border-color:var(--molten)}.neg.wrong b{color:var(--molten-text)}
.neg small{display:block;color:var(--ash);font-family:var(--f-mono);font-size:12px;margin-top:4px;overflow-wrap:anywhere}
.steps{list-style:none;margin:0;padding:0;display:grid;gap:8px}
.steps li{display:grid;grid-template-columns:84px 1fr;gap:4px 12px;background:var(--row);border:1px solid var(--line);border-radius:var(--r-row);padding:10px 14px;font-size:14px;color:var(--ink-2)}
.steps li b{font:500 11px/1.6 var(--f-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ember-text)}.steps li.bad b{color:var(--molten-text)}
.steps li small{grid-column:2;color:var(--ash);font-family:var(--f-mono);font-size:12px;overflow-wrap:anywhere}
.why{border:1px solid var(--line-2);border-radius:var(--r-card);padding:var(--s-4) var(--s-5);background:var(--obsidian);margin:var(--s-5) 0}
.why p{margin:0;font-size:15px;color:var(--ink-2)}.why p+p{margin-top:8px}
.meta{display:grid;gap:6px;font:400 13px/1.6 var(--f-mono);color:var(--ash);margin:var(--s-4) 0 0}.meta code{overflow-wrap:anywhere}
@media (max-width:640px){.steps li{grid-template-columns:1fr}.steps li small{grid-column:1}}
</style>
</head>
<body>
<!-- nav:start -->
<a class="skip" href="#main">Skip to content</a>
<nav class="nav" aria-label="Main" data-nav-version="5">
<div class="container nav-row">
<a href="/" class="logo" aria-label="Igneum home">
<svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg>
<span class="word">IGNEUM</span>
</a>
<span class="devnet" id="nav-devnet" title="igneum-devnet-3, chain id 4463. Coins have no value. The chain may reset."><span class="dot off" id="foot-dot"></span><span id="foot-state">devnet</span></span>
<div class="nav-groups" id="nav-groups" role="list">
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-mine" data-group="mine" aria-expanded="false" aria-controls="nav-panel-mine" aria-haspopup="true">Mine<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-network" data-group="network" data-active aria-expanded="false" aria-controls="nav-panel-network" aria-haspopup="true">Network<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-learn" data-group="learn" aria-expanded="false" aria-controls="nav-panel-learn" aria-haspopup="true">Learn<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
</div>
<div class="nav-controls">
<button class="btn icon-btn" type="button" data-theme-toggle aria-label="Switch to light theme"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg></button>
<a href="/download" class="btn primary small cta" data-nav="download">Download</a>
<button class="burger" id="nav-burger" type="button" aria-expanded="false" aria-controls="nav-sheet" aria-label="Open the menu" aria-haspopup="true"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path class="bars" d="M4 6h16M4 12h16M4 18h16"/><path class="cross" d="m6 6 12 12M6 18 18 6"/></svg></button>
</div>
</div>
<div class="panels" id="nav-panels">
<div class="panel-wrap container">
<div class="nav-panel" id="nav-panel-mine" data-group="mine" role="region" aria-labelledby="nav-btn-mine" hidden>
<div class="panel-list">
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-network" data-group="network" role="region" aria-labelledby="nav-btn-network" hidden>
<div class="panel-list">
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt" aria-current="page"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
<div class="panel-list">
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
</div>
</div>
</div>
</div>
<div class="nav-sheet" id="nav-sheet" role="dialog" aria-modal="true" aria-label="Menu" hidden>
<div class="sheet-scroll">
<div class="sheet-group"><div class="sheet-head">Mine</div>
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt" aria-current="page"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
</div>
<div class="sheet-foot">
<a href="/download" class="btn primary big" data-nav="download">Download</a>
<div class="sheet-social"><a href="https://discord.gg/igneum" target="_blank" rel="noopener">Discord</a><a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener">Source</a><a href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener">Reddit</a></div>
</div>
</div>
</div>
</nav>
<script>
(function(){
// the slim bar (site-ui-5, 7 Oct 2026): three group buttons open one panel each; the burger opens the sheet on phones.
// Keyboard: Enter or Space opens a group, Escape closes it and returns focus to its button, arrows move between the
// group buttons, Tab walks the open panel's links, a click outside closes. The theme toggle flips data-theme and stores it.
var n=document.querySelector('.nav');if(!n)return;
var btns=[].slice.call(n.querySelectorAll('.group-btn')),panels=[].slice.call(n.querySelectorAll('.nav-panel')),wrap=n.querySelector('.panels');
var burger=document.getElementById('nav-burger'),sheet=document.getElementById('nav-sheet');
function panelOf(g){return panels.filter(function(p){return p.getAttribute('data-group')===g;})[0];}
function closeAll(focusBack){var was=null;btns.forEach(function(b){if(b.getAttribute('aria-expanded')==='true')was=b;b.setAttribute('aria-expanded','false');});panels.forEach(function(p){p.hidden=true;});n.classList.remove('panel-open');if(focusBack&&was)was.focus();}
function open(g){closeAll(false);var b=btns.filter(function(x){return x.getAttribute('data-group')===g;})[0],p=panelOf(g);if(!b||!p)return;b.setAttribute('aria-expanded','true');p.hidden=false;n.classList.add('panel-open');anchor(b,p);}
// the panel sits under its button; a panel that would run past the container's right edge aligns its right edge to the button's right edge instead (never past the viewport)
function anchor(b,p){var pw=n.querySelector('.panel-wrap');if(!pw)return;var cr=pw.getBoundingClientRect(),br=b.getBoundingClientRect(),w=p.offsetWidth,left=br.left-cr.left;if(left+w>cr.width)left=(br.right-cr.left)-w;if(left<0)left=0;p.style.setProperty('--panel-left',Math.round(left)+'px');}
window.addEventListener('resize',function(){var o=btns.filter(function(x){return x.getAttribute('aria-expanded')==='true';})[0];if(o)anchor(o,panelOf(o.getAttribute('data-group')));}); btns.forEach(function(b,i){
b.addEventListener('click',function(){var on=b.getAttribute('aria-expanded')==='true';if(on)closeAll(false);else open(b.getAttribute('data-group'));});
b.addEventListener('keydown',function(e){
var k=e.key;
if(k==='ArrowRight'||k==='ArrowLeft'){e.preventDefault();var j=(i+(k==='ArrowRight'?1:btns.length-1))%btns.length;btns[j].focus();if(n.classList.contains('panel-open'))open(btns[j].getAttribute('data-group'));}
else if(k==='ArrowDown'){e.preventDefault();open(b.getAttribute('data-group'));var f=panelOf(b.getAttribute('data-group')).querySelector('a');if(f)f.focus();}
else if(k==='Escape'){closeAll(true);}
});
});
if(wrap)wrap.addEventListener('keydown',function(e){if(e.key==='Escape'){e.stopPropagation();closeAll(true);}});
document.addEventListener('click',function(e){if(!n.classList.contains('panel-open'))return;if(e.target.closest('.group-btn')||e.target.closest('.nav-panel'))return;closeAll(false);});
document.addEventListener('focusin',function(e){if(!n.classList.contains('panel-open'))return;if(n.contains(e.target))return;closeAll(false);});
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&n.classList.contains('panel-open'))closeAll(true);});
// the sheet on phones
function setSheet(o){if(!burger||!sheet)return;n.classList.toggle('open',o);sheet.hidden=!o;burger.setAttribute('aria-expanded',o?'true':'false');burger.setAttribute('aria-label',o?'Close the menu':'Open the menu');document.documentElement.classList.toggle('nav-locked',o);if(o){var f=sheet.querySelector('a');if(f)f.focus();}}
if(burger&&sheet){
burger.addEventListener('click',function(){setSheet(!n.classList.contains('open'));});
sheet.addEventListener('click',function(e){if(e.target.closest('a'))setSheet(false);});
sheet.addEventListener('keydown',function(e){
if(e.key==='Escape'){setSheet(false);burger.focus();return;}
if(e.key!=='Tab')return;var f=sheet.querySelectorAll('a,button');if(!f.length)return;var first=f[0],last=f[f.length-1];
if(e.shiftKey&&document.activeElement===first){e.preventDefault();burger.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();burger.focus();}
});
burger.addEventListener('keydown',function(e){if(e.key==='Tab'&&!e.shiftKey&&n.classList.contains('open')){e.preventDefault();var f=sheet.querySelector('a');if(f)f.focus();}});
var mq=window.matchMedia('(min-width:861px)');if(mq.addEventListener)mq.addEventListener('change',function(){setSheet(false);});
}
// the theme toggle
var t=n.querySelector('[data-theme-toggle]');
function paintToggle(){var dark=document.documentElement.getAttribute('data-theme')!=='light';t.setAttribute('aria-label',dark?'Switch to light theme':'Switch to dark theme');t.innerHTML=dark?'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg>':'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.7 13.3A8.5 8.5 0 0 1 10.7 3.3 8.5 8.5 0 1 0 20.7 13.3Z"/></svg>';}
if(t){t.addEventListener('click',function(){var v=document.documentElement.getAttribute('data-theme')==='light'?'dark':'light';document.documentElement.setAttribute('data-theme',v);try{localStorage.setItem('igneum-theme',v);}catch(e){}document.dispatchEvent(new CustomEvent('igneum-theme'));});paintToggle();document.addEventListener('igneum-theme',paintToggle);}
})();
</script>
<!-- nav:end -->
<main id="main" class="wrap">
<div class="head">
<div class="breadcrumb"><a href="/">Igneum</a><span>/</span><span>Payment receipt</span></div>
<div class="eyebrow"><span class="line"></span>Reference app 2 · Devnet 3, no value</div>
<h1>A receipt any third party re-verifies<span class="accent">.</span></h1>
</div>
<p class="note">Paste a Devnet 3 transaction hash. This tab fetches the raw transaction, the including block's merkle path, every header up to the certified checkpoint and the certificate, then proves inclusion and finality itself. Download the receipt as JSON. Anyone checks it later with a one-file verifier, offline, with no node and no network.</p>
<div class="why">
<p><b>Why this only works on a proven chain.</b> A merchant's receipt is only worth something if the payment cannot be undone and the proof of that fits in a file: here the certificate is a weighted BLS signature by the miners over a checkpoint, and the block holding the payment hashes into that checkpoint's past. On a chain with probabilistic finality a receipt is a guess that ages well; here it is a fact that a file carries.</p>
<p>Source: <code>site/light/core.js</code> (the checks), <code>site/light/app.js</code> (this page), <code>tools/reference-apps/receipt/verify-receipt.src.mjs</code> (the one-file verifier, bundled to <code>/receipt/verify-receipt.js</code>). Test with the negative cases: <a href="/light/test">/light/test</a> in the browser, <code>tools/reference-apps/light-service/verify.test.mjs</code> under Node.</p>
</div>
<section class="card" aria-labelledby="verify">
<h2 id="verify">Get a receipt</h2>
<form class="vform" data-form="receipt" novalidate>
<label for="tx">Transaction hash</label>
<div class="row"><input id="tx" name="tx" inputmode="text" autocomplete="off" spellcheck="false" placeholder="0x…" pattern="^0x[0-9a-fA-F]{64}$"><button class="btn primary" type="submit">Prove it in this tab</button></div>
<div class="vstatus" data-status role="status" aria-live="polite">Devnet 3, no value. A transaction is final once a certified checkpoint has it in its past, about 30 to 90 s after it executes.</div>
</form>
<div data-result></div>
</section>
<h2 id="offline">Re-verify offline</h2>
<p class="note">The verifier is one file of plain JavaScript with the hashing and curve code inside it: <a href="/receipt/verify-receipt.js" download>verify-receipt.js</a>. It runs under Node 18 or later (<code>node verify-receipt.js receipt.json</code>) and prints each check, then <code>VERIFIED</code> or <code>REFUSED</code> with the reason. It fetches nothing. Flip one byte of the file and it is refused: <code>node verify-receipt.js receipt.json --tamper</code> runs that case first so you see the refusal before the verdict.</p>
<h2 id="checked">What the receipt proves</h2>
<ol>
<li>The transaction hash is keccak256 of the raw signed transaction in the file, so the recipient, the amount and the data are the signed ones.</li>
<li>The transaction is a leaf of the including block's <code>hash_merkle_root</code> (BLAKE2b-256 keyed MerkleBranchHash up the path).</li>
<li>Every header from the including block to the certified checkpoint recomputes and links to the next by a direct parent.</li>
<li>The certificate over that checkpoint verifies: the aggregate BLS signature of the signers, the voter order, two thirds of active weight and 17/30 of total.</li>
</ol>
<p class="note">Not proven by the file: the execution result (status, gas) is carried as the node reported it and labelled so; the voter table with weights comes from the node (spec 10.1). Both are named on the result.</p>
<p class="asof">Devnet 3, no value. The chain may reset. A demonstration of the verification path, not a product.</p>
</main>
<script type="module" src="/light/app.js"></script>
<!-- footer:start -->
<footer class="site-footer">
<div class="container">
<div class="footer-grid">
<div class="footer-brand">
<a href="/" class="logo" aria-label="Igneum home"><svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg><span class="word">IGNEUM</span><span class="stop">.</span></a>
<p>Mined by GPUs. Proven by fire.</p>
<div class="social" aria-label="Igneum elsewhere">
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" aria-label="Igneum source: the spec, the vectors and the issues"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M8.6 17.6 3 12l5.6-5.6 1.4 1.4L5.8 12l4.2 4.2zm6.8 0-1.4-1.4L18.2 12 14 7.8l1.4-1.4L21 12z"></path></svg></a>
<a href="https://discord.gg/igneum" target="_blank" rel="noopener" aria-label="The Igneum Discord"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19.6 5.3A17 17 0 0 0 15.4 4l-.2.4a15.6 15.6 0 0 1 3.9 1.9 13.6 13.6 0 0 0-14.2 0A15.6 15.6 0 0 1 8.8 4.4L8.6 4a17 17 0 0 0-4.2 1.3C1.8 9.2 1.1 13 1.4 16.7a17.1 17.1 0 0 0 5.2 2.6l1.1-1.8a10.8 10.8 0 0 1-1.7-.8l.4-.3a12.2 12.2 0 0 0 11.2 0l.4.3-1.7.8 1.1 1.8a17 17 0 0 0 5.2-2.6c.4-4.3-.7-8-3-11.4zM8.7 14.4c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1zm6.6 0c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1z"></path></svg></a>
<a data-social="reddit" href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener" aria-label="Igneum on Reddit"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M22 12.1a2.2 2.2 0 0 0-3.7-1.6 10.8 10.8 0 0 0-5.8-1.8l1-4.6 3.2.7a1.5 1.5 0 1 0 .2-.9l-3.6-.8a.5.5 0 0 0-.5.4l-1.1 5.2a10.8 10.8 0 0 0-5.9 1.8A2.2 2.2 0 1 0 3.4 14a4.3 4.3 0 0 0 0 .7c0 3.4 3.9 6.1 8.6 6.1s8.6-2.7 8.6-6.1a4.3 4.3 0 0 0 0-.7 2.2 2.2 0 0 0 1.4-1.9zM7 13.6a1.5 1.5 0 1 1 1.5 1.5A1.5 1.5 0 0 1 7 13.6zm8.6 4.1a5.7 5.7 0 0 1-3.6 1.1 5.7 5.7 0 0 1-3.6-1.1.4.4 0 0 1 .6-.6 4.9 4.9 0 0 0 3 .9 4.9 4.9 0 0 0 3-.9.4.4 0 0 1 .6.6zm-.3-2.6a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5z"></path></svg></a>
</div>
<div class="footer-dl" aria-label="Download Ember"><a href="/download#windows"><span class="osmark mini" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Windows</a><a href="/download#mac"><span class="osmark mini" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>macOS</a><a href="/download#linux"><span class="osmark mini" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Linux</a><a href="/download#hive"><span class="osmark mini" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>HiveOS</a></div>
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" class="text-link">Specification and test vectors<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 18 18 6M6 6h12v12"/></svg></a>
</div>
<div class="footer-column"><h4>Run</h4><div>
<a href="/download">Download Ember</a>
<a href="/miner">The miner</a>
<a href="/app">The app</a>
<a href="/wallet">The wallet</a>
<a href="/miners">GPU bench table</a>
<a href="/dev-fee">The dev fee</a>
<a href="/faucet">Testnet faucet</a>
<a href="/metamask">Add Igneum to MetaMask</a>
</div></div>
<div class="footer-column"><h4>Read</h4><div>
<a href="/litepaper">Litepaper</a>
<a href="/evidence">Evidence</a>
<a href="/bench">Engineering log</a>
<a href="/ledger">Ledger: every criticism</a>
<a href="/claims">What Igneum does not claim</a>
<a href="/randomx">Igneum vs RandomX</a>
<a href="/provenance">Built on the shoulders</a>
</div></div>
<div class="footer-column"><h4>Explore</h4><div>
<a href="/live">Live devnet</a>
<a href="/explorer">Explorer</a>
<a href="/journey">Journey</a>
<a href="/download">Downloads: devnet build</a>
<a href="https://discord.gg/igneum" target="_blank" rel="noopener">Community Discord</a>
<a href="mailto:hello@igneum.network">hello@igneum.network</a>
<a href="https://git.igneum.network/igneum-network/spec/issues" rel="noopener">An issue on the spec</a>
</div></div>
</div>
<div class="footer-bottom">
<p>Igneum Labs LTD · Unit IH-00-01-01-OF-01, Level 01, Innovation One, Dubai International Financial Centre. Experimental software. Nothing on this site is an offer to sell anything.<br>Report a flaw by email or by an issue on the spec. Nobody from Igneum will ask for your seed. © 2026 Igneum · not an offer to sell anything</p>
<div class="theme-group" role="group" aria-label="Theme"><button type="button" data-theme-set="system" aria-pressed="true">System</button><button type="button" data-theme-set="light" aria-pressed="false">Light</button><button type="button" data-theme-set="dark" aria-pressed="false">Dark</button></div>
</div>
</div>
</footer>
<script>
(function(){
// theme control: stores light or dark under igneum-theme; system clears it and follows the OS. The head applied the value before paint.
var bs=document.querySelectorAll('[data-theme-set]');if(!bs.length)return;
var media=window.matchMedia('(prefers-color-scheme: light)');
function stored(){try{var t=localStorage.getItem('igneum-theme');return t==='light'||t==='dark'?t:'system';}catch(e){return 'system';}}
function apply(){var c=stored();document.documentElement.setAttribute('data-theme',c==='system'?(media.matches?'light':'dark'):c);bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});document.dispatchEvent(new CustomEvent('igneum-theme'));}
bs.forEach(function(b){b.addEventListener('click',function(){var v=b.getAttribute('data-theme-set');try{if(v==='system')localStorage.removeItem('igneum-theme');else localStorage.setItem('igneum-theme',v);}catch(e){}apply();});});
if(media.addEventListener)media.addEventListener('change',function(){if(stored()==='system')apply();});
document.addEventListener('igneum-theme',function(){var c=stored();bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});});
bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===stored()?'true':'false');});
// the network state word in the status bar: one read of the observer per page view
var fs=document.getElementById('foot-state'),fd=document.getElementById('foot-dot');if(fs&&fd){fetch('/api/live?window=30',{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){var on=d&&d.ok&&d.state&&!d.state.stale;fs.textContent=on?'devnet live':'devnet, observer stale';fd.className='dot '+(on?'live':'off');}).catch(function(){fs.textContent='devnet';});}
})();
</script>
<!-- footer:end -->
<script>
(function(){
// one request per button; the wallet shows the chain to the user and decides. No key, no seed, no signature here.
var CHAINS = {
testnet: { chainId: '0x116e', chainName: 'Igneum Testnet', nativeCurrency: { name: 'Igneum', symbol: 'IGN', decimals: 18 }, rpcUrls: ['https://rpc.testnet.igneum.network'], blockExplorerUrls: [] },
devnet: { chainId: '0x116f', chainName: 'Igneum Devnet (local node)', nativeCurrency: { name: 'Igneum', symbol: 'IGN', decimals: 18 }, rpcUrls: ['http://127.0.0.1:26790'], blockExplorerUrls: [] }
};
function status(name, text) { var el = document.querySelector('[data-status="' + name + '"]'); if (el) el.textContent = text; }
Array.prototype.forEach.call(document.querySelectorAll('[data-add]'), function (btn) {
btn.addEventListener('click', function () {
var name = btn.getAttribute('data-add'); var chain = CHAINS[name];
if (!window.ethereum || !window.ethereum.request) { status(name, 'No Ethereum wallet found in this browser. Install MetaMask, or add the network by hand (below).'); return; }
status(name, 'Waiting for the wallet...');
window.ethereum.request({ method: 'wallet_addEthereumChain', params: [chain] })
.then(function () { status(name, 'Added. The wallet is on ' + chain.chainName + ' (chain id ' + parseInt(chain.chainId, 16) + ').'); })
.catch(function (e) { status(name, e && e.code === 4001 ? 'Cancelled in the wallet.' : 'The wallet refused: ' + ((e && e.message) || 'unknown error') + '. Add it by hand (below).'); });
});
});
})();
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -92,6 +92,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -124,6 +127,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -10,6 +10,9 @@
<url><loc>https://igneum.network/ledger</loc><lastmod>2026-10-07</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
<url><loc>https://igneum.network/bench</loc><lastmod>2026-10-07</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
<url><loc>https://igneum.network/evidence</loc><lastmod>2026-10-07</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
<url><loc>https://igneum.network/light</loc><lastmod>2026-10-08</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
<url><loc>https://igneum.network/receipt</loc><lastmod>2026-10-08</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
<url><loc>https://igneum.network/oracle</loc><lastmod>2026-10-08</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
<url><loc>https://igneum.network/miners</loc><lastmod>2026-10-07</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
<url><loc>https://igneum.network/journey</loc><lastmod>2026-10-07</lastmod><changefreq>weekly</changefreq><priority>0.5</priority></url>
<url><loc>https://igneum.network/claims</loc><lastmod>2026-10-07</lastmod><changefreq>monthly</changefreq><priority>0.5</priority></url>

View file

@ -124,6 +124,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
@ -156,6 +159,9 @@
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
<a href="/light" data-nav="light"><b>Light wallet</b><span>A balance your browser proves, trusting no node.</span></a>
<a href="/receipt" data-nav="receipt"><b>Payment receipt</b><span>A receipt any third party re-verifies offline.</span></a>
<a href="/oracle" data-nav="oracle"><b>State oracle on Sepolia</b><span>Devnet 3 balances read from another chain.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>

View file

@ -14,7 +14,7 @@ import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const GROUPS = {
mine: ['/miner', '/download', '/app', '/wallet', '/miners', '/dev-fee', '/faucet', '/metamask'],
network: ['/live', '/explorer', '/journey', '/bench', '/evidence'],
network: ['/live', '/explorer', '/journey', '/bench', '/evidence', '/light', '/receipt', '/oracle'],
learn: ['/litepaper', '/income', '/ledger', '/claims', '/randomx', '/provenance'],
};
const ROUTES = Object.values(GROUPS).flat();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
node_modules/

View file

@ -0,0 +1,43 @@
{
"name": "igneum-light-service",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "igneum-light-service",
"version": "0.1.0",
"dependencies": {
"@noble/curves": "2.4.0",
"@noble/hashes": "2.4.0"
}
},
"node_modules/@noble/curves": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.4.0.tgz",
"integrity": "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "2.4.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
"integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
}
}
}

View file

@ -0,0 +1,8 @@
{
"name": "igneum-light-service",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Igneum reference apps: the read service behind rpc.devnet.igneum.network/light (runs on build-1)",
"dependencies": { "@noble/curves": "2.4.0", "@noble/hashes": "2.4.0" }
}

View file

@ -0,0 +1,265 @@
// Igneum reference apps, the read service (8 October 2026). Runs on build-1 at 127.0.0.1:26890 behind
// https://rpc.devnet.igneum.network/light/ (the build-server lane's Caddy route, prefix stripped). GET only, CORS open,
// answers JSON that a browser verifies by itself with site/light/core.js: nothing here is a verdict, everything is data
// the page recomputes. Reads a Devnet 3 node that executes (the exec JSON-RPC for records, receipts and eth_getProof;
// the wRPC JSON websocket for block bodies and headers).
//
// /checkpoint the latest certified checkpoint (proxied from the site's /api/checkpoint?source=dn3)
// /balance?address=0x..&checkpoint=<hash>&index=<n>
// the newest paid segment record under that checkpoint, the carrier block's coinbase and
// merkle path, the header path carrier..checkpoint, and eth_getProof at the record's block
// /receipt?tx=0x..&checkpoint=<hash>&index=<n>
// the raw transaction, its including block's merkle path, the header path to the checkpoint
// /health
//
// Env: LIGHT_PORT (26890), EXEC_RPC (http://127.0.0.1:26881), WRPC (ws://127.0.0.1:28880), DATABASE_URL (the Devnet 3
// observer's Neon string: /checkpoint reads the dn3_ rows directly), else CHECKPOINT_URL (the site's API). Zero npm dependencies beyond @noble/hashes
// (blake2b, keccak) installed beside this file on the box.
import { createServer } from 'node:http';
import { blake2b } from '@noble/hashes/blake2.js';
import { keccak_256 } from '@noble/hashes/sha3.js';
import { coinbaseTxHash, merkleRoot, merklePath, hexToBytes, bytesToHex, segmentRecordsOf } from '../../../site/light/core.js';
import { readCheckpoint, neon } from '../../../site/api/checkpoint.mjs';
const PORT = Number(process.env.LIGHT_PORT || 26890);
const EXEC = process.env.EXEC_RPC || 'http://127.0.0.1:26881';
const WRPC = process.env.WRPC || 'ws://127.0.0.1:28880';
const CHECKPOINT_URL = process.env.CHECKPOINT_URL || 'https://igneum.network/api/checkpoint?source=dn3';
const SEGMENT_BLOCKS = 8;
const strip = s => String(s).replace(/^0x/i, '').toLowerCase();
const log = (...a) => console.log(new Date().toISOString(), ...a);
// ---- the exec JSON-RPC -------------------------------------------------------------------------------------------
async function exec(method, params) {
const r = await fetch(EXEC, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(20000) });
const j = await r.json();
if (j.error) throw new Error(`${method}: ${j.error.message || JSON.stringify(j.error)}`);
return j.result;
}
// ---- the wRPC JSON websocket, one connection, reopened on loss; u64 nonces kept as strings ---------------------------
let ws = null, wsOpen = null, nextId = 1; const pending = new Map();
function wrpcConnect() {
if (wsOpen) return wsOpen;
wsOpen = new Promise((resolve, reject) => {
const s = new WebSocket(WRPC);
s.addEventListener('open', () => { ws = s; resolve(s); });
s.addEventListener('error', e => { wsOpen = null; reject(new Error('wrpc: ' + (e.message || 'socket error'))); });
s.addEventListener('close', () => { ws = null; wsOpen = null; for (const [, p] of pending) p.reject(new Error('wrpc: closed')); pending.clear(); });
s.addEventListener('message', e => {
let m; try { m = JSON.parse(String(e.data).replace(/"nonce":(\d+)/g, '"nonce":"$1"')); } catch { return; }
const p = pending.get(m.id); if (!p) return; pending.delete(m.id);
if (m.error) p.reject(new Error('wrpc ' + p.method + ': ' + (m.error.message || JSON.stringify(m.error)))); else p.resolve(m.params);
});
});
return wsOpen;
}
async function wrpc(method, params) {
const s = await wrpcConnect();
const id = nextId++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject, method });
s.send(JSON.stringify({ id, method, params }));
setTimeout(() => { if (pending.has(id)) { pending.delete(id); reject(new Error('wrpc ' + method + ': timeout')); } }, 20000);
});
}
// ---- headers: the verifier's shape (site/verify/core.js headerHash), cached by hash ---------------------------------
const headerCache = new Map();
function shapeHeader(h) {
return {
hash: h.hash, version: h.version, parents_by_level: h.parentsByLevel, hash_merkle_root: h.hashMerkleRoot, accepted_id_merkle_root: h.acceptedIdMerkleRoot,
utxo_commitment: h.utxoCommitment, timestamp: String(h.timestamp), bits: h.bits, nonce: String(h.nonce), daa_score: String(h.daaScore),
blue_work: h.blueWork, blue_score: String(h.blueScore), pruning_point: h.pruningPoint, vote_key_hash: h.voteKeyHash,
};
}
async function header(hash) {
const k = strip(hash);
if (headerCache.has(k)) return headerCache.get(k);
const b = (await wrpc('getBlock', { hash: k, includeTransactions: false })).block;
const h = shapeHeader(b.header);
if (headerCache.size > 20000) headerCache.clear();
headerCache.set(k, h);
return h;
}
async function body(hash) {
return (await wrpc('getBlock', { hash: strip(hash), includeTransactions: true })).block;
}
// The header path from `from` (a block in the past of chain block `chainNumber`) up to the checkpoint (chain block
// `cpNumber`): [from, ..., chain block chainNumber, ..., checkpoint]; every next header names the previous as a direct parent.
async function headerPath(fromHash, chainNumber, cpNumber, cpHash) {
const chainHashes = [];
for (let n = chainNumber; n <= cpNumber; n++) {
const b = await exec('eth_getBlockByNumber', ['0x' + n.toString(16), false]);
if (!b) throw new Error(`chain block ${n} is not executed on this node`);
chainHashes.push(strip(b.hash));
}
if (chainHashes[chainHashes.length - 1] !== strip(cpHash)) throw new Error(`chain block ${cpNumber} on this node is ${chainHashes[chainHashes.length - 1].slice(0, 12)}, not the certified checkpoint ${strip(cpHash).slice(0, 12)}`);
// down from the chain block to `from` through direct parents (a merged block sits a few parents below)
const start = chainHashes[0];
let down = [];
if (strip(fromHash) !== start) {
const target = strip(fromHash);
const prev = new Map([[start, null]]);
let frontier = [start]; let found = false;
for (let depth = 0; depth < 80 && frontier.length && !found; depth++) {
const next = [];
for (const h of frontier) {
const hd = await header(h);
for (const p of (hd.parents_by_level[0] || [])) {
const ps = strip(p);
if (prev.has(ps)) continue;
prev.set(ps, h);
if (ps === target) { found = true; break; }
next.push(ps);
}
if (found) break;
}
frontier = next;
}
if (!found) throw new Error(`block ${target.slice(0, 12)} is not within 80 parents below chain block ${chainNumber}`);
for (let h = target; h !== null; h = prev.get(h)) down.push(h);
down = down.slice(0, -1); // ends with the chain block, which chainHashes carries
}
const order = [...down, ...chainHashes];
const out = [];
for (const h of order) out.push(await header(h));
for (let i = 1; i < out.length; i++) if (!(out[i].parents_by_level[0] || []).map(strip).includes(strip(out[i - 1].hash))) throw new Error(`header ${i} of the path does not name header ${i - 1} as a direct parent`);
return out;
}
// ---- the checkpoint -----------------------------------------------------------------------------------------------
async function checkpoint() {
if (process.env.DATABASE_URL) {
// the Devnet 3 observer's rows, read the way the site's /api/checkpoint reads them (DATABASE_URL from the box's observer env)
const cp = await readCheckpoint(neon(), 'dn3');
if (!cp) throw httpError(404, 'no certified Devnet 3 checkpoint stored yet');
return { ok: true, now: new Date().toISOString(), ...cp };
}
const r = await fetch(CHECKPOINT_URL, { signal: AbortSignal.timeout(15000), cache: 'no-store' });
const j = await r.json();
if (!j.ok) throw new Error('checkpoint: ' + (j.error || r.status));
return j;
}
async function chainNumberOf(hash) {
const b = await exec('eth_getBlockByHash', ['0x' + strip(hash), false]);
if (!b) throw new Error(`the checkpoint ${strip(hash).slice(0, 12)} is not an executed chain block on this node yet`);
return Number(b.number);
}
// ---- /balance -------------------------------------------------------------------------------------------------------
async function balance(q) {
const address = q.get('address') || '';
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) throw httpError(400, 'address: 0x and 40 hex digits');
const cpHash = q.get('checkpoint'), cpIndex = Number(q.get('index'));
if (!cpHash || !(cpIndex >= 0)) throw httpError(400, 'checkpoint and index are required (from /api/checkpoint?source=dn3)');
const cpNumber = await chainNumberOf(cpHash);
// the newest paid segment record carried at or below the checkpoint
let chosen = null;
for (let first = Math.floor(cpNumber / SEGMENT_BLOCKS) * SEGMENT_BLOCKS, k = 0; first >= 0 && k < 400; first -= SEGMENT_BLOCKS, k++) {
const s = await exec('igneum_getSegmentRecords', ['0x' + first.toString(16)]);
if (!s || !s.paid) continue;
const carrierNumber = Number(s.paid.carrierNumber);
if (carrierNumber > cpNumber) continue;
const c = (s.carried || []).find(x => x.valid && Number(x.carrierNumber) === carrierNumber && strip(x.keyHash) === strip(s.paid.keyHash));
if (!c) continue;
chosen = { first, last: Number(c.last), carrier: strip(c.carrier), carrierNumber, keyHash: c.keyHash, statement: c.statement, paidWei: s.paid.wei };
break;
}
if (!chosen) throw httpError(503, `no paid segment record carried at or below chain block ${cpNumber} within 400 segments`);
const carrier = await body(chosen.carrier);
const coinbase = carrier.transactions[0];
const evmHashes = (carrier.evmTransactions || []).map(raw => keccak_256(hexToBytes(strip(raw))));
const leaves = [coinbaseTxHash(coinbase, blake2b), ...evmHashes];
const root = bytesToHex(merkleRoot(leaves, blake2b));
if (root !== strip(carrier.header.hashMerkleRoot)) throw httpError(500, `the carrier's body root recomputes to ${root.slice(0, 12)}, the header says ${strip(carrier.header.hashMerkleRoot).slice(0, 12)}: the serialisation here is wrong, not the chain`);
const records = segmentRecordsOf(hexToBytes(strip(coinbase.payload)));
const rec = records.find(r => Number(r.first) === chosen.first && Number(r.last) === chosen.last);
if (!rec) throw httpError(500, `the carrier's coinbase holds ${records.length} segment record(s), none for segment ${chosen.first}`);
const headers = await headerPath(chosen.carrier, chosen.carrierNumber, cpNumber, cpHash);
const account = await exec('eth_getProof', [address, [], '0x' + chosen.last.toString(16)]);
const chainId = await exec('eth_chainId', []);
return {
ok: true, now: new Date().toISOString(), chain_id: 'igneum-devnet-3', address,
checkpoint: { hash: strip(cpHash), index: cpIndex, chain_block: cpNumber },
segment: { first: chosen.first, last: chosen.last, carrier: chosen.carrier, carrier_chain_block: chosen.carrierNumber, aggregator_key_hash: chosen.keyHash, paid_wei: chosen.paidWei, statement: chosen.statement },
headers,
carrier: { coinbase, evm_tx_hashes: evmHashes.map(bytesToHex), leaf_index: 0, merkle_siblings: siblings(leaves, 0), amount_wire_len: 8 },
segment_record_hex: bytesToHex(rec.bytes),
account: { ...account, evm_chain_id: Number(chainId) },
};
}
function siblings(leaves, index) {
// recompute the actual sibling hashes level by level (merklePath only gives the shape)
const H = d => blake2b(d, { dkLen: 32, key: new TextEncoder().encode('MerkleBranchHash') });
const Z = new Uint8Array(32); const out = []; let level = leaves.slice(); let i = index;
while (level.length > 1) {
out.push(bytesToHex(i % 2 === 0 ? (level[i + 1] || Z) : level[i - 1]));
const next = []; for (let k = 0; k < level.length; k += 2) { const l = level[k], r = level[k + 1] || Z; const m = new Uint8Array(64); m.set(l); m.set(r, 32); next.push(H(m)); }
level = next; i = i >> 1;
}
return out;
}
void merklePath;
// ---- /receipt -------------------------------------------------------------------------------------------------------
async function receipt(q) {
const tx = q.get('tx') || '';
if (!/^0x[0-9a-fA-F]{64}$/.test(tx)) throw httpError(400, 'tx: 0x and 64 hex digits');
const cpHash = q.get('checkpoint'), cpIndex = Number(q.get('index'));
if (!cpHash || !(cpIndex >= 0)) throw httpError(400, 'checkpoint and index are required (from /api/checkpoint?source=dn3)');
const t = await exec('eth_getTransactionByHash', [tx]);
if (!t) throw httpError(404, 'the node has not executed a transaction with that hash (unknown, or not yet in a block)');
const rcpt = await exec('eth_getTransactionReceipt', [tx]);
const cpNumber = await chainNumberOf(cpHash);
const chainNumber = Number(t.blockNumber);
if (chainNumber > cpNumber) throw httpError(409, `not final yet: executed at chain block ${chainNumber}, the latest certified checkpoint is chain block ${cpNumber}; try again in about ${chainNumber - cpNumber + 30} s`);
const including = strip(t.igneum.includingBlock);
const b = await body(including);
const raws = b.evmTransactions || [];
const idx = raws.findIndex(raw => bytesToHex(keccak_256(hexToBytes(strip(raw)))) === strip(tx));
if (idx < 0) throw httpError(500, 'the including block\'s body does not carry the transaction');
const leaves = [coinbaseTxHash(b.transactions[0], blake2b), ...raws.map(raw => keccak_256(hexToBytes(strip(raw))))];
const root = bytesToHex(merkleRoot(leaves, blake2b));
if (root !== strip(b.header.hashMerkleRoot)) throw httpError(500, `the including block's body root recomputes to ${root.slice(0, 12)}, the header says ${strip(b.header.hashMerkleRoot).slice(0, 12)}`);
const headers = await headerPath(including, chainNumber, cpNumber, cpHash);
return {
ok: true, now: new Date().toISOString(), chain_id: 'igneum-devnet-3', tx_hash: strip(tx), raw_tx_hex: strip(raws[idx]),
checkpoint: { hash: strip(cpHash), index: cpIndex, chain_block: cpNumber },
including_block: { header: headers[0], leaf_index: idx + 1, leaf_count: leaves.length, merkle_siblings: siblings(leaves, idx + 1) },
headers,
execution: rcpt ? { chain_block: chainNumber, chain_block_hash: strip(rcpt.blockHash), status: rcpt.status, gas_used: rcpt.gasUsed, from: rcpt.from, to: rcpt.to, contract_address: rcpt.contractAddress, logs: (rcpt.logs || []).length, effective_gas_price: rcpt.effectiveGasPrice, as_reported_by: 'the node (not proven here)' } : null,
tx_as_reported: { from: t.from, to: t.to, value: t.value, nonce: t.nonce, type: t.type, chain_id: t.chainId },
};
}
// ---- the server -----------------------------------------------------------------------------------------------------
function httpError(status, message) { const e = new Error(message); e.status = status; return e; }
const server = createServer(async (req, res) => {
const u = new URL(req.url || '/', 'http://x');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'content-type');
res.setHeader('Cache-Control', 'no-store');
if (req.method === 'OPTIONS') { res.writeHead(204); return res.end(); }
if (req.method !== 'GET') { res.writeHead(405, { 'content-type': 'application/json' }); return res.end('{"ok":false,"error":"GET only"}'); }
const t0 = Date.now();
try {
let out;
if (u.pathname === '/health') out = { ok: true, exec: EXEC, wrpc: WRPC, status: await exec('igneum_getExecStatus', []).catch(e => String(e.message)) };
else if (u.pathname === '/checkpoint') out = await checkpoint();
else if (u.pathname === '/balance') out = await balance(u.searchParams);
else if (u.pathname === '/receipt') out = await receipt(u.searchParams);
else throw httpError(404, 'no such endpoint: /checkpoint, /balance, /receipt, /health');
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(out));
log(req.method, u.pathname, 200, Date.now() - t0, 'ms');
} catch (e) {
const status = e.status || 500;
res.writeHead(status, { 'content-type': 'application/json' });
res.end(JSON.stringify({ ok: false, error: String(e.message || e) }));
log(req.method, u.pathname + u.search, status, Date.now() - t0, 'ms', String(e.message || e).slice(0, 200));
}
});
server.listen(PORT, '127.0.0.1', () => log(`light service on 127.0.0.1:${PORT}, exec ${EXEC}, wrpc ${WRPC}`));

View file

@ -0,0 +1,64 @@
// The reference apps' checks against real Devnet 3 data: the genuine case verifies, every tampered case is refused.
// Known-failed first. Runs under Node 22 with the two noble libraries installed beside this file (npm install here):
// node verify.test.mjs [receipt.json] [balance.json] [checkpoint.json] default: ../fixtures/dn3-*.json
// Exits 1 on the first case that does not behave. The same core.js runs in the browser pages and in the one-file
// offline receipt verifier, so this is the test that proves their negative cases.
import { readFileSync } from 'node:fs';
import { blake2b } from '@noble/hashes/blake2.js';
import { keccak_256 } from '@noble/hashes/sha3.js';
import { bls12_381 } from '@noble/curves/bls12-381.js';
import { verifyReceipt, verifyBalance } from '../../../site/light/core.js';
const deps = { blake2b, bls: bls12_381, keccak: keccak_256 };
const here = new URL('.', import.meta.url).pathname;
const load = p => JSON.parse(readFileSync(p, 'utf8'));
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); };
let failed = 0;
function expect(name, result, want) {
const ok = result.verified === want;
const line = `${ok ? 'ok ' : 'FAIL'} ${want ? 'verifies' : 'refused '} ${name}${result.verified ? '' : ' :: ' + result.reason}`;
console.log(line); if (!ok) failed++;
}
const receiptPath = process.argv[2] || here + '../fixtures/dn3-receipt.json';
const receipt = load(receiptPath);
console.log(`receipt ${receipt.tx_hash.slice(0, 12)} on ${receipt.chain_id}, checkpoint ${receipt.checkpoint.index}, ${receipt.headers.length} headers`);
// the known-failed cases first
{ const d = clone(receipt); d.raw_tx_hex = flipHex(d.raw_tx_hex, 40); expect('raw transaction altered by one nibble', verifyReceipt(d, deps), false); }
{ const d = clone(receipt); d.tx_hash = flipHex(d.tx_hash, 10); expect('transaction hash altered', verifyReceipt(d, deps), false); }
{ const d = clone(receipt); d.including_block.leaf_index = d.including_block.leaf_index === 1 ? 2 : 1; expect('merkle leaf index moved', verifyReceipt(d, deps), false); }
{ const d = clone(receipt); d.including_block.merkle_siblings[0] = flipHex(d.including_block.merkle_siblings[0], 3); expect('a merkle sibling altered', verifyReceipt(d, deps), false); }
{ const d = clone(receipt); if (d.headers.length > 2) { d.headers.splice(Math.floor(d.headers.length / 2), 1); expect('a header removed from the path', verifyReceipt(d, deps), false); } }
{ const d = clone(receipt); const h = d.headers[Math.floor(d.headers.length / 2)]; h.nonce = String(BigInt(h.nonce) ^ 1n); expect('a header nonce altered', verifyReceipt(d, deps), false); }
{ const d = clone(receipt); d.headers[0].hash_merkle_root = flipHex(d.headers[0].hash_merkle_root, 5); expect('the including block\'s body root altered', verifyReceipt(d, deps), false); }
{ const d = clone(receipt); const c = d.checkpoint.certificate; c.certificate.aggregate_signature_hex = flipHex(c.certificate.aggregate_signature_hex, 20); expect('certificate signature altered', verifyReceipt(d, deps), false); }
{ const d = clone(receipt); d.checkpoint.hash = flipHex(d.checkpoint.hash, 60); expect('receipt points at another checkpoint than its certificate', verifyReceipt(d, deps), false); }
{ const d = clone(receipt); const c = d.checkpoint.certificate; const bm = parseInt(c.certificate.bitmap_hex.slice(0, 2), 16); let p = 0; while (!(bm & (1 << p))) p++; c.certificate.bitmap_hex = (bm & ~(1 << p)).toString(16).padStart(2, '0') + c.certificate.bitmap_hex.slice(2); expect(`a signer dropped from the certificate bitmap (position ${p})`, verifyReceipt(d, deps), false); }
// then the genuine one
const r = verifyReceipt(receipt, deps);
expect(`genuine receipt (${r.ms} ms)`, r, true);
if (r.verified) console.log(` ${r.tx.type === 2 ? 'EIP-1559' : 'type ' + r.tx.type} tx to ${r.tx.to}, value ${r.tx.value} wei, in block ${r.block.slice(0, 12)} at DAA ${r.block_daa}, ${r.headers} headers to checkpoint ${r.checkpoint}`);
const balancePath = process.argv[3] || here + '../fixtures/dn3-balance.json';
let balance = null; try { balance = load(balancePath); } catch { console.log('no balance fixture yet (' + balancePath + '), skipping the balance cases'); }
if (balance) {
const cp = balance.checkpoint_certificate || load(process.argv[4] || here + '../fixtures/dn3-checkpoint.json');
console.log(`balance of ${balance.address} at chain block ${balance.segment.last}, checkpoint ${balance.checkpoint.index}, ${balance.headers.length} headers`);
{ const d = clone(balance); d.account.accountProof[d.account.accountProof.length - 1] = flipHex(d.account.accountProof[d.account.accountProof.length - 1], 30); expect('account proof: a node altered', verifyBalance(cp, d, deps), false); }
{ const d = clone(balance); d.account.balance = '0x' + (BigInt(d.account.balance) + 1n).toString(16); expect('reported balance raised by one wei', verifyBalance(cp, d, deps), false); }
{ const d = clone(balance); d.account.stateRoot = flipHex(d.account.stateRoot, 8); expect('state root altered', verifyBalance(cp, d, deps), false); }
{ const d = clone(balance); d.segment_record_hex = flipHex(d.segment_record_hex, 2 * (2 + 8 + 8 + 32 + 48 + 20 + 148) + 3); expect('post_root altered inside the segment record', verifyBalance(cp, d, deps), false); }
{ const d = clone(balance); d.segment_record_hex = flipHex(d.segment_record_hex, d.segment_record_hex.length - 10); expect('aggregator signature altered', verifyBalance(cp, d, deps), false); }
{ const d = clone(balance); d.carrier.coinbase.payload = flipHex(d.carrier.coinbase.payload, 30); expect('coinbase payload altered', verifyBalance(cp, d, deps), false); }
{ const d = clone(balance); d.carrier.merkle_siblings[0] = flipHex(d.carrier.merkle_siblings[0], 1); expect('a merkle sibling altered', verifyBalance(cp, d, deps), false); }
{ const d = clone(balance); if (d.headers.length > 2) { d.headers.splice(1, 1); expect('a header removed from the path', verifyBalance(cp, d, deps), false); } }
{ const d = clone(balance); d.checkpoint.hash = flipHex(d.checkpoint.hash, 1); expect('proof names another checkpoint than the certificate', verifyBalance(cp, d, deps), false); }
{ const c = clone(cp); c.certificate.aggregate_signature_hex = flipHex(c.certificate.aggregate_signature_hex, 20); expect('certificate signature altered', verifyBalance(c, balance, deps), false); }
{ const d = clone(balance); d.address = '0x' + '11'.repeat(20); expect('another address with this proof', verifyBalance(cp, d, deps), false); }
const b = verifyBalance(cp, balance, deps);
expect(`genuine balance (${b.ms} ms)`, b, true);
if (b.verified) { console.log(` ${b.balance_wei} wei at chain block ${b.block}, ${b.headers} headers, ${b.aggregator}`); for (const s of b.steps) console.log(' ' + (s.ok ? 'ok ' : 'no ') + s.name + ': ' + s.detail); }
}
console.log(failed ? `FAILED ${failed}` : 'RESULT every case behaved');
process.exit(failed ? 1 : 0);

View file

@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
// Keyed BLAKE2b-256 through the EIP-152 compression precompile at address 0x09.
// The precompile input is 213 bytes: rounds (u32 big-endian), h (8 u64 little-endian),
// m (128 bytes), t (2 u64 little-endian), f (one byte). It returns the new h (64 bytes).
// Parameter block: h0 ^= 0x01010000 ^ (keylen << 8) ^ outlen, with outlen 32.
// The key padded to 128 bytes is the first block with t = 128 and f = 0.
// The message follows in 128-byte blocks; the last block carries f = 1 and t = 128 + total bytes.
library Blake2b {
uint64 private constant IV0 = 0x6a09e667f3bcc908;
uint64 private constant IV1 = 0xbb67ae8584caa73b;
uint64 private constant IV2 = 0x3c6ef372fe94f82b;
uint64 private constant IV3 = 0xa54ff53a5f1d36f1;
uint64 private constant IV4 = 0x510e527fade682d1;
uint64 private constant IV5 = 0x9b05688c2b3e6c1f;
uint64 private constant IV6 = 0x1f83d9abfb41bd6b;
uint64 private constant IV7 = 0x5be0cd19137e2179;
function swap64(uint64 v) internal pure returns (uint64 r) {
r = ((v & 0x00000000000000FF) << 56) | ((v & 0x000000000000FF00) << 40)
| ((v & 0x0000000000FF0000) << 24) | ((v & 0x00000000FF000000) << 8)
| ((v & 0x000000FF00000000) >> 8) | ((v & 0x0000FF0000000000) >> 24)
| ((v & 0x00FF000000000000) >> 40) | ((v & 0xFF00000000000000) >> 56);
}
// The 32-byte keyed digest of `data`. The key is at most 64 bytes and never empty here.
function hash256(bytes memory key, bytes memory data) internal view returns (bytes32 out) {
require(key.length > 0 && key.length <= 64, "blake2b: key length");
bytes memory buf = new bytes(213);
uint64[8] memory h = [IV0, IV1, IV2, IV3, IV4, IV5, IV6, IV7];
h[0] ^= uint64(0x01010000) ^ (uint64(key.length) << 8) ^ uint64(32);
assembly {
let p := add(buf, 32)
mstore8(add(p, 3), 12) // rounds, big-endian u32 = 12
for { let i := 0 } lt(i, 8) { i := add(i, 1) } {
let w := mload(add(h, mul(i, 32)))
// byte-swap the u64 into little-endian
let s := 0
for { let k := 0 } lt(k, 8) { k := add(k, 1) } {
s := or(shl(8, s), and(shr(mul(k, 8), w), 0xff))
}
mstore(add(p, add(4, mul(i, 8))), shl(192, s))
}
}
uint256 n = data.length;
// The key block
_zeroBlock(buf);
assembly { mcopy(add(buf, 100), add(key, 32), mload(key)) }
_compress(buf, 128, n == 0);
// The message blocks
uint256 done = 0;
while (done < n) {
uint256 take = n - done;
if (take > 128) take = 128;
if (take < 128) _zeroBlock(buf);
assembly { mcopy(add(buf, 100), add(add(data, 32), done), take) }
done += take;
_compress(buf, 128 + done, done == n);
}
assembly { out := mload(add(buf, 36)) }
}
function _zeroBlock(bytes memory buf) private pure {
assembly {
let m := add(buf, 100)
mstore(m, 0)
mstore(add(m, 32), 0)
mstore(add(m, 64), 0)
mstore(add(m, 96), 0)
}
}
function _compress(bytes memory buf, uint256 t, bool last) private view {
uint64 tle = swap64(uint64(t));
bool ok;
assembly {
let p := add(buf, 32)
mstore(add(p, 196), shl(192, tle)) // t0 little-endian, then zeros for t1 and f
if last { mstore8(add(p, 212), 1) }
ok := staticcall(gas(), 0x09, p, 213, add(p, 4), 64)
}
require(ok, "blake2b: precompile");
}
}

View file

@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
// The shared Devnet 3 certificate verifier on Sepolia. The oracle reads certified checkpoints from it.
interface IIgneumCertificateVerifier {
function chainId() external view returns (string memory);
function tableId() external view returns (bytes32);
function verifyCertificate(uint64 index, bytes32 checkpoint, bytes calldata bitmap, bytes calldata signature)
external
view
returns (bool ok, uint256 signedWeight, uint256 totalWeight);
function submitCertificate(uint64 index, bytes32 checkpoint, bytes calldata bitmap, bytes calldata signature) external;
function finalCheckpoint(uint64 index) external view returns (bytes32);
function isFinal(bytes32 checkpoint) external view returns (bool);
function verifyAccount(bytes32 stateRoot, address account, bytes[] calldata proof)
external
pure
returns (bool exists, uint256 nonce, uint256 balance, bytes32 storageRoot, bytes32 codeHash);
}

View file

@ -0,0 +1,170 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
// RLP reading and Merkle Patricia trie proofs in Ethereum's layout (keccak-keyed secure trie).
// A proof is the node list eth_getProof returns, root first, in path order. Every node is
// hashed and compared with the reference that led to it. Nodes shorter than 32 bytes may be
// embedded in their parent; those are walked in place.
library Mpt {
bytes32 internal constant EMPTY_ROOT = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421;
struct Item {
bool isList;
uint256 start; // position of the item's prefix
uint256 off; // position of the item's payload
uint256 len; // payload length
}
function item(bytes memory b, uint256 p) internal pure returns (Item memory it) {
require(p < b.length, "rlp: short");
uint8 x = uint8(b[p]);
it.start = p;
if (x < 0x80) {
it.off = p;
it.len = 1;
} else if (x < 0xb8) {
it.len = x - 0x80;
it.off = p + 1;
} else if (x < 0xc0) {
uint256 ll = x - 0xb7;
it.len = uintAt(b, p + 1, ll);
it.off = p + 1 + ll;
} else if (x < 0xf8) {
it.isList = true;
it.len = x - 0xc0;
it.off = p + 1;
} else {
uint256 ll = x - 0xf7;
it.isList = true;
it.len = uintAt(b, p + 1, ll);
it.off = p + 1 + ll;
}
require(it.off + it.len <= b.length, "rlp: overrun");
}
function uintAt(bytes memory b, uint256 p, uint256 n) internal pure returns (uint256 v) {
require(n <= 32 && p + n <= b.length, "rlp: bad length");
for (uint256 i = 0; i < n; i++) v = (v << 8) | uint8(b[p + i]);
}
function slice(bytes memory b, uint256 off, uint256 len) internal pure returns (bytes memory out) {
out = new bytes(len);
assembly { mcopy(add(out, 32), add(add(b, 32), off), len) }
}
function nibble(bytes32 key, uint256 i) private pure returns (uint8) {
uint8 x = uint8(key[i >> 1]);
return (i & 1) == 0 ? x >> 4 : x & 15;
}
// Walks the proof for a 32-byte (already hashed) key. Returns (found, value bytes).
// Reverts on any node that does not hash to its reference or is malformed.
function get(bytes32 root, bytes32 key, bytes[] calldata proof) internal pure returns (bool found, bytes memory value) {
if (proof.length == 0) {
require(root == EMPTY_ROOT, "mpt: no nodes for a non-empty root");
return (false, "");
}
bytes memory node = proof[0];
require(keccak256(node) == root, "mpt: the first node is not the root");
uint256 pi = 1;
uint256 pos = 0; // nibbles of the key consumed
uint256 np = 0; // position of the current node inside `node`
for (uint256 guard = 0; guard < 80; guard++) {
Item memory list = item(node, np);
require(list.isList, "mpt: node is not a list");
uint256[17] memory starts;
uint256 count = 0;
uint256 p = list.off;
while (p < list.off + list.len) {
require(count < 17, "mpt: too many items");
Item memory it = item(node, p);
starts[count++] = p;
p = it.off + it.len;
}
Item memory child;
if (count == 17) {
if (pos == 64) {
Item memory v = item(node, starts[16]);
if (v.len == 0) return (false, "");
return (true, slice(node, v.off, v.len));
}
child = item(node, starts[nibble(key, pos)]);
pos += 1;
} else if (count == 2) {
Item memory hp = item(node, starts[0]);
require(!hp.isList && hp.len > 0, "mpt: bad path");
uint8 flag = uint8(node[hp.off]) >> 4;
bool leaf = (flag & 2) != 0;
uint256 n = hp.len * 2 - ((flag & 1) != 0 ? 1 : 2);
for (uint256 i = 0; i < n; i++) {
uint256 k = i + ((flag & 1) != 0 ? 1 : 2);
uint8 x = uint8(node[hp.off + (k >> 1)]);
uint8 nib = (k & 1) == 0 ? x >> 4 : x & 15;
if (pos + i >= 64 || nib != nibble(key, pos + i)) return (false, ""); // the key diverges
}
pos += n;
child = item(node, starts[1]);
if (leaf) {
if (pos != 64) return (false, "");
return (true, slice(node, child.off, child.len));
}
} else {
revert("mpt: node with an odd item count");
}
if (child.isList) {
np = child.start; // an embedded node
continue;
}
if (child.len == 0) return (false, "");
require(child.len == 32, "mpt: bad reference");
bytes32 want;
assembly { want := mload(add(add(node, 32), mload(add(child, 64)))) }
require(pi < proof.length, "mpt: a referenced node is missing");
node = proof[pi++];
require(keccak256(node) == want, "mpt: node does not hash to its reference");
np = 0;
}
revert("mpt: path too deep");
}
function toUint(bytes memory b, uint256 off, uint256 len) internal pure returns (uint256 v) {
require(len <= 32, "rlp: integer too wide");
for (uint256 i = 0; i < len; i++) v = (v << 8) | uint8(b[off + i]);
}
function toBytes32(bytes memory b, uint256 off, uint256 len) internal pure returns (bytes32 v) {
require(len == 32, "rlp: not 32 bytes");
assembly { v := mload(add(add(b, 32), off)) }
}
// An eth_getProof account proof under `stateRoot`.
function account(bytes32 stateRoot, address a, bytes[] calldata proof)
internal
pure
returns (bool exists, uint256 nonce, uint256 balance, bytes32 storageRoot, bytes32 codeHash)
{
(bool found, bytes memory v) = get(stateRoot, keccak256(abi.encodePacked(a)), proof);
if (!found) return (false, 0, 0, bytes32(0), bytes32(0));
Item memory list = item(v, 0);
require(list.isList, "mpt: account is not a list");
Item memory f0 = item(v, list.off);
Item memory f1 = item(v, f0.off + f0.len);
Item memory f2 = item(v, f1.off + f1.len);
Item memory f3 = item(v, f2.off + f2.len);
require(f3.off + f3.len == list.off + list.len, "mpt: account has not four fields");
exists = true;
nonce = toUint(v, f0.off, f0.len);
balance = toUint(v, f1.off, f1.len);
storageRoot = toBytes32(v, f2.off, f2.len);
codeHash = toBytes32(v, f3.off, f3.len);
}
// An eth_getProof storage proof under `storageRoot`; a proven absence reads as zero.
function storageSlot(bytes32 storageRoot, bytes32 slot, bytes[] calldata proof) internal pure returns (bytes32) {
(bool found, bytes memory v) = get(storageRoot, keccak256(abi.encodePacked(slot)), proof);
if (!found) return bytes32(0);
Item memory it = item(v, 0);
require(!it.isList && it.off + it.len == v.length, "mpt: storage value is not a string");
return bytes32(toUint(v, it.off, it.len));
}
}

View file

@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {IIgneumCertificateVerifier} from "./IIgneumCertificateVerifier.sol";
import {Mpt} from "./Mpt.sol";
// A stand-in for the shared verifier until its Sepolia address lands. It records any certificate
// whose index and checkpoint are non-zero without checking the BLS signature. verifyAccount is real.
contract StubCertificateVerifier is IIgneumCertificateVerifier {
mapping(uint64 => bytes32) private _final;
mapping(bytes32 => bool) private _isFinal;
event Recorded(uint64 indexed index, bytes32 checkpoint);
function chainId() external pure returns (string memory) {
return "igneum-devnet-3";
}
function tableId() external pure returns (bytes32) {
return keccak256("stub: no voter table installed");
}
function verifyCertificate(uint64 index, bytes32 checkpoint, bytes calldata, bytes calldata)
external
pure
returns (bool ok, uint256 signedWeight, uint256 totalWeight)
{
ok = index != 0 && checkpoint != bytes32(0);
return (ok, ok ? 1 : 0, 1);
}
function submitCertificate(uint64 index, bytes32 checkpoint, bytes calldata, bytes calldata) external {
require(index != 0 && checkpoint != bytes32(0), "stub: index and checkpoint must be non-zero");
bytes32 have = _final[index];
require(have == bytes32(0) || have == checkpoint, "stub: index already has another checkpoint");
_final[index] = checkpoint;
_isFinal[checkpoint] = true;
emit Recorded(index, checkpoint);
}
function finalCheckpoint(uint64 index) external view returns (bytes32) {
return _final[index];
}
function isFinal(bytes32 checkpoint) external view returns (bool) {
return _isFinal[checkpoint];
}
function verifyAccount(bytes32 stateRoot, address account, bytes[] calldata proof)
external
pure
returns (bool exists, uint256 nonce, uint256 balance, bytes32 storageRoot, bytes32 codeHash)
{
return Mpt.account(stateRoot, account, proof);
}
}

View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Bundles the offline receipt verifier into one plain-JavaScript file (site/receipt/verify-receipt.js) with esbuild: the
# noble libraries inlined, Node built-ins external, no npm at run time. Runs on the build box (nothing heavy on the Mac):
# tools/reference-apps/receipt/bundle.sh from the worktree root; needs node and npx (esbuild is fetched by npx)
# The bundle is committed beside the pages: the site deploys from site/ alone and installs nothing.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
SRC=tools/reference-apps/receipt/verify-receipt.src.mjs
OUT=site/receipt/verify-receipt.js
mkdir -p site/receipt
( cd tools/reference-apps/light-service && [ -d node_modules/@noble/curves ] || npm install --no-audit --no-fund >/dev/null )
NODE_PATH=tools/reference-apps/light-service/node_modules npx --yes esbuild@0.24.2 "$SRC" --bundle --platform=node --format=cjs --target=node18 --legal-comments=inline \
--banner:js="#!/usr/bin/env node
// Igneum payment receipt verifier, one file, offline. Source: tools/reference-apps/receipt/verify-receipt.src.mjs and site/light/core.js
// (the same checks the browser page runs), bundled with @noble/hashes 2.4.0 and @noble/curves 2.4.0 (MIT). Usage: node verify-receipt.js receipt.json [--tamper]" \
--outfile="$OUT"
chmod +x "$OUT"
echo "bundled $OUT: $(wc -c < "$OUT") bytes, sha256 $(sha256sum "$OUT" | cut -c1-16)"

View file

@ -0,0 +1,33 @@
// Igneum payment receipt, the offline verifier (8 October 2026). This is the SOURCE; the one-file build at
// site/receipt/verify-receipt.js bundles it with @noble/hashes (BLAKE2b, keccak) and @noble/curves (BLS12-381) so the
// shipped file needs no npm and no network: `node verify-receipt.js receipt.json [--tamper]`. Build: bundle.sh (esbuild
// on the build box). The checks are site/light/core.js, the same file the browser page runs.
import { readFileSync } from 'node:fs';
import { blake2b } from '@noble/hashes/blake2.js';
import { keccak_256 } from '@noble/hashes/sha3.js';
import { bls12_381 } from '@noble/curves/bls12-381.js';
import { verifyReceipt, formatIgn } from '../../../site/light/core.js';
const args = process.argv.slice(2);
const file = args.find(a => !a.startsWith('--'));
if (!file) { console.error('usage: node verify-receipt.js receipt.json [--tamper] (Igneum receipt, format igneum-receipt-v1; verifies offline)'); process.exit(2); }
const receipt = JSON.parse(readFileSync(file, 'utf8'));
if (receipt.format !== 'igneum-receipt-v1') { console.error(`REFUSED: not an igneum-receipt-v1 file (format ${receipt.format})`); process.exit(1); }
const deps = { blake2b, bls: bls12_381, keccak: keccak_256 };
const print = r => { for (const s of r.steps) console.log(` ${s.ok ? 'ok ' : 'REFUSED'} ${s.name}\n ${s.detail}`); };
if (args.includes('--tamper')) {
// the known-failed case first: the same file with one nibble of the raw transaction altered must be refused
const bad = JSON.parse(JSON.stringify(receipt));
const h = bad.raw_tx_hex; const i = 40; bad.raw_tx_hex = h.slice(0, i) + (parseInt(h[i], 16) ^ 1).toString(16) + h.slice(i + 1);
const t = verifyReceipt(bad, deps);
console.log(`tampered copy (one nibble of the raw transaction): ${t.verified ? 'NOT REFUSED, this verifier is broken' : 'REFUSED'}${t.reason ? ' :: ' + t.reason : ''}`);
if (t.verified) process.exit(1);
}
const r = verifyReceipt(receipt, deps);
console.log(`receipt 0x${receipt.tx_hash} on ${receipt.chain_id} (Devnet 3, no value)`);
print(r);
if (!r.verified) { console.log(`REFUSED: ${r.reason}`); process.exit(1); }
const t = r.tx;
console.log(`VERIFIED in ${r.ms} ms: ${formatIgn(t.value)} IGN to ${t.to || 'contract creation'} (${t.value} wei), in block ${r.block.slice(0, 16)} at DAA ${r.block_daa} (${new Date(Number(r.block_time)).toISOString()}), final under checkpoint ${r.checkpoint}; ${r.headers} headers checked; ${r.certificate}.`);
console.log('As reported by the node, not proven by this file: from ' + (receipt.tx_as_reported && receipt.tx_as_reported.from) + (receipt.execution ? `, execution status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}` : '') + '. The voter table with weights came from the node (spec 10.1).');