igneum/tools/proving-v1/coverage.mjs
igneum-labs e6d1396bf1 Proving v1: prover on by default, the aggregated segment record, host modes chain, aggregate and verify-segment, the app's aggregator step, spec 7.8, the fast-time harness and the coverage tool
Step 1: app/igneum-app/src/provedefault.rs decides once per install (NVIDIA 12 GB or more, WSL2 answering on
Windows, Linux native, Apple silicon off until measured), never switching an explicit on back off; the Settings
switch line and the tile line say why (5 unit tests). tools/proving-v1/pc2-prover-cost.ps1 is the PC 2 job
(5 min mining alone, 5 min with the prover, GPU memory and host RAM peaks, the sp1-gpu-server's SM targets).

Step 2: the host gains --mode chain (consecutive fixtures, each block aggregated with the previous block's
proof by recursion), --mode aggregate (the live aggregator over shard proof files, a run of blocks in one
process) and --mode verify-segment (the node's verifier against the pinned aggregator key); the app's prover
loop gains aggregate_once (spec 7.8). Eight consecutive live fixtures (blocks 81046 to 81053, node 1's export
at tip 81076) under proving/fixtures/chain/. tools/proving-v1/pc2-chain.ps1 is the PC 2 job (held).

Steps 3 and 4: tools/proving-v1/coverage.mjs (the proven-block share and the on-chain latency from one node's
RPC), tools/proving-v1/net.mjs (the fast-time 3-node harness on 29950+ with the known-finished and
known-failed cases of the chain rule and the unproven rule), the four proving_v1 fields in
infra/fast-time/override-60x.json. Spec 7.8, the 7.4 rows, the 5.3 sentence, docs/plans/proving-v1.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 20:21:04 +00:00

89 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
// Proving v1 step 3: the live devnet's proven-block share and the proof latency distribution over a window, read
// from one node's execution-layer RPC (what the chain carried: every node agrees on it) and, beside it, the live
// page's 10-minute proving object. Read-only: no job, no switch.
//
// node tools/proving-v1/coverage.mjs [--rpc http://127.0.0.1:26790] [--minutes 30] [--live https://igneum.network/api/live]
// [--out <json>] [--watch]
//
// Without --watch: one pass over the last --minutes of chain blocks (by block timestamp) at the tip: per block the
// shard plan (shards), the paid rows (carrierNumber) and the carrier's timestamp; prints the table for the bench log.
// With --watch: samples the live page every 60 s for --minutes, then the pass.
//
// Latency here is on-chain: the carrying chain block's timestamp minus the proven block's timestamp (the record was
// verified by its producer before that, so this bounds "block time to verified record" from above).
import { writeFileSync } from 'node:fs';
const args = process.argv.slice(2);
const opt = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : d; };
const RPC = opt('--rpc', 'http://127.0.0.1:26790');
const LIVE = opt('--live', 'https://igneum.network/api/live');
const MINUTES = +opt('--minutes', 30);
const OUT = opt('--out', null);
const WATCH = args.includes('--watch');
const log = (...a) => console.log(new Date().toISOString().slice(11, 19), ...a);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let id = 0;
async function rpc(method, params = []) {
const r = await fetch(RPC, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params }), signal: AbortSignal.timeout(30000) });
const j = await r.json(); if (j.error) throw new Error(j.error.message); return j.result;
}
const hexn = (v) => (v == null ? null : Number(BigInt(v)));
const pct = (a, b) => (b ? (100 * a / b).toFixed(1) + '%' : 'n/a');
function quantiles(xs) {
if (!xs.length) return { n: 0 };
const s = [...xs].sort((a, b) => a - b);
const q = (p) => s[Math.min(s.length - 1, Math.floor(p * (s.length - 1)))];
return { n: s.length, min: s[0], p50: q(0.5), p90: q(0.9), p99: q(0.99), max: s[s.length - 1], mean: +(s.reduce((a, b) => a + b, 0) / s.length).toFixed(1) };
}
async function pass() {
const tip = hexn(await rpc('eth_blockNumber'));
const tipBlock = await rpc('eth_getBlockByNumber', ['0x' + tip.toString(16), false]);
const tipTs = hexn(tipBlock.timestamp);
const from = tipTs - MINUTES * 60;
// walk back by timestamp
const ts = new Map();
const tsOf = async (n) => { if (!ts.has(n)) { const b = await rpc('eth_getBlockByNumber', ['0x' + n.toString(16), false]); ts.set(n, hexn(b.timestamp)); } return ts.get(n); };
let first = tip;
while (first > 1 && (await tsOf(first - 1)) >= from) first--;
log(`window: chain blocks ${first}..${tip} (${tip - first + 1} blocks, ${MINUTES} min by block timestamp), tip DAA via status`);
const status = await rpc('igneum_getProvingStatus');
const rows = [];
for (let n = first; n <= tip; n++) {
const r = await rpc('igneum_getProofRecords', ['0x' + n.toString(16)]);
const plan = await rpc('igneum_getShardPlan', ['0x' + n.toString(16)]).catch(() => null);
const shards = plan ? plan.shards.length : 0;
const paid = (r.paid || []).filter(Boolean);
const bt = await tsOf(n);
const lat = [];
for (const p of paid) { const c = hexn(p.carrierNumber); if (c != null) lat.push((await tsOf(c)) - bt); }
rows.push({ n, shards, pgas: plan ? plan.shards.reduce((a, s) => a + hexn(s.pgas), 0) : 0, paid: paid.length, latencies: lat, carried: (r.carried || []).length });
}
const blocks = rows.length;
const any = rows.filter((x) => x.paid > 0).length;
const full = rows.filter((x) => x.shards > 0 && x.paid === x.shards).length;
const shardsTotal = rows.reduce((a, x) => a + x.shards, 0), shardsPaid = rows.reduce((a, x) => a + x.paid, 0);
const lat = quantiles(rows.flatMap((x) => x.latencies));
const content = rows.filter((x) => x.pgas > 0);
const contentPaid = content.filter((x) => x.paid === x.shards).length;
const summary = { rpc: RPC, window: { first, last: tip, blocks, minutes: MINUTES, from_ts: from, to_ts: tipTs }, blocks_with_any_paid_shard: any, blocks_fully_proven: full, share_any: pct(any, blocks), share_full: pct(full, blocks), shards_total: shardsTotal, shards_paid: shardsPaid, share_shards: pct(shardsPaid, shardsTotal), content_blocks: content.length, content_blocks_fully_proven: contentPaid, latency_s: lat, status: { paidShards: status.paidShards, pool: status.pool, verifier: status.verifier, tipDaa: hexn(status.tipDaa), v1: status.v1 || null } };
log(`RESULT coverage: ${blocks} blocks, ${any} with a paid shard (${summary.share_any}), ${full} fully proven (${summary.share_full}); shards ${shardsPaid}/${shardsTotal} (${summary.share_shards}); content blocks ${content.length}, fully proven ${contentPaid}; on-chain latency s: ${JSON.stringify(lat)}`);
return { summary, rows };
}
const samples = [];
if (WATCH) {
const end = Date.now() + MINUTES * 60 * 1000;
while (Date.now() < end) {
const lv = await fetch(LIVE, { signal: AbortSignal.timeout(20000) }).then((r) => r.json()).catch((e) => ({ error: e.message }));
const p = lv.proving || {};
const s = { t: new Date().toISOString(), block_count: lv.state?.block_count, blocks_per_second_60s: lv.state?.blocks_per_second_60s, shards_proven_10m: p.shards_proven_10m, blocks_fully_proven_10m: p.blocks_fully_proven_10m, median_proof_lag_s: p.median_proof_lag_s, provers_10m: p.provers_10m, error: lv.error };
samples.push(s);
log(`live: ${JSON.stringify(s)}`);
await sleep(60000);
}
}
const result = await pass();
result.live_samples = samples;
if (OUT) { writeFileSync(OUT, JSON.stringify(result, null, 2)); log(`written ${OUT}`); }