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)}