150 lines
9.4 KiB
JavaScript
150 lines
9.4 KiB
JavaScript
// Igneum light client, server half. GET /api/checkpoint returns the latest certified checkpoint with everything a
|
|
// browser needs to verify it by itself (site/verify/verify.js does the verifying; this function only ships data):
|
|
// the certificate as carried in a block, the canonical voter list with public keys and weights, and the
|
|
// selected-chain headers from the previous locked checkpoint to this one. Read from what tools/observer wrote to
|
|
// Neon (table live_certificates, or fintest_live_certificates for the test network).
|
|
//
|
|
// ?source=live the live chain (default: the dn4_ tables, or LIVE_TABLE_PREFIX); certificate:null with a reason before its first lock, never the fixture
|
|
// ?source=dn4 the same as live (the Igneum 2.0 devnet's name, for callers that want to say so)
|
|
// ?source=test the finality test network's fixture (fintest_live_certificates), only with the ops key in x-igneum-ops-key; 404 otherwise
|
|
// ?index=N one certified checkpoint by index instead of the newest (the explorer's per-block re-check, 8 October 2026)
|
|
//
|
|
// Zero dependencies: Neon's HTTP SQL endpoint over Node's built-in fetch, like live.mjs.
|
|
|
|
const MAX_HEADERS = 200;
|
|
|
|
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 tablePrefix = () => (process.env.LIVE_TABLE_PREFIX === undefined ? 'dn4_' : process.env.LIVE_TABLE_PREFIX).replace(/[^a-z0-9_]/gi, '');
|
|
// the live chain's name from its table prefix (dn4_ is igneum-devnet-4); LIVE_NETWORK overrides
|
|
const liveNetwork = () => process.env.LIVE_NETWORK || (/^dn(\d+)_$/.test(tablePrefix()) ? `igneum-devnet-${tablePrefix().slice(2, -1)}` : 'igneum-devnet-4');
|
|
// the finality test network's fixture (fintest_live_certificates) is served only to a request carrying the ops key (main, 8 Oct 2026 18:4x:
|
|
// a fixture chain answered on the public 2.0 site as source test, network igneum-devnet-7); with no IGNEUM_OPS_KEY set it is never served
|
|
const opsOk = req => { const k = process.env.IGNEUM_OPS_KEY || ''; const h = String((req.headers && req.headers['x-igneum-ops-key']) || ''); return k.length > 0 && h === k; };
|
|
// the latest DAA the observer has seen on the live chain, or null (the window fills to DAA 7,200 before the first lock; no wall-clock estimate, main 8 Oct 2026)
|
|
async function liveDaa(sql) {
|
|
const rows = await sql(`SELECT max(daa_score) AS daa FROM ${tablePrefix()}live_blocks`).catch(() => []);
|
|
const d = rows && rows[0] && rows[0].daa; return d === null || d === undefined ? null : Number(d);
|
|
}
|
|
const noCertificate = (source, daa) => ({ source, network: liveNetwork(), certificate: null, live_daa: daa, finality_window_daa: 7200, reason: 'no finality certificate yet; the first lock comes when the weight window fills at DAA 7,200' });
|
|
async function latest(sql, table, index = null) {
|
|
// A table the observer has not created yet (the live chain before the cut-over) reads as "no certificate"
|
|
const rows = await (index === null ? sql(`SELECT * FROM ${table} ORDER BY index DESC LIMIT 1`) : sql(`SELECT * FROM ${table} WHERE index = $1 LIMIT 1`, [index])).catch(() => []);
|
|
return rows[0] || null;
|
|
}
|
|
|
|
function shape(row, source) {
|
|
const headers = (row.headers || []).slice(-MAX_HEADERS).map(h => ({
|
|
hash: h.hash, version: num(h.version), parents_by_level: h.parentsByLevel || [],
|
|
hash_merkle_root: h.hashMerkleRoot, accepted_id_merkle_root: h.acceptedIdMerkleRoot, utxo_commitment: h.utxoCommitment,
|
|
timestamp: String(h.timestamp), bits: num(h.bits), nonce: String(h.nonce), daa_score: String(h.daaScore),
|
|
blue_work: h.blueWork, blue_score: String(h.blueScore), pruning_point: h.pruningPoint, vote_key_hash: h.voteKeyHash,
|
|
}));
|
|
const voters = (row.voters || []).map(v => ({ vote_key_hash: v.key_hash, pubkey_hex: v.pubkey, weight: num(v.weight), participation: num(v.participation) }));
|
|
return {
|
|
source,
|
|
network: row.chain_id,
|
|
chain_id: row.chain_id,
|
|
index: num(row.index),
|
|
hash: row.hash,
|
|
blue_score: num(row.blue_score),
|
|
daa_score: num(row.daa_score),
|
|
certificate: {
|
|
bytes_hex: row.certificate_hex,
|
|
voter_count: num(row.voter_count),
|
|
bitmap_hex: row.bitmap_hex,
|
|
aggregate_signature_hex: row.signature_hex,
|
|
aggregator: row.aggregator,
|
|
aggregator_proof_hex: row.aggregator_proof_hex,
|
|
carrier: row.carrier,
|
|
},
|
|
voters,
|
|
voters_at_index: num(row.voters_index),
|
|
total_weight: num(row.total_weight),
|
|
active_weight: num(row.active_weight),
|
|
previous: row.prev_index === null || row.prev_index === undefined ? null : { index: num(row.prev_index), hash: row.prev_hash },
|
|
headers,
|
|
headers_complete: !!row.headers_complete && (row.headers || []).length <= MAX_HEADERS,
|
|
rule: { quorum_active: '2/3', floor_total: 0.567, floor_total_exact: '17/30', vote_message: 'igneum-vote-v1/<chain_id> 0x00 index_le64 checkpoint_hash', dst: 'IGNEUM_VOTE_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_', key_hash: 'BLAKE2b-256 keyed IgneumVoteKeyHash over the 48-byte G1 key' },
|
|
stored_at: row.created_at,
|
|
};
|
|
}
|
|
|
|
/** The earliest certified Devnet 3 checkpoint whose chain block number is at least `number` (the reference apps' receipt:
|
|
* the smallest proof that a block is final), from the dn3_ rows; null when no certificate reaches that block yet.
|
|
* `chainNumberOf(hash)` resolves a checkpoint hash to its chain block number (the exec RPC); rows are tried newest first
|
|
* until one falls below `number`, so the cost is a few lookups. */
|
|
export async function earliestCheckpointAbove(sql, number, chainNumberOf, table = `${tablePrefix()}live_certificates`) {
|
|
const rows = await sql(`SELECT index, hash FROM ${table} ORDER BY index DESC LIMIT 400`).catch(() => []);
|
|
let pick = null;
|
|
for (const r of rows) {
|
|
const n = await chainNumberOf(r.hash).catch(() => null);
|
|
if (n === null) continue;
|
|
if (n >= number) pick = r; else break;
|
|
}
|
|
if (!pick) return null;
|
|
const full = await sql(`SELECT * FROM ${table} WHERE index = $1 LIMIT 1`, [Number(pick.index)]);
|
|
return full[0] ? shape(full[0], 'dn3') : null;
|
|
}
|
|
|
|
/** The latest certified checkpoint of `want` ('live', 'test' or 'dn3') through `sql`, shaped for the verifier, or null
|
|
* (the reference apps' read service calls this off Vercel: 'dn3' names the dn3_ tables whatever LIVE_TABLE_PREFIX says). */
|
|
export async function readCheckpoint(sql, want = 'live') {
|
|
// 'live', 'dn4' (and the retired 'dn3') read that chain's tables and never fall back to the test fixture; 'test' is the fixture, for the ops
|
|
// caller only (the handler gates it; this function is called off Vercel by the reference apps' read service, which names its chain)
|
|
if (want === 'test') { const row = await latest(sql, 'fintest_live_certificates'); return row ? shape(row, 'test') : null; }
|
|
const table = (want === 'dn3' || want === 'dn4') ? `${want}_live_certificates` : `${tablePrefix()}live_certificates`;
|
|
const row = await latest(sql, table);
|
|
return row ? shape(row, want === 'dn3' || want === 'dn4' ? want : 'live') : null;
|
|
}
|
|
export { neon };
|
|
|
|
export default async function handler(req, res) {
|
|
res.setHeader('Cache-Control', 'public, max-age=5');
|
|
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 want = String((req.query && req.query.source) || 'live');
|
|
const idx = req.query && req.query.index !== undefined && /^\d{1,12}$/.test(String(req.query.index)) ? Number(req.query.index) : null;
|
|
if (want === 'test') {
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
if (!opsOk(req)) return res.status(404).json({ ok: false, error: 'no such source' });
|
|
const row = await latest(sql, 'fintest_live_certificates', idx);
|
|
if (!row) return res.status(404).json({ ok: false, error: 'no certified checkpoint stored yet' });
|
|
return res.status(200).json({ ok: true, now: new Date().toISOString(), ...shape(row, 'test') });
|
|
}
|
|
// the public answer: the live chain's tables (source live; dn4 names them too), never the test fixture
|
|
const source = want === 'dn4' ? 'dn4' : 'live';
|
|
const row = await latest(sql, `${tablePrefix()}live_certificates`, idx);
|
|
if (!row) {
|
|
res.setHeader('Cache-Control', 'public, max-age=5');
|
|
if (idx !== null) return res.status(404).json({ ok: false, network: liveNetwork(), error: `no certified checkpoint at index ${idx} yet` });
|
|
return res.status(200).json({ ok: true, now: new Date().toISOString(), ...noCertificate(source, await liveDaa(sql)) });
|
|
}
|
|
// the hand merge of 12:22 BST (a0b62cf7) returned `...cp` with no cp bound: every caller read "cp is not defined" (8 Oct 2026, build-server lane)
|
|
return res.status(200).json({ ok: true, now: new Date().toISOString(), ...shape(row, source) });
|
|
} catch (e) {
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
return res.status(500).json({ ok: false, error: String(e.message || e) });
|
|
}
|
|
}
|