#!/usr/bin/env node // proba-piata-28001.mjs: the complete market flow MEASURED on testnet 28001 (roadmap point 28): order -> proof -> dispute with a deadline // -> settlement, on AereComputeMarketV3 deployed from the committed artifact, with test keys only (the requester's key from // AERE_TESTNET_KEY_FILE; the provider's and the arbiter's derived from it, never printed, funded from it). Never chain 2800. // S1 happy path post (escrow) -> claim (bond) -> compute -> execution envelope NOTARIZED -> submit -> finalize refused inside the // window (WindowOpen) -> window passes -> dispute refused (WindowClosed) -> finalize by a third party -> Paid; // the judge VALID; the envelope read back under a post-quantum certified anchor; the AIP-23 settlement receipt // S2 dispute upheld post -> claim -> a WRONG result -> the requester recomputes and disputes (bond) -> timeout refused // (ArbitrationTimeoutNotPassed), a non-arbiter refused (NotArbiter) -> the arbiter rules against the provider -> Slashed // S3 dispute lost post -> claim -> a correct result -> a bad-faith dispute -> the arbiter rules for the provider -> Paid, challenger forfeits // S4 deadline post (short deadline) -> claim -> no result -> reclaim refused before the deadline -> after it, reclaimExpired -> Refunded // S5 open job post (short deadline) -> after it, claim refused (DeadlinePassed) -> cancelJob -> Refunded // Every scenario's timeline is judged by piata.mjs (pure rules) and saved in dovezi/ for the offline laboratory (proba-judecata.mjs). // Exit 0 when every row is as named; 1 otherwise; 2 when the chain, the key or ethers are missing. // AERE_TESTNET_KEY_FILE= AERE_ETHERS_DIR= node proba-piata-28001.mjs [--fereastra 90] [--piata 0x..] 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 } from 'node:url'; import { incarcaArtefact, verificaDesfasurarea, contract, cronologie, judeca, chitanta, plicExecutie, ruleaza, rezultatOcteti, rezultatHash, specHash } from './piata.mjs'; import { specNou, programSha256 } from './sarcina.mjs'; import { notarizeEnvelope } from '../agents/agent-ancora.mjs'; const AICI = path.dirname(fileURLToPath(import.meta.url)); const RAD = path.resolve(AICI, '..', '..'); const a = process.argv.slice(2); const opt = (k, d) => { const i = a.indexOf(k); return i >= 0 ? a[i + 1] : d; }; const RPC = process.env.AERE_TESTNET_RPC || 'https://testnet-rpc.aere.network'; const KF = process.env.AERE_TESTNET_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 VERIFY_PROOF = process.env.AERE_VERIFY_PROOF || path.join(RAD, 'verificator-falcon', 'verify-proof.mjs'); const FEREASTRA = Number(opt('--fereastra', 90)); const NOTAR_28001 = '0x70099E62735500AA2F85B60C518551a57B202d54'; const PIN = '0x4388a21c'; const DOVEZI = path.join(AICI, 'dovezi'); fs.mkdirSync(DOVEZI, { recursive: true }); const DESF = path.join(AICI, 'desfasurare-28001.json'); const nem = (m) => { console.log('NEMASURAT: ' + m); process.exit(2); }; let verzi = 0, rosii = 0; const cer = (n, c, d = '') => { console.log(` [${c ? 'VERDE ' : 'ROSU '}] ${n}${d ? ' (' + String(d).slice(0, 200) + ')' : ''}`); c ? verzi++ : rosii++; }; if (!fs.existsSync(KF)) nem('no test key at ' + KF + ' (AERE_TESTNET_KEY_FILE)'); let ethers; try { ({ ethers } = createRequire(path.join(ETHERS_DIR, 'package.json'))('ethers')); } catch (e) { nem('ethers not loadable from ' + ETHERS_DIR + ': ' + e.message); } const cheia = (f) => { const l = fs.readFileSync(f, 'utf8').split(/\r?\n/).map((x) => x.trim()).find((x) => /^(d|PRIVATE_KEY)=/.test(x)) || ''; const h = l.replace(/^(d|PRIVATE_KEY)=/, '').replace(/^0x/, ''); if (!/^[0-9a-fA-F]{1,64}$/.test(h)) nem('the key file has no d= line'); return '0x' + h.padStart(64, '0'); }; const provider = new ethers.JsonRpcProvider(RPC, undefined, { staticNetwork: false }); const chainId = Number((await provider.getNetwork()).chainId); if (chainId !== 28001) nem(`the RPC serves chain ${chainId}; this flow runs only on testnet 28001`); const req = new ethers.Wallet(cheia(KF), provider); const deriva = (rol) => new ethers.Wallet('0x' + crypto.createHash('sha256').update(req.privateKey + ':' + rol).digest('hex'), provider); // derived, never printed const prov = deriva('provider'), arb = deriva('arbiter'); const art = incarcaArtefact(); const E = (n) => ethers.parseEther(n); const ora = async () => (await provider.getBlock('latest')).timestamp; const asteapta = async (ts, ce) => { process.stdout.write(` waiting for chain time >= ${ts} (${ce})`); while ((await ora()) < ts) { await new Promise((r) => setTimeout(r, 3000)); process.stdout.write('.'); } console.log(''); }; const tx = async (nume, p) => { const r = await (await p).wait(1, 180000); if (!r || r.status !== 1) throw new Error(nume + ' failed'); console.log(` ${nume}: tx ${r.hash.slice(0, 18)}.. block ${r.blockNumber} gas ${r.gasUsed}`); return r; }; // a refusal by a NAMED custom error (static call), not just "it reverted" const refuza = async (nume, fn, eroare, c) => { try { await fn(); return cer(nume, false, 'did not revert'); } catch (e) { let n = e.revert && e.revert.name; if (!n && e.data) { try { n = c.interface.parseError(e.data).name; } catch {} } if (!n && e.info && e.info.error && e.info.error.data) { try { n = c.interface.parseError(e.info.error.data).name; } catch {} } cer(nume, n === eroare, 'reverted with ' + (n || String(e.shortMessage || e.message).slice(0, 80))); } }; const verifyProof = (env, rpc = null) => { const f = path.join(os.tmpdir(), 'piata-' + crypto.randomUUID() + '.json'); fs.writeFileSync(f, JSON.stringify(env)); try { const r = spawnSync(process.execPath, [VERIFY_PROOF, f, '--json', ...(rpc ? ['--rpc', rpc, '--chain', '28001'] : [])], { encoding: 'utf8', timeout: 300000 }); try { return JSON.parse(r.stdout); } catch { return null; } } finally { fs.rmSync(f, { force: true }); } }; const dovada = { la: new Date().toISOString(), chainId, rpc: RPC, fereastra: FEREASTRA, scenarii: {} }; // ----- deployment (reused when the code at the recorded address is the artifact with our immutables) ----- const ARGS = { ZK_VERIFIER: NOTAR_28001, PINNED_PROOF_SELECTOR: PIN, ARBITER: arb.address, GOVERNANCE: req.address }; let piata = opt('--piata', null); if (!piata && fs.existsSync(DESF)) { const d = JSON.parse(fs.readFileSync(DESF, 'utf8')); if (d.chainId === chainId) piata = d.market; } if (piata) { const v = await verificaDesfasurarea({ provider, market: piata, artifact: art, args: ARGS }); if (!v.ok) { console.log(' the recorded market does not verify (' + v.motiv + '); deploying anew'); piata = null; } } if (!piata) { console.log(` deploying AereComputeMarketV3 from the artifact (${art.solc}; ZK_VERIFIER = AereNotary 28001: a contract with code, so ZK_VERIFIED jobs can never pay here; pin ${PIN})`); const f = new ethers.ContractFactory(art.abi, art.bytecode, req); const c = await f.deploy(ARGS.ZK_VERIFIER, ARGS.ARBITER, ARGS.GOVERNANCE, ARGS.PINNED_PROOF_SELECTOR); const rc = await c.deploymentTransaction().wait(1, 180000); piata = await c.getAddress(); fs.writeFileSync(DESF, JSON.stringify({ chainId, market: piata, deployTx: rc.hash, block: rc.blockNumber, args: ARGS, artifactSourceSha256: art.sourceSha256, at: new Date().toISOString() }, null, 1)); console.log(` deployed at ${piata}, tx ${rc.hash.slice(0, 18)}.. block ${rc.blockNumber} gas ${rc.gasUsed}`); } const desf = JSON.parse(fs.readFileSync(DESF, 'utf8')); const vd = await verificaDesfasurarea({ provider, market: piata, artifact: art, args: ARGS }); cer('D1 the market\'s runtime code is the committed artifact with ZK_VERIFIER, pin, ARBITER, GOVERNANCE as recorded', vd.ok, vd.motiv); dovada.desfasurare = { ...desf, codeOk: vd.ok }; // ----- funding the derived keys from the requester's test key ----- for (const [w, n, min] of [[prov, 'provider', '0.06'], [arb, 'arbiter', '0.03']]) { if ((await provider.getBalance(w.address)) < E(min)) await tx('fund ' + n, req.sendTransaction({ to: w.address, value: E(min) })); } const C = (w) => contract(ethers, provider, piata, art, w); const posteaza = async (spec, { deadlineIn = 600, window = FEREASTRA, reward = '0.01', bond = '0.004' } = {}) => { const deadline = (await ora()) + deadlineIn; const r = await tx('postJob', C(req).postJob({ token: ethers.ZeroAddress, reward: E(reward), providerBond: E(bond), specHash: specHash(spec), zkProgramVKey: ethers.ZeroHash, mode: 0, deadline, challengeWindow: window, pqcSettlement: false, offchainResult: false }, { value: E(reward) })); const id = Number(C(req).interface.parseLog(r.logs.find((l) => l.address.toLowerCase() === piata.toLowerCase())).args.id); return { id, deadline, bond: E(bond) }; }; const scenariu = async (nume, titlu, fn) => { console.log(`\n${nume} ${titlu}`); const spec = specNou({ iters: 20000 }); const s = { spec, pasi: [] }; dovada.scenarii[nume] = s; try { await fn(spec, s); } catch (e) { cer(nume + ' did not break', false, e.shortMessage || e.message); } }; const judecaSiSalveaza = async (nume, s, id, extra = {}) => { const d = await cronologie({ ethers, provider, market: piata, artifact: art, jobId: id, fromBlock: desf.block, spec: s.spec, envelope: extra.envelope || null }); const r = judeca(d); s.date = d; s.judecata = r; for (const l of r.randuri) console.log(' ' + l); cer(`${nume} the judge on the timeline: VALID, status ${r.status}${r.replay ? ', replay ' + (r.replay.providerRight ? 'confirms' : 'contradicts') + ' the provider' : ''}`, r.verdict === 'VALID', r.motive.join('; ')); return { d, r }; }; try { await scenariu('S1', 'happy path: order -> proof notarized -> submit -> window -> finalize -> Paid', async (spec, s) => { const { id } = await posteaza(spec); s.jobId = id; await tx('claimJob (provider, bond)', C(prov).claimJob(id, '0x', { value: E('0.004') })); const t0 = Date.now(); const rez = ruleaza(spec); s.computeMs = Date.now() - t0; const env = plicExecutie({ spec, result: rez, programSha256: programSha256() }); // one notarization formula for the whole house (agents/agent-ancora.mjs): it reads the key from a file, so the provider's derived // key goes through a 0600 temp file that lives only for this call const kt = path.join(os.tmpdir(), 'piata-prov-' + process.pid + '.key'); fs.writeFileSync(kt, 'd=' + prov.privateKey.slice(2) + '\n', { mode: 0o600 }); let n; try { n = await notarizeEnvelope({ envelope: env, kind: 'aere-proof-of-execution', rpc: RPC, keyFile: kt, ethers }); } finally { fs.rmSync(kt, { force: true }); } if (!n.ok) throw new Error('notarization of the execution envelope: ' + n.motiv); const notarizat = n.envelope; console.log(` notarized the execution envelope (provider): firstSeen ${notarizat.notarization.firstSeen}${notarizat.notarization.txHash ? ', tx ' + notarizat.notarization.txHash.slice(0, 18) + '..' : ''}`); s.envelope = notarizat; await tx('submitResult (provider)', C(prov).submitResult(id, rezultatOcteti(rez))); await refuza('S1 finalize inside the window is refused: WindowOpen', () => C(arb).finalize.staticCall(id, '0x'), 'WindowOpen', C(arb)); const j = await C(req).getJob(id); await asteapta(Number(j.submittedAt) + FEREASTRA, 'the challenge window'); await refuza('S1 a dispute after the window is refused: WindowClosed', () => C(req).dispute.staticCall(id, rezultatHash(rez), { value: E('0.004') }), 'WindowClosed', C(req)); const before = await provider.getBalance(prov.address); await tx('finalize (a third party: the arbiter key as "anyone")', C(arb).finalize(id, '0x')); const after = await provider.getBalance(prov.address); cer('S1 the provider received reward + bond (0.014 tAERE) at finalize', after - before === E('0.014'), ethers.formatEther(after - before) + ' tAERE'); const { d, r } = await judecaSiSalveaza('S1', s, id, { envelope: notarizat }); let v = null; for (let k = 0; k < 24; k++) { v = verifyProof(notarizat, RPC); const f = v && (v.levels || []).find((l) => l.level === 'finality'); if (f && f.state !== 'PENDING') break; await new Promise((ok) => setTimeout(ok, 5000)); } const fin = v && (v.levels || []).find((l) => l.level === 'finality'); s.verifyProof = v; cer('S1 the provider\'s execution envelope verifies with the reference verifier under a post-quantum certified anchor (finality PASSED)', v && v.verdict === 'VALID' && fin && fin.state === 'PASSED', fin ? fin.state + ': ' + String(fin.detail).slice(0, 120) : '?'); cer('S1 the notarized proof existed before the window closed (firstSeen <= submittedAt + window)', notarizat.notarization.firstSeen <= d.job.submittedAt + d.job.challengeWindow, `firstSeen ${notarizat.notarization.firstSeen}, window end ${d.job.submittedAt + d.job.challengeWindow}`); const ch = chitanta(d); s.chitanta = ch; const vc = verifyProof(ch); s.chitantaVerdict = vc; cer('S1 the settlement receipt (AIP-23 settlement) names the payout tx, the provider and 0.01 tAERE, and verifies (VALID, finality ABSENT: not notarized)', ch.statement.txHash === r.terminal.txHash && ch.statement.to.toLowerCase() === prov.address.toLowerCase() && ch.statement.amount === E('0.01').toString() && vc && vc.verdict === 'VALID', vc && (vc.levels || []).map((l) => l.level + ':' + l.state).join(' ')); }); await scenariu('S2', 'dispute upheld: a wrong result, the requester recomputes and disputes, the arbiter slashes', async (spec, s) => { const { id } = await posteaza(spec); s.jobId = id; await tx('claimJob (provider, bond)', C(prov).claimJob(id, '0x', { value: E('0.004') })); const rez = ruleaza(spec); const gresit = { ...rez, digest: '0x' + 'ee'.repeat(32) }; await tx('submitResult (provider, WRONG digest)', C(prov).submitResult(id, rezultatOcteti(gresit))); const recomputat = rezultatHash(ruleaza(spec)); await tx('dispute (requester as challenger, recomputed hash, bond)', C(req).dispute(id, recomputat, { value: E('0.004') })); await refuza('S2 resolveByTimeout before ARBITRATION_TIMEOUT is refused: ArbitrationTimeoutNotPassed', () => C(req).resolveByTimeout.staticCall(id), 'ArbitrationTimeoutNotPassed', C(req)); await refuza('S2 resolveDispute by a non-arbiter is refused: NotArbiter', () => C(req).resolveDispute.staticCall(id, false), 'NotArbiter', C(req)); const before = await provider.getBalance(req.address); const r = await tx('resolveDispute(false) by the arbiter', C(arb).resolveDispute(id, false)); const after = await provider.getBalance(req.address); cer('S2 the requester got the reward back + its bond back + the provider\'s slashed bond (0.018 tAERE)', after - before === E('0.018'), ethers.formatEther(after - before) + ' tAERE'); await judecaSiSalveaza('S2', s, id); s.gasResolve = String(r.gasUsed); }); await scenariu('S3', 'dispute lost: a correct result disputed in bad faith; the arbiter sides with the provider', async (spec, s) => { const { id } = await posteaza(spec); s.jobId = id; await tx('claimJob (provider, bond)', C(prov).claimJob(id, '0x', { value: E('0.004') })); const rez = ruleaza(spec); await tx('submitResult (provider, correct)', C(prov).submitResult(id, rezultatOcteti(rez))); await tx('dispute (requester, a made-up recomputed hash, bond)', C(req).dispute(id, '0x' + 'dd'.repeat(32), { value: E('0.004') })); const before = await provider.getBalance(prov.address); await tx('resolveDispute(true) by the arbiter', C(arb).resolveDispute(id, true)); const after = await provider.getBalance(prov.address); cer('S3 the provider received reward + its bond + the challenger\'s forfeited bond (0.018 tAERE)', after - before === E('0.018'), ethers.formatEther(after - before) + ' tAERE'); await judecaSiSalveaza('S3', s, id); }); await scenariu('S4', 'deadline: claimed, never delivered; the requester reclaims after the deadline, the bond is slashed to it', async (spec, s) => { const { id, deadline } = await posteaza(spec, { deadlineIn: 45 }); s.jobId = id; await tx('claimJob (provider, bond)', C(prov).claimJob(id, '0x', { value: E('0.004') })); await refuza('S4 reclaimExpired before the deadline is refused: DeadlineNotPassed', () => C(req).reclaimExpired.staticCall(id), 'DeadlineNotPassed', C(req)); await asteapta(deadline + 1, 'the deadline'); await refuza('S4 submitResult after the deadline is refused: DeadlinePassed', () => C(prov).submitResult.staticCall(id, rezultatOcteti(ruleaza(spec))), 'DeadlinePassed', C(prov)); const before = await provider.getBalance(req.address); const r = await tx('reclaimExpired (requester)', C(req).reclaimExpired(id)); const after = await provider.getBalance(req.address); const gaz = r.gasUsed * (r.gasPrice || r.effectiveGasPrice || 0n); cer('S4 the requester got the reward + the slashed provider bond (0.014 tAERE, minus its own gas)', after - before + gaz === E('0.014'), ethers.formatEther(after - before + gaz) + ' tAERE net of gas'); await judecaSiSalveaza('S4', s, id); }); await scenariu('S5', 'an open job past its deadline: a claim is refused, the requester cancels', async (spec, s) => { const { id, deadline } = await posteaza(spec, { deadlineIn: 30 }); s.jobId = id; await asteapta(deadline + 1, 'the deadline'); await refuza('S5 claimJob after the deadline is refused: DeadlinePassed', () => C(prov).claimJob.staticCall(id, '0x', { value: E('0.004') }), 'DeadlinePassed', C(prov)); await tx('cancelJob (requester)', C(req).cancelJob(id)); await judecaSiSalveaza('S5', s, id); }); } finally { dovada.rezumat = { verzi, rosii }; const out = path.join(DOVEZI, 'piata-28001-' + new Date().toISOString().replace(/[:.]/g, '').slice(0, 15) + 'Z.json'); fs.writeFileSync(out, JSON.stringify(dovada, null, 1)); console.log('\n evidence: ' + path.relative(RAD, out)); console.log(`market settlement on 28001: ${verzi} VERDE, ${rosii} ROSU`); process.exitCode = rosii ? 1 : 0; }