175 lines
20 KiB
JavaScript
175 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
// proba-verifiable-data.mjs: the laboratory of the verifiable dataset (roadmap point 20), three states per row: VERDE (the named
|
|
// verdict), ROSU (another verdict), STRICAT (the probe itself broke). No network, except --viu.
|
|
// node proba-verifiable-data.mjs the laboratory: 24 rows (honest answers VALID; nine forged ones INVALID, each
|
|
// refused for its named reason; availability sampling on a local server)
|
|
// node proba-verifiable-data.mjs --control-negativ plantings on a COPY of baza.mjs (PAZA markers): each must turn its forged
|
|
// fixture green-by-mistake, which proves the check is load-bearing
|
|
// node proba-verifiable-data.mjs --viu plus: the newest commitment notarized on testnet 28001 with a test key and read
|
|
// back under a post-quantum certified anchor (AERE_NOTARY_KEY_FILE, AERE_ETHERS_DIR)
|
|
// Env: AERE_VERIFY_PROOF (the AIP-23 verifier; without it the envelope rows are NEMASURAT and the exit code is 2), AERE_VD_MODUL (the
|
|
// module under test; the control sets it to its planted copy).
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import crypto from 'node:crypto';
|
|
import { createRequire } from 'node:module';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { leafHash, merkleTree } from '../verify-layer/arbore-merkle.mjs';
|
|
import { porneste } from './serveste.mjs';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
const RAD = path.resolve(AICI, '..', '..');
|
|
const MODUL = process.env.AERE_VD_MODUL || path.join(AICI, 'baza.mjs');
|
|
const VERIFY_PROOF = process.env.AERE_VERIFY_PROOF || path.join(RAD, 'verificator-falcon', 'verify-proof.mjs');
|
|
const RPC_28001 = process.env.AERE_TESTNET_RPC || 'https://testnet-rpc.aere.network';
|
|
const CHEIE = process.env.AERE_NOTARY_KEY_FILE || path.join(RAD, 'tools', 'aere-quantum-shield', 'dovezi-l1', '.chei', 'cheie-de-proba-28001.key');
|
|
const ETHERS_DIR = process.env.AERE_ETHERS_DIR || path.join(RAD, 'contracts');
|
|
const a = process.argv.slice(2); const viu = a.includes('--viu'); const control = a.includes('--control-negativ');
|
|
const B = await import(pathToFileURL(MODUL).href + '?t=' + Date.now());
|
|
|
|
const TARI = ['RO', 'DE', 'FR', 'ES', 'IT'];
|
|
function inregistrari(n, de = 0) { const r = []; for (let k = de; k < de + n; k++) r.push({ id: 'r' + k, tara: TARI[k % 5], varsta: 18 + ((k * 37) % 63), email: 'u' + k + '@example.test', privat: 'nu-se-dezvaluie-' + k }); return r; }
|
|
const clona = (x) => JSON.parse(JSON.stringify(x));
|
|
|
|
/** The fixtures: each returns { verdict, detail }; `asteptat` is the verdict the honest module must give. */
|
|
export async function fixturi(B, { dir, server }) {
|
|
const F = [];
|
|
const db = B.deschide(dir); const c1 = B.versiune(db, 1), c2 = B.versiune(db, 2);
|
|
const brut = (f) => db.inregistrari.slice(0, c2.size).filter((r) => f(r.fields)).length; // the count straight from the records, to compare with the answer
|
|
F.push({ nume: 'L1 a record of version 1 proves itself (log inclusion, field commitments)', asteptat: 'VALID', r: () => { const x = B.verificaInregistrare(B.dovadaInregistrare(db, 1, 7), c1); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'L2 version 1 is a prefix of version 2 (consistency)', asteptat: 'VALID', r: () => { const x = B.verificaConsistentaVersiuni(B.dovadaConsistenta(db, 1, 2), c1, c2); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'L3 equality query tara=RO at version 2: VALID, as many rows as the data has, only tara disclosed', asteptat: 'VALID', r: () => {
|
|
const q = B.interogheaza(db, 2, { field: 'tara', eq: 'RO' }); const x = B.verificaRaspuns(q, c2);
|
|
const n = brut((r) => r.tara === 'RO'); const doarTara = q.rows.every((r) => Object.keys(r.disclosed).join() === 'tara' && !JSON.stringify(r).includes('nu-se-dezvaluie'));
|
|
return { verdict: x.verdict === 'VALID' && x.rows === n && doarTara ? 'VALID' : 'INVALID', detail: `${x.rows} rows (data: ${n}); only tara: ${doarTara}; ${x.motive.join('; ')}` }; } });
|
|
F.push({ nume: 'L4 range query varsta 30..40 with email disclosed too: VALID, count as the data', asteptat: 'VALID', r: () => {
|
|
const q = B.interogheaza(db, 2, { field: 'varsta', from: 30, to: 40, fields: ['email'] }); const x = B.verificaRaspuns(q, c2);
|
|
const n = brut((r) => r.varsta >= 30 && r.varsta <= 40); const cuEmail = q.rows.every((r) => r.disclosed.email && r.disclosed.varsta && !r.disclosed.privat);
|
|
return { verdict: x.verdict === 'VALID' && x.rows === n && cuEmail ? 'VALID' : 'INVALID', detail: `${x.rows} rows (data: ${n}); varsta+email only: ${cuEmail}; ${x.motive.join('; ')}` }; } });
|
|
F.push({ nume: 'L5 open range varsta <= 20 (run starts at index 0, no neighbour before) and varsta >= 79 (ends the index)', asteptat: 'VALID', r: () => {
|
|
const q1 = B.interogheaza(db, 2, { field: 'varsta', to: 20 }); const q2 = B.interogheaza(db, 2, { field: 'varsta', from: 79 });
|
|
const x1 = B.verificaRaspuns(q1, c2), x2 = B.verificaRaspuns(q2, c2);
|
|
return { verdict: x1.verdict === 'VALID' && x2.verdict === 'VALID' && q1.before === null && q2.after === null && x1.rows === brut((r) => r.varsta <= 20) && x2.rows === brut((r) => r.varsta >= 79) ? 'VALID' : 'INVALID', detail: `${x1.rows} + ${x2.rows} rows; ${x1.motive.concat(x2.motive).join('; ')}` }; } });
|
|
F.push({ nume: 'L6 a query with no match: zero rows, the two neighbours prove the gap', asteptat: 'VALID', r: () => { const q = B.interogheaza(db, 2, { field: 'tara', eq: 'GB' }); const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: `${x.rows} rows, before=${q.before && q.before.value}, after=${q.after && q.after.value}; ${x.motive.join('; ')}` }; } });
|
|
F.push({ nume: 'L7 the audit rebuilds both versions from the records: every root matches', asteptat: 'VALID', r: () => { const x = B.auditeaza(db); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.probleme.join('; ') }; } });
|
|
// forged answers: each must be INVALID for its named reason
|
|
F.push({ nume: 'N1 a disclosed value changed after commitment (varsta 31 -> 32)', asteptat: 'INVALID', paza: 'camp', r: () => { const q = B.interogheaza(db, 2, { field: 'tara', eq: 'DE', fields: ['varsta'] }); q.rows[0].disclosed.varsta.value += 1; const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'N2 a row dropped from the middle of the run', asteptat: 'INVALID', r: () => { const q = B.interogheaza(db, 2, { field: 'tara', eq: 'FR' }); q.rows.splice(3, 1); const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'N3 the last matching row dropped and shown as the neighbour after (its value is still in range)', asteptat: 'INVALID', paza: 'completitudine', r: () => {
|
|
const q = B.interogheaza(db, 2, { field: 'tara', eq: 'IT' }); const ultima = q.rows.pop();
|
|
q.after = { pos: ultima.pos, value: ultima.disclosed.tara.value, i: ultima.i, recordLeaf: B.frunzaInregistrare(ultima.id, ultima.c), indexProof: ultima.indexProof };
|
|
const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'N4 the first matching row dropped and shown as the neighbour before', asteptat: 'INVALID', paza: 'completitudine', r: () => {
|
|
const q = B.interogheaza(db, 2, { field: 'varsta', from: 50, to: 60 }); const prima = q.rows.shift();
|
|
q.before = { pos: prima.pos, value: prima.disclosed.varsta.value, i: prima.i, recordLeaf: B.frunzaInregistrare(prima.id, prima.c), indexProof: prima.indexProof };
|
|
const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'N5 an answer of version 1 presented against the commitment of version 2', asteptat: 'INVALID', r: () => { const q = B.interogheaza(db, 1, { field: 'tara', eq: 'RO' }); const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'N6 a rewritten history: a "version 2" built from the records with one changed, with the honest consistency proof', asteptat: 'INVALID', paza: 'consistenta', r: () => {
|
|
const fals = clona(c2); fals.logRoot = '0x' + crypto.createHash('sha256').update('alt arbore').digest('hex');
|
|
const d = B.dovadaConsistenta(db, 1, 2); d.to.commitmentHash = B.hashAngajament(fals);
|
|
const x = B.verificaConsistentaVersiuni(d, c1, fals); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'N7 a consistency proof with one hash of the path changed', asteptat: 'INVALID', r: () => { const d = B.dovadaConsistenta(db, 1, 2); d.proof.path[0] = '0x' + 'ab'.repeat(32); const x = B.verificaConsistentaVersiuni(d, c1, c2); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'N8 a publisher whose index is NOT sorted (two rows with valid proofs, out of order)', asteptat: 'INVALID', paza: 'ordine', r: () => {
|
|
// two records, the index built in the wrong order (b before a), everything else honest: the reader sees it on the first answer with two rows
|
|
const recs = [{ id: 'x', fields: { k: 'b' }, salts: { k: '00'.repeat(16) } }, { id: 'y', fields: { k: 'a' }, salts: { k: '11'.repeat(16) } }];
|
|
const rc = recs.map((r) => { const c = { k: B.angajamentCamp('k', r.fields.k, r.salts.k) }; return { ...r, c, leaf: B.frunzaInregistrare(r.id, c) }; });
|
|
const log = merkleTree(rc.map((r) => r.leaf)); const idx = merkleTree(rc.map((r, i) => B.frunzaIndex('k', r.fields.k, i, r.leaf))); // unsorted: 'b' at 0, 'a' at 1
|
|
const c = { v: 1, format: B.FORMAT, name: 'nesortat', version: 1, size: 2, logRoot: log.root(), createdAt: '2026-10-02T00:00:00.000Z', indexes: { k: { root: idx.root(), size: 2 } }, previous: null };
|
|
const rand = (i) => ({ pos: i, i, id: rc[i].id, c: rc[i].c, disclosed: { k: { value: rc[i].fields.k, salt: rc[i].salts.k } }, logProof: log.inclusionProof(i, 2), indexProof: idx.inclusionProof(i, 2) });
|
|
const q = { v: 1, kind: B.KIND_RASPUNS, name: 'nesortat', version: 1, commitmentHash: B.hashAngajament(c), field: 'k', from: 'a', to: 'b', indexSize: 2, rows: [rand(0), rand(1)], before: null, after: null };
|
|
const x = B.verificaRaspuns(q, c); return { verdict: x.verdict, detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'N9 a record whose log proof points at another position', asteptat: 'INVALID', r: () => { const d = B.dovadaInregistrare(db, 2, 5); d.i = 6; d.logProof.index = 6; const x = B.verificaInregistrare(d, c2); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } });
|
|
F.push({ nume: 'N10 malformed answers (a row without the queried field, a null row, a neighbour without a value, rows not a list) are refused by name, never thrown', asteptat: 'INVALID', r: () => {
|
|
const rez = [(q) => { delete q.rows[1].disclosed.varsta; }, (q) => { q.rows[1] = null; }, (q) => { q.rows[0] = null; }, (q) => { delete q.before.value; }, (q) => { q.rows = 'x'; }].map((f) => { const q = B.interogheaza(db, 2, { field: 'varsta', from: 30, to: 40 }); f(q); const x = B.verificaRaspuns(q, c2); return x.verdict + ':' + x.reguli.length; });
|
|
return { verdict: rez.every((x) => /^INVALID:[1-9]/.test(x)) ? 'INVALID' : 'VALID', detail: rez.join(' ') }; } });
|
|
// availability sampling against the local server
|
|
const ia = async (u) => { const r = await fetch(u, { signal: AbortSignal.timeout(10000) }); if (!r.ok) return null; return r.json(); };
|
|
F.push({ nume: 'A1 availability sampling, 32 records drawn from a seed, all served and verified', asteptat: 'VALID', r: async () => { const x = await B.esantioneaza({ url: server.url, commitment: c2, k: 32, seed: 'proba-a1', ia }); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: `${x.verificate}/${x.cerute}; ${x.motive.join('; ')}` }; } });
|
|
F.push({ nume: 'A2 a publisher that withholds record 123: the full sample finds it', asteptat: 'INVALID', paza: 'esantion', r: async () => { const x = await B.esantioneaza({ url: server.ascuns, commitment: c2, k: c2.size, seed: 'proba-a2', ia }); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: `${x.verificate}/${x.cerute}; not served: ${x.lipsa.join(',')}` }; } });
|
|
F.push({ nume: 'A3 the served query answer (HTTP) verifies like the local one', asteptat: 'VALID', r: async () => { const q = await ia(server.url + '/query/2?field=tara&eq=%22ES%22&fields=varsta'); const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: `${x.rows} rows; ${x.motive.join('; ')}` }; } });
|
|
F.push({ nume: 'A4 the served consistency proof 1 -> 2 verifies', asteptat: 'VALID', r: async () => { const d = await ia(server.url + '/consistency/1/2'); const x = B.verificaConsistentaVersiuni(d, c1, c2); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } });
|
|
// the commitment as an AIP-23 envelope
|
|
F.push({ nume: 'E1 the envelope of version 2 names this commitment (sha256 of its canonical bytes); a changed commitment does not', asteptat: 'VALID', r: () => { const env = B.plicAngajament(c2); const alt = clona(c2); alt.size += 1; return { verdict: B.verificaPlicAngajament(env, c2) && !B.verificaPlicAngajament(env, alt) ? 'VALID' : 'INVALID', detail: env.statementHash.slice(0, 18) + '..' }; } });
|
|
F.push({ nume: 'E2 the AIP-23 reference verifier accepts the envelope (no new verification code)', asteptat: 'VALID', nemasurat: !fs.existsSync(VERIFY_PROOF), r: () => {
|
|
const f = path.join(os.tmpdir(), 'vd-plic-' + process.pid + '.json'); fs.writeFileSync(f, JSON.stringify(B.plicAngajament(c2)));
|
|
try { const r = spawnSync(process.execPath, [VERIFY_PROOF, f, '--json'], { encoding: 'utf8', timeout: 120000 }); let v; try { v = JSON.parse(r.stdout); } catch { return { verdict: 'STRICAT', detail: 'verifier gave no JSON' }; } return { verdict: v.verdict, detail: (v.levels || []).map((l) => l.level + ':' + l.state).join(' ') }; } finally { fs.rmSync(f, { force: true }); } } });
|
|
return F;
|
|
}
|
|
|
|
/** Builds the dataset of the laboratory: 300 records committed as version 1, 100 more as version 2. */
|
|
export function laborator(dir) {
|
|
const db = B.creeaza(path.join(dir, 'set'), { name: 'proba', indexes: ['tara', 'varsta'] });
|
|
B.adauga(db, inregistrari(300)); B.angajeaza(db, { createdAt: '2026-10-02T08:00:00.000Z' });
|
|
B.adauga(db, inregistrari(100, 300)); B.angajeaza(db, { createdAt: '2026-10-02T08:01:00.000Z' });
|
|
return path.join(dir, 'set');
|
|
}
|
|
|
|
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-vd-'));
|
|
let codIesire = 0;
|
|
try {
|
|
const dir = laborator(T);
|
|
const s1 = await porneste({ dir }); const s2 = await porneste({ dir, ascunde: [123] });
|
|
const server = { url: s1.url, ascuns: s2.url };
|
|
try {
|
|
if (!control) {
|
|
let verzi = 0, rosii = 0, stricate = 0, nem = 0;
|
|
for (const f of await fixturi(B, { dir, server })) {
|
|
if (f.nemasurat) { console.log(` [NEMASURAT] ${f.nume}: the AIP-23 verifier is not at ${VERIFY_PROOF} (AERE_VERIFY_PROOF)`); nem++; continue; }
|
|
let r; try { r = await f.r(); } catch (e) { r = { verdict: 'STRICAT', detail: String(e.stack || e.message).split('\n').slice(0, 2).join(' ').slice(0, 200) }; }
|
|
const stare = r.verdict === 'STRICAT' ? 'STRICAT' : r.verdict === f.asteptat ? 'VERDE' : 'ROSU';
|
|
if (stare === 'VERDE') verzi++; else if (stare === 'ROSU') rosii++; else stricate++;
|
|
console.log(` [${stare.padEnd(7)}] ${f.nume}: ${r.verdict}${r.detail ? ' (' + r.detail.slice(0, 160) + ')' : ''}`);
|
|
}
|
|
if (viu) {
|
|
const db = B.deschide(dir); const c2 = B.versiune(db, 2); const env = B.plicAngajament(c2);
|
|
const { notarizeEnvelope } = await import('../agents/agent-ancora.mjs');
|
|
const { ethers } = createRequire(path.join(ETHERS_DIR, 'package.json'))('ethers');
|
|
const n = await notarizeEnvelope({ envelope: env, kind: 'aere-proof-of-data', rpc: RPC_28001, keyFile: CHEIE, ethers });
|
|
if (!n.ok) { console.log(` [STRICAT] V1 notarization on 28001: ${n.motiv}`); stricate++; }
|
|
else {
|
|
const f = path.join(T, 'plic-notarizat.json'); fs.writeFileSync(f, JSON.stringify(n.envelope, null, 1));
|
|
console.log(` [VERDE ] V1 version 2 notarized on 28001: firstSeen ${n.envelope.notarization.firstSeen}${n.envelope.notarization.txHash ? ', tx ' + n.envelope.notarization.txHash + ' block ' + n.envelope.notarization.block : ' (already)'}`); verzi++;
|
|
let v = null;
|
|
for (let k = 0; k < 24; k++) { const r = spawnSync(process.execPath, [VERIFY_PROOF, f, '--json', '--rpc', RPC_28001, '--chain', '28001'], { encoding: 'utf8', timeout: 300000 }); try { v = JSON.parse(r.stdout); } catch { v = null; } const fin = v && (v.levels || []).find((l) => l.level === 'finality'); if (fin && fin.state !== 'PENDING') break; await new Promise((ok) => setTimeout(ok, 5000)); }
|
|
const fin = v && (v.levels || []).find((l) => l.level === 'finality');
|
|
const okFin = v && v.verdict === 'VALID' && fin && fin.state === 'PASSED';
|
|
console.log(` [${okFin ? 'VERDE ' : 'ROSU '}] V2 read back under a post-quantum certified anchor: ${v ? v.verdict : '?'}; finality ${fin ? fin.state + ': ' + String(fin.detail).slice(0, 140) : '?'}`); okFin ? verzi++ : rosii++;
|
|
const dov = path.join(AICI, 'dovezi'); fs.mkdirSync(dov, { recursive: true });
|
|
const out = path.join(dov, 'notarizare-28001-' + new Date().toISOString().replace(/[:.]/g, '').slice(0, 15) + 'Z.json');
|
|
fs.writeFileSync(out, JSON.stringify({ commitment: c2, envelope: n.envelope, verdict: v }, null, 1)); console.log(' evidence: ' + path.relative(RAD, out));
|
|
}
|
|
}
|
|
console.log(`\nverifiable data: ${verzi} VERDE, ${rosii} ROSU, ${stricate} STRICAT${nem ? ', ' + nem + ' NEMASURAT' : ''}`);
|
|
codIesire = stricate || rosii ? 1 : nem ? 2 : 0;
|
|
} else {
|
|
// the control: a planting per PAZA marker on a copy of baza.mjs; the fixture that the marker guards must turn green-by-mistake
|
|
const SURSA = fs.readFileSync(MODUL, 'utf8'); let bune = 0; const marcaje = ['camp', 'ordine', 'completitudine', 'consistenta', 'esantion'];
|
|
const toate = await fixturi(B, { dir, server });
|
|
for (const m of marcaje) {
|
|
const copie = path.join(AICI, `_plantat-baza-${process.pid}-${m}.mjs`); const n = SURSA.split(`/*PAZA:${m}*/`).length - 1;
|
|
const tinte = toate.filter((f) => f.paza === m);
|
|
let stare, detaliu;
|
|
try {
|
|
if (n < 1 || !tinte.length) { stare = 'STRICAT'; detaliu = `marker ${n} times, ${tinte.length} fixtures`; }
|
|
else {
|
|
fs.writeFileSync(copie, SURSA.split(`/*PAZA:${m}*/`).join("|| String(1) === '1' /*PLANTAT*/"));
|
|
const P = await import(pathToFileURL(copie).href + '?t=' + Date.now());
|
|
const fx = await fixturi(P, { dir, server }); const rez = [];
|
|
for (const t of tinte) { const f = fx.find((x) => x.nume === t.nume); const r = await f.r(); rez.push(`${t.nume.split(' ')[0]}:${r.verdict}`); }
|
|
const verzi = rez.every((x) => x.endsWith(':VALID'));
|
|
stare = verzi ? 'VERDE' : 'ROSU'; detaliu = (verzi ? 'the forged fixture is accepted with the check planted out: ' : 'the planting did not change the verdict: ') + rez.join(' ');
|
|
}
|
|
} catch (e) { stare = 'STRICAT'; detaliu = String(e.message).slice(0, 160); } finally { fs.rmSync(copie, { force: true }); }
|
|
if (stare === 'VERDE') bune++;
|
|
console.log(` [${stare.padEnd(7)}] PAZA:${m} (${n} marker${n === 1 ? '' : 's'}) ${detaliu}`);
|
|
}
|
|
const ramase = fs.readdirSync(AICI).filter((f) => f.startsWith('_plantat-baza-'));
|
|
console.log(`CONTROL: ${bune} of ${marcaje.length} checks proven load-bearing${ramase.length ? '; copies left: ' + ramase.join(',') : ''}`);
|
|
codIesire = bune === marcaje.length && !ramase.length ? 0 : 1;
|
|
}
|
|
} finally { await s1.inchide(); await s2.inchide(); }
|
|
} finally { fs.rmSync(T, { recursive: true, force: true }); }
|
|
process.exitCode = codIesire;
|