igneum/site/lib/proof.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

119 lines
8.7 KiB
JavaScript

// The proof checks a browser runs on /proof/<hash>, against a real tail. TAIL is the last 360 bytes of the compressed
// shard proof of devnet block 59199 shard 0 (made 5 October 2026 by the Mac's host before the guests were pinned;
// 1,272,897 bytes). The host printed for it: statement 0xdad25faf2aa6b9cfafa200562ff2e511677eae521af3d232bb3a256bfbd32a35,
// block 59199 shard 0 prover 0xdd442fCbb964A3aFDc90D49B408e8DD296FA86E8 (igneum-prove-host --mode verify, 6 October
// 2026), so the keccak of the parsed public values must reproduce that statement.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { parseProofTail, decodeStatement, statementOf, proofHashOf, checkProof, blockState, txState, lotteryLine, fromHex, toHex, keccak256, STATEMENT_LEN } from './proof.mjs';
import { keccak256 as keccakEth } from './eth.mjs';
const TAIL = '06d52e065d50a6d1474801000000000000000000000000116f000000000000e73fe10881755e4dd2e316bad57c210e124546de87c3c66b54f285f54133ba82aead00000000000000000000000038046159e1bf364d16df7645adc5a18f254dd70ffab279ed30785b727b52a3d538046159e1bf364d16df7645adc5a18f254dd70ffab279ed30785b727b52a3d500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f4501340178cce1f22de96fd23db5c21b3cd245b3cc061d0811859d047e23ee993b4d7c5484ab50c80c12ec38af13679fcafcc04de483849ee9e5afe1b7844f256e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421000000000000000000000000000000000000000000000000dd442fcbb964a3afdc90d49b408e8dd296fa86e8060000000000000076362e312e3000';
const STATEMENT = '0xdad25faf2aa6b9cfafa200562ff2e511677eae521af3d232bb3a256bfbd32a35';
const BLOCK = 'e10881755e4dd2e316bad57c210e124546de87c3c66b54f285f54133ba82aead';
// a stand-in proof file: 2,000 bytes of deterministic filler where the STARK would be, then the real tail
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;
}
test('keccak here equals the faucet library\'s keccak', () => {
for (const s of ['', 'abc', 'x'.repeat(135), 'y'.repeat(136), 'z'.repeat(500)]) {
const b = new TextEncoder().encode(s);
assert.equal(toHex(keccak256(b)), toHex(keccakEth(b)));
}
assert.equal(toHex(keccak256(new Uint8Array(0))), '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470');
});
test('the tail parse finds the 328 public values and the SP1 version', () => {
const t = parseProofTail(fakeProof());
assert.equal(t.publicValues.length, STATEMENT_LEN);
assert.equal(t.version, 'v6.1.0');
assert.equal(t.proofEnd, 2000 + 9); // the 9 bytes of the real proof's end that TAIL starts with
});
test('the public values hash to the statement the host printed, and decode to block 59199 shard 0', () => {
const t = parseProofTail(fakeProof());
assert.equal(statementOf(t.publicValues), STATEMENT);
const s = decodeStatement(t.publicValues);
assert.equal(s.chain_id, 4463);
assert.equal(s.number, 59199);
assert.equal(s.block_hash, BLOCK);
assert.equal(s.shard, 0);
assert.equal(s.tx_start, 0);
assert.equal(s.tx_count, 0);
assert.equal(s.prover, '0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8');
assert.equal(s.gas_used, 0); assert.equal(s.pgas_used, 0); assert.equal(s.executed, 0); assert.equal(s.skipped, 0);
assert.notEqual(s.pre_root, s.post_root); // no transactions, but the segment's rewards and payouts move the state
});
test('a file that is not an SP1 proof is refused with a reason', () => {
assert.throws(() => parseProofTail(new Uint8Array(100)), /too short/);
const b = fakeProof(); b[b.length - 1] = 1;
assert.throws(() => parseProofTail(b), /TEE/);
const c = new Uint8Array(600); c[599] = 0;
assert.throws(() => parseProofTail(c), /no SP1 version/);
assert.throws(() => decodeStatement(new Uint8Array(10)), /328 bytes/);
});
test('checkProof passes the genuine record and fails each tampered one', async () => {
const bytes = fakeProof();
const ph = '0x' + createHash('sha256').update(bytes).digest('hex');
assert.equal(await proofHashOf(bytes), ph);
const record = { block_hash: BLOCK, number: 59199, shard: 0, statement: STATEMENT, proof_hash: ph, payout: '0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8' };
const ok = await checkProof(bytes, record);
assert.equal(ok.ok, true); assert.equal(ok.checks.length, 3); assert.equal(ok.statement.number, 59199); assert.equal(ok.bytes, bytes.length);
// one byte of the STARK flipped: the proof hash no longer matches, the statement still does
const flipped = new Uint8Array(bytes); flipped[100] ^= 1;
const r1 = await checkProof(flipped, record);
assert.equal(r1.ok, false); assert.equal(r1.checks[0].ok, false); assert.equal(r1.checks[1].ok, true);
// a record for another block: the statement and the fields disagree
const r2 = await checkProof(bytes, { ...record, block_hash: 'ab'.repeat(32), statement: '0x' + '11'.repeat(32) });
assert.equal(r2.ok, false); assert.equal(r2.checks[0].ok, true); assert.equal(r2.checks[1].ok, false); assert.equal(r2.checks[2].ok, false);
// a public value altered inside the file: statement mismatch
const pv = new Uint8Array(bytes); pv[bytes.length - 1 - 6 - 8 - 1] ^= 1;
const r3 = await checkProof(pv, { ...record, proof_hash: await proofHashOf(pv) });
assert.equal(r3.ok, false); assert.equal(r3.checks[1].ok, false);
});
test('blockState: the one word per block, never stronger than the chain', () => {
const chain = (x) => blockState({ chain: true, color: 'blue', number: 10, blue_score: 100, shards: ['paid'], locked_blue_score: 200, finality_active: true, ...x });
assert.deepEqual(chain({}).state, 'finalised');
assert.equal(chain({ finality_active: false }).state, 'finality not active');
assert.equal(chain({ locked_blue_score: 50 }).state, 'proven');
assert.equal(chain({ locked_blue_score: 50, shards: ['paid', 'planned'] }).state, 'executed');
assert.match(chain({ locked_blue_score: 50, shards: ['paid', 'planned'] }).note, /1 of 2 shards proven/);
assert.equal(chain({ locked_blue_score: null, shards: [] }).state, 'executed');
assert.equal(chain({ number: null }).state, 'included');
assert.equal(chain({ locked_blue_score: 50, finality_active: false }).failure, 'finality paused');
assert.equal(chain({ locked_blue_score: 50, finality_active: true }).failure, null);
const merged = blockState({ chain: false, color: 'blue', number: null, blue_score: 100, shards: [], locked_blue_score: 200, finality_active: true, merged_locked: null });
assert.equal(merged.state, 'included');
assert.equal(blockState({ chain: false, color: 'blue', number: null, merged_locked: true, finality_active: true }).state, 'finalised');
assert.equal(blockState({ chain: false, color: 'red', number: null }).state, 'included');
assert.match(blockState({ chain: false, color: 'red', number: null }).note, /red/);
assert.equal(blockState({ chain: false, color: 'pending', number: null }).state, 'pending');
assert.equal(blockState({ chain: false, color: 'pending', number: 12 }).failure, null);
assert.match(blockState({ chain: false, color: 'pending', number: 12 }).note, /left the selected chain at number 12/);
assert.equal(blockState({ chain: false, color: 'blue', number: 12 }).state, 'included');
assert.match(blockState({ chain: false, color: 'blue', number: 12 }).note, /reorg.*merged blue/);
});
test('txState: the node\'s word when it has one, else the block\'s', () => {
assert.deepEqual(txState({ state: 'proven', failure: null }, null, true), { state: 'proven', failure: null, source: 'node' });
assert.deepEqual(txState({ state: 'included', failure: 'skipped' }, null, false).failure, 'skipped');
assert.equal(txState(null, { state: 'finalised', failure: null }, true).state, 'finalised');
assert.equal(txState(null, { state: 'finalised', failure: null }, false).state, 'included');
assert.equal(txState(null, { state: 'pending', failure: null }, true).state, 'included');
assert.equal(txState({ executed: true }, { state: 'executed' }, true).source, 'block'); // a pre-P17 node has flags, no state
});
test('lotteryLine names the live class and generator, never a fixed one', () => {
assert.match(lotteryLine({ program_class: 3, next_program_class: 3, epoch_index: 55 }), /class v3 \/ generator 3 \(epoch 55;/);
assert.match(lotteryLine({ program_class: 2, next_program_class: 3, epoch_index: 42 }), /class v2 \/ generator 2 \(epoch 42, next epoch class v3;/);
assert.match(lotteryLine({ program_class: 4, next_program_class: 4, epoch_index: 900 }), /class v4 \/ generator 4/);
assert.match(lotteryLine(null), /unknown until the observer reads/);
assert.doesNotMatch(lotteryLine(null), /generator v2/);
});