#!/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 [--online] one artifact: { artifact, input } (a vector file is one); verdict on stdout // node implementare-aere.mjs --batch [{ 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=)'); 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 | [--online] | --batch '); 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; } }