#!/usr/bin/env node // proba-verifica-chitanta.mjs (2026-10-27): the public reader of a payment receipt (verifica-chitanta.mjs) run as a stranger runs it, // with no chain and no network: a local JSON-RPC stub (127.0.0.1, one route per behaviour) and a stub of the AIP-23 reference verifier // that answers the shape of verify-proof.mjs (verdict VALID / PARTIAL / INVALID, the finality level PASSED / PENDING / UNMEASURED / // FAILED). Every row asks for the exit code AND the verdict word of the last line, and the verdicts are those of POST /v1/payments/verify // (cloud-gateway/plati-api.mjs): VALID only with the notarization's finality PASSED and the transaction compared on chain, PARTIAL when // something was not measured, INVALID with a reason. Until 2026-10-27 the reader wrote VALID and exited 0 without --verify-proof, without // --rpc, and when the reference verifier answered PARTIAL / PENDING; it called a file without policy and inputs INVALID; it called an // unknown transaction UNMEASURED where the route says INVALID; and it compared the transaction on an RPC of another chain. The rows // marked (NEW) are red on that form: AERE_VERIFICA_CHITANTA_CLI= runs the probe on it. // node proba-verifica-chitanta.mjs -> 0 every row as named, 1 otherwise import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import http from 'node:http'; import { spawn } from 'node:child_process'; import { fileURLToPath, pathToFileURL } from 'node:url'; const AICI = path.dirname(fileURLToPath(import.meta.url)); const CLI = process.env.AERE_VERIFICA_CHITANTA_CLI ? path.resolve(process.env.AERE_VERIFICA_CHITANTA_CLI) : path.join(AICI, 'verifica-chitanta.mjs'); const R = await import(pathToFileURL(path.join(AICI, 'risc.mjs')).href), C = await import(pathToFileURL(path.join(AICI, 'chitanta.mjs')).href); const POLITICA = { v: 1, kind: 'aere-payment-risk-policy', id: 'lab-cli-2026-10-27', currency: 'USD', perPaymentMaxUsd: '1.00', window: { seconds: 86400, maxUsd: '2.50', maxCount: 3 }, allowedAssets: [{ chainId: 28001, asset: 'native', decimals: 18 }], refusedCounterparties: [{ address: '0x70099E62735500AA2F85B60C518551a57B202d54', reason: 'a contract with no path to withdraw what is sent to it (AereNotary on 28001)', source: 'contracts/deployments/notary-28001.json' }] }; const PAYER = '0x' + 'aa'.repeat(20), PAYEE = '0x' + 'bb'.repeat(20), RATE = { usdPerUnit: '0.5', source: 'declared for the laboratory', at: '2026-10-27T08:00:00.000Z' }; const INPUTS = { chainId: 28001, payer: PAYER, payee: PAYEE, asset: 'native', amount: '400000000000000000', rate: RATE, identity: null, history: [], now: 1793000000 }; const TX = '0x' + '11'.repeat(32), BLOC = '0x' + '22'.repeat(32); const v = R.judeca(POLITICA, INPUTS); const { instruction, envelope } = C.construiesteChitanta({ chainId: 28001, txHash: TX, blockHash: BLOC, payer: PAYER, payee: PAYEE, asset: 'native', decimals: 18, amount: INPUTS.amount, rate: RATE, verdict: v, policyId: POLITICA.id, createdAt: '2026-10-27T08:00:01.000Z' }); const plic = { ...envelope, notarization: { chainId: 28001, notary: '0x70099E62735500AA2F85B60C518551a57B202d54', firstSeen: 1793000100, txHash: '0x' + '33'.repeat(32), block: 7 } }; const D = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-cititor-chitanta-')); const scrie = (nume, o) => { const f = path.join(D, nume); fs.writeFileSync(f, JSON.stringify(o)); return f; }; const FISIER = scrie('chitanta.json', { instruction, envelope: plic, policy: POLITICA, inputs: INPUTS }); const FARA_POLITICA = scrie('chitanta-api.json', { instruction, envelope: plic }); // the shape of `receipt` in the route's answer const fals = JSON.parse(JSON.stringify({ instruction, envelope: plic, policy: POLITICA, inputs: INPUTS })); fals.instruction.amount = '500000000000000000'; const FALS = scrie('chitanta-falsa.json', fals); // the reference verifier, stubbed: the state comes from the environment the reader inherits const VP = path.join(D, 'verify-proof-stub.mjs'); fs.writeFileSync(VP, "const s = process.env.AERE_STUB_FIN || 'PASSED';\n" + "if (s === 'NOJSON') { console.log('not json'); process.exit(2); }\n" + "const other = process.env.AERE_STUB_ALT || null;\n" + "const levels = [{ level: 'integrity', state: 'PASSED', detail: 'stub' }, { level: 'finality', state: s, detail: 'stub ' + s }];\n" + "if (other) levels.push({ level: 'witness', state: other, detail: 'stub' });\n" + "const failed = levels.some((l) => l.state === 'FAILED'), unm = levels.some((l) => l.state === 'UNMEASURED' || l.state === 'PENDING');\n" + "console.log(JSON.stringify({ verdict: failed ? 'INVALID' : unm ? 'PARTIAL' : 'VALID', levels }));\n" + "process.exitCode = failed ? 1 : unm ? 2 : 0;\n"); // the chain, stubbed: /bun answers this payment, /necunoscut does not know it, /alt-lant is another chain, /mut answers 500 const txBun = { hash: TX, from: PAYER, to: PAYEE, value: '0x58d15e176280000', blockHash: BLOC }, rcBun = { status: '0x1', blockHash: BLOC }; const srv = http.createServer((q, s) => { let b = ''; q.on('data', (d) => { b += d; }); q.on('end', () => { const mod = q.url.replace(/^\//, ''); let j = {}; try { j = JSON.parse(b); } catch { /* malformed */ } if (mod === 'mut') { s.writeHead(500); s.end('down'); return; } const rez = j.method === 'eth_chainId' ? (mod === 'alt-lant' ? '0xaf0' : '0x6d61') : j.method === 'eth_getTransactionByHash' ? (mod === 'necunoscut' ? null : txBun) : j.method === 'eth_getTransactionReceipt' ? (mod === 'necunoscut' ? null : rcBun) : null; s.writeHead(200, { 'content-type': 'application/json' }); s.end(JSON.stringify({ jsonrpc: '2.0', id: j.id ?? 1, result: rez })); }); }); await new Promise((ok) => srv.listen(0, '127.0.0.1', ok)); const RPC = (mod) => `http://127.0.0.1:${srv.address().port}/${mod}`; const ruleaza = (args, env = {}) => new Promise((ok) => { const p = spawn(process.execPath, [CLI, ...args], { env: { ...process.env, AERE_VERIFY_PROOF: '', ...env }, stdio: ['ignore', 'pipe', 'pipe'] }); let out = ''; p.stdout.on('data', (d) => { out += d; }); p.stderr.on('data', (d) => { out += d; }); const t = setTimeout(() => p.kill(), 120000); p.on('close', (cod) => { clearTimeout(t); ok({ cod, out, ultim: out.trim().split('\n').pop() || '' }); }); }); let rele = 0, n = 0; const cer = (nume, r, cod, cuvant, alt) => { n++; const ok = r.cod === cod && r.ultim.startsWith(cuvant) && (!alt || alt.test(r.out)); console.log(` [${ok ? 'VERDE ' : 'ROSU '}] ${nume}: exit ${r.cod}, "${r.ultim.slice(0, 150)}"`); if (!ok) rele++; }; try { cer('V1 the full receipt file, the chain agreeing, finality PASSED: VALID, notarized (the route says VALID)', await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED' }), 0, 'VALID, notarized'); cer('V2 (NEW) without --verify-proof: PARTIAL, exit 2, the envelope not judged (the route never answers VALID without the verifier)', await ruleaza([FISIER, '--rpc', RPC('bun')]), 2, 'PARTIAL', /no --verify-proof/); cer('V3 (NEW) without --rpc and without --verify-proof: PARTIAL, exit 2, the chain not consulted', await ruleaza([FISIER]), 2, 'PARTIAL', /not consulted/); cer('V4 (NEW) the reference verifier answers PARTIAL, finality PENDING (a receipt checked right after it was issued): PARTIAL, exit 2 (the route says PARTIAL)', await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'PENDING' }), 2, 'PARTIAL', /finality PENDING/); cer('V5 (NEW) finality UNMEASURED: PARTIAL, exit 2', await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'UNMEASURED' }), 2, 'PARTIAL', /finality UNMEASURED/); cer('V6 finality FAILED (the verifier says INVALID): INVALID, exit 1 (the route says INVALID)', await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'FAILED' }), 1, 'INVALID'); cer('V7 (NEW) the chain does not answer (HTTP 500): PARTIAL, exit 2, said (the route says PARTIAL since 2026-10-27)', await ruleaza([FISIER, '--rpc', RPC('mut'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED' }), 2, 'PARTIAL', /did not answer/); cer('V8 (NEW) the chain answers and does not know the transaction: INVALID, exit 1, said (as the route)', await ruleaza([FISIER, '--rpc', RPC('necunoscut'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED' }), 1, 'INVALID', /does not know/); cer('V9 (NEW) an RPC of another chain (2800 for a 28001 receipt): PARTIAL, exit 2, the transaction not compared there', await ruleaza([FISIER, '--rpc', RPC('alt-lant'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED' }), 2, 'PARTIAL', /serves chain 2800/); cer('V10 (NEW) the `receipt` of the route alone ({instruction, envelope}, no policy, no inputs): PARTIAL, exit 2, naming what is missing and the receiptFile that carries it (not INVALID on a good receipt)', await ruleaza([FARA_POLITICA, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED' }), 2, 'PARTIAL', /no policy and inputs.*receiptFile/); cer('V11 a forged receipt (amount changed after the envelope), everything else agreeing: INVALID, exit 1', await ruleaza([FALS, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED' }), 1, 'INVALID', /does not commit/); cer('V12 (NEW) --verify-proof at a path where no verifier is: PARTIAL, exit 2, said', await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', path.join(D, 'lipsa.mjs')]), 2, 'PARTIAL', /not at/); cer('V13 the verifier answers PARTIAL for another level (witness UNMEASURED) with finality PASSED: VALID, as the route, which decides on finality', await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED', AERE_STUB_ALT: 'UNMEASURED' }), 0, 'VALID, notarized'); cer('V14 (NEW) the verifier prints no JSON: PARTIAL, exit 2, said', await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'NOJSON' }), 2, 'PARTIAL', /gave no JSON/); } finally { await new Promise((ok) => srv.close(ok)); fs.rmSync(D, { recursive: true, force: true }); } console.log(`\nthe public reader of a receipt (${path.basename(CLI)}): ${n - rele}/${n} as named${process.env.AERE_VERIFICA_CHITANTA_CLI ? ' (reader under test: ' + CLI + ')' : ''}`); process.exitCode = rele ? 1 : 0;