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.
273 lines
23 KiB
JavaScript
273 lines
23 KiB
JavaScript
// 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));
|
|
}
|