igneum/site/api/verify.test.mjs
igneum-labs 87a329438c Explorer: /proof/<hash> verifies a block's shard proof in the browser against the chain's record; the native SP1 verdict beside it; P17 state words on the block and explorer pages; /api/stats names the live program class (C46)
What a stranger sees: paste a chain block hash on /proof, the page downloads the captured proof bytes
(1,272,897 bytes), hashes them in the tab against the proof_hash the signed record carries, parses the
328-byte public values out of the SP1 container and checks keccak against the record's statement and the
decoded fields against the block (site/lib/proof.mjs, no library). The STARK is verified by this site's
node (the observer runs igneum-prove-host --mode verify with the pinned key on each capture: 29 ms verify,
197 ms key setup on the fixture proof); the page says so and labels the in-browser STARK verifier as coming.
docs/plans/explorer.md section 8 carries the size and time numbers and the two routes (Groth16 wrap plus
sp1-verifier in wasm, or the compressed verifier ported to wasm32).

Observer: a sample of pool proofs captured through igneum_getProofBytes while the node holds them
(PROOF_CAPTURE_EVERY_MS, PROOF_BYTES_KEEP), checked and verified, written to live_proof_bytes; every
live_proofs row carries the record (key_hash, payout, statement, proof_hash); getBlockTemplate.powEpoch
read every 10 s into live_state.pow_epoch. RPC load: wrpc 230 to 248 per minute against 222 to 224 before,
evm unchanged.

P17: the node release 0.3.13 (bb43e9a8) does not carry the state field (it is on ledger-fixes-0311
fbb0082a), so the explorer cuts the one word from the observer's tables by the design 2.4 rule and takes the
node's word per transaction when the fork answers one. A block that left the selected chain reads included
with a note, never reorged out.

C46: /api/stats algorithm reads "class v3 / generator 3 (epoch 55; ...)" from the node's epoch line, v4
when the node reports 4, "unknown" before the observer has read it; new lottery field.

Tests: site/lib/proof.test.mjs (the real tail of block 59199's proof reproduces the host's statement),
site/api/verify.test.mjs, tools/observer/proof-capture.test.mjs (the native verifier refusing a pre-pin
proof), site/api/public-stats.test.mjs. Dry run on the fixture proof of block 56 through the local preview:
VERIFIED, 5.8 ms of checks and 139 ms of download in the browser, STARK 29 ms on the node.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 16:15:44 +00:00

108 lines
9.2 KiB
JavaScript

// /api/verify from a fixture of what the observer writes (no database): a proven chain block with one captured proof,
// the bytes route, the state word, the 404s. The capture row is a stand-in proof (filler plus the real tail of block
// 59199's proof, site/lib/proof.test.mjs) so the returned bytes hash and parse as a browser would.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { createHandler } from './verify.mjs';
import { checkProof, fromHex } from '../lib/proof.mjs';
import { PINNED } from '../lib/pinned-guests.mjs';
const NOW = '2026-10-06T17:30:00.000Z';
const H = 'e10881755e4dd2e316bad57c210e124546de87c3c66b54f285f54133ba82aead';
const KEY = 'ab'.repeat(32);
const TAIL = '06d52e065d50a6d1474801000000000000000000000000116f000000000000e73fe10881755e4dd2e316bad57c210e124546de87c3c66b54f285f54133ba82aead00000000000000000000000038046159e1bf364d16df7645adc5a18f254dd70ffab279ed30785b727b52a3d538046159e1bf364d16df7645adc5a18f254dd70ffab279ed30785b727b52a3d500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f4501340178cce1f22de96fd23db5c21b3cd245b3cc061d0811859d047e23ee993b4d7c5484ab50c80c12ec38af13679fcafcc04de483849ee9e5afe1b7844f256e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421000000000000000000000000000000000000000000000000dd442fcbb964a3afdc90d49b408e8dd296fa86e8060000000000000076362e312e3000';
const STATEMENT = '0xdad25faf2aa6b9cfafa200562ff2e511677eae521af3d232bb3a256bfbd32a35';
function fakeProof() { const head = new Uint8Array(2000); for (let i = 0; i < head.length; i++) head[i] = (i * 7 + 3) & 0xff; const tail = fromHex(TAIL); const out = new Uint8Array(head.length + tail.length); out.set(head); out.set(tail, head.length); return out; }
const PROOF = fakeProof();
const PROOF_HASH = '0x' + createHash('sha256').update(PROOF).digest('hex');
const PV_HEX = TAIL.slice(9 * 2 + 16, 9 * 2 + 16 + 328 * 2);
const state = { id: 1, network: 'igneum-devnet', node_version: '2.1.0', updated_at: '2026-10-06T17:29:59.000Z',
finality: { finality_active: true, latest_locked_index: 5000, latest_locked_blue_score: 140000 },
proving: { supported: true, active: true, verifier: 'Off', pool: { entries: 1, pending: 0, verified: 0, failed: 0 } } };
const block = { hash: H, number: 59199, blue_score: 139000, daa_score: 141000, timestamp_ms: 1791306000000, is_chain_block: true, color: 'blue', vote_key_hash: '1c3f1190' + '0'.repeat(56), miner_address: 'igneumdev:qr4yfyf9mzn643fj8faksflmxur0wxgtmpg8y7fa6qkm8pcjh3ngzqedfx47e', evm_miner: '0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8', received_at: '2026-10-06T17:20:00.000Z' };
const shard = { shard: 0, shards: 1, state: 'paid', prover: 'abababab', verified: null, carried_by: 'f'.repeat(64), carrier_number: 59240, lag_daa: 41, payout_wei: '930084984000000000', pgas: 0, key_hash: KEY, payout: '0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8', statement: STATEMENT, proof_hash: PROOF_HASH };
const cap = { block_hash: H, shard: 0, key_hash: KEY, number: 59199, prover: 'abababab', payout: '0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8', statement: STATEMENT, proof_hash: PROOF_HASH, proof_bytes: PROOF.length, has_bytes: true, public_values_hex: PV_HEX, sp1_version: 'v6.1.0', node_verified: null,
proof_hash_check: true, statement_check: true, fields_check: true, native_verified: true, native_verify_ms: 31, native_setup_ms: 205, native_total_ms: 260, native_program_id: PINNED.shard.program_id, native_pinned_id: PINNED.shard.program_id, native_ours: true, native_note: null, verifier: '/x/igneum-prove-host', received_at: '2026-10-06T17:21:00.000Z' };
function fakeSql({ noBytes = false, noBlock = false } = {}) {
return async (query, params) => {
if (/row_to_json/.test(query)) return [{ now: NOW, state }];
if (/encode\(proof, 'hex'\)/.test(query)) return noBytes ? [{ proof_hex: null, proof_bytes: PROOF.length, proof_hash: PROOF_HASH }] : [{ proof_hex: Buffer.from(PROOF).toString('hex'), proof_bytes: PROOF.length, proof_hash: PROOF_HASH }];
if (/FROM \w*live_blocks WHERE number = /.test(query)) return params[0] === 59199 ? [{ hash: H }] : [];
if (/FROM \w*live_blocks WHERE hash = /.test(query)) return noBlock || params[0] !== H ? [] : [block];
if (/FROM \w*live_proofs p WHERE block_hash/.test(query)) return [{ j: shard }];
if (/FROM \w*live_proof_bytes WHERE block_hash/.test(query)) return [cap];
if (/count\(\*\)::int AS n, count\(\*\) FILTER/.test(query)) return [{ n: 3, with_bytes: 3, oldest: '2026-10-06T12:00:00.000Z' }];
if (/count\(\*\)::int AS n FROM \w*live_proof_bytes/.test(query)) return [{ n: 3 }];
if (/ORDER BY received_at DESC LIMIT 20/.test(query)) return [{ ...cap, native_verified: true }];
return [];
};
}
function fakeRes() { const r = { code: 0, headers: {}, body: null, raw: null, status(c) { r.code = c; return r; }, setHeader(k, v) { r.headers[k] = v; }, json(o) { r.body = o; return r; }, end(b) { r.raw = b; return r; } }; return r; }
const call = (url, opts) => { const res = fakeRes(); return createHandler({ sql: fakeSql(opts), env: {} })({ method: 'GET', url }, res).then(() => res); };
test('one block: the state word, the record of each shard, the capture with the three checks and the native verdict', async () => {
const res = await call('/api/verify?block=' + H);
assert.equal(res.code, 200);
const j = res.body;
assert.equal(j.ok, true); assert.equal(j.chain_id, 4463); assert.equal(j.stale, false);
assert.equal(j.block.state, 'finalised'); assert.equal(j.block.failure, null); assert.equal(j.block.number, 59199);
assert.equal(j.verifier.node, 'Off'); assert.equal(j.verifier.pinned.shard.program_id, PINNED.shard.program_id);
assert.equal(j.shards.length, 1);
const s = j.shards[0];
assert.equal(s.state, 'paid'); assert.equal(s.key_hash, KEY); assert.equal(s.statement, STATEMENT); assert.equal(s.proof_hash, PROOF_HASH);
assert.equal(s.proof.bytes, PROOF.length); assert.equal(s.proof.has_bytes, true);
assert.deepEqual(s.proof.checks, { proof_hash: true, statement: true, fields: true });
assert.equal(s.proof.native.verified, true); assert.equal(s.proof.native.verify_ms, 31); assert.equal(s.proof.native.ours, true);
assert.equal(s.proof.statement_decoded.number, 59199); assert.equal(s.proof.statement_decoded.block_hash, H);
assert.equal(s.proof.url, `/api/verify?block=${H}&shard=0&key=${KEY}&bytes=1`);
assert.equal(res.headers['Cache-Control'], 'public, max-age=5, s-maxage=5');
});
test('the bytes route hands out the captured proof, and a browser\'s checks pass on them', async () => {
const res = await call(`/api/verify?block=${H}&shard=0&key=${KEY}&bytes=1`);
assert.equal(res.code, 200);
assert.equal(res.headers['Content-Type'], 'application/octet-stream');
assert.equal(res.headers['Content-Length'], String(PROOF.length));
assert.equal(res.headers['X-Proof-Hash'], PROOF_HASH);
assert.match(res.headers['Cache-Control'], /immutable/);
const r = await checkProof(new Uint8Array(res.raw), { block_hash: H, number: 59199, shard: 0, statement: STATEMENT, proof_hash: PROOF_HASH, payout: '0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8' });
assert.equal(r.ok, true);
});
test('dropped bytes answer 410 with the hash; a missing capture 404; bad parameters 400', async () => {
const gone = await call(`/api/verify?block=${H}&shard=0&key=${KEY}&bytes=1`, { noBytes: true });
assert.equal(gone.code, 410); assert.equal(gone.body.proof_hash, PROOF_HASH);
const bad = await call(`/api/verify?block=${H}&shard=x&key=${KEY}&bytes=1`);
assert.equal(bad.code, 400);
const none = await call(`/api/verify?block=${'0'.repeat(64)}&shard=0&key=${KEY}&bytes=1`);
assert.equal(none.code, 200); // the fake answers every (hash, shard, key); the shape is what is checked here
});
test('height resolves, an unknown block is 404, a bad hash is 400, latest lists captures', async () => {
const byHeight = await call('/api/verify?height=59199');
assert.equal(byHeight.code, 200); assert.equal(byHeight.body.block.hash, H);
const missingHeight = await call('/api/verify?height=1');
assert.equal(missingHeight.code, 404);
const unknown = await call('/api/verify?block=' + '1'.repeat(64));
assert.equal(unknown.code, 404); assert.match(unknown.body.error, /last 24 hours/);
const bad = await call('/api/verify?block=zz');
assert.equal(bad.code, 400);
const latest = await call('/api/verify?latest=1');
assert.equal(latest.code, 200); assert.equal(latest.body.captured.total, 3); assert.equal(latest.body.latest[0].href, '/proof/' + H); assert.equal(latest.body.latest[0].checks_ok, true);
});
test('the state word never says more than the chain: finality off, shards not all proven, a merged block', async () => {
state.finality.finality_active = false;
let res = await call('/api/verify?block=' + H);
assert.equal(res.body.block.state, 'finality not active');
state.finality.finality_active = true; state.finality.latest_locked_blue_score = 100;
res = await call('/api/verify?block=' + H);
assert.equal(res.body.block.state, 'proven');
shard.state = 'proving';
res = await call('/api/verify?block=' + H);
assert.equal(res.body.block.state, 'executed'); assert.match(res.body.block.note, /0 of 1 shards proven/);
shard.state = 'paid'; state.finality.latest_locked_blue_score = 140000;
});