aere-quantum/agents/martor-agenti.mjs

1130 lines
115 KiB
JavaScript

#!/usr/bin/env node
// martor-agenti.mjs: MARTORUL GAZDUIT al registrelor agentilor (roadmap pista B, B3, linia Agents, punctul 22, 2026-10-19).
//
// Ce lipsea dupa agent-ancora.mjs (lantul ca martor, 2026-09-30): ca sa aiba un moment certificat al capului, proprietarul agentului
// trebuia sa alerge singur notarul, portofelul de gaz si verificatorul AIP-23. Aici e un serviciu la care agentul TRIMITE capul
// registrului lui (semnat cu cheia lui ML-DSA-65) si primeste inapoi o dovada pe care o verifica oricine, fara martor.
//
// Forma e cea care exista deja, nu una inventata:
// - capul trimis e un CHECKPOINT al jurnalului agentului, ca in C2SP tlog-witness (`add-checkpoint`): martorul il primeste numai
// daca e semnat de cheia jurnalului (aici a agentului), daca numeste ca predecesor exact ultimul cap pe care martorul l-a vazut
// pentru acel jurnal (altfel 409 cu ultimul cap, ca 409-ul cu marimea ultimului checkpoint din C2SP), si il persista INAINTE sa
// raspunda. Registrul agentului e un lant de hash-uri, nu un arbore: o dovada de consistenta intre doua capete ar cere intrarile,
// adica actiunile, pe care martorul nu le vede niciodata. In locul ei agentul semneaza `previous` (capul pe care il continua);
// coborarea efectiva o verifica oricine tine registrul (ancorele trebuie sa fie toate pe registrul aratat).
// - doua afirmatii semnate de agent care dau doua hash-uri diferite aceleiasi pozitii (acelasi seq, sau `previous` contra unui cap deja
// vazut) sunt o dovada de ECHIVOCARE: martorul nu primeste capul, pastreaza dovada si o intoarce (C2SP: "MUST NOT cosign").
// - capetele primite sunt frunzele unui jurnal transparent RFC 9162 (ca Rekor si Certificate Transparency): la primire o promisiune
// semnata de martor cu momentul primirii (analogul SCT / signed entry timestamp), apoi, la cel mult maxMergeDelay (MMD-ul din RFC 9162),
// un cap de jurnal (checkpoint: numele jurnalului, marimea, radacina, ca in C2SP tlog-checkpoint) semnat de martor si NOTARIZAT pe
// AereNotary (testnetul 28001 in sandbox), deci momentul lui e certificat post-cuantic de verificatorul AIP-23, nu declarat de martor.
// - chitanta = capul de jurnal (plic AIP-23 `aere-proof-of-agent-witness-checkpoint`, VALID la verificatorul de referinta) + capul primit
// + pozitia + dovada de includere (RFC 9162, 2.1.3), ca `verification.inclusionProof` din Rekor.
// - jurnalul e IMPARTIT IN CIOBURI (2026-10-19, revizia B3, constatarile 3 si 12), cum isi impart jurnalele Rekor si Certificate Transparency
// (sharding, temporal sharding): un ciob plin se INGHEATA cu un cap de jurnal final si martorul incepe ciobul urmator, cu nume nou
// (`<jurnal>/<k>`), al carui prim cap de jurnal numeste capul final al celui de dinainte. Martorul tine ciobul curent si pe cel de
// dinainte (CIOBURI_PASTRATE); unul mai vechi se scoate de pe disc si din memorie, iar chitantele lui raman verificabile fara martor.
// Pana la reparatie un jurnal plin raspundea 503 tuturor, pentru totdeauna, si il putea umple oricine.
// - a doua revizie adversariala (2026-10-19, 15 constatari R2-01..R2-15): lista capetelor unui agent (GET .../heads?session) e SEMNATA de martor
// pagina cu pagina, cu momentul ei, si numai pagina semnata conteaza la `verify --witness-url` (pana atunci id-ul din /health si lista erau
// nesemnate, deci un server ostil la adresa data se dadea drept martorul fixat); o dovada de echivocare se leaga de ciobul capului pe care il
// contrazice (deci sunt cel mult atatea cat frunze pastrate si nimeni nu poate umple locul alteia); un ciob nu se scoate pana nu i s-a
// notarizat capul de jurnal final; arborele jurnalului e INCREMENTAL (fara memorie pe interval); lacatul starii isi poarta pornirea masinii.
//
// TIMPUL, spus exact (si scris in README): capul afirma "la createdAt capul meu e seq" (semnat de agent). Martorul il primeste numai la cel mult
// TOLERANTA_S de ceasul lui si scrie momentul primirii in frunza (sub radacina notarizata). Lantul certifica momentul F (firstSeen) al capului de
// jurnal; verificatorul cere primire <= F + toleranta si F - primire <= intarzierea maxima (a martorului si a lui). De aici ancora {seq, hash, at:
// createdAt} pentru verifyLedger: o intrare de DUPA cap care declara un moment sub createdAt - 300 s contrazice semnatura agentului insusi
// (antedatare prinsa). Lantul singur margineste createdAt la cel putin F - intarzierea maxima - 600 s; strangerea la 300 s de ceasul martorului
// tine NUMAI pentru un martor pe care verificatorul il FIXEAZA (`martori`, `--witness`): orice cheie ML-DSA-65 isi poate fi singura martor
// (revizia B3, constatarile 1 si 17), deci fara fixare rezultatul spune ca marginea e numai a lantului si numeste martorul fiecarei chitante.
// Martorul nu vede si nu judeca actiunile: primeste numai capete (seq + hash) si refuza orice camp in plus.
//
// node martor-agenti.mjs request --key agent.key.pem --ledger l.json [--seq N] [--previous <seq>|none] [--out head.json]
// node martor-agenti.mjs submit --key agent.key.pem --ledger l.json --url <base> [--seq N] [--out response.json] (previous = ultimul cap al martorului)
// node martor-agenti.mjs receipt --url <base> --id 0x<head statementHash> --ledger l.json [--witness aere-witness:<40 hex>] [--out receipt.json]
// (scrie raspunsul NUMAI daca e chitanta capului cerut, a registrului dat, cu semnatura martorului verificata)
// node martor-agenti.mjs verify-receipt --receipt r.json [--ledger l.json] [--witness aere-witness:<40 hex>] numai local (fara lant)
// node martor-agenti.mjs verify-equivocation --proof p.json
// Lantul (momentul certificat) il judeca `agent-cli.mjs verify --anchors receipt.json --rpc <url> --verify-proof <verify-proof.mjs> --witness <id>`.
// Iesiri: 0 da (si: nimic nou de trimis); 1 nu (refuzat, invalid); 2 folosire gresita sau NEMASURAT (martorul nu raspunde, raspunde 503 sau 429,
// chitanta inca nenotarizata, un cap inca pending).
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { agentIdFromKey, sessionId, agentFromPrivateKeyPem } from './agent-ledger.mjs';
import { HEAD_KIND, firstSeenFromVerdict, ruleazaVerificatorul } from './agent-ancora.mjs';
import { leafHash, verifyInclusion, verifyConsistency } from '../verify-layer/arbore-merkle.mjs';
import { buildProof, SCHEME } from '../proof-kinds/proof-kinds.mjs';
export { HEAD_KIND, verifyConsistency };
export const VERSION = 'aere-agent-witness/1.1 (2026-10-19)';
export const CHECKPOINT_KIND = 'aere-proof-of-agent-witness-checkpoint'; // proof-kinds: 'agent-witness-checkpoint'
export const PROMISE_KIND = 'aere-proof-of-time'; // proof-kinds: 'time' (un subiect a existat la un moment, dupa o sursa)
export const RECEIPT_FORMAT = 'aere-agent-witness-receipt-1';
export const EQUIVOCATION_KIND = 'aere-agent-head-equivocation';
export const LOG_IMPLICIT = 'aere-agent-witness/28001';
export const LANT = 28001; // sandbox: testnetul public; mainnet-ul (2800) la activare, cu tranzactiile fondatorului
export const TOLERANTA_S = 300; // aceeasi ca a registrului (agent-ledger.mjs)
export const MERGE_MAX_S = 1800; // intarzierea maxima promisa intre primire si un cap notarizat care o acopera
export const CERERE_MAX_OCTETI = 16384; // un cap semnat ML-DSA-65 are ~8 KB (cheia PEM ~2,7 KB, semnatura ~4,4 KB in base64)
// PLAFOANELE starii, DERIVATE din masuratoare pe starea CEA MAI REA (2026-10-19, a doua revizie B3, R2-02 si R2-06; Node 24.14.1, Windows; pe
// Linux NEMASURAT; README, "Bounds"): doua cioburi pline (2 x 20.000 de frunze), un cap de jurnal pe frunza (trafic mic: cel mult un cap pe tur)
// si o dovada de echivocare pastrata pe frunza (o sesiune pe frunza: marginea dovezilor prin constructie, ECHIVOCARI_TOTAL):
// - pe disc 1.320 MB (33 KB pe frunza: capul semnat si promisiunea ~15,5 KB, randul frunzei 0,35 KB, capul de jurnal ~7,9 KB, dovada ~7,9 KB,
// numai capul nou); instalatorul cere 2048 MB liberi (~1,55x).
// - in memorie, intr-un proces nou: deschiderea 266 MB RSS la varf (procesul gol 50 MB; 37 s pe laptop), functionarea (2.000 de chitante, 2.000
// de pagini semnate, 2.000 de dovezi, consistenta pe ciobul inghetat) 277 MB, cu cele trei limitatoare la plafonul de chei (3 x 100.000) 330 MB:
// MemoryMax=512M al unitatii (~1,55x). Prima masurare (un cap de jurnal la 1.000 de frunze, 180,6 MB, MemoryMax=400M) a ratat cazul acesta: cu
// un cap de jurnal pe frunza memoria pe interval a arborelui (~5 KB pe cap de jurnal) ducea redeschiderea la ~372 MB (masurat de verificator)
// si functionarea peste 400M; arborele e acum incremental (arboreIncremental, mai jos).
// - inainte de a doua revizie: nicio primire peste 200 ms (rotirile incluse; cea mai lunga 157 ms); dupa ea latenta pe cerere NEMASURATA (rularea
// starii celei mai rele, cu un cap de jurnal construit dupa fiecare frunza si fara fsync: 8,6 ms pe frunza in medie).
export const PLAFON_FRUNZE = 20000; // frunze pe ciob
export const CIOBURI_PASTRATE = 2; // ciobul curent si cel de dinainte
// dovezile de echivocare PASTRATE: cel mult UNA pe agent si politica ("o dovada ajunge"). Prima reparatie (constatarea 9) a pus si un plafon
// GLOBAL de 2.000, pe care il putea umple oricine cu sesiuni proprii (doua POST-uri pe sesiune), iar de acolo o echivocare REALA a altui agent nu
// mai era pastrata (revizia B3 a doua, R2-02). Acum o dovada se leaga de ciobul capului pe care il CONTRAZICE si se scoate odata cu el; fiecare
// dovada are deci o sesiune proprie cu cel putin o frunza intr-un ciob pastrat, iar numarul lor e marginit PRIN CONSTRUCTIE de frunzele pastrate:
// cel mult CIOBURI_PASTRATE x PLAFON_FRUNZE = 40.000. Plafonul global e chiar marginea aceasta (nu se atinge niciodata pe starea reala; il pot
// cobori numai probele), deci un strain nu poate nici impinge afara, nici bloca dovada altcuiva. Pe disc o dovada tine numai capul NOU semnat
// (capul contrazis e deja in `capete/` al ciobului ei): ~7,9 KB, deci cel mult 40.000 x 7,9 KB = ~316 MB; in memorie un rand de index (masurat in
// README, "Bounds"). Peste plafon (numai cu un plafon coborat) dovada se intoarce clientului, nepastrata, si /health numara refuzurile.
export const ECHIVOCARI_TOTAL = CIOBURI_PASTRATE * PLAFON_FRUNZE;
// lista capetelor unui agent (GET .../heads?session), SEMNATA de martor pagina cu pagina (revizia B3 a doua, R2-01); verificatorul o primeste
// numai daca e semnata de martorul fixat si e mai noua de TOLERANTA_S fata de ceasul lui (o pagina veche, reluata, nu e lista de acum)
export const PAGINA_KIND = 'aere-agent-witness-heads';
export const PAGINA_MAX = 100; // capete pe pagina in GET .../heads (si sesiuni pe pagina, fara ?session)
export const SANDBOX = 'sandbox: testnet 28001; mainnet: at activation';
const ALG = 'ml-dsa-65';
const H32 = /^0x[0-9a-f]{64}$/;
const HASH_INTRARE = /^[0-9a-f]{64}$/;
const AGENT_ID = /^aere-agent:[0-9a-f]{40}$/;
const WITNESS_ID = /^aere-witness:[0-9a-f]{40}$/;
const SESIUNE = /^[0-9a-f]{32}$/;
const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
const sha = (b) => '0x' + crypto.createHash('sha256').update(typeof b === 'string' ? Buffer.from(b, 'utf8') : b).digest('hex');
const obiect = (x) => !!x && typeof x === 'object' && !Array.isArray(x);
// REGULA (c), ca la agent-cli.mjs (revizia B3 a cincea, R5-04): tipul unei valori venite din afara se judeca INAINTE de orice folosire; String() pe
// un obiect {"toString": 0} arunca, deci in judecati un camp care trebuie sa fie sir trece prin sir(), care da sirul gol pentru orice alt tip
const sir = (x) => (typeof x === 'string' ? x : '');
const CAMPURI_PLIC = ['v', 'kind', 'statement', 'statementHash', 'signature'];
const CAMPURI_CAP = ['v', 'kind', 'agentId', 'session', 'policyHash', 'seq', 'hash', 'createdAt', 'previous'];
const CAMPURI_SEMNATURA = ['scheme', 'publicKey', 'signature'];
// forma capului de jurnal: VOCABULARUL felului din proof-kinds, scris o singura data acolo (revizia B3, constatarea 8)
const FEL_PUNCT = SCHEME['agent-witness-checkpoint'];
export const eWitnessId = (x) => typeof x === 'string' && WITNESS_ID.test(x);
/** Regulile pe care le numeste un refuz al martorului sau al verificarii unei chitante (campul `rule`). */
export const REGULI = ['forma', 'continut', 'sesiune', 'integritate', 'cheie', 'identitate', 'semnatura', 'timp', 'echivocare', 'vechi', 'succesor', 'plin',
'chitanta-forma', 'punct-forma', 'punct-integritate', 'punct-semnatura', 'martor', 'frunza', 'includere', 'primire', 'promisiune', 'registru', 'lant', 'intarziere',
'pagina-forma', 'pagina-integritate', 'pagina-vechi'];
const refuz = (rule, error, extra = {}) => { if (!REGULI.includes(rule)) throw new Error('agent-witness: undeclared rule ' + rule); return { ok: false, rule, error, ...extra }; };
/** id-ul martorului, derivat din cheia lui publica ML-DSA-65, ca agentId */
export const witnessIdFromKey = (pub) => 'aere-witness:' + crypto.createHash('sha256').update(pub.export({ type: 'spki', format: 'der' })).digest('hex').slice(0, 40);
const semneaza = (text, privateKey) => crypto.sign(null, Buffer.from(text, 'utf8'), privateKey).toString('base64');
const cheiePublica = (pem) => { if (typeof pem !== 'string') return null; try { return crypto.createPublicKey(pem); } catch { return null; } };
const verificaSemnaturaText = (text, pub, sig) => { try { return crypto.verify(null, Buffer.from(text, 'utf8'), pub, Buffer.from(String(sig), 'base64')); } catch { return false; } };
// ------------------------------------------------------------------------------------------------ capul trimis (partea agentului)
/**
* Cererea catre martor: capul registrului la `seq` (implicit ultima intrare), ca plic AIP-23 `aere-agent-ledger-head-attestation` SEMNAT cu
* cheia agentului (forma semnaturii AIP-23: ml-dsa-65 peste textul canonic al declaratiei). `previous` = capul pe care il continua: null
* pentru primul cap trimis, un seq (hash-ul se ia din registru) sau {seq, hash} (de la martor; trebuie sa fie in acest registru).
*/
export function cerereMartor(ledger, identity, { seq = null, previous = null, createdAt = new Date().toISOString() } = {}) {
if (!ledger || !Array.isArray(ledger.entries) || !ledger.entries.length) throw new Error('agent-witness: a ledger with at least one entry is required');
if (!identity || !identity.privateKey || identity.agentId !== ledger.agentId) throw new Error('agent-witness: the identity is not the agent of this ledger');
const s = seq == null ? ledger.entries.length - 1 : Number(seq);
const e = ledger.entries[s];
if (!Number.isInteger(s) || !e || e.seq !== s || !HASH_INTRARE.test(String(e.hash))) throw new Error(`agent-witness: the ledger has no entry ${seq}`);
let prev = null;
if (previous != null) {
const ps = typeof previous === 'object' ? Number(previous.seq) : Number(previous);
const pe = ledger.entries[ps];
if (!Number.isInteger(ps) || ps >= s || !pe) throw new Error(`agent-witness: the previous head ${ps} is not an entry before ${s}`);
if (typeof previous === 'object' && previous.hash !== pe.hash) throw new Error(`agent-witness: the previous head the witness holds (seq ${ps}) is not in this ledger: this ledger is another branch`);
prev = { seq: ps, hash: pe.hash };
}
if (!ISO.test(String(createdAt))) throw new Error('agent-witness: createdAt must be an ISO time in UTC (YYYY-MM-DDTHH:MM:SS[.mmm]Z)');
const statement = { v: 1, kind: HEAD_KIND, agentId: ledger.agentId, session: ledger.session, policyHash: String(ledger.policyHash).toLowerCase(), seq: s, hash: e.hash, createdAt, previous: prev };
const text = JSON.stringify(statement);
return { v: 1, kind: HEAD_KIND + '-attestation', statement, statementHash: sha(text),
signature: { scheme: ALG, publicKey: identity.publicKey.export({ type: 'spki', format: 'pem' }), signature: semneaza(text, identity.privateKey) } };
}
/**
* Judecata unei cereri, aceeasi la martor (la primire, cu `acumS` = ceasul lui) si la verificator (fara ceas): forma EXACTA (orice camp in
* plus e refuzat: martorul primeste un cap si semnatura lui, nimic din registru), sesiunea derivata, integritatea, cheia ML-DSA-65 din care
* se deriva agentId, semnatura, si momentul declarat la cel mult `toleranta` de ceasul martorului.
*/
export function verificaCapSemnat(env, { acumS = null, toleranta = TOLERANTA_S } = {}) {
if (!(obiect(env) && obiect(env.statement) /*PAZA-MARTOR:forma*/)) return refuz('forma', 'not an envelope {v, kind, statement, statementHash, signature}');
const inPlus = [...Object.keys(env).filter((k) => !CAMPURI_PLIC.includes(k)), ...Object.keys(env.statement).filter((k) => !CAMPURI_CAP.includes(k)).map((k) => 'statement.' + k),
...(obiect(env.signature) ? Object.keys(env.signature).filter((k) => !CAMPURI_SEMNATURA.includes(k)).map((k) => 'signature.' + k) : []),
...(obiect(env.statement.previous) ? Object.keys(env.statement.previous).filter((k) => !['seq', 'hash'].includes(k)).map((k) => 'statement.previous.' + k) : [])];
if (!(inPlus.length === 0 /*PAZA-MARTOR:continut*/)) return refuz('continut', `the witness accepts a ledger head and its signature, never the ledger's content: field(s) ${inPlus.slice(0, 5).join(', ')} refused`);
const st = env.statement;
const lipsa = [...CAMPURI_PLIC.filter((k) => !(k in env)), ...CAMPURI_CAP.filter((k) => !(k in st)).map((k) => 'statement.' + k)];
if (lipsa.length) return refuz('forma', `missing field(s): ${lipsa.join(', ')}`);
const pv = st.previous;
const formaBuna = env.v === 1 && env.kind === HEAD_KIND + '-attestation' && st.v === 1 && st.kind === HEAD_KIND && AGENT_ID.test(sir(st.agentId))
&& SESIUNE.test(sir(st.session)) && H32.test(sir(st.policyHash)) && Number.isSafeInteger(st.seq) && st.seq >= 0 && HASH_INTRARE.test(sir(st.hash))
&& typeof st.createdAt === 'string' && ISO.test(st.createdAt) && !Number.isNaN(Date.parse(st.createdAt))
&& (pv === null || (obiect(pv) && Number.isSafeInteger(pv.seq) && pv.seq >= 0 && pv.seq < st.seq && HASH_INTRARE.test(sir(pv.hash))))
&& H32.test(sir(env.statementHash)) && obiect(env.signature) && env.signature.scheme === ALG && typeof env.signature.publicKey === 'string' && typeof env.signature.signature === 'string';
if (!(formaBuna /*PAZA-MARTOR:forma*/)) return refuz('forma', 'the head is not well formed (agentId, session, policyHash, seq, hash of 64 hex, createdAt in UTC, previous null or {seq < seq, hash}, an ml-dsa-65 signature)');
if (!(st.session === sessionId(st.agentId, st.policyHash) /*PAZA-MARTOR:sesiune*/)) return refuz('sesiune', 'the session is not the one derived from the agent and the policy');
const text = JSON.stringify(st);
if (!(sha(text) === env.statementHash /*PAZA-MARTOR:integritate*/)) return refuz('integritate', 'sha256(statement) is not the statementHash (modified head)');
const pub = cheiePublica(env.signature.publicKey);
if (!(!!pub && pub.asymmetricKeyType === ALG /*PAZA-MARTOR:cheie*/)) return refuz('cheie', `the key is ${pub ? pub.asymmetricKeyType : 'unreadable'}, not ML-DSA-65: an agent identity is an ML-DSA-65 key`);
if (!(agentIdFromKey(pub) === st.agentId /*PAZA-MARTOR:identitate*/)) return refuz('identitate', 'the agentId is not derived from the key that signs (another agent\'s id)');
if (!(verificaSemnaturaText(text, pub, env.signature.signature) /*PAZA-MARTOR:semnatura*/)) return refuz('semnatura', 'the signature does not verify over the canonical statement text');
if (acumS != null) {
const t = Date.parse(st.createdAt) / 1000;
if (!(Math.abs(t - Number(acumS)) <= toleranta /*PAZA-MARTOR:timp*/)) return refuz('timp', `the head says ${st.createdAt}, more than ${toleranta} s away from the witness clock (${new Date(Number(acumS) * 1000).toISOString()})`);
}
return { ok: true, head: st, statementHash: env.statementHash, pub };
}
// ------------------------------------------------------------------------------------------------ echivocarea (numai capete)
// ce afirma un cap semnat despre pozitia `seq`: hash-ul lui, daca e chiar acolo, sau `previous.hash`, daca il numeste ca predecesor
const afirmatia = (st, seq) => (st.seq === seq ? st.hash : st.previous && st.previous.seq === seq ? st.previous.hash : undefined);
export const dovadaEchivocare = (a, b, seq) => ({ v: 1, kind: EQUIVOCATION_KIND, agentId: a.statement.agentId, session: a.statement.session, seq, a, b });
/** id-ul unei dovezi: sha256 peste perechea de statementHash-uri (aceeasi pereche, acelasi id) */
export const idEchivocare = (p) => sha(JSON.stringify([p.a.statementHash, p.b.statementHash]));
/** Doua capete semnate de aceeasi cheie de agent (ML-DSA-65), in aceeasi sesiune, care dau doua hash-uri diferite aceleiasi pozitii. */
export function verificaEchivocare(p) {
if (!obiect(p) || p.kind !== EQUIVOCATION_KIND || p.v !== 1 || !Number.isSafeInteger(p.seq)) return refuz('echivocare', `not an ${EQUIVOCATION_KIND} v1`);
for (const x of [p.a, p.b]) { const v = verificaCapSemnat(x); if (!v.ok) return refuz(v.rule, 'a head of the proof does not verify: ' + v.error); }
const A = p.a.statement, B = p.b.statement;
if (A.agentId !== p.agentId || B.agentId !== p.agentId || A.session !== p.session || B.session !== p.session) return refuz('echivocare', 'the two heads are not of the same agent and session');
const ha = afirmatia(A, p.seq), hb = afirmatia(B, p.seq);
if (ha === undefined || hb === undefined) return refuz('echivocare', `one of the heads says nothing about position ${p.seq}`);
if (ha === hb) return refuz('echivocare', `the two heads agree on position ${p.seq}`);
return { ok: true, agentId: p.agentId, session: p.session, seq: p.seq };
}
// ------------------------------------------------------------------------------------------------ frunza, promisiunea, capul de jurnal
const DOMENIU_FRUNZA = Buffer.from('aere-agent-witness-leaf/1\u0000', 'utf8');
/** datele frunzei: domeniul, cei 32 de octeti ai statementHash-ului capului primit, momentul primirii (ms, u64 big-endian) */
export function dateFrunza(headStatementHash, receivedAtMs) {
if (!H32.test(String(headStatementHash)) || !Number.isSafeInteger(receivedAtMs) || receivedAtMs < 0) throw new Error('agent-witness: a leaf needs a 32-byte head hash and a receive time in ms');
const t = Buffer.alloc(8); t.writeBigUInt64BE(BigInt(receivedAtMs));
return Buffer.concat([DOMENIU_FRUNZA, Buffer.from(headStatementHash.slice(2), 'hex'), t]);
}
export const frunzaMartor = (h, r) => leafHash(dateFrunza(h, r));
// ------------------------------------------------------------------------------------------------ arborele jurnalului, INCREMENTAL
// ARBORELE INCREMENTAL (revizia B3 a doua, R2-06): `merkleTree` din aere-verify-layer tine o memorie pe INTERVAL care nu se goleste, iar radacina
// fiecarei marimi noi adauga in ea ~log2(n) intervale. La deschidere martorul refacea radacina FIECARUI cap de jurnal semnat, deci o stare cu un cap
// de jurnal pe frunza (trafic mic: cel mult un cap pe tur) tinea ~5 KB pe cap de jurnal in plus (masurat de verificator: 372 MB la deschiderea a
// doua cioburi pline, fata de 172-183 MB cu un cap de jurnal la 1.000), si in functionare ciobul inghetat aduna la fel, cu fiecare chitanta
// servita. Aici se tin numai blocurile PERFECTE aliniate (2^h frunze de la un multiplu de 2^h), 32 de octeti fiecare intr-un Buffer pe nivel: cel
// mult 2n hash-uri (~1,3 MB la 20.000 de frunze), adaugate o data cu frunza; radacina oricarei marimi si orice dovada de includere sau de consistenta
// se calculeaza din ele, in O(log^2 n), FARA memorie. Calculul e cel din RFC 9162 (2.1.1, 2.1.3, 2.1.4), ca in merkleTree; proba compara cele
// doua pe toate marimile pana la 70 si pe toate perechile de marimi pana la 40, iar dovezile le verifica tot verificatorul comun (verifyInclusion,
// verifyConsistency).
const NOD_PREFIX = Buffer.from([0x01]);
const nodArbore = (a, b) => crypto.createHash('sha256').update(NOD_PREFIX).update(a).update(b).digest();
const RADACINA_GOALA = crypto.createHash('sha256').update(Buffer.alloc(0)).digest();
const sub2 = (n) => { let k = 1; while (k * 2 < n) k *= 2; return k; }; // cea mai mare putere a lui 2 strict mai mica decat n (n >= 2)
const hx32 = (b) => '0x' + b.toString('hex');
export function arboreIncremental() {
const niv = []; // niv[h] = { b: Buffer, n }: blocurile perfecte de 2^h frunze, al j-lea la 32j
const pune = (h, hash) => {
let L = niv[h]; if (!L) L = niv[h] = { b: Buffer.alloc(32 * 64), n: 0 };
if ((L.n + 1) * 32 > L.b.length) { const nb = Buffer.alloc(L.b.length * 2); L.b.copy(nb, 0, 0, L.n * 32); L.b = nb; }
hash.copy(L.b, L.n * 32); return ++L.n;
};
const bloc = (h, j) => niv[h].b.subarray(32 * j, 32 * j + 32);
const marime = () => (niv[0] ? niv[0].n : 0);
// MTH(D[a:b]) din RFC 9162, 2.1.1: un bloc perfect aliniat se citeste, altfel se imparte la cea mai mare putere a lui 2 mai mica decat marimea
const mth = (a, b) => {
const n = b - a; if (n === 0) return RADACINA_GOALA;
if ((n & (n - 1)) === 0 && a % n === 0) return bloc(31 - Math.clz32(n), a / n);
const k = sub2(n); return nodArbore(mth(a, a + k), mth(a + k, b));
};
const drum = (m, a, b) => { if (b - a <= 1) return []; const k = sub2(b - a); return m < k ? [...drum(m, a, a + k), mth(a + k, b)] : [...drum(m - k, a + k, b), mth(a, a + k)]; };
const sub = (m, a, b, compl) => {
const n = b - a; if (m === n) return compl ? [] : [mth(a, b)];
const k = sub2(n); return m <= k ? [...sub(m, a, a + k, compl), mth(a + k, b)] : [...sub(m - k, a + k, b, false), mth(a, a + k)];
};
const root = (n = marime()) => { if (!Number.isInteger(n) || n < 0 || n > marime()) throw new Error(`merkle: tree size ${n} outside 0..${marime()}`); return hx32(mth(0, n)); };
return {
get size() { return marime(); },
/** o frunza noua (hash-ul ei de frunza, 0x + 64 hex); blocurile pe care le completeaza se calculeaza acum, o singura data */
adauga(frunzaHex) {
if (typeof frunzaHex !== 'string' || !H32.test(frunzaHex)) throw new Error('merkle: a leaf hash is 0x + 64 hex');
let h = 0; let n = pune(0, Buffer.from(frunzaHex.slice(2), 'hex'));
while (n % 2 === 0) { const p = nodArbore(bloc(h, n - 2), bloc(h, n - 1)); h++; n = pune(h, p); }
},
root,
inclusionProof(index, treeSize = marime()) {
if (!Number.isInteger(treeSize) || treeSize < 1 || treeSize > marime()) throw new Error(`merkle: tree size ${treeSize} outside 1..${marime()}`);
if (!Number.isInteger(index) || index < 0 || index >= treeSize) throw new Error(`merkle: index ${index} outside the tree of ${treeSize}`);
return { index, treeSize, leafHash: hx32(bloc(0, index)), path: drum(index, 0, treeSize).map(hx32), rootHash: root(treeSize) };
},
consistencyProof(firstSize, secondSize = marime()) {
if (!Number.isInteger(secondSize) || secondSize < 1 || secondSize > marime()) throw new Error(`merkle: tree size ${secondSize} outside 1..${marime()}`);
if (!Number.isInteger(firstSize) || firstSize < 1 || firstSize > secondSize) throw new Error(`merkle: first size ${firstSize} outside 1..${secondSize}`);
return { firstSize, secondSize, firstRoot: root(firstSize), secondRoot: root(secondSize), path: sub(firstSize, 0, secondSize, true).map(hx32) };
},
};
}
function plicSemnat(env, cheie) { const text = JSON.stringify(env.statement); return { ...env, signature: { scheme: ALG, publicKey: cheie.publicKeyPem, signature: semneaza(text, cheie.privateKey) } }; }
/** promisiunea de la primire: plic AIP-23 `time` (capul, ca digest, a existat la momentul primirii, dupa martor), semnat de martor */
export function promisiune(cheie, headStatementHash, receivedAtMs) {
const at = new Date(receivedAtMs).toISOString();
return plicSemnat(buildProof('time', { subjectHash: headStatementHash, source: cheie.witnessId, at, createdAt: at }), cheie);
}
/** capul de jurnal: plic AIP-23 `agent-witness-checkpoint`, semnat de martor (constructorul felului ii judeca vocabularul) */
export function capDeJurnal(cheie, { treeSize, root, previous = null, log, chainId, maxMergeDelay, createdAt = new Date().toISOString() }) {
return plicSemnat(buildProof('agent-witness-checkpoint', { root, previous: previous || undefined, log, witness: cheie.witnessId, chainId, treeSize, maxMergeDelay, createdAt }), cheie);
}
function verificaSemnaturaMartor(env, witnessId) {
const s = env && env.signature;
if (!obiect(s) || s.scheme !== ALG || typeof s.publicKey !== 'string' || typeof s.signature !== 'string') return refuz('punct-semnatura', 'the envelope carries no ml-dsa-65 signature of the witness');
const pub = cheiePublica(s.publicKey);
if (!(!!pub && pub.asymmetricKeyType === ALG && witnessIdFromKey(pub) === witnessId /*PAZA-MARTOR:martor*/)) return refuz('martor', `the signing key is not the ML-DSA-65 key of ${witnessId}`);
if (!(verificaSemnaturaText(JSON.stringify(env.statement), pub, s.signature) /*PAZA-MARTOR:punct-semnatura*/)) return refuz('punct-semnatura', 'the witness signature does not verify over the canonical statement text');
return { ok: true };
}
/** forma unei declaratii de cap de jurnal: felul, versiunea, campurile obligatorii si VOCABULARUL lor, din proof-kinds (nicio a doua copie) */
export const formaPunctului = (st) => obiect(st) && st.kind === CHECKPOINT_KIND && st.v === 1 && FEL_PUNCT.req.every((c) => st[c] !== undefined && st[c] !== null)
&& Object.entries(FEL_PUNCT.valori).every(([c, [bun]]) => st[c] === undefined || bun(st[c]));
/** un cap de jurnal intreg: forma, integritatea, semnatura martorului pe care il numeste */
export function verificaPunct(env) {
const st = env && env.statement;
if (!(obiect(env) && env.kind === CHECKPOINT_KIND + '-attestation' && formaPunctului(st) /*PAZA-MARTOR:punct-forma*/)) return refuz('punct-forma', `not an ${CHECKPOINT_KIND} attestation with a size, a root, a log name, a witness id, a chain and a merge delay (the vocabulary of the kind in proof-kinds)`);
if (!(H32.test(sir(env.statementHash)) && sha(JSON.stringify(st)) === env.statementHash /*PAZA-MARTOR:punct-integritate*/)) return refuz('punct-integritate', 'sha256(statement) is not the statementHash of the checkpoint (modified checkpoint)');
return verificaSemnaturaMartor(env, st.witness);
}
// ------------------------------------------------------------------------------------------------ chitanta (verificarea fara martor)
/** o chitanta, sau raspunsul GET al martorului care o poarta in `receipt` */
export const esteChitanta = (x) => obiect(x) && (obiect(x.witnessReceipt) || (obiect(x.receipt) && obiect(x.receipt.witnessReceipt)));
export const chitantaDin = (x) => (obiect(x) && obiect(x.receipt) && obiect(x.receipt.witnessReceipt) ? x.receipt : x);
const listaMartori = (martori) => {
if (martori == null) return null;
if (!Array.isArray(martori) || !martori.length || !martori.every(eWitnessId)) throw new Error('agent-witness: pinned witnesses must be a non-empty list of aere-witness:<40 hex>');
return martori;
};
/**
* Verificarea LOCALA a unei chitante (nimic din retea): capul de jurnal semnat de martor, capul primit semnat de agent, frunza refacuta
* din capul primit si momentul primirii, dovada de includere pana la radacina capului de jurnal, regula de primire a martorului, cu
* `ledger` ca capul (si predecesorul lui) sunt intrari ale ACESTUI registru, si cu `martori` ca martorul e unul FIXAT de verificator.
* Momentul certificat il da `ancoreDinChitante` (lantul).
*/
export function verificaChitanta(plic, { ledger = null, toleranta = TOLERANTA_S, martori = null } = {}) {
const fixati = listaMartori(martori);
const w = obiect(plic) ? plic.witnessReceipt : null;
// (pana la a cincea revizie, R5-04, un receivedAtMs negativ trecea de aici si arunca mai jos, in dateFrunza; un leafHash {"toString": 0} arunca aici)
if (!(obiect(w) && w.format === RECEIPT_FORMAT && obiect(w.head) && Number.isSafeInteger(w.receivedAtMs) && w.receivedAtMs >= 0 /*PAZA-MARTOR:chitanta-tipuri*/ && Number.isSafeInteger(w.leafIndex) && w.leafIndex >= 0
&& Number.isSafeInteger(w.treeSize) && w.treeSize >= 1 && H32.test(sir(w.leafHash)) && Array.isArray(w.path) && w.path.every((x) => H32.test(sir(x))) && (w.promise === undefined || obiect(w.promise))
&& Array.isArray(w.path) /*PAZA-MARTOR:chitanta-forma*/)) return refuz('chitanta-forma', `not a receipt of the agents witness (${RECEIPT_FORMAT}: head, receivedAtMs, leafIndex, treeSize, leafHash, path)`);
const p = verificaPunct(plic);
if (!p.ok) return p;
const st = plic.statement;
// martorul fixat de verificator (revizia B3, constatarile 1 si 17): fara fixare orice cheie ML-DSA-65 e un martor valid al ei insasi
if (!(!fixati || fixati.includes(st.witness) /*PAZA-MARTOR:martor*/)) return refuz('martor', `the receipt is signed by ${st.witness}, not by a witness the verifier pinned (${fixati.join(', ')})`);
const c = verificaCapSemnat(w.head, { toleranta });
if (!c.ok) return { ...c, error: 'the head in the receipt: ' + c.error };
if (!(frunzaMartor(w.head.statementHash, w.receivedAtMs) === w.leafHash /*PAZA-MARTOR:frunza*/)) return refuz('frunza', 'the leaf is not the leaf of this head received at this time');
if (!(w.treeSize === st.treeSize && verifyInclusion({ leafHash: w.leafHash, index: w.leafIndex, treeSize: w.treeSize, path: w.path, rootHash: st.root }) /*PAZA-MARTOR:includere*/)) return refuz('includere', 'the inclusion proof does not lead from the leaf to the root of this checkpoint (RFC 9162, 2.1.3)');
const C = Date.parse(c.head.createdAt) / 1000, R = w.receivedAtMs / 1000;
if (!(Math.abs(C - R) <= toleranta /*PAZA-MARTOR:primire*/)) return refuz('primire', `the witness logged a head claimed at ${c.head.createdAt} as received ${Math.round(R - C)} s later: outside its own ${toleranta} s rule`);
if (w.promise !== undefined) {
const pr = w.promise; const ps = obiect(pr) ? pr.statement : null;
if (!(obiect(ps) && pr.kind === PROMISE_KIND + '-attestation' && ps.kind === PROMISE_KIND && ps.subjectHash === w.head.statementHash && ps.source === st.witness
&& typeof ps.at === 'string' && Date.parse(ps.at) === w.receivedAtMs && sha(JSON.stringify(ps)) === pr.statementHash && verificaSemnaturaMartor(pr, st.witness).ok /*PAZA-MARTOR:promisiune*/)) return refuz('promisiune', 'the promise is not the witness-signed time of this head at this receive time');
}
if (ledger) {
const h = c.head; const e = Array.isArray(ledger.entries) ? ledger.entries[h.seq] : null;
const pe = h.previous && Array.isArray(ledger.entries) ? ledger.entries[h.previous.seq] : null;
if (!(h.agentId === ledger.agentId && h.session === ledger.session && h.policyHash === sir(ledger.policyHash).toLowerCase() && !!e && e.hash === h.hash
&& (!h.previous || (!!pe && pe.hash === h.previous.hash)) /*PAZA-MARTOR:registru*/)) return refuz('registru', `the witnessed head (seq ${h.seq}${h.previous ? ', previous ' + h.previous.seq : ''}) is not in this ledger: another agent, another policy or another branch`);
}
return { ok: true, head: c.head, checkpoint: { treeSize: st.treeSize, root: st.root, statementHash: plic.statementHash, witness: st.witness, chainId: st.chainId, maxMergeDelay: st.maxMergeDelay, log: st.log },
witnessPinned: !!fixati, receivedAt: R, createdAt: C };
}
/**
* Ancorele registrului din chitantele martorului, pentru verifyLedger: fiecare chitanta verificata local (verificaChitanta, cu registrul si cu
* martorii fixati), apoi capul ei de jurnal judecat de verificatorul AIP-23 de referinta (`ruleaza`, implicit verify-proof.mjs): semnatura
* martorului PASSED si finalitate post-cuantica PASSED, de unde momentul F (firstSeen certificat). Se cere: primirea nu dupa F (+ toleranta), si
* F - primire sub intarzierea maxima a martorului si a verificatorului (`mergeMax`). Ancora: {seq, hash, at: createdAt}, cu martorul numit
* (`witness`), daca a fost fixat (`witnessPinned`) si marginea pe care o da lantul singur (`chainLowerBound` = F - intarzierea - 2 x toleranta).
* `chain` (lantul cerut) si `mergeMax` sunt numere intregi pozitive, altfel refuz (o valoare gresita nu are voie sa scoata paza). `verdicte` (o Map,
* optionala) tine verdictul verificatorului AIP-23 pe cap de jurnal, intre chemari. Intoarce {stare, anchors, randuri}.
*/
export async function ancoreDinChitante(ledger, plicuri, { verifyProof, rpc, chain = null, ruleaza = ruleazaVerificatorul, mergeMax = MERGE_MAX_S, toleranta = TOLERANTA_S, tmpDir = null, martori = null, verdicte = null } = {}) {
if (!(chain == null || (Number.isSafeInteger(chain) && chain > 0))) throw new Error(`agent-witness: the chain asked must be a positive integer, not ${chain}`);
if (!(Number.isSafeInteger(Number(mergeMax)) && Number(mergeMax) > 0)) throw new Error(`agent-witness: the maximum merge delay must be a positive integer of seconds, not ${mergeMax}`);
const fixati = listaMartori(martori);
const anchors = [], randuri = []; let stare = 'OK';
const cade = (rand) => { randuri.push({ ...rand, pass: false }); stare = 'FAILED'; };
const nem = (rand) => { randuri.push({ ...rand, pass: null }); if (stare === 'OK') stare = 'NEMASURAT'; };
const dir = fs.mkdtempSync(path.join(tmpDir || os.tmpdir(), 'aere-martor-agenti-'));
try {
for (const [i, x] of plicuri.entries()) {
const plic = chitantaDin(x);
const l = verificaChitanta(plic, { ledger, toleranta, martori: fixati });
if (!l.ok) { cade({ receipt: i, rule: l.rule, detail: l.error }); continue; }
const w = l.checkpoint.witness;
const n = plic.notarization;
if (!obiect(n)) { nem({ receipt: i, seq: l.head.seq, witness: w, detail: 'the checkpoint of this receipt is not notarized yet: ask the witness again later' }); continue; }
const lantNotar = Number.isSafeInteger(n.chainId) ? n.chainId : NaN;
const lant = chain || (Number.isSafeInteger(lantNotar) ? lantNotar : null);
if (!(lantNotar === plic.statement.chainId && (!chain || chain === plic.statement.chainId) /*PAZA-MARTOR:lant*/)) { cade({ receipt: i, seq: l.head.seq, witness: w, rule: 'lant', detail: `the checkpoint names chain ${plic.statement.chainId}, its notarization ${Number.isSafeInteger(n.chainId) ? n.chainId : typeof n.chainId}${chain ? ', the verifier asks ' + chain : ''}` }); continue; }
// un cap de jurnal (plicul si notarizarea lui) se judeca O DATA, oricate chitante il poarta (`verdicte`, impartit intre chemari: sub
// --witness-url verificatorul aduce chitanta fiecarui cap tinut, iar capetele aceluiasi tur impart capul de jurnal)
const cheieV = `${lant}|${plic.statementHash}|${JSON.stringify(n)}`;
let v = verdicte && verdicte.has(cheieV) ? verdicte.get(cheieV) : undefined;
if (v === undefined) { const f = path.join(dir, `${i}.json`); fs.writeFileSync(f, JSON.stringify(plic)); v = await ruleaza(verifyProof, f, { rpc, chain: lant }); if (verdicte) verdicte.set(cheieV, v); }
if (!v) { nem({ receipt: i, seq: l.head.seq, witness: w, detail: 'the AIP-23 verifier did not answer' }); continue; }
if (!(String(v.statementHash || '').toLowerCase() === plic.statementHash /*PAZA-MARTOR:lant*/)) { cade({ receipt: i, seq: l.head.seq, witness: w, rule: 'lant', detail: 'the AIP-23 verifier judged another statementHash' }); continue; }
const sg = Array.isArray(v.levels) ? v.levels.find((x2) => x2.level === 'signature') : null;
if (!(!!sg && sg.state === 'PASSED' /*PAZA-MARTOR:lant*/)) { (sg && sg.state === 'FAILED' ? cade : nem)({ receipt: i, seq: l.head.seq, witness: w, rule: 'lant', detail: `the AIP-23 verifier gives the witness signature ${sg ? sg.state : 'no level'}` }); continue; }
const fs2 = firstSeenFromVerdict(v);
if (fs2.stare === 'FAILED') { cade({ receipt: i, seq: l.head.seq, witness: w, rule: 'lant', detail: fs2.motiv }); continue; }
if (!(fs2.stare === 'PASSED' /*PAZA-MARTOR:lant*/)) { nem({ receipt: i, seq: l.head.seq, witness: w, detail: fs2.motiv }); continue; }
const F = fs2.at;
if (!(l.receivedAt <= F + toleranta /*PAZA-MARTOR:primire*/)) { cade({ receipt: i, seq: l.head.seq, witness: w, rule: 'primire', detail: `the witness says it received the head at ${new Date(l.receivedAt * 1000).toISOString()}, after the chain first saw the checkpoint that covers it (${new Date(F * 1000).toISOString()})` }); continue; }
const mmd = Math.min(Number(mergeMax), l.checkpoint.maxMergeDelay);
if (!(F - l.receivedAt <= mmd + toleranta /*PAZA-MARTOR:intarziere*/)) { cade({ receipt: i, seq: l.head.seq, witness: w, rule: 'intarziere', detail: `the checkpoint was first seen ${Math.round(F - l.receivedAt)} s after the head was received, more than the maximum merge delay ${mmd} s: the chain cannot vouch for the time the head claims` }); continue; }
const marginaLant = Math.floor(F - mmd - 2 * toleranta);
anchors.push({ seq: l.head.seq, hash: l.head.hash, at: Math.floor(l.createdAt), firstSeen: F, receivedAt: l.receivedAt, checkpoint: plic.statementHash, treeSize: l.checkpoint.treeSize,
witness: w, witnessPinned: !!fixati, chainLowerBound: marginaLant, chainId: lant, chainAsked: !!chain,
by: fixati ? 'agents witness receipt of a pinned witness: the head the agent signed at `at`, received by the witness within 300 s of its clock, in a checkpoint first seen on chain under a post-quantum certified anchor'
: 'agents witness receipt of an UNPINNED witness (any ML-DSA-65 key can witness itself): the head the agent signed at `at`; the chain alone bounds `at` to no earlier than `chainLowerBound`' });
randuri.push({ receipt: i, seq: l.head.seq, witness: w, pass: true, detail: `seq ${l.head.seq} claimed at ${new Date(l.createdAt * 1000).toISOString()}, received ${Math.round(l.receivedAt - l.createdAt)} s later by ${w}${fixati ? ' (pinned)' : ` (NOT pinned: the chain alone bounds the claim to no earlier than ${new Date(marginaLant * 1000).toISOString()}; pass --witness to pin it)`}, checkpoint of size ${l.checkpoint.treeSize} first seen on chain ${lant}${chain ? '' : ' (the chain the receipt declares; pass --chain to require one)'} at ${new Date(F * 1000).toISOString()}` });
}
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
return { stare, anchors: anchors.sort((a, b) => a.seq - b.seq), randuri };
}
/**
* Registrul aratat contine TOATE capetele pe care martorul le tine pentru agent si politica? (revizia B3, constatarea 2) Chitantele le alege
* agentul; o ramura despartita DUPA ultimul cap pentru care da o chitanta trece de ele. `raspuns` = lista intreaga a martorului
* ({agentId, session, heads: [{seq, hash, id}], equivocations: [...]}, adunata din paginile GET .../heads de `citesteCapeteMartorului`).
* FAILED "registru" daca un cap martorizat nu e intrare a registrului, FAILED "echivocare" daca martorul tine o dovada pentru sesiune.
*/
export function verificaCapeteMartorului(ledger, raspuns) {
const randuri = []; let stare = 'OK';
if (!(obiect(raspuns) && Array.isArray(raspuns.heads) && raspuns.agentId === ledger.agentId && raspuns.session === ledger.session)) return { stare: 'NEMASURAT', randuri: [{ pass: null, detail: 'the answer of the witness is not the list of heads of this agent and policy' }] };
// un element care nu e {seq, hash} e un raspuns pe care nu il putem judeca (NEMASURAT), nu o ramura: pana la a doua revizie (R2-04) un `null`
// arunca o exceptie iesita cu 1, iar un `5` sau un `{}` dadea "FAIL [registru] ... seq undefined"
const rau = raspuns.heads.findIndex((h) => !(obiect(h) && Number.isSafeInteger(h.seq) && h.seq >= 0));
if (!(rau < 0 /*PAZA-MARTOR:capete-forma*/)) return { stare: 'NEMASURAT', randuri: [{ pass: null, detail: `the witness listed an element ${rau} that is not a head {seq, hash}: an answer that cannot be judged` }] };
for (const h of raspuns.heads) {
const e = Array.isArray(ledger.entries) ? ledger.entries[h.seq] : null;
if (!(!!e && e.hash === h.hash /*PAZA-MARTOR:registru*/)) { randuri.push({ pass: false, rule: 'registru', seq: h.seq, detail: `the witness holds head seq ${h.seq} (${sir(h.hash).slice(0, 16)}...), which is not an entry of this ledger: this ledger is another branch` }); stare = 'FAILED'; }
}
const eq = Array.isArray(raspuns.equivocations) ? raspuns.equivocations : [];
if (!(eq.length === 0 /*PAZA-MARTOR:echivocare*/)) { randuri.push({ pass: false, rule: 'echivocare', detail: `the witness keeps ${eq.length} proof(s) that this agent signed two heads for one position under this policy (${eq.map((x) => sir(obiect(x) ? x.id : '')).join(', ')})` }); stare = 'FAILED'; }
if (stare === 'OK') randuri.push({ pass: true, detail: `all ${raspuns.heads.length} head(s) the witness holds for this agent and policy are entries of this ledger${raspuns.heads.length ? ' (the last: seq ' + raspuns.heads[raspuns.heads.length - 1].seq + ')' : ''}; no equivocation kept` });
return { stare, randuri };
}
/**
* O pagina SEMNATA a capetelor unui agent si unei politici (revizia B3 a doua, R2-01): plicul `aere-agent-witness-heads-attestation` pe care
* martorul il pune langa fiecare pagina din GET .../heads?session (`signedPage`). Se cere: forma exacta (fiecare cap {seq, hash, id}, in ordine,
* de la `from`; `next`, `count`, `latest`, dovezile pastrate, momentul), agentul, sesiunea si `from` ceruti, integritatea, semnatura ML-DSA-65 a
* martorului numit in pagina, si momentul paginii la cel mult `toleranta` de ceasul verificatorului (o pagina veche, reluata de un intermediar,
* nu e lista de acum). Intoarce {ok, statement} sau un refuz numit.
*/
export function verificaPaginaCapetelor(env, { agentId, session, from, acumS = Date.now() / 1000, toleranta = TOLERANTA_S }) {
const st = obiect(env) ? env.statement : null;
const capBun = (h, i, l) => obiect(h) && Object.keys(h).length === 3 && Number.isSafeInteger(h.seq) && h.seq >= from && HASH_INTRARE.test(sir(h.hash)) && H32.test(sir(h.id)) && (i === 0 || h.seq > l[i - 1].seq);
const formaBuna = obiect(env) && env.kind === PAGINA_KIND + '-attestation' && obiect(st) && st.v === 1 && st.kind === PAGINA_KIND && eWitnessId(st.witness)
&& st.agentId === agentId && st.session === session && st.from === from && sir(st.witness) === st.witness && Array.isArray(st.heads) && st.heads.length <= PAGINA_MAX && st.heads.every(capBun)
&& (st.next === null || (Number.isSafeInteger(st.next) && st.heads.length > 0 && st.next > st.heads[st.heads.length - 1].seq))
&& Number.isSafeInteger(st.count) && st.count >= st.heads.length && Array.isArray(st.equivocations) && st.equivocations.every((q) => obiect(q) && H32.test(sir(q.id)) && Number.isSafeInteger(q.seq))
&& (st.latest === null || (obiect(st.latest) && Number.isSafeInteger(st.latest.seq) && HASH_INTRARE.test(sir(st.latest.hash)))) && typeof st.at === 'string' && ISO.test(st.at);
if (!(formaBuna /*PAZA-MARTOR:pagina-forma*/)) return refuz('pagina-forma', `not a page of heads signed by a witness (${PAGINA_KIND}: the agent, the policy and the page asked, each head {seq, hash, id} in order, next, count, latest, the kept proofs, the time)`);
if (!(H32.test(sir(env.statementHash)) && sha(JSON.stringify(st)) === env.statementHash /*PAZA-MARTOR:pagina-integritate*/)) return refuz('pagina-integritate', 'sha256(statement) is not the statementHash of the page (a modified page)');
const s = verificaSemnaturaMartor(env, st.witness);
if (!(s.ok /*PAZA-MARTOR:pagina-semnatura*/)) return { ...s, error: 'the page of heads: ' + s.error };
const t = Date.parse(st.at) / 1000;
if (!(Math.abs(t - Number(acumS)) <= toleranta /*PAZA-MARTOR:pagina-vechi*/)) return refuz('pagina-vechi', `the page was signed at ${st.at}, more than ${toleranta} s from the verifier clock: a page of another time, not the list of now`);
return { ok: true, statement: st };
}
/**
* Lista intreaga a capetelor martorului de la `baza` pentru agent si sesiune, pagina cu pagina (`?from=&limit=`), din paginile SEMNATE de martor
* (verificaPaginaCapetelor): campurile nesemnate de langa ele, si /health, le poate scrie oricine sta la adresa aleasa (pana la a doua revizie,
* R2-01, id-ul martorului se lua nesemnat din /health). Paginile trebuie sa fie ale aceluiasi martor si sa se lege (`next` -> `from`), iar ultima
* sa dea chiar numarul si ultimul cap semnate. Intoarce {ok, witness, at, agentId, session, heads, equivocations} sau {ok: false, motiv}: un martor
* care nu raspunde nu e un zero.
*/
export async function citesteCapeteMartorului(baza, agentId, session, { cere = cereJson, paginiMax = 1000, acumS = () => Date.now() / 1000, toleranta = TOLERANTA_S } = {}) {
const U = String(baza).replace(/\/+$/, '');
const heads = []; let from = 0, witness = null;
for (let p = 0; p < paginiMax; p++) {
const r = await cere(`${U}/v1/agents/witness/${agentId}/heads?session=${session}&from=${from}&limit=${PAGINA_MAX}`);
if (r.status !== 200 || !obiect(r.j)) return { ok: false, motiv: `the witness did not list the heads (${r.status || r.eroare})` };
const v = verificaPaginaCapetelor(r.j.signedPage, { agentId, session, from, acumS: acumS(), toleranta });
if (!v.ok) return { ok: false, rule: v.rule, motiv: `the page of heads from seq ${from} is not a list signed by a witness now: [${v.rule}] ${v.error}` };
const st = v.statement;
if (witness && st.witness !== witness) return { ok: false, motiv: `the pages are signed by two witnesses (${witness}, ${st.witness})` };
witness = st.witness; heads.push(...st.heads);
if (st.next === null) {
const ultim = heads[heads.length - 1];
const aduna = heads.length === st.count && (ultim ? !!st.latest && st.latest.seq === ultim.seq && st.latest.hash === ultim.hash : st.latest === null);
if (!(aduna /*PAZA-MARTOR:pagina-numar*/)) return { ok: false, motiv: `the signed pages give ${heads.length} head(s), but the last page signs ${st.count} and another latest head: the list changed while it was read, ask again` };
return { ok: true, witness, at: st.at, agentId, session, heads, equivocations: st.equivocations };
}
from = st.next;
}
return { ok: false, motiv: `more than ${paginiMax} pages of heads` };
}
/**
* O cerere care, pe un 429 al martorului, asteapta cat spune Retry-After si reincearca (revizia B3 a cincea, R5-06): verificatorul aduce cate o
* chitanta pe cap tinut, iar serviciul da 1.200 de citiri pe client in 10 minute. `buget` ({ ramasS }) e comun tuturor cererilor unei verificari:
* un 429 fara Retry-After, sau cu o asteptare peste ce a ramas din buget, se intoarce asa cum e (cel care cheama il numeste NEMASURAT).
*/
export function cereRabdator(buget, { cere = cereJson, dormi = (ms) => new Promise((g) => setTimeout(g, ms)) } = {}) {
return async (url, o) => {
for (let i = 0; ; i++) {
const r = await cere(url, o);
if (!(r.status === 429 && Number.isSafeInteger(r.retryAfter) && r.retryAfter <= buget.ramasS && i < 50) /*PAZA-MARTOR:asteapta-429*/) return r;
buget.ramasS -= r.retryAfter; buget.asteptatS = (buget.asteptatS || 0) + r.retryAfter; await dormi(r.retryAfter * 1000 + 50);
}
};
}
/** Starea unui cap la martorul de la `baza` (GET /v1/agents/witness/<id>): {status, j} sau {status: 0, eroare}. Chitanta din ea se verifica toata. */
export const stareaCapuluiLa = (baza, id, { cere = cereJson } = {}) => cere(`${String(baza).replace(/\/+$/, '')}/v1/agents/witness/${id}`);
async function cereJson(url, o = {}) {
try { const r = await fetch(url, { ...o, signal: AbortSignal.timeout(30000), headers: { 'content-type': 'application/json', 'user-agent': 'aere-agent-witness/1', ...(o.headers || {}) } }); let j = null; try { j = await r.json(); } catch { j = null; }
const ra = r.headers.get('retry-after'); return { status: r.status, j, retryAfter: ra !== null && /^\d{1,6}$/.test(ra) ? Number(ra) : null }; }
catch (e) { return { status: 0, eroare: String(e.message || e).slice(0, 200) }; }
}
// ------------------------------------------------------------------------------------------------ jurnalul martorului (partea serviciului)
/** cheia martorului (ML-DSA-65, PKCS#8 PEM, 0600), nascuta la prima deschidere a starii; nu se tipareste niciodata */
export function cheiaMartorului(dir) {
const f = path.join(dir, 'witness.key.pem');
if (!fs.existsSync(f)) { const { privateKey } = crypto.generateKeyPairSync(ALG); fs.writeFileSync(f, privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600, flag: 'wx' }); }
const privateKey = crypto.createPrivateKey(fs.readFileSync(f, 'utf8'));
if (privateKey.asymmetricKeyType !== ALG) throw new Error('agent-witness: the witness key is not ML-DSA-65');
const publicKey = crypto.createPublicKey(privateKey);
return { privateKey, publicKey, publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }), witnessId: witnessIdFromKey(publicKey) };
}
// SCRIERILE DURABILE (revizia B3, constatarea 16): fs.writeSync poate scrie MAI PUTIN decat i se da (discul aproape plin) fara sa arunce, iar
// pana la reparatie nimeni nu citea ce intoarce: in memorie frunza exista, pe disc ramanea jumatate de rand, si frunza urmatoare se lipea de el.
// Acum se scrie pana la ultimul octet; la esec fisierul se taie inapoi la marimea de dinainte (iar daca nici asta nu merge, martorul nu mai
// primeste nimic pana la repornire, cand randul taiat e ultimul si se taie la deschidere).
function scrieTot(fd, buf) {
let off = 0;
while (off < buf.length) {
const n = fs.writeSync(fd, buf, off, buf.length - off);
if (!(Number.isSafeInteger(n) && n > 0)) throw Object.assign(new Error('agent-witness: a write wrote nothing'), { code: 'EIO' });
off += n;
}
}
const fsyncDosar = (d) => { if (process.platform === 'win32') return; let fd; try { fd = fs.openSync(d, 'r'); fs.fsyncSync(fd); } catch { /* sistemul nu permite: ramane fsync-ul fisierului */ } finally { if (fd !== undefined) fs.closeSync(fd); } };
let contorTmp = 0;
function scrieAtomic(f, text) {
const t = `${f}.tmp-${process.pid}-${++contorTmp}`; const fd = fs.openSync(t, 'w');
try { scrieTot(fd, Buffer.from(text, 'utf8')); fs.fsyncSync(fd); } catch (e) { fs.closeSync(fd); fs.rmSync(t, { force: true }); throw e; }
fs.closeSync(fd); fs.renameSync(t, f); fsyncDosar(path.dirname(f));
}
function adaugaDurabil(f, text) {
const buf = Buffer.from(text, 'utf8'); const fd = fs.openSync(f, 'a'); let inainte;
try { inainte = fs.fstatSync(fd).size; scrieTot(fd, buf); fs.fsyncSync(fd); }
catch (e) {
if (inainte !== undefined) {
// pe Windows un descriptor deschis cu 'a' nu are dreptul de a taia (SetEndOfFile); atunci se taie dupa cale
try { fs.ftruncateSync(fd, inainte); fs.fsyncSync(fd); } catch { try { fs.truncateSync(f, inainte); } catch { e.taiereEsuata = true; } }
}
throw e;
}
finally { fs.closeSync(fd); }
}
const pidViu = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } };
// LACATUL STARII (revizia B3 a doua, R2-10): pana la reparatie lacatul tinea numai pid-ul, iar dupa o oprire necurata (pana de curent, reset,
// panica) ramanea pe disc; daca dupa repornire pid-ul era al altui proces viu (zona deasa a pid-urilor de pornire; EPERM se numara "viu"), martorul
// refuza sa porneasca la fiecare incercare si unitatea ramanea `failed`. Acum lacatul isi poarta si pornirea MASINII (`boot_id` pe Linux; pe
// alte sisteme momentul pornirii din os.uptime, la cel mult 120 s) si momentul pornirii PROCESULUI (campul 22 din /proc/<pid>/stat, unde exista):
// un lacat scris inainte de ultima pornire a masinii, sau al carui pid e acum al altui proces, nu tine starea. Un lacat in forma veche (numai
// pid-ul) se judeca la fel ca inainte.
export function idPornire() {
try { const b = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim(); if (/^[0-9a-f-]{36}$/.test(b)) return 'linux:' + b; } catch { /* nu e Linux */ }
return 'up:' + Math.round(Date.now() / 1000 - os.uptime());
}
export function pornireProces(pid) {
try { const s = fs.readFileSync(`/proc/${pid}/stat`, 'utf8'); const f = s.slice(s.lastIndexOf(')') + 2).split(' '); return /^\d+$/.test(f[19] || '') ? f[19] : null; } catch { return null; }
}
const aceeasiPornire = (a, b) => (String(a).startsWith('up:') && String(b).startsWith('up:') ? Math.abs(Number(String(a).slice(3)) - Number(String(b).slice(3))) <= 120 : a === b);
/** Tine lacatul `text` starea? {tinut, pid, motiv}. `viu`, `pornire` si `boot` se pot inlocui (probele). */
export function lacatTinut(text, { propriu = process.pid, viu = pidViu, pornire = pornireProces, boot = idPornire() } = {}) {
let o = null; try { o = JSON.parse(String(text)); } catch { o = null; }
if (typeof o === 'number') o = { pid: o }; // forma veche: numai pid-ul
if (!obiect(o) || !Number.isSafeInteger(o.pid) || o.pid <= 0) return { tinut: false, pid: null, motiv: 'an unreadable lock' };
if (o.pid === propriu) return { tinut: false, pid: o.pid, motiv: 'the lock of this process' };
if (!viu(o.pid)) return { tinut: false, pid: o.pid, motiv: 'its process is gone' };
if (!(o.boot == null || aceeasiPornire(o.boot, boot) /*PAZA-MARTOR:lacat-pornire*/)) return { tinut: false, pid: o.pid, motiv: 'written before the last start of the machine' };
const acum = o.start == null ? null : pornire(o.pid);
if (!(acum == null || acum === String(o.start) /*PAZA-MARTOR:lacat-proces*/)) return { tinut: false, pid: o.pid, motiv: 'its pid now belongs to another process' };
return { tinut: true, pid: o.pid, motiv: 'held' };
}
const numeCiob = (log, k) => (k === 0 ? log : `${log}/${k}`);
// cautarea dupa seq intr-o lista de capete ordonata crescator dupa seq
function gaseste(lista, seq) { let a = 0, b = lista.length - 1; while (a <= b) { const m = (a + b) >> 1; const q = lista[m].q; if (q === seq) return lista[m]; if (q < seq) a = m + 1; else b = m - 1; } return null; }
function primulDeLa(lista, seq) { let a = 0, b = lista.length; while (a < b) { const m = (a + b) >> 1; if (lista[m].q < seq) a = m + 1; else b = m; } return a; }
/**
* Deschide (sau creeaza) starea martorului: `dir/martor.json` (jurnalul, lantul, martorul), cheia, `cioburi/<k>/` (frunzele `frunze.ndjson`,
* adaugate si sincronizate pe disc INAINTE de raspuns; capetele semnate primite `capete/`; capetele de jurnal si notarizarea lor `puncte/`),
* `echivocari-2/` (dovezile pastrate; `echivocari/` tine dovezile formei de dinainte de a doua revizie, citite mai departe). Un lacat (`.lacat`: pid-ul,
* pornirea masinii si a procesului) refuza un al doilea proces pe aceeasi stare.
* La deschidere se REFUZA o stare stricata (un rand de frunza stricat in mijloc, o frunza fara capul ei, un cap de jurnal semnat peste mai multe
* frunze decat sunt, sau a carui radacina nu se reface): martorul ar semna altfel o ramura a propriului jurnal. Un rand taiat la coada (proces
* oprit in mijlocul scrierii, deci fara raspuns) se taie.
* `maxEchivocari` (implicit CIOBURI_PASTRATE x maxFrunze, marginea prin constructie) il coboara numai o proba. `faraLant` (laboratorul, fara
* nicio notarizare): rotirea nu asteapta notarizarea capului de jurnal final al ciobului care se scoate.
*/
export function deschideMartor(dir, { log = LOG_IMPLICIT, chainId = LANT, maxMergeDelay = MERGE_MAX_S, maxFrunze = PLAFON_FRUNZE, maxEchivocari = null,
ceas = () => Date.now(), toleranta = TOLERANTA_S, jurnal = () => {}, faraLant = false } = {}) {
if (!(Number.isSafeInteger(maxFrunze) && maxFrunze >= 1)) throw new Error('agent-witness: maxFrunze must be an integer >= 1');
// plafonul dovezilor = marginea lor prin constructie (revizia B3 a doua, R2-02): o dovada pe sesiune, legata de ciobul capului contrazis
if (maxEchivocari == null) maxEchivocari = CIOBURI_PASTRATE * maxFrunze;
if (!(Number.isSafeInteger(maxEchivocari) && maxEchivocari >= 0)) throw new Error('agent-witness: maxEchivocari must be an integer >= 0');
for (const d of ['', 'cioburi', 'echivocari', 'echivocari-2']) fs.mkdirSync(path.join(dir, d), { recursive: true });
const lacat = path.join(dir, '.lacat');
if (fs.existsSync(lacat)) { const t = lacatTinut(fs.readFileSync(lacat, 'utf8')); if (t.tinut) throw new Error(`agent-witness: the state ${dir} is held by process ${t.pid}`); jurnal(`agent-witness: a lock of process ${t.pid} was left (${t.motiv}); removed`); fs.rmSync(lacat, { force: true }); }
fs.writeFileSync(lacat, JSON.stringify({ pid: process.pid, boot: idPornire(), start: pornireProces(process.pid) }), { flag: 'wx' });
try { return deschide(); } catch (e) { try { fs.rmSync(lacat, { force: true }); } catch { /* idem */ } throw e; }
function deschide() {
const cheie = cheiaMartorului(dir);
const fc = path.join(dir, 'martor.json');
const conf = { log, chainId, witness: cheie.witnessId, maxMergeDelay };
if (fs.existsSync(fc)) {
const v = JSON.parse(fs.readFileSync(fc, 'utf8'));
for (const k of Object.keys(conf)) if (v[k] !== conf[k]) throw new Error(`agent-witness: the state was made with ${k}=${v[k]}, not ${conf[k]}; a log never changes its name, chain, witness or merge delay`);
if (v.format === 'aere-agent-witness-state-1') {
// starea din forma 1 (un singur jurnal, la radacina): devine ciobul 0, cu aceleasi frunze, capete si capete de jurnal
const d0 = path.join(dir, 'cioburi', '0'); fs.mkdirSync(d0, { recursive: true });
for (const f of ['frunze.ndjson', 'puncte', 'capete']) if (fs.existsSync(path.join(dir, f))) fs.renameSync(path.join(dir, f), path.join(d0, f));
scrieAtomic(fc, JSON.stringify({ ...v, format: 'aere-agent-witness-state-2', migratedAt: new Date(ceas()).toISOString() }, null, 1) + '\n');
jurnal('agent-witness: the state of form 1 became shard 0 of form 2');
} else if (v.format !== 'aere-agent-witness-state-2') throw new Error(`agent-witness: unknown state format ${v.format}`);
} else scrieAtomic(fc, JSON.stringify({ format: 'aere-agent-witness-state-2', ...conf, createdAt: new Date(ceas()).toISOString() }, null, 1) + '\n');
// ---- cioburile: se pastreaza ultimele CIOBURI_PASTRATE; unul mai vechi ramas pe disc (o stergere intrerupta) se sterge acum, NUMAI daca
// capul lui de jurnal final e notarizat (revizia B3 a doua, R2-07: altfel capetele lui ar ramane fara moment certificat, pentru totdeauna);
// un ciob nascut pe jumatate (`.nou-*`, o creare intrerupta, R2-08) nu e un ciob si se sterge.
// STERGEREA unui ciob (revizia B3 a treia, R3-01): pana atunci rotirea stergea dosarul ciobului ASINCRON, pe loc, iar o oprire in acea
// fereastra (o repornire, un OOM, o pana) lasa un REST: ciob.json si frunze.ndjson plecau primele, puncte/ golit, capete/ pe jumatate. La
// deschidere R2-07 il judeca drept un ciob intreg nenotarizat si il TINEA: gol, nu mai putea fi notarizat niciodata, deci rotirea astepta
// pentru totdeauna (503 notarization_behind pentru toti), sau, cu capete de jurnal ramase, martorul refuza sa porneasca. Acum un ciob de
// scos se REDENUMESTE intai, sincron, in `.sters-<k>-*` (o redenumire e atomica), si abia apoi se sterge; deschiderea sterge fara judecata
// tot ce poarta acest nume. Daca redenumirea nu merge, dosarul primeste intai marcajul `.de-sters`. Iar un ciob mai vechi decat cele
// pastrate caruia ii lipseste ciob.json sau frunze.ndjson (un rest al formei de dinainte, care nu redenumea) nu e un ciob, ci un rest.
const dc = path.join(dir, 'cioburi');
for (const f of fs.readdirSync(dc)) if (/^\.(nou|sters)-/.test(f)) { try { fs.rmSync(path.join(dc, f), { recursive: true, force: true, maxRetries: 3 }); } catch { /* la urmatoarea deschidere */ } }
let ks = fs.readdirSync(dc).filter((f) => /^\d+$/.test(f)).map(Number).sort((a, b) => a - b);
if (!ks.length) { creeazaCiobPeDisc(0, null); ks = [0]; }
const kMax = ks[ks.length - 1];
const finalNotarizatPeDisc = (k) => {
try { const pd = path.join(dc, String(k), 'puncte'); const n = fs.readdirSync(pd).filter((f) => /^\d+\.json$/.test(f)).map((f) => Number(f.slice(0, -5))).sort((a, b) => b - a)[0];
return n !== undefined && !!JSON.parse(fs.readFileSync(path.join(pd, n + '.json'), 'utf8')).notarization; } catch { return false; }
};
// doua garzi, fiecare cu randul ei (revizia B3 a patra, R4-06: plantata intreaga, jumatatea marcajului nu era masurata singura)
const marcatDeSters = (k) => fs.existsSync(path.join(dc, String(k), '.de-sters')) /*PAZA-MARTOR:ciob-marcat*/;
const faraFisiereleCiobului = (k) => { const d = path.join(dc, String(k)); return (!fs.existsSync(path.join(d, 'ciob.json')) || !fs.existsSync(path.join(d, 'frunze.ndjson'))) /*PAZA-MARTOR:ciob-fara-fisiere*/; };
const restDeStergere = (k) => marcatDeSters(k) || faraFisiereleCiobului(k);
// (o stergere care nu reuseste acum, de pilda fiindca cea asincrona a rotirii inca ruleaza, nu opreste pornirea: ciobul nu se mai incarca)
// (de la primul ciob vechi pastrat, toate cele de dupa raman: cioburile pastrate sunt mereu consecutive)
const pastrate = new Set(); let tine = false;
for (const k of ks) {
if (k > kMax - CIOBURI_PASTRATE) { pastrate.add(k); continue; }
if (restDeStergere(k) /*PAZA-MARTOR:ciob-rest*/) { jurnal(`agent-witness: shard ${k} is the rest of an interrupted removal (marked, or without ciob.json or frunze.ndjson): removed`); try { stergeDosarCiob(k, false); } catch (e) { jurnal(`agent-witness: shard ${k} could not be removed yet (${e.code || 'error'})`); } continue; }
if (tine) { pastrate.add(k); continue; }
if (!(faraLant || finalNotarizatPeDisc(k) /*PAZA-MARTOR:ciob-vechi-notarizat*/)) { pastrate.add(k); tine = true; jurnal(`agent-witness: shard ${k} is older than the shards kept, but its final checkpoint is not notarized: kept until it is`); continue; }
try { stergeDosarCiob(k, false); } catch (e) { jurnal(`agent-witness: shard ${k} is not kept and could not be removed yet (${e.code || 'error'})`); }
}
ks = ks.filter((k) => pastrate.has(k));
for (let j = 1; j < ks.length; j++) if (ks[j] !== ks[j - 1] + 1) throw new Error(`agent-witness: shard ${ks[j - 1] + 1} is missing between ${ks[j - 1]} and ${ks[j]}`);
const cioburi = new Map(); // k -> ciobul
const dupaCap = new Map(); // statementHash-ul capului -> frunza (din oricare ciob pastrat)
const capete = new Map(); // agentId|session -> { agentId, session, policyHash, heads: [frunze ordonate dupa seq] }
const sesiuniAgent = new Map(); // agentId -> Map(session -> sesiunea)
// dovezile pastrate: id -> { id, z (sesiunea), seq, k (ciobul capului contrazis), v1 (fisier al formei de dinainte, in echivocari/) }; sesiunea
// tine id-ul dovezii ei in `z.eq` (una pe sesiune)
const echivocari = new Map();
const echivNepastrate = { una: 0, plafon: 0 }; // dovezi intoarse si nepastrate, de la pornire: sesiunea avea deja una / plafonul
const fisierEchiv = (q) => path.join(dir, q.v1 ? 'echivocari' : 'echivocari-2', q.id.slice(2) + '.json');
let stricat = false; // o scriere a cazut si nici taierea inapoi nu a mers: nu se mai primeste nimic pana la repornire
// capetele refuzate de bugetul pe ora, numarate pe ora curenta: /health le arata (o inundatie care intarzie pe toti se vede, nu se ghiceste)
const refuzBuget = { de: 0, n: 0, total: 0, notarizare: 0 };
const numaraRefuzBuget = (acumMs) => { if (acumMs - refuzBuget.de >= 3_600_000) { refuzBuget.de = acumMs; refuzBuget.n = 0; } refuzBuget.n++; refuzBuget.total++; };
// un ciob de scos: REDENUMIT intai (sincron, atomic) in `.sters-<k>-*`, apoi sters (asincron dupa o rotire, cu zeci de mii de fisiere);
// daca redenumirea nu merge, dosarul e marcat `.de-sters` inainte de stergere (revizia B3 a treia, R3-01). Un rest ramas dupa o oprire in
// timpul stergerii nu mai poate fi luat drept un ciob la deschidere.
function stergeDosarCiob(k, asincron) {
const d = path.join(dc, String(k)); let tinta = d;
try { const t = path.join(dc, `.sters-${k}-${process.pid}-${++contorTmp}`); fs.renameSync(d, t) /*PAZA-MARTOR:ciob-sters-intai*/; fsyncDosar(dc); tinta = t; }
catch (e) { if (e.code === 'ENOENT') return; try { fs.writeFileSync(path.join(d, '.de-sters'), ''); } catch { /* ramane restul: il judeca deschiderea */ } }
if (asincron) fs.rm(tinta, { recursive: true, force: true }, (e) => { if (e) jurnal(`agent-witness: shard ${k} could not be removed yet (${e.code || 'error'}); the next open removes it`); });
else fs.rmSync(tinta, { recursive: true, force: true, maxRetries: 3 });
}
// un ciob nou se naste INTREG (revizia B3 a doua, R2-08): dosarul cu ciob.json (care numeste capul final al ciobului de dinainte) se face sub un
// nume temporar si abia apoi se redenumeste; pana la reparatie mkdir venea intai, iar o scriere cazuta (ENOSPC) urmata de o repornire lasa un
// ciob fara ciob.json, pe care deschiderea il rescria cu previousShardFinal null: legatura dintre cioburi pierduta pe veci
function creeazaCiobPeDisc(k, inainte) {
const d = path.join(dc, String(k)); const fcb = path.join(d, 'ciob.json');
const meta = JSON.stringify({ format: 'aere-agent-witness-shard-1', k, log: numeCiob(log, k), previousShardFinal: inainte, createdAt: new Date(ceas()).toISOString() }, null, 1) + '\n';
if (fs.existsSync(d)) { if (!fs.existsSync(fcb)) scrieAtomic(fcb, meta); return d; } // un dosar al formei de dinainte, fara ciob.json
const t = path.join(dc, `.nou-${k}-${process.pid}-${++contorTmp}`);
try {
for (const s of ['', 'capete', 'puncte']) fs.mkdirSync(path.join(t, s), { recursive: true });
scrieAtomic(path.join(t, 'ciob.json'), meta); fs.renameSync(t, d) /*PAZA-MARTOR:ciob-intreg*/; fsyncDosar(dc);
} catch (e) { try { fs.rmSync(t, { recursive: true, force: true }); } catch { /* la urmatoarea deschidere */ } throw e; }
return d;
}
const sesiunea = (a, s, p) => {
const key = a + '|' + s; let z = capete.get(key);
if (!z) { z = { key, agentId: a, session: s, policyHash: p, heads: [], eq: null }; capete.set(key, z); if (!sesiuniAgent.has(a)) sesiuniAgent.set(a, new Map()); sesiuniAgent.get(a).set(s, z); }
return z;
};
function incarcaCiob(k) {
const d = path.join(dc, String(k));
for (const s of ['capete', 'puncte']) fs.mkdirSync(path.join(d, s), { recursive: true });
const fcb = path.join(d, 'ciob.json');
let meta;
if (fs.existsSync(fcb)) meta = JSON.parse(fs.readFileSync(fcb, 'utf8'));
else {
// un ciob fara ciob.json (o creare intrerupta a formei de dinainte, R2-08): legatura se reface din capul de jurnal FINAL al ciobului de
// dinainte, daca el e pastrat si capul lui acopera toate frunzele lui (rotirea il scrie inainte de ciobul nou); altfel ramane null, spus
const pr = cioburi.get(k - 1); const fin = pr && pr.puncte.length ? pr.puncte[pr.puncte.length - 1] : null;
const legat = fin && fin.treeSize === pr.frunze.length ? fin.statementHash : null;
meta = { k, log: numeCiob(log, k), previousShardFinal: legat /*PAZA-MARTOR:ciob-legatura*/ };
if (k > 0 && !legat) jurnal(`agent-witness: shard ${k} had no ciob.json and the final checkpoint of shard ${k - 1} is not here: its link to the shard before is lost`);
scrieAtomic(fcb, JSON.stringify({ format: 'aere-agent-witness-shard-1', ...meta, createdAt: new Date(ceas()).toISOString(), repairedAt: new Date(ceas()).toISOString() }, null, 1) + '\n');
}
if (meta.k !== k || meta.log !== numeCiob(log, k)) throw new Error(`agent-witness: shard ${k} names itself ${meta.k} / ${meta.log}`);
const ciob = { k, dir: d, log: meta.log, inainte: meta.previousShardFinal || null, createdAt: meta.createdAt || null, ff: path.join(d, 'frunze.ndjson'), frunze: [], puncte: [], notarizate: [], arbore: arboreIncremental() };
if (fs.existsSync(ciob.ff)) {
const text = fs.readFileSync(ciob.ff, 'utf8'); const randuri = text.split('\n'); const coada = randuri.pop();
let octeti = 0;
for (const [i, r] of randuri.entries()) {
let o = null; try { o = JSON.parse(r); } catch { o = null; }
// numai ULTIMUL rand poate fi taiat (fara '\n'); un rand stricat urmat de altele e o stare stricata, nu o coada
const bun = obiect(o) && o.i === i && H32.test(String(o.h)) && Number.isSafeInteger(o.r) && o.r >= 0 && AGENT_ID.test(String(o.a)) && SESIUNE.test(String(o.s)) && Number.isSafeInteger(o.q) && o.q >= 0
&& (o.e === undefined || HASH_INTRARE.test(String(o.e))) && (o.p === undefined || H32.test(String(o.p)));
if (!(bun /*PAZA-MARTOR:frunze-mijloc*/)) throw new Error(`agent-witness: shard ${k}, frunze.ndjson line ${i} is damaged and lines follow it: refusing to start (cutting it would lose leaves already answered, under signed checkpoints)`);
const fh = path.join(d, 'capete', o.h.slice(2) + '.json');
if (!fs.existsSync(fh)) throw new Error(`agent-witness: shard ${k}: the head of leaf ${i} is missing`);
let e = o.e, p = o.p;
if (e === undefined || p === undefined) { const st = JSON.parse(fs.readFileSync(fh, 'utf8')).envelope.statement; e = st.hash; p = st.policyHash; } // forma 1
const z = sesiunea(o.a, o.s, p);
const x = { h: o.h, r: o.r, q: o.q, e, k, i, z };
ciob.frunze.push(x); ciob.arbore.adauga(frunzaMartor(o.h, o.r)); dupaCap.set(x.h, x); z.heads.push(x);
octeti += Buffer.byteLength(r, 'utf8') + 1;
}
if (coada !== '' /*PAZA-MARTOR:coada*/) { fs.truncateSync(ciob.ff, octeti); jurnal(`agent-witness: shard ${k}: a leaf line cut at the tail (never answered) was cut`); }
}
// capetele de jurnal: in memorie numai rezumatul (plicul semnat sta pe disc si se citeste cand se cere o chitanta)
const nume = fs.readdirSync(path.join(d, 'puncte')).filter((f) => /^\d+\.json$/.test(f)).map((f) => Number(f.slice(0, -5))).sort((a, b) => a - b);
nume.forEach((n, j) => { if (n !== j) throw new Error(`agent-witness: shard ${k}: checkpoint ${j} is missing or misnumbered`); });
for (const n of nume) {
const pf = JSON.parse(fs.readFileSync(path.join(d, 'puncte', n + '.json'), 'utf8')); const st = pf.envelope.statement;
const p = { k, n, statementHash: pf.envelope.statementHash, treeSize: st.treeSize, root: st.root, createdAt: st.createdAt, notarization: pf.notarization || null, attempts: pf.attempts || 0, lastError: pf.lastError || null };
if (st.log !== ciob.log || st.witness !== cheie.witnessId) throw new Error(`agent-witness: shard ${k}: checkpoint ${n} is of another log or witness`);
// un cap de jurnal semnat peste mai multe frunze decat sunt pe disc, sau cu alta radacina: frunze confirmate s-au pierdut sau s-au schimbat.
// Doua garzi, fiecare cu proba ei (revizia B3 a doua, R2-15: o singura conditie plantata intreaga lasa jumatatea "radacina" nemasurata)
if (!(p.treeSize <= ciob.frunze.length /*PAZA-MARTOR:radacina-marime*/)) throw new Error(`agent-witness: shard ${k}: checkpoint ${n} was signed over ${p.treeSize} leaves, and the leaves on disk are ${ciob.frunze.length}: refusing to start (the witness would sign a fork of its own log)`);
if (!(ciob.arbore.root(p.treeSize) === p.root /*PAZA-MARTOR:radacina*/)) throw new Error(`agent-witness: shard ${k}: checkpoint ${n} was signed over ${p.treeSize} leaves with root ${String(p.root).slice(0, 18)}..., and the leaves on disk do not give it: refusing to start (the witness would sign a fork of its own log)`);
if (ciob.puncte.length && p.treeSize <= ciob.puncte[ciob.puncte.length - 1].treeSize) throw new Error(`agent-witness: shard ${k}: checkpoint ${n} does not grow the log`);
ciob.puncte.push(p); if (p.notarization) ciob.notarizate.push(p);
}
cioburi.set(k, ciob);
return ciob;
}
for (const k of ks) incarcaCiob(k);
let cur = cioburi.get(ks[ks.length - 1]);
// dovezile pastrate: in memorie numai indexul. Forma 2 (echivocari-2/, de la a doua revizie): {v: 2, ciob, held, seq, b}, legata de ciobul capului
// contrazis `held` (pastrat, altfel dovada se sterge). Forma 1 (echivocari/, dovada intreaga): legata acum tot de ciobul capului ei `a`, cand acesta
// e pastrat; altfel se sterge (fara capul contrazis, martorul nu mai tine nimic din ce contrazice ea)
for (const [sub, v1] of [['echivocari-2', false], ['echivocari', true]]) {
for (const f of fs.readdirSync(path.join(dir, sub)).filter((x) => /^[0-9a-f]{64}\.json$/.test(x))) {
const fp = path.join(dir, sub, f); let o = null; try { o = JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { o = null; }
let x = null, seq = null;
if (!v1 && obiect(o) && o.v === 2 && obiect(o.b) && H32.test(String(o.held))) { x = dupaCap.get(o.held) || null; seq = o.seq; if (x && x.k !== o.ciob) x = null; }
else if (v1 && obiect(o)) { const p = obiect(o.proof) ? o.proof : o; if (p.kind === EQUIVOCATION_KIND && obiect(p.a)) { x = dupaCap.get(String(p.a.statementHash)) || null; seq = p.seq; } }
if (!x || !Number.isSafeInteger(seq)) { fs.rmSync(fp, { force: true }); continue; }
// doua dovezi pe aceeasi sesiune (o inlocuire intrerupta, R3-02): ramane cea al carei cap contrazis sta in ciobul mai nou
const vechi = x.z.eq ? echivocari.get(x.z.eq) : null;
if (vechi && vechi.k >= x.k /*PAZA-MARTOR:echivocare-departajare*/) { fs.rmSync(fp, { force: true }); continue; }
if (vechi) { echivocari.delete(vechi.id); fs.rmSync(fisierEchiv(vechi), { force: true }); }
const id = '0x' + f.slice(0, 64);
echivocari.set(id, { id, z: x.z, seq, k: x.k, v1 }); x.z.eq = id;
}
}
const fisierCap = (x) => path.join(dc, String(x.k), 'capete', x.h.slice(2) + '.json');
const capulPrimit = (x) => JSON.parse(fs.readFileSync(fisierCap(x), 'utf8'));
const plicPunct = (p) => JSON.parse(fs.readFileSync(path.join(dc, String(p.k), 'puncte', p.n + '.json'), 'utf8')).envelope;
// capul de jurnal care acopera frunza: cel mai TIMPURIU notarizat (momentul cel mai strans), altfel cel mai timpuriu construit
function acoperitor(x) {
const c = cioburi.get(x.k); const caut = (l) => { let a = 0, b = l.length; while (a < b) { const m = (a + b) >> 1; if (l[m].treeSize > x.i) b = m; else a = m + 1; } return a < l.length ? l[a] : null; };
return caut(c.notarizate) || caut(c.puncte);
}
function stareScurta(x) {
const p = acoperitor(x);
return { seq: x.q, hash: x.e, id: x.h, log: cioburi.get(x.k).log, leafIndex: x.i, receivedAt: new Date(x.r).toISOString(),
status: !p ? 'pending' : p.notarization ? 'notarized' : 'checkpointed', checkpoint: p ? { n: p.n, treeSize: p.treeSize, statementHash: p.statementHash } : null };
}
function construiestePunctIn(c) {
const ultim = c.puncte[c.puncte.length - 1];
if (!c.frunze.length || (ultim && ultim.treeSize >= c.frunze.length)) return null;
const n = c.puncte.length;
const env = capDeJurnal(cheie, { treeSize: c.frunze.length, root: c.arbore.root(c.frunze.length), previous: ultim ? ultim.statementHash : c.inainte, log: c.log, chainId, maxMergeDelay, createdAt: new Date(ceas()).toISOString() });
scrieAtomic(path.join(c.dir, 'puncte', n + '.json'), JSON.stringify({ n, envelope: env, notarization: null, attempts: 0, lastError: null }) + '\n');
const p = { k: c.k, n, statementHash: env.statementHash, treeSize: env.statement.treeSize, root: env.statement.root, createdAt: env.statement.createdAt, notarization: null, attempts: 0, lastError: null };
c.puncte.push(p); return p;
}
function scoateCiob(k) {
const c = cioburi.get(k); if (!c) return;
cioburi.delete(k);
const atinse = new Set();
for (const x of c.frunze) { dupaCap.delete(x.h); atinse.add(x.z); }
// o singura trecere pe sesiune (capetele unei sesiuni sunt ordonate, cele ale ciobului scos sunt la inceput)
for (const z of atinse) {
z.heads = z.heads.filter((x) => x.k > k);
if (!z.heads.length) { capete.delete(z.key); const m = sesiuniAgent.get(z.agentId); if (m) { m.delete(z.session); if (!m.size) sesiuniAgent.delete(z.agentId); } }
}
// dovezile capetelor acestui ciob pleaca odata cu ele (o dovada fara capul contrazis nu mai arata nimic din ce tine martorul)
for (const [id, q] of echivocari) if (q.k <= k) { echivocari.delete(id); if (q.z.eq === id) q.z.eq = null; try { fs.rmSync(fisierEchiv(q), { force: true }); } catch { /* se sterge la urmatoarea deschidere */ } }
// pe disc: dupa ce memoria nu il mai stie; redenumit intai, apoi sters asincron (zeci de mii de fisiere), iar o stergere intrerupta o reia
// urmatoarea deschidere, fara sa il mai poata lua drept un ciob (R3-01)
stergeDosarCiob(k, true);
}
// capul de jurnal final al unui ciob, notarizat (peste toate frunzele lui)
const finalNotarizat = (c) => { const p = c.puncte[c.puncte.length - 1]; return !!p && !!p.notarization && p.treeSize === c.frunze.length; };
// o rotire acum ar scoate un ciob al carui cap de jurnal final nu e notarizat? (primeste refuza atunci capetele noi: R2-07)
const rotireaPierdeNenotarizat = () => !faraLant && [...cioburi.values()].some((c) => c.k <= cur.k + 1 - CIOBURI_PASTRATE && !finalNotarizat(c));
// ciobul curent e plin: capul lui de jurnal FINAL (peste toate frunzele), apoi ciobul urmator, apoi se scoate cel mai vechi peste plafon (primeste
// nu cheama rotirea cat un ciob de scos e nenotarizat)
function roteste() {
const vechi = cur;
construiestePunctIn(vechi);
const fin = vechi.puncte[vechi.puncte.length - 1];
const k = vechi.k + 1; creeazaCiobPeDisc(k, fin.statementHash);
cur = incarcaCiob(k);
for (const j of [...cioburi.keys()]) if (j <= k - CIOBURI_PASTRATE) scoateCiob(j);
jurnal(`agent-witness: shard ${vechi.k} (${vechi.log}) is full at ${vechi.frunze.length} leaves, final checkpoint ${fin.n}; now shard ${k} (${cur.log})`);
}
// un ciob mai vechi decat cel curent e INGHETAT: capul lui de jurnal final trebuie sa acopere toate frunzele (o rotire intrerupta)
for (const c of cioburi.values()) if (c !== cur) construiestePunctIn(c);
function stareaCapului(id) {
const x = dupaCap.get(String(id).toLowerCase());
if (!x) return { cod: 404, corp: { error: 'unknown_head', hint: 'POST the head first; the id is its statementHash. A head of a shard the sandbox no longer keeps is unknown too (its receipt stays verifiable without the witness)', sandbox: SANDBOX } };
const c = cioburi.get(x.k); const cap = capulPrimit(x); const s = stareScurta(x);
const baza = { id: x.h, agentId: x.z.agentId, session: x.z.session, seq: x.q, log: c.log, leafIndex: x.i, receivedAt: s.receivedAt, promise: cap.promise, sandbox: SANDBOX };
const p = acoperitor(x);
if (!p) return { cod: 200, corp: { status: 'pending', ...baza, maxMergeDelay, mergeBy: new Date(x.r + maxMergeDelay * 1000).toISOString(), hint: 'the next checkpoint will cover it' } };
const d = c.arbore.inclusionProof(x.i, p.treeSize);
const receipt = { ...plicPunct(p), ...(p.notarization ? { notarization: p.notarization } : {}),
witnessReceipt: { format: RECEIPT_FORMAT, head: cap.envelope, receivedAtMs: x.r, receivedAt: s.receivedAt, leafIndex: x.i, treeSize: p.treeSize, leafHash: d.leafHash, path: d.path, promise: cap.promise } };
return { cod: 200, corp: { status: p.notarization ? 'notarized' : 'checkpointed', ...baza, checkpoint: { n: p.n, treeSize: p.treeSize, statementHash: p.statementHash },
...(p.notarization ? {} : { notarization: p.lastError ? { state: 'retrying', lastError: p.lastError } : { state: 'pending' } }), receipt } };
}
// o echivocare: dovada se intoarce MEREU; se PASTREAZA numai prima a sesiunii (agent si politica), legata de ciobul capului contrazis `tinut`
// (revizia B3 a doua, R2-02: legata de ciobul curent, cum era, o dovada traia mai mult decat capul ei, iar dovezile unor sesiuni proprii puteau
// umple plafonul global, dupa care echivocarea reala a altui agent nu mai era pastrata). Pe disc numai capul nou semnat; capul contrazis e in
// capete/ al ciobului ei. Un plafon sub marginea prin constructie (numai in probe) refuza pastrarea, numarata in /health.
// DOVADA CARE TRAIESTE CEL MAI MULT (revizia B3 a treia, R3-02): o dovada pastrata pleaca odata cu ciobul capului pe care il contrazice, deci un
// agent care isi ocupa singur locul sesiunii cu o echivocare contra unui cap din ciobul VECHI facea ca echivocarea care conteaza (contra unui
// cap din ciobul curent) sa fie intoarsa nepastrata, iar dupa rotire sesiunea ramanea fara nicio dovada. Acum o dovada noua al carei cap
// contrazis sta intr-un ciob mai NOU ia locul celei pastrate (tot una pe sesiune, deci aceeasi margine).
function echivoca(tinut, env, poz, limitaAgent, detail) {
const proof = dovadaEchivocare(capulPrimit(tinut).envelope, env, poz); const pid = idEchivocare(proof); const z = tinut.z;
let kept = echivocari.has(pid); let inlocuita = null;
const q0 = z.eq ? echivocari.get(z.eq) || null : null;
const inlocuieste = !!q0 && q0.k < tinut.k /*PAZA-MARTOR:echivocare-mai-noua*/;
if (!kept && (inlocuieste || !z.eq /*PAZA-MARTOR:echivocare-una*/) && (inlocuieste || echivocari.size < maxEchivocari /*PAZA-MARTOR:echivocare-plafon*/)) {
// pastrarea e o schimbare de stare: o numara limita pe agent (o dovada deja pastrata, nu)
if (limitaAgent && limitaAgent(proof.agentId) /*PAZA-MARTOR:ritm*/) return { cod: 429, corp: { error: 'rate_limited', detail: 'too many changes for this agent in the window', sandbox: SANDBOX } };
const q = { id: pid, z, seq: poz, k: tinut.k /*PAZA-MARTOR:echivocare-ciob*/, v1: false };
scrieAtomic(fisierEchiv(q), JSON.stringify({ v: 2, ciob: q.k, held: tinut.h, seq: poz, b: env }) + '\n');
if (inlocuieste) { echivocari.delete(q0.id); try { fs.rmSync(fisierEchiv(q0), { force: true }); } catch { /* o dovada in plus pe disc: deschiderea pastreaza una pe sesiune */ } inlocuita = q0.id; }
echivocari.set(pid, q) /*PAZA-MARTOR:echivocare*/; z.eq = pid;
kept = true;
} else if (!kept) { if (z.eq) echivNepastrate.una++; else echivNepastrate.plafon++; }
return { cod: 409, corp: { error: 'equivocation', detail, proof, proofId: pid, kept, ...(inlocuita ? { replaced: inlocuita } : {}), ...(kept ? {} : { note: z.eq ? 'not kept: the witness keeps one proof per agent and policy (it holds ' + z.eq + ', which lives at least as long); this one is returned to you' : 'not kept: the witness keeps at most ' + maxEchivocari + ' proofs in all; this one is returned to you' }), sandbox: SANDBOX } };
}
return {
dir, cheie, witnessId: cheie.witnessId, log, chainId, maxMergeDelay, maxFrunze,
/** Primeste un cap trimis. Intoarce {cod, corp}: 202 primit, 200 deja vazut, 4xx refuz numit, 409 consistenta sau echivocare, 429 limita pe agent
* (`limitaAgent`: numara numai schimbarile de stare, dupa semnatura), 503 bugetul jurnalului pe ora (`limitaGlobala`) sau o scriere cazuta. */
primeste(env, { limitaAgent = null, limitaGlobala = null } = {}) {
if (stricat) return { cod: 503, corp: { error: 'witness_error', code: 'io_error', detail: 'the witness could not write its log and accepts nothing until it restarts', sandbox: SANDBOX } };
const acumMs = ceas();
const v = verificaCapSemnat(env, { acumS: acumMs / 1000, toleranta });
if (!v.ok) return { cod: ['cheie', 'identitate', 'semnatura'].includes(v.rule) ? 403 : v.rule === 'timp' ? 422 : 400, corp: { error: 'refused', rule: v.rule, detail: v.error, sandbox: SANDBOX } };
const st = v.head; const id = v.statementHash;
// un cap DEJA VAZUT (acelasi plic, sau aceeasi pozitie cu acelasi hash si un predecesor care nu contrazice nimic) nu schimba nimic, deci
// nu consuma cota agentului: pana la reparatie oricine consuma cota unui agent retrimitandu-i capul PUBLIC (revizia B3, constatarea 11)
if (dupaCap.has(id)) return stareaCapului(id);
const z = capete.get(st.agentId + '|' + st.session); const lista = z ? z.heads : [];
const laSeq = gaseste(lista, st.seq);
const laPrev = st.previous ? gaseste(lista, st.previous.seq) : null;
// 1. aceeasi pozitie, alt hash: echivocare
if (laSeq && !(laSeq.e === st.hash) /*PAZA-MARTOR:echivocare*/) return echivoca(laSeq, env, st.seq, limitaAgent, `two heads signed by the agent give position ${st.seq} two hashes`);
// 2. predecesorul numit contra unui cap deja vazut la acea pozitie: echivocare, INAINTE de iesirea "deja vazut" (pana la reparatie un cap
// cu acelasi seq si hash dar cu predecesorul contrazis iesea 200 si contradictia semnata nu era pastrata; revizia B3, constatarea 4)
if (laPrev && !(laPrev.e === st.previous.hash) /*PAZA-MARTOR:echivocare*/) return echivoca(laPrev, env, st.previous.seq, limitaAgent, `the head names as its previous (seq ${st.previous.seq}) a hash the agent signed otherwise before`);
// 3. aceeasi pozitie, acelasi hash, nimic contrazis: acelasi cap, cerut din nou cu alt moment declarat
if (laSeq) return stareaCapului(laSeq.h);
const ultim = lista.length ? lista[lista.length - 1] : null;
const latest = ultim ? { seq: ultim.q, hash: ultim.e, id: ultim.h } : null;
// 4. un cap mai VECHI decat ultimul vazut (si nevazut la pozitia lui): o intoarcere in timp, refuzata
if (!(!ultim || st.seq > ultim.q) /*PAZA-MARTOR:vechi*/) return { cod: 409, corp: { error: 'stale_head', detail: `seq ${st.seq} is older than the last head witnessed for this agent and policy (seq ${ultim.q})`, latest, sandbox: SANDBOX } };
// 5. succesorul: `previous` trebuie sa fie exact ultimul cap vazut (null daca niciunul), ca old size din C2SP tlog-witness
const prevBun = ultim ? !!st.previous && st.previous.seq === ultim.q && st.previous.hash === ultim.e : st.previous === null;
if (!(prevBun /*PAZA-MARTOR:succesor*/)) return { cod: 409, corp: { error: 'not_successor', detail: ultim ? `the head must name as previous the last head witnessed (seq ${ultim.q})` : 'the witness holds no head for this agent and policy: the first head names previous null', latest, sandbox: SANDBOX } };
// 6. bugetul jurnalului pe ora (comun): un ciob nu se umple mai repede decat in o zi, deci chitantele lui se servesc cel putin o zi. Bugetul
// se CITESTE aici (`limitaGlobala.ar`, fara sa consume) si se consuma abia dupa cota agentului: un cap refuzat de buget nu schimba nimic,
// deci nu consuma cota agentului (revizia B3 a doua, R2-13: pana atunci pasul cotei venea intai, si un agent care reincerca in timpul
// unei inundatii primea 429 si dupa ce bugetul se elibera). O functie fara `.ar` (o proba) se cheama o data, aici.
const bugetPlin = limitaGlobala ? (typeof limitaGlobala.ar === 'function' ? limitaGlobala.ar() : limitaGlobala()) : false;
if (bugetPlin /*PAZA-MARTOR:buget*/) return (numaraRefuzBuget(acumMs), { cod: 503, corp: { error: 'log_busy', detail: 'the sandbox log takes a limited number of new heads per hour, for everyone; try again later', retryAfterS: 300, sandbox: SANDBOX } });
// 7. ciobul curent plin si o rotire ar scoate un ciob al carui cap de jurnal final nu e inca notarizat: capetele NOI asteapta (revizia B3 a doua,
// R2-07: pana atunci ciobul se scotea oricum, iar capetele lui, primite cu 202 si "mergeBy", ramaneau fara moment certificat pentru totdeauna)
const plin = !(cur.frunze.length < maxFrunze);
if (!(!plin || !rotireaPierdeNenotarizat() /*PAZA-MARTOR:rotire-notarizata*/)) { refuzBuget.notarizare++; return { cod: 503, corp: { error: 'log_busy', code: 'notarization_behind', detail: 'the log is full and the final checkpoint of the shard it would remove is not notarized yet; new heads wait until it is', retryAfterS: 600, sandbox: SANDBOX } }; }
// 8. limita pe agent: numai o cerere AUTENTIFICATA care SCHIMBA starea (un cap nou) o consuma
if (limitaAgent && limitaAgent(st.agentId) /*PAZA-MARTOR:ritm*/) return { cod: 429, corp: { error: 'rate_limited', detail: 'too many heads for this agent in the window', sandbox: SANDBOX } };
if (limitaGlobala && typeof limitaGlobala.ar === 'function') limitaGlobala(); // bugetul, acum consumat (a fost numai citit la pasul 6)
// 9. ciobul curent plin: rotirea (pana la reparatie: 503 log_full pentru totdeauna)
if (!(cur.frunze.length < maxFrunze /*PAZA-MARTOR:plin*/)) roteste();
// primit: capul semnat si promisiunea pe disc, apoi frunza, sincronizate INAINTE de raspuns
const promise = promisiune(cheie, id, acumMs);
const fh = path.join(cur.dir, 'capete', id.slice(2) + '.json');
scrieAtomic(fh, JSON.stringify({ envelope: env, receivedAtMs: acumMs, promise }) + '\n');
const i = cur.frunze.length;
try { adaugaDurabil(cur.ff, JSON.stringify({ i, h: id, r: acumMs, a: st.agentId, s: st.session, p: st.policyHash, q: st.seq, e: st.hash }) + '\n'); }
catch (e) { if (e.taiereEsuata) stricat = true; try { fs.rmSync(fh, { force: true }); } catch { /* ramane un cap fara frunza, nefolosit */ } throw e; }
const zz = sesiunea(st.agentId, st.session, st.policyHash);
const x = { h: id, r: acumMs, q: st.seq, e: st.hash, k: cur.k, i, z: zz };
cur.frunze.push(x); cur.arbore.adauga(frunzaMartor(id, acumMs)); dupaCap.set(id, x); zz.heads.push(x);
return { cod: 202, corp: { status: 'pending', id, agentId: st.agentId, session: st.session, seq: st.seq, log: cur.log, leafIndex: i, receivedAt: new Date(acumMs).toISOString(), promise,
maxMergeDelay, mergeBy: new Date(acumMs + maxMergeDelay * 1000).toISOString(), poll: '/v1/agents/witness/' + id, sandbox: SANDBOX } };
},
stareaCapului,
/** Un cap de jurnal nou peste toate frunzele ciobului curent, daca au venit frunze de la ultimul; semnat, scris pe disc. Intoarce-l sau null. */
construiestePunct() { return construiestePunctIn(cur); },
/** capetele de jurnal de notarizat: ultimul al fiecarui ciob pastrat, daca nu e notarizat (cele vechi nenotarizate sunt acoperite de el) */
deNotarizate() { return [...cioburi.values()].map((c) => c.puncte[c.puncte.length - 1]).filter((p) => p && !p.notarization); },
deNotarizat() { return this.deNotarizate()[0] || null; },
plicPunct,
inregistreazaNotarizarea(ref, rez) {
const p = typeof ref === 'number' ? cur.puncte[ref] : (cioburi.get(ref && ref.k) || { puncte: [] }).puncte[ref && ref.n];
if (!p) throw new Error('agent-witness: no checkpoint ' + JSON.stringify(ref));
const c = cioburi.get(p.k); const f = path.join(c.dir, 'puncte', p.n + '.json'); const pf = JSON.parse(fs.readFileSync(f, 'utf8'));
p.attempts++;
// public numai un COD (unfunded, chain_refused, ...): mesajul intreg merge numai in jurnalul local (revizia B3, constatarea 18)
if (rez && rez.ok) { p.notarization = rez.notarization; p.lastError = null; c.notarizate.push(p); c.notarizate.sort((a, b) => a.treeSize - b.treeSize); }
else p.lastError = rez && typeof rez.cod === 'string' && /^[a-z_]{1,40}$/.test(rez.cod) ? rez.cod : 'notarization_failed';
scrieAtomic(f, JSON.stringify({ ...pf, notarization: p.notarization, attempts: p.attempts, lastError: p.lastError }) + '\n');
return p;
},
/**
* Capetele martorizate ale unui agent. Cu `session`: o PAGINA a capetelor acelei sesiuni (`from` = primul seq, `limit` <= PAGINA_MAX), cu
* `next` (seq-ul de unde continua, sau null), ultimul cap si id-urile dovezilor pastrate (dovada intreaga: `dovadaEchivocarii`). Fara: sesiunile
* agentului (cel mult PAGINA_MAX, de la `after`), fiecare cu numarul capetelor si ultimul cap. Nimic citit de pe disc (revizia B3, constatarea 10).
* O pagina a unei sesiuni poarta si `signedPage`: aceeasi pagina (fiecare cap {seq, hash, id}, `next`, `count`, `latest`, dovezile pastrate) cu
* momentul ei, SEMNATA de martor (revizia B3 a doua, R2-01); verificatorul crede numai pagina semnata (verificaPaginaCapetelor).
*/
capeteAgent(agentId, session = null, { from = 0, limit = PAGINA_MAX, after = null } = {}) {
if (!(Number.isSafeInteger(from) && from >= 0 && Number.isSafeInteger(limit) && limit >= 1 && limit <= PAGINA_MAX)) throw new Error(`agent-witness: from >= 0 and 1 <= limit <= ${PAGINA_MAX}`);
const eqLista = (z) => (z && z.eq && echivocari.has(z.eq) ? [{ id: z.eq, seq: echivocari.get(z.eq).seq }] : []);
const m = sesiuniAgent.get(agentId);
if (session != null) {
const z = m ? m.get(session) || null : null; const l = z ? z.heads : [];
const a = primulDeLa(l, from); const pagina = l.slice(a, a + limit);
const ultim = l.length ? l[l.length - 1] : null;
const latest = ultim ? { seq: ultim.q, hash: ultim.e, id: ultim.h } : null; const next = a + limit < l.length ? l[a + limit].q : null; const equivocations = eqLista(z);
const st = { v: 1, kind: PAGINA_KIND, witness: cheie.witnessId, log, agentId, session, from, limit, count: l.length, latest,
heads: pagina.map((x) => ({ seq: x.q, hash: x.e, id: x.h })), next, equivocations, at: new Date(ceas()).toISOString() };
const signedPage = plicSemnat({ v: 1, kind: PAGINA_KIND + '-attestation', statement: st, statementHash: sha(JSON.stringify(st)) }, cheie);
return { agentId, session, policyHash: z ? z.policyHash : null, count: l.length, latest, heads: pagina.map(stareScurta), next, equivocations, signedPage, sandbox: SANDBOX };
}
const sortate = [...(m ? m.keys() : [])].sort().filter((s) => after == null || s > after);
const pagina = sortate.slice(0, PAGINA_MAX);
return { agentId, sessions: pagina.map((s) => { const z = m.get(s); const ultim = z.heads.length ? z.heads[z.heads.length - 1] : null;
return { session: s, policyHash: z.policyHash, count: z.heads.length, latest: ultim ? { seq: ultim.q, hash: ultim.e, id: ultim.h } : null, equivocations: eqLista(z).length }; }),
nextSession: sortate.length > PAGINA_MAX ? pagina[pagina.length - 1] : null, sandbox: SANDBOX };
},
/** o dovada de echivocare pastrata, dupa id-ul ei (forma 2: refacuta din capul contrazis, tinut de martor, si capul nou, din fisierul ei) */
dovadaEchivocarii(id) {
const q = echivocari.get(String(id).toLowerCase());
if (!q) return { cod: 404, corp: { error: 'unknown_equivocation', sandbox: SANDBOX } };
const o = JSON.parse(fs.readFileSync(fisierEchiv(q), 'utf8'));
const proof = q.v1 ? (obiect(o.proof) ? o.proof : o) : dovadaEchivocare(capulPrimit(dupaCap.get(o.held)).envelope, o.b, o.seq);
return { cod: 200, corp: { id: q.id, agentId: q.z.agentId, session: q.z.session, seq: q.seq, proof, sandbox: SANDBOX } };
},
/**
* Ultimul cap de jurnal al unui ciob pastrat (implicit cel curent; `shard`: si unul INGHETAT, al carui ultim cap e cel final) si cel notarizat,
* iar cu `from` dovada de consistenta in acel ciob de la marimea aceea (RFC 9162, 2.1.4). Pana la a doua revizie (R2-09) numai ciobul curent:
* dupa rotire un monitor nu putea cere consistenta ciobului inghetat pana la capul lui final.
*/
capulJurnalului(from = null, shard = null) {
const c = shard == null ? cur : cioburi.get(Number(shard)) /*PAZA-MARTOR:ciob-cerut*/;
if (!c) return { cod: 404, corp: { error: 'unknown_shard', shardsKept: [...cioburi.keys()], sandbox: SANDBOX } };
const ultim = c.puncte[c.puncte.length - 1] || null; const notarizat = c.notarizate[c.notarizate.length - 1] || null;
const corp = { log: c.log, shard: c.k, frozen: c !== cur, previousShardFinal: c.inainte, witness: cheie.witnessId, chainId, maxMergeDelay, leaves: c.frunze.length, maxLeaves: maxFrunze,
latest: ultim ? { ...plicPunct(ultim), ...(ultim.notarization ? { notarization: ultim.notarization } : {}) } : null,
latestNotarized: notarizat ? { ...plicPunct(notarizat), notarization: notarizat.notarization } : null,
shards: [...cioburi.values()].map((x) => ({ shard: x.k, log: x.log, leaves: x.frunze.length, frozen: x !== cur, previousShardFinal: x.inainte, latest: x.puncte.length ? x.puncte[x.puncte.length - 1].statementHash : null, finalNotarized: x !== cur ? finalNotarizat(x) : undefined })), sandbox: SANDBOX };
if (from != null && ultim) {
const mm = Number(from); const ts = ultim.treeSize;
if (!Number.isSafeInteger(mm) || mm < 1 || mm > ts) return { cod: 400, corp: { error: 'bad_from', hint: `1..${ts}`, sandbox: SANDBOX } };
corp.consistency = c.arbore.consistencyProof(mm, ts);
}
return { cod: 200, corp };
},
sanatate() {
const ultim = cur.puncte[cur.puncte.length - 1] || null; let notarizat = null;
for (const c of [...cioburi.values()].reverse()) if (!notarizat && c.notarizate.length) notarizat = c.notarizate[c.notarizate.length - 1];
return { ok: !stricat, version: VERSION, log, witness: cheie.witnessId, chainId, maxMergeDelay,
shard: cur.k, shardLog: cur.log, leaves: cur.frunze.length, maxLeaves: maxFrunze, shardsKept: [...cioburi.keys()], leavesKept: [...cioburi.values()].reduce((a, c) => a + c.frunze.length, 0),
checkpoints: cur.puncte.length, equivocations: echivocari.size, maxEquivocations: maxEchivocari, sessions: capete.size,
// dovezile intoarse si NEPASTRATE de la pornire: sesiunea avea deja una (normal), sau plafonul (pe starea reala nu se atinge: R2-02)
equivocationsNotKept: { onePerSession: echivNepastrate.una, cap: echivNepastrate.plafon },
logBusy: { refusedThisHour: ceas() - refuzBuget.de < 3_600_000 ? refuzBuget.n : 0, refusedSinceStart: refuzBuget.total, notarizationBehindSinceStart: refuzBuget.notarizare,
rotationWaitsForNotarization: !(cur.frunze.length < maxFrunze) && rotireaPierdeNenotarizat() },
lastCheckpoint: ultim ? { shard: cur.k, n: ultim.n, treeSize: ultim.treeSize, createdAt: ultim.createdAt, notarized: !!ultim.notarization, lastError: ultim.lastError } : null,
lastNotarized: notarizat ? { shard: notarizat.k, n: notarizat.n, treeSize: notarizat.treeSize, firstSeen: notarizat.notarization.firstSeen, txHash: notarizat.notarization.txHash || null } : null, sandbox: SANDBOX };
},
inchide() { try { const t = lacatTinut(fs.readFileSync(lacat, 'utf8'), { propriu: -1 }); if (t.pid === process.pid) fs.rmSync(lacat, { force: true }); } catch { /* deja scos */ } },
};
}
}
// ------------------------------------------------------------------------------------------------ linia de comanda (partea agentului)
class Folosire extends Error {}
async function main(argv) {
const [cmd, ...rest] = argv;
// un steag dat fara valoare (pus ultimul, sau urmat de alt steag) ori cu valoarea goala e folosire gresita (2), nu un steag nedat (revizia B3 a
// treia, R3-03: `verify-receipt --ledger ""`, sau `--ledger` pus ultimul, scotea in tacere legarea chitantei de registru)
const get = (f) => { const i = rest.indexOf(f); if (i < 0) return undefined; const v = rest[i + 1]; if (!(v !== undefined && v !== '' && !/^--[a-z]/.test(v)) /*PAZA-MARTOR:steag-fara-valoare*/) throw new Folosire(`${f} needs a value`); return v; };
// STEAGURILE CUNOSCUTE ale fiecarei comenzi (revizia B3 a patra, R4-05): argumentele vin in perechi steag + valoare; un argument care nu e un
// steag al comenzii (`--witness=<id>`, `--ledger=<l>`, o greseala de tastare ca `--ledgr`) e folosire gresita (2), nu un steag nedat, iar un steag
// dat de doua ori la fel, afara de --witness (repetabil). Pana atunci `verify-receipt --witness=<id>` iesea 0 pentru chitanta oricarui martor.
const STEAGURI = { request: ['--key', '--ledger', '--seq', '--previous', '--out'], submit: ['--key', '--ledger', '--url', '--seq', '--out'], receipt: ['--url', '--id', '--ledger', '--witness', '--out'],
'verify-receipt': ['--receipt', '--ledger', '--witness'], 'verify-equivocation': ['--proof'] };
if (Object.hasOwn(STEAGURI, cmd)) {
const vazute = new Set();
for (let i = 0; i < rest.length; i += 2) {
const s = String(rest[i]);
if (!(STEAGURI[cmd].includes(s) /*PAZA-MARTOR:steag-necunoscut*/)) throw new Folosire(`${cmd}: unknown argument ${JSON.stringify(s.slice(0, 60))} (its flags: ${STEAGURI[cmd].join(' ')}; the value follows its flag, never --flag=value)`);
if (!(!vazute.has(s) || s === '--witness') /*PAZA-MARTOR:steag-dublu*/) throw new Folosire(`${cmd}: ${s} is given twice`);
vazute.add(s);
}
}
// toate aparitiile unui steag repetabil, CU valorile goale (revizia B3 a doua, R2-12): `--witness ""` (o variabila necompletata), `--witness ,` sau
// un `--witness` final fara valoare dadeau o lista goala, adica "nefixat", si comanda iesea 0 pentru chitanta oricarui martor; acum sunt folosire
// gresita, ca in agent-cli.mjs
const toate = (f) => rest.flatMap((x, i) => (x === f ? (rest[i + 1] === undefined ? [''] : String(rest[i + 1]).split(',')) /*PAZA-MARTOR:steag-gol*/ : []));
const citeste = (f, ce) => { if (!f) throw new Folosire(`--${ce} is required`); try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { throw new Folosire(`cannot read ${ce} file ${f}: ${e.message}`); } };
const scrie = (o, f) => { const s = JSON.stringify(o, null, 1) + '\n'; if (f) fs.writeFileSync(f, s); else process.stdout.write(s); };
const identitate = () => { const k = get('--key'); if (!k) throw new Folosire('--key agent.key.pem is required'); return agentFromPrivateKeyPem(fs.readFileSync(k, 'utf8')); };
const baza = () => { const u = get('--url'); if (!u || !/^https?:\/\//.test(u)) throw new Folosire('--url <base URL of the witness, e.g. https://cloud.aere.network> is required'); return u.replace(/\/+$/, ''); };
const cere = cereJson;
if (cmd === 'request') {
const p = get('--previous'); const prev = p == null || p === 'none' ? null : Number(p);
const env = cerereMartor(citeste(get('--ledger'), 'ledger'), identitate(), { seq: get('--seq') != null ? Number(get('--seq')) : null, previous: prev });
scrie(env, get('--out')); if (get('--out')) console.error(`head seq ${env.statement.seq}, previous ${prev ?? 'none'}: ${env.statementHash}`); return 0;
}
if (cmd === 'submit') {
const ledger = citeste(get('--ledger'), 'ledger'); const id = identitate(); const U = baza();
const h = await cere(`${U}/v1/agents/witness/${ledger.agentId}/heads?session=${ledger.session}&from=0&limit=1`);
if (h.status !== 200 || !h.j) { console.error(`NOT MEASURED: the witness did not list the heads (${h.status || h.eroare})`); return 2; }
// regula (c) pe drumul "nimic nou" (revizia B3 a sasea, N3): `latest` vine din campul NESEMNAT al paginii, deci tipul fiecarei valori se judeca
// inainte de folosire: iesire 2, --out nescris, niciodata o exceptie. Pana atunci un {"toString":0} in seq sau in id arunca (iesire 1), iar un
// status {"toString":0} in starea capului scria --out si apoi arunca
const ultim = h.j.latest === undefined || h.j.latest === null ? null : h.j.latest;
if (!(ultim === null || (obiect(ultim) && Number.isSafeInteger(ultim.seq) && ultim.seq >= 0 && HASH_INTRARE.test(sir(ultim.hash)) && H32.test(sir(ultim.id)))) /*PAZA-MARTOR:nimic-nou-latest*/) { console.error('NOT MEASURED: the witness listed a latest head that is not {seq, hash, id} (a wrong form or wrong types): ask again later'); return 2; }
const s = get('--seq') != null ? Number(get('--seq')) : ledger.entries.length - 1;
// nimic nou de trimis (revizia B3, constatarea 7): martorul tine deja chiar intrarea ceruta; pana la reparatie iesea 1 ("REFUSED")
if (ultim && Array.isArray(ledger.entries)) {
const e = ledger.entries[ultim.seq];
if (ultim.seq === s && e && e.hash === ultim.hash) {
const r = await cere(`${U}/v1/agents/witness/${ultim.id}`);
if (!r.status) { console.error(`NOT MEASURED: ${r.eroare}`); return 2; }
// starea capului trebuie sa fie chiar starea lui (200, JSON): un 503, un 429 sau o pagina HTML nu e "nimic nou", si nu se scrie in --out
// drept starea capului (revizia B3 a treia, R3-05)
if (!(r.status === 200 && obiect(r.j) && r.j.id === ultim.id && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-nimic-nou*/) { console.error(`NOT MEASURED: the witness lists seq ${s} as its last head of this ledger, but did not give its state (HTTP ${r.status}${obiect(r.j) && sir(r.j.error) ? ': ' + r.j.error : ''}${r.status === 200 && obiect(r.j) && r.j.id === ultim.id ? '; no status' : ''}); ask again later`); return 2; }
scrie(r.j, get('--out')); console.error(`nothing new: the witness already holds seq ${s} of this ledger (${r.j.status}) ${ultim.id}`); return 0;
}
if (!e || e.hash !== ultim.hash) { console.error(`REFUSED before sending: the witness holds seq ${ultim.seq} with a hash ${e ? 'this ledger does not have there' : 'beyond the end of this ledger'}: this ledger is another branch, or it lost entries`); return 1; }
if (ultim.seq > s) { console.error(`REFUSED before sending: the witness already holds a newer head of this ledger (seq ${ultim.seq}) than the one asked (seq ${s}): a head older than the last one witnessed is refused`); return 1; }
}
let env; try { env = cerereMartor(ledger, id, { seq: s, previous: ultim ? { seq: ultim.seq, hash: ultim.hash } : null }); }
catch (e) { console.error(`REFUSED before sending: ${e.message}`); return 1; }
const r = await cere(`${U}/v1/agents/witness`, { method: 'POST', body: JSON.stringify(env) });
if (!r.status) { console.error(`NOT MEASURED: ${r.eroare}`); return 2; }
// un 200 sau 202 e un succes numai daca e chiar starea capului trimis (JSON, cu un id, sesiunea si pozitia lui; un 200 "deja vazut" poate purta
// id-ul capului tinut la aceeasi pozitie, cu alt moment declarat): o pagina HTML a unei margini sau a unui proxy nu dovedeste ca martorul a
// primit capul, deci NEMASURAT (2), si --out nu se scrie (revizia B3 a patra, R4-04)
if ((r.status === 200 || r.status === 202) && !(obiect(r.j) && H32.test(sir(r.j.id)) && r.j.session === env.statement.session && r.j.seq === env.statement.seq && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-trimis*/) { console.error(`NOT MEASURED: HTTP ${r.status}, but the answer is not the state of the head sent (${obiect(r.j) ? 'another id, session or position, or no status' : 'not JSON'}): ask the witness for ${env.statementHash} later`); return 2; }
scrie(r.j, get('--out')); console.error(`HTTP ${r.status}: ${obiect(r.j) ? sir(r.j.status) || sir(r.j.error) : 'not JSON'}${obiect(r.j) && sir(r.j.id) ? ' ' + r.j.id : ''}`);
return r.status === 200 || r.status === 202 ? 0 : r.status >= 500 || r.status === 429 ? 2 : 1;
}
if (cmd === 'receipt') {
const idc = String(get('--id') || ''); if (!H32.test(idc)) throw new Folosire('--id 0x<64 hex> (the statementHash of the head) is required');
// ce scrie receipt e ce va preda agentul in --anchors, iar o chitanta predata care nu tine pe registru sau cu semnatura martorului invalida e VINA
// registrului (agentul raspunde de ce preda). Deci raspunsul se scrie NUMAI daca e chitanta capului cerut (--id), a ACESTUI registru (--ledger:
// acelasi agent, politica, seq si hash, si predecesorul), cu semnatura martorului verificata (cu cel fixat prin --witness, daca e dat; altfel cu
// cheia pe care o numeste chitanta, si mesajul spune ca martorul nu e fixat); altfel 2 cu motivul numit, si nimic scris (revizia B3 a sasea, N2:
// pana atunci un proxy fara cheie la --url dadea agentului cinstit chitanta adevarata a capului altui agent, sau una cu semnatura rupta, si
// fisierul predat iesea 1 din vina adresei)
const ledger = citeste(get('--ledger'), 'ledger'); if (!obiect(ledger)) throw new Folosire('--ledger must be the ledger of the agent (a JSON object)');
const martori = toate('--witness'); if (martori.some((x) => !eWitnessId(x))) throw new Folosire('--witness aere-witness:<40 hex>[,...]');
const r = await cere(`${baza()}/v1/agents/witness/${idc}`);
if (!r.status) { console.error(`NOT MEASURED: ${r.eroare}`); return 2; }
// un martor sau o usa care nu poate raspunde acum (503, 429, orice 5xx) e NEMASURAT, ca la submit, nu un refuz (revizia B3 a doua, R2-11)
if (r.status >= 500 || r.status === 429 /*PAZA-MARTOR:cod-indisponibil*/) { console.error(`NOT MEASURED: HTTP ${r.status}: ${obiect(r.j) ? sir(r.j.error) : 'not JSON'} (the witness cannot answer now; ask again later)`); return 2; }
if (r.status !== 200) { console.error(`HTTP ${r.status}: ${obiect(r.j) ? sir(r.j.error) : 'not JSON'}`); return 1; }
// un 200 care nu e JSON (o margine sau un proxy cu o pagina HTML) e NEMASURAT, nu o exceptie iesita cu 1 (revizia B3 a treia, R3-05)
if (!(obiect(r.j) && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-ne-json*/) { console.error('NOT MEASURED: HTTP 200, but the answer is not the state of a head (not JSON, or no status): ask again later'); return 2; }
const plic = esteChitanta(r.j) ? chitantaDin(r.j) : null;
const capPrimit = plic && obiect(plic.witnessReceipt) && obiect(plic.witnessReceipt.head) ? plic.witnessReceipt.head.statementHash : null;
const l = plic && capPrimit === idc /*PAZA-MARTOR:receipt-id*/ ? verificaChitanta(plic, { ledger /*PAZA-MARTOR:receipt-registru*/, martori: martori.length ? martori : null /*PAZA-MARTOR:receipt-fixat*/ }) : null;
if (!(!!l && l.ok) /*PAZA-MARTOR:receipt-verificata*/) {
const motiv = !plic ? `the witness has it ${r.j.status.slice(0, 40)} and the answer carries no receipt${r.j.status === 'notarized' ? '' : ' yet (ask again after its next checkpoint)'}`
: capPrimit !== idc ? `the answer is the receipt of another head, not of ${idc}` : `the receipt does not hold here: [${l.rule}] ${l.error}`;
console.error(`NOT MEASURED: ${motiv}; nothing written${get('--out') ? ' to ' + get('--out') : ''}`); return 2;
}
scrie(r.j, get('--out'));
console.error(`status ${r.j.status}${r.j.status === 'notarized' ? '' : ' (not notarized yet: the chain cannot vouch for it until it is)'}; the receipt of seq ${l.head.seq} of this ledger, signed by ${l.checkpoint.witness}${l.witnessPinned ? ' (pinned)' : ' (NOT pinned: its signature was checked with the key the receipt names; pass --witness to pin the witness)'}`);
return r.j.status === 'notarized' ? 0 : 2;
}
if (cmd === 'verify-receipt') {
const martori = toate('--witness'); if (martori.some((x) => !eWitnessId(x))) throw new Folosire('--witness aere-witness:<40 hex>[,...]');
const x = citeste(get('--receipt'), 'receipt');
// raspunsul GET al unui cap inca `pending` (niciun cap de jurnal nu il acopera) nu e o chitanta GRESITA, e una care nu exista inca: NEMASURAT
if (obiect(x) && !esteChitanta(x) && x.status === 'pending' /*PAZA-MARTOR:cod-pending*/) { console.log(JSON.stringify({ ok: null, status: 'pending', id: x.id || null, note: 'not a receipt yet: no checkpoint covers this head; ask the witness again after its next checkpoint' }, null, 1)); return 2; }
const v = verificaChitanta(chitantaDin(x), { ledger: get('--ledger') ? citeste(get('--ledger'), 'ledger') : null, martori: martori.length ? martori : null });
console.log(JSON.stringify(v.ok ? { ok: true, seq: v.head.seq, checkpoint: v.checkpoint, witnessPinned: v.witnessPinned, receivedAt: v.receivedAt, note: 'local checks only: the time the chain certifies is judged by agent-cli.mjs verify --anchors' + (v.witnessPinned ? '' : '; the witness was not pinned (--witness)') } : { ok: false, rule: v.rule, error: v.error }, null, 1));
return v.ok ? 0 : 1;
}
if (cmd === 'verify-equivocation') { const v = verificaEchivocare(citeste(get('--proof'), 'proof')); console.log(JSON.stringify(v)); return v.ok ? 0 : 1; }
throw new Folosire('usage: martor-agenti.mjs request|submit|receipt|verify-receipt|verify-equivocation ... (see README.md)');
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main(process.argv.slice(2)).then((c) => { process.exitCode = c; }, (e) => { console.error(`agent-witness: ${e.message}`); process.exitCode = e instanceof Folosire ? 2 : 1; });
}