From 303038a75d0b8fdeb9c4a6c1cc9bce628aa8a2f9 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:35:21 +0000 Subject: [PATCH] /api/stats gains proving (coverage_10m, provers, lag, verifier) from live_state.proving; the live page says whose verifier is off (reviewer C33) A third party reading the public API could not see that proving coverage on the devnet is 2.7% of chain blocks with one prover (13 of 476 in ten minutes at 22:33 UTC) while the homepage says every block is proven. The object is the one /api/live reads, with coverage_10m = blocks_fully_proven_10m / blocks_10m and a note that the observer's node reads the chain's payouts with its own verifier off. Documented with the live figures, in the contract test and the live check. Co-Authored-By: Claude Fable 5.1 --- docs/api/public-stats.md | 6 ++++++ site/api/public-stats.test.mjs | 9 ++++++++- site/api/stats.mjs | 21 ++++++++++++++++++++- site/live.html | 2 +- tools/ci/public-api-check.mjs | 5 +++-- 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/docs/api/public-stats.md b/docs/api/public-stats.md index d41fb08a..b421df03 100644 --- a/docs/api/public-stats.md +++ b/docs/api/public-stats.md @@ -31,8 +31,14 @@ https://igneum.network on master). | `block_reward` | `E(daa)` of spec 2.5 at the newest DAA score: `sompi` (8 decimals), `ign`, the 80% `miner_ign` and 20% `proving_pool_ign`, `ramp_factor`, `halving_period`, `next_halving_daa`, `next_halving_in_s` | `site/lib/emission.mjs` | | `last_block` | Hash, time, age, blue score, DAA, coinbase address, vote key id, EVM transaction count, whether it is a chain block | `live_blocks` | | `finality` | Finality v2: active, the latest locked checkpoint index and blue score | observer | +| `proving` | Proving v0, the same object `/api/live` and `/live` read: `active`, `activation_daa`, the last ten minutes' `blocks_10m` (chain blocks), `blocks_fully_proven_10m` (every shard proven and paid), `shards_proven_10m`, `shards_paid_10m`, `provers_10m`, `median_proof_lag_s`, `coverage_10m` (= fully proven / blocks), `verifier` (the observer node's own SP1 verifier, "Off" on the devnet: it reads the chain's payouts, not its own verification), `pool_pending`, `note` | `live_state.proving` | | `peers`, `mempool`, `miners_10m` | Connected peers, mempool size, distinct vote keys in 10 min (a card runs several) | observer | +The `proving` object on the live devnet at 22:33 UTC on 5 October 2026 (reviewer C33): `blocks_10m` 476, +`blocks_fully_proven_10m` 13, `shards_paid_10m` 13, `provers_10m` 1, `median_proof_lag_s` 46, `coverage_10m` 0.0273, +`verifier` "Off", `pool_pending` 43. Coverage is what a third party should read before "every block is proven": tonight +it is 2.7% of chain blocks with one prover, 46 s behind the tip. + Note for a profitability calculator: `block_reward` is per blue block merged; at the 1 block per second target that is also the reward per DAA second. The 20% proving-pool part is paid to provers, not to the miner of the block, so a miner's expected income per block is `miner_ign`. During the 30-day launch ramp the reward climbs from 10% to 100% diff --git a/site/api/public-stats.test.mjs b/site/api/public-stats.test.mjs index c7853f41..3c4cf3e3 100644 --- a/site/api/public-stats.test.mjs +++ b/site/api/public-stats.test.mjs @@ -2,7 +2,7 @@ // (FIELDS in each handler), computed from a fixture of what the observer writes. Runs without a database. import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { createHandler as stats, FIELDS as STATS_FIELDS, REWARD_FIELDS, LAST_BLOCK_FIELDS, rewardAt } from './stats.mjs'; +import { createHandler as stats, FIELDS as STATS_FIELDS, REWARD_FIELDS, LAST_BLOCK_FIELDS, PROVING_FIELDS, rewardAt, provingOf } from './stats.mjs'; import { createHandler as supply, FIELDS as SUPPLY_FIELDS, SCHEDULE_FIELDS } from './supply.mjs'; const NOW = '2026-10-05T19:21:56.000Z'; @@ -11,6 +11,7 @@ const state = { node_version: '2.1.0', network: 'igneum-devnet', blocks_60s: 58, blocks_per_minute: Array.from({ length: 60 }, (_, i) => [1791227000000 + i * 60000, 60]), observer_started_at: '2026-10-05T19:01:00.000Z', updated_at: '2026-10-05T19:21:55.000Z', finality: { chain_id: 'igneum-devnet', finality_active: true, latest_locked_index: 4073, latest_locked_blue_score: 122190 }, + proving: { supported: true, active: true, activation_daa: 84100, tip_daa: 137124, verifier: 'Off', pool: { failed: 0, entries: 43, pending: 43, verified: 0 }, paid_shards_total: 619, shard_budget_pgas: 7500000, blocks_10m: 476, blocks_fully_proven_10m: 13, shards_proven_10m: 13, shards_paid_10m: 13, median_proof_lag_s: 46, provers_10m: 1 }, supply_check: { checked_at: NOW, sampled: 500, rule_match: 500, rule_mismatch: 0, sum_match: 480, sum_mismatch: 0, sum_skipped: 20, examples: [], bps: 1 }, }; const last = { hash: '2622db7698c6825b370bbffba03ea11bba250792040296971d2b790a954203ee', blue_score: 122211, daa_score: 125064, timestamp_ms: 1791227608731, @@ -25,6 +26,12 @@ test('/api/stats carries every documented field and the reward of spec 2.5', asy for (const f of STATS_FIELDS) assert.ok(f in res.body, `missing ${f}`); for (const f of REWARD_FIELDS) assert.ok(f in res.body.block_reward, `missing block_reward.${f}`); for (const f of LAST_BLOCK_FIELDS) assert.ok(f in res.body.last_block, `missing last_block.${f}`); + for (const f of PROVING_FIELDS) assert.ok(f in res.body.proving, `missing proving.${f}`); + assert.equal(res.body.proving.coverage_10m, 0.0273); // 13 of 476 chain blocks fully proven (the live devnet at 22:33 UTC, 5 Oct 2026) + assert.equal(res.body.proving.provers_10m, 1); + assert.equal(res.body.proving.verifier, 'Off'); + assert.equal(provingOf(null).supported, false); + assert.equal(provingOf({ supported: false, reason: 'node without proving' }).note, 'node without proving'); assert.equal(res.body.chain_id, 4463); assert.equal(res.body.height, 81263); assert.equal(res.body.block_reward.sompi, '454486399'); // E(125,064), the payload of block 2622db76 diff --git a/site/api/stats.mjs b/site/api/stats.mjs index eb18517e..aa63b5e6 100644 --- a/site/api/stats.mjs +++ b/site/api/stats.mjs @@ -9,7 +9,8 @@ import { blockSubsidy, rampFactor, HALVING_INTERVAL_SECONDS, LAUNCH_RAMP_SECONDS export const STALE_AFTER_S = 30; export const FIELDS = ['ok', 'now', 'network', 'chain_id', 'node_version', 'algorithm', 'stale', 'age_s', 'height', 'block_count', 'header_count', 'daa', 'blue_score', 'difficulty', 'hashrate', 'hashrate_unit', 'hashrate_source', 'block_time_target_s', 'block_time_measured_s', 'blocks_per_day_target', 'blocks_per_day_measured', - 'block_reward', 'last_block', 'finality', 'peers', 'mempool', 'miners_10m', 'observer_updated_at', 'source']; + 'block_reward', 'last_block', 'finality', 'proving', 'peers', 'mempool', 'miners_10m', 'observer_updated_at', 'source']; +export const PROVING_FIELDS = ['supported', 'active', 'activation_daa', 'blocks_10m', 'blocks_fully_proven_10m', 'shards_proven_10m', 'shards_paid_10m', 'provers_10m', 'median_proof_lag_s', 'coverage_10m', 'verifier', 'pool_pending', 'note']; export const REWARD_FIELDS = ['sompi', 'ign', 'miner_ign', 'proving_pool_ign', 'split', 'daa_used', 'ramp_factor', 'halving_period', 'next_halving_daa', 'next_halving_in_s']; export const LAST_BLOCK_FIELDS = ['hash', 'time', 'ts_ms', 'age_s', 'blue_score', 'daa', 'miner', 'miner_id', 'tx_count', 'chain']; @@ -25,6 +26,23 @@ export function rewardAt(daa) { }; } +// Proving v0 as the observer reports it (live_state.proving, the same object /api/live and /live read): the last ten +// minutes of chain blocks, how many had every shard proven and paid, and by how many provers. coverage_10m is +// blocks_fully_proven_10m / blocks_10m. The observer's node runs with its SP1 verifier off: it reads the chain's +// payouts (a shard counts as paid when a later chain block paid it), not its own verification. +export function provingOf(pv) { + if (!pv || !pv.supported) return { supported: false, active: false, activation_daa: null, blocks_10m: null, blocks_fully_proven_10m: null, shards_proven_10m: null, shards_paid_10m: null, provers_10m: null, median_proof_lag_s: null, coverage_10m: null, verifier: null, pool_pending: null, note: pv && pv.reason ? pv.reason : 'the observer has not reported the proving layer' }; + const blocks = num(pv.blocks_10m), full = num(pv.blocks_fully_proven_10m); + return { + supported: true, active: !!pv.active, activation_daa: num(pv.activation_daa), + blocks_10m: blocks, blocks_fully_proven_10m: full, shards_proven_10m: num(pv.shards_proven_10m), shards_paid_10m: num(pv.shards_paid_10m), provers_10m: num(pv.provers_10m), + median_proof_lag_s: num(pv.median_proof_lag_s), + coverage_10m: blocks ? Math.round(full / blocks * 10000) / 10000 : null, + verifier: pv.verifier || null, pool_pending: pv.pool ? num(pv.pool.pending) : null, + note: 'chain blocks of the last 10 minutes and how many had every shard proven and paid; the observer\'s node reads the chain\'s payouts with its own verifier off, so a paid shard means a carrying block paid it, not that this node verified it', + }; +} + export function shape({ now, state: s, last, miners10m }) { const updated = s && s.updated_at ? new Date(s.updated_at).getTime() : null; const age = updated === null ? null : Math.max(0, (now - updated) / 1000); @@ -55,6 +73,7 @@ export function shape({ now, state: s, last, miners10m }) { tx_count: num(last.tx_count), chain: !!last.is_chain_block, } : null, finality: s && s.finality ? { active: !!s.finality.finality_active, latest_locked_index: num(s.finality.latest_locked_index), latest_locked_blue_score: num(s.finality.latest_locked_blue_score), chain_id: s.finality.chain_id || null } : { active: false, latest_locked_index: null, latest_locked_blue_score: null, chain_id: null }, + proving: provingOf(s && s.proving), peers: s ? num(s.peers) : null, mempool: s ? num(s.mempool) : null, miners_10m: miners10m, observer_updated_at: s ? s.updated_at : null, source: 'tools/observer reading one node every 2 s; reward from docs/spec/02-consensus.md 2.5 at the node\'s DAA score', diff --git a/site/live.html b/site/live.html index 55d540d9..84ca6db2 100644 --- a/site/live.html +++ b/site/live.html @@ -242,7 +242,7 @@ main{padding-bottom:var(--sec)}
blockschainincluded, paidpendingexcludedjust arrivedselected chainone lane per miner, colour from its id; hover a block, or tap it to keep its details up
finalityfinallocked checkpointproposedreads "paused" whenever under two thirds of the weight is signing
-
provingshard plannedprovingverified, prover colourpaidwhere proofs land behind the tiphover a cell, or tap it to keep its details up
+
provingshard plannedprovingverified, prover colourpaidwhere proofs land behind the tiphover a cell, or tap it to keep its details up. States come from the observer's node, which runs with its own verifier off: a shard reads as paid when a later chain block paid it, not because this node verified the proof
diff --git a/tools/ci/public-api-check.mjs b/tools/ci/public-api-check.mjs index 488307ed..177e40e0 100644 --- a/tools/ci/public-api-check.mjs +++ b/tools/ci/public-api-check.mjs @@ -4,7 +4,7 @@ // node tools/ci/public-api-check.mjs [https://igneum.network] // Exit 1 on a missing field, a non-200 answer or a stale observer older than 10 minutes (the numbers are what pool // software and profitability sites read; a stale answer is a wrong answer). -import { FIELDS as STATS, REWARD_FIELDS, LAST_BLOCK_FIELDS } from '../../site/api/stats.mjs'; +import { FIELDS as STATS, REWARD_FIELDS, LAST_BLOCK_FIELDS, PROVING_FIELDS } from '../../site/api/stats.mjs'; import { FIELDS as SUPPLY, SCHEDULE_FIELDS } from '../../site/api/supply.mjs'; const base = (process.argv[2] || process.env.PUBLIC_API_URL || 'https://igneum.network').replace(/\/$/, ''); @@ -19,9 +19,10 @@ const s = await get('/api/stats'); for (const f of STATS) if (!(f in s)) missing.push(`stats.${f}`); if (s.block_reward) for (const f of REWARD_FIELDS) if (!(f in s.block_reward)) missing.push(`stats.block_reward.${f}`); if (s.last_block) for (const f of LAST_BLOCK_FIELDS) if (!(f in s.last_block)) missing.push(`stats.last_block.${f}`); +if (s.proving) for (const f of PROVING_FIELDS) if (!(f in s.proving)) missing.push(`stats.proving.${f}`); else missing.push('stats.proving'); const p = await get('/api/supply'); for (const f of SUPPLY) if (!(f in p)) missing.push(`supply.${f}`); if (p.schedule && p.schedule[0]) for (const f of SCHEDULE_FIELDS) if (!(f in p.schedule[0])) missing.push(`supply.schedule.${f}`); if (missing.length) { console.error(`public api check: ${missing.length} missing field(s): ${missing.join(', ')}`); process.exit(1); } if (s.stale && s.age_s > 600) { console.error(`public api check: the observer is stale (${s.age_s} s)`); process.exit(1); } -console.log(`public api check: ${base} /api/stats (${STATS.length} fields, height ${s.height}, reward ${s.block_reward && s.block_reward.ign} IGN, ${s.stale ? 'STALE ' + s.age_s + ' s' : 'live'}) and /api/supply (${SUPPLY.length} fields, circulating ${p.circulating_ign} IGN of ${p.max_supply_ign}) answer with every documented field`); +console.log(`public api check: ${base} /api/stats (${STATS.length} fields, height ${s.height}, reward ${s.block_reward && s.block_reward.ign} IGN, proving coverage ${s.proving && s.proving.coverage_10m}, ${s.stale ? 'STALE ' + s.age_s + ' s' : 'live'}) and /api/supply (${SUPPLY.length} fields, circulating ${p.circulating_ign} IGN of ${p.max_supply_ign}) answer with every documented field`);