/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 <noreply@anthropic.com>
This commit is contained in:
parent
894fe08f34
commit
303038a75d
5 changed files with 38 additions and 5 deletions
|
|
@ -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%
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ main{padding-bottom:var(--sec)}
|
|||
<div class="legend">
|
||||
<div><span class="lk">blocks</span><span><i class="sw" style="background:var(--ember);border:1.5px solid #5CB8FF"></i>chain</span><span><i class="sw" style="background:rgba(92,184,255,.7);border:1.5px solid #5CB8FF"></i>included, paid</span><span><i class="sw" style="border:1.5px solid var(--ash)"></i>pending</span><span><i class="sw" style="border:1.5px solid rgba(154,154,158,.6);background:linear-gradient(45deg,transparent 44%,var(--ash) 44%,var(--ash) 56%,transparent 56%) center/8px 8px no-repeat"></i>excluded</span><span><i class="sw" style="border:2px solid var(--molten);box-shadow:0 0 8px rgba(255,179,92,.7)"></i>just arrived</span><span><i class="sw" style="width:22px;height:0;border-top:2px solid var(--ember)"></i>selected chain</span><span class="txt">one lane per miner, colour from its id; hover a block, or tap it to keep its details up</span></div>
|
||||
<div><span class="lk">finality</span><span><i class="sw" style="width:22px;height:10px;background:rgba(242,84,27,.22);border-radius:2px"></i>final</span><span><i class="sw" style="width:3px;height:12px;background:var(--molten);border-radius:1px"></i>locked checkpoint</span><span><i class="sw" style="width:22px;height:10px;background:rgba(154,154,158,.10);border:1px solid rgba(154,154,158,.3);border-radius:2px"></i>proposed</span><span class="txt">reads "paused" whenever under two thirds of the weight is signing</span></div>
|
||||
<div><span class="lk">proving</span><span><i class="sw" style="width:12px;height:9px;border:1px solid rgba(154,154,158,.55);border-radius:2px"></i>shard planned</span><span><i class="sw" style="width:12px;height:9px;border:1px solid var(--molten);background:rgba(255,179,92,.25);border-radius:2px"></i>proving</span><span><i class="sw" style="width:12px;height:9px;background:#3FD39A;border-radius:2px"></i>verified, prover colour</span><span><i class="sw" style="width:12px;height:9px;background:#3FD39A;border-radius:2px;position:relative"><i style="position:absolute;left:3px;top:1px;width:3px;height:5px;border:solid #0C0C0E;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)"></i></i>paid</span><span><i class="sw" style="width:0;height:12px;border-left:1px dashed var(--molten)"></i>where proofs land behind the tip</span><span class="txt">hover a cell, or tap it to keep its details up</span></div>
|
||||
<div><span class="lk">proving</span><span><i class="sw" style="width:12px;height:9px;border:1px solid rgba(154,154,158,.55);border-radius:2px"></i>shard planned</span><span><i class="sw" style="width:12px;height:9px;border:1px solid var(--molten);background:rgba(255,179,92,.25);border-radius:2px"></i>proving</span><span><i class="sw" style="width:12px;height:9px;background:#3FD39A;border-radius:2px"></i>verified, prover colour</span><span><i class="sw" style="width:12px;height:9px;background:#3FD39A;border-radius:2px;position:relative"><i style="position:absolute;left:3px;top:1px;width:3px;height:5px;border:solid #0C0C0E;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)"></i></i>paid</span><span><i class="sw" style="width:0;height:12px;border-left:1px dashed var(--molten)"></i>where proofs land behind the tip</span><span class="txt">hover 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</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue