171 lines
18 KiB
JavaScript
171 lines
18 KiB
JavaScript
#!/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=<key> AERE_ETHERS_DIR=<dir with node_modules/ethers> 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=<hex> 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;
|
|
}
|