igneum/site/api/live.mjs
igneum-labs 51d61f0dca Finality v2: fork reading guide, spec implementation notes, bench entry, observer checkpoints, live page locks
- docs/fork-divergence.md: "Finality v2" table (every file, risk, merge note), decisions
- docs/spec/03-finality.md: section 3.10 implementation notes, clause by clause
- docs/bench-log.md: test-network results (72 of 72 steady locks, median 0.80 s; equivocation
  strip; partition: 0 locks at 39.6% of total with the floor binding, heal in 30 s), follower
- tools/observer: live_checkpoints table, FinalityLock subscription, "checkpoint N locked" events
- site: /api/live adds checkpoints and locked/final flags; /live draws the lock ring and final line

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 21:54:24 +00:00

137 lines
6.5 KiB
JavaScript

// Igneum live devnet feed. GET /api/live returns what tools/observer wrote to Neon:
// {now, state, blocks (last 90 s), miners (last 10 min), events (last 30), finality (checkpoints, newest lock)}.
// Four queries, all on indexed columns. Cached one second at the edge.
// Zero dependencies: Neon's HTTP SQL endpoint over Node's built-in fetch.
const STALE_AFTER_S = 30;
function neon() {
const url = process.env.DATABASE_URL;
if (!url) throw new Error('DATABASE_URL is not set');
const host = new URL(url).hostname.replace('-pooler', '');
return async (query, params = []) => {
const r = await fetch(`https://${host}/sql`, {
method: 'POST',
headers: { 'Neon-Connection-String': url, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, params }),
});
const j = await r.json();
if (!r.ok) throw new Error(j.message || JSON.stringify(j));
return j.rows || [];
};
}
const num = v => (v === null || v === undefined ? null : Number(v));
const short = h => (h ? String(h).slice(0, 16) : null);
const minerId = vk => (vk ? String(vk).slice(0, 8) : null);
export default async function handler(req, res) {
res.setHeader('Cache-Control', 'public, max-age=1');
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.method !== 'GET') {
res.setHeader('Allow', 'GET');
return res.status(405).json({ ok: false, error: 'method not allowed' });
}
try {
const sql = neon();
const [head, blockRows, minerRows, checkpointRows] = await Promise.all([
sql(`SELECT now() AS now,
(SELECT row_to_json(s) FROM live_state s WHERE s.id = 1) AS state,
(SELECT json_agg(e) FROM (SELECT ts, kind, text FROM live_events ORDER BY ts DESC LIMIT 30) e) AS events`),
sql(`SELECT hash, blue_score, daa_score, timestamp_ms, parent_hashes, is_chain_block, vote_key_hash, received_at
FROM live_blocks
WHERE received_at > now() - interval '90 seconds'
ORDER BY received_at DESC
LIMIT 600`),
sql(`SELECT vote_key_hash, count(*)::int AS blocks, max(received_at) AS last_seen, max(engine) AS engine, max(miner_address) AS address
FROM live_blocks
WHERE received_at > now() - interval '10 minutes' AND vote_key_hash IS NOT NULL
GROUP BY vote_key_hash
ORDER BY blocks DESC, last_seen DESC`),
// Finality v2: the most recent checkpoints; an older observer has no table yet, so this query may fail
sql(`SELECT index, hash, blue_score, daa_score, state, signed_weight, active_weight, total_weight, fraction_active, fraction_total, votes_seen, voters, locked_at, updated_at
FROM live_checkpoints
ORDER BY index DESC
LIMIT 40`).catch(() => []),
]);
const now = new Date(head[0].now).getTime();
const s = head[0].state || null;
const updated = s && s.updated_at ? new Date(s.updated_at).getTime() : null;
const age_s = updated === null ? null : Math.max(0, (now - updated) / 1000);
const stale = age_s === null || age_s > STALE_AFTER_S;
const total10m = minerRows.reduce((a, m) => a + m.blocks, 0);
const state = {
stale,
age_s: age_s === null ? null : Math.round(age_s * 10) / 10,
network: s ? s.network : null,
node_version: s ? s.node_version : null,
block_count: s ? num(s.block_count) : null,
header_count: s ? num(s.header_count) : null,
blue_score: s ? num(s.blue_score) : null,
difficulty: s ? num(s.difficulty) : null,
hashes_per_second_estimate: s ? num(s.hashes_per_second_estimate) : null,
peers: s ? num(s.peers) : null,
mempool: s ? num(s.mempool) : null,
blocks_60s: s ? num(s.blocks_60s) : null,
blocks_per_second_60s: s && s.blocks_60s !== null ? Math.round(num(s.blocks_60s) / 60 * 1000) / 1000 : null,
blocks_per_minute: s ? s.blocks_per_minute || [] : [],
miners_10m: minerRows.length,
observer_started_at: s ? s.observer_started_at : null,
updated_at: s ? s.updated_at : null,
};
const fin = (s && s.finality) || null;
const checkpoints = checkpointRows.reverse().map(c => ({
index: num(c.index), hash: short(c.hash), blue_score: num(c.blue_score), daa: num(c.daa_score), state: c.state,
signed: num(c.signed_weight), active: num(c.active_weight), total: num(c.total_weight),
fraction_active: num(c.fraction_active), fraction_total: num(c.fraction_total), votes: num(c.votes_seen), voters: num(c.voters),
locked_at: c.locked_at, updated_at: c.updated_at,
}));
const lockedHashes = new Set(checkpoints.filter(c => c.state === 'locked').map(c => c.hash));
const newestLock = checkpoints.filter(c => c.state === 'locked').slice(-1)[0] || null;
const finality = {
supported: !!fin,
active: fin ? !!fin.finality_active : false,
next_index: fin ? num(fin.next_index) : null,
latest_locked_index: newestLock ? newestLock.index : (fin ? num(fin.latest_locked_index) || null : null),
latest_locked_hash: newestLock ? newestLock.hash : null,
latest_locked_blue_score: newestLock ? newestLock.blue_score : null,
params: fin ? fin.params || null : null,
weights: fin ? fin.weights || null : null,
checkpoints,
};
const blocks = blockRows.reverse().map(b => ({
hash: short(b.hash),
blue_score: num(b.blue_score),
daa: num(b.daa_score),
ts: num(b.timestamp_ms),
rx: new Date(b.received_at).getTime(),
parents: (b.parent_hashes || []).map(short),
chain: !!b.is_chain_block,
miner: minerId(b.vote_key_hash),
// Finality v2: a locked checkpoint block, and whether the block sits at or before the newest lock
locked: lockedHashes.has(short(b.hash)),
final: !!(newestLock && b.is_chain_block && num(b.blue_score) <= newestLock.blue_score),
}));
const miners = minerRows.map(m => ({
id: minerId(m.vote_key_hash),
vote_key_hash: m.vote_key_hash,
blocks: m.blocks,
share: total10m ? Math.round(m.blocks / total10m * 1000) / 10 : 0,
last_seen: m.last_seen,
engine: m.engine || null,
address: m.address || null,
}));
const events = (head[0].events || []).map(e => ({ ts: e.ts, kind: e.kind, text: e.text }));
return res.status(200).json({ ok: true, now: new Date(now).toISOString(), state, blocks, miners, events, finality });
} catch (e) {
res.setHeader('Cache-Control', 'no-store');
return res.status(500).json({ ok: false, error: String(e.message || e) });
}
}