diff --git a/site/api/explorer.mjs b/site/api/explorer.mjs index a570fa41d..6ee22e6b1 100644 --- a/site/api/explorer.mjs +++ b/site/api/explorer.mjs @@ -125,11 +125,14 @@ export function createHandler({ env = process.env, sql, evm = evmCall } = {}) { // ---- stats ---- if (q.has('stats')) { - const [chain10, fin] = await Promise.all([ + const [chain10, fin, paid10] = await Promise.all([ sql(`SELECT count(*)::int AS blocks_10m, count(*) FILTER (WHERE is_chain_block)::int AS chain_10m, count(DISTINCT vote_key_hash)::int AS miners_10m, max(daa_score) AS daa, max(number) AS height FROM ${T}live_blocks WHERE received_at > now() - interval '10 minutes'`), Promise.resolve(s && s.finality ? s.finality : null), + // payments landing in the window for blocks of any age (the observer's shards_paid_10m counts only blocks of the last 10 minutes, which reads 0 while the lag runs to 500 DAA) + sql(`SELECT count(*)::int AS n, count(DISTINCT prover)::int AS provers, sum(payout_wei::numeric)::text AS payout_wei FROM ${T}live_proofs WHERE state = 'paid' AND updated_at > now() - interval '10 minutes'`).catch(() => [{ n: null, provers: null, payout_wei: null }]), ]); const c = chain10[0] || {}; + const p10 = paid10[0] || {}; const w = fin && fin.weights ? fin.weights : null; const activations = params ? Object.entries(params).filter(([k, v]) => /_activation_daa$/.test(k) && Number.isFinite(Number(v))).map(([k, v]) => ({ name: k.replace(/_activation_daa$/, ''), daa: Number(v) })) : []; return res.status(200).json({ @@ -140,6 +143,7 @@ export function createHandler({ env = process.env, sql, evm = evmCall } = {}) { miners_10m: num(c.miners_10m), finality: fin ? { active: !!fin.finality_active, voters: w ? num(w.voters) : null, total_weight: w ? num(w.total_weight) : null, active_weight: w ? num(w.active_weight) : null, keys: w && Array.isArray(w.keys) ? w.keys.length : null, latest_locked_index: num(fin.latest_locked_index), latest_locked_blue_score: num(fin.latest_locked_blue_score), params: fin.params || null } : null, proving: s && s.proving ? s.proving : null, + paid_10m_any_age: { shards: num(p10.n), provers: num(p10.provers), payout_wei: p10.payout_wei || '0', note: 'shards paid in the last 10 minutes for blocks of any age' }, class: classAt(num(c.daa), params), activations, digest: ix && ix.node_info ? ix.node_info.digest : null, genesis: ix && ix.node_info ? ix.node_info.genesis : null, budgets: null, }); @@ -147,7 +151,7 @@ export function createHandler({ env = process.env, sql, evm = evmCall } = {}) { // ---- proving view ---- if (q.has('proving')) { - const [blocks, provers24, provers1, lat, totals] = await Promise.all([ + const [blocks, provers24, provers1, lat, totals, paid10] = await Promise.all([ sql(`SELECT b.hash, b.number, b.daa_score, b.blue_score, b.timestamp_ms, b.vote_key_hash, b.evm_miner, b.tx_count, b.proof_records, count(p.shard)::int AS total, count(*) FILTER (WHERE p.state = 'planned')::int AS planned, count(*) FILTER (WHERE p.state = 'proving')::int AS proving, count(*) FILTER (WHERE p.state = 'verified')::int AS verified, count(*) FILTER (WHERE p.state = 'paid')::int AS paid, @@ -161,6 +165,7 @@ export function createHandler({ env = process.env, sql, evm = evmCall } = {}) { sql(`SELECT count(*)::int AS paid_1h, percentile_cont(0.5) WITHIN GROUP (ORDER BY lag_daa) AS p50, percentile_cont(0.9) WITHIN GROUP (ORDER BY lag_daa) AS p90, max(lag_daa) AS max_lag, min(lag_daa) AS min_lag FROM ${T}live_proofs WHERE state = 'paid' AND updated_at > now() - interval '1 hour'`), sql(`SELECT state, count(*)::int AS n, sum(payout_wei::numeric)::text AS payout_wei FROM ${T}live_proofs WHERE received_at > now() - interval '24 hours' GROUP BY state`), + sql(`SELECT count(*)::int AS n, count(DISTINCT prover)::int AS provers, sum(payout_wei::numeric)::text AS payout_wei FROM ${T}live_proofs WHERE state = 'paid' AND updated_at > now() - interval '10 minutes'`).catch(() => []), ]); const hour = new Map(provers1.map(p => [p.prover, p])); return res.status(200).json({ @@ -169,6 +174,7 @@ export function createHandler({ env = process.env, sql, evm = evmCall } = {}) { provers: provers24.map(p => ({ id: p.prover, shards_24h: num(p.shards), payout_wei_24h: p.payout_wei || '0', median_lag_24h: p.median_lag === null ? null : Number(p.median_lag), shards_1h: hour.has(p.prover) ? num(hour.get(p.prover).shards) : 0, payout_wei_1h: hour.has(p.prover) ? hour.get(p.prover).payout_wei || '0' : '0', last_paid: p.last_paid })), latency_1h: lat[0] ? { paid: num(lat[0].paid_1h), p50_daa: lat[0].p50 === null ? null : Number(lat[0].p50), p90_daa: lat[0].p90 === null ? null : Number(lat[0].p90), min_daa: num(lat[0].min_lag), max_daa: num(lat[0].max_lag), note: 'lag in DAA score from the proven block to the block whose coinbase paid the shard; Devnet 3 targets one DAA step per second' } : null, totals_24h: Object.fromEntries(totals.map(t => [t.state, { shards: num(t.n), payout_wei: t.payout_wei || '0' }])), + paid_10m_any_age: paid10[0] ? { shards: num(paid10[0].n), provers: num(paid10[0].provers), payout_wei: paid10[0].payout_wei || '0' } : null, }); } diff --git a/site/block.html b/site/block.html index 47aa4c99d..b4544108b 100644 --- a/site/block.html +++ b/site/block.html @@ -352,14 +352,14 @@ const id = decodeURIComponent(location.pathname.split('/').pop() || ''); const query = /^\d{1,12}$/.test(id) ? 'height=' + id : 'block=' + encodeURIComponent(id.replace(/^0x/i, '')); async function recheck(index, blockHash, isCheckpoint) { // the verified badge: this browser fetches the certificate that covers the block and checks it with the site's light client - const el = $('recheck'); el.innerHTML = `
Re-checking certificate ${int(index)} in this browser.
`; + const el = $('recheck'); el.innerHTML = `Re-checking certificate ${int(index)} in this browser. The STARK proofs of the shards are not re-run here; the node's verdict on them is shown in the shard table as the node's.
`; const badge = document.createElement('span'); badge.className = 'badge wait'; badge.textContent = 'checking the certificate'; $('chips').appendChild(badge); try { const { fetchCheckpoint, verify } = await import('/verify/verify.js'); const data = await fetchCheckpoint('/api/checkpoint?index=' + index); const r = verify(data); if (r.verified) { - badge.className = 'badge ok'; badge.textContent = isCheckpoint ? 'verified in this browser' : `verified under lock ${int(index)} in this browser`; + badge.className = 'badge ok'; badge.textContent = "certificate re-checked in this browser; the node's verdict on the STARK"; $('rc-line').innerHTML = `Certificate ${int(index)} re-checked in this browser in ${r.ms} ms: ${int(r.headers_checked)} header hashes recomputed, ${int(r.signers)} of ${int(r.voters)} voters' BLS keys aggregated, the signature verifies over ${(r.weight_fraction_total * 100).toFixed(1)}% of all weight. ${isCheckpoint ? 'This block is the certified checkpoint.' : 'This block sits under that lock on the selected chain.'} The light client is /verify/core.js; the data it checked is /api/checkpoint?index=${index}.`; } else { badge.className = 'badge bad'; badge.textContent = 'certificate did not verify'; $('rc-line').textContent = `Certificate ${int(index)} did not verify in this browser: ${r.error || 'unknown reason'}.`; } } catch (e) { badge.className = 'badge wait'; badge.textContent = 're-check unavailable'; $('rc-line').textContent = 'The re-check could not run: ' + (e.message || e); } diff --git a/site/explorer.html b/site/explorer.html index a716b4feb..6c0a35476 100644 --- a/site/explorer.html +++ b/site/explorer.html @@ -268,7 +268,7 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin