igneum/site/lc/test.html

88 lines
8.4 KiB
HTML

<!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');
// the public RPC allows 20 requests a second: walk at most 40 blocks below the checkpoint, 80 ms apart
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 - 40 && !tx; k--) { const b = await rpc('eth_getBlockByNumber', ['0x' + k.toString(16), false]); if (b && b.transactions.length) tx = b.transactions[0]; await new Promise(r => setTimeout(r, 80)); } }
if (!tx) throw new Error('no transaction found in the 40 chain blocks below the checkpoint; pass ?tx=');
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>