165 lines
17 KiB
JavaScript
165 lines
17 KiB
JavaScript
#!/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 notarization was PENDING or UNMEASURED; 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. An envelope
|
|
// the reference verifier calls PARTIAL on ANOTHER level (the witness, a signature scheme it cannot check) with finality PASSED stays
|
|
// VALID, as on the route (V13; corrected 2026-10-27: this note said the verifier answering PARTIAL was the case). The rows marked (NEW)
|
|
// are red on that form: AERE_VERIFICA_CHITANTA_CLI=<a copy of the old reader beside its modules> runs the probe on it. The rows marked
|
|
// (NEW2) are red on the form of the first correction of 2026-10-27 (an RPC of another chain still handed on to the verifier, a verifier
|
|
// that did not start called 'not run', a file that is not a receipt ending in an exception).
|
|
// 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');
|
|
// 2026-10-27 (R9-S1): the stub answers as verify-proof.mjs answers, not as the reader assumes: with --offline the finality is UNMEASURED
|
|
// ('offline: ...'); with --rpc it asks that endpoint eth_chainId, and on ANOTHER chain than --chain it answers what the published
|
|
// verifier answers there: FAILED (the form served until 2026-10-27: 'the chain's most recent anchor did not verify: FAIL: ... not anchoring
|
|
// on a regular grid', measured from a fresh clone) or, with AERE_STUB_ALTLANT=UNMEASURED, the corrected form ('serves chain X, not
|
|
// chain Y'). Its argv is appended to AERE_STUB_ARGS, so a row can ask what the reader handed it.
|
|
fs.writeFileSync(VP, "import fs from 'node:fs';\nconst argv = process.argv.slice(2);\n"
|
|
+ "if (process.env.AERE_STUB_ARGS) fs.appendFileSync(process.env.AERE_STUB_ARGS, JSON.stringify(argv) + '\\n');\n"
|
|
+ "const s0 = process.env.AERE_STUB_FIN || 'PASSED';\n"
|
|
+ "if (s0 === 'NOJSON') { console.log('not json'); process.exit(2); }\n"
|
|
+ "const other = process.env.AERE_STUB_ALT || null;\n"
|
|
+ "const ir = argv.indexOf('--rpc'), ic = argv.indexOf('--chain'); const rpc = ir >= 0 ? argv[ir + 1] : null, chain = ic >= 0 ? Number(argv[ic + 1]) : null;\n"
|
|
+ "let s = s0, detail = 'stub ' + s0;\n"
|
|
+ "if (argv.includes('--offline')) { s = 'UNMEASURED'; detail = 'offline: the notarization this envelope claims was not checked'; }\n"
|
|
+ "else if (rpc && chain) {\n"
|
|
+ " const j = await fetch(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] }) }).then((x) => x.json()).catch(() => null);\n"
|
|
+ " const served = j && j.result ? Number(BigInt(j.result)) : null;\n"
|
|
+ " if (served !== null && served !== chain) {\n"
|
|
+ " if ((process.env.AERE_STUB_ALTLANT || 'FAILED') === 'FAILED') { s = 'FAILED'; detail = 'the chain' + String.fromCharCode(39) + 's most recent anchor did not verify: FAIL: the two most recent anchors (24630896, 24630864) are 32 apart, which is not a multiple of 32 on the grid from 64. The chain is not anchoring on a regular grid.'; }\n"
|
|
+ " else { s = 'UNMEASURED'; detail = 'the endpoint ' + rpc + ' serves chain ' + served + ', not chain ' + chain + ': nothing about chain ' + chain + ' was measured there'; }\n"
|
|
+ " }\n}\n"
|
|
+ "const levels = [{ level: 'integrity', state: 'PASSED', detail: 'stub' }, { level: 'finality', state: s, detail }];\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");
|
|
const VP_MORT = path.join(D, 'verify-proof-mort.mjs');
|
|
fs.writeFileSync(VP_MORT, "import '@noble/pachet-care-nu-exista-r9';\nconsole.log('{}');\n");
|
|
// files that are not receipts (R9-S9): {}, a list, a text that is not JSON; and a real receipt without its instruction.rate
|
|
const GOL = scrie('gol.json', {}), LISTA = scrie('lista.json', []);
|
|
const NU_JSON = path.join(D, 'nu-json.json'); fs.writeFileSync(NU_JSON, 'nu json {');
|
|
const faraRata = JSON.parse(JSON.stringify({ instruction, envelope: plic, policy: POLITICA, inputs: INPUTS })); delete faraRata.instruction.rate;
|
|
const FARA_RATA = scrie('fara-rata.json', faraRata);
|
|
const ARGS = path.join(D, 'argumente.txt');
|
|
const argsVazute = () => { try { return fs.readFileSync(ARGS, 'utf8'); } catch { return ''; } };
|
|
// 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, stricat = null;
|
|
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 {
|
|
const r1 = await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED' });
|
|
// 2026-10-27 (R9-S1): a reader under test that does not START (a copy put where its imports do not resolve: node then ends with its
|
|
// "Node.js vNN" line) measures nothing, and its red rows would look like a verdict about it; the run stops STRICAT (exit 2)
|
|
if (/^Node\.js v\d/.test(r1.ultim)) { stricat = 'the reader under test does not start (' + (r1.out.split('\n').find((l) => /Error/.test(l)) || r1.ultim).trim().slice(0, 200) + '); a copy of an old reader goes beside its modules'; throw new Error('stricat'); }
|
|
cer('V1 the full receipt file, the chain agreeing, finality PASSED: VALID, notarized (the route says VALID)', r1, 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/);
|
|
// 2026-10-28 (R10-STRAIN-05, NEW3): with UNMEASURED the reader must not state that the notarization is not under an anchor (a fact
|
|
// about the chain nobody measured); the old reader wrote exactly that for every state but PASSED
|
|
cer('V5 (NEW, NEW3) finality UNMEASURED: PARTIAL, exit 2, the notarization NOT MEASURED, never "not under an anchor yet"',
|
|
await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'UNMEASURED' }), 2, 'PARTIAL',
|
|
/^(?![\s\S]*is not under a post-quantum certified anchor yet)[\s\S]*was not measured under a post-quantum certified anchor \(finality UNMEASURED/);
|
|
cer('V5b (NEW3) finality PENDING keeps the statement about the chain: "not under a post-quantum certified anchor yet"',
|
|
await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'PENDING' }), 2, 'PARTIAL',
|
|
/is not under a post-quantum certified anchor yet \(finality PENDING/);
|
|
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/);
|
|
fs.rmSync(ARGS, { force: true });
|
|
const r9 = await ruleaza([FISIER, '--rpc', RPC('alt-lant'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED', AERE_STUB_ARGS: ARGS });
|
|
// (NEW2) the verifier answers as the published one did until 2026-10-27 for an RPC of another chain (FAILED): the reader must not
|
|
// hand it that RPC at all; a reader that does comes out INVALID on an authentic receipt
|
|
r9.ultim = argsVazute().includes('/alt-lant') ? 'HANDED THE RPC OF ANOTHER CHAIN TO THE VERIFIER: ' + r9.ultim : r9.ultim;
|
|
cer('V9 (NEW, NEW2) an RPC of another chain (2800 for a 28001 receipt), the verifier as published until 2026-10-27: PARTIAL, exit 2, the transaction and the notarization not compared there, the RPC not handed to the verifier (judged offline)',
|
|
r9, 2, 'PARTIAL', /^(?=[\s\S]*serves chain 2800)(?=[\s\S]*reference verifier, offline)/);
|
|
fs.rmSync(ARGS, { force: true });
|
|
const r9b = await ruleaza([FISIER, '--rpc', RPC('alt-lant'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED', AERE_STUB_ALTLANT: 'UNMEASURED', AERE_STUB_ARGS: ARGS });
|
|
r9b.ultim = argsVazute().includes('/alt-lant') ? 'HANDED THE RPC OF ANOTHER CHAIN TO THE VERIFIER: ' + r9b.ultim : r9b.ultim;
|
|
cer('V9b (NEW2) the same with the corrected verifier (UNMEASURED for another chain): PARTIAL, exit 2, the RPC not handed on',
|
|
r9b, 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/);
|
|
cer('V15 (NEW2) the verifier does not start (an import that does not resolve, as verify-proof.mjs alone without npm install): PARTIAL, exit 2, it RAN and gave no JSON, with its error line and the files it needs named (not "not run")',
|
|
await ruleaza([FISIER, '--rpc', RPC('bun'), '--verify-proof', VP_MORT]), 2, 'PARTIAL',
|
|
/envelope \(reference verifier\): ran, gave no JSON \(exit 1\): .*Cannot find package[\s\S]*verify-anchor\.mjs, mpt\.mjs, falcon512\.mjs, registry-v3\.mjs and package\.json beside it/);
|
|
cer('V16 (NEW2) a file holding {}: INVALID, exit 1, the file is not a receipt (not an exception with the same exit code)',
|
|
await ruleaza([GOL, '--rpc', RPC('bun'), '--verify-proof', VP]), 1, 'INVALID: the file is not a receipt: it has no instruction object');
|
|
cer('V17 (NEW2) a file that is not JSON: INVALID, exit 1, said',
|
|
await ruleaza([NU_JSON, '--rpc', RPC('bun'), '--verify-proof', VP]), 1, 'INVALID: the file is not a receipt: it is not JSON');
|
|
cer('V18 (NEW2) a file holding a list: INVALID, exit 1, said',
|
|
await ruleaza([LISTA]), 1, 'INVALID: the file is not a receipt: it holds a list');
|
|
cer('V19 (NEW2) a receipt without instruction.rate: INVALID, exit 1, for the named rule (the amount or the rate), not an exception',
|
|
await ruleaza([FARA_RATA, '--rpc', RPC('bun'), '--verify-proof', VP], { AERE_STUB_FIN: 'PASSED' }), 1, 'INVALID', /rules: INVALID: .*rate/);
|
|
} catch (e) { if (!stricat) throw e; } finally { await new Promise((ok) => srv.close(ok)); fs.rmSync(D, { recursive: true, force: true }); }
|
|
if (stricat) { console.log('STRICAT: ' + stricat); process.exitCode = 2; } else {
|
|
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;
|
|
}
|