aere-quantum/conformance/implementare-aere.mjs

116 lines
11 KiB
JavaScript

#!/usr/bin/env node
// implementare-aere.mjs: the published verifiers behind the one verdict form (verdict.mjs). This file holds NO verification rule of
// its own: an AIP-23 envelope is judged by the reference verifier (verify-proof.mjs, run as the program it is), a payment by the
// risk module, a receipt by the receipt verifier, a market timeline by the market judge, a dataset record, answer, consistency proof
// or sample by the dataset module. What it adds is the translation: the rule ids those verifiers name -> the public rule ids and
// the level of each (reguli.mjs), and the composition (a receipt or a timeline that carries an AIP-23 envelope gets that envelope
// judged too, so one verdict covers the artifact).
//
// node implementare-aere.mjs --capabilities what this implementation judges (artifacts, rules, online)
// node implementare-aere.mjs <file.json> [--online] one artifact: { artifact, input } (a vector file is one); verdict on stdout
// node implementare-aere.mjs --batch <list.json> [{ file, online }]; one verdict per line, in order
//
// Offline (no --online) nothing here opens a connection: an envelope that claims a notarization is judged without the claim and the
// verdict says so (PARTIAL: claim-not-checked). With --online the envelope goes to the reference verifier with the chain's endpoint.
// Exit: 0 VALID, 1 INVALID, 2 PARTIAL, 3 the artifact could not be judged at all (not a verdict).
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawn } from 'node:child_process';
import { VERIFICATOARE, FINALITY_SITES, VERIFY_PROOF, incarca } from './reguli.mjs';
import { acumulator, pica, trece, nemasurat, verdictUnic } from './verdict.mjs';
const RPC = { 2800: process.env.AERE_CONFORMANCE_RPC_2800 || 'https://rpc.aere.network', 28001: process.env.AERE_CONFORMANCE_RPC_28001 || 'https://testnet-rpc.aere.network' };
const LANT = (n) => (n === 28001 ? 'chain 28001, a testnet' : 'chain ' + n);
const ARTIFACTE = Object.values(VERIFICATOARE).flatMap((v) => v.artifacts);
const toateRegulile = Object.values(VERIFICATOARE).flatMap((v) => Object.values(v.reguli).map((r) => r[0]));
function ruleaza(cmd, args, timeout) {
return new Promise((gata) => {
const c = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] }); let out = '', err = ''; const t = setTimeout(() => c.kill(), timeout);
c.stdout.on('data', (d) => { out += d; }); c.stderr.on('data', (d) => { err += d; });
c.on('error', (e) => { clearTimeout(t); gata({ code: null, out, err: String(e.message) }); }); c.on('close', (code) => { clearTimeout(t); gata({ code, out, err }); });
});
}
/** The envelope, judged by the reference verifier; its levels land in the accumulator under the public rule ids. */
async function plic(acc, env, { online, chain }) {
if (!fs.existsSync(VERIFY_PROOF)) throw new Error('the AIP-23 reference verifier is not at ' + VERIFY_PROOF + ' (AERE_VERIFY_PROOF=<verify-proof.mjs>)');
const claim = env && typeof env === 'object' ? env.notarization : null; let judecat = env;
if (!online && claim) { judecat = { ...env }; delete judecat.notarization; }
const f = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'aere-conf-')), 'envelope.json'); fs.writeFileSync(f, JSON.stringify(judecat === undefined ? null : judecat));
const lant = Number(chain || (claim && claim.chainId) || 28001);
const args = [VERIFY_PROOF, f, '--json']; if (online) { if (!RPC[lant]) throw new Error('no endpoint known for chain ' + lant); args.push('--chain', String(lant), '--rpc', RPC[lant]); }
let r; try { r = await ruleaza(process.execPath, args, online ? 240000 : 60000); } finally { fs.rmSync(path.dirname(f), { recursive: true, force: true }); }
let j; try { j = JSON.parse(r.out); } catch { throw new Error('the reference verifier gave no verdict (exit ' + r.code + '): ' + (r.err || r.out).trim().split('\n').slice(-1)[0].slice(0, 160)); }
const nivel = (n) => (j.levels || []).find((x) => x.level === n); const R = VERIFICATOARE['aip23-envelope'].reguli;
for (const n of ['form', 'integrity']) { const x = nivel(n); if (!x) continue; if (x.state === 'FAILED') pica(acc, 'integrity', R[n][0], x.detail); else trece(acc, 'integrity', 'envelope ' + n); }
const s = nivel('signature');
if (!s) nemasurat(acc, 'authenticity', 'not-reached', 'the envelope failed before its signature was looked at');
else if (s.state === 'PASSED') trece(acc, 'authenticity', s.detail);
else if (s.state === 'FAILED') pica(acc, 'authenticity', R.signature[0], s.detail);
else nemasurat(acc, 'authenticity', s.state === 'ABSENT' ? 'absent' : 'unmeasurable', s.detail);
const fin = nivel('finality'); const ambele = (cause, reason) => { nemasurat(acc, 'anchoring', cause, reason); nemasurat(acc, 'finality', cause, reason); };
if (!online && claim) ambele('claim-not-checked', 'the envelope claims a notarization on chain ' + (claim.chainId || '?') + '; this run was offline, the claim was not checked');
else if (!fin) ambele('not-reached', 'the envelope failed before its notarization was looked at');
else if (fin.state === 'PASSED') { trece(acc, 'anchoring', fin.detail); trece(acc, 'finality', 'the post-quantum certificate of the covering anchor verified (' + LANT(lant) + '; the finality is that chain\'s)'); }
else if (fin.state === 'FAILED') {
const loc = FINALITY_SITES.filter((x) => x.re.test(fin.detail));
if (loc.length !== 1) throw new Error('the reference verifier refused finality at a site this suite does not classify: ' + fin.detail.slice(0, 120));
pica(acc, loc[0].level, R.finality[0], fin.detail);
if (loc[0].level === 'anchoring') nemasurat(acc, 'finality', 'not-applicable', 'nothing is anchored, so no certificate covers it');
else nemasurat(acc, 'anchoring', 'not-reached', 'the anchor certificate did not verify, the notary state under it was not read');
} else ambele(fin.state === 'ABSENT' ? 'absent' : fin.state === 'PENDING' ? 'pending' : 'unmeasurable', fin.detail);
}
const faraPlic = (acc) => { for (const l of ['authenticity', 'anchoring', 'finality']) nemasurat(acc, l, 'not-judged', 'this artifact carries no AIP-23 envelope; its commitment is signed or anchored through one, judged as the artifact `envelope`'); };
/** The rules a module named, as public ids at their levels; `text` is the module's own reasons. */
function reguli(acc, verifier, surse, motive, titlu) {
const R = VERIFICATOARE[verifier].reguli; const text = (motive || []).filter(Boolean).join('; ');
for (const s of surse) { if (!R[s]) throw new Error(`${verifier} named a rule this suite does not map: ${s}`); pica(acc, R[s][1], R[s][0], text); }
if (!surse.length) trece(acc, 'integrity', titlu);
}
async function judeca(artifact, input, online) {
const acc = acumulator();
if (artifact === 'envelope') await plic(acc, input.envelope, { online, chain: input.chain });
else if (artifact === 'payment-risk') {
const M = await incarca('risc'); const v = M.judeca(input.policy, input.inputs);
if ((v.decision === 'ALLOW') !== (v.rules.length === 0)) throw new Error('the risk module refused without naming a rule');
reguli(acc, 'payment-risk', v.rules, v.reasons, 'the declared policy allows this payment'); faraPlic(acc);
} else if (artifact === 'payment-receipt') {
const M = await incarca('chitanta'); const r = M.verificaChitanta(input.receipt, { policy: input.policy, inputs: input.inputs, tx: input.tx, receipt: input.txReceipt });
if ((r.verdict === 'VALID') !== (r.reguli.length === 0)) throw new Error('the receipt verifier refused without naming a rule');
reguli(acc, 'payment-receipt', r.reguli, r.motive, 'no rule of the receipt verifier refused' + (r.chainChecked ? ' (transaction facts supplied)' : ' (no transaction facts supplied)'));
await plic(acc, input.receipt && input.receipt.envelope, { online, chain: input.chain });
} else if (artifact === 'market-timeline') {
const M = await incarca('piata'); const r = M.judeca(input);
if (r.verdict === 'INCOMPLETE') nemasurat(acc, 'integrity', 'incomplete', 'the timeline is not terminal (' + (r.status || 'no job or no events') + ')');
else { if ((r.verdict === 'VALID') !== (r.reguli.length === 0)) throw new Error('the market judge refused without naming a rule'); reguli(acc, 'market-timeline', r.reguli, r.motive, 'no rule of the market judge refused (status ' + r.status + ')'); }
if (input.envelope) await plic(acc, input.envelope, { online, chain: input.chainId }); else faraPlic(acc);
} else if (artifact.startsWith('dataset-')) {
const M = await incarca('baza'); let r;
if (artifact === 'dataset-record') r = M.verificaInregistrare(input.record, input.commitment);
else if (artifact === 'dataset-answer') { r = M.verificaRaspuns(input.answer, input.commitment); r.ok = r.verdict === 'VALID'; }
else if (artifact === 'dataset-consistency') r = M.verificaConsistentaVersiuni(input.proof, input.older, input.newer);
else if (artifact === 'dataset-sample') r = await M.esantioneaza({ url: 'vector://served', commitment: input.commitment, k: input.k, seed: input.seed, ia: async (u) => (input.served || {})[u.split('/').pop()] || null });
else throw new Error('unknown artifact ' + artifact);
if (r.ok !== (r.reguli.length === 0)) throw new Error('the dataset module refused without naming a rule');
reguli(acc, 'dataset', r.reguli, r.motive, 'no rule of the dataset module refused'); faraPlic(acc);
} else throw new Error('unknown artifact ' + artifact);
return verdictUnic(artifact, acc);
}
async function unul(file, online) {
let j; try { j = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { return { error: 'the file cannot be read: ' + e.message }; }
if (!j || !ARTIFACTE.includes(j.artifact) || !j.input) return { error: 'not { artifact, input } with a known artifact (' + ARTIFACTE.join(', ') + ')' };
try { return await judeca(j.artifact, j.input, online); } catch (e) { return { error: String(e.message).split('\n')[0].slice(0, 300) }; }
}
const a = process.argv.slice(2);
if (a.includes('--capabilities')) console.log(JSON.stringify({ v: 1, kind: 'aere-conformance-capabilities', name: 'aere reference verifiers (JavaScript)', artifacts: ARTIFACTE, rules: toateRegulile.sort(), online: true, batch: true }));
else if (a.includes('--batch')) {
const lista = JSON.parse(fs.readFileSync(a[a.indexOf('--batch') + 1], 'utf8')); const rez = new Array(lista.length); let urm = 0;
const lucrator = async () => { while (urm < lista.length) { const k = urm++; rez[k] = await unul(lista[k].file, !!lista[k].online); } };
await Promise.all(Array.from({ length: Math.min(6, lista.length) }, lucrator));
for (const r of rez) console.log(JSON.stringify(r));
} else {
const file = a.find((x) => !x.startsWith('--'));
if (!file) { console.error('usage: node implementare-aere.mjs --capabilities | <file.json> [--online] | --batch <list.json>'); process.exitCode = 3; }
else { const r = await unul(file, a.includes('--online')); console.log(JSON.stringify(r, null, 1)); process.exitCode = r.error ? 3 : r.verdict === 'VALID' ? 0 : r.verdict === 'INVALID' ? 1 : 2; }
}