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>
119 lines
8.7 KiB
JavaScript
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/);
|
|
});
|