igneum/tools/reference-apps/receipt/verify-receipt.src.mjs

38 lines
4.2 KiB
JavaScript

// Igneum payment receipt, the offline verifier (8 October 2026). This is the SOURCE; the one-file build at
// site/lc/verify-receipt.js bundles it with @noble/hashes (BLAKE2b, keccak) and @noble/curves (BLS12-381) so the
// shipped file needs no npm and no network: `node verify-receipt.js receipt.json [--tamper]`. Build: bundle.sh (esbuild
// on the build box). The checks are site/lc/core.js, the same file the browser page runs.
import { readFileSync } from 'node:fs';
import { blake2b } from '@noble/hashes/blake2.js';
import { keccak_256 } from '@noble/hashes/sha3.js';
import { bls12_381 } from '@noble/curves/bls12-381.js';
import { verifyReceipt, verifyPaymentReceipt, formatIgn } from '../../../site/lc/core.js';
const args = process.argv.slice(2);
const file = args.find(a => !a.startsWith('--'));
if (!file) { console.error('usage: node verify-receipt.js receipt.json [--tamper] (Igneum receipt, format igneum-receipt-v1; verifies offline)'); process.exit(2); }
const receipt = JSON.parse(readFileSync(file, 'utf8'));
if (receipt.format !== 'igneum-receipt-v1') { console.error(`REFUSED: not an igneum-receipt-v1 file (format ${receipt.format})`); process.exit(1); }
const deps = { blake2b, bls: bls12_381, keccak: keccak_256 };
const print = r => { for (const s of r.steps) console.log(` ${s.ok ? 'ok ' : 'REFUSED'} ${s.name}\n ${s.detail}`); };
if (args.includes('--tamper')) {
// the known-failed case first: the same file with one nibble of the raw transaction altered must be refused
const bad = JSON.parse(JSON.stringify(receipt));
const h = bad.raw_tx_hex; const i = 40; bad.raw_tx_hex = h.slice(0, i) + (parseInt(h[i], 16) ^ 1).toString(16) + h.slice(i + 1);
const t = verifyReceipt(bad, deps);
console.log(`tampered copy (one nibble of the raw transaction): ${t.verified ? 'NOT REFUSED, this verifier is broken' : 'REFUSED'}${t.reason ? ' :: ' + t.reason : ''}`);
if (t.verified) process.exit(1);
}
const r = receipt.segment ? verifyPaymentReceipt(receipt, deps) : verifyReceipt(receipt, deps);
const payment = !!(r.verified && r.payment);
console.log(`${payment ? 'PAYMENT RECEIPT' : 'TRANSACTION INCLUSION RECEIPT'} 0x${receipt.tx_hash} on ${receipt.chain_id} (Devnet 3, no value)`);
console.log(payment
? 'What this file authenticates: that the signed transaction is included in a finalised block and executed with status success, its receipt sitting in the shard receipts trie whose root the proven segment statement commits to. Trusted: the voter table from the node; the SP1 proof behind the statement is verified by nodes, not here.'
: 'What this file authenticates: that the signed transaction is included in a block that is finalised. What it does not: the execution outcome (status, gas), which is carried as the node reported it.' + (receipt.payment_unavailable ? ' Why no payment receipt: ' + receipt.payment_unavailable : ''));
print(r);
if (!r.verified) { console.log(`REFUSED: ${r.reason}`); process.exit(1); }
const t = r.tx;
if (payment) console.log(`PAYMENT VERIFIED in ${r.ms} ms: ${formatIgn(t.value)} IGN (${t.value} wei of the native coin) to ${t.to || 'contract creation'}, executed with status success (${r.outcome.logs.length} log(s)) at chain block ${receipt.execution.chain_block}, in block ${r.block.slice(0, 16)} at DAA ${r.block_daa}, finalised under checkpoint ${r.checkpoint}; ${r.certificate}.`);
else console.log(`INCLUSION VERIFIED in ${r.ms} ms: a signed transaction of ${formatIgn(t.value)} IGN to ${t.to || 'contract creation'} (${t.value} wei) is included in block ${r.block.slice(0, 16)} at DAA ${r.block_daa} (${new Date(Number(r.block_time)).toISOString()}), finalised under checkpoint ${r.checkpoint}; ${r.headers} headers checked; ${r.certificate}.`);
console.log('Reported by the node, not authenticated by this file: from ' + (receipt.tx_as_reported && receipt.tx_as_reported.from) + (payment ? '' : (receipt.execution ? `, executed with status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}` : '')) + '. The voter table with weights came from the node (spec 10.1). In the four words: ' + (payment ? 'included, executed, proven and finalised are authenticated under the stated trust.' : 'included and finalised are authenticated, executed is reported, proven is not claimed.'));