Merge reference-apps cb7bf85d into master (gate: green on cb7bf85d, recorded by tools/ci/pre-push.sh; landed on the box mirror)
This commit is contained in:
commit
8d13005fd6
69 changed files with 10296 additions and 21 deletions
|
|
@ -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>
|
||||
|
|
@ -127,6 +130,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>
|
||||
|
|
|
|||
|
|
@ -142,6 +142,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>
|
||||
|
|
@ -181,6 +184,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>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,34 @@ function shape(row, source) {
|
|||
};
|
||||
}
|
||||
|
||||
/** The earliest certified Devnet 3 checkpoint whose chain block number is at least `number` (the reference apps' receipt:
|
||||
* the smallest proof that a block is final), from the dn3_ rows; null when no certificate reaches that block yet.
|
||||
* `chainNumberOf(hash)` resolves a checkpoint hash to its chain block number (the exec RPC); rows are tried newest first
|
||||
* until one falls below `number`, so the cost is a few lookups. */
|
||||
export async function earliestCheckpointAbove(sql, number, chainNumberOf, table = 'dn3_live_certificates') {
|
||||
const rows = await sql(`SELECT index, hash FROM ${table} ORDER BY index DESC LIMIT 400`).catch(() => []);
|
||||
let pick = null;
|
||||
for (const r of rows) {
|
||||
const n = await chainNumberOf(r.hash).catch(() => null);
|
||||
if (n === null) continue;
|
||||
if (n >= number) pick = r; else break;
|
||||
}
|
||||
if (!pick) return null;
|
||||
const full = await sql(`SELECT * FROM ${table} WHERE index = $1 LIMIT 1`, [Number(pick.index)]);
|
||||
return full[0] ? shape(full[0], 'dn3') : null;
|
||||
}
|
||||
|
||||
/** The latest certified checkpoint of `want` ('live', 'test' or 'dn3') through `sql`, shaped for the verifier, or null
|
||||
* (the reference apps' read service calls this off Vercel: 'dn3' names the dn3_ tables whatever LIVE_TABLE_PREFIX says). */
|
||||
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, `${tablePrefix()}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', '*');
|
||||
|
|
@ -93,7 +121,7 @@ export default async function handler(req, res) {
|
|||
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) });
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -149,6 +152,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>
|
||||
|
|
|
|||
|
|
@ -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" aria-current="page"><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>
|
||||
|
|
@ -154,6 +157,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" aria-current="page"><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>
|
||||
|
|
|
|||
|
|
@ -142,6 +142,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>
|
||||
|
|
@ -181,6 +184,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -154,6 +157,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>
|
||||
|
|
|
|||
|
|
@ -410,7 +410,9 @@ const PAGES = [['index.html', ''], ['download.html', 'download'], ['income.html'
|
|||
// the three) and /proving; the Explorer entry of the Network panel is the current one on all five
|
||||
['explorer.html', 'explorer'], ['block.html', 'explorer'], ['address.html', 'explorer'], ['tx.html', 'explorer'], ['proving.html', 'explorer'],
|
||||
// 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}`);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -153,6 +156,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -153,6 +156,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -143,6 +146,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -164,6 +167,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>
|
||||
|
|
|
|||
|
|
@ -141,6 +141,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>
|
||||
|
|
@ -180,6 +183,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -156,6 +159,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -154,6 +157,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -113,6 +116,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -145,6 +148,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -153,6 +156,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>
|
||||
|
|
@ -273,12 +279,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>Economy simulation: mining versus proving under stress</h3><p>Sim/economy: mining versus proving under stress, agent-based</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>Difficulty rule attacked seven ways</h3><p>Difficulty rule under attack: pool hopping, pulsed rental, timestamp stretching, short-lane oscillation, epoch games, polluted window, block flood</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>Timestamp attack on the difficulty rule fixed</h3><p>Difficulty rule: timestamp attack fixed , simulator regression, 3-node forger test</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>Devnet v4: nine branches merged into one node</h3><p>Devnet-v4 integration: nine branches merged, 3-node test network on the merged node, Windows cross-build</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>Generator v2 adopted: every hash does 128 distinct reads</h3><p>Generator version 2 adopted: exact load count, fresh-source loads, program acceptance; every vector re-cut, three workers re-checked, 20,000-program census, devnet-v4 binaries rebuilt</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 GPU proof of an Igneum block: 1.4 s on an RTX 5090</h3><p>Proving v0 on the RTX 5090: first GPU proof of an Igneum block</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>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></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>
|
||||
|
|
|
|||
|
|
@ -222,33 +222,33 @@
|
|||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Sim/economy: mining versus proving under stress, agent-based",
|
||||
"short": "Economy simulation: mining versus proving under stress"
|
||||
"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": "Difficulty rule under attack: pool hopping, pulsed rental, timestamp stretching, short-lane oscillation, epoch games, polluted window, block flood",
|
||||
"short": "Difficulty rule attacked seven ways"
|
||||
"text": "One-click Windows workers: what the Apple M5 Max could measure",
|
||||
"short": "One-click Windows workers"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Difficulty rule: timestamp attack fixed , simulator regression, 3-node forger test",
|
||||
"short": "Timestamp attack on the difficulty rule fixed"
|
||||
"text": "First hourly program swap on the live devnet: compile-ahead, no pause, two cards",
|
||||
"short": "First live hourly swap: no pause on Mac, NVIDIA or AMD"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Devnet-v4 integration: nine branches merged, 3-node test network on the merged node, Windows cross-build",
|
||||
"short": "Devnet v4: nine branches merged into one node"
|
||||
"text": "Proving: devnet v4 shards on the Apple M5 Max CPU, loaded machine",
|
||||
"short": "Proving: devnet v4 shards on the Apple M5 Max CPU, loaded machine"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Generator version 2 adopted: exact load count, fresh-source loads, program acceptance; every vector re-cut, three workers re-checked, 20,000-program census, devnet-v4 binaries rebuilt",
|
||||
"short": "Generator v2 adopted: every hash does 128 distinct reads"
|
||||
"text": "First finality lock on the live devnet: checkpoint 242 at 77.4% of all weight, two hours after genesis",
|
||||
"short": "First live finality lock: 77.4% of weight, 17 voters"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Proving v0 on the RTX 5090: first GPU proof of an Igneum block",
|
||||
"short": "First GPU proof of an Igneum block: 1.4 s on an RTX 5090"
|
||||
"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…"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
119
site/lc/app.js
Normal file
119
site/lc/app.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// Igneum reference apps, the browser driver for /light and /receipt (8 October 2026). Fetches data from the read
|
||||
// service behind rpc.devnet.igneum.network/light and verifies every byte in this tab with core.js. Libraries, pinned:
|
||||
// BLAKE2b and keccak from @noble/hashes 2.4.0, BLS12-381 from @noble/curves 2.4.0 (pure JavaScript, served by jsdelivr
|
||||
// as ES modules). Known-failed first: before the genuine result is shown, a copy of the same proof with one byte
|
||||
// altered is run through the same verifier and must read as refused.
|
||||
import { blake2b } from 'https://cdn.jsdelivr.net/npm/@noble/hashes@2.4.0/blake2.js/+esm';
|
||||
import { keccak_256 } from 'https://cdn.jsdelivr.net/npm/@noble/hashes@2.4.0/sha3.js/+esm';
|
||||
import { bls12_381 } from 'https://cdn.jsdelivr.net/npm/@noble/curves@2.4.0/bls12-381.js/+esm';
|
||||
import { verifyBalance, verifyReceipt, formatIgn } from './core.js';
|
||||
|
||||
export const LIBRARIES = { '@noble/hashes': '2.4.0', '@noble/curves': '2.4.0' };
|
||||
const deps = { blake2b, bls: bls12_381, keccak: keccak_256 };
|
||||
const q = new URLSearchParams(location.search);
|
||||
export const API = (q.get('api') || 'https://rpc.devnet.igneum.network/light').replace(/\/$/, '');
|
||||
const $ = s => document.querySelector(s);
|
||||
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const short = h => { const s = String(h).replace(/^0x/, ''); return s.slice(0, 8) + '…' + s.slice(-6); };
|
||||
const clone = x => JSON.parse(JSON.stringify(x));
|
||||
const flipHex = (s, at) => { const h = s.replace(/^0x/, ''); const i = Math.min(at, h.length - 1); const d = (parseInt(h[i], 16) ^ 1).toString(16); return (s.startsWith('0x') ? '0x' : '') + h.slice(0, i) + d + h.slice(i + 1); };
|
||||
|
||||
async function getJson(url) {
|
||||
const r = await fetch(url, { cache: 'no-store' });
|
||||
let j = null; try { j = await r.json(); } catch { j = null; }
|
||||
if (!j) throw new Error(`${url.replace(API, '')} answered ${r.status} with no JSON`);
|
||||
if (!j.ok) throw new Error(j.error || `${url.replace(API, '')} answered ${r.status}`);
|
||||
return j;
|
||||
}
|
||||
|
||||
function setStatus(text, kind = '') { const el = $('[data-status]'); if (!el) return; el.textContent = text; el.dataset.kind = kind; }
|
||||
function stepsHtml(result) {
|
||||
return '<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}`);
|
||||
// the service may answer with an earlier certificate (the first checkpoint above the carrier: the smallest proof); verified like any other
|
||||
if (proof.checkpoint_certificate) { Object.assign(cp, proof.checkpoint_certificate); }
|
||||
setStatus('verifying in this tab…', 'busy');
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
// known-failed first: the same proof with one byte of the last account-proof node altered
|
||||
const bad = clone(proof); const last = bad.account.accountProof.length - 1;
|
||||
bad.account.accountProof[last] = flipHex(bad.account.accountProof[last], 30);
|
||||
const neg = verifyBalance(cp, bad, deps);
|
||||
const res = verifyBalance(cp, proof, deps);
|
||||
window.__igneumLight = { cp, proof, neg, res };
|
||||
const top = res.verified
|
||||
? `<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}`);
|
||||
// the service may answer with an earlier certificate (the first checkpoint above the block: the smallest proof); it is verified like any other
|
||||
const receipt = { format: 'igneum-receipt-v1', issued: new Date().toISOString(), ...r, checkpoint: { ...r.checkpoint, certificate: r.checkpoint.certificate || cp } };
|
||||
delete receipt.ok; delete receipt.now;
|
||||
setStatus('verifying in this tab…', 'busy');
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
const bad = clone(receipt); bad.raw_tx_hex = flipHex(bad.raw_tx_hex, 40);
|
||||
const neg = verifyReceipt(bad, deps);
|
||||
const res = verifyReceipt(receipt, deps);
|
||||
window.__igneumReceipt = { receipt, neg, res };
|
||||
const t = res.tx || {};
|
||||
const when = new Date(Number(res.block_time || 0)).toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
||||
const top = res.verified
|
||||
? `<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="/lc/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(); }
|
||||
388
site/lc/core.js
Normal file
388
site/lc/core.js
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
// 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}`;
|
||||
});
|
||||
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)} headers, ${strip(proof.headers[0].hash).slice(0, 12)} up to ${strip(cp.hash).slice(0, 12)}`);
|
||||
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 over the record under the network\'s tag)', () => {
|
||||
if (!verifySegmentRecordSignature(cp.chain_id, record, bls)) throw new Error('the aggregator\'s signature does not verify');
|
||||
// the aggregator signs with a vote key; a prove-only key has no mining weight and is not in the voter table, which is
|
||||
// reported, not refused (the record's standing comes from the nodes' native check and the payout, not from weight)
|
||||
const kh = voteKeyHash(record.pubkey, blake2b);
|
||||
const voter = (cp.voters || []).find(v => (v.vote_key_hash || voteKeyHash(hexToBytes(v.pubkey_hex), blake2b)) === kh);
|
||||
const total = (cp.voters || []).reduce((a, v) => a + Number(v.weight), 0);
|
||||
return voter
|
||||
? `aggregator ${kh.slice(0, 12)}, a voter with weight ${voter.weight} of ${total} (${(100 * Number(voter.weight) / total).toFixed(2)}%)`
|
||||
: `aggregator ${kh.slice(0, 12)}, a prove-only key (not in the voter table of ${cp.voters.length}, no mining weight)`;
|
||||
});
|
||||
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');
|
||||
// the EVM chain id in the statement is the one in force at that block (Devnet 3: 4463 below its class v5 floor, 4464 above);
|
||||
// the record's binding to the chain is the signed message, which names the network by its string ("igneum-devnet-3")
|
||||
const ids = cp.chain_id === 'igneum-devnet-3' ? [4463, 4464] : [Number(proof.account.evm_chain_id)];
|
||||
if (!ids.includes(Number(st.chain_id))) throw new Error(`statement chain id ${st.chain_id} is not ${cp.chain_id}'s (${ids.join(' or ')})`);
|
||||
return `EVM 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}`;
|
||||
});
|
||||
let acct = null;
|
||||
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);
|
||||
acct = a;
|
||||
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: proof.headers.length, 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`;
|
||||
});
|
||||
let tx = null;
|
||||
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)}`);
|
||||
tx = parseTx(raw);
|
||||
return `${raw.length} bytes, type ${tx.type}, to ${tx.to || 'contract creation'}, value ${tx.value} wei, nonce ${tx.nonce}`;
|
||||
});
|
||||
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)} headers, ${strip(receipt.headers[0].hash).slice(0, 12)} up to ${strip(cp.hash).slice(0, 12)}`);
|
||||
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: receipt.headers.length, 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();
|
||||
86
site/lc/test.html
Normal file
86
site/lc/test.html
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<!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/lc/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: 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 && proof.checkpoint_certificate) Object.assign(cp, proof.checkpoint_certificate);
|
||||
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 (or added to a one-leaf body)', d => { if (d.carrier.merkle_siblings.length) d.carrier.merkle_siblings[0] = flipHex(d.carrier.merkle_siblings[0], 1); else d.carrier.merkle_siblings.push('00'.repeat(32)); }],
|
||||
['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.startsWith('ERROR') ? 1 : 0);
|
||||
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>
|
||||
5322
site/lc/verify-receipt.js
Normal file
5322
site/lc/verify-receipt.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
|
|
@ -157,6 +160,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 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>
|
||||
|
|
|
|||
362
site/light.html
Normal file
362
site/light.html
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
<!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}
|
||||
.kv>div{min-width:0;overflow-wrap:anywhere}.card .kv .mono{font-family:var(--f-mono);font-size:13px}
|
||||
@media (max-width:640px){.kv{grid-template-columns:1fr}.kv>div{padding:6px 0}.kv .k{padding-top:10px;border-top:1px solid var(--line)}.kv>div:not(.k){border-top:0}}
|
||||
.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 class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-build" data-group="build" aria-expanded="false" aria-controls="nav-panel-build" aria-haspopup="true">Build<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="/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 class="nav-panel" id="nav-panel-build" data-group="build" role="region" aria-labelledby="nav-btn-build" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/build" data-nav="build"><b>Build on Igneum</b><span>Chain ids, RPC, a contract in five minutes.</span></a>
|
||||
<a href="/faucet" data-nav="faucet"><b>Devnet faucet</b><span>10 IGN of devnet coin, once a day.</span></a>
|
||||
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
|
||||
<a href="/grants" data-nav="grants"><b>Grants</b><span>Tooling, apps, infrastructure, research.</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="/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-group"><div class="sheet-head">Build</div>
|
||||
<a href="/build" data-nav="build"><b>Build on Igneum</b><span>Chain ids, RPC, a contract in five minutes.</span></a>
|
||||
<a href="/faucet" data-nav="faucet"><b>Devnet faucet</b><span>10 IGN of devnet coin, once a day.</span></a>
|
||||
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
|
||||
<a href="/grants" data-nav="grants"><b>Grants</b><span>Tooling, apps, infrastructure, research.</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/lc/core.js</code> (the checks), <code>site/lc/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="/lc/test">/lc/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 under the network's tag, whether the key is a voter with weight or a prove-only key, and 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="/lc/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">Devnet 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="/build">Build on Igneum</a>
|
||||
<a href="/swap">Swap</a>
|
||||
<a href="/grants">Grants</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -210,6 +213,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -294,6 +297,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -155,6 +158,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -150,6 +153,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -154,6 +157,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>
|
||||
|
|
|
|||
361
site/oracle.html
Normal file
361
site/oracle.html
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
<!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}
|
||||
.kv>div{min-width:0;overflow-wrap:anywhere}.card .kv .mono{font-family:var(--f-mono);font-size:13px}
|
||||
@media (max-width:640px){.kv{grid-template-columns:1fr}.kv>div{padding:6px 0}.kv .k{padding-top:10px;border-top:1px solid var(--line)}.kv>div:not(.k){border-top:0}}
|
||||
.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 class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-build" data-group="build" aria-expanded="false" aria-controls="nav-panel-build" aria-haspopup="true">Build<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="/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 class="nav-panel" id="nav-panel-build" data-group="build" role="region" aria-labelledby="nav-btn-build" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/build" data-nav="build"><b>Build on Igneum</b><span>Chain ids, RPC, a contract in five minutes.</span></a>
|
||||
<a href="/faucet" data-nav="faucet"><b>Devnet faucet</b><span>10 IGN of devnet coin, once a day.</span></a>
|
||||
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
|
||||
<a href="/grants" data-nav="grants"><b>Grants</b><span>Tooling, apps, infrastructure, research.</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="/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-group"><div class="sheet-head">Build</div>
|
||||
<a href="/build" data-nav="build"><b>Build on Igneum</b><span>Chain ids, RPC, a contract in five minutes.</span></a>
|
||||
<a href="/faucet" data-nav="faucet"><b>Devnet faucet</b><span>10 IGN of devnet coin, once a day.</span></a>
|
||||
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
|
||||
<a href="/grants" data-nav="grants"><b>Grants</b><span>Tooling, apps, infrastructure, research.</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>0xefe9879de29c401eeff195d5bf71c86b9caaa1b2 <small>IgneumStateOracle, deployed 8 October 2026 (tx 0xb8650f5b…d644, block 11869608)</small></div>
|
||||
<div class="k">Verifier</div><div class="mono" data-verifier-address>0xa197ef31d5d5613125779179668e2482ac69a6f6 <small>StubCertificateVerifier, the stand-in: it records any well-formed certificate and does not check the BLS signature; the shared IgneumCertificateVerifier replaces it through setVerifier the day it lands</small></div>
|
||||
<div class="k">Chain</div><div>Sepolia, chain id 11155111; Devnet 3 (igneum-devnet-3), no value</div>
|
||||
<div class="k">First proven root</div><div class="mono">Devnet 3 chain block 26631, post_root 0xf567a84f…a82fd, stored 8 October 2026 (tx 0xf360e54c…224b, 404,376 gas); provenBalance read 92.029323 IGN for 0x636de2dc…44ea on Sepolia, equal to the Devnet 3 node's eth_getProof</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 while the stand-in verifier is installed no certificate signature is checked on chain either; the voter table the shared verifier holds is installed by its deployer. Gas on Sepolia, measured 8 October 2026: a 62-header path 13.3 million (about 215,000 per header, 15 precompile blocks each), a 10-header path about 2.2 million, the state-root store 177,000, a balance read 153,000. 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">Devnet 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="/build">Build on Igneum</a>
|
||||
<a href="/swap">Swap</a>
|
||||
<a href="/grants">Grants</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -77,6 +80,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -154,6 +157,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>
|
||||
|
|
|
|||
|
|
@ -141,6 +141,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>
|
||||
|
|
@ -180,6 +183,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -153,6 +156,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>
|
||||
|
|
|
|||
363
site/receipt.html
Normal file
363
site/receipt.html
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
<!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}
|
||||
.kv>div{min-width:0;overflow-wrap:anywhere}.card .kv .mono{font-family:var(--f-mono);font-size:13px}
|
||||
@media (max-width:640px){.kv{grid-template-columns:1fr}.kv>div{padding:6px 0}.kv .k{padding-top:10px;border-top:1px solid var(--line)}.kv>div:not(.k){border-top:0}}
|
||||
.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 class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-build" data-group="build" aria-expanded="false" aria-controls="nav-panel-build" aria-haspopup="true">Build<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="/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 class="nav-panel" id="nav-panel-build" data-group="build" role="region" aria-labelledby="nav-btn-build" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/build" data-nav="build"><b>Build on Igneum</b><span>Chain ids, RPC, a contract in five minutes.</span></a>
|
||||
<a href="/faucet" data-nav="faucet"><b>Devnet faucet</b><span>10 IGN of devnet coin, once a day.</span></a>
|
||||
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
|
||||
<a href="/grants" data-nav="grants"><b>Grants</b><span>Tooling, apps, infrastructure, research.</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="/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-group"><div class="sheet-head">Build</div>
|
||||
<a href="/build" data-nav="build"><b>Build on Igneum</b><span>Chain ids, RPC, a contract in five minutes.</span></a>
|
||||
<a href="/faucet" data-nav="faucet"><b>Devnet faucet</b><span>10 IGN of devnet coin, once a day.</span></a>
|
||||
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
|
||||
<a href="/grants" data-nav="grants"><b>Grants</b><span>Tooling, apps, infrastructure, research.</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/lc/core.js</code> (the checks), <code>site/lc/app.js</code> (this page), <code>tools/reference-apps/receipt/verify-receipt.src.mjs</code> (the one-file verifier, bundled to <code>/lc/verify-receipt.js</code>). Test with the negative cases: <a href="/lc/test">/lc/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="/lc/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="/lc/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">Devnet 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="/build">Build on Igneum</a>
|
||||
<a href="/swap">Swap</a>
|
||||
<a href="/grants">Grants</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -131,6 +134,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>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,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>
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@ dt{color:var(--ash)}dd{margin:0;font-family:var(--f-mono);font-size:13px;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>
|
||||
|
|
@ -162,6 +165,9 @@ dt{color:var(--ash)}dd{margin:0;font-family:var(--f-mono);font-size:13px;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>
|
||||
|
|
|
|||
|
|
@ -142,6 +142,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>
|
||||
|
|
@ -181,6 +184,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -163,6 +166,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>
|
||||
|
|
|
|||
|
|
@ -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', '/metamask'],
|
||||
network: ['/live', '/explorer', '/journey', '/bench', '/evidence'],
|
||||
network: ['/live', '/explorer', '/journey', '/bench', '/evidence', '/light', '/receipt', '/oracle'],
|
||||
learn: ['/litepaper', '/income', '/ledger', '/claims', '/randomx', '/provenance'],
|
||||
build: ['/build', '/faucet', '/swap', '/grants'], // the builder programme (8 October 2026): the developer entry page, the Devnet 3 faucet, the swap, the grants
|
||||
};
|
||||
|
|
|
|||
1
tools/reference-apps/fixtures/dn3-balance.json
Normal file
1
tools/reference-apps/fixtures/dn3-balance.json
Normal file
File diff suppressed because one or more lines are too long
1
tools/reference-apps/fixtures/dn3-checkpoint.json
Normal file
1
tools/reference-apps/fixtures/dn3-checkpoint.json
Normal file
File diff suppressed because one or more lines are too long
1
tools/reference-apps/fixtures/dn3-receipt.json
Normal file
1
tools/reference-apps/fixtures/dn3-receipt.json
Normal file
File diff suppressed because one or more lines are too long
1
tools/reference-apps/light-service/.gitignore
vendored
Normal file
1
tools/reference-apps/light-service/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
||||
43
tools/reference-apps/light-service/package-lock.json
generated
Normal file
43
tools/reference-apps/light-service/package-lock.json
generated
Normal 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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
tools/reference-apps/light-service/package.json
Normal file
8
tools/reference-apps/light-service/package.json
Normal 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" }
|
||||
}
|
||||
276
tools/reference-apps/light-service/serve.mjs
Normal file
276
tools/reference-apps/light-service/serve.mjs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
// 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/lc/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/lc/core.js';
|
||||
import { readCheckpoint, earliestCheckpointAbove, 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}`);
|
||||
// the smallest proof: the earliest certified checkpoint at or above the carrier (the certificate used travels in the answer)
|
||||
let certificate = null;
|
||||
if (process.env.DATABASE_URL) certificate = await earliestCheckpointAbove(neon(), chosen.carrierNumber, chainNumberOf).catch(() => null);
|
||||
let cpN = cpNumber, cpH = cpHash, cpI = cpIndex;
|
||||
if (certificate) { cpH = certificate.hash; cpI = Number(certificate.index); cpN = await chainNumberOf(cpH); }
|
||||
const headers = await headerPath(chosen.carrier, chosen.carrierNumber, cpN, cpH);
|
||||
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(cpH), index: cpI, chain_block: cpN },
|
||||
checkpoint_certificate: certificate ? { ok: true, ...certificate } : undefined,
|
||||
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');
|
||||
let cpHash = q.get('checkpoint'), cpIndex = Number(q.get('index'));
|
||||
if (!cpHash || !(cpIndex >= 0)) throw httpError(400, 'checkpoint and index are required (from /checkpoint)');
|
||||
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 chainNumber = Number(t.blockNumber);
|
||||
// the smallest proof: the earliest certified checkpoint at or above the block (from the Devnet 3 observer's rows), else
|
||||
// the checkpoint the caller named; the certificate used is returned so the page verifies against that one
|
||||
let certificate = null;
|
||||
if (process.env.DATABASE_URL) certificate = await earliestCheckpointAbove(neon(), chainNumber, chainNumberOf).catch(() => null);
|
||||
if (certificate) { cpHash = certificate.hash; cpIndex = Number(certificate.index); }
|
||||
const cpNumber = await chainNumberOf(cpHash);
|
||||
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, certificate: certificate ? { ok: true, ...certificate } : undefined },
|
||||
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}`));
|
||||
64
tools/reference-apps/light-service/verify.test.mjs
Normal file
64
tools/reference-apps/light-service/verify.test.mjs
Normal 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/lc/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'); // the certificate the service answered with
|
||||
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); if (d.carrier.merkle_siblings.length) { d.carrier.merkle_siblings[0] = flipHex(d.carrier.merkle_siblings[0], 1); expect('a merkle sibling altered', verifyBalance(cp, d, deps), false); } else { d.carrier.merkle_siblings.push('00'.repeat(32)); expect('a merkle sibling added to a one-leaf body', 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);
|
||||
1
tools/reference-apps/oracle/.gitignore
vendored
Normal file
1
tools/reference-apps/oracle/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
||||
52
tools/reference-apps/oracle/README.md
Normal file
52
tools/reference-apps/oracle/README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Igneum Devnet 3 state oracle on Sepolia
|
||||
|
||||
A contract on Ethereum Sepolia that stores proven Devnet 3 state roots and lets other contracts read a proven
|
||||
Devnet 3 balance or storage slot. Certificates are read from the shared verifier (`IIgneumCertificateVerifier`);
|
||||
a `StubCertificateVerifier` stands in until the real one lands (`setVerifier`, deployer only, swaps it).
|
||||
|
||||
Addresses, transaction hashes, blocks and gas are in `deployment.json` (every write is appended under `writes`).
|
||||
|
||||
## What the oracle checks on chain
|
||||
|
||||
1. Every header in the path is hashed with keyed BLAKE2b-256 ("BlockHash") through the EIP-152 precompile and
|
||||
parsed out of the same bytes. Each next header must list the previous hash among its level-0 parents. The last
|
||||
hash must be the checkpoint the verifier holds for the given certificate index.
|
||||
2. The carrier's coinbase transaction is hashed ("TransactionHash") and the merkle path ("MerkleBranchHash", a
|
||||
missing right child is 32 zero bytes) must reach the carrier's hash_merkle_root, parsed from the header bytes.
|
||||
3. The coinbase payload is walked from the transaction bytes (so it is bound to the hash); the nested sections
|
||||
`IGNS || IGNP || IGNF` are taken from the end; the chosen 586-byte segment record gives the statement's
|
||||
number, block hash and post_root (number must equal the record's `last`, block hash must equal the record's
|
||||
block, chain id must be Devnet 3's 0x116f). The root is stored under the number with the certificate index.
|
||||
4. `provenBalance`, `provenAccount` and `provenStorage` verify eth_getProof account and storage proofs against
|
||||
the stored post_root with the contract's own RLP and keccak-keyed Merkle Patricia trie walk, and revert with a
|
||||
reason on any mismatch.
|
||||
|
||||
Not checked on chain in this slice (also stated by `trust()`): the aggregator's BLS signature over the segment
|
||||
record and the ZK proof behind it. Also not checked: the segment record's version field (the layout is fixed
|
||||
either way).
|
||||
|
||||
## Files
|
||||
|
||||
- `contracts/Blake2b.sol`: keyed BLAKE2b-256 over the precompile at 0x09.
|
||||
- `contracts/Mpt.sol`: RLP reading, MPT proofs, account and storage decoding.
|
||||
- `contracts/IIgneumCertificateVerifier.sol`, `contracts/StubCertificateVerifier.sol`, `contracts/IgneumStateOracle.sol`.
|
||||
- `compile.mjs`: solc-js (via IR, optimizer 200, cancun) to `artifacts/*.json`.
|
||||
- `deploy.mjs`: EIP-1559 deploy of the stub and the oracle, writes `deployment.json` (`--force`, `--oracle-only`).
|
||||
- `demo.mjs`: submits the certificate and the state root for `fixtures/dn3-balance.json` (or the synthetic vector
|
||||
when the fixture is absent) and prints the Sepolia-read balance beside the fixture's.
|
||||
- `test.mjs`: the vectors and negatives through eth_call. Part A is the receipt fixture (real headers, a real merkle
|
||||
path); part B is a seeded synthetic vector (segment record, coinbase, three-header path, account and storage
|
||||
proofs); part C is the balance fixture when it exists.
|
||||
- `lib.mjs`: the byte layouts, a JS mirror of the BLAKE2b driver (checked against @noble/hashes), the trie builder.
|
||||
|
||||
## Run
|
||||
|
||||
```
|
||||
node compile.mjs
|
||||
node deploy.mjs
|
||||
node test.mjs
|
||||
node demo.mjs
|
||||
```
|
||||
|
||||
The deployer key is read from `~/.config/igneum/sepolia-deployer`. Sepolia's gas schedule is repriced (a plain
|
||||
transfer estimates 12,000 gas), so the numbers in `deployment.json` are what this network charges.
|
||||
620
tools/reference-apps/oracle/artifacts/IgneumStateOracle.json
Normal file
620
tools/reference-apps/oracle/artifacts/IgneumStateOracle.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
35
tools/reference-apps/oracle/compile.mjs
Normal file
35
tools/reference-apps/oracle/compile.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Compiles the oracle contracts with solc-js and writes artifacts/<Name>.json ({abi, bytecode, deployedBytecode}).
|
||||
import solc from 'solc';
|
||||
import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const dir = path.join(here, 'contracts');
|
||||
const sources = {};
|
||||
for (const f of readdirSync(dir)) if (f.endsWith('.sol')) sources[f] = { content: readFileSync(path.join(dir, f), 'utf8') };
|
||||
const input = {
|
||||
language: 'Solidity',
|
||||
sources,
|
||||
settings: {
|
||||
optimizer: { enabled: true, runs: 200 },
|
||||
evmVersion: 'cancun',
|
||||
viaIR: true,
|
||||
outputSelection: { '*': { '*': ['abi', 'evm.bytecode.object', 'evm.deployedBytecode.object'] } },
|
||||
},
|
||||
};
|
||||
const out = JSON.parse(solc.compile(JSON.stringify(input)));
|
||||
let failed = false;
|
||||
for (const e of out.errors ?? []) { console.error(e.formattedMessage); if (e.severity === 'error') failed = true; }
|
||||
if (failed) process.exit(1);
|
||||
mkdirSync(path.join(here, 'artifacts'), { recursive: true });
|
||||
const want = ['IgneumStateOracle', 'StubCertificateVerifier'];
|
||||
for (const file of Object.keys(out.contracts)) {
|
||||
for (const [name, c] of Object.entries(out.contracts[file])) {
|
||||
if (!want.includes(name)) continue;
|
||||
const art = { name, solc: solc.version(), abi: c.abi, bytecode: '0x' + c.evm.bytecode.object, deployedBytecode: '0x' + c.evm.deployedBytecode.object };
|
||||
writeFileSync(path.join(here, 'artifacts', name + '.json'), JSON.stringify(art, null, 1) + '\n');
|
||||
console.log(name, 'creation bytes', c.evm.bytecode.object.length / 2, 'runtime bytes', c.evm.deployedBytecode.object.length / 2);
|
||||
}
|
||||
}
|
||||
console.log('solc', solc.version());
|
||||
85
tools/reference-apps/oracle/contracts/Blake2b.sol
Normal file
85
tools/reference-apps/oracle/contracts/Blake2b.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
292
tools/reference-apps/oracle/contracts/IgneumStateOracle.sol
Normal file
292
tools/reference-apps/oracle/contracts/IgneumStateOracle.sol
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.28;
|
||||
|
||||
import {IIgneumCertificateVerifier} from "./IIgneumCertificateVerifier.sol";
|
||||
import {Blake2b} from "./Blake2b.sol";
|
||||
import {Mpt} from "./Mpt.sol";
|
||||
|
||||
// Stores proven Igneum Devnet 3 state roots on Sepolia and answers balance and storage reads under them.
|
||||
//
|
||||
// A state root enters through submitStateRoot with:
|
||||
// headers the serialized headers from the carrier block up to a certified checkpoint, carrier first
|
||||
// coinbaseTx the carrier's serialized coinbase transaction
|
||||
// leafIndex, siblings the merkle path from the coinbase hash to the carrier's hash_merkle_root
|
||||
// recordIndex which segment record inside the coinbase payload's IGNS section to take
|
||||
// Every header is hashed with keyed BLAKE2b-256 ("BlockHash") on chain and parsed out of the same bytes.
|
||||
// Each next header must list the previous hash among its level-0 parents. The last hash must be the
|
||||
// checkpoint the verifier holds for the given certificate index. The coinbase hash ("TransactionHash")
|
||||
// must reach the carrier's hash_merkle_root through the path ("MerkleBranchHash"). The record's
|
||||
// statement then gives (number, block hash, post state root), which is stored under the number.
|
||||
contract IgneumStateOracle {
|
||||
struct Root {
|
||||
bytes32 postRoot;
|
||||
bytes32 blockHash;
|
||||
bytes32 carrier;
|
||||
uint64 certIndex;
|
||||
}
|
||||
|
||||
IIgneumCertificateVerifier public verifier;
|
||||
address public owner;
|
||||
uint64 public immutable evmChainId; // the statement's chain id, 0 to skip the check
|
||||
mapping(uint64 => Root) private _roots;
|
||||
|
||||
event StateRoot(uint64 indexed number, bytes32 postRoot, bytes32 blockHash, uint64 certIndex, bytes32 carrier);
|
||||
event VerifierSet(address verifier);
|
||||
|
||||
uint256 private constant RECORD_LEN = 586;
|
||||
|
||||
constructor(address verifier_, uint64 evmChainId_) {
|
||||
verifier = IIgneumCertificateVerifier(verifier_);
|
||||
owner = msg.sender;
|
||||
evmChainId = evmChainId_;
|
||||
}
|
||||
|
||||
function setVerifier(address v) external {
|
||||
require(msg.sender == owner, "oracle: owner only");
|
||||
verifier = IIgneumCertificateVerifier(v);
|
||||
emit VerifierSet(v);
|
||||
}
|
||||
|
||||
function trust() external pure returns (string memory) {
|
||||
return "Checked on chain: header hashes and parent links to a certified checkpoint, the coinbase merkle path, the segment record layout, and the account and storage proofs under post_root. Not checked on chain in this slice: the aggregator's BLS signature over the segment record and the ZK proof behind it.";
|
||||
}
|
||||
|
||||
// ---- hashes -------------------------------------------------------------------------------------
|
||||
|
||||
function headerHash(bytes memory header) public view returns (bytes32) {
|
||||
return Blake2b.hash256("BlockHash", header);
|
||||
}
|
||||
|
||||
function transactionHash(bytes memory tx_) public view returns (bytes32) {
|
||||
return Blake2b.hash256("TransactionHash", tx_);
|
||||
}
|
||||
|
||||
function merkleRoot(bytes32 leaf, uint256 index, bytes32[] calldata siblings) public view returns (bytes32 h) {
|
||||
h = leaf;
|
||||
for (uint256 i = 0; i < siblings.length; i++) {
|
||||
h = (index & 1) == 0
|
||||
? Blake2b.hash256("MerkleBranchHash", abi.encodePacked(h, siblings[i]))
|
||||
: Blake2b.hash256("MerkleBranchHash", abi.encodePacked(siblings[i], h));
|
||||
index >>= 1;
|
||||
}
|
||||
require(index == 0, "merkle: leaf index beyond the path");
|
||||
}
|
||||
|
||||
// ---- headers ------------------------------------------------------------------------------------
|
||||
|
||||
function le64(bytes memory b, uint256 off) private pure returns (uint64) {
|
||||
require(off + 8 <= b.length, "header: short");
|
||||
uint256 w;
|
||||
assembly { w := mload(add(add(b, 32), off)) }
|
||||
return Blake2b.swap64(uint64(w >> 192));
|
||||
}
|
||||
|
||||
function word(bytes memory b, uint256 off) private pure returns (bytes32 w) {
|
||||
require(off + 32 <= b.length, "header: short");
|
||||
assembly { w := mload(add(add(b, 32), off)) }
|
||||
}
|
||||
|
||||
// Parses the level-0 parents and hash_merkle_root out of a serialized header.
|
||||
function parseHeader(bytes memory h) public pure returns (uint256 parentsOff, uint256 parentCount, bytes32 merkle) {
|
||||
uint64 levels = le64(h, 2);
|
||||
require(levels >= 1, "header: no parent levels");
|
||||
uint256 off = 10;
|
||||
parentCount = le64(h, off);
|
||||
parentsOff = off + 8;
|
||||
off = parentsOff + 32 * parentCount;
|
||||
for (uint256 l = 1; l < levels; l++) {
|
||||
uint256 cnt = le64(h, off);
|
||||
off += 8 + 32 * cnt;
|
||||
}
|
||||
merkle = word(h, off);
|
||||
}
|
||||
|
||||
function listsParent(bytes memory h, uint256 parentsOff, uint256 parentCount, bytes32 x) private pure returns (bool) {
|
||||
for (uint256 i = 0; i < parentCount; i++) {
|
||||
if (word(h, parentsOff + 32 * i) == x) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hashes every header, checks each parent link, and that the last hash is `checkpoint`.
|
||||
// Returns the carrier's hash and hash_merkle_root.
|
||||
function checkHeaderPath(bytes[] calldata headers, bytes32 checkpoint) public view returns (bytes32 carrier, bytes32 merkle) {
|
||||
require(headers.length > 0, "headers: none");
|
||||
bytes32 prev;
|
||||
for (uint256 i = 0; i < headers.length; i++) {
|
||||
bytes memory h = headers[i];
|
||||
(uint256 pOff, uint256 pCount, bytes32 m) = parseHeader(h);
|
||||
if (i == 0) {
|
||||
merkle = m;
|
||||
} else {
|
||||
require(listsParent(h, pOff, pCount, prev), "headers: a header does not name the previous one as a parent");
|
||||
}
|
||||
prev = headerHash(h);
|
||||
if (i == 0) carrier = prev;
|
||||
}
|
||||
require(prev == checkpoint, "headers: the last header is not the certified checkpoint");
|
||||
}
|
||||
|
||||
// ---- the coinbase transaction and its segment record ---------------------------------------------
|
||||
|
||||
// Walks the serialized transaction to the payload. Amounts are 8 bytes on the wire.
|
||||
function payloadOf(bytes memory t) public pure returns (uint256 off, uint256 len) {
|
||||
uint16 version = uint16(le64(t, 0) & 0xffff);
|
||||
uint256 p = 2;
|
||||
uint256 nIn = le64(t, p);
|
||||
p += 8;
|
||||
for (uint256 i = 0; i < nIn; i++) {
|
||||
p += 36;
|
||||
uint256 sigLen = le64(t, p);
|
||||
p += 8 + sigLen;
|
||||
if (version < 1) p += 1;
|
||||
p += 8;
|
||||
if (version >= 1) p += 2;
|
||||
}
|
||||
uint256 nOut = le64(t, p);
|
||||
p += 8;
|
||||
for (uint256 i = 0; i < nOut; i++) {
|
||||
p += 8 + 2;
|
||||
uint256 spkLen = le64(t, p);
|
||||
p += 8 + spkLen;
|
||||
if (version >= 1) {
|
||||
require(p < t.length, "coinbase: short");
|
||||
if (uint8(t[p]) != 0) p += 34;
|
||||
p += 1;
|
||||
}
|
||||
}
|
||||
p += 8 + 20 + 8;
|
||||
len = le64(t, p);
|
||||
off = p + 8;
|
||||
require(off + len <= t.length, "coinbase: payload overruns");
|
||||
}
|
||||
|
||||
// The nested sections at the end of the extra data: items || len_le32 || TAG. Returns the IGNS items.
|
||||
function segmentSection(bytes memory t, uint256 payloadOff, uint256 payloadLen) public pure returns (uint256 off, uint256 len) {
|
||||
require(payloadLen >= 19, "coinbase: payload too short");
|
||||
uint256 scriptLen = uint8(t[payloadOff + 18]);
|
||||
require(19 + scriptLen <= payloadLen, "coinbase: script overruns");
|
||||
uint256 start = payloadOff + 19 + scriptLen;
|
||||
uint256 end = payloadOff + payloadLen;
|
||||
end = takeSection(t, start, end, "IGNF");
|
||||
end = takeSection(t, start, end, "IGNP");
|
||||
uint256 before = takeSection(t, start, end, "IGNS");
|
||||
require(before != end, "coinbase: no IGNS section");
|
||||
off = before;
|
||||
len = end - 8 - before;
|
||||
}
|
||||
|
||||
// If the bytes in [start, end) end with `tag`, returns the start of that section; else returns `end`.
|
||||
function takeSection(bytes memory t, uint256 start, uint256 end, bytes4 tag) private pure returns (uint256) {
|
||||
if (end < start + 8) return end;
|
||||
bytes4 have;
|
||||
assembly { have := mload(add(add(t, 32), sub(end, 4))) }
|
||||
if (have != tag) return end;
|
||||
uint256 len = le32(t, end - 8);
|
||||
if (len + 8 > end - start) return end;
|
||||
return end - 8 - len;
|
||||
}
|
||||
|
||||
function le32(bytes memory b, uint256 off) private pure returns (uint32) {
|
||||
uint256 w;
|
||||
assembly { w := mload(add(add(b, 32), off)) }
|
||||
uint32 be = uint32(w >> 224);
|
||||
return ((be & 0xff) << 24) | ((be & 0xff00) << 8) | ((be & 0xff0000) >> 8) | (be >> 24);
|
||||
}
|
||||
|
||||
// Reads record `recordIndex` of the IGNS section: (last block number, block hash, post state root, chain id).
|
||||
function readRecord(bytes memory t, uint256 recordIndex)
|
||||
public
|
||||
pure
|
||||
returns (uint64 number, bytes32 blockHash, bytes32 postRoot, uint64 chainId)
|
||||
{
|
||||
(uint256 pOff, uint256 pLen) = payloadOf(t);
|
||||
(uint256 sOff, uint256 sLen) = segmentSection(t, pOff, pLen);
|
||||
require(sLen % RECORD_LEN == 0, "record: section length");
|
||||
require((recordIndex + 1) * RECORD_LEN <= sLen, "record: index beyond the section");
|
||||
uint256 r = sOff + recordIndex * RECORD_LEN;
|
||||
uint64 last = le64(t, r + 10);
|
||||
bytes32 block_ = word(t, r + 18);
|
||||
uint256 pv = r + 118;
|
||||
chainId = uint64(uint256(word(t, pv)) >> 192);
|
||||
number = uint64(uint256(word(t, pv + 8)) >> 192);
|
||||
blockHash = word(t, pv + 16);
|
||||
postRoot = word(t, pv + 148);
|
||||
require(number == last, "record: statement is not for the segment's last block");
|
||||
require(blockHash == block_, "record: block hash is not the statement's");
|
||||
require(postRoot != bytes32(0), "record: zero post_root");
|
||||
}
|
||||
|
||||
// ---- the whole check ----------------------------------------------------------------------------
|
||||
|
||||
function verifyStateRoot(
|
||||
uint64 certIndex,
|
||||
bytes[] calldata headers,
|
||||
bytes calldata coinbaseTx,
|
||||
uint256 leafIndex,
|
||||
bytes32[] calldata siblings,
|
||||
uint256 recordIndex
|
||||
) public view returns (uint64 number, bytes32 postRoot, bytes32 blockHash, bytes32 carrier) {
|
||||
bytes32 checkpoint = verifier.finalCheckpoint(certIndex);
|
||||
require(checkpoint != bytes32(0), "oracle: no certificate at that index");
|
||||
bytes32 merkle;
|
||||
(carrier, merkle) = checkHeaderPath(headers, checkpoint);
|
||||
bytes memory t = coinbaseTx;
|
||||
bytes32 leaf = transactionHash(t);
|
||||
require(merkleRoot(leaf, leafIndex, siblings) == merkle, "merkle: path does not reach the carrier's hash_merkle_root");
|
||||
uint64 chainId;
|
||||
(number, blockHash, postRoot, chainId) = readRecord(t, recordIndex);
|
||||
require(evmChainId == 0 || chainId == evmChainId, "record: statement chain id");
|
||||
}
|
||||
|
||||
function submitStateRoot(
|
||||
uint64 certIndex,
|
||||
bytes[] calldata headers,
|
||||
bytes calldata coinbaseTx,
|
||||
uint256 leafIndex,
|
||||
bytes32[] calldata siblings,
|
||||
uint256 recordIndex
|
||||
) external returns (uint64 number, bytes32 postRoot) {
|
||||
bytes32 blockHash;
|
||||
bytes32 carrier;
|
||||
(number, postRoot, blockHash, carrier) = verifyStateRoot(certIndex, headers, coinbaseTx, leafIndex, siblings, recordIndex);
|
||||
Root storage r = _roots[number];
|
||||
require(r.postRoot == bytes32(0) || r.postRoot == postRoot, "oracle: another root is stored for that block");
|
||||
_roots[number] = Root(postRoot, blockHash, carrier, certIndex);
|
||||
emit StateRoot(number, postRoot, blockHash, certIndex, carrier);
|
||||
}
|
||||
|
||||
// ---- reads --------------------------------------------------------------------------------------
|
||||
|
||||
function stateRoot(uint64 number) public view returns (bytes32 postRoot, bytes32 blockHash, bytes32 carrier, uint64 certIndex) {
|
||||
Root storage r = _roots[number];
|
||||
require(r.postRoot != bytes32(0), "oracle: no proven root for that block");
|
||||
return (r.postRoot, r.blockHash, r.carrier, r.certIndex);
|
||||
}
|
||||
|
||||
function provenAccount(uint64 number, address a, bytes[] calldata accountProof)
|
||||
public
|
||||
view
|
||||
returns (bool exists, uint256 nonce, uint256 balance, bytes32 storageRoot, bytes32 codeHash)
|
||||
{
|
||||
(bytes32 root,,,) = stateRoot(number);
|
||||
return Mpt.account(root, a, accountProof);
|
||||
}
|
||||
|
||||
function provenBalance(uint64 number, address a, bytes[] calldata accountProof) external view returns (uint256 balance) {
|
||||
(,, balance,,) = provenAccount(number, a, accountProof);
|
||||
}
|
||||
|
||||
function provenStorage(uint64 number, address a, bytes32 slot, bytes[] calldata accountProof, bytes[] calldata storageProof)
|
||||
external
|
||||
view
|
||||
returns (bytes32)
|
||||
{
|
||||
(bool exists,,, bytes32 storageRoot,) = provenAccount(number, a, accountProof);
|
||||
if (!exists) {
|
||||
require(storageProof.length == 0, "mpt: storage proof for an absent account");
|
||||
return bytes32(0);
|
||||
}
|
||||
return Mpt.storageSlot(storageRoot, slot, storageProof);
|
||||
}
|
||||
}
|
||||
170
tools/reference-apps/oracle/contracts/Mpt.sol
Normal file
170
tools/reference-apps/oracle/contracts/Mpt.sol
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
40
tools/reference-apps/oracle/demo.mjs
Normal file
40
tools/reference-apps/oracle/demo.mjs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Reads a Devnet 3 balance from Sepolia. With fixtures/dn3-balance.json: submits its certificate through the verifier
|
||||
// (stub or real), submits the state root with the full proof, then calls provenBalance and prints the Sepolia-read
|
||||
// balance beside the fixture's. Without it: the same flow on the synthetic vector, and says so.
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import * as L from './lib.mjs';
|
||||
|
||||
const d = L.deployment();
|
||||
const pub = L.publicClient();
|
||||
const wallet = L.walletClient(L.deployerAccount());
|
||||
const abi = L.artifact('IgneumStateOracle').abi;
|
||||
const read = (functionName, args) => pub.readContract({ address: d.oracle.address, abi, functionName, args });
|
||||
const fx = L.fixtures + '/dn3-balance.json';
|
||||
const real = existsSync(fx);
|
||||
const V = real ? L.vectorFromBalanceFixture(JSON.parse(readFileSync(fx, 'utf8'))) : L.buildSynthetic();
|
||||
console.log(L.ukTime(), 'UK oracle', d.oracle.address, 'on Sepolia, verifier', await read('verifier'), real ? '' : '(no dn3-balance.json yet: synthetic vector)');
|
||||
console.log('vector:', V.kind, 'checkpoint', V.certIndex, V.checkpoint.slice(0, 12), 'headers', V.headers.length, 'chain block', V.number, 'address', L.hex0x(V.address));
|
||||
|
||||
const c = await L.ensureCertificate(pub, wallet, d, V.certIndex, L.hex0x(L.hexToBytes(V.checkpoint)), V.bitmap, V.signature);
|
||||
console.log(L.ukTime(), 'UK certificate', V.certIndex, c.already ? 'already recorded on the verifier' : 'recorded: tx ' + c.tx + ' block ' + c.block + ' gas ' + c.gasUsed);
|
||||
const r = await L.ensureStateRoot(pub, wallet, d, V);
|
||||
console.log(L.ukTime(), 'UK state root for block', V.number, r.already ? 'already stored' : 'stored: tx ' + r.tx + ' block ' + r.block + ' gas ' + r.gasUsed + ' (' + r.headers + ' headers, ' + r.calldataBytes + ' bytes of calldata)');
|
||||
const [root, blockHash, carrier, certIndex] = await read('stateRoot', [V.number]);
|
||||
console.log('stored post_root', root, 'block hash', blockHash.slice(0, 14), 'carrier', carrier.slice(0, 14), 'certificate', certIndex);
|
||||
|
||||
const balance = await read('provenBalance', [V.number, L.hex0x(V.address), V.accountProof.map(L.hex0x)]);
|
||||
const expected = real ? V.balance : V.account.balance;
|
||||
const ign = w => (Number(w / 10n ** 12n) / 1e6).toFixed(6);
|
||||
console.log('');
|
||||
console.log('Sepolia-read balance :', balance.toString(), 'wei =', ign(balance), 'IGN');
|
||||
console.log('fixture balance :', expected.toString(), 'wei =', ign(expected), 'IGN');
|
||||
console.log(balance === expected ? 'MATCH' : 'MISMATCH');
|
||||
if (real && V.storageProofs.length) {
|
||||
for (const sp of V.storageProofs) {
|
||||
const v = await read('provenStorage', [V.number, L.hex0x(V.address), L.hex0x(sp.slot), V.accountProof.map(L.hex0x), sp.proof.map(L.hex0x)]);
|
||||
console.log('slot', L.hex0x(sp.slot).slice(0, 14), 'Sepolia-read', v, 'fixture', sp.value.toString(), BigInt(v) === sp.value ? 'MATCH' : 'MISMATCH');
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
console.log('trust():', await read('trust'));
|
||||
process.exit(balance === expected ? 0 : 1);
|
||||
40
tools/reference-apps/oracle/deploy.mjs
Normal file
40
tools/reference-apps/oracle/deploy.mjs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Deploys StubCertificateVerifier and IgneumStateOracle(stub, Devnet 3 EVM chain id) to Sepolia with EIP-1559
|
||||
// transactions, waits for each receipt and writes deployment.json. Refuses to redeploy over an existing
|
||||
// deployment.json unless --force is given; --oracle-only keeps the stub from deployment.json and redeploys the oracle.
|
||||
import { writeFileSync, existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { here, artifact, publicClient, walletClient, deployerAccount, EVM_CHAIN_ID, ukTime, deployment } from './lib.mjs';
|
||||
|
||||
const out = path.join(here, 'deployment.json');
|
||||
if (existsSync(out) && !process.argv.includes('--force')) {
|
||||
const d = deployment();
|
||||
console.log('deployment.json exists (stub', d.stub.address, 'oracle', d.oracle.address + '); pass --force to redeploy');
|
||||
process.exit(0);
|
||||
}
|
||||
const account = deployerAccount();
|
||||
const pub = publicClient();
|
||||
const wallet = walletClient(account);
|
||||
const bal = await pub.getBalance({ address: account.address });
|
||||
console.log(ukTime(), 'UK deployer', account.address, 'balance', Number(bal) / 1e18, 'ETH');
|
||||
|
||||
async function deploy(name, args) {
|
||||
const art = artifact(name);
|
||||
const hash = await wallet.deployContract({ abi: art.abi, bytecode: art.bytecode, args });
|
||||
console.log(ukTime(), 'UK', name, 'sent', hash);
|
||||
const r = await pub.waitForTransactionReceipt({ hash, timeout: 180_000 });
|
||||
if (r.status !== 'success') throw new Error(name + ' deployment reverted in ' + hash);
|
||||
console.log(ukTime(), 'UK', name, 'at', r.contractAddress, 'block', r.blockNumber, 'gas', r.gasUsed);
|
||||
return { address: r.contractAddress, tx: hash, block: Number(r.blockNumber), gasUsed: Number(r.gasUsed) };
|
||||
}
|
||||
const oracleOnly = process.argv.includes('--oracle-only');
|
||||
const previous = existsSync(out) ? deployment() : null;
|
||||
const stub = oracleOnly && previous ? previous.stub : await deploy('StubCertificateVerifier', []);
|
||||
const oracle = await deploy('IgneumStateOracle', [stub.address, EVM_CHAIN_ID]);
|
||||
const after = await pub.getBalance({ address: account.address });
|
||||
const d = {
|
||||
network: 'sepolia', chainId: 11155111, rpc: 'https://ethereum-sepolia-rpc.publicnode.com', deployer: account.address,
|
||||
evmChainId: EVM_CHAIN_ID, verifier: 'stub', stub, oracle, deployedAt: new Date().toISOString(), spentWei: (bal - after).toString(),
|
||||
previous: previous ? [...(previous.previous || []), { stub: previous.stub, oracle: previous.oracle, deployedAt: previous.deployedAt, writes: previous.writes || [] }] : [],
|
||||
};
|
||||
writeFileSync(out, JSON.stringify(d, null, 1) + '\n');
|
||||
console.log(ukTime(), 'UK wrote', out, 'spent', Number(bal - after) / 1e18, 'ETH');
|
||||
122
tools/reference-apps/oracle/deployment.json
Normal file
122
tools/reference-apps/oracle/deployment.json
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"network": "sepolia",
|
||||
"chainId": 11155111,
|
||||
"rpc": "https://ethereum-sepolia-rpc.publicnode.com",
|
||||
"deployer": "0xe1D08384ef4c4c1511c8f05F94C914C4DeDc2787",
|
||||
"evmChainId": 4463,
|
||||
"verifier": "stub",
|
||||
"stub": {
|
||||
"address": "0xa197ef31d5d5613125779179668e2482ac69a6f6",
|
||||
"tx": "0xa01091b62dfb4a89b25afb57a3d384677a9ad62370748504d2b3f288c0d3ad38",
|
||||
"block": 11869583,
|
||||
"gasUsed": 8434842
|
||||
},
|
||||
"oracle": {
|
||||
"address": "0xefe9879de29c401eeff195d5bf71c86b9caaa1b2",
|
||||
"tx": "0xb8650f5b2a6965b94fde56a668e9c593b227f5b7e8d78949e32edd7f79d2d644",
|
||||
"block": 11869608,
|
||||
"gasUsed": 19585581
|
||||
},
|
||||
"deployedAt": "2026-10-08T10:37:13.434Z",
|
||||
"spentWei": "19585913954877",
|
||||
"previous": [
|
||||
{
|
||||
"stub": {
|
||||
"address": "0xa197ef31d5d5613125779179668e2482ac69a6f6",
|
||||
"tx": "0xa01091b62dfb4a89b25afb57a3d384677a9ad62370748504d2b3f288c0d3ad38",
|
||||
"block": 11869583,
|
||||
"gasUsed": 8434842
|
||||
},
|
||||
"oracle": {
|
||||
"address": "0xa0754e555933b21703cdc6f9ff62e22ddb0a641e",
|
||||
"tx": "0x411b44f37c17834fab674a1fb5f6a0d9680cc0d3c485797068961158139edbb7",
|
||||
"block": 11869584,
|
||||
"gasUsed": 19585581
|
||||
},
|
||||
"deployedAt": "2026-10-08T10:32:27.409Z",
|
||||
"writes": [
|
||||
{
|
||||
"at": "2026-10-08T10:35:00Z",
|
||||
"what": "submitCertificate",
|
||||
"vector": "synthetic",
|
||||
"index": "9001",
|
||||
"tx": "0x4ee21dc6fc3b5e72e29ae04bb6cace759193ce117246e4417704b8e627d8b0de",
|
||||
"block": 11869597,
|
||||
"gasUsed": 239120
|
||||
},
|
||||
{
|
||||
"at": "2026-10-08T10:35:00Z",
|
||||
"what": "submitStateRoot",
|
||||
"vector": "synthetic",
|
||||
"number": "27039",
|
||||
"certIndex": "9001",
|
||||
"tx": "0x2d7f282580fdbb245af47abe429d48573d72c06403bf75fdc5407ffdd97d8df0",
|
||||
"block": 11869598,
|
||||
"gasUsed": 580161,
|
||||
"headers": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"writes": [
|
||||
{
|
||||
"at": "2026-10-08T10:37:50.681Z",
|
||||
"what": "submitCertificate",
|
||||
"index": "9002",
|
||||
"checkpoint": "0x6bef4a7e59cae5ef017598418fa0d3be7f1528d8169191014596d9cc232fd036",
|
||||
"verifier": "0xa197ef31D5D5613125779179668E2482AC69a6F6",
|
||||
"tx": "0x6c2e8ff6506853aac877712b3a5e51ac57ab3426f71a03422ffd9cfae5c3ef4c",
|
||||
"block": 11869611,
|
||||
"gasUsed": 239120
|
||||
},
|
||||
{
|
||||
"at": "2026-10-08T10:38:04.312Z",
|
||||
"what": "submitStateRoot",
|
||||
"vector": "synthetic",
|
||||
"number": "9000027039",
|
||||
"postRoot": "0xb03ecb60bbd03a652155ee5e4c76bc5ad2e8623fe24ef3810ea4f4628e94dff6",
|
||||
"certIndex": "9002",
|
||||
"tx": "0x8907936e01b8e1ce323a68263d574128793b4db55d74738484a4881ff6788219",
|
||||
"block": 11869612,
|
||||
"gasUsed": 580281,
|
||||
"calldataBytes": 1863,
|
||||
"headers": 3
|
||||
},
|
||||
{
|
||||
"at": "2026-10-08T10:44:27.702Z",
|
||||
"what": "submitCertificate",
|
||||
"index": "2060",
|
||||
"checkpoint": "0xbbf3757d7f7bd6b718affcb9f2e15f59da55f7129d6885d7e26d40dd9ea1d934",
|
||||
"verifier": "0xa197ef31D5D5613125779179668E2482AC69a6F6",
|
||||
"tx": "0xa23da37f02ad28e6d9b371a3d2f07490be06283b718933bf47f900429bd55c9b",
|
||||
"block": 11869644,
|
||||
"gasUsed": 240856
|
||||
},
|
||||
{
|
||||
"at": "2026-10-08T10:44:36.578Z",
|
||||
"what": "submitStateRoot",
|
||||
"vector": "devnet-3",
|
||||
"number": "26631",
|
||||
"postRoot": "0xf567a84fa10dcabab5f68c712f9553f2f2032cb675bdaba1d13c7901a67a82fd",
|
||||
"certIndex": "2060",
|
||||
"tx": "0xb3875bbd311a91457c3f4b139d6eff38cfb5970e3260d9096f5d8070d1d62e85",
|
||||
"block": 11869645,
|
||||
"gasUsed": 642331,
|
||||
"calldataBytes": 5701,
|
||||
"headers": 1
|
||||
},
|
||||
{
|
||||
"at": "2026-10-08T10:44:39.214Z",
|
||||
"what": "submitStateRoot",
|
||||
"vector": "devnet-3",
|
||||
"number": "26631",
|
||||
"postRoot": "0xf567a84fa10dcabab5f68c712f9553f2f2032cb675bdaba1d13c7901a67a82fd",
|
||||
"certIndex": "2060",
|
||||
"tx": "0xf360e54c8f8c40a73c25c8fdfd214ff559ec0ee281beda452e7b56e2a2cf224b",
|
||||
"block": 11869645,
|
||||
"gasUsed": 404376,
|
||||
"calldataBytes": 5701,
|
||||
"headers": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
352
tools/reference-apps/oracle/lib.mjs
Normal file
352
tools/reference-apps/oracle/lib.mjs
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
// Shared helpers for the oracle scripts: byte layouts (header, coinbase transaction, segment record, coinbase
|
||||
// payload), a JS mirror of the BLAKE2b driver the contract runs through the EIP-152 precompile, a small
|
||||
// Merkle Patricia trie builder for synthetic vectors, and the RPC plumbing (viem, Sepolia, the deployer key).
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { blake2b } from '@noble/hashes/blake2.js';
|
||||
import { keccak_256 } from '@noble/hashes/sha3.js';
|
||||
import { createPublicClient, createWalletClient, http, defineChain } from 'viem';
|
||||
import { privateKeyToAccount } from 'viem/accounts';
|
||||
import { headerHash, coinbaseTxHash, merkleRootFromPath, rlpEncode, trimBig, hexToBytes, bytesToHex, segmentRecordsOf } from '../../../site/lc/core.js';
|
||||
|
||||
export { blake2b, keccak_256, headerHash, coinbaseTxHash, merkleRootFromPath, rlpEncode, trimBig, hexToBytes, bytesToHex };
|
||||
export const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const fixtures = path.resolve(here, '../fixtures');
|
||||
export const RPC = 'https://ethereum-sepolia-rpc.publicnode.com';
|
||||
export const EVM_CHAIN_ID = 0x116f; // Devnet 3's EVM chain id, from the receipt fixture
|
||||
export const strip = s => String(s).replace(/^0x/i, '');
|
||||
export const hex0x = b => '0x' + bytesToHex(b);
|
||||
const te = new TextEncoder();
|
||||
|
||||
export 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; }
|
||||
export const u16le = v => { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, Number(v), true); return b; };
|
||||
export const u32le = v => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, Number(v), true); return b; };
|
||||
export const u64le = v => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(v), true); return b; };
|
||||
export const u32be = v => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, Number(v), false); return b; };
|
||||
export const u64be = v => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(v), false); return b; };
|
||||
export function randomBytes(n) { const b = new Uint8Array(n); for (let i = 0; i < n; i++) b[i] = Math.floor(Math.random() * 256); return b; }
|
||||
// A seeded byte source (xorshift32) so a synthetic vector is the same on every run.
|
||||
export function seeded(seed) { let x = seed >>> 0 || 1; return n => { const b = new Uint8Array(n); for (let i = 0; i < n; i++) { x ^= x << 13; x >>>= 0; x ^= x >>> 17; x ^= x << 5; x >>>= 0; b[i] = x & 0xff; } return b; }; }
|
||||
|
||||
// ---- the header, byte for byte as site/verify/core.js headerHash hashes it -----------------------------------
|
||||
export function serializeHeader(h) {
|
||||
const levels = h.parents_by_level || [];
|
||||
const parts = [u16le(h.version), u64le(levels.length)];
|
||||
for (const level of levels) { parts.push(u64le(level.length)); for (const p of level) parts.push(hexToBytes(strip(p))); }
|
||||
parts.push(hexToBytes(strip(h.hash_merkle_root)), hexToBytes(strip(h.accepted_id_merkle_root)), hexToBytes(strip(h.utxo_commitment)),
|
||||
u64le(h.timestamp), u32le(h.bits), u64le(h.nonce), u64le(h.daa_score), u64le(h.blue_score));
|
||||
const bw = String(h.blue_work).replace(/^0+/, '');
|
||||
const bwBytes = bw.length ? hexToBytes(bw.length % 2 ? '0' + bw : bw) : new Uint8Array(0);
|
||||
parts.push(u64le(bwBytes.length), bwBytes, hexToBytes(strip(h.pruning_point)), hexToBytes(strip(h.vote_key_hash)));
|
||||
return concat(parts);
|
||||
}
|
||||
|
||||
// ---- the coinbase transaction, byte for byte as site/light/core.js coinbaseTxHash hashes it ---------------------
|
||||
export function serializeCoinbase(tx, 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));
|
||||
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 concat(parts);
|
||||
}
|
||||
|
||||
// ---- the segment record (586 bytes) and the coinbase payload sections ----------------------------------------
|
||||
export function encodeBlockStatement(s) {
|
||||
return concat([u64be(s.chain_id), u64be(s.number), s.block_hash, s.parent_hash, u32be(s.shard_count), s.tx_commitment, s.pre_root, s.post_root, s.receipts,
|
||||
u64be(s.gas_used), u64be(s.pgas_used), u32be(s.executed), u32be(s.skipped), s.provers, s.shard_vk, s.agg_vk, u64be(s.chain_len)]);
|
||||
}
|
||||
export function encodeSegmentRecord(r) {
|
||||
const pv = encodeBlockStatement(r.statement);
|
||||
if (pv.length !== 340) throw new Error('statement is ' + pv.length + ' bytes');
|
||||
const b = concat([u16le(r.version), u64le(r.first), u64le(r.last), r.block, r.pubkey, r.payout, pv, r.proof_hash, r.signature]);
|
||||
if (b.length !== 586) throw new Error('record is ' + b.length + ' bytes');
|
||||
return b;
|
||||
}
|
||||
const section = (items, tag) => concat([items, u32le(items.length), te.encode(tag)]);
|
||||
// blue_score || subsidy || script version || script len || script || IGNS || IGNP || IGNF
|
||||
export function buildCoinbasePayload({ blueScore = 1n, subsidy = 0n, script = new Uint8Array(0), minerBytes = new Uint8Array(0), records = [], ignp = new Uint8Array(0), ignf = new Uint8Array(0) }) {
|
||||
return concat([u64le(blueScore), u64le(subsidy), u16le(0), new Uint8Array([script.length]), script, minerBytes,
|
||||
section(concat(records), 'IGNS'), section(ignp, 'IGNP'), section(ignf, 'IGNF')]);
|
||||
}
|
||||
|
||||
// ---- keyed BLAKE2b-256 the way the contract drives the EIP-152 compression function -------------------------
|
||||
const MASK = (1n << 64n) - 1n;
|
||||
const IV = [0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n, 0x510e527fade682d1n, 0x9b05688c2b3e6c1fn, 0x1f83d9abfb41bd6bn, 0x5be0cd19137e2179n];
|
||||
const SIGMA = [
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
|
||||
[11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
|
||||
[9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
|
||||
[12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
|
||||
[6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
|
||||
];
|
||||
const rotr = (x, n) => ((x >> n) | (x << (64n - n))) & MASK;
|
||||
// The compression function F(h, m, t, f) with 12 rounds, exactly what the precompile computes.
|
||||
export function blake2bF(h, m, t0, t1, f) {
|
||||
const v = new Array(16);
|
||||
for (let i = 0; i < 8; i++) { v[i] = h[i]; v[i + 8] = IV[i]; }
|
||||
v[12] ^= t0; v[13] ^= t1; if (f) v[14] ^= MASK;
|
||||
const G = (a, b, c, d, x, y) => {
|
||||
v[a] = (v[a] + v[b] + x) & MASK; v[d] = rotr(v[d] ^ v[a], 32n); v[c] = (v[c] + v[d]) & MASK; v[b] = rotr(v[b] ^ v[c], 24n);
|
||||
v[a] = (v[a] + v[b] + y) & MASK; v[d] = rotr(v[d] ^ v[a], 16n); v[c] = (v[c] + v[d]) & MASK; v[b] = rotr(v[b] ^ v[c], 63n);
|
||||
};
|
||||
for (let r = 0; r < 12; r++) {
|
||||
const s = SIGMA[r % 10];
|
||||
G(0, 4, 8, 12, m[s[0]], m[s[1]]); G(1, 5, 9, 13, m[s[2]], m[s[3]]); G(2, 6, 10, 14, m[s[4]], m[s[5]]); G(3, 7, 11, 15, m[s[6]], m[s[7]]);
|
||||
G(0, 5, 10, 15, m[s[8]], m[s[9]]); G(1, 6, 11, 12, m[s[10]], m[s[11]]); G(2, 7, 8, 13, m[s[12]], m[s[13]]); G(3, 4, 9, 14, m[s[14]], m[s[15]]);
|
||||
}
|
||||
for (let i = 0; i < 8; i++) h[i] ^= v[i] ^ v[i + 8];
|
||||
}
|
||||
const wordsLE = block => { const dv = new DataView(block.buffer, block.byteOffset, 128); const m = []; for (let i = 0; i < 16; i++) m.push(dv.getBigUint64(i * 8, true)); return m; };
|
||||
// Same driver as Blake2b.sol: parameter block, the key as the first block (t = 128), then the message blocks.
|
||||
export function keyedBlake2b256(key, data) {
|
||||
const h = IV.slice();
|
||||
h[0] ^= 0x01010000n ^ (BigInt(key.length) << 8n) ^ 32n;
|
||||
const kb = new Uint8Array(128); kb.set(key);
|
||||
blake2bF(h, wordsLE(kb), 128n, 0n, data.length === 0);
|
||||
let done = 0;
|
||||
while (done < data.length) {
|
||||
const take = Math.min(128, data.length - done);
|
||||
const mb = new Uint8Array(128); mb.set(data.subarray(done, done + take));
|
||||
done += take;
|
||||
blake2bF(h, wordsLE(mb), 128n + BigInt(done), 0n, done === data.length);
|
||||
}
|
||||
const out = new Uint8Array(32); const dv = new DataView(out.buffer);
|
||||
for (let i = 0; i < 4; i++) dv.setBigUint64(i * 8, h[i], true);
|
||||
return out;
|
||||
}
|
||||
export const nobleKeyed = (key, data) => blake2b(data, { dkLen: 32, key });
|
||||
|
||||
// ---- a Merkle Patricia trie builder (secure trie, 32-byte keys) for synthetic vectors --------------------------
|
||||
const nibblesOf = b => { const out = []; for (const x of b) out.push(x >> 4, x & 15); return out; };
|
||||
function hp(nibs, leaf) {
|
||||
const odd = nibs.length & 1; const flag = (leaf ? 2 : 0) | odd;
|
||||
const out = []; let i = 0;
|
||||
if (odd) { out.push((flag << 4) | nibs[0]); i = 1; } else out.push(flag << 4);
|
||||
for (; i < nibs.length; i += 2) out.push((nibs[i] << 4) | nibs[i + 1]);
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
const common = (a, b) => { let i = 0; while (i < a.length && i < b.length && a[i] === b[i]) i++; return i; };
|
||||
export class Trie {
|
||||
constructor() { this.root = null; }
|
||||
put(keyBytes, valueBytes) { this.root = this._put(this.root, nibblesOf(keyBytes), valueBytes); }
|
||||
_put(n, path, value) {
|
||||
if (!n) return { kind: 'leaf', path, value };
|
||||
if (n.kind === 'leaf') {
|
||||
const cp = common(n.path, path);
|
||||
if (cp === n.path.length && cp === path.length) return { kind: 'leaf', path, value };
|
||||
const br = { kind: 'branch', children: new Array(16).fill(null), value: null };
|
||||
const place = (p, v) => { if (p.length === cp) br.value = v; else br.children[p[cp]] = { kind: 'leaf', path: p.slice(cp + 1), value: v }; };
|
||||
place(n.path, n.value); place(path, value);
|
||||
return cp ? { kind: 'ext', path: path.slice(0, cp), child: br } : br;
|
||||
}
|
||||
if (n.kind === 'ext') {
|
||||
const cp = common(n.path, path);
|
||||
if (cp === n.path.length) return { kind: 'ext', path: n.path, child: this._put(n.child, path.slice(cp), value) };
|
||||
const br = { kind: 'branch', children: new Array(16).fill(null), value: null };
|
||||
const rem = n.path.slice(cp);
|
||||
br.children[rem[0]] = rem.length === 1 ? n.child : { kind: 'ext', path: rem.slice(1), child: n.child };
|
||||
if (path.length === cp) br.value = value; else br.children[path[cp]] = { kind: 'leaf', path: path.slice(cp + 1), value };
|
||||
return cp ? { kind: 'ext', path: path.slice(0, cp), child: br } : br;
|
||||
}
|
||||
if (path.length === 0) return { ...n, value };
|
||||
const children = n.children.slice();
|
||||
children[path[0]] = this._put(children[path[0]], path.slice(1), value);
|
||||
return { kind: 'branch', children, value: n.value };
|
||||
}
|
||||
encode(n) {
|
||||
if (n.kind === 'leaf') return rlpEncodeRaw([hp(n.path, true), n.value]);
|
||||
if (n.kind === 'ext') return rlpEncodeRaw([hp(n.path, false), this.ref(n.child)]);
|
||||
return rlpEncodeRaw([...n.children.map(c => (c ? this.ref(c) : new Uint8Array(0))), n.value || new Uint8Array(0)]);
|
||||
}
|
||||
// A child reference: the keccak of its encoding, or the encoding itself (spliced in raw) when shorter than 32 bytes.
|
||||
ref(n) { const e = this.encode(n); return e.length < 32 ? { __raw: e } : keccak_256(e); }
|
||||
rootHash() { return this.root ? keccak_256(this.encode(this.root)) : keccak_256(rlpEncode(new Uint8Array(0))); }
|
||||
// The proof nodes for a key, root first: every hash-referenced node on the path (embedded ones travel inside their parent).
|
||||
proof(keyBytes) {
|
||||
const out = []; let n = this.root; let path = nibblesOf(keyBytes); let embedded = false;
|
||||
while (n) {
|
||||
const e = this.encode(n);
|
||||
if (!embedded) out.push(e);
|
||||
let c;
|
||||
if (n.kind === 'leaf') break;
|
||||
if (n.kind === 'ext') { if (common(n.path, path) !== n.path.length) break; path = path.slice(n.path.length); c = n.child; }
|
||||
else { if (!path.length) break; c = n.children[path[0]]; path = path.slice(1); if (!c) break; }
|
||||
embedded = this.encode(c).length < 32; n = c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
// rlpEncode in core.js knows only bytes and lists; this one also splices a {__raw} item in as it is.
|
||||
export function rlpEncodeRaw(item) {
|
||||
if (item && item.__raw) return item.__raw;
|
||||
if (item instanceof Uint8Array) return rlpEncode(item);
|
||||
const body = concat(item.map(rlpEncodeRaw));
|
||||
const n = body.length;
|
||||
const prefix = n < 56 ? new Uint8Array([0xc0 + n]) : (() => { const bytes = []; let x = n; while (x > 0) { bytes.unshift(x & 0xff); x = Math.floor(x / 256); } return new Uint8Array([0xf7 + bytes.length, ...bytes]); })();
|
||||
return concat([prefix, body]);
|
||||
}
|
||||
|
||||
export const EMPTY_ROOT = '56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421';
|
||||
export const encodeAccount = a => rlpEncode([trimBig(a.nonce), trimBig(a.balance), hexToBytes(strip(a.storageRoot)), hexToBytes(strip(a.codeHash))]);
|
||||
|
||||
// ---- RPC plumbing ----------------------------------------------------------------------------------------------
|
||||
export const sepolia = defineChain({ id: 0xaa36a7, name: 'Sepolia', nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: [RPC] } } });
|
||||
export const publicClient = () => createPublicClient({ chain: sepolia, transport: http(RPC, { timeout: 120_000 }) });
|
||||
export function deployerAccount() {
|
||||
const k = JSON.parse(readFileSync(path.join(process.env.HOME, '.config/igneum/sepolia-deployer'), 'utf8'));
|
||||
return privateKeyToAccount(k.private_key);
|
||||
}
|
||||
export const walletClient = account => createWalletClient({ account, chain: sepolia, transport: http(RPC, { timeout: 120_000 }) });
|
||||
export const artifact = name => JSON.parse(readFileSync(path.join(here, 'artifacts', name + '.json'), 'utf8'));
|
||||
export const deployment = () => JSON.parse(readFileSync(path.join(here, 'deployment.json'), 'utf8'));
|
||||
export const ukTime = () => new Date().toLocaleTimeString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
// The revert reason out of a viem error, or the message's first line.
|
||||
export function reasonOf(e) {
|
||||
const m = String(e.shortMessage || e.message || e);
|
||||
const r = /reason:\s*([^\n]+)/.exec(String(e.message || '')) || /reverted with the following reason:\s*([^\n]+)/.exec(String(e.message || ''));
|
||||
return r ? r[1].trim() : m.split('\n')[0];
|
||||
}
|
||||
|
||||
// ---- a synthetic balance proof: a state trie, a segment record, a coinbase, a merkle path and a two-header path --
|
||||
// Everything the contract checks is real; only the aggregator's signature and the certificate are made up, which is
|
||||
// what the stub verifier accepts. The checkpoint index is 9002 so it never collides with a real certificate (9001 holds an earlier synthetic vector).
|
||||
export function buildSynthetic(evmChainId = EVM_CHAIN_ID, seed = 0x1a2b3c4d) {
|
||||
const randomBytes = seeded(seed);
|
||||
const address = randomBytes(20);
|
||||
const storage = new Trie();
|
||||
const slot1 = new Uint8Array(32); slot1[31] = 1;
|
||||
const slot5 = new Uint8Array(32); slot5[31] = 5;
|
||||
const v1 = hexToBytes('abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
|
||||
storage.put(keccak_256(slot1), rlpEncode(v1));
|
||||
storage.put(keccak_256(slot5), rlpEncode(trimBig(42n)));
|
||||
for (let i = 0; i < 6; i++) storage.put(keccak_256(randomBytes(32)), rlpEncode(trimBig(BigInt(1000 + i))));
|
||||
const account = { nonce: 7n, balance: 123456789n * 10n ** 15n, storageRoot: bytesToHex(storage.rootHash()), codeHash: bytesToHex(keccak_256(new Uint8Array(0))) };
|
||||
const state = new Trie();
|
||||
state.put(keccak_256(address), encodeAccount(account));
|
||||
for (let i = 0; i < 9; i++) state.put(keccak_256(randomBytes(20)), encodeAccount({ nonce: BigInt(i), balance: BigInt(i) * 10n ** 18n, storageRoot: EMPTY_ROOT, codeHash: account.codeHash }));
|
||||
const postRoot = state.rootHash();
|
||||
const number = 9_000_027_039n; // far above any Devnet 3 block number, so it never collides with a real record
|
||||
const blockHash = randomBytes(32);
|
||||
const record = encodeSegmentRecord({
|
||||
version: 2, first: number - 3n, last: number, block: blockHash, pubkey: randomBytes(48), payout: randomBytes(20),
|
||||
statement: { chain_id: BigInt(evmChainId), number, block_hash: blockHash, parent_hash: randomBytes(32), shard_count: 1, tx_commitment: randomBytes(32), pre_root: randomBytes(32), post_root: postRoot, receipts: randomBytes(32), gas_used: 21000n, pgas_used: 0n, executed: 1, skipped: 0, provers: randomBytes(32), shard_vk: randomBytes(32), agg_vk: randomBytes(32), chain_len: 4n },
|
||||
proof_hash: randomBytes(32), signature: randomBytes(96),
|
||||
});
|
||||
const payload = buildCoinbasePayload({ blueScore: 62371n, subsidy: 50000000n, script: hexToBytes('20' + bytesToHex(randomBytes(32)) + 'ac'), minerBytes: te.encode('synthetic'), records: [record], ignf: randomBytes(40) });
|
||||
const coinbase = { version: 1, inputs: [], outputs: [{ value: '50000000', scriptPublicKey: '0000' + '20' + bytesToHex(randomBytes(32)) + 'ac' }], lockTime: 0, subnetworkId: '0100000000000000000000000000000000000000', gas: 0, payload: bytesToHex(payload), mass: 0 };
|
||||
const cbBytes = serializeCoinbase(coinbase);
|
||||
const leaf = coinbaseTxHash(coinbase, blake2b);
|
||||
const leaves = [leaf, randomBytes(32), randomBytes(32)];
|
||||
const H = d => blake2b(d, { dkLen: 32, key: te.encode('MerkleBranchHash') });
|
||||
const siblings = [leaves[1], H(concat([leaves[2], new Uint8Array(32)]))];
|
||||
const merkle = merkleRootFromPath(leaf, 0, siblings, blake2b);
|
||||
const mk = (merkleRoot, parents, blueScore) => ({ version: 1538, parents_by_level: [parents, parents], hash_merkle_root: bytesToHex(merkleRoot), accepted_id_merkle_root: bytesToHex(randomBytes(32)), utxo_commitment: bytesToHex(randomBytes(32)), timestamp: '1759900000000', bits: 486805716, nonce: '1234567890123', daa_score: '62684', blue_work: '00000000000000000000000000000000000020dfbc77ac31', blue_score: String(blueScore), pruning_point: bytesToHex(randomBytes(32)), vote_key_hash: bytesToHex(randomBytes(32)) });
|
||||
const carrier = mk(merkle, [bytesToHex(randomBytes(32))], 62371); carrier.hash = headerHash(carrier, blake2b);
|
||||
const mid = mk(randomBytes(32), [carrier.hash, bytesToHex(randomBytes(32))], 62372); mid.hash = headerHash(mid, blake2b);
|
||||
const top = mk(randomBytes(32), [bytesToHex(randomBytes(32)), mid.hash], 62373); top.hash = headerHash(top, blake2b);
|
||||
return {
|
||||
kind: 'synthetic', certIndex: 9002n, checkpoint: top.hash, headers: [carrier, mid, top], coinbase, coinbaseBytes: cbBytes, leafIndex: 0, siblings, recordIndex: 0,
|
||||
number, postRoot, blockHash, address, account, accountProof: state.proof(keccak_256(address)),
|
||||
slots: [{ slot: slot1, value: v1, proof: storage.proof(keccak_256(slot1)) }, { slot: slot5, value: trimBig(42n), proof: storage.proof(keccak_256(slot5)) }],
|
||||
absent: (() => { const s = new Uint8Array(32); s[31] = 9; return { slot: s, proof: storage.proof(keccak_256(s)) }; })(),
|
||||
};
|
||||
}
|
||||
|
||||
// ---- contract arguments and the two writes --------------------------------------------------------------------
|
||||
export function proofArgs(v) {
|
||||
return [v.certIndex, v.headers.map(h => hex0x(serializeHeader(h))), hex0x(v.coinbaseBytes), BigInt(v.leafIndex), v.siblings.map(hex0x), BigInt(v.recordIndex)];
|
||||
}
|
||||
// Appends a write to deployment.json so every transaction is on record.
|
||||
export function recordWrite(entry) {
|
||||
const p = path.join(here, 'deployment.json');
|
||||
const d = JSON.parse(readFileSync(p, 'utf8'));
|
||||
d.writes = d.writes || [];
|
||||
d.writes.push({ at: new Date().toISOString(), ...entry });
|
||||
writeFileSync(p, JSON.stringify(d, null, 1) + '\n');
|
||||
}
|
||||
// The public RPC is a pool of nodes; a read right after a receipt can land on one that lags. Poll until it agrees.
|
||||
// Never send while one of our transactions is still pending, then read the pre-check again: a stale read on a
|
||||
// lagging node must not turn into a second, identical submission (seen once on 8 October 2026, nonces 8 and 9).
|
||||
async function quietMempool(pub, address) {
|
||||
await untilVisible(async () => (await pub.getTransactionCount({ address, blockTag: 'pending' })) === (await pub.getTransactionCount({ address, blockTag: 'latest' })), 'an empty mempool for the deployer');
|
||||
}
|
||||
async function untilVisible(check, what) {
|
||||
for (let i = 0; i < 30; i++) { if (await check()) return; await new Promise(r => setTimeout(r, 1000)); }
|
||||
throw new Error(what + ' is not visible on the RPC 30 s after its receipt');
|
||||
}
|
||||
export async function ensureCertificate(pub, wallet, d, index, checkpoint, bitmap = '0x', signature = '0x') {
|
||||
const stubAbi = artifact('StubCertificateVerifier').abi;
|
||||
const verifierAddr = await pub.readContract({ address: d.oracle.address, abi: artifact('IgneumStateOracle').abi, functionName: 'verifier' });
|
||||
const same = async () => (await pub.readContract({ address: verifierAddr, abi: stubAbi, functionName: 'finalCheckpoint', args: [index] })).toLowerCase() === checkpoint.toLowerCase();
|
||||
if (await same()) return { verifier: verifierAddr, already: true };
|
||||
await quietMempool(pub, wallet.account.address);
|
||||
if (await same()) return { verifier: verifierAddr, already: true };
|
||||
const hash = await wallet.writeContract({ address: verifierAddr, abi: stubAbi, functionName: 'submitCertificate', args: [index, checkpoint, bitmap, signature] });
|
||||
const r = await pub.waitForTransactionReceipt({ hash, timeout: 180_000 });
|
||||
if (r.status !== 'success') throw new Error('submitCertificate reverted in ' + hash);
|
||||
await untilVisible(same, 'certificate ' + index);
|
||||
const out = { verifier: verifierAddr, tx: hash, block: Number(r.blockNumber), gasUsed: Number(r.gasUsed) };
|
||||
recordWrite({ what: 'submitCertificate', index: String(index), checkpoint, ...out });
|
||||
return out;
|
||||
}
|
||||
export async function ensureStateRoot(pub, wallet, d, v) {
|
||||
const abi = artifact('IgneumStateOracle').abi;
|
||||
const stored = async () => { try { const [root] = await pub.readContract({ address: d.oracle.address, abi, functionName: 'stateRoot', args: [v.number] }); return root.toLowerCase() === hex0x(v.postRoot).toLowerCase(); } catch { return false; } };
|
||||
if (await stored()) return { already: true };
|
||||
await quietMempool(pub, wallet.account.address);
|
||||
if (await stored()) return { already: true };
|
||||
const args = proofArgs(v);
|
||||
const gas = await pub.estimateContractGas({ address: d.oracle.address, abi, functionName: 'submitStateRoot', args, account: wallet.account });
|
||||
const hash = await wallet.writeContract({ address: d.oracle.address, abi, functionName: 'submitStateRoot', args, gas: gas + gas / 5n });
|
||||
const r = await pub.waitForTransactionReceipt({ hash, timeout: 180_000 });
|
||||
if (r.status !== 'success') throw new Error('submitStateRoot reverted in ' + hash);
|
||||
await untilVisible(async () => { try { const [root] = await pub.readContract({ address: d.oracle.address, abi, functionName: 'stateRoot', args: [v.number] }); return root.toLowerCase() === hex0x(v.postRoot).toLowerCase(); } catch { return false; } }, 'state root ' + v.number);
|
||||
const out = { tx: hash, block: Number(r.blockNumber), gasUsed: Number(r.gasUsed), calldataBytes: args[1].reduce((a, h) => a + (h.length - 2) / 2, 0) + (args[2].length - 2) / 2, headers: args[1].length };
|
||||
recordWrite({ what: 'submitStateRoot', vector: v.kind, number: String(v.number), postRoot: hex0x(v.postRoot), certIndex: String(v.certIndex), ...out });
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- the balance fixture (the light service's /balance body, with the certificate beside it) -------------------
|
||||
// Accepts the flat /balance body, or {proof, certificate} / {balance, checkpoint} wrappers; the certificate's bitmap
|
||||
// and signature come from the fixture when present, else from dn3-checkpoint.json when its index matches.
|
||||
export function vectorFromBalanceFixture(fx) {
|
||||
const p = fx.proof || fx.balance || fx;
|
||||
let cert = fx.certificate || (p.checkpoint && p.checkpoint.certificate) || fx.checkpoint_certificate || null;
|
||||
if (!cert) { try { const c = JSON.parse(readFileSync(path.join(fixtures, 'dn3-checkpoint.json'), 'utf8')); if (Number(c.index) === Number(p.checkpoint.index)) cert = c; } catch {} }
|
||||
const c = cert && cert.certificate ? cert.certificate : cert || {};
|
||||
const records = segmentRecordsOf(hexToBytes(strip(p.carrier.coinbase.payload)));
|
||||
const recordIndex = records.findIndex(r => bytesToHex(r.bytes) === strip(p.segment_record_hex));
|
||||
if (recordIndex < 0) throw new Error('the carrier coinbase carries ' + records.length + ' record(s), none is segment_record_hex');
|
||||
const st = records[recordIndex].statement;
|
||||
const acct = p.account;
|
||||
return {
|
||||
kind: 'devnet-3', certIndex: BigInt(p.checkpoint.index), checkpoint: strip(p.checkpoint.hash), headers: p.headers,
|
||||
bitmap: c.bitmap_hex ? '0x' + strip(c.bitmap_hex) : '0x', signature: c.aggregate_signature_hex ? '0x' + strip(c.aggregate_signature_hex) : '0x',
|
||||
coinbase: p.carrier.coinbase, coinbaseBytes: serializeCoinbase(p.carrier.coinbase, p.carrier.amount_wire_len || 8),
|
||||
leafIndex: Number(p.carrier.leaf_index), siblings: p.carrier.merkle_siblings.map(x => hexToBytes(strip(x))), recordIndex,
|
||||
number: st.number, postRoot: hexToBytes(st.post_root), blockHash: hexToBytes(st.block_hash), evmChainId: Number(st.chain_id),
|
||||
address: hexToBytes(strip(p.address)), balance: BigInt(acct.balance), nonce: BigInt(acct.nonce || 0), accountProof: acct.accountProof.map(x => hexToBytes(strip(x))),
|
||||
reportedStateRoot: acct.stateRoot ? strip(acct.stateRoot) : null, reportedBlock: acct.blockNumber != null ? Number(acct.blockNumber) : null,
|
||||
storageProofs: (acct.storageProof || []).map(sp => ({ slot: hexToBytes(strip(sp.key).padStart(64, '0')), value: BigInt(sp.value), proof: sp.proof.map(x => hexToBytes(strip(x))) })),
|
||||
};
|
||||
}
|
||||
|
||||
381
tools/reference-apps/oracle/package-lock.json
generated
Normal file
381
tools/reference-apps/oracle/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
{
|
||||
"name": "igneum-state-oracle",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "igneum-state-oracle",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "2.4.0",
|
||||
"solc": "0.8.28",
|
||||
"viem": "^2.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adraffy/ens-normalize": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
|
||||
"integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
|
||||
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
|
||||
"integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves/node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"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/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
|
||||
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip32": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
|
||||
"integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/curves": "~1.9.0",
|
||||
"@noble/hashes": "~1.8.0",
|
||||
"@scure/base": "~1.2.5"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip32/node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip39": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
|
||||
"integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "~1.8.0",
|
||||
"@scure/base": "~1.2.5"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip39/node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/abitype": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
|
||||
"integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.4",
|
||||
"zod": "^3.22.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/command-exists": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
|
||||
"integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
|
||||
"integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/isows": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
|
||||
"integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sha3": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
|
||||
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/memorystream": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
||||
"integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ox": {
|
||||
"version": "0.14.54",
|
||||
"resolved": "https://registry.npmjs.org/ox/-/ox-0.14.54.tgz",
|
||||
"integrity": "sha512-52PGH6ldJmWY9+iy/TBSJUsCSy5NmLvkVBYTINmO8srsjRrCmCdO575kGizmZ1jpNQ1gnlpVwiciIpU37fcULA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@adraffy/ens-normalize": "^1.11.0",
|
||||
"@noble/ciphers": "^1.3.0",
|
||||
"@noble/curves": "1.9.1",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@scure/bip32": "^1.7.0",
|
||||
"@scure/bip39": "^1.6.0",
|
||||
"abitype": "^1.2.3",
|
||||
"eventemitter3": "5.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ox/node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/solc": {
|
||||
"version": "0.8.28",
|
||||
"resolved": "https://registry.npmjs.org/solc/-/solc-0.8.28.tgz",
|
||||
"integrity": "sha512-AFCiJ+b4RosyyNhnfdVH4ZR1+TxiL91iluPjw0EJslIu4LXGM9NYqi2z5y8TqochC4tcH9QsHfwWhOIC9jPDKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"command-exists": "^1.2.8",
|
||||
"commander": "^8.1.0",
|
||||
"follow-redirects": "^1.12.1",
|
||||
"js-sha3": "0.8.0",
|
||||
"memorystream": "^0.3.1",
|
||||
"semver": "^5.5.0",
|
||||
"tmp": "0.0.33"
|
||||
},
|
||||
"bin": {
|
||||
"solcjs": "solc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"os-tmpdir": "~1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/viem": {
|
||||
"version": "2.57.4",
|
||||
"resolved": "https://registry.npmjs.org/viem/-/viem-2.57.4.tgz",
|
||||
"integrity": "sha512-ro+8AKrcGU6tPfou0gBRYv6bxnU3tPxxJGY4qpJwjiR40eHCWzeGhrAgYMlIncFtlnry2/yGjz57WOC67oshCQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/curves": "1.9.1",
|
||||
"@noble/hashes": "1.8.0",
|
||||
"@scure/bip32": "1.7.0",
|
||||
"@scure/bip39": "1.6.0",
|
||||
"abitype": "1.2.3",
|
||||
"isows": "1.0.7",
|
||||
"ox": "0.14.54",
|
||||
"ws": "8.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/viem/node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
tools/reference-apps/oracle/package.json
Normal file
9
tools/reference-apps/oracle/package.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "igneum-state-oracle",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Igneum reference apps: the Devnet 3 state oracle on Sepolia (contracts, deploy, demo, tests)",
|
||||
"scripts": { "compile": "node compile.mjs", "deploy": "node deploy.mjs", "demo": "node demo.mjs", "test": "node test.mjs" },
|
||||
"dependencies": { "@noble/hashes": "2.4.0", "solc": "0.8.28", "viem": "^2.21.0" }
|
||||
}
|
||||
129
tools/reference-apps/oracle/test.mjs
Normal file
129
tools/reference-apps/oracle/test.mjs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// The oracle's tests against Sepolia. Every negative runs through eth_call (no gas). The only transactions are the
|
||||
// one-time stub certificate for the synthetic vector and its submitStateRoot, skipped when already on chain.
|
||||
// Part A: the receipt fixture (real Devnet 3 headers, a real merkle path with an EVM transaction leaf)
|
||||
// Part B: the synthetic vector (segment record, coinbase, two-header path, MPT account and storage proofs)
|
||||
// Part C: the balance fixture when it exists (real segment record and eth_getProof)
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import * as L from './lib.mjs';
|
||||
|
||||
const d = L.deployment();
|
||||
const pub = L.publicClient();
|
||||
const abi = L.artifact('IgneumStateOracle').abi;
|
||||
const oracle = d.oracle.address;
|
||||
const read = (functionName, args) => pub.readContract({ address: oracle, abi, functionName, args });
|
||||
let pass = 0, fail = 0;
|
||||
const ok = (name, cond, detail = '') => { cond ? pass++ : fail++; console.log((cond ? 'PASS ' : 'FAIL ') + name + (detail ? ' (' + detail + ')' : '')); };
|
||||
async function expectRevert(name, fn, want) {
|
||||
try { await fn(); ok(name, false, 'did not revert'); }
|
||||
catch (e) { const r = L.reasonOf(e); ok(name, !want || r.includes(want), 'reverted: ' + r); }
|
||||
}
|
||||
const clone = x => JSON.parse(JSON.stringify(x));
|
||||
const flipByte = (hex, i) => { const b = L.hexToBytes(L.strip(hex)); b[i] ^= 0x01; return L.hex0x(b); };
|
||||
console.log(L.ukTime(), 'UK oracle', oracle, 'verifier', await read('verifier'));
|
||||
|
||||
// ---- Part A: the receipt fixture --------------------------------------------------------------------------------
|
||||
const f = JSON.parse(readFileSync(L.fixtures + '/dn3-receipt.json'));
|
||||
const H = f.headers.map(h => L.hex0x(L.serializeHeader(h)));
|
||||
const cp = '0x' + L.strip(f.checkpoint.hash);
|
||||
{
|
||||
let same = 0;
|
||||
for (let i = 0; i < H.length; i += 8) {
|
||||
const got = await Promise.all(H.slice(i, i + 8).map(h => read('headerHash', [h])));
|
||||
got.forEach((g, k) => { if (L.strip(g) === f.headers[i + k].hash) same++; });
|
||||
}
|
||||
ok('A1 headerHash on chain matches all fixture headers', same === H.length, same + ' of ' + H.length);
|
||||
const [carrier, merkle] = await read('checkHeaderPath', [H, cp]);
|
||||
ok('A2 checkHeaderPath accepts the 62-header path to checkpoint ' + f.checkpoint.index, L.strip(carrier) === f.headers[0].hash && L.strip(merkle) === f.headers[0].hash_merkle_root);
|
||||
console.log(' gas estimate for the view', await pub.estimateContractGas({ address: oracle, abi, functionName: 'checkHeaderPath', args: [H, cp], account: d.deployer }));
|
||||
await expectRevert('A3 a header removed from the path', () => read('checkHeaderPath', [H.filter((_, i) => i !== 30), cp]), 'does not name the previous one');
|
||||
const altered = clone(f.headers[5]); altered.nonce = String(BigInt(altered.nonce) + 1n);
|
||||
await expectRevert('A4 a header with its nonce altered', () => read('checkHeaderPath', [H.map((h, i) => (i === 5 ? L.hex0x(L.serializeHeader(altered)) : h)), cp]), 'does not name the previous one');
|
||||
await expectRevert('A5 the right path to the wrong checkpoint', () => read('checkHeaderPath', [H, flipByte(cp, 0)]), 'not the certified checkpoint');
|
||||
await expectRevert('A6 the path reversed', () => read('checkHeaderPath', [H.slice().reverse(), cp]), 'does not name the previous one');
|
||||
const ib = f.including_block; const sib = ib.merkle_siblings.map(s => '0x' + s);
|
||||
const root = await read('merkleRoot', ['0x' + f.tx_hash, BigInt(ib.leaf_index), sib]);
|
||||
ok('A7 merkleRoot reaches hash_merkle_root (leaf ' + ib.leaf_index + ' of ' + ib.leaf_count + ')', L.strip(root) === f.headers[0].hash_merkle_root);
|
||||
const root2 = await read('merkleRoot', ['0x' + f.tx_hash, BigInt(ib.leaf_index), sib.map((s, i) => (i === 2 ? flipByte(s, 3) : s))]);
|
||||
ok('A8 a sibling altered gives another root', L.strip(root2) !== f.headers[0].hash_merkle_root);
|
||||
const root3 = await read('merkleRoot', ['0x' + f.tx_hash, BigInt(ib.leaf_index ^ 1), sib]);
|
||||
ok('A9 the wrong leaf index gives another root', L.strip(root3) !== f.headers[0].hash_merkle_root);
|
||||
await expectRevert('A10 a leaf index beyond the path', () => read('merkleRoot', ['0x' + f.tx_hash, 1n << 40n, sib]), 'beyond the path');
|
||||
ok('A11 keccak of the raw transaction is the leaf', L.bytesToHex(L.keccak_256(L.hexToBytes(f.raw_tx_hex))) === f.tx_hash);
|
||||
}
|
||||
|
||||
// ---- Part B: the synthetic vector -----------------------------------------------------------------------------
|
||||
const S = L.buildSynthetic();
|
||||
const wallet = L.walletClient(L.deployerAccount());
|
||||
{
|
||||
const c = await L.ensureCertificate(pub, wallet, d, S.certIndex, L.hex0x(L.hexToBytes(S.checkpoint)));
|
||||
console.log(' stub certificate', S.certIndex, c.already ? 'already recorded' : 'recorded in ' + c.tx + ' block ' + c.block + ' gas ' + c.gasUsed);
|
||||
const args = L.proofArgs(S);
|
||||
const [number, postRoot, blockHash] = await read('verifyStateRoot', args);
|
||||
ok('B1 verifyStateRoot accepts the synthetic proof', number === S.number && L.strip(postRoot) === L.bytesToHex(S.postRoot) && L.strip(blockHash) === L.bytesToHex(S.blockHash), 'block ' + number + ' post_root ' + postRoot.slice(0, 14));
|
||||
console.log(' gas estimate for the view', await pub.estimateContractGas({ address: oracle, abi, functionName: 'verifyStateRoot', args, account: d.deployer }));
|
||||
const withArgs = (patch) => { const a = args.slice(); patch(a); return read('verifyStateRoot', a); };
|
||||
await expectRevert('B2 a certificate index the verifier does not hold', () => withArgs(a => { a[0] = 9999n; }), 'no certificate');
|
||||
await expectRevert('B3 the middle header removed', () => withArgs(a => { a[1] = [a[1][0], a[1][2]]; }), 'does not name the previous one');
|
||||
const carrierAltered = clone(S.headers[0]); carrierAltered.nonce = '1';
|
||||
await expectRevert('B4 the carrier header with its nonce altered', () => withArgs(a => { a[1] = [L.hex0x(L.serializeHeader(carrierAltered)), a[1][1], a[1][2]]; }), 'does not name the previous one');
|
||||
const topAltered = clone(S.headers[2]); topAltered.timestamp = String(BigInt(topAltered.timestamp) + 1n);
|
||||
await expectRevert('B5 the checkpoint header altered', () => withArgs(a => { a[1] = [a[1][0], a[1][1], L.hex0x(L.serializeHeader(topAltered))]; }), 'not the certified checkpoint');
|
||||
await expectRevert('B6 a merkle sibling altered', () => withArgs(a => { a[4] = [flipByte(a[4][0], 0), a[4][1]]; }), 'hash_merkle_root');
|
||||
await expectRevert('B7 the wrong leaf index', () => withArgs(a => { a[3] = 1n; }), 'hash_merkle_root');
|
||||
const cb = L.hexToBytes(L.strip(args[2]));
|
||||
const postRootOff = (() => { const want = L.bytesToHex(S.postRoot); const h = L.bytesToHex(cb); return h.indexOf(want) / 2; })();
|
||||
const tampered = cb.slice(); tampered[postRootOff + 5] ^= 0xff;
|
||||
await expectRevert('B8 the record\'s post_root altered inside the coinbase (offset ' + postRootOff + ')', () => withArgs(a => { a[2] = L.hex0x(tampered); }), 'hash_merkle_root');
|
||||
await expectRevert('B9 a record index beyond the IGNS section', () => withArgs(a => { a[5] = 1n; }), 'beyond the section');
|
||||
const tamperedNumber = cb.slice(); tamperedNumber[postRootOff - 148 + 8 + 7] ^= 0x01;
|
||||
await expectRevert('B10 the statement number altered', () => withArgs(a => { a[2] = L.hex0x(tamperedNumber); }), 'hash_merkle_root');
|
||||
const r = await L.ensureStateRoot(pub, wallet, d, S);
|
||||
console.log(' synthetic root for block', S.number, r.already ? 'already stored' : 'stored in ' + r.tx + ' block ' + r.block + ' gas ' + r.gasUsed + ' calldata ' + r.calldataBytes + ' bytes');
|
||||
const [root, bh, carrier, certIndex] = await read('stateRoot', [S.number]);
|
||||
ok('B11 stateRoot stored', L.strip(root) === L.bytesToHex(S.postRoot) && certIndex === S.certIndex && L.strip(carrier) === S.headers[0].hash);
|
||||
const addr = L.hex0x(S.address); const ap = S.accountProof.map(L.hex0x);
|
||||
const bal = await read('provenBalance', [S.number, addr, ap]);
|
||||
ok('B12 provenBalance returns the trie balance', bal === S.account.balance, bal + ' wei');
|
||||
console.log(' gas estimate for provenBalance', await pub.estimateContractGas({ address: oracle, abi, functionName: 'provenBalance', args: [S.number, addr, ap], account: d.deployer }));
|
||||
const acct = await read('provenAccount', [S.number, addr, ap]);
|
||||
ok('B13 provenAccount nonce, storage root and code hash', acct[0] === true && acct[1] === 7n && L.strip(acct[3]) === S.account.storageRoot && L.strip(acct[4]) === S.account.codeHash);
|
||||
for (const s of S.slots) {
|
||||
const v = await read('provenStorage', [S.number, addr, L.hex0x(s.slot), ap, s.proof.map(L.hex0x)]);
|
||||
ok('B14 provenStorage slot ' + s.slot[31], L.strip(v).replace(/^0+/, '') === L.bytesToHex(s.value).replace(/^0+/, ''), v);
|
||||
}
|
||||
const absent = await read('provenStorage', [S.number, addr, L.hex0x(S.absent.slot), ap, S.absent.proof.map(L.hex0x)]);
|
||||
ok('B15 provenStorage of an unset slot reads zero (exclusion proof)', BigInt(absent) === 0n);
|
||||
await expectRevert('B16 a flipped byte in the account proof\'s last node', () => read('provenBalance', [S.number, addr, ap.map((n, i) => (i === ap.length - 1 ? flipByte(n, 10) : n))]), 'mpt');
|
||||
await expectRevert('B17 a flipped byte in the root node', () => read('provenBalance', [S.number, addr, ap.map((n, i) => (i === 0 ? flipByte(n, 40) : n))]), 'not the root');
|
||||
await expectRevert('B18 the wrong block number', () => read('provenBalance', [S.number + 1n, addr, ap]), 'no proven root');
|
||||
await expectRevert('B19 the proof with its nodes reordered', () => read('provenBalance', [S.number, addr, ap.slice().reverse()]), 'not the root');
|
||||
await expectRevert('B20 a storage proof with a flipped node', () => read('provenStorage', [S.number, addr, L.hex0x(S.slots[0].slot), ap, S.slots[0].proof.map((n, i) => (i === 1 ? flipByte(L.hex0x(n), 5) : L.hex0x(n)))]), 'mpt');
|
||||
await expectRevert('B21 the account proof used as a storage proof', () => read('provenStorage', [S.number, addr, L.hex0x(S.slots[0].slot), ap, ap]), 'not the root');
|
||||
const other = L.hex0x(L.seeded(99)(20));
|
||||
await expectRevert('B22 another address under this account proof', () => read('provenBalance', [S.number, other, ap]), 'mpt');
|
||||
}
|
||||
|
||||
// ---- Part C: the balance fixture ------------------------------------------------------------------------------
|
||||
if (existsSync(L.fixtures + '/dn3-balance.json')) {
|
||||
const V = L.vectorFromBalanceFixture(JSON.parse(readFileSync(L.fixtures + '/dn3-balance.json')));
|
||||
const c = await L.ensureCertificate(pub, wallet, d, V.certIndex, L.hex0x(L.hexToBytes(V.checkpoint)), V.bitmap, V.signature);
|
||||
console.log(' certificate', V.certIndex, c.already ? 'already recorded' : 'recorded in ' + c.tx);
|
||||
const args = L.proofArgs(V);
|
||||
const [number, postRoot] = await read('verifyStateRoot', args);
|
||||
ok('C1 verifyStateRoot accepts the real Devnet 3 proof', number === V.number && L.strip(postRoot) === L.bytesToHex(V.postRoot), 'block ' + number);
|
||||
const r = await L.ensureStateRoot(pub, wallet, d, V);
|
||||
console.log(' real root for block', V.number, r.already ? 'already stored' : 'stored in ' + r.tx + ' gas ' + r.gasUsed);
|
||||
const bal = await read('provenBalance', [V.number, L.hex0x(V.address), V.accountProof.map(L.hex0x)]);
|
||||
ok('C2 provenBalance equals the node\'s eth_getProof balance', bal === V.balance, bal + ' wei');
|
||||
await expectRevert('C3 a flipped byte in the real account proof', () => read('provenBalance', [V.number, L.hex0x(V.address), V.accountProof.map((n, i) => (i === V.accountProof.length - 1 ? flipByte(L.hex0x(n), 10) : L.hex0x(n)))]), 'mpt');
|
||||
const realAltered = clone(V.headers[0]); realAltered.nonce = String(BigInt(realAltered.nonce) + 1n);
|
||||
await expectRevert('C4 the real carrier header with its nonce altered', () => read('verifyStateRoot', [args[0], [L.hex0x(L.serializeHeader(realAltered)), ...args[1].slice(1)], ...args.slice(2)]), V.headers.length === 1 ? 'not the certified checkpoint' : 'does not name');
|
||||
await expectRevert('C5 the real path emptied', () => read('verifyStateRoot', [args[0], [], ...args.slice(2)]), 'headers: none');
|
||||
await expectRevert('C6 the real certificate index off by one', () => read('verifyStateRoot', [args[0] + 1n, ...args.slice(1)]), 'no certificate');
|
||||
const cbReal = L.hexToBytes(L.strip(args[2])); const off = L.bytesToHex(cbReal).indexOf(L.bytesToHex(V.postRoot)) / 2; cbReal[off + 3] ^= 0x80;
|
||||
await expectRevert('C7 the real record\'s post_root altered inside the coinbase (offset ' + off + ')', () => read('verifyStateRoot', [...args.slice(0, 2), L.hex0x(cbReal), ...args.slice(3)]), 'hash_merkle_root');
|
||||
} else {
|
||||
console.log(' Part C skipped: fixtures/dn3-balance.json is not there yet');
|
||||
}
|
||||
console.log(L.ukTime(), 'UK', pass, 'passed,', fail, 'failed');
|
||||
process.exit(fail ? 1 : 0);
|
||||
18
tools/reference-apps/receipt/bundle.sh
Normal file
18
tools/reference-apps/receipt/bundle.sh
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env bash
|
||||
# Bundles the offline receipt verifier into one plain-JavaScript file (site/lc/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/lc/verify-receipt.js
|
||||
mkdir -p site/lc
|
||||
( 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/lc/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)"
|
||||
33
tools/reference-apps/receipt/verify-receipt.src.mjs
Normal file
33
tools/reference-apps/receipt/verify-receipt.src.mjs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Igneum payment receipt, the offline verifier (8 October 2026). This is the SOURCE; the one-file build at
|
||||
// site/lc/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/lc/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/lc/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).');
|
||||
Loading…
Reference in a new issue