aere-quantum/conformance/reguli.mjs

141 lines
13 KiB
JavaScript

// reguli.mjs: which verifiers the conformance suite enrols, the rules each of them NAMES (derived from its code, never typed here as
// a list of its own), the public id and the level of each rule.
//
// A verifier that lives in this repository as a module exports `REGULI` (the rules it can name; its helper throws on any other) and
// carries a /*PAZA:<rule>*/ marker on every check of a rule. The derivation imports the module, reads its source, and requires:
// declared == marked == mapped below. A rule added to a verifier and not mapped here, or mapped here and gone from the verifier, is a
// refusal of the derivation, not a silent drift. The AIP-23 reference verifier (verify-proof.mjs) is a published program, not edited
// for this suite: its rules are its level names, read from its `level('<name>', ... 'FAILED'` sites, and every FAILED site of its
// `finality` level must be classified below (anchoring or finality) or the derivation refuses.
import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
export const LEVELS = ['integrity', 'authenticity', 'anchoring', 'finality'];
/** The AIP-23 reference verifier: AERE_VERIFY_PROOF=<verify-proof.mjs> (published with aere-node), or the development repository. */
export const VERIFY_PROOF = process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', '..', 'verificator-falcon', 'verify-proof.mjs');
/** The modules judged in-process. `env` names the variable through which the planting control substitutes a planted copy. */
export const MODULE = {
risc: { url: new URL('../payments/risc.mjs', import.meta.url), env: 'AERE_CONF_MODUL_RISC' },
chitanta: { url: new URL('../payments/chitanta.mjs', import.meta.url), env: 'AERE_CONF_MODUL_CHITANTA' },
piata: { url: new URL('../market-settlement/piata.mjs', import.meta.url), env: 'AERE_CONF_MODUL_PIATA' },
baza: { url: new URL('../verifiable-data/baza.mjs', import.meta.url), env: 'AERE_CONF_MODUL_BAZA' },
};
export const caleModul = (k) => process.env[MODULE[k].env] || fileURLToPath(MODULE[k].url);
export const incarca = (k) => import(pathToFileURL(caleModul(k)).href);
const I = 'integrity';
/** verifier -> { artifacts it judges, where its rules come from, source rule -> [public id, level, what the rule says] } */
export const VERIFICATOARE = {
'aip23-envelope': { artifacts: ['envelope'], sursa: 'verify-proof', reguli: {
form: ['envelope.form', I, 'the envelope has a statement (object) and a statementHash (0x + 64 hex)'],
integrity: ['envelope.integrity', I, 'sha256 of the canonical statement text is the statementHash'],
signature: ['envelope.signature', 'authenticity', 'the ML-DSA signature the envelope carries verifies over the canonical statement text'],
finality: ['envelope.finality', 'anchoring', 'the statementHash is recorded in the notary under a state root that a verified post-quantum anchor certificate covers (level by site: see finalitySites)'] } },
'payment-risk': { artifacts: ['payment-risk'], sursa: 'risc', reguli: {
forma: ['risk.form', I, 'payer and payee are addresses; chain, asset, a positive integer amount and the time are given'],
activ: ['risk.asset', I, 'the asset on that chain is allowed by the declared policy'],
contrapartida: ['risk.counterparty', I, 'the payee is not the payer and is not a refused counterparty of the policy'],
curs: ['risk.rate', I, 'the USD rate is declared (value, source, time) when the policy requires it'],
limita: ['risk.limit', I, 'the amount in USD is within the per-payment limit of the policy'],
fereastra: ['risk.window', I, 'the amount and the count stay within the sliding window of the policy'] } },
'payment-receipt': { artifacts: ['payment-receipt'], sursa: 'chitanta', reguli: {
legatura: ['receipt.binding', I, 'the settlement envelope commits to this instruction and says the same thing'],
suma: ['receipt.amount', I, 'the USD amount is the amount at the declared rate, exactly'],
politica: ['receipt.policy', I, 'the policy shown hashes to the policy the risk verdict names'],
risc: ['receipt.risk', I, 'the risk verdict reproduces from the inputs and the policy, and it allowed this payment'],
lant: ['receipt.chain', I, 'the transaction supplied is this payment: payer, payee, value, block, mined'] } },
'market-timeline': { artifacts: ['market-timeline'], sursa: 'piata', reguli: {
forma: ['market.form', I, 'the timeline is well formed: events with name, arguments, sender, integer time and block; the job and the constants with their integer fields'],
intreg: ['market.complete', I, 'the timeline carries every event the job state implies: posted; claimed when a provider is set; submitted and disputed when their times are set'],
postare: ['market.posting', I, 'the JobPosted event matches the job (requester, spec, deadline, reward)'],
termen: ['market.deadline', I, 'claim and submission are within the deadline; an expiry refund only after it'],
fereastra: ['market.window', I, 'a dispute is raised inside the challenge window; a third party finalizes only after it'],
arbitru: ['market.arbiter', I, 'a dispute is resolved by the arbiter, or timed out only after the arbitration timeout'],
plata: ['market.payout', I, 'the terminal payout matches the final state (who, how much, which bond)'],
rezultat: ['market.result', I, 'the result bytes hash to the commitment on chain'],
replay: ['market.replay', I, 'the public task recomputed agrees with who was paid or slashed'],
dovada: ['market.proof', I, 'the execution envelope names this job and existed before the window closed'] } },
dataset: { artifacts: ['dataset-record', 'dataset-answer', 'dataset-consistency', 'dataset-sample'], sursa: 'baza', reguli: {
forma: ['dataset.form', I, 'a row carries its field commitments and its disclosed fields'],
angajament: ['dataset.commitment', I, 'the record, answer or proof names the commitment it is checked against'],
camp: ['dataset.field', I, 'a disclosed value opens its salted field commitment'],
includere: ['dataset.inclusion', I, 'the record is in the log of that version (RFC 9162 inclusion)'],
index: ['dataset.index', I, 'the row is at its stated position of the sorted index'],
interval: ['dataset.range', I, 'every row returned is inside the queried range'],
ordine: ['dataset.order', I, 'consecutive rows are in index order'],
completitudine: ['dataset.completeness', I, 'the rows are the whole contiguous run; the neighbours on both sides are outside the range'],
consistenta: ['dataset.consistency', I, 'the older log is a prefix of the newer (RFC 9162 consistency) and the versions are linked'],
esantion: ['dataset.sample', I, 'every sampled position is served and verifies'] } },
};
/**
* The FAILED sites of verify-proof's `finality` level, classified. `fragment` must appear on exactly one such source line; `re`
* matches the detail the verifier prints there; `level` is where the suite reports it. Only the last one is reachable by an honest
* endpoint (a claim that is not on chain); the others need an endpoint that lies or a chain whose anchor does not verify.
*/
export const FINALITY_SITES = [
{ fragment: 'most recent anchor did not verify', re: /^the chain's most recent anchor did not verify/, level: 'finality' },
{ fragment: 'the header the endpoint served for block', re: /^the header the endpoint served for block/, level: 'anchoring' },
{ fragment: 'has no account at', re: /has no account at/, level: 'anchoring' },
{ fragment: 'is not the AereNotary contract', re: /is not the AereNotary contract/, level: 'anchoring' },
{ fragment: 'the Merkle proof for block', re: /^the Merkle proof for block/, level: 'anchoring' },
{ fragment: 'not notarized:', re: /^not notarized:/, level: 'anchoring', reachable: true },
];
/** Verifiers published beside these that the suite does NOT enrol yet, with the pattern that counts their refusal sites. */
export const NEINROLATE = [
{ name: 'ai-trust', files: [new URL('../ai-trust/lant-de-incredere.mjs', import.meta.url)], re: /\bfail\('[a-z-]+', '[a-z-]+'/g, why: 'its refusals already carry a level and a code; enrolling it is a vector per code, not done here' },
{ name: 'agents (ledger)', files: [new URL('../agents/agent-ledger.mjs', import.meta.url)], re: /ok: false, error:/g, why: 'its refusals are free text; a rule id per refusal comes first' },
{ name: 'identity', files: [new URL('../identity/identity.mjs', import.meta.url)], re: /\bok\('/g, why: 'its checks are labelled rows, not rule ids' },
{ name: 'verify-layer (log and tree)', files: [new URL('../verify-layer/sidecar.mjs', import.meta.url), new URL('../verify-layer/jurnal-arbore.mjs', import.meta.url)], re: /ok: false/g, why: 'its refusals are free text; a rule id per refusal comes first' },
];
/** public rule id -> { verifier, source, level, says } for every mapped rule */
export function hartaPublica() {
const h = {};
for (const [v, d] of Object.entries(VERIFICATOARE)) for (const [src, [id, level, says]] of Object.entries(d.reguli)) h[id] = { verifier: v, source: src, level, says };
return h;
}
/** source rule ids of a verifier -> public ids */
export const public_ = (verifier, surse) => [...new Set(surse.map((s) => { const r = VERIFICATOARE[verifier].reguli[s]; if (!r) throw new Error(`${verifier}: the verifier named a rule this suite does not map: ${s}`); return r[0]; }))].sort();
/**
* Derives the rule table from the code. Returns { tabel, probleme, nemasurat }: `probleme` are refusals (a rule declared and not
* marked, marked and not declared, declared and not mapped, mapped and not declared, an unclassified finality site); `nemasurat`
* names a verifier whose source could not be read here. `cai` substitutes the file of a module (the probe's control derives from a
* planted copy to show that a rule added to a verifier and not mapped here is refused).
*/
export async function deriva({ cai = {} } = {}) {
const probleme = [], nemasurat = []; const tabel = { v: 1, kind: 'aere-conformance-rules', levels: LEVELS, verifiers: {}, notEnrolled: [] };
for (const [v, d] of Object.entries(VERIFICATOARE)) {
const rules = []; let declared = [], sites = {}, extra = {};
if (d.sursa === 'verify-proof') {
if (!fs.existsSync(VERIFY_PROOF)) { nemasurat.push(`${v}: the reference verifier is not at ${VERIFY_PROOF} (AERE_VERIFY_PROOF=<verify-proof.mjs>)`); continue; }
const linii = fs.readFileSync(VERIFY_PROOF, 'utf8').split('\n').filter((l) => /level\('[a-z]+',[^;]*'FAILED'/.test(l) && !/^\s*\/\//.test(l));
for (const l of linii) for (const m of l.matchAll(/level\('([a-z]+)',[^;]*?'FAILED'/g)) sites[m[1]] = (sites[m[1]] || 0) + 1;
declared = Object.keys(sites);
const fin = linii.filter((l) => /level\('finality'/.test(l));
for (const s of FINALITY_SITES) { const n = fin.filter((l) => l.includes(s.fragment)).length; if (n !== 1) probleme.push(`${v}: the finality site "${s.fragment}" appears on ${n} FAILED lines of the verifier (expected 1)`); }
if (fin.length !== FINALITY_SITES.length) probleme.push(`${v}: the verifier has ${fin.length} FAILED sites at its finality level, the suite classifies ${FINALITY_SITES.length}`);
extra = { finalitySites: FINALITY_SITES.map((s) => ({ site: s.fragment, level: s.level, reachableWithAnHonestEndpoint: !!s.reachable })) };
} else {
const p = cai[d.sursa] || fileURLToPath(MODULE[d.sursa].url); const src = fs.readFileSync(p, 'utf8'); const M = await import(pathToFileURL(p).href);
if (!Array.isArray(M.REGULI)) { probleme.push(`${v}: the module does not export REGULI`); continue; }
declared = [...M.REGULI];
for (const m of src.matchAll(/\/\*PAZA:([a-z]+)\*\//g)) sites[m[1]] = (sites[m[1]] || 0) + 1;
for (const r of declared) if (!sites[r]) probleme.push(`${v}: rule ${r} is declared and no check carries its marker`);
for (const r of Object.keys(sites)) if (!declared.includes(r)) probleme.push(`${v}: a check is marked ${r}, a rule the module does not declare`);
}
for (const r of declared) if (!d.reguli[r]) probleme.push(`${v}: the verifier names rule ${r} and the suite does not map it (a new rule needs a public id, a level and a vector)`);
for (const r of Object.keys(d.reguli)) if (!declared.includes(r)) probleme.push(`${v}: the suite maps rule ${r}, which the verifier no longer names`);
for (const [src, [id, level, says]] of Object.entries(d.reguli)) rules.push({ id, source: src, level, sites: sites[src] || 0, says });
tabel.verifiers[v] = { artifacts: d.artifacts, rules, ...extra };
}
for (const n of NEINROLATE) {
let sitesN = 0, citit = true;
for (const u of n.files) { const p = fileURLToPath(u); if (!fs.existsSync(p)) { citit = false; continue; } sitesN += (fs.readFileSync(p, 'utf8').match(n.re) || []).length; }
tabel.notEnrolled.push({ verifier: n.name, refusalSites: citit ? sitesN : null, why: n.why });
if (citit && sitesN === 0) probleme.push(`not enrolled ${n.name}: the pattern that counts its refusal sites finds none (the pattern is stale)`);
}
return { tabel, probleme, nemasurat };
}