ai-trust, proof-of-ai, verifiable-data: the trust chain of an AI result, the AIP-23 envelope of an agent action, and a dataset whose answers are checkable without the data

ai-trust/ links six existing proofs (agent identity, model artifact attestation, host runtime record, policy and signed ledger,
Proof of AI envelope, on-chain witness) into one bundle judged by one verifier that checks only the links; 13/13 in the lab,
16/16 live on testnet 28001 (ledger head notarized). proof-of-ai/ is the AIP-23 envelope builder it uses. verifiable-data/
publishes append-only versions with an RFC 9162 root and a sorted index per field, range and equality queries proven correct
and complete, selective disclosure per field, availability by sampling, and the AIP-23 data envelope notarized (version 2 on
testnet 28001). agents/agent-ancora.mjs gains notarizeEnvelope({kind}), the general form of notarizeHead. Each README says what
is not built: zero-knowledge queries need a prover and are not here. No third party has reviewed any of this.
This commit is contained in:
Aere Network 2026-10-02 11:28:14 +03:00
parent f84a5e8c18
commit cf8c56792f
18 changed files with 1624 additions and 3 deletions

View File

@ -15,6 +15,9 @@ notarization command of the verification layer, and the agents' x402 wallet (EIP
| [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass | | [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass |
| [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again | | [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again |
| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, either holding the payment key for the owner or co-signing from a 2-of-2 contract wallet that neither the agent nor the owner can spend alone; both run on the public testnet with their evidence; and the chain as the witness of a ledger: a notarized head, read through the AIP-23 verifier under a post-quantum certified anchor, bounds entry times from below (a backdated entry is caught) | | [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, either holding the payment key for the owner or co-signing from a 2-of-2 contract wallet that neither the agent nor the owner can spend alone; both run on the public testnet with their evidence; and the chain as the witness of a ledger: a notarized head, read through the AIP-23 verifier under a post-quantum certified anchor, bounds entry times from below (a backdated entry is caught) |
| [`proof-of-ai/`](proof-of-ai/) | the AIP-23 envelope of an AI agent's action: model, version, weights digest, prompt, input, output, tools, agent, requester, as digests; verified by the same AIP-23 verifier as every other proof |
| [`verifiable-data/`](verifiable-data/) | a dataset whose every answer is checkable against a short commitment without the data: RFC 9162 commitments per version (append-only, consistency proofs), equality and range queries proven correct AND complete (contiguous run of a sorted index plus its two neighbours), hash-based selective disclosure of fields, availability sampling against the serving endpoint, and the version's AIP-23 envelope notarized and read back under a post-quantum certified anchor; not zero knowledge, and says so |
| [`ai-trust/`](ai-trust/) | the trust chain of an AI result: the agent's identity (agents/), the model artifact's attestation, the host's runtime record (verify-layer/), the policy and signed ledger, the Proof of AI envelope and the chain as witness, joined in one bundle whose verifier checks only the links between them |
| [`identity/`](identity/) | post-quantum credentials: an issuer signs claims (hybrid Ed25519 + ML-DSA-65, both required), the holder shows only the claims it picks (selective disclosure in the manner of SD-JWT, RFC 9901, in a format of its own), bound to the verifier's audience and nonce; delegation to a phone, an agent or a ten-minute session key that can only narrow; revocation and an issuer status list in the manner of W3C Bitstring Status List; all checked offline from the files; and compliance without surveillance: a verifier's policy (issuers, required claims) judged on a presentation, recorded as an AIP-23 envelope that carries no personal data; and the Travel Rule between VASPs, the IVMS101 data sealed for the receiving VASP with a hybrid X25519 + ML-KEM-768 key encapsulation, signed, bound to the transfer and acknowledged; and the same credentials as standard SD-JWT (IETF RFC 9901): issued with the issuer's Ed25519 key (EdDSA) or its ML-DSA-65 key, presented with a key binding JWT, and verified against the RFC's own example vectors (`identity-cli.mjs verify-sdjwt`, behind Aere Cloud's POST /v1/identity/sd-jwt/verify) | | [`identity/`](identity/) | post-quantum credentials: an issuer signs claims (hybrid Ed25519 + ML-DSA-65, both required), the holder shows only the claims it picks (selective disclosure in the manner of SD-JWT, RFC 9901, in a format of its own), bound to the verifier's audience and nonce; delegation to a phone, an agent or a ten-minute session key that can only narrow; revocation and an issuer status list in the manner of W3C Bitstring Status List; all checked offline from the files; and compliance without surveillance: a verifier's policy (issuers, required claims) judged on a presentation, recorded as an AIP-23 envelope that carries no personal data; and the Travel Rule between VASPs, the IVMS101 data sealed for the receiving VASP with a hybrid X25519 + ML-KEM-768 key encapsulation, signed, bound to the transfer and acknowledged; and the same credentials as standard SD-JWT (IETF RFC 9901): issued with the issuer's Ed25519 key (EdDSA) or its ML-DSA-65 key, presented with a key binding JWT, and verified against the RFC's own example vectors (`identity-cli.mjs verify-sdjwt`, behind Aere Cloud's POST /v1/identity/sd-jwt/verify) |
Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them. Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them.
@ -37,6 +40,9 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
| control-plane | planner 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) | | control-plane | planner 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) |
| identity | 44/44 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 19/19 (`node proba-travel-rule.mjs`), SD-JWT 23/23 (`node proba-sdjwt.mjs`, the RFC 9901 vectors included), measured 2026-09-30 | 49/49 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 19/19 (`node control-negativ-travel-rule.mjs`); SD-JWT 28/28 (`node control-negativ-sdjwt.mjs`) | | identity | 44/44 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 19/19 (`node proba-travel-rule.mjs`), SD-JWT 23/23 (`node proba-sdjwt.mjs`, the RFC 9901 vectors included), measured 2026-09-30 | 49/49 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 19/19 (`node control-negativ-travel-rule.mjs`); SD-JWT 28/28 (`node control-negativ-sdjwt.mjs`) |
| agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 51/51, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`x402/proba-x402-testnet.mjs`, `x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository | | agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 51/51, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`x402/proba-x402-testnet.mjs`, `x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository |
| proof-of-ai | 6/6 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-proof-of-ai.mjs`); without it the probe reports NEMASURAT and exits 2 | one negative control inside the test (an envelope touched after its hash: INVALID) |
| verifiable-data | 22/22 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-verifiable-data.mjs`: honest answers VALID, nine forged ones INVALID for their named reason, availability sampling on a local server; the envelope row is NEMASURAT without the verifier, exit 2); 24/24 with `--viu` on testnet 28001 on 2026-10-02 (a version notarized with a test key and read back under a post-quantum certified anchor; evidence in `dovezi/`) | 5/5 (`node proba-verifiable-data.mjs --control-negativ`: field commitment, index order, completeness, consistency, sampling, each planted out on a copy and each forged fixture then accepted) |
| ai-trust | 13/13 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-lant-de-incredere.mjs`: the whole chain on a committed fixture, then ten links broken on purpose, each refused for its named reason); without the verifier it exits 2 | 8/8 (`node proba-lant-de-incredere.mjs --control-negativ`) |
Code comments, most function and variable names (also many exported between the files of a component), test names and control Code comments, most function and variable names (also many exported between the files of a component), test names and control
messages are in Romanian, and so are the two command words of the KMS HSM tool (explained in its README). Error codes, error messages are in Romanian, and so are the two command words of the KMS HSM tool (explained in its README). Error codes, error
@ -45,4 +51,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ...
## Licence ## Licence
MIT, see [LICENSE](LICENSE). Files: 158 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 12, proof-kinds 3, control-plane 19, agents 39, identity 15, readiness 4). MIT, see [LICENSE](LICENSE). Files: 174 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 12, proof-kinds 3, control-plane 19, agents 39, identity 15, proof-of-ai 3, ai-trust 6, verifiable-data 7, readiness 4).

View File

@ -67,10 +67,16 @@ export function cheiaDinFisier(f) {
} }
/** Pune statementHash-ul capului pe AereNotary. Lantul se citeste de pe RPC, nu se crede din argument; cheia din fisier, netiparita. */ /** Pune statementHash-ul capului pe AereNotary. Lantul se citeste de pe RPC, nu se crede din argument; cheia din fisier, netiparita. */
export async function notarizeHead({ envelope, rpc, keyFile, ethers, notary = null, confirmMainnet = process.env.AERE_CONFIRM_MAINNET === 'yes' }) { export function notarizeHead(o) { return notarizeEnvelope({ ...o, kind: HEAD_KIND }); }
/**
* Pune statementHash-ul unui plic AIP-23 de felul `kind` pe AereNotary (2026-10-02: forma generala a lui notarizeHead, ca datele
* verificabile sa notarizeze acelasi fel, din acelasi loc; fara `kind`, numai capul de registru). Aceleasi garzi: lantul de pe RPC,
* refuz pe 2800 fara AERE_CONFIRM_MAINNET, cheia din fisier si niciodata tiparita.
*/
export async function notarizeEnvelope({ envelope, kind = HEAD_KIND, rpc, keyFile, ethers, notary = null, confirmMainnet = process.env.AERE_CONFIRM_MAINNET === 'yes' }) {
const taie = (s) => String(s ?? '').replace(/(0x)?[0-9a-fA-F]{40,}/g, '<hex>').slice(0, 300); const taie = (s) => String(s ?? '').replace(/(0x)?[0-9a-fA-F]{40,}/g, '<hex>').slice(0, 300);
const st = envelope && envelope.statement; const st = envelope && envelope.statement;
if (!st || envelope.kind !== HEAD_KIND + '-attestation' || st.kind !== HEAD_KIND || !H32.test(String(envelope.statementHash))) return { ok: false, cod: 2, motiv: `REFUSED: not an ${HEAD_KIND} attestation with a 32-byte statementHash` }; if (!st || envelope.kind !== kind + '-attestation' || st.kind !== kind || !H32.test(String(envelope.statementHash))) return { ok: false, cod: 2, motiv: `REFUSED: not an ${kind} attestation with a 32-byte statementHash` };
if (sha(JSON.stringify(st)) !== envelope.statementHash) return { ok: false, cod: 1, motiv: 'REFUSED: statementHash does not match the statement (modified envelope)' }; if (sha(JSON.stringify(st)) !== envelope.statementHash) return { ok: false, cod: 1, motiv: 'REFUSED: statementHash does not match the statement (modified envelope)' };
try { try {
const provider = new ethers.JsonRpcProvider(rpc); const provider = new ethers.JsonRpcProvider(rpc);

49
ai-trust/README.md Normal file
View File

@ -0,0 +1,49 @@
# AERE AI Trust Chain
Trust infrastructure for an AI result, built only from pieces that already exist and are verified on their own: the agent's
post-quantum identity, the attestation of the model artifact, the host's runtime record, the agent's policy and signed ledger, the
Proof of AI envelope of the output, and the chain as the witness of time. This component adds **no new proof**: it adds the
**links** between those proofs, in one bundle, judged by one verifier that calls the existing verifiers and checks only that the
digests, the identities and the times agree.
| facet | what proves it | the link checked here |
|---|---|---|
| identity | `agents/` (agentId derived from an ML-DSA-65 key) | the ledger, the bundle and the output name the id the key derives |
| version | Proof of Software attestation of the model artifact (hybrid signature) | the output's `model.weightsSha256` is an artifact of the attestation; the version labels agree |
| runtime | the verify-layer sidecar's `runtime` record in its audit log (chain, head) | the host ran that same artifact (`matches`), before the output; the record is an entry of an intact log that continues the attested head |
| permissions | the agent's policy and ledger (`agents/`) | the ledger verifies; an **allowed** entry links the output by its provenance hash; the entry's time matches the output's |
| provenance | the Proof of AI envelope (AIP-23; digests only) | the envelope verifies with the AIP-23 reference verifier; notarized, its finality is read under a post-quantum certified anchor |
| witness | the ledger head notarized on Aere (`agents/agent-ancora.mjs`) | the heads read back through the reference verifier become the ledger's time anchors; without them every time is the key holder's statement |
Verdict: `VALID` when every present level PASSED **and** the witness PASSED; `INVALID` when a level FAILED; `PARTIAL` otherwise
(an UNMEASURED level, or no witness). Every level reports what it rests on. A verdict says that the bytes link and who vouches for
each link. It does not say the model is good, the host is honest, or the output is true.
```
import { construieste, verifica } from './lant-de-incredere.mjs';
const bundle = construieste({ ledger, policy: { policy, policyHash }, proofOfAi, software: { attestation }, runtime: { record, auditLog, head }, witness: { heads: [notarizedHead], chainId: 28001 } });
const v = await verifica(bundle, { rpc: 'https://testnet-rpc.aere.network', chain: 28001 }); // rpc: only for notarized pieces
```
## What it is not
- Not an attestation of execution in a TEE: the runtime record is the **host's declaration**, as the sidecar says; what is checkable
without trusting the host is that the declaration was made before the output and sits in a history published outside the host.
- Not a judgement of the signer of the model attestation: who holds the signing keys is `pos.mjs verify --signer`'s question.
- Not a judgement of the content: prompts, inputs and outputs are digests; the verifier proves that bytes with these hashes were
named, not what they say.
## Probes
```
AERE_VERIFY_PROOF=<verify-proof.mjs from aere-node> node proba-lant-de-incredere.mjs # no network: 13 rows (the chain, then every link broken on purpose)
AERE_VERIFY_PROOF=... node proba-lant-de-incredere.mjs --control-negativ # 8 plantings in a copy of the verifier; each must turn its fixture red
AERE_VERIFY_PROOF=... AERE_NOTARY_KEY_FILE=<key> AERE_ETHERS_DIR=<dir with node_modules/ethers> node proba-lant-de-incredere.mjs --viu
# plus the witness on testnet 28001 (a notarization paid with a test key)
```
The probes use the real children beside this component: the verify-layer sidecar (record, attest-head), the agents (ledger, policy,
anchor), proof-of-ai, and the AIP-23 reference verifier named by `AERE_VERIFY_PROOF` (without it the probe exits 2, NEMASURAT). The
model artifact and its Proof of Software attestation are a committed fixture (`fixturi/`: 4 KB of random bytes attested with
laboratory keys by `pos.mjs`, which is not part of this component; the digest is recomputed from the bytes, never read from the
attestation). `--viu` writes its evidence in `dovezi/`.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,213 @@
// lant-de-incredere.mjs - AERE AI Trust Chain (roadmap master point 21, track B, 2026-10-02): the LINKS between the six facets of
// trust in an AI result, each of which already exists as its own proof, joined into one bundle that one verifier judges:
// identity the agent's post-quantum identity (agents/: agentId derived from its ML-DSA-65 key)
// version what model ran: a Proof of Software attestation of the model artifact (weights or runtime), hybrid-signed
// runtime that it ran: the host's runtime record (verify-layer sidecar, kind runtime) in its audit log, before the output
// permissions that it was allowed: the agent's policy and signed ledger (agents/), the entry that links the output
// provenance what came out: the Proof of AI envelope (model, prompt, input, output, tools, agent, requester: digests only)
// witness when: the ledger head notarized on Aere, read under a post-quantum certified anchor (agents/agent-ancora.mjs)
// Nothing here re-implements a child verifier: every envelope is judged by the AIP-23 reference verifier (verify-proof.mjs), the
// ledger by verifyLedger, the audit log by the sidecar's own chain check. This module checks only the LINKS between them (equal
// digests, equal identities, the order of times) and reports each level in four states (PASSED, FAILED, UNMEASURED, ABSENT) with
// what it rests on. A verdict says that the bytes link and who vouches for each; it does not say the model is good, the host is
// honest, or the output is true.
// VALID every present level PASSED and the witness PASSED (a time somebody other than the agent fixed)
// INVALID a level FAILED
// PARTIAL otherwise (a level UNMEASURED, or no witness)
// Node.js 24 only; the children are in this repository (agents, verify-layer, proof-of-software, proof-of-ai, verify-proof.mjs).
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { agentIdFromKey, verifyLedger, canonical } from '../agents/agent-ledger.mjs';
import { hashPolicy } from '../agents/agent-policy.mjs';
import { verificaJurnal, verificaFataDeCap } from '../verify-layer/sidecar.mjs';
import { anchorsFromNotarizedHeads } from '../agents/agent-ancora.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
export const FORMAT = 'aere-ai-trust-chain-1';
export const LEVELS = ['form', 'identity', 'version', 'runtime', 'permissions', 'provenance', 'witness'];
export const TOLERANCE_S = 300;
export const VERIFY_PROOF_DEFAULT = process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', '..', 'verificator-falcon', 'verify-proof.mjs');
const H32 = /^0x[0-9a-f]{64}$/;
const sha = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex');
const lower = (x) => (typeof x === 'string' ? x.toLowerCase() : x);
const lvl = (state, extra = {}) => ({ state, ...extra });
/** the hash a ledger entry stores for its provenance: the ledger's sha over the canonical JSON (with or without 0x, both forms matched) */
export const provenanceHash = (envelope) => sha(Buffer.from(canonical(envelope), 'utf8'));
const sameHash = (a, b) => String(a || '').replace(/^0x/, '').toLowerCase() === String(b || '').replace(/^0x/, '').toLowerCase() && String(a || '').length > 0;
/** build the bundle from the pieces (files already parsed); the links are not computed here, the verifier recomputes them */
export function construieste({ ledger, policy, proofOfAi, software = null, runtime = null, witness = null, createdAt = new Date().toISOString() }) {
if (!ledger || !Array.isArray(ledger.entries) || !ledger.publicKeyPem || !ledger.agentId) throw new Error('a ledger (agent-ledger export) is required');
if (!policy) throw new Error('the policy is required');
if (!proofOfAi || !proofOfAi.statement || !H32.test(String(proofOfAi.statementHash || ''))) throw new Error('a Proof of AI envelope is required');
const b = {
format: FORMAT, createdAt,
identity: { agentId: ledger.agentId, publicKeyPem: ledger.publicKeyPem },
permissions: { policy: policy.policy || policy, policyHash: policy.policyHash || null, ledger, revocations: [] },
provenance: { proofOfAi },
version: software ? { attestation: software.attestation || software, artifactName: software.artifactName || null } : null,
runtime: runtime ? { record: runtime.record, auditLog: runtime.auditLog || null, head: runtime.head || null } : null,
witness: witness ? { heads: witness.heads || [], chainId: witness.chainId || null } : null,
};
if (policy.revocations) b.permissions.revocations = policy.revocations;
return b;
}
/** run the AIP-23 reference verifier on an envelope; returns its JSON (levels as a map) or null */
export function judecatorAip23(verifyProof, envelope, { rpc = null, chain = null } = {}) {
return new Promise((ok) => {
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-ai-trust-')); const f = path.join(d, 'plic.json'); fs.writeFileSync(f, JSON.stringify(envelope));
const args = [verifyProof, f, '--json', ...(rpc ? ['--rpc', rpc] : []), ...(chain ? ['--chain', String(chain)] : [])];
const c = spawn(process.execPath, args); let out = '';
c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', () => {});
const t = setTimeout(() => c.kill(), 300000);
const gata = (j) => { clearTimeout(t); fs.rmSync(d, { recursive: true, force: true }); ok(j); };
c.on('error', () => gata(null));
c.on('close', () => { try { const j = JSON.parse(out); ok(gata({ verdict: j.verdict, levels: Object.fromEntries((j.levels || []).map((l) => [l.level, l])) })); } catch { gata(null); } });
});
}
const parsed = (j, name) => (j && j.levels && j.levels[name]) ? j.levels[name] : { state: 'UNMEASURED', detail: 'the reference verifier gave no ' + name + ' level' };
const envelopeOk = (j) => j && ['form', 'integrity'].every((n) => parsed(j, n).state === 'PASSED') && (parsed(j, 'signature').state !== 'FAILED');
export async function verifica(bundle, { verifyProof = VERIFY_PROOF_DEFAULT, rpc = null, chain = null, now = Math.floor(Date.now() / 1000) } = {}) {
const levels = Object.fromEntries(LEVELS.map((l) => [l, lvl('ABSENT')]));
const assumptions = {};
const fail = (l, reason, detail) => { levels[l] = lvl('FAILED', { reason, detail }); };
const unm = (l, reason, detail) => { levels[l] = lvl('UNMEASURED', { reason, detail }); };
// 1. form
const isObj = (x) => x !== null && typeof x === 'object' && !Array.isArray(x);
if (!isObj(bundle) || bundle.format !== FORMAT || !isObj(bundle.identity) || !isObj(bundle.permissions) || !isObj(bundle.provenance) || !isObj(bundle.permissions.ledger) || !isObj(bundle.provenance.proofOfAi)) {
fail('form', 'form', 'not an ' + FORMAT + ' bundle with identity, permissions.ledger and provenance.proofOfAi');
return verdict(levels, assumptions);
}
levels.form = lvl('PASSED');
const poa = bundle.provenance.proofOfAi, st = poa.statement || {}, ledger = bundle.permissions.ledger;
const outputAt = Math.floor(Date.parse(st.createdAt || '') / 1000);
// 2. identity: the id is derived from the key, and the ledger, the bundle and the output name the same agent
try {
const derived = agentIdFromKey(crypto.createPublicKey(bundle.identity.publicKeyPem));
const same = derived === bundle.identity.agentId && derived === ledger.agentId && derived === st.agentId /*PAZA:identity*/;
if (!same) fail('identity', 'identity-mismatch', `key derives ${derived}; bundle says ${bundle.identity.agentId}, ledger ${ledger.agentId}, output ${st.agentId}`);
else levels.identity = lvl('PASSED', { detail: `${derived} (ML-DSA-65 key; the id is derived from the key, nobody can claim it without the key)` });
} catch (e) { fail('identity', 'identity-key', 'the public key cannot be read: ' + String(e.message).slice(0, 80)); }
assumptions.identity = ['ML-DSA-65 unbroken; the agent key holder is the agent'];
// 3. version: the model artifact attested (Proof of Software), and the output names that very artifact
let attestedSha = null;
if (bundle.version && bundle.version.attestation) {
const att = bundle.version.attestation;
const j = await judecatorAip23(verifyProof, att, { rpc: att.notarization ? rpc : null, chain });
if (!j) unm('version', 'version-verifier', 'the AIP-23 reference verifier did not answer for the attestation');
else if (!envelopeOk(j)) fail('version', 'version-attestation', 'the Proof of Software attestation does not verify: ' + ['form', 'integrity', 'signature'].map((n) => n + ' ' + parsed(j, n).state).join(', '));
else {
const arts = (att.statement && att.statement.artifacts) || [];
const w = lower(st.model && st.model.weightsSha256);
const hit = arts.find((a) => lower(a.sha256) === w);
const ok = !!(w && hit) /*PAZA:version*/;
if (!w) unm('version', 'model-weights-not-named', 'the Proof of AI names no model.weightsSha256, so no artifact can be matched');
else if (!ok) fail('version', 'version-artifact-mismatch', `the output names model weights ${w.slice(0, 14)}.., the attestation covers ${arts.map((a) => a.name + ' ' + String(a.sha256).slice(0, 14) + '..').join(', ') || 'no artifact'}`);
else {
const v = att.statement.subject && att.statement.subject.version; const mv = st.model && st.model.version;
if (v && mv && String(v) !== String(mv)) fail('version', 'version-label-mismatch', `the output says model version ${mv}, the attestation ${v}`);
else { const h = hit || arts[0] || { name: '?', sha256: w }; attestedSha = lower(h.sha256); levels.version = lvl('PASSED', { detail: `${h.name} ${String(h.sha256).slice(0, 18)}.. attested as ${att.statement.subject ? att.statement.subject.name + '@' + att.statement.subject.version : '?'}; signature ${parsed(j, 'signature').state}${att.notarization ? ', finality ' + parsed(j, 'finality').state : ''}` }); }
}
}
assumptions.version = ['the attestation proves what its signing keys attested (who holds them is a separate judgement: pos.mjs verify --signer); the artifact digest is what links it to the output'];
}
// 4. runtime: the host declared that THIS artifact ran, in its audit log, before the output
if (bundle.runtime && bundle.runtime.record) {
const rec = bundle.runtime.record, rs = rec.statement || {};
const j = await judecatorAip23(verifyProof, rec, { rpc: rec.notarization ? rpc : null, chain });
if (!j) unm('runtime', 'runtime-verifier', 'the AIP-23 reference verifier did not answer for the runtime record');
else if (!envelopeOk(j) || rec.kind !== 'aere-proof-of-runtime-attestation') fail('runtime', 'runtime-record', 'the runtime record is not a verified aere-proof-of-runtime-attestation');
else {
const target = attestedSha || lower(st.model && st.model.weightsSha256);
const ok = !!(target && lower(rs.artifactSha256) === target && rs.matches === true) /*PAZA:runtime*/;
if (!target) unm('runtime', 'runtime-no-artifact', 'no attested artifact to match the runtime record against');
else if (!ok) fail('runtime', 'runtime-artifact-mismatch', `the host ran ${String(rs.artifactSha256).slice(0, 14)}.. (matches ${rs.matches}), the output names ${target.slice(0, 14)}..`);
else {
const runAt = Math.floor(Date.parse(rs.createdAt || '') / 1000);
const ordered = Number.isFinite(runAt) && Number.isFinite(outputAt) && runAt <= outputAt + TOLERANCE_S /*PAZA:order*/;
if (!ordered) fail('runtime', 'runtime-after-output', `the runtime record is dated ${rs.createdAt}, the output ${st.createdAt}`);
else {
let note = `host ${rs.host} declared the artifact running at ${rs.createdAt}`;
const log = bundle.runtime.auditLog;
if (Array.isArray(log)) {
const v = verificaJurnal(log);
if (!v.ok) { fail('runtime', 'audit-log-broken', `the audit log chain breaks at seq ${v.rupt}: ${v.motiv}`); }
else if (!log.some((e) => e.proof && lower(e.proof.statementHash) === lower(rec.statementHash)) /*PAZA:audit*/) fail('runtime', 'runtime-not-in-audit-log', 'the runtime record is not an entry of the audit log given');
else {
note += `; in the host's audit log (${log.length} entries, chain intact)`;
if (bundle.runtime.head) {
const c = verificaFataDeCap(log, bundle.runtime.head);
if (!c.ok) fail('runtime', 'audit-head', 'the log does not continue the attested head: ' + (c.motiv || ''));
else { note += `; continues the attested head (count ${bundle.runtime.head.statement && bundle.runtime.head.statement.count})`; if (bundle.runtime.head.notarization && rpc) { const hj = await judecatorAip23(verifyProof, bundle.runtime.head, { rpc, chain }); note += `; head finality ${hj ? parsed(hj, 'finality').state : 'UNMEASURED'}`; } }
}
}
} else note += '; no audit log given (the record stands alone)';
if (levels.runtime.state !== 'FAILED') levels.runtime = lvl('PASSED', { detail: note });
}
}
}
assumptions.runtime = ['the runtime record is the HOST\'s declaration (the sidecar reads the export the operator makes); it proves what the host said ran, not that it did; the audit log has no key and is judged against a head published outside the host'];
}
// 5. permissions: the ledger verifies under its policy, the output is an entry of it, allowed, at a matching time
let anchors = [], witnessNote = null;
if (bundle.witness && Array.isArray(bundle.witness.heads) && bundle.witness.heads.length) {
if (!rpc) { unm('witness', 'witness-needs-rpc', 'the notarized heads can be read only through an endpoint of the chain (--rpc)'); }
else {
const a = await anchorsFromNotarizedHeads(ledger, bundle.witness.heads, { verifyProof, rpc, chain: chain || bundle.witness.chainId || null });
if (a.stare === 'OK' && a.anchors.length) { anchors = a.anchors; witnessNote = a.randuri.map((r) => r.detail).join(' | '); }
else if (a.stare === 'FAILED') fail('witness', 'witness-head', a.randuri.filter((r) => !r.pass).map((r) => r.detail).join(' | '));
else unm('witness', 'witness-unmeasured', a.randuri.map((r) => r.detail).join(' | ') || 'no anchor could be read');
}
}
try {
const { policy, policyHash } = hashPolicy(bundle.permissions.policy);
if (bundle.permissions.policyHash && lower(bundle.permissions.policyHash) !== policyHash) fail('permissions', 'policy-hash', 'the bundle pins another policy hash than the policy given');
else {
const v = verifyLedger(ledger, { policy, policyHash, revocations: bundle.permissions.revocations || [], anchors, maxTime: now + TOLERANCE_S });
if (!v.ok) fail('permissions', 'ledger-invalid', String(v.error || '').slice(0, 200));
else {
const ph = provenanceHash(poa);
const entry = ledger.entries.find((e) => e.body && sameHash(e.body.provenance, ph)) /*PAZA:output-in-ledger*/;
if (!entry) fail('permissions', 'output-not-in-ledger', 'no ledger entry links this Proof of AI envelope (provenance hash ' + ph.slice(0, 14) + '..)');
else if (!(entry.body.decision && entry.body.decision.allowed === true) /*PAZA:allowed*/) fail('permissions', 'action-refused', `the ledger entry seq ${entry.seq} was refused: ${entry.body.decision && entry.body.decision.reason}`);
else if (!(Number.isFinite(outputAt) && Math.abs(Number(entry.body.at) - outputAt) <= TOLERANCE_S)) fail('permissions', 'ledger-time-mismatch', `the ledger entry is at ${entry.body.at}, the output at ${outputAt} (${st.createdAt})`);
else levels.permissions = lvl('PASSED', { detail: `entry seq ${entry.seq} (${entry.body.action && entry.body.action.kind}${entry.body.action && entry.body.action.tool ? ' ' + entry.body.action.tool : ''}) allowed under policy ${policyHash.slice(0, 14)}..; ${ledger.entries.length} entries verified, revocations ${v.revocations ? v.revocations.given : 0}, time ${v.time ? v.time.lowerBound : '?'}` });
if (anchors.length && levels.witness.state === 'ABSENT') levels.witness = lvl('PASSED', { detail: `${anchors.length} anchor(s) from notarized heads under a post-quantum certified state: ${witnessNote}` });
}
}
} catch (e) { fail('permissions', 'policy', String(e.message).slice(0, 160)); }
assumptions.permissions = ['the policy given is the one pinned (its hash is recomputed); revocations come from the owner, outside the ledger, and only those given are judged'];
// 6. provenance: the Proof of AI envelope itself
{
const j = await judecatorAip23(verifyProof, poa, { rpc: poa.notarization ? rpc : null, chain });
if (!j) unm('provenance', 'provenance-verifier', 'the AIP-23 reference verifier did not answer');
else if (poa.kind !== 'aere-proof-of-ai-attestation' || !envelopeOk(j)) fail('provenance', 'provenance-envelope', 'the Proof of AI envelope does not verify: ' + ['form', 'integrity', 'signature'].map((n) => n + ' ' + parsed(j, n).state).join(', '));
else if (poa.notarization && !rpc) unm('provenance', 'provenance-finality-needs-rpc', 'the envelope claims a notarization that only an endpoint can confirm');
else if (poa.notarization && parsed(j, 'finality').state !== 'PASSED') { const f = parsed(j, 'finality'); (f.state === 'FAILED' ? fail : unm)('provenance', 'provenance-finality', String(f.detail).slice(0, 200)); }
else levels.provenance = lvl('PASSED', { detail: `model ${st.model && st.model.name}@${st.model && st.model.version}, prompt ${String(st.promptHash).slice(0, 12)}.., output ${String(st.outputHash).slice(0, 12)}.., tools [${(st.tools || []).join(', ')}], requester ${String(st.requesterHash).slice(0, 12)}..; signature ${parsed(j, 'signature').state}${poa.notarization ? ', finality ' + parsed(j, 'finality').state : ''}` });
assumptions.provenance = ['digests only: the prompt, input, output and requester are proven to exist as the bytes whose hashes these are, not shown; the envelope\'s time is its producer\'s statement unless notarized'];
}
if (levels.witness.state !== 'ABSENT') assumptions.witness = ['the time is the chain\'s (the notary records the block time; on 28001 and 2800 every validator is run by the Foundation): whoever notarizes can only notarize late'];
else assumptions.witness = ['no witness: every time in this bundle is the statement of whoever holds the keys'];
return verdict(levels, assumptions);
}
function verdict(levels, assumptions) {
const failed = LEVELS.find((l) => levels[l].state === 'FAILED');
let v, reason = null;
if (failed) { v = 'INVALID'; reason = levels[failed].reason; }
else {
const unmL = LEVELS.find((l) => levels[l].state === 'UNMEASURED');
if (unmL) { v = 'PARTIAL'; reason = levels[unmL].reason; }
else if (levels.witness.state !== 'PASSED') { v = 'PARTIAL'; reason = 'unwitnessed'; }
else v = 'VALID';
}
return { verdict: v, reason, levels: Object.fromEntries(LEVELS.map((l) => [l, levels[l].state])), details: levels, assumptions,
notice: 'A verdict says that the bytes link and who vouches for each link. It does not say the model is good, the host is honest, or the output is true.' };
}

View File

@ -0,0 +1,162 @@
#!/usr/bin/env node
// proba-lant-de-incredere.mjs: the AI trust chain end to end in the LABORATORY, with the real children (Proof of Software attest,
// the verify-layer sidecar, the agent ledger, Proof of AI, the AIP-23 reference verifier), then the fixtures (each link broken on
// purpose, refused for its named reason), then the plantings (--control-negativ: a copy of the verifier with one check removed must
// turn exactly its fixture ROSU). --viu adds the witness: the ledger head notarized on testnet 28001 with the L1 test key, read back
// under a post-quantum certified anchor, and the bundle judged VALID with --rpc.
// node proba-lant-de-incredere.mjs [--viu] [--control-negativ]
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { newAgentIdentity, openLedger } from '../agents/agent-ledger.mjs';
import { definePolicy } from '../agents/agent-policy.mjs';
import { buildProofOfAi } from '../proof-of-ai/proof-of-ai.mjs';
import { ledgerHeadEnvelope, notarizeHead, cheiaDinFisier } from '../agents/agent-ancora.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const RAD = path.resolve(AICI, '..', '..');
// the children: the sidecar lives beside this component (public copy: ../verify-layer/, rewritten at staging); the AIP-23 reference
// verifier comes from AERE_VERIFY_PROOF (public copy, as for agents/) or from the development repository; without it: NEMASURAT (2)
const SIDECAR = fileURLToPath(new URL('../verify-layer/sidecar.mjs', import.meta.url));
const VERIFY_PROOF = process.env.AERE_VERIFY_PROOF || path.join(RAD, 'verificator-falcon', 'verify-proof.mjs');
const RPC_28001 = process.env.AERE_TESTNET_RPC || 'https://testnet-rpc.aere.network';
// --viu: the notarization key (gas only, a test key) and the folder whose node_modules hold ethers
const CHEIE = process.env.AERE_NOTARY_KEY_FILE || path.join(RAD, 'tools', 'aere-quantum-shield', 'dovezi-l1', '.chei', 'cheie-de-proba-28001.key');
const ETHERS_DIR = process.env.AERE_ETHERS_DIR || path.join(RAD, 'contracts');
// the model artifact and its Proof of Software attestation are a COMMITTED fixture (fixturi/: 4 KB of random bytes attested with
// laboratory keys by tools/proof-of-software/pos.mjs, which is not part of this component); regenerate with
// node ../proof-of-software/pos.mjs keygen --out k.json && node ../proof-of-software/pos.mjs attest --out fixturi/atestare-model.json --name aere-model-proba --version 1.0.0 --keys k.json fixturi/model-weights.bin
const FIXTURI = path.join(AICI, 'fixturi');
if (!fs.existsSync(VERIFY_PROOF)) { console.log(`NEMASURAT: verificatorul AIP-23 nu e la ${VERIFY_PROOF}; AERE_VERIFY_PROOF=<verify-proof.mjs>`); process.exit(2); }
const sha = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex');
const run = (args, cwd) => { const r = spawnSync(process.execPath, args, { cwd, encoding: 'utf8', timeout: 180000 }); if (r.status !== 0) throw new Error(`${path.basename(args[0])} ${args[1]} exited ${r.status}: ${(r.stdout + r.stderr).slice(-300)}`); return r.stdout; };
const rehash = (env) => ({ ...env, statementHash: sha(Buffer.from(JSON.stringify(env.statement), 'utf8')) });
const clone = (x) => JSON.parse(JSON.stringify(x));
/** the laboratory pieces: a model artifact attested, a host that ran it, an agent allowed to use it, an output */
export function laborator({ toolAllowed = true } = {}) {
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-ai-trust-lab-'));
// version: the committed fixture (the artifact and its hybrid-signed Proof of Software attestation); the digest is recomputed here,
// never read from the attestation, so a fixture whose artifact and attestation drifted apart is caught at P1
const art = path.join(T, 'model-weights.bin'); fs.copyFileSync(path.join(FIXTURI, 'model-weights.bin'), art); const artSha = sha(fs.readFileSync(art));
const attestation = JSON.parse(fs.readFileSync(path.join(FIXTURI, 'atestare-model.json'), 'utf8'));
// runtime: the host declares the artifact running, in its audit log, 60 s before the output
const t0 = Math.floor(Date.now() / 1000) - 120, t1 = t0 + 60;
const iso = (t) => new Date(t * 1000).toISOString();
run([SIDECAR, 'record', '--kind', 'runtime', '--artifact', art, '--attested', artSha, '--host', 'lab-probe', '--log', path.join(T, 'audit.log'), '--at', iso(t0)], T);
run([SIDECAR, 'attest-head', '--log', path.join(T, 'audit.log'), '--host', 'lab-probe', '--out', path.join(T, 'head.json')], T);
const auditLog = fs.readFileSync(path.join(T, 'audit.log'), 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
const record = auditLog[0].proof; const head = JSON.parse(fs.readFileSync(path.join(T, 'head.json'), 'utf8'));
// permissions + identity: an agent allowed (or not) to call the model tool
const identity = newAgentIdentity();
const { policy, policyHash } = definePolicy({ agentId: identity.agentId, tools: toolAllowed ? ['model:aere-model-proba'] : ['model:other'] });
const ledger = openLedger({ identity, policy, policyHash, now: () => t1 });
// provenance: the output, naming the attested artifact and the agent
const proofOfAi = buildProofOfAi({ model: { name: 'aere-model-proba', version: '1.0.0', weightsSha256: artSha }, prompt: 'summarise document X', input: 'document X, 12 kB', output: 'a summary of document X', tools: ['model:aere-model-proba'], agentId: identity.agentId, requester: 'tester', createdAt: iso(t1) });
const r = ledger.record({ kind: 'tool', tool: 'model:aere-model-proba', args: { promptHash: proofOfAi.statement.promptHash } }, { provenance: proofOfAi, at: t1 });
return { T, art, artSha, attestation, record, auditLog, head, identity, policy, policyHash, ledger, proofOfAi, allowed: r.allowed, t0, t1 };
}
export async function proba(mod, cer, { viu = false } = {}) {
const L = laborator();
const bundle = mod.construieste({ ledger: L.ledger.export(), policy: { policy: L.policy, policyHash: L.policyHash }, proofOfAi: L.proofOfAi, software: { attestation: L.attestation }, runtime: { record: L.record, auditLog: L.auditLog, head: L.head } });
const v0 = await mod.verifica(bundle, { verifyProof: VERIFY_PROOF });
const toate = ['identity', 'version', 'runtime', 'permissions', 'provenance'];
cer('P1', 'laborator: toate cele cinci fatete PASSED, martorul ABSENT, verdict PARTIAL (unwitnessed)', toate.every((l) => v0.levels[l] === 'PASSED') && v0.levels.witness === 'ABSENT' && v0.verdict === 'PARTIAL' && v0.reason === 'unwitnessed' ? 'VERDE' : 'ROSU', `${v0.verdict} (${v0.reason}) ${JSON.stringify(v0.levels)}`);
cer('P2', 'intrarea registrului a fost PERMISA de politica (unealta model:aere-model-proba in lista)', L.allowed ? 'VERDE' : 'ROSU');
const v1 = await mod.verifica(mod.construieste({ ledger: L.ledger.export(), policy: { policy: L.policy, policyHash: L.policyHash }, proofOfAi: L.proofOfAi }), { verifyProof: VERIFY_PROOF });
cer('P3', 'fara versiune si fara runtime: nivelurile ABSENT, restul PASSED, tot PARTIAL unwitnessed (ce lipseste e spus, nu inventat)', v1.levels.version === 'ABSENT' && v1.levels.runtime === 'ABSENT' && v1.levels.permissions === 'PASSED' && v1.verdict === 'PARTIAL' ? 'VERDE' : 'ROSU', JSON.stringify(v1.levels));
// fixtures: each link broken on purpose
const fix = [
['F1', 'iesirea schimbata in Proof of AI (hash refacut): nu mai e intrarea din registru -> permissions FAILED output-not-in-ledger', (b) => { b.provenance.proofOfAi.statement.outputHash = sha(Buffer.from('another output')); b.provenance.proofOfAi = rehash(b.provenance.proofOfAi); }, 'permissions', 'output-not-in-ledger'],
['F2', 'digestul ponderilor schimbat in Proof of AI: nu e artefactul atestat -> version FAILED version-artifact-mismatch', (b) => { b.provenance.proofOfAi.statement.model.weightsSha256 = '0x' + 'ab'.repeat(32); b.provenance.proofOfAi = rehash(b.provenance.proofOfAi); }, 'version', 'version-artifact-mismatch'],
['F3', 'alt agentId in Proof of AI: identity FAILED identity-mismatch', (b) => { b.provenance.proofOfAi.statement.agentId = 'aere-agent:' + '11'.repeat(20); b.provenance.proofOfAi = rehash(b.provenance.proofOfAi); }, 'identity', 'identity-mismatch'],
['F4', 'inregistrarea de runtime numeste alt artefact: runtime FAILED runtime-artifact-mismatch', (b) => { b.runtime.record.statement.artifactSha256 = '0x' + 'cd'.repeat(32); b.runtime.record = rehash(b.runtime.record); }, 'runtime', 'runtime-artifact-mismatch'],
['F6', 'inregistrarea de runtime datata DUPA iesire: runtime FAILED runtime-after-output', (b) => { b.runtime.record.statement.createdAt = new Date((L.t1 + 3600) * 1000).toISOString(); b.runtime.record = rehash(b.runtime.record); }, 'runtime', 'runtime-after-output'],
['F7', 'o intrare a registrului modificata (argumentele): permissions FAILED ledger-invalid', (b) => { b.permissions.ledger.entries[0].body.action.args = { promptHash: '0x' + '00'.repeat(32) }; }, 'permissions', 'ledger-invalid'],
['F8', 'jurnalul de audit fara inregistrarea de runtime: runtime FAILED runtime-not-in-audit-log', (b) => { b.runtime.auditLog = []; b.runtime.head = null; }, 'runtime', 'runtime-not-in-audit-log'],
['F9', 'atestarea Proof of Software modificata dupa hash: version FAILED version-attestation', (b) => { b.version.attestation.statement.artifacts[0].sha256 = '0x' + 'ef'.repeat(32); }, 'version', 'version-attestation'],
['F10', 'jurnalul de audit cu lantul rupt (hash-ul intrarii schimbat): runtime FAILED audit-log-broken', (b) => { b.runtime.auditLog[0].hash = '0x' + '77'.repeat(32); }, 'runtime', 'audit-log-broken'],
];
for (const [id, eticheta, muta, nivel, motiv] of fix) {
const b = clone(bundle); muta(b);
const v = await mod.verifica(b, { verifyProof: VERIFY_PROOF });
const ok = v.verdict === 'INVALID' && v.levels[nivel] === 'FAILED' && v.details[nivel].reason === motiv;
cer(id, eticheta, ok ? 'VERDE' : 'ROSU', `${v.verdict} (${v.reason}); ${nivel} ${v.levels[nivel]} ${v.details[nivel].reason || ''}`);
}
{
const L2 = laborator({ toolAllowed: false });
const b = mod.construieste({ ledger: L2.ledger.export(), policy: { policy: L2.policy, policyHash: L2.policyHash }, proofOfAi: L2.proofOfAi, software: { attestation: L2.attestation }, runtime: { record: L2.record, auditLog: L2.auditLog } });
const v = await mod.verifica(b, { verifyProof: VERIFY_PROOF });
cer('F5', 'unealta nepermisa de politica: intrarea REFUZATA exista, iesirea nu poate fi "permisa" -> permissions FAILED action-refused', !L2.allowed && v.levels.permissions === 'FAILED' && v.details.permissions.reason === 'action-refused' ? 'VERDE' : 'ROSU', `${v.verdict} ${v.details.permissions.reason || ''}`);
}
if (viu) {
// the witness: the ledger head notarized on 28001 with the laboratory test key (gas only), read back under a certified anchor
const require = createRequire(path.join(ETHERS_DIR, 'package.json'));
const { ethers } = require('ethers');
const headEnv = ledgerHeadEnvelope(L.ledger.export());
let notarized;
// notarizeHead returns { ok, envelope }: the ENVELOPE (with its notarization) is the head the witness level reads
try { const r = await notarizeHead({ envelope: headEnv, rpc: RPC_28001, keyFile: CHEIE, ethers }); if (!r || !r.ok) throw new Error('notarizeHead: ' + JSON.stringify(r).slice(0, 160)); notarized = r.envelope; } catch (e) { cer('V1', 'capul registrului notarizat pe 28001', 'STRICAT', String(e.message).slice(0, 160)); return; }
cer('V1', 'capul registrului notarizat pe 28001 (cheia de proba L1, numai gaz)', notarized && notarized.notarization ? 'VERDE' : 'ROSU', notarized && notarized.notarization ? `tx ${String(notarized.notarization.txHash).slice(0, 14)}.. block ${notarized.notarization.block}` : JSON.stringify(notarized).slice(0, 120));
const bw = mod.construieste({ ledger: L.ledger.export(), policy: { policy: L.policy, policyHash: L.policyHash }, proofOfAi: L.proofOfAi, software: { attestation: L.attestation }, runtime: { record: L.record, auditLog: L.auditLog, head: L.head }, witness: { heads: [notarized], chainId: 28001 } });
let vw = null;
for (let i = 0; i < 20; i++) { vw = await mod.verifica(bw, { verifyProof: VERIFY_PROOF, rpc: RPC_28001, chain: 28001 }); if (vw.levels.witness !== 'UNMEASURED') break; await new Promise((ok) => setTimeout(ok, 8000)); }
cer('V2', 'cu martorul citit sub o ancora certificata (--rpc 28001): witness PASSED, verdict VALID', vw && vw.verdict === 'VALID' && vw.levels.witness === 'PASSED' ? 'VERDE' : (vw && vw.levels.witness === 'UNMEASURED' ? 'STRICAT' : 'ROSU'), vw ? `${vw.verdict} (${vw.reason}); witness ${vw.levels.witness}: ${String(vw.details.witness.detail || vw.details.witness.reason || '').slice(0, 160)}` : 'fara verdict');
const vb = await mod.verifica(bw, { verifyProof: VERIFY_PROOF });
cer('V3', 'acelasi pachet FARA --rpc: martorul UNMEASURED (witness-needs-rpc), nu PASSED', vb.levels.witness === 'UNMEASURED' && vb.verdict === 'PARTIAL' ? 'VERDE' : 'ROSU', `${vb.verdict} (${vb.reason})`);
const d = path.join(RAD, 'tools', 'aere-ai-trust', 'dovezi'); fs.mkdirSync(d, { recursive: true });
const f = path.join(d, `lant-28001-${new Date().toISOString().replace(/[:.]/g, '').slice(0, 15)}Z.json`);
fs.writeFileSync(f, JSON.stringify({ bundle: bw, verdict: vw }, null, 1)); console.log(' dovada scrisa: ' + f);
}
}
if (process.argv.includes('--control-negativ')) {
const SURSA = fs.readFileSync(path.join(AICI, 'lant-de-incredere.mjs'), 'utf8');
const PLANTARI = [
{ nume: '(a) identitatea necomparata', de: '/*PAZA:identity*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F3'] },
{ nume: '(b) artefactul atestat necomparat cu ponderile', de: '/*PAZA:version*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F2'] },
{ nume: '(c) artefactul rulat necomparat', de: '/*PAZA:runtime*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F4'] },
{ nume: '(d) ordinea timpilor nejudecata', de: '/*PAZA:order*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F6'] },
{ nume: '(e) iesirea necautata in registru (prima intrare luata oricum)', de: '/*PAZA:output-in-ledger*/', cu: '|| ledger.entries[0] /*PLANTAT*/', rosii: ['F1'] },
// (f) and (g) sit inside a NEGATED condition (`if (!(x) /*PAZA*/)`): the planting that silences the check is `&& false`, not `|| true`
// (measured 2026-10-02: `|| true` made the honest rows fail instead of letting the fixture pass)
{ nume: '(f) decizia refuzata luata drept permisa', de: '/*PAZA:allowed*/', cu: "&& String(1) === '2' /*PLANTAT*/", rosii: ['F5'] },
{ nume: '(g) inregistrarea necautata in jurnalul de audit', de: '/*PAZA:audit*/', cu: "&& String(1) === '2' /*PLANTAT*/", rosii: ['F8'] },
{ nume: '(h) controlul controlului: o ancora care nu exista', de: '/*PAZA:nu-exista*/', cu: 'x', rosii: [], asteapta: 'STRICAT' },
];
let bune = 0;
for (const [k, p] of PLANTARI.entries()) {
const n = SURSA.split(p.de).length - 1;
const copie = path.join(AICI, `_plantat-lant-${process.pid}-${k}.mjs`);
let stare, detaliu = '';
try {
if (n !== 1) { stare = 'STRICAT'; detaliu = `ancora apare de ${n} ori (cerut 1)`; }
else {
fs.writeFileSync(copie, SURSA.replace(p.de, p.cu));
const mod = await import(pathToFileURL(copie).href);
const randuri = [];
// a probe that THROWS on the planted copy is STRICAT for that planting, not the end of the control
try { await proba(mod, (id, eticheta, st, det) => randuri.push({ id, stare: st, det })); } catch (e) { randuri.push({ id: 'proba', stare: 'STRICAT', det: String(e.message).slice(0, 160) }); }
const stricate = randuri.filter((x) => x.stare === 'STRICAT'); const rosii = randuri.filter((x) => x.stare === 'ROSU').map((x) => x.id);
if (stricate.length) { stare = 'STRICAT'; detaliu = 'proba a cazut: ' + stricate.map((x) => x.id + ' ' + x.det).join(' | ').slice(0, 200); }
else { const prezise = p.rosii.every((id) => rosii.includes(id)); const numai = rosii.every((id) => p.rosii.includes(id)); stare = prezise && numai ? 'VERDE' : 'ROSU'; detaliu = `rosii: ${rosii.join(',') || '-'} (prezis ${p.rosii.join(',') || '-'})`; }
}
} finally { fs.rmSync(copie, { force: true }); }
const ok = stare === (p.asteapta || 'VERDE'); if (ok) bune++;
console.log(` ${ok ? 'BINE' : 'ESEC'} ${p.nume}: ${stare}${p.asteapta ? ' (prezis ' + p.asteapta + ')' : ''} ${detaliu}`);
}
const ramase = fs.readdirSync(AICI).filter((f) => f.startsWith('_plantat-lant-'));
console.log(`CONTROL: ${bune} din ${PLANTARI.length} cum s-a prezis${ramase.length ? '; copii ramase: ' + ramase.join(',') : ''}`);
process.exitCode = bune === PLANTARI.length && !ramase.length ? 0 : 1;
} else {
const mod = await import('./lant-de-incredere.mjs');
let verzi = 0, rosii = 0, stricate = 0;
await proba(mod, (id, eticheta, stare, det) => { if (stare === 'VERDE') verzi++; else if (stare === 'ROSU') rosii++; else stricate++; console.log(` [${stare.padEnd(7)}] ${id.padEnd(4)} ${eticheta}${det ? ' ' + det : ''}`); }, { viu: process.argv.includes('--viu') });
console.log(`\nlantul de incredere pentru AI: ${verzi} VERDE, ${rosii} ROSU, ${stricate} STRICAT`);
process.exitCode = rosii || stricate ? 1 : 0;
}

18
proof-of-ai/README.md Normal file
View File

@ -0,0 +1,18 @@
# AERE Proof of AI (roadmap B2 / #38)
Un plic AERE Proof Protocol (AIP-23) care dovedeste provenienta unei actiuni de agent AI - ce model, ce versiune, ce prompt, ce
unelte, ce iesire, ce agent, cine a cerut - verificat de ACELASI verificator ca orice alt proof AERE, fara niciun cod nou de
verificare. Notarizarea si finalitatea post-cuantica vin din AIP-23 (statementHash notarizat pe AereNotary, acoperit de o ancora PQ).
```
import { buildProofOfAi } from './proof-of-ai.mjs';
const plic = buildProofOfAi({ model:{name,version,weightsSha256}, prompt, input, output, tools:[...], agentId, requester, createdAt });
// -> plic AIP-23; se verifica cu tools/aere-proof-protocol/verify.mjs
```
Plicul tine numai DIGESTURI (promptul, intrarea, iesirea, cine a cerut se stocheaza ca SHA-256, nu brut). Se aliniaza cu contractul
de pe lant `contracts/contracts/compliance/AereAIProof.sol` (modelId, inputHash, outputHash, requesterHash), dar e PORTABIL: exista
fara lant, il cere doar pentru finalitate.
Proba: `node proba-proof-of-ai.mjs` (6/6, cu control negativ: iesire schimbata dupa hash -> INVALID). Teza demonstrata: un standard,
multe feluri de dovada - acelasi verificator, un fel de proof nou, zero cod nou.

View File

@ -0,0 +1,61 @@
'use strict';
// Proba Proof-of-AI (B2): construieste un plic, il verifica cu ACELASI verificator AIP-23 (fara cod nou de verificare), si un
// CONTROL NEGATIV: un plic caruia i se atinge statement-ul dupa hash pica la integritate. Dovedeste teza "un standard, multe feluri".
// node proba-proof-of-ai.mjs -> 0 toate cum trebuia, 1 altfel
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { buildProofOfAi, KIND } from './proof-of-ai.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
// Verificatorul AIP-23: AERE_VERIFY_PROOF=<verify-proof.mjs> (copia publica, ca la agents/) sau, in depozitul de dezvoltare,
// ../aere-proof-protocol/verify.mjs; fara el proba nu poate masura nimic si iese 2 (NEMASURAT), nu verde
const VERIFY = process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs');
if (!fs.existsSync(VERIFY)) { console.log(`NEMASURAT: verificatorul AIP-23 nu e la ${VERIFY}; AERE_VERIFY_PROOF=<verify-proof.mjs>`); process.exit(2); }
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'poai-'));
let rele = 0;
const cer = (n, c) => { console.log(` [${c ? 'OK ' : 'RAU '}] ${n}`); if (!c) rele++; };
function verif(plicPath) {
try { return { cod: 0, out: execFileSync(process.execPath, [VERIFY, plicPath, '--json'], { encoding: 'utf8' }) }; }
catch (e) { return { cod: e.status ?? 2, out: (e.stdout || '') + (e.stderr || '') }; }
}
const verdict = (o) => { try { return JSON.parse(o).verdict; } catch { return '?'; } };
// verificatorul AIP-23 e in ENGLEZA (public-ready, D-402/B4): niveluri->levels, nivel->level, stare->state, TRECUT->PASSED, PICAT->FAILED
const nivel = (o, n) => { try { return JSON.parse(o).levels.find((x) => x.level === n).state; } catch { return '?'; } };
try {
const plic = buildProofOfAi({
model: { name: 'aere-fable', version: '5.1', weightsSha256: '0x' + '22'.repeat(32) },
prompt: 'rezuma documentul X',
input: 'documentul X, 12kb',
output: 'rezumatul: ...',
tools: ['retrieval', 'calculator'],
agentId: '0xa11ce00000000000000000000000000000000001',
requester: 'client-42',
createdAt: '2026-09-26T09:00:00.000Z',
});
cer('plicul are kind aere-proof-of-ai-attestation', plic.kind === KIND + '-attestation');
cer('statement-ul are model, promptHash, inputHash, outputHash, tools', !!(plic.statement.model && plic.statement.promptHash && plic.statement.inputHash && plic.statement.outputHash && Array.isArray(plic.statement.tools)));
cer('nu contine continut brut (doar digesturi)', !JSON.stringify(plic).includes('rezuma documentul X') && !JSON.stringify(plic).includes('rezumatul: ...'));
const f = path.join(T, 'ai.json'); fs.writeFileSync(f, JSON.stringify(plic, null, 1));
const r = verif(f);
cer('verificatorul AIP-23 il valideaza (fara cod nou)', verdict(r.out) === 'VALID' && nivel(r.out, 'integrity') === 'PASSED' && r.cod === 0);
// CONTROL NEGATIV: schimb outputHash dupa ce statementHash e fixat -> integritate PICAT
const rau = JSON.parse(JSON.stringify(plic)); rau.statement.outputHash = '0x' + '33'.repeat(32);
const fr = path.join(T, 'ai-rau.json'); fs.writeFileSync(fr, JSON.stringify(rau, null, 1));
const rr = verif(fr);
cer('CONTROL: iesire schimbata dupa hash -> INVALID', verdict(rr.out) === 'INVALID' && nivel(rr.out, 'integrity') === 'FAILED' && rr.cod === 1);
// acelasi statement -> acelasi hash (determinist)
const plic2 = buildProofOfAi({ model: { name: 'aere-fable', version: '5.1', weightsSha256: '0x' + '22'.repeat(32) }, prompt: 'rezuma documentul X', input: 'documentul X, 12kb', output: 'rezumatul: ...', tools: ['retrieval', 'calculator'], agentId: '0xa11ce00000000000000000000000000000000001', requester: 'client-42', createdAt: '2026-09-26T09:00:00.000Z' });
cer('determinist: acelasi continut -> acelasi statementHash', plic2.statementHash === plic.statementHash);
} finally {
fs.rmSync(T, { recursive: true, force: true });
}
console.log(`\nB2 proof-of-ai: ${6 - rele}/6 cum trebuia`);
process.exit(rele ? 1 : 0);

View File

@ -0,0 +1,52 @@
'use strict';
// AERE Proof of AI (roadmap B2 / #38): un plic AERE Proof Protocol (AIP-23) care dovedeste provenienta unei actiuni de agent AI -
// ce model, ce versiune, ce prompt, ce unelte, ce iesire, ce agent, cine a cerut - astfel incat un tert sa verifice fara sa aiba
// incredere in producator, cu ACELASI verificator ca orice alt proof AERE (tools/aere-proof-protocol/verify.mjs). Nu adauga niciun
// cod nou de verificare: asta e chiar teza standardului ("un plic, multe feluri de dovada"). Notarizarea + finalitatea post-cuantica
// vin gratis din AIP-23 (statementHash notarizat pe AereNotary, acoperit de o ancora PQ). Numai Node 24, node:crypto.
//
// Se aliniaza cu contractul de pe lant AereAIProof.sol (modelId, inputHash, outputHash, requesterHash): campurile de mai jos se pot
// mapa la o atestare on-chain, dar plicul e PORTABIL (nu cere lantul ca sa existe; il cere doar ca sa capete finalitate).
import crypto from 'node:crypto';
export const KIND = 'aere-proof-of-ai';
const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex');
const hexOf = (v) => {
if (v == null) return null;
if (typeof v === 'string' && /^0x[0-9a-fA-F]{64}$/.test(v)) return v.toLowerCase(); // deja un digest
return sha256(Buffer.from(typeof v === 'string' ? v : JSON.stringify(v), 'utf8'));
};
/**
* Construieste un plic Proof-of-AI (AIP-23). Nu tine niciun continut sensibil: doar digesturi.
* @param {object} p
* @param {{name:string, version?:string, weightsSha256?:string}} p.model
* @param {string} [p.prompt] promptul brut (se stocheaza doar digestul) SAU
* @param {string} [p.promptHash] digestul promptului (0x+64) daca nu vrei sa-l dai brut
* @param {string} [p.input] intrarea (digest stocat) SAU p.inputHash
* @param {string} [p.output] iesirea (digest stocat) SAU p.outputHash
* @param {string[]} [p.tools] uneltele pe care agentul avea voie sa le foloseasca / le-a folosit
* @param {string} [p.agentId] identitatea agentului (adresa / id)
* @param {string} [p.requester] cine a cerut (digest stocat) SAU p.requesterHash
* @param {string} p.createdAt RFC3339
* @returns {object} plicul AIP-23 { v, kind, statement, statementHash }
*/
export function buildProofOfAi(p) {
if (!p || !p.model || !p.model.name) throw new Error('proof-of-ai: model.name e obligatoriu');
if (!p.createdAt) throw new Error('proof-of-ai: createdAt e obligatoriu (RFC3339)');
const statement = {
v: 1,
kind: KIND,
model: { name: p.model.name, version: p.model.version || null, weightsSha256: p.model.weightsSha256 || null },
promptHash: p.promptHash ? p.promptHash.toLowerCase() : hexOf(p.prompt),
inputHash: p.inputHash ? p.inputHash.toLowerCase() : hexOf(p.input),
outputHash: p.outputHash ? p.outputHash.toLowerCase() : hexOf(p.output),
tools: Array.isArray(p.tools) ? [...p.tools] : [],
agentId: p.agentId || null,
requesterHash: p.requesterHash ? p.requesterHash.toLowerCase() : hexOf(p.requester),
createdAt: p.createdAt,
};
const text = JSON.stringify(statement); // canonic AIP-23: ordinea de inserare, SHA-256
return { v: 1, kind: KIND + '-attestation', statement, statementHash: sha256(Buffer.from(text, 'utf8')) };
}

63
verifiable-data/README.md Normal file
View File

@ -0,0 +1,63 @@
# AERE Verifiable Data
A dataset whose every answer can be checked against a short commitment, by anyone, without the data and without trusting the server.
Built from pieces that already exist beside it: the RFC 9162 Merkle tree of the verification layer (`verify-layer/arbore-merkle.mjs`),
the AIP-23 `data` envelope (`proof-kinds/`), the notary call of the agents (`agents/agent-ancora.mjs`) and the AIP-23 reference
verifier for the time of a version under a post-quantum certified anchor.
| what | how it is proven | with |
|---|---|---|
| a record is in the dataset at a version | inclusion proof in the version's log (RFC 9162, log2(n) hashes) | `verifica.mjs inregistrare` |
| a newer version only appended | consistency proof between the two log roots; the newer commitment names the older as `previous` | `verifica.mjs consistenta` |
| a query answer is correct AND complete | every row proves itself in the log and at its position in the SORTED index of the queried field; the rows are one contiguous run; the neighbour before the run is below the range and the one after is above it, each with its proof | `verifica.mjs raspuns` |
| a row reveals only what was asked | every field is committed as SHA-256(salt \| field \| value) in the record leaf; undisclosed fields stay hidden, disclosed ones cannot change | the same |
| the publisher serves what it committed to | availability sampling: random positions fetched and verified against the commitment; a withheld record fails the sample that lands on it | `verifica.mjs disponibilitate` |
| when a version existed | the commitment's AIP-23 envelope notarized on AereNotary and read back under a post-quantum certified anchor (`finality` of the reference verifier) | `verifica.mjs angajament --rpc` |
```
node vd.mjs creeaza ./set --nume users --index country,age
node vd.mjs adauga ./set records.ndjson # {"id":"u1","country":"RO","age":31,"email":"..."} per line
node vd.mjs angajeaza ./set --out c1.json # version 1: commitment + plicuri/1.json (its AIP-23 envelope)
node vd.mjs interogheaza ./set 1 --camp country --eq '"RO"' --campuri age --out a.json
node verifica.mjs raspuns a.json --angajament c1.json # VALID (n rows) | INVALID + reasons
node vd.mjs serveste ./set --port 8797 # GET /v1/vd/users/{commitments|record/1/7|query/1?field=age&from=30&to=40|consistency/1/2}
node verifica.mjs disponibilitate --url http://127.0.0.1:8797/v1/vd/users --angajament c1.json --esantioane 32
```
A commitment is `{ name, version, size, logRoot, indexes: { field: { root, size } }, createdAt, previous }`; its hash is the SHA-256 of its
canonical JSON, and that hash is the `sha256` of the version's `aere-proof-of-data` envelope. Records are flat JSON objects with a
string `id`; values are strings, finite numbers, booleans or null (an index needs a total order: by type, then numerically or by code
unit, in one function both sides use). A dataset appends; it never rewrites.
## What it is not, and what is not measured
- **Not zero knowledge.** Selective disclosure here is hash-based: the reader learns the disclosed values and the commitments of the
rest. A proof ABOUT rows without revealing them (a count, a sum, membership in a set, "some row satisfies P") needs a proving system;
the one this network verifies on chain (SP1, Groth16 over BN254) needs about 24 GiB to prove, measured 2026-09-26, and no such
circuit is built. Until it is, a "ZK query" is not offered, and this README says so instead of calling disclosure zero knowledge.
- **Completeness rests on the index being sorted.** A single answer proves its rows and its two neighbours against the committed index
root; that no matching row was omitted follows only if the index is in order, which the reader checks locally on the rows returned
(an unsorted pair is refused) and which anyone holding the data checks in full with `vd.mjs auditeaza`. A publisher that commits an
unsorted index is caught by the first answer that shows two rows out of order, or by an audit, not by a one-row answer.
- **Availability is retrievability under sampling, not a data-availability scheme.** k verified samples make a publisher that hides a
share s of its records survive with probability (1 - s)^k; there is no erasure coding and no network of samplers.
- **The time of a version is the chain's only when notarized**; an unnotarized commitment carries the publisher's `createdAt`.
- **Not storage.** The records live where the publisher keeps them (a folder here); this component commits, proves and serves, it does
not replicate, encrypt or back up.
- Index trees are rebuilt per version (sorted); consistency proofs cover the LOG only. Queries on fields that are not indexed are not
offered. Field values above 4096 characters, arrays and nested objects are refused.
## Probes
```
AERE_VERIFY_PROOF=<verify-proof.mjs from aere-node> node proba-verifiable-data.mjs # 22 rows: honest answers VALID, nine forged ones INVALID
# for their named reason, sampling on a local server
AERE_VERIFY_PROOF=... node proba-verifiable-data.mjs --control-negativ # 5 plantings on a copy of baza.mjs; each must make its
# forged fixture pass, which proves the check is load-bearing
AERE_VERIFY_PROOF=... AERE_NOTARY_KEY_FILE=<key> AERE_ETHERS_DIR=<dir with node_modules/ethers> node proba-verifiable-data.mjs --viu
# plus a notarization on testnet 28001 (a test key) read back
# under a post-quantum certified anchor; evidence in dovezi/
```
Without the AIP-23 verifier the envelope row is reported NEMASURAT and the exit code is 2. The probe never touches chain 2800; the notary
call refuses 2800 unless `AERE_CONFIRM_MAINNET=yes`.

272
verifiable-data/baza.mjs Normal file
View File

@ -0,0 +1,272 @@
// baza.mjs: the verifiable dataset (roadmap master point 20, pista B, 2026-10-02). A dataset is an append-only list of records; a
// VERSION is a commitment to it: the RFC 9162 Merkle root of the record leaves (the log, `../verify-layer/arbore-merkle.mjs`), its size,
// and for each indexed field the root of a SORTED index tree over (value, record). From a commitment anyone can verify, without the data:
// - a record is in the dataset at a version (inclusion proof in the log, log2(n) hashes);
// - a newer version only APPENDED (consistency proof between the two log roots): a version that is not a prefix of the next is a
// rewritten history;
// - a query answer (equality or range on an indexed field) is COMPLETE and CORRECT: every returned row proves itself in the log and in
// the index, the rows are the contiguous run of the sorted index, and the two neighbours outside the range are shown with their
// proofs, so no matching row could have been left out; the completeness rests on the index being sorted, which anyone holding the
// data audits in full (`auditeaza`) and no single query can;
// - a row reveals only the fields the query asked for (selective disclosure): every field is committed as
// SHA-256(salt | field | value) in the record leaf, so an undisclosed field stays hidden and a disclosed one cannot be changed. This
// is hash-based disclosure, NOT zero knowledge: the verifier learns the disclosed values; a proof about rows without revealing them
// (counts, sums, membership in a set) needs a proving system and is not built here (see README).
// The commitment itself is an AIP-23 `data` envelope (proof-kinds), notarizable on AereNotary and read back under a post-quantum
// certified anchor by the reference verifier: the TIME of a version is then the chain's, not the publisher's.
// Only node:crypto and the Merkle tree of the verification layer. Records: JSON objects with a string `id` (unique) and flat fields
// whose values are strings, finite numbers, booleans or null; arrays and nested objects are refused (an index needs a total order).
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { leafHash, merkleTree, verifyInclusion, verifyConsistency } from '../verify-layer/arbore-merkle.mjs';
import { buildProof } from '../proof-kinds/proof-kinds.mjs';
export const FORMAT = 'aere-verifiable-dataset-1';
export const KIND_RASPUNS = 'aere-verifiable-query-answer';
export const KIND_INREGISTRARE = 'aere-verifiable-record';
export const KIND_CONSISTENTA = 'aere-verifiable-consistency';
const H = /^0x[0-9a-f]{64}$/;
const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex');
const utf8 = (s) => Buffer.from(s, 'utf8');
/** Canonical JSON: keys sorted at every depth, no whitespace; refuses what JSON cannot carry faithfully. */
export function canonical(x) {
if (x === null) return 'null';
if (typeof x === 'string') return JSON.stringify(x);
if (typeof x === 'boolean') return x ? 'true' : 'false';
if (typeof x === 'number') { if (!Number.isFinite(x)) throw new Error('canonical: a number must be finite'); return JSON.stringify(x); }
if (Array.isArray(x)) return '[' + x.map(canonical).join(',') + ']';
if (typeof x === 'object') return '{' + Object.keys(x).sort().map((k) => { if (x[k] === undefined) throw new Error('canonical: undefined under ' + k); return JSON.stringify(k) + ':' + canonical(x[k]); }).join(',') + '}';
throw new Error('canonical: unsupported value of type ' + typeof x);
}
const tip = (v) => v === null ? 'null' : typeof v;
const TIPURI = ['boolean', 'null', 'number', 'string'];
/** Total order on field values: by type name, then by value (numbers numerically, strings by code unit). One place, used by both sides. */
export function comparaValori(a, b) {
const ta = tip(a), tb = tip(b);
if (!TIPURI.includes(ta) || !TIPURI.includes(tb)) throw new Error('compare: a field value must be a string, a finite number, a boolean or null');
if (ta !== tb) return ta < tb ? -1 : 1;
if (ta === 'number') return a < b ? -1 : a > b ? 1 : 0;
if (ta === 'boolean') return a === b ? 0 : a ? 1 : -1;
const sa = String(a), sb = String(b); return sa < sb ? -1 : sa > sb ? 1 : 0;
}
export const angajamentCamp = (field, value, salt) => sha256(utf8(String(salt) + '|' + field + '|' + canonical(value)));
export const frunzaInregistrare = (id, c) => leafHash(utf8(canonical({ id, c })));
export const frunzaIndex = (field, value, i, recordLeaf) => leafHash(utf8(canonical({ field, value, i, recordLeaf })));
export const hashAngajament = (commitment) => sha256(utf8(canonical(commitment)));
function valideazaInregistrare(r) {
if (!r || typeof r !== 'object' || Array.isArray(r)) throw new Error('a record is a JSON object');
if (typeof r.id !== 'string' || !r.id.length || r.id.length > 256) throw new Error('a record needs a string id (1..256 chars)');
const fields = r.fields && typeof r.fields === 'object' ? r.fields : Object.fromEntries(Object.entries(r).filter(([k]) => k !== 'id'));
for (const [k, v] of Object.entries(fields)) {
if (!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(k)) throw new Error('field name "' + k + '" is not [A-Za-z_][A-Za-z0-9_]{0,63}');
if (!TIPURI.includes(tip(v)) || (typeof v === 'number' && !Number.isFinite(v))) throw new Error('field "' + k + '" must be a string, a finite number, a boolean or null');
if (typeof v === 'string' && v.length > 4096) throw new Error('field "' + k + '" is longer than 4096 chars');
}
return { id: r.id, fields };
}
/** Creates a dataset folder: set.json (name, indexed fields), an empty record file, no versions. */
export function creeaza(dir, { name, indexes = [] }) {
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(String(name))) throw new Error('dataset name: [a-z0-9][a-z0-9-]{0,63}');
if (!Array.isArray(indexes) || indexes.some((f) => !/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(f))) throw new Error('indexes: a list of field names');
fs.mkdirSync(dir, { recursive: true });
if (fs.existsSync(path.join(dir, 'set.json'))) throw new Error('the dataset already exists at ' + dir);
fs.writeFileSync(path.join(dir, 'set.json'), JSON.stringify({ format: FORMAT, name, indexes: [...new Set(indexes)].sort() }, null, 1));
fs.writeFileSync(path.join(dir, 'inregistrari.ndjson'), '');
fs.writeFileSync(path.join(dir, 'versiuni.json'), '[]');
return deschide(dir);
}
export function deschide(dir) {
const set = JSON.parse(fs.readFileSync(path.join(dir, 'set.json'), 'utf8'));
if (set.format !== FORMAT) throw new Error('not a ' + FORMAT + ' dataset: ' + dir);
const text = fs.readFileSync(path.join(dir, 'inregistrari.ndjson'), 'utf8');
const inregistrari = text.split('\n').filter(Boolean).map((l) => JSON.parse(l));
const versiuni = JSON.parse(fs.readFileSync(path.join(dir, 'versiuni.json'), 'utf8'));
return { dir, set, inregistrari, versiuni, _cache: new Map() };
}
/** Appends records (never rewrites): each gets a fresh 16-byte salt per field; ids must be new. */
export function adauga(db, records) {
const ids = new Set(db.inregistrari.map((r) => r.id)); const noi = [];
for (const r of records) {
const { id, fields } = valideazaInregistrare(r);
if (ids.has(id)) throw new Error('duplicate id "' + id + '" (a dataset appends; it never rewrites a record)');
ids.add(id);
const salts = Object.fromEntries(Object.keys(fields).map((k) => [k, crypto.randomBytes(16).toString('hex')]));
noi.push({ id, fields, salts });
}
fs.appendFileSync(path.join(db.dir, 'inregistrari.ndjson'), noi.map((r) => JSON.stringify(r) + '\n').join(''));
db.inregistrari.push(...noi); db._cache.clear();
return noi.length;
}
const angajamente = (r) => Object.fromEntries(Object.keys(r.fields).sort().map((k) => [k, angajamentCamp(k, r.fields[k], r.salts[k])]));
/** The trees of a version of `size` records: the log and one sorted index per indexed field (memoized per size). */
function arbori(db, size) {
if (db._cache.has(size)) return db._cache.get(size);
if (!Number.isInteger(size) || size < 1 || size > db.inregistrari.length) throw new Error('version size ' + size + ' outside 1..' + db.inregistrari.length);
const recs = db.inregistrari.slice(0, size).map((r, i) => { const c = angajamente(r); return { i, id: r.id, c, leaf: frunzaInregistrare(r.id, c) }; });
const log = merkleTree(recs.map((r) => r.leaf));
const index = {};
for (const f of db.set.indexes) {
const intrari = recs.filter((r) => f in db.inregistrari[r.i].fields).map((r) => ({ value: db.inregistrari[r.i].fields[f], i: r.i, recordLeaf: r.leaf }));
intrari.sort((a, b) => comparaValori(a.value, b.value) || a.i - b.i);
index[f] = { intrari, tree: merkleTree(intrari.map((e) => frunzaIndex(f, e.value, e.i, e.recordLeaf))) };
}
const t = { recs, log, index }; db._cache.set(size, t); return t;
}
/** Commits the current records as a new version; returns the commitment (also appended to versiuni.json). */
export function angajeaza(db, { createdAt = new Date().toISOString() } = {}) {
const size = db.inregistrari.length; if (!size) throw new Error('nothing to commit: the dataset is empty');
const prev = db.versiuni[db.versiuni.length - 1] || null;
if (prev && prev.size === size) throw new Error('nothing new since version ' + prev.version + ' (' + size + ' records)');
const t = arbori(db, size);
const commitment = { v: 1, format: FORMAT, name: db.set.name, version: db.versiuni.length + 1, size, logRoot: t.log.root(), createdAt,
indexes: Object.fromEntries(db.set.indexes.map((f) => [f, { root: t.index[f].tree.root(), size: t.index[f].intrari.length }])),
previous: prev ? hashAngajament(prev) : null };
db.versiuni.push(commitment);
fs.writeFileSync(path.join(db.dir, 'versiuni.json'), JSON.stringify(db.versiuni, null, 1));
return commitment;
}
export function versiune(db, n) { const c = db.versiuni.find((x) => x.version === Number(n)); if (!c) throw new Error('no version ' + n); return c; }
const fara = (fields, care) => care === '*' ? { ...fields } : Object.fromEntries(Object.entries(fields).filter(([k]) => care.includes(k)));
const dezvaluie = (r, care) => Object.fromEntries(Object.keys(fara(r.fields, care)).map((k) => [k, { value: r.fields[k], salt: r.salts[k] }]));
const dovada = (p) => ({ index: p.index, treeSize: p.treeSize, path: p.path });
/** One record at a version, with its log inclusion proof; `fields` = '*' (all) or a list of field names to disclose. */
export function dovadaInregistrare(db, n, i, { fields = '*' } = {}) {
const c = versiune(db, n); const t = arbori(db, c.size); const r = t.recs[i]; if (!r) throw new Error('record ' + i + ' outside version ' + n + ' (' + c.size + ' records)');
return { v: 1, kind: KIND_INREGISTRARE, name: c.name, version: c.version, commitmentHash: hashAngajament(c), i, id: r.id, c: r.c, disclosed: dezvaluie(db.inregistrari[i], fields), logProof: dovada(t.log.inclusionProof(i, c.size)) };
}
/** Equality (eq) or range (from..to, inclusive, either side open) on an indexed field; `fields` = the fields to disclose besides it. */
export function interogheaza(db, n, { field, eq, from, to, fields = [], limit = 1000 } = {}) {
const c = versiune(db, n); if (!c.indexes[field]) throw new Error('field "' + field + '" is not indexed in ' + c.name);
if (eq !== undefined) { from = eq; to = eq; }
if (from !== undefined && to !== undefined && comparaValori(from, to) > 0) throw new Error('from is above to');
const t = arbori(db, c.size); const ix = t.index[field]; const e = ix.intrari;
let start = 0; while (start < e.length && from !== undefined && comparaValori(e[start].value, from) < 0) start++;
let end = start; while (end < e.length && (to === undefined || comparaValori(e[end].value, to) <= 0)) end++;
if (end - start > limit) throw new Error('the answer would have ' + (end - start) + ' rows, above the limit ' + limit);
const care = fields === '*' ? '*' : [...new Set([field, ...fields])];
const rows = e.slice(start, end).map((x, k) => ({ pos: start + k, i: x.i, id: t.recs[x.i].id, c: t.recs[x.i].c, disclosed: dezvaluie(db.inregistrari[x.i], care),
logProof: dovada(t.log.inclusionProof(x.i, c.size)), indexProof: dovada(ix.tree.inclusionProof(start + k, e.length)) }));
const vecin = (pos) => pos < 0 || pos >= e.length ? null : ({ pos, value: e[pos].value, i: e[pos].i, recordLeaf: e[pos].recordLeaf, indexProof: dovada(ix.tree.inclusionProof(pos, e.length)) });
return { v: 1, kind: KIND_RASPUNS, name: c.name, version: c.version, commitmentHash: hashAngajament(c), field, from: from === undefined ? null : from, to: to === undefined ? null : to,
indexSize: e.length, rows, before: vecin(start - 1), after: vecin(end) };
}
/** The log of version a is a prefix of the log of version b (RFC 9162 consistency). */
export function dovadaConsistenta(db, a, b) {
const ca = versiune(db, a), cb = versiune(db, b); if (ca.size > cb.size) throw new Error('version ' + a + ' is larger than version ' + b);
const t = arbori(db, cb.size);
return { v: 1, kind: KIND_CONSISTENTA, name: cb.name, from: { version: ca.version, commitmentHash: hashAngajament(ca) }, to: { version: cb.version, commitmentHash: hashAngajament(cb) }, proof: t.log.consistencyProof(ca.size, cb.size) };
}
/** Rebuilds every version from the records and compares the roots: the full audit anyone holding the data runs. */
export function auditeaza(db) {
const probleme = [];
for (const c of db.versiuni) {
try { const t = arbori(db, c.size); if (t.log.root() !== c.logRoot) probleme.push('version ' + c.version + ': log root differs');
for (const f of Object.keys(c.indexes)) { if (!t.index[f] || t.index[f].tree.root() !== c.indexes[f].root || t.index[f].intrari.length !== c.indexes[f].size) probleme.push('version ' + c.version + ': index ' + f + ' differs'); }
} catch (e) { probleme.push('version ' + c.version + ': ' + e.message); }
}
for (let k = 1; k < db.versiuni.length; k++) if (db.versiuni[k].previous !== hashAngajament(db.versiuni[k - 1])) probleme.push('version ' + db.versiuni[k].version + ' does not name version ' + db.versiuni[k - 1].version + ' as previous');
return { ok: probleme.length === 0, probleme };
}
// ----- verification, without the data (what a reader runs on a commitment and an answer) -----
const ok = (motive, cond, m) => { if (!cond) motive.push(m); return cond; };
function verificaRand(r, commitment, field, motive, eticheta) {
if (!r || typeof r !== 'object' || !r.c || typeof r.c !== 'object' || !r.disclosed) return ok(motive, false, eticheta + ': malformed row');
for (const [k, d] of Object.entries(r.disclosed)) {
if (!d || typeof d.salt !== 'string') return ok(motive, false, eticheta + ': disclosed field ' + k + ' without a salt');
const okCamp = r.c[k] === angajamentCamp(k, d.value, d.salt) /*PAZA:camp*/;
if (!ok(motive, okCamp, eticheta + ': the disclosed value of "' + k + '" does not match its commitment')) return false;
}
const leaf = frunzaInregistrare(r.id, r.c);
if (!ok(motive, r.logProof && verifyInclusion({ ...r.logProof, leafHash: leaf, treeSize: commitment.size, rootHash: commitment.logRoot }) && r.logProof.index === r.i, eticheta + ': the record is not in the log of version ' + commitment.version)) return false;
if (field !== null) {
if (!ok(motive, r.disclosed[field] !== undefined, eticheta + ': the queried field "' + field + '" is not disclosed')) return false;
const ix = commitment.indexes[field];
const okIndex = r.indexProof && verifyInclusion({ ...r.indexProof, leafHash: frunzaIndex(field, r.disclosed[field].value, r.i, leaf), treeSize: ix.size, rootHash: ix.root }) && r.indexProof.index === r.pos;
if (!ok(motive, okIndex, eticheta + ': the row is not at position ' + r.pos + ' of the index "' + field + '"')) return false;
}
return true;
}
/** A query answer against a commitment: every row proves itself, the rows are the whole contiguous run, the neighbours are outside. */
export function verificaRaspuns(answer, commitment) {
const motive = [];
if (!answer || answer.kind !== KIND_RASPUNS || !commitment) return { verdict: 'INVALID', motive: ['not a ' + KIND_RASPUNS], rows: 0 };
ok(motive, answer.commitmentHash === hashAngajament(commitment) && answer.version === commitment.version && answer.name === commitment.name, 'the answer names another commitment');
const f = answer.field; const ix = commitment.indexes && commitment.indexes[f];
if (!ok(motive, !!ix, 'the field "' + f + '" is not indexed in the commitment')) return { verdict: 'INVALID', motive, rows: 0 };
ok(motive, answer.indexSize === ix.size, 'the answer\'s index size differs from the commitment\'s');
const from = answer.from, to = answer.to; const rows = Array.isArray(answer.rows) ? answer.rows : [];
if (from !== null && to !== null) ok(motive, comparaValori(from, to) <= 0, 'from is above to');
const inRange = (v) => (from === null || comparaValori(v, from) >= 0) && (to === null || comparaValori(v, to) <= 0);
let start = rows.length ? rows[0].pos : (answer.before ? answer.before.pos + 1 : 0);
for (const [k, r] of rows.entries()) {
if (!verificaRand(r, commitment, f, motive, 'row ' + k)) continue;
ok(motive, r.pos === start + k, 'row ' + k + ' breaks the contiguous run of the index');
ok(motive, inRange(r.disclosed[f].value), 'row ' + k + ' is outside the queried range');
if (k) { const okOrdine = comparaValori(rows[k - 1].disclosed[f].value, r.disclosed[f].value) < 0 || (comparaValori(rows[k - 1].disclosed[f].value, r.disclosed[f].value) === 0 && rows[k - 1].i < r.i) /*PAZA:ordine*/; ok(motive, okOrdine, 'rows ' + (k - 1) + ' and ' + k + ' are not in index order'); }
}
const end = start + rows.length;
const vecin = (n, pos, cond, m) => {
if (!n) return ok(motive, cond, m);
const okVecin = n.pos === pos && n.indexProof && verifyInclusion({ ...n.indexProof, leafHash: frunzaIndex(f, n.value, n.i, n.recordLeaf), treeSize: ix.size, rootHash: ix.root }) && n.indexProof.index === pos;
return ok(motive, okVecin, 'the neighbour at position ' + pos + ' does not prove itself in the index');
};
// completeness: the row before the run is below the range (or the run starts at 0), the row after is above it (or the run ends the index)
const okInainte = vecin(answer.before, start - 1, start === 0, 'the run does not start at 0 and no neighbour before it is shown') && (!answer.before || (from !== null && comparaValori(answer.before.value, from) < 0)) /*PAZA:completitudine*/;
ok(motive, okInainte, 'a row before the run could still be in the range (the answer may be incomplete)');
const okDupa = vecin(answer.after, end, end === ix.size, 'the run does not end the index and no neighbour after it is shown') && (!answer.after || (to !== null && comparaValori(answer.after.value, to) > 0)) /*PAZA:completitudine*/;
ok(motive, okDupa, 'a row after the run could still be in the range (the answer may be incomplete)');
return { verdict: motive.length ? 'INVALID' : 'VALID', motive, rows: rows.length };
}
/** One record (from `dovadaInregistrare` or the serving endpoint) against a commitment. */
export function verificaInregistrare(rec, commitment) {
const motive = [];
if (!rec || rec.kind !== KIND_INREGISTRARE) return { ok: false, motive: ['not a ' + KIND_INREGISTRARE] };
ok(motive, rec.commitmentHash === hashAngajament(commitment) && rec.version === commitment.version, 'the record names another commitment');
verificaRand(rec, commitment, null, motive, 'record ' + rec.i);
return { ok: motive.length === 0, motive };
}
/** A consistency proof between two commitments: the older log is a prefix of the newer (nothing rewritten, nothing removed). */
export function verificaConsistentaVersiuni(d, older, newer) {
const motive = [];
if (!d || d.kind !== KIND_CONSISTENTA || !d.proof) return { ok: false, motive: ['not a ' + KIND_CONSISTENTA] };
ok(motive, d.from.commitmentHash === hashAngajament(older) && d.to.commitmentHash === hashAngajament(newer), 'the proof names other commitments');
const okRadacini = d.proof.firstRoot === older.logRoot && d.proof.secondRoot === newer.logRoot && d.proof.firstSize === older.size && d.proof.secondSize === newer.size /*PAZA:consistenta*/;
ok(motive, okRadacini, 'the proof\'s roots or sizes are not the commitments\'');
ok(motive, verifyConsistency(d.proof), 'the older log is not a prefix of the newer (rewritten history)');
ok(motive, newer.previous === hashAngajament(older) || newer.version !== older.version + 1, 'the newer version does not name the older as previous');
return { ok: motive.length === 0, motive };
}
/**
* Availability sampling: k record positions drawn from a seed (random when none; the seed is printed so the draw can be repeated), each
* fetched from `url` + /record/<version>/<i> through `ia(url) -> JSON | null` and verified against the commitment. A record that is not
* served, or does not verify, fails the sample. What this measures: the publisher serves what it committed to, NOW, at the sampled
* positions; k samples bound the share of unserved records a publisher can hide only probabilistically ((1 - share)^k survive).
*/
export async function esantioneaza({ url, commitment, k = 16, seed = null, ia }) {
const n = commitment.size; const cerute = Math.min(Math.max(Number(k) || 1, 1), n);
const s = seed || crypto.randomBytes(16).toString('hex'); const pozitii = new Set(); let ctr = 0;
while (pozitii.size < cerute) { const h = crypto.createHash('sha256').update(s + ':' + ctr++).digest(); pozitii.add(Number(h.readBigUInt64BE(0) % BigInt(n))); }
const lipsa = [], motive = []; let verificate = 0;
for (const i of [...pozitii].sort((x, y) => x - y)) {
let rec = null; try { rec = await ia(url.replace(/\/$/, '') + '/record/' + commitment.version + '/' + i); } catch (e) { motive.push('record ' + i + ': ' + String(e.message).slice(0, 80)); }
if (!rec) { lipsa.push(i); continue; }
const r = verificaInregistrare(rec, commitment);
if (r.ok && rec.i === i) verificate++; else motive.push('record ' + i + ': ' + (rec.i !== i ? 'another record was served' : r.motive.join('; ')));
}
const okEsantion = lipsa.length === 0 && verificate === cerute /*PAZA:esantion*/;
if (lipsa.length) motive.push('not served: ' + lipsa.join(', '));
return { ok: okEsantion, seed: s, cerute, verificate, lipsa, motive };
}
/** The AIP-23 `data` envelope of a commitment: sha256 = SHA-256 of the canonical commitment, name = dataset@version. */
export function plicAngajament(commitment, { createdAt = commitment.createdAt } = {}) {
return buildProof('data', { content: utf8(canonical(commitment)), name: commitment.name + '@' + commitment.version, createdAt });
}
export function verificaPlicAngajament(env, commitment) {
const st = env && env.statement;
return !!st && env.kind === 'aere-proof-of-data-attestation' && st.sha256 === hashAngajament(commitment) && st.name === commitment.name + '@' + commitment.version && H.test(String(env.statementHash));
}

View File

@ -0,0 +1,67 @@
{
"commitment": {
"v": 1,
"format": "aere-verifiable-dataset-1",
"name": "proba",
"version": 2,
"size": 400,
"logRoot": "0x373a9e95032d46ea613cfedb887c7e79b11b2e9c211dc8ce4f8051c7d6534bb1",
"createdAt": "2026-10-02T08:01:00.000Z",
"indexes": {
"tara": {
"root": "0x9df01d08ddc09b3bf6a966f2446f0345148031100b5ca7d2d1cc27a6e4753c15",
"size": 400
},
"varsta": {
"root": "0xe5ca27f5a9e5308f719c74fc2b42c80b554e4321d512e785ce951da03f09d4d1",
"size": 400
}
},
"previous": "0x60000ce734e7479355b910c41cf1be305cd0242d16fd1d4a65ebcf43fcbc68f3"
},
"envelope": {
"v": 1,
"kind": "aere-proof-of-data-attestation",
"statement": {
"v": 1,
"kind": "aere-proof-of-data",
"sha256": "0xea7441856ff78c64bc549b763816a76fbe40b049c024843427d42761981741cb",
"name": "proba@2",
"createdAt": "2026-10-02T08:01:00.000Z"
},
"statementHash": "0xeb177d94c7bfa1de42c35a7096b454bda04c9456dad0ca6e5922f78b92192260",
"notarization": {
"chainId": 28001,
"notary": "0x70099E62735500AA2F85B60C518551a57B202d54",
"firstSeen": 1790929167,
"txHash": "0x39938e8ef28f62f60ad3d5f6a205d248e499fe1eda0e1e1851b1c43cc8f08c62",
"block": 4400052
}
},
"verdict": {
"verdict": "VALID",
"statementHash": "0xeb177d94c7bfa1de42c35a7096b454bda04c9456dad0ca6e5922f78b92192260",
"levels": [
{
"level": "form",
"state": "PASSED",
"detail": "kind=aere-proof-of-data-attestation"
},
{
"level": "integrity",
"state": "PASSED",
"detail": "sha256(statement) == statementHash (0xeb177d94c7bfa1de..)"
},
{
"level": "signature",
"state": "ABSENT",
"detail": "the envelope carries no signature"
},
{
"level": "finality",
"state": "PASSED",
"detail": "post-quantum: the state of block 4400063, the parent of anchor 4400064 whose post-quantum certificate verified here records this statementHash in AereNotary 0x70099e62735500aa2f85b60c518551a57b202d54 as first seen at 2026-10-02T08:19:27.000Z (1790929167); first seen in block 4400052, transaction 0x39938e8ef28f62f6.. (located through the endpoint's logs, not part of what is certified)"
}
]
}
}

View File

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

View File

@ -0,0 +1,52 @@
// serveste.mjs: the read-only HTTP face of a verifiable dataset (roadmap point 20, "availability"): commitments, records with their
// inclusion proofs, query answers with their completeness proofs, consistency proofs between versions. Every answer is checkable
// against a commitment with verifica.mjs, without trusting this server. What this server proves is only that the bytes were served
// when asked: availability sampling (verifica.mjs disponibilitate) draws random record positions and verifies each against the
// commitment; a publisher that withholds a record fails the sample that lands on it. This is retrievability under sampling, not an
// erasure-coded data-availability scheme.
// GET /v1/vd/<name>/commitments every version's commitment (+ its AIP-23 envelope, notarized or not)
// GET /v1/vd/<name>/commitment/<n>
// GET /v1/vd/<name>/record/<n>/<i> one record at version n with its log proof (all fields, or none with --privat)
// GET /v1/vd/<name>/query/<n>?field=f&eq=J | &from=J&to=J [&fields=a,b|*] [&limit=N] J = a JSON value ("x", 42, true, null)
// GET /v1/vd/<name>/consistency/<a>/<b>
// Only GET; JSON; the dataset is re-read when versiuni.json changes; bodies are bounded by the query limit (default 1000 rows).
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { deschide, versiune, dovadaInregistrare, interogheaza, dovadaConsistenta } from './baza.mjs';
const json = (res, cod, o) => { const b = Buffer.from(JSON.stringify(o)); res.writeHead(cod, { 'content-type': 'application/json', 'content-length': b.length, 'cache-control': 'no-store' }); res.end(b); };
const dinJson = (s) => { if (s === null || s === undefined) return undefined; try { return JSON.parse(s); } catch { throw new Error('a query value is JSON: "x", 42, true or null (got ' + String(s).slice(0, 40) + ')'); } };
/** Starts the server; resolves with { server, url, inchide }. `ascunde`: record positions the server withholds (a test hook, for the
* availability control: a publisher that does not serve a record). */
export function porneste({ dir, port = 0, host = '127.0.0.1', privat = false, limit = 1000, ascunde = [] } = {}) {
let db = deschide(dir); let mtime = fs.statSync(path.join(dir, 'versiuni.json')).mtimeMs;
const plic = (n) => { const f = path.join(dir, 'plicuri', n + '.json'); return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : null; };
const recarca = () => { const m = fs.statSync(path.join(dir, 'versiuni.json')).mtimeMs; if (m !== mtime) { db = deschide(dir); mtime = m; } };
const ascunse = new Set(ascunde.map(Number));
const server = http.createServer((req, res) => {
try {
if (req.method !== 'GET') return json(res, 405, { error: 'method_not_allowed' });
const url = new URL(req.url, 'http://x'); const p = url.pathname.split('/').filter(Boolean);
if (p[0] !== 'v1' || p[1] !== 'vd' || p.length < 4) return json(res, 404, { error: 'not_found' });
recarca();
if (p[2] !== db.set.name) return json(res, 404, { error: 'no_such_dataset' });
const n = (s) => { const x = Number(s); if (!Number.isInteger(x) || x < 0) throw new Error('not a non-negative integer: ' + s); return x; };
if (p[3] === 'health') return json(res, 200, { ok: true, name: db.set.name, versions: db.versiuni.length, records: db.inregistrari.length });
if (p[3] === 'commitments' && p.length === 4) return json(res, 200, { name: db.set.name, commitments: db.versiuni.map((c) => ({ commitment: c, envelope: plic(c.version) })) });
if (p[3] === 'commitment' && p.length === 5) { const c = versiune(db, n(p[4])); return json(res, 200, { commitment: c, envelope: plic(c.version) }); }
if (p[3] === 'record' && p.length === 6) { const i = n(p[5]); if (ascunse.has(i)) return json(res, 404, { error: 'record_not_served' }); return json(res, 200, dovadaInregistrare(db, n(p[4]), i, { fields: privat ? [] : '*' })); }
if (p[3] === 'query' && p.length === 5) {
const q = url.searchParams; const fields = q.get('fields'); const lim = Math.min(Number(q.get('limit') || limit) || limit, limit);
return json(res, 200, interogheaza(db, n(p[4]), { field: q.get('field'), eq: dinJson(q.get('eq')), from: dinJson(q.get('from')), to: dinJson(q.get('to')), fields: fields === '*' ? '*' : (fields ? fields.split(',').filter(Boolean) : []), limit: lim }));
}
if (p[3] === 'consistency' && p.length === 6) return json(res, 200, dovadaConsistenta(db, n(p[4]), n(p[5])));
return json(res, 404, { error: 'not_found' });
} catch (e) { return json(res, 400, { error: 'bad_request', detail: String(e.message).slice(0, 200) }); }
});
return new Promise((ok, nu) => {
server.on('error', nu);
server.listen(port, host, () => { const a = server.address(); ok({ server, url: `http://${host}:${a.port}/v1/vd/${db.set.name}`, inchide: () => new Promise((r) => server.close(r)) }); });
});
}

61
verifiable-data/vd.mjs Normal file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env node
// vd.mjs: the publisher's command line of a verifiable dataset (roadmap point 20). Values on the command line are JSON ("x", 42).
// node vd.mjs creeaza <dir> --nume <name> [--index f1,f2] a new dataset folder
// node vd.mjs adauga <dir> <records.ndjson|records.json> append records ({id, ...fields}); never rewrites
// node vd.mjs angajeaza <dir> [--out c.json] a new version: commitment + its AIP-23 envelope (plicuri/<n>.json)
// node vd.mjs notarizeaza <dir> <n> --rpc URL --key-file f --ethers-dir d [--notary 0x..]
// the envelope's statementHash on AereNotary (refuses 2800 unless AERE_CONFIRM_MAINNET=yes)
// node vd.mjs interogheaza <dir> <n> --camp f (--eq J | [--de-la J] [--pana-la J]) [--campuri a,b|*] [--out answer.json]
// node vd.mjs inregistrare <dir> <n> <i> [--campuri a,b|*] [--out rec.json]
// node vd.mjs consistenta <dir> <a> <b> [--out proof.json]
// node vd.mjs auditeaza <dir> rebuild every version from the records and compare the roots
// node vd.mjs serveste <dir> [--port 8797] [--privat] the read-only HTTP face (serveste.mjs)
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { creeaza, deschide, adauga, angajeaza, versiune, dovadaInregistrare, interogheaza, dovadaConsistenta, auditeaza, plicAngajament, hashAngajament } from './baza.mjs';
import { notarizeEnvelope } from '../agents/agent-ancora.mjs';
import { porneste } from './serveste.mjs';
const a = process.argv.slice(2); const cmd = a[0]; const dir = a[1];
const opt = (k, d = null) => { const i = a.indexOf(k); return i >= 0 && i + 1 < a.length ? a[i + 1] : d; };
const are = (k) => a.includes(k);
const J = (s) => (s === null ? undefined : JSON.parse(s));
const scrie = (o) => { const f = opt('--out'); const t = JSON.stringify(o, null, 1); if (f) { fs.writeFileSync(f, t); console.log('written ' + f); } else console.log(t); };
const plicuri = (d) => { const p = path.join(d, 'plicuri'); fs.mkdirSync(p, { recursive: true }); return p; };
try {
if (cmd === 'creeaza') { const db = creeaza(dir, { name: opt('--nume'), indexes: (opt('--index', '') || '').split(',').filter(Boolean) }); console.log('created ' + db.set.name + ' at ' + dir + ' (indexes: ' + (db.set.indexes.join(', ') || 'none') + ')'); }
else if (cmd === 'adauga') {
const db = deschide(dir); const t = fs.readFileSync(a[2], 'utf8');
const recs = a[2].endsWith('.ndjson') ? t.split('\n').filter(Boolean).map((l) => JSON.parse(l)) : JSON.parse(t);
console.log('appended ' + adauga(db, Array.isArray(recs) ? recs : [recs]) + ' records (' + db.inregistrari.length + ' in total)');
}
else if (cmd === 'angajeaza') {
const db = deschide(dir); const c = angajeaza(db); const env = plicAngajament(c);
fs.writeFileSync(path.join(plicuri(dir), c.version + '.json'), JSON.stringify(env, null, 1));
console.log(`version ${c.version}: ${c.size} records, log root ${c.logRoot.slice(0, 18)}.., commitment ${hashAngajament(c).slice(0, 18)}.., envelope ${env.statementHash.slice(0, 18)}..`);
scrie(c);
}
else if (cmd === 'notarizeaza') {
const db = deschide(dir); const c = versiune(db, a[2]); const f = path.join(dir, 'plicuri', c.version + '.json');
if (!fs.existsSync(f)) throw new Error('no envelope for version ' + c.version + ' (run angajeaza first)');
const env = JSON.parse(fs.readFileSync(f, 'utf8')); if (env.notarization) { console.log('already notarized: firstSeen ' + env.notarization.firstSeen); process.exitCode = 0; }
else {
const ethersDir = opt('--ethers-dir'); if (!ethersDir || !opt('--rpc') || !opt('--key-file')) throw new Error('--rpc, --key-file and --ethers-dir are required');
const { ethers } = createRequire(path.join(path.resolve(ethersDir), 'package.json'))('ethers');
const r = await notarizeEnvelope({ envelope: env, kind: 'aere-proof-of-data', rpc: opt('--rpc'), keyFile: opt('--key-file'), ethers, notary: opt('--notary') });
if (!r.ok) { console.log(r.motiv); process.exitCode = r.cod || 1; }
else { fs.writeFileSync(f, JSON.stringify(r.envelope, null, 1)); const n = r.envelope.notarization; console.log(`notarized on chain ${n.chainId}: firstSeen ${n.firstSeen}${n.txHash ? ', tx ' + n.txHash + ' block ' + n.block : ' (already)'}`); }
}
}
else if (cmd === 'interogheaza') {
const db = deschide(dir); const campuri = opt('--campuri', '');
scrie(interogheaza(db, a[2], { field: opt('--camp'), eq: J(opt('--eq')), from: J(opt('--de-la')), to: J(opt('--pana-la')), fields: campuri === '*' ? '*' : campuri.split(',').filter(Boolean), limit: Number(opt('--limita', 1000)) }));
}
else if (cmd === 'inregistrare') { const db = deschide(dir); const campuri = opt('--campuri', '*'); scrie(dovadaInregistrare(db, a[2], Number(a[3]), { fields: campuri === '*' ? '*' : campuri.split(',').filter(Boolean) })); }
else if (cmd === 'consistenta') { const db = deschide(dir); scrie(dovadaConsistenta(db, a[2], a[3])); }
else if (cmd === 'auditeaza') { const db = deschide(dir); const r = auditeaza(db); console.log(r.ok ? `AUDIT OK: ${db.versiuni.length} versions rebuilt from ${db.inregistrari.length} records, every root matches` : 'AUDIT FAILED:\n ' + r.probleme.join('\n ')); process.exitCode = r.ok ? 0 : 1; }
else if (cmd === 'serveste') { const s = await porneste({ dir, port: Number(opt('--port', 8797)), privat: are('--privat') }); console.log('serving ' + s.url + (are('--privat') ? ' (records without field values)' : '')); }
else { console.log('usage: vd.mjs creeaza|adauga|angajeaza|notarizeaza|interogheaza|inregistrare|consistenta|auditeaza|serveste ... (see the header)'); process.exitCode = 2; }
} catch (e) { console.log('ERROR: ' + String(e.message).slice(0, 300)); process.exitCode = 1; }

View File

@ -0,0 +1,54 @@
#!/usr/bin/env node
// verifica.mjs: the reader's side of a verifiable dataset, without the data (roadmap point 20). Everything is judged against a
// COMMITMENT (a version's JSON) and prints VALID / INVALID with the reasons; exit 0 VALID, 1 INVALID, 2 could not measure.
// node verifica.mjs raspuns <answer.json> --angajament <c.json> a query answer: correct, complete, in order
// node verifica.mjs inregistrare <record.json> --angajament <c.json> one record with its proof
// node verifica.mjs consistenta <proof.json> --vechi <c1.json> --nou <c2.json> the newer version only appended
// node verifica.mjs disponibilitate --url <http://host:port/v1/vd/name> --angajament <c.json> [--esantioane 16] [--seed <hex>]
// random records fetched and verified (retrievability)
// node verifica.mjs angajament <c.json> --plic <env.json> [--verify-proof <verify-proof.mjs>] [--rpc URL --chain 28001]
// the AIP-23 envelope is this commitment's; with --rpc,
// its notarization under a post-quantum certified anchor
import fs from 'node:fs';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { verificaRaspuns, verificaInregistrare, verificaConsistentaVersiuni, verificaPlicAngajament, esantioneaza, hashAngajament } from './baza.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const a = process.argv.slice(2); const cmd = a[0];
const opt = (k, d = null) => { const i = a.indexOf(k); return i >= 0 && i + 1 < a.length ? a[i + 1] : d; };
const citeste = (f) => JSON.parse(fs.readFileSync(f, 'utf8'));
const iesi = (verdict, motive = [], extra = '') => { console.log(`${verdict}${extra ? ' ' + extra : ''}${motive.length ? '\n ' + motive.join('\n ') : ''}`); process.exitCode = verdict === 'VALID' ? 0 : verdict === 'INVALID' ? 1 : 2; };
const ruleaza = (verifyProof, envFile, { rpc, chain }) => new Promise((ok) => {
const args = [verifyProof, envFile, '--json', ...(rpc ? ['--rpc', rpc] : []), ...(chain ? ['--chain', String(chain)] : [])];
const c = spawn(process.execPath, args); let out = ''; c.stdout.on('data', (d) => { out += d; }); c.stderr.on('data', () => {});
const ceas = setTimeout(() => c.kill(), 300000); c.on('error', () => { clearTimeout(ceas); ok(null); }); c.on('close', () => { clearTimeout(ceas); try { ok(JSON.parse(out)); } catch { ok(null); } });
});
async function main() {
if (cmd === 'raspuns') { const r = verificaRaspuns(citeste(a[1]), citeste(opt('--angajament'))); return iesi(r.verdict, r.motive, `(${r.rows} rows)`); }
if (cmd === 'inregistrare') { const r = verificaInregistrare(citeste(a[1]), citeste(opt('--angajament'))); return iesi(r.ok ? 'VALID' : 'INVALID', r.motive); }
if (cmd === 'consistenta') { const r = verificaConsistentaVersiuni(citeste(a[1]), citeste(opt('--vechi')), citeste(opt('--nou'))); return iesi(r.ok ? 'VALID' : 'INVALID', r.motive); }
if (cmd === 'disponibilitate') {
const url = opt('--url'); const c = citeste(opt('--angajament')); if (!url) throw new Error('--url is required');
const ia = async (u) => { const r = await fetch(u, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(20000) }); if (!r.ok) return null; return r.json(); };
const r = await esantioneaza({ url, commitment: c, k: Number(opt('--esantioane', 16)), seed: opt('--seed', null), ia });
return iesi(r.ok ? 'VALID' : 'INVALID', r.motive, `(${r.verificate} of ${r.cerute} sampled records verified, seed ${r.seed}${r.lipsa.length ? '; not served: ' + r.lipsa.join(',') : ''})`);
}
if (cmd === 'angajament') {
const c = citeste(a[1]); const env = citeste(opt('--plic'));
if (!verificaPlicAngajament(env, c)) return iesi('INVALID', ['the envelope is not this commitment\'s (sha256/name differ, or not an aere-proof-of-data attestation)']);
const vp = opt('--verify-proof', process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', '..', 'verificator-falcon', 'verify-proof.mjs'));
if (!fs.existsSync(vp)) return iesi('UNMEASURED', ['the AIP-23 verifier is not at ' + vp + ' (AERE_VERIFY_PROOF or --verify-proof)'], `commitment ${hashAngajament(c)} is the envelope's`);
const v = await ruleaza(vp, opt('--plic'), { rpc: opt('--rpc'), chain: opt('--chain') });
if (!v) return iesi('UNMEASURED', ['the verifier gave no JSON']);
const fin = (v.levels || []).find((l) => l.level === 'finality');
const extra = `envelope ${v.verdict}; finality ${fin ? fin.state + ': ' + String(fin.detail).slice(0, 160) : 'absent'}`;
if (v.verdict !== 'VALID') return iesi('INVALID', [], extra);
if (opt('--rpc') && !(fin && fin.state === 'PASSED')) return iesi(fin && fin.state === 'FAILED' ? 'INVALID' : 'UNMEASURED', [], extra);
return iesi('VALID', [], extra);
}
console.log('usage: verifica.mjs raspuns|inregistrare|consistenta|disponibilitate|angajament ... (see the header)'); process.exitCode = 2;
}
try { await main(); } catch (e) { iesi('UNMEASURED', [String(e.message).slice(0, 200)]); }