diff --git a/site/api/checkpoint.mjs b/site/api/checkpoint.mjs index 8d5509075..5b645bb8e 100644 --- a/site/api/checkpoint.mjs +++ b/site/api/checkpoint.mjs @@ -73,6 +73,23 @@ function shape(row, source) { }; } +/** 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 = 'dn3_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. */ export async function readCheckpoint(sql, want = 'live') { let row = null, source = null; diff --git a/site/lc/app.js b/site/lc/app.js index 293e47095..419028950 100644 --- a/site/lc/app.js +++ b/site/lc/app.js @@ -67,7 +67,8 @@ export async function runReceipt(tx) { const cp = await getJson(`${API}/checkpoint`); setStatus(`checkpoint ${cp.index} fetched; fetching the inclusion proof…`, 'busy'); const r = await getJson(`${API}/receipt?tx=${tx}&checkpoint=${cp.hash}&index=${cp.index}`); - const receipt = { format: 'igneum-receipt-v1', issued: new Date().toISOString(), ...r, checkpoint: { ...r.checkpoint, certificate: cp } }; + // the service may answer with an earlier certificate (the first checkpoint above the block: the smallest proof); it is verified like any other + const receipt = { format: 'igneum-receipt-v1', issued: new Date().toISOString(), ...r, checkpoint: { ...r.checkpoint, certificate: r.checkpoint.certificate || cp } }; delete receipt.ok; delete receipt.now; setStatus('verifying in this tab…', 'busy'); await new Promise(resolve => setTimeout(resolve, 20)); diff --git a/site/lc/test.html b/site/lc/test.html index b508608cc..ac23464e0 100644 --- a/site/lc/test.html +++ b/site/lc/test.html @@ -42,7 +42,7 @@ try { let tx = q.get('tx'); if (!tx) { const cb = await rpc('eth_getBlockByHash', ['0x' + cp.hash, false]); const n = parseInt(cb.number, 16); for (let k = n - 1; k > n - 200 && !tx; k--) { const b = await rpc('eth_getBlockByNumber', ['0x' + k.toString(16), false]); if (b && b.transactions.length) tx = b.transactions[0]; } } const rr = await get(`${API}/receipt?tx=${tx}&checkpoint=${cp.hash}&index=${cp.index}`); - const receipt = { format: 'igneum-receipt-v1', ...rr, checkpoint: { ...rr.checkpoint, certificate: cp } }; + const receipt = { format: 'igneum-receipt-v1', ...rr, checkpoint: { ...rr.checkpoint, certificate: rr.checkpoint.certificate || cp } }; const R = [ ['raw transaction altered by one nibble', d => { d.raw_tx_hex = flipHex(d.raw_tx_hex, 40); }], ['transaction hash altered', d => { d.tx_hash = flipHex(d.tx_hash, 10); }], diff --git a/tools/reference-apps/light-service/serve.mjs b/tools/reference-apps/light-service/serve.mjs index 3966334ca..c66128517 100644 --- a/tools/reference-apps/light-service/serve.mjs +++ b/tools/reference-apps/light-service/serve.mjs @@ -19,7 +19,7 @@ import { createServer } from 'node:http'; import { blake2b } from '@noble/hashes/blake2.js'; import { keccak_256 } from '@noble/hashes/sha3.js'; import { coinbaseTxHash, merkleRoot, merklePath, hexToBytes, bytesToHex, segmentRecordsOf } from '../../../site/lc/core.js'; -import { readCheckpoint, neon } from '../../../site/api/checkpoint.mjs'; +import { readCheckpoint, earliestCheckpointAbove, neon } from '../../../site/api/checkpoint.mjs'; const PORT = Number(process.env.LIGHT_PORT || 26890); const EXEC = process.env.EXEC_RPC || 'http://127.0.0.1:26881'; @@ -207,13 +207,18 @@ void merklePath; async function receipt(q) { const tx = q.get('tx') || ''; if (!/^0x[0-9a-fA-F]{64}$/.test(tx)) throw httpError(400, 'tx: 0x and 64 hex digits'); - const cpHash = q.get('checkpoint'), cpIndex = Number(q.get('index')); - if (!cpHash || !(cpIndex >= 0)) throw httpError(400, 'checkpoint and index are required (from /api/checkpoint?source=dn3)'); + let cpHash = q.get('checkpoint'), cpIndex = Number(q.get('index')); + if (!cpHash || !(cpIndex >= 0)) throw httpError(400, 'checkpoint and index are required (from /checkpoint)'); const t = await exec('eth_getTransactionByHash', [tx]); if (!t) throw httpError(404, 'the node has not executed a transaction with that hash (unknown, or not yet in a block)'); const rcpt = await exec('eth_getTransactionReceipt', [tx]); - const cpNumber = await chainNumberOf(cpHash); const chainNumber = Number(t.blockNumber); + // the smallest proof: the earliest certified checkpoint at or above the block (from the Devnet 3 observer's rows), else + // the checkpoint the caller named; the certificate used is returned so the page verifies against that one + let certificate = null; + if (process.env.DATABASE_URL) certificate = await earliestCheckpointAbove(neon(), chainNumber, chainNumberOf).catch(() => null); + if (certificate) { cpHash = certificate.hash; cpIndex = Number(certificate.index); } + const cpNumber = await chainNumberOf(cpHash); if (chainNumber > cpNumber) throw httpError(409, `not final yet: executed at chain block ${chainNumber}, the latest certified checkpoint is chain block ${cpNumber}; try again in about ${chainNumber - cpNumber + 30} s`); const including = strip(t.igneum.includingBlock); const b = await body(including); @@ -226,7 +231,7 @@ async function receipt(q) { const headers = await headerPath(including, chainNumber, cpNumber, cpHash); return { ok: true, now: new Date().toISOString(), chain_id: 'igneum-devnet-3', tx_hash: strip(tx), raw_tx_hex: strip(raws[idx]), - checkpoint: { hash: strip(cpHash), index: cpIndex, chain_block: cpNumber }, + checkpoint: { hash: strip(cpHash), index: cpIndex, chain_block: cpNumber, certificate: certificate || undefined }, including_block: { header: headers[0], leaf_index: idx + 1, leaf_count: leaves.length, merkle_siblings: siblings(leaves, idx + 1) }, headers, execution: rcpt ? { chain_block: chainNumber, chain_block_hash: strip(rcpt.blockHash), status: rcpt.status, gas_used: rcpt.gasUsed, from: rcpt.from, to: rcpt.to, contract_address: rcpt.contractAddress, logs: (rcpt.logs || []).length, effective_gas_price: rcpt.effectiveGasPrice, as_reported_by: 'the node (not proven here)' } : null,