aere-quantum/payments/proba-plata-28001.mjs

116 lines
14 KiB
JavaScript

#!/usr/bin/env node
// proba-plata-28001.mjs: the complete payment flow of a PERSON or a COMPANY (not an agent), measured on testnet 28001 (roadmap point 25):
// identity (the payer's address; a compliance record may be named, it is not judged here) -> authorization (the payer signs the
// transaction itself) -> RISK (risc.mjs: a declared policy, the USD rate declared as an input, the issuer's own receipts as history)
// -> payment (a plain native transfer) -> proof (the transaction and its receipt read back) -> settlement (the receipt's envelope
// notarized by the payee on AereNotary and read back under a post-quantum certified anchor) -> RECEIPT (instruction + AIP-23 settlement
// envelope, verified with verifica-chitanta.mjs and with the reference verifier). Test keys only: the payer's from AERE_TESTNET_KEY_FILE,
// the payee's derived from it (never printed), funded from it for the notarization gas. Never chain 2800.
// P1 0.4 tAERE at the declared rate 0.5 USD/tAERE = 0.20 USD: ALLOW -> paid -> notarized -> VALID (rules, chain, finality PASSED)
// P2 3 tAERE = 1.50 USD: REFUSED above the per-payment limit; no transaction is sent (measured: the payer's nonce does not move)
// P3 two more payments of 0.20 USD: ALLOW (history = this issuer's receipts); the fourth: REFUSED above the window count (4 > 3)
// N1 the receipt of P1 forged (amount changed): INVALID for the named reason; N2 a looser policy under the same hash: INVALID
// AERE_TESTNET_KEY_FILE=<key> AERE_ETHERS_DIR=<dir with node_modules/ethers> node proba-plata-28001.mjs
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 { definePolicy, judeca, hashOf } from './risc.mjs';
import { construiesteChitanta, verificaChitanta } from './chitanta.mjs';
import { notarizeEnvelope } from '../agents/agent-ancora.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const RAD = path.resolve(AICI, '..', '..');
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 DOVEZI = path.join(AICI, 'dovezi'); fs.mkdirSync(DOVEZI, { recursive: true });
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, 220) + ')' : ''}`); c ? verzi++ : rosii++; };
if (!fs.existsSync(KF)) nem('no test key at ' + KF);
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}; only testnet 28001`);
const payer = new ethers.Wallet(cheia(KF), provider);
const payee = new ethers.Wallet('0x' + crypto.createHash('sha256').update(payer.privateKey + ':payee-company').digest('hex'), provider); // derived, never printed
const E = (n) => ethers.parseEther(n);
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; };
const verifyProof = (env, rpc = null) => { const f = path.join(os.tmpdir(), 'plata-' + 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 }); } };
// the policy of this issuer (the payee company), declared; the refused counterparties are addresses the house MEASURED as sinks on 28001:
// contracts with no path to withdraw what is sent to them (deployment records in this repository), not a sanctions list
const desfPiata = (() => { try { return JSON.parse(fs.readFileSync(path.join(RAD, 'tools', 'aere-market-settlement', 'desfasurare-28001.json'), 'utf8')).market; } catch { return null; } })();
const { policy, policyHash } = definePolicy({ v: 1, kind: 'aere-payment-risk-policy', id: 'payee-company-28001-2026-10-02', currency: 'USD', perPaymentMaxUsd: '1.00', window: { seconds: 86400, maxUsd: '2.50', maxCount: 3 },
allowedAssets: [{ chainId: 28001, asset: 'native', decimals: 18 }],
refusedCounterparties: [{ address: '0x70099E62735500AA2F85B60C518551a57B202d54', reason: 'a contract with no path to withdraw what is sent to it (AereNotary on 28001)', source: 'contracts/deployments/notary-28001.json' },
...(desfPiata ? [{ address: desfPiata, reason: 'a contract that only pays out through its own job flow (AereComputeMarketV3 on 28001): a plain transfer to it is lost', source: 'tools/aere-market-settlement/desfasurare-28001.json' }] : []),
{ address: '0x0000000000000000000000000000000000000000', reason: 'the zero address: funds sent there are unrecoverable', source: 'measured on every EVM chain' }] });
const RATE = { usdPerUnit: '0.5', source: 'declared by the payee for this test (28001 has no market price; tAERE is a test asset)', at: new Date().toISOString() };
const dovada = { la: new Date().toISOString(), chainId, rpc: RPC, policy, policyHash, rate: RATE, plati: [], istoric: [] };
const istoric = []; // the issuer's own receipts: the only history the risk step has
const inputsPentru = (amount, now) => ({ chainId, payer: payer.address.toLowerCase(), payee: payee.address.toLowerCase(), asset: 'native', amount: amount.toString(), rate: RATE, identity: null, history: istoric.map((h) => ({ ...h })), now });
const acum = async () => (await provider.getBlock('latest')).timestamp;
try {
if ((await provider.getBalance(payee.address)) < E('0.02')) await tx('fund the payee company (gas for notarization)', payer.sendTransaction({ to: payee.address, value: E('0.02') }));
console.log(` issuer policy ${policyHash.slice(0, 18)}..: per payment 1.00 USD, window 24 h 2.50 USD / 3 payments, ${policy.refusedCounterparties.length} refused counterparties (measured sinks), rate declared ${RATE.usdPerUnit} USD/tAERE`);
// ----- P1: a payment that passes, paid, notarized, verified -----
const plateste = async (eticheta, amount) => {
const now = await acum(); const inputs = inputsPentru(amount, now); const v = judeca(policy, inputs);
console.log(` ${eticheta}: risk ${v.decision} for ${ethers.formatEther(amount)} tAERE = ${v.measured.amountUsd ?? '?'} USD${v.reasons.length ? ' (' + v.reasons.join('; ') + ')' : ''}`);
if (v.decision !== 'ALLOW') return { v, inputs };
const r = await tx(eticheta + ' native transfer (the payer signs)', payer.sendTransaction({ to: payee.address, value: amount }));
const { instruction, envelope } = construiesteChitanta({ chainId, txHash: r.hash, blockHash: r.blockHash, payer: payer.address, payee: payee.address, asset: 'native', decimals: 18, amount: amount.toString(), rate: RATE, verdict: v, policyId: policy.id, purpose: 'test purchase ' + eticheta });
istoric.push({ at: now, amountUsd: instruction.amountUsd, payee: payee.address.toLowerCase(), txHash: r.hash });
return { v, inputs, r, instruction, envelope };
};
const p1 = await plateste('P1', E('0.4'));
cer('P1 0.4 tAERE at 0.5 USD/tAERE = 0.20 USD: ALLOW, paid, receipt issued', !!p1.r && p1.instruction.amountUsd === '0.200000', p1.instruction && p1.instruction.amountUsd + ' USD, tx ' + p1.r.hash.slice(0, 18) + '..');
const txP1 = await provider.getTransaction(p1.r.hash), rcP1 = await provider.getTransactionReceipt(p1.r.hash);
const chainFacts = { tx: { from: txP1.from, to: txP1.to, value: txP1.value.toString(), blockHash: txP1.blockHash }, receipt: { status: rcP1.status, blockHash: rcP1.blockHash } };
const v1 = verificaChitanta({ instruction: p1.instruction, envelope: p1.envelope }, { policy, inputs: p1.inputs, ...chainFacts });
cer('P1 the receipt verifies: envelope commits to the instruction, USD exact, policy under hash, verdict reproduced, the transaction on chain is this payment', v1.verdict === 'VALID', v1.motive.join('; '));
// the payee (the company) notarizes the receipt's envelope: one notarization formula for the house, the key through a 0600 temp file
const kt = path.join(os.tmpdir(), 'plata-payee-' + process.pid + '.key'); fs.writeFileSync(kt, 'd=' + payee.privateKey.slice(2) + '\n', { mode: 0o600 });
let n; try { n = await notarizeEnvelope({ envelope: p1.envelope, kind: 'aere-proof-of-settlement', rpc: RPC, keyFile: kt, ethers }); } finally { fs.rmSync(kt, { force: true }); }
cer('P1 the receipt\'s envelope notarized on 28001 by the payee', n.ok, n.ok ? 'firstSeen ' + n.envelope.notarization.firstSeen + (n.envelope.notarization.txHash ? ', tx ' + n.envelope.notarization.txHash.slice(0, 18) + '..' : '') : n.motiv);
const chitanta = { instruction: p1.instruction, envelope: n.ok ? n.envelope : p1.envelope, policy, inputs: p1.inputs };
const fisier = path.join(DOVEZI, 'chitanta-28001-' + new Date().toISOString().replace(/[:.]/g, '').slice(0, 15) + 'Z.json'); fs.writeFileSync(fisier, JSON.stringify(chitanta, null, 1));
let vf = null; for (let k = 0; k < 24; k++) { vf = verifyProof(chitanta.envelope, RPC); const f = vf && (vf.levels || []).find((l) => l.level === 'finality'); if (f && f.state !== 'PENDING') break; await new Promise((ok) => setTimeout(ok, 5000)); }
const fin = vf && (vf.levels || []).find((l) => l.level === 'finality');
cer('P1 the reference verifier: envelope VALID, finality PASSED under a post-quantum certified anchor', vf && vf.verdict === 'VALID' && fin && fin.state === 'PASSED', fin ? fin.state + ': ' + String(fin.detail).slice(0, 140) : '?');
const cli = spawnSync(process.execPath, [path.join(AICI, 'verifica-chitanta.mjs'), fisier, '--rpc', RPC, '--verify-proof', VERIFY_PROOF], { encoding: 'utf8', timeout: 300000 });
cer('P1 verifica-chitanta.mjs (the public reader, plain JSON-RPC, no ethers): exit 0, VALID, notarized', cli.status === 0 && /^VALID, notarized/m.test(cli.stdout), (cli.stdout || '').trim().split('\n').pop());
dovada.plati.push({ eticheta: 'P1', instruction: p1.instruction, verificare: v1, verifyProof: vf, cli: (cli.stdout || '').trim().split('\n') });
// ----- P2: above the per-payment limit: refused, nothing sent -----
const nonce0 = await provider.getTransactionCount(payer.address);
const p2 = await plateste('P2', E('3'));
const nonce1 = await provider.getTransactionCount(payer.address);
cer('P2 3 tAERE = 1.50 USD: REFUSED above the per-payment limit, no transaction sent (nonce unchanged)', p2.v.decision === 'REFUSE' && /per-payment limit/.test(p2.v.reasons.join()) && nonce0 === nonce1, p2.v.reasons.join('; '));
dovada.plati.push({ eticheta: 'P2', verdict: p2.v, nonce: [nonce0, nonce1] });
// ----- P3: the window count: two more pass, the fourth is refused -----
const p3a = await plateste('P3a', E('0.4')), p3b = await plateste('P3b', E('0.4'));
const nonce2 = await provider.getTransactionCount(payer.address);
const p3c = await plateste('P3c', E('0.4'));
const nonce3 = await provider.getTransactionCount(payer.address);
cer('P3 the second and third payments of 0.20 USD pass (history = this issuer\'s receipts); the fourth is REFUSED above the window count (4 > 3), nothing sent', !!p3a.r && !!p3b.r && p3c.v.decision === 'REFUSE' && /window count/.test(p3c.v.reasons.join()) && nonce2 === nonce3, p3c.v.reasons.join('; ') + '; window ' + p3c.v.measured.windowUsd + ' USD / ' + p3c.v.measured.windowCount);
dovada.plati.push({ eticheta: 'P3', chitante: [p3a.instruction, p3b.instruction], refuz: p3c.v });
// ----- negatives on the real receipt -----
const fals = JSON.parse(JSON.stringify(chitanta)); fals.instruction.amount = '500000000000000000'; fals.inputs.amount = fals.instruction.amount;
const vn1 = verificaChitanta({ instruction: fals.instruction, envelope: fals.envelope }, { policy, inputs: fals.inputs, ...chainFacts });
cer('N1 the receipt forged (amount 0.5 instead of 0.4): INVALID, the envelope no longer commits to the instruction and the chain disagrees', vn1.verdict === 'INVALID' && /does not commit/.test(vn1.motive.join()), vn1.motive.join('; '));
const vn2 = verificaChitanta({ instruction: chitanta.instruction, envelope: chitanta.envelope }, { policy: { ...policy, perPaymentMaxUsd: '10.00' }, inputs: p1.inputs, ...chainFacts });
cer('N2 a looser policy shown under the receipt\'s policy hash: INVALID (policy changed under its hash)', vn2.verdict === 'INVALID' && /changed under its hash/.test(vn2.motive.join()), vn2.motive.join('; '));
dovada.negative = { N1: vn1, N2: vn2 };
dovada.istoric = istoric;
} finally {
dovada.rezumat = { verzi, rosii }; const out = path.join(DOVEZI, 'plata-28001-' + new Date().toISOString().replace(/[:.]/g, '').slice(0, 15) + 'Z.json');
fs.writeFileSync(out, JSON.stringify(dovada, null, 1)); console.log(' evidence: ' + path.relative(RAD, out));
console.log(`payments on 28001: ${verzi} VERDE, ${rosii} ROSU`); process.exitCode = rosii ? 1 : 0;
}