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>
74 lines
7.2 KiB
JavaScript
74 lines
7.2 KiB
JavaScript
// The observer's proof capture: the host-output parsers against lines the host printed on 6 October 2026, the
|
|
// capture against a fake node, and (on a Mac with the app installed) the native verifier on a real proof the
|
|
// pinned key must refuse (block 59199 shard 0, made before the guests were pinned: "IS NOT OURS").
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { createHash } from 'node:crypto';
|
|
import { parseVerifyOutput, parseIdOutput, nativeVerify, captureProof, MAC_APP_HOST } from './proof-capture.mjs';
|
|
import { fromHex } from '../../site/lib/proof.mjs';
|
|
|
|
const NOT_OURS = `STAGE setup start 2026-10-06T15:46:55Z
|
|
RESULT setup: 0.213 s (light verifier, pinned key), shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a at 2026-10-06T15:46:55Z
|
|
STAGE verify start 2026-10-06T15:46:55Z
|
|
RESULT verify: NOT VERIFIED in 0.001 s; block 59199 shard 0 prover 0xdd442fCbb964A3aFDc90D49B408e8DD296FA86E8 statement 0xdad25faf2aa6b9cfafa200562ff2e511677eae521af3d232bb3a256bfbd32a35 (want 0x0000000000000000000000000000000000000000000000000000000000000000) program id 0x0559759b3d8740b26ceceb2c56054b89194878ab691b018d7dd2f8af2f2242dd IS NOT OURS 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a (the prover runs another guest build) proof 1272897 bytes at 2026-10-06T15:46:55Z
|
|
`;
|
|
const OURS = `RESULT setup: 0.205 s (light verifier, pinned key), shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a at 2026-10-06T17:00:00Z
|
|
RESULT verify: VERIFIED in 0.031 s; block 135100 shard 0 prover 0xdd442fCbb964A3aFDc90D49B408e8DD296FA86E8 statement 0x1111111111111111111111111111111111111111111111111111111111111111 (want 0x1111111111111111111111111111111111111111111111111111111111111111) program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a (ours) proof 1272897 bytes at 2026-10-06T17:00:00Z
|
|
`;
|
|
const ID = 'RESULT id: pinned guests: shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a (2832504 bytes, sha256 0x150f4c05a2951fc5) aggregator id 0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896 (319744 bytes), pinned 2026-10-05T16:20:38Z on Darwin somewhere arm64, SP1 6.8.1 circuit v6.1.0\n';
|
|
|
|
test('parseVerifyOutput: the refused proof of another guest build', () => {
|
|
const p = parseVerifyOutput(NOT_OURS);
|
|
assert.equal(p.verified, false); assert.equal(p.verify_ms, 1); assert.equal(p.setup_ms, 213);
|
|
assert.equal(p.ours, false);
|
|
assert.equal(p.claimed_id, '0x0559759b3d8740b26ceceb2c56054b89194878ab691b018d7dd2f8af2f2242dd');
|
|
assert.equal(p.pinned_id, '0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a');
|
|
assert.equal(p.block, 59199); assert.equal(p.shard, 0); assert.equal(p.proof_bytes, 1272897);
|
|
assert.equal(p.statement_got, '0xdad25faf2aa6b9cfafa200562ff2e511677eae521af3d232bb3a256bfbd32a35');
|
|
assert.match(p.note, /another guest build/);
|
|
});
|
|
test('parseVerifyOutput: a verified proof of ours', () => {
|
|
const p = parseVerifyOutput(OURS);
|
|
assert.equal(p.verified, true); assert.equal(p.verify_ms, 31); assert.equal(p.ours, true); assert.equal(p.note, '');
|
|
assert.equal(p.claimed_id, p.pinned_id);
|
|
});
|
|
test('parseVerifyOutput: no result line', () => {
|
|
const p = parseVerifyOutput('error: --proof <file>\n');
|
|
assert.equal(p.verified, null); assert.match(p.note, /error/);
|
|
});
|
|
test('parseIdOutput', () => {
|
|
const i = parseIdOutput(ID);
|
|
assert.equal(i.shard_program_id, '0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a');
|
|
assert.equal(i.shard_elf_bytes, 2832504); assert.equal(i.sp1_crate_version, '6.8.1'); assert.equal(i.sp1_circuit_version, 'v6.1.0'); assert.equal(i.pinned_at, '2026-10-05T16:20:38Z');
|
|
assert.equal(parseIdOutput('nothing'), null);
|
|
});
|
|
|
|
// a fake node: a 2,000-byte stand-in STARK plus the real tail of block 59199's proof (site/lib/proof.test.mjs)
|
|
const TAIL = '06d52e065d50a6d1474801000000000000000000000000116f000000000000e73fe10881755e4dd2e316bad57c210e124546de87c3c66b54f285f54133ba82aead00000000000000000000000038046159e1bf364d16df7645adc5a18f254dd70ffab279ed30785b727b52a3d538046159e1bf364d16df7645adc5a18f254dd70ffab279ed30785b727b52a3d500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f4501340178cce1f22de96fd23db5c21b3cd245b3cc061d0811859d047e23ee993b4d7c5484ab50c80c12ec38af13679fcafcc04de483849ee9e5afe1b7844f256e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421000000000000000000000000000000000000000000000000dd442fcbb964a3afdc90d49b408e8dd296fa86e8060000000000000076362e312e3000';
|
|
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('captureProof against a fake node: every browser check passes, no verifier configured', async () => {
|
|
const bytes = fakeProof();
|
|
const proofHash = '0x' + createHash('sha256').update(bytes).digest('hex');
|
|
const entry = { shard: 0, keyHash: '0x' + 'ab'.repeat(32), payout: '0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8', statement: '0xdad25faf2aa6b9cfafa200562ff2e511677eae521af3d232bb3a256bfbd32a35', proofHash, verified: null };
|
|
const calls = [];
|
|
const evm = async (m, p) => { calls.push([m, p]); return { number: '0xe73f', shard: 0, keyHash: entry.keyHash, verified: null, statement: entry.statement, proofHash, proof: '0x' + Buffer.from(bytes).toString('hex') }; };
|
|
const row = await captureProof({ evm, blockHash: 'e10881755e4dd2e316bad57c210e124546de87c3c66b54f285f54133ba82aead', number: 59199, entry, host: '' });
|
|
assert.deepEqual(calls[0], ['igneum_getProofBytes', ['0xe73f', 0, '0x' + 'ab'.repeat(32)]]);
|
|
assert.equal(row.proof_bytes, bytes.length); assert.equal(row.proof_hash_check, true); assert.equal(row.statement_check, true); assert.equal(row.fields_check, true);
|
|
assert.equal(row.sp1_version, 'v6.1.0'); assert.equal(row.decoded.number, 59199); assert.equal(row.prover, 'abababab');
|
|
assert.equal(row.native_verified, null); assert.match(row.native_note, /no verifier configured/);
|
|
// the node no longer holds it
|
|
const gone = await captureProof({ evm: async () => { throw new Error('no such proof in the pool'); }, blockHash: 'e1', number: 1, entry, host: '' });
|
|
assert.match(gone.error, /no such proof/);
|
|
});
|
|
|
|
const HOST = process.env.IGNEUM_PROOF_VERIFIER || (existsSync(MAC_APP_HOST) ? MAC_APP_HOST : '');
|
|
const REAL = `${process.env.HOME}/Library/Application Support/Igneum/app/proving/block-59199-shard-0-compressed.bin`;
|
|
test('nativeVerify: the pinned verifier refuses a proof of another guest build (the known-failed case)', { skip: !(HOST && existsSync(REAL)) && 'needs the Mac app\'s igneum-prove-host and the saved proof of block 59199' }, async () => {
|
|
const bytes = new Uint8Array(readFileSync(REAL));
|
|
const v = await nativeVerify({ host: HOST, bytes, statement: '0xdad25faf2aa6b9cfafa200562ff2e511677eae521af3d232bb3a256bfbd32a35' });
|
|
assert.equal(v.verified, false); assert.equal(v.ours, false); assert.equal(v.block, 59199); assert.ok(v.total_ms > 0);
|
|
assert.equal(v.claimed_id, '0x0559759b3d8740b26ceceb2c56054b89194878ab691b018d7dd2f8af2f2242dd');
|
|
});
|