// piata.mjs: the dispute and settlement rail of the verifiable infrastructure market (roadmap master point 28, 2026-10-02), on what is // live: AereComputeMarketV3 (escrow, provider bond, challenge window, dispute with a challenger bond, arbiter with a permissionless // timeout, deadline reclaim), the AIP-23 execution envelope of the provider's result notarized on AereNotary (the chain's time for the // delivery, under a post-quantum certified anchor), and the AIP-23 settlement receipt of the terminal payout. This file has three parts: // 1. the deployment: the runtime code a market MUST have, rebuilt from the committed artifact and the constructor arguments // (`codulAsteptat`), so a reader checks eth_getCode before trusting any event the market emitted; // 2. the timeline: every event of a job with its block time and the sender of its transaction (`cronologie`), a JSON a reader can keep; // 3. the JUDGE (`judeca`): pure rules over the timeline, no chain: deadlines, the challenge window, who resolved, that the terminal // payout matches the state, that the result bytes hash to the on-chain commitment, that a REPLAY of the public spec agrees with // whoever the arbiter sided with, and that the provider's notarized envelope names this result and existed before the window // closed. Every rule carries a PAZA marker for the planting control. `chitanta` turns the terminal payout into an AIP-23 // `settlement` envelope, verifiable with the reference verifier. // ethers is injected (createRequire of the caller's ethers); the rest is node:crypto and the siblings beside this component. import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { fileURLToPath } from 'node:url'; import { buildProof } from '../proof-kinds/proof-kinds.mjs'; import { ruleaza, rezultatHash, rezultatOcteti, specHash as specHashOf, keccak, canonical } from './sarcina.mjs'; const AICI = path.dirname(fileURLToPath(import.meta.url)); export const ARTEFACT = path.join(AICI, 'AereComputeMarketV3.json'); export const STATUS = ['None', 'Open', 'Claimed', 'Submitted', 'Disputed', 'Paid', 'Refunded', 'Slashed']; /** The rules the judge names. A refusal can only name one of these (the helper throws otherwise), so the list is the whole list. */ export const REGULI = ['forma', 'intreg', 'postare', 'termen', 'fereastra', 'arbitru', 'plata', 'rezultat', 'replay', 'dovada']; export const MODE = ['REPLAY', 'ZK_VERIFIED', 'OPTIMISTIC']; const H32 = /^0x[0-9a-f]{64}$/i; const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); export function incarcaArtefact(f = ARTEFACT) { const a = JSON.parse(fs.readFileSync(f, 'utf8')); if (a.kind !== 'aere-compute-market-v3-artifact' || !a.deployedBytecode || !a.immutables || !a.abi) throw new Error('not a market artifact: ' + f); return a; } /** The runtime code of a deployment: the artifact's deployedBytecode with every immutable written at its positions. */ export function codulAsteptat(a, args) { const cod = Buffer.from(a.deployedBytecode.slice(2), 'hex'); for (const [nume, im] of Object.entries(a.immutables)) { const v = args[nume]; if (v === undefined) throw new Error('missing immutable ' + nume); const w = Buffer.alloc(32); if (/^address|^contract/.test(im.type)) Buffer.from(v.replace(/^0x/, '').padStart(40, '0'), 'hex').copy(w, 12); else if (/^bytes(\d+)$/.test(im.type)) Buffer.from(v.replace(/^0x/, ''), 'hex').copy(w, 0); else throw new Error('immutable type not handled: ' + im.type); for (const p of im.positions) { if (p.length !== 32) throw new Error('immutable slot not 32 bytes'); w.copy(cod, p.start); } } return '0x' + cod.toString('hex'); } /** eth_getCode of the market equals the artifact with these constructor arguments; returns { ok, motiv }. */ export async function verificaDesfasurarea({ provider, market, artifact, args }) { const viu = (await provider.getCode(market)).toLowerCase(); const asteptat = codulAsteptat(artifact, args).toLowerCase(); if (viu === '0x') return { ok: false, motiv: 'no code at ' + market }; if (viu !== asteptat) return { ok: false, motiv: `the code at ${market} is not AereComputeMarketV3 with these immutables (${viu.length / 2 - 1} vs ${asteptat.length / 2 - 1} bytes, first difference at byte ${[...viu].findIndex((c, i) => c !== asteptat[i]) >> 1})` }; return { ok: true, motiv: 'runtime code equals the artifact (' + (viu.length / 2 - 1) + ' bytes) with ZK_VERIFIER, PINNED_PROOF_SELECTOR, ARBITER, GOVERNANCE as given' }; } export const contract = (ethers, provider, market, artifact, signer = null) => new ethers.Contract(market, artifact.abi, signer || provider); const s = (v) => (typeof v === 'bigint' ? v.toString() : v); export function jobDinLant(j) { return { requester: j.requester, provider: j.provider, token: j.token, reward: s(j.reward), providerBond: s(j.providerBond), specHash: j.specHash, resultHash: j.resultHash, zkProgramVKey: j.zkProgramVKey, mode: Number(j.mode), status: Number(j.status), pqcSettlement: j.pqcSettlement, offchainResult: j.offchainResult, deadline: Number(j.deadline), challengeWindow: Number(j.challengeWindow), submittedAt: Number(j.submittedAt), disputedAt: Number(j.disputedAt) }; } /** The timeline of a job: its state, its constants, and every event (with block time and tx sender), oldest first. */ export async function cronologie({ ethers, provider, market, artifact, jobId, fromBlock = 0, spec = null, envelope = null }) { const c = contract(ethers, provider, market, artifact); const [net, job, arbiter, governance, timeout, zk, pin] = await Promise.all([provider.getNetwork(), c.getJob(jobId), c.ARBITER(), c.GOVERNANCE(), c.ARBITRATION_TIMEOUT(), c.ZK_VERIFIER(), c.PINNED_PROOF_SELECTOR()]); const nume = artifact.abi.filter((x) => x.type === 'event' && x.inputs[0] && x.inputs[0].name === 'id').map((x) => x.name); const brute = []; for (const n of nume) for (const l of await c.queryFilter(c.filters[n](jobId), fromBlock, 'latest')) brute.push(l); brute.sort((a, b) => a.blockNumber - b.blockNumber || a.index - b.index); const blocuri = new Map(), txs = new Map(); const events = []; for (const l of brute) { if (!blocuri.has(l.blockHash)) blocuri.set(l.blockHash, await provider.getBlock(l.blockHash)); if (!txs.has(l.transactionHash)) txs.set(l.transactionHash, await provider.getTransaction(l.transactionHash)); const b = blocuri.get(l.blockHash), t = txs.get(l.transactionHash); const args = {}; l.fragment.inputs.forEach((inp, i) => { args[inp.name] = s(l.args[i]); }); events.push({ name: l.fragment.name, args, blockNumber: l.blockNumber, blockHash: l.blockHash, timestamp: b.timestamp, txHash: l.transactionHash, from: t.from, logIndex: l.index }); } const sub = events.find((e) => e.name === 'ResultSubmitted'); return { chainId: Number(net.chainId), market, jobId: String(jobId), constants: { arbiter, governance, arbitrationTimeout: Number(timeout), zkVerifier: zk, pinnedProofSelector: pin }, job: jobDinLant(job), events, result: sub ? sub.args.result : null, spec, envelope }; } // ----- the judge: pure rules over a timeline ----- const ok = (m, cond, text) => { if (!cond) m.push(text); return !!cond; }; const low = (x) => String(x || '').toLowerCase(); export function judeca(d) { // a timeline the judge cannot read is refused by name, never thrown at the caller (2026-10-03: until today a missing field threw) try { return judecaForma(d); } catch (e) { return { verdict: 'INVALID', motive: ['malformed timeline: ' + String(e.message).slice(0, 120)], reguli: ['forma'], randuri: [] }; } } function judecaForma(d) { const motive = [], randuri = [], reguli = new Set(); const cere = (cond, text, regula) => { if (!REGULI.includes(regula)) throw new Error('piata: undeclared rule ' + regula); if (!cond) { motive.push(text); reguli.add(regula); } return !!cond; }; const j = d.job, ev = d.events || []; const un = (n) => ev.find((e) => e.name === n); const toate = (n) => ev.filter((e) => e.name === n); const t = (e) => (e ? e.timestamp : null); if (!j || !ev.length) return { verdict: 'INCOMPLETE', motive: ['no job or no events'], reguli: [], randuri }; const intregi = (...x) => x.every(Number.isInteger); const okForma = typeof j === 'object' && Array.isArray(ev) && ev.every((e) => e && typeof e === 'object' && typeof e.name === 'string' && e.args && typeof e.args === 'object' && typeof e.from === 'string' && intregi(e.timestamp, e.blockNumber)) && !!d.constants && typeof d.constants.arbiter === 'string' && intregi(d.constants.arbitrationTimeout, j.deadline, j.submittedAt, j.challengeWindow, j.status) && (d.result == null || typeof d.result === 'string') /*PAZA:forma*/; if (!cere(okForma, 'malformed timeline: events need name, args, from, integer timestamp and blockNumber; the job and the constants their integer fields', 'forma')) return { verdict: 'INVALID', motive, reguli: [...reguli].sort(), randuri }; for (const e of ev) randuri.push(`${new Date(e.timestamp * 1000).toISOString()} #${e.blockNumber} ${e.name} by ${e.from.slice(0, 10)}.. ${Object.entries(e.args).filter(([k]) => k !== 'result').map(([k, v]) => k + '=' + (H32.test(String(v)) ? String(v).slice(0, 10) + '..' : String(v).length > 20 ? String(v).slice(0, 10) + '..' : v)).join(' ')}`); const posted = un('JobPosted'), claimed = un('JobClaimed'), submitted = un('ResultSubmitted') || un('ResultReferenced'), disputed = un('JobDisputed'), resolved = un('DisputeResolved'), timedOut = un('DisputeTimedOut'), paid = un('JobPaid'), refunded = un('JobRefunded'), slashed = un('JobSlashed'); // the timeline is WHOLE against the job state: an event the state implies cannot be withheld (2026-10-03: until today a timeline // without its ResultSubmitted event skipped the challenge-window rule and came out VALID; found by the conformance suite) const okIntreg = !!posted && (!j.provider || /^0x0{40}$/.test(String(j.provider)) || !!claimed) && (!(j.submittedAt > 0) || !!submitted) && (!(j.disputedAt > 0) || !!disputed) /*PAZA:intreg*/; cere(okIntreg, 'the timeline lacks an event the job state implies (JobPosted; JobClaimed when a provider is set; the submission when submittedAt is set; JobDisputed when disputedAt is set)', 'intreg'); if (!posted) return { verdict: 'INVALID', motive, reguli: [...reguli].sort(), randuri }; // R0 the posting matches the job (the reward is compared through the payout events: the contract zeroes j.reward on settlement) cere(posted && low(posted.args.requester) === low(j.requester) && low(posted.args.specHash) === low(j.specHash) && Number(posted.args.deadline) === j.deadline && (j.reward === '0' || posted.args.reward === j.reward) /*PAZA:postare*/, 'the JobPosted event does not match the job (requester, reward, specHash, deadline)', 'postare'); // R1 deadlines: a claim and a submission only up to the deadline if (claimed) cere(t(claimed) <= j.deadline /*PAZA:termen*/, 'the job was claimed after its deadline', 'termen'); if (submitted) cere(t(submitted) <= j.deadline && t(submitted) === j.submittedAt /*PAZA:termen*/, 'the result was submitted after the deadline, or submittedAt does not match the event', 'termen'); if (refunded && claimed && !submitted && !disputed) cere(t(refunded) > j.deadline /*PAZA:termen*/, 'the escrow was reclaimed as expired before the deadline', 'termen'); // R2 the challenge window: a dispute strictly inside it, a finalize (by anyone but the requester) only after it if (disputed) cere(submitted && t(disputed) < j.submittedAt + j.challengeWindow /*PAZA:fereastra*/, 'the dispute was raised after the challenge window closed', 'fereastra'); if (paid && !resolved && submitted) cere(low(paid.from) === low(j.requester) || t(paid) >= j.submittedAt + j.challengeWindow /*PAZA:fereastra*/, 'the provider was paid inside the challenge window by someone other than the requester (acceptResult is the requester\'s)', 'fereastra'); // R3 who resolved: the arbiter, or the permissionless timeout after ARBITRATION_TIMEOUT if (resolved) cere(low(resolved.from) === low(d.constants.arbiter) /*PAZA:arbitru*/, 'the dispute was resolved by an address that is not the ARBITER', 'arbitru'); if (timedOut) cere(disputed && t(timedOut) >= j.disputedAt + d.constants.arbitrationTimeout /*PAZA:arbitru*/, 'the dispute was timed out before ARBITRATION_TIMEOUT', 'arbitru'); if (resolved || timedOut) cere(!!disputed /*PAZA:arbitru*/, 'a resolution without a dispute', 'arbitru'); // R4 the terminal payout matches the state const st = STATUS[j.status]; let terminal = null; if (st === 'Paid') { const okP = paid && low(paid.args.provider) === low(j.provider) && paid.args.reward === posted.args.reward && (!claimed || paid.args.bondReturned === claimed.args.providerBond) && (!resolved || resolved.args.providerWon === true) /*PAZA:plata*/; cere(okP, 'status Paid without a matching JobPaid (provider, reward, bond returned, providerWon)', 'plata'); terminal = paid && { status: st, payee: paid.args.provider, amount: paid.args.reward, txHash: paid.txHash, blockHash: paid.blockHash }; } else if (st === 'Slashed') { const okS = slashed && disputed && resolved && resolved.args.providerWon === false && low(slashed.args.challenger) === low(disputed.args.challenger) && (!claimed || slashed.args.slashedProviderBond === claimed.args.providerBond) /*PAZA:plata*/; cere(okS, 'status Slashed without a matching JobSlashed (challenger, provider bond) and DisputeResolved(false)', 'plata'); terminal = slashed && { status: st, payee: slashed.args.challenger, amount: slashed.args.slashedProviderBond, refundedTo: j.requester, txHash: slashed.txHash, blockHash: slashed.blockHash }; } else if (st === 'Refunded') { const okR = refunded && low(refunded.args.requester) === low(j.requester) && refunded.args.reward === posted.args.reward /*PAZA:plata*/; cere(okR, 'status Refunded without a matching JobRefunded (requester, reward)', 'plata'); terminal = refunded && { status: st, payee: refunded.args.requester, amount: refunded.args.reward, txHash: refunded.txHash, blockHash: refunded.blockHash }; } cere((j.status !== 5 || j.reward === '0') /*PAZA:plata*/, 'a paid job still holds a reward', 'plata'); // R5 the result bytes hash to the on-chain commitment if (d.result && submitted) cere(keccak(Buffer.from(d.result.slice(2), 'hex')) === low(j.resultHash) && low(submitted.args.resultHash) === low(j.resultHash) /*PAZA:rezultat*/, 'the submitted bytes do not hash to the job\'s resultHash', 'rezultat'); // R6 REPLAY: the public spec recomputed; the arbiter's decision must agree with the recomputation let replay = null; if (d.spec && d.result && submitted) { cere(specHashOf(d.spec) === low(j.specHash) /*PAZA:replay*/, 'the public spec does not hash to the job\'s specHash', 'replay'); try { const r = ruleaza(d.spec); const corect = rezultatHash(r) === low(j.resultHash); replay = { recomputedHash: rezultatHash(r), providerRight: corect }; if (disputed) { replay.challengerRecomputedRight = low(disputed.args.recomputedResultHash) === rezultatHash(r); } if (resolved) cere(resolved.args.providerWon === corect /*PAZA:replay*/, `the arbiter sided with the ${resolved.args.providerWon ? 'provider' : 'challenger'} but the replay says the provider is ${corect ? 'right' : 'wrong'}`, 'replay'); if (st === 'Paid' && !resolved) cere(corect /*PAZA:replay*/, 'the provider was paid for a result the replay does not reproduce (no one disputed in time)', 'replay'); } catch (e) { cere(false, 'the replay could not run: ' + e.message, 'replay'); } } // R7 the provider's envelope names this result and existed before the window closed if (d.envelope) { const e = d.envelope, stmt = e.statement || {}; const okE = e.kind === 'aere-proof-of-execution-attestation' && low(stmt.outputHash) === low(j.resultHash) && low(stmt.inputHash) === low(j.specHash) && sha256(Buffer.from(JSON.stringify(stmt), 'utf8')) === low(e.statementHash) /*PAZA:dovada*/; cere(okE, 'the execution envelope does not name this job\'s specHash/resultHash, or its statementHash is not its statement\'s', 'dovada'); if (e.notarization && submitted) cere(Number(e.notarization.firstSeen) <= j.submittedAt + j.challengeWindow /*PAZA:dovada*/, 'the envelope was notarized only after the challenge window closed', 'dovada'); } const terminalStatus = ['Paid', 'Refunded', 'Slashed'].includes(st); return { verdict: motive.length ? 'INVALID' : terminalStatus ? 'VALID' : 'INCOMPLETE', motive, reguli: [...reguli].sort(), randuri, status: st, terminal, replay }; } /** The AIP-23 settlement receipt of a terminal job: the payout transaction, the parties, the amount, the instruction (market, job, status). */ export function chitanta(d, { createdAt = new Date().toISOString() } = {}) { const r = judeca(d); if (r.verdict !== 'VALID' || !r.terminal) throw new Error('no valid terminal settlement to receipt: ' + (r.motive[0] || r.verdict)); const t = r.terminal; return buildProof('settlement', { chainId: d.chainId, txHash: t.txHash, blockHash: t.blockHash, from: d.market, to: t.payee, amount: t.amount, asset: 'native', instruction: Buffer.from(canonical({ market: d.market, jobId: d.jobId, status: t.status, resultHash: d.job.resultHash }), 'utf8'), createdAt }); } /** The provider's execution envelope for a result (spec in, result out), to notarize before submitting. */ export function plicExecutie({ spec, result, programSha256, createdAt = new Date().toISOString() }) { return buildProof('execution', { program: spec.task, inputHash: specHashOf(spec), outputHash: rezultatHash(result), programSha256, exitCode: 0, createdAt }); } export { ruleaza, rezultatHash, rezultatOcteti, specHashOf as specHash, canonical };