AereAgentWallet2of2 holds the agent's tokens and accepts only the agent's signature followed by the policy service's, over the same digest. In the new cosign mode the wallet service signs only its half, after every check it already made, and the agent adds its half only after it recomputes the payment itself (payer, recipient, amount, the nonce of its own ledger entry, validity, digest, declared policy signer). verifica-plati.mjs requires the wallet's code on chain to be exactly the compiled contract with the two signers; recompileaza-contract.mjs recompiles the published artifact byte for byte with solc 0.8.23. Tests: co-signing 16/16, payment verifier 14/14, negative control 30/30, wallet 25/25. On the public testnet 28001 on 2026-09-29: 13/13 with the 2-of-2 wallet (the agent alone and the policy signer alone refused by the facilitator and by the token asked on chain) and 9/9 with the wallet key. Evidence in dovezi-28001/. Nothing here has been run on the Aere Network mainnet.
82 lines
6.6 KiB
JavaScript
82 lines
6.6 KiB
JavaScript
// Proba verificatorului de plati (verifica-plati.mjs), fara retea: un RPC local care serveste raspunsurile INREGISTRATE de pe testnetul
|
|
// 28001 pentru cele doua dosare-dovada din dovezi-28001/ (portofelul EOA si portofelul-contract 2-din-2, inregistreaza-rpc.mjs), deci
|
|
// date reale de pe lant. Fiecare verificare are cazul ei
|
|
// care trebuie sa o inroseasca, construit din datele reale schimbate intr-un singur loc.
|
|
// node proba-verifica-plati.mjs iesire 0 = toate cum trebuia
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import http from 'node:http';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { verificaPlati } from './verifica-plati.mjs';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
const D = path.join(AICI, 'dovezi-28001');
|
|
// doua rulari pe 28001: portofelul EOA (plati-agent-<data>) si portofelul-contract 2-din-2 (plati-agent-2of2-<data>), fiecare cu inregistrarea ei
|
|
const ultimul = (re) => fs.readdirSync(D).filter((n) => re.test(n)).sort().pop();
|
|
const dovadaF = ultimul(/^plati-agent-\d.*\.json$/), inregF = ultimul(/^rpc-inregistrat-\d.*\.json$/);
|
|
const dovada2F = ultimul(/^plati-agent-2of2-.*\.json$/), inreg2F = ultimul(/^rpc-inregistrat-2of2-.*\.json$/);
|
|
let ok = 0, rau = 0;
|
|
const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; };
|
|
if (!dovadaF || !inregF || !dovada2F || !inreg2F) { console.log(`NEMASURAT: lipseste un dosar-dovada sau o inregistrare RPC in ${D}`); process.exit(2); }
|
|
const dovada = JSON.parse(fs.readFileSync(path.join(D, dovadaF), 'utf8'));
|
|
const inreg = JSON.parse(fs.readFileSync(path.join(D, inregF), 'utf8'));
|
|
const dovada2 = JSON.parse(fs.readFileSync(path.join(D, dovada2F), 'utf8'));
|
|
const inreg2 = JSON.parse(fs.readFileSync(path.join(D, inreg2F), 'utf8'));
|
|
const copie = (o) => JSON.parse(JSON.stringify(o));
|
|
|
|
// un RPC local peste inregistrare (sau peste o varianta schimbata a ei)
|
|
async function rpcDin(rec) {
|
|
const srv = http.createServer((req, res) => { let s = ''; req.on('data', (x) => { s += x; }); req.on('end', () => {
|
|
const q = JSON.parse(s); let result = null;
|
|
if (q.method === 'eth_chainId') result = rec.chainId;
|
|
else if (q.method === 'eth_getTransactionReceipt') result = rec.receipts[String(q.params[0]).toLowerCase()] || null;
|
|
else if (q.method === 'eth_getLogs') result = rec.transfersOut;
|
|
else if (q.method === 'eth_getCode') result = rec.code || '0x';
|
|
res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: q.id, result }));
|
|
}); });
|
|
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
|
|
return { url: `http://127.0.0.1:${srv.address().port}`, close: () => srv.close() };
|
|
}
|
|
async function judeca(dov, rec, allTransfers = true) { const r = await rpcDin(rec); try { return await verificaPlati(dov, { rpc: r.url, allTransfers }); } finally { r.close(); } }
|
|
const pica = (v, re) => v.checks.some((c) => c.pass === false && re.test(c.name));
|
|
|
|
try {
|
|
const v0 = await judeca(dovada, inreg);
|
|
cer(v0.verdict === 'VALID' && v0.checks.every((c) => c.pass === true), `1. dosarul real, peste raspunsurile reale de pe 28001: VALID (${v0.checks.length} verificari)`);
|
|
const tx1 = dovada.payments[1].transaction.toLowerCase();
|
|
|
|
const d2 = copie(dovada); d2.ledger.entries[dovada.payments[1].entrySeq].body.action.amount = '1';
|
|
cer(pica(await judeca(d2, inreg), /the ledger verifies/), '2. CONTROL: o suma schimbata in registru -> registrul nu mai verifica');
|
|
const d3 = copie(dovada); d3.payments[1].entryHash = 'ab'.repeat(32);
|
|
cer(pica(await judeca(d3, inreg), /the entry is in the ledger/), '3. CONTROL: plata numeste o intrare care nu e in registru -> INVALID');
|
|
const d4 = copie(dovada); d4.wallet = '0x' + '11'.repeat(20);
|
|
cer(pica(await judeca(d4, inreg, false), /allowed payment from the wallet/), '4. CONTROL: alt portofel decat cel din intrari -> INVALID');
|
|
const r5 = copie(inreg); r5.receipts[tx1].status = '0x0';
|
|
cer(pica(await judeca(dovada, r5), /status 1/), '5. CONTROL: chitanta cu status 0 -> INVALID');
|
|
const r6 = copie(inreg); for (const l of r6.receipts[tx1].logs) if (l.topics.length === 3 && l.topics[2] && !/^0x0{24}/.test(l.topics[2])) l.topics[2] = '0x' + 'cd'.repeat(32);
|
|
cer(pica(await judeca(dovada, r6), /AuthorizationUsed/), '6. CONTROL: nonce-ul de pe lant nu e sha256(hash-ul intrarii) -> INVALID');
|
|
const r7 = copie(inreg); for (const l of r7.receipts[tx1].logs) if (l.data && l.data !== '0x') l.data = '0x' + (1n).toString(16).padStart(64, '0');
|
|
cer(pica(await judeca(dovada, r7), /Transfer\(wallet/), '7. CONTROL: suma din Transfer-ul de pe lant alta decat in intrare -> INVALID');
|
|
const r8 = copie(inreg); r8.transfersOut = [...r8.transfersOut, { ...r8.transfersOut[0], transactionHash: '0x' + 'ee'.repeat(32) }];
|
|
cer(pica(await judeca(dovada, r8), /every Transfer out of the wallet/), '8. CONTROL: un Transfer din portofel fara plata in registru -> INVALID');
|
|
const r9 = copie(inreg); r9.chainId = '0xaf0';
|
|
const v9 = await judeca(dovada, r9);
|
|
cer(v9.verdict === 'UNMEASURED', `9. CONTROL: un RPC al altui lant (2800) -> ${v9.verdict}, nu VALID si nu INVALID`);
|
|
const v10 = await verificaPlati(dovada, { rpc: 'http://127.0.0.1:9', allTransfers: true });
|
|
cer(v10.verdict === 'UNMEASURED', `10. CONTROL: un RPC care nu raspunde -> ${v10.verdict}`);
|
|
|
|
// portofelul-contract 2-din-2: codul de pe lant trebuie sa fie contractul compilat cu cei doi semnatari din dosar
|
|
const e2 = /is the 2-of-2 contract/;
|
|
const v11 = await judeca(dovada2, inreg2);
|
|
cer(v11.verdict === 'VALID' && v11.checks.every((c) => c.pass === true) && v11.checks.some((c) => e2.test(c.name)),
|
|
`11. dosarul portofelului 2-din-2, peste raspunsurile reale de pe 28001: VALID (${v11.checks.length} verificari, cu codul contractului)`);
|
|
const r12 = copie(inreg2); const pozitie = r12.code.length - 120; r12.code = r12.code.slice(0, pozitie) + (r12.code[pozitie] === '0' ? '1' : '0') + r12.code.slice(pozitie + 1);
|
|
cer(pica(await judeca(dovada2, r12), e2), '12. CONTROL: un singur octet schimbat in codul de pe lant -> INVALID');
|
|
const d13 = copie(dovada2); d13.walletContract.agentSigner = '0x' + '22'.repeat(20);
|
|
cer(pica(await judeca(d13, inreg2), e2), '13. CONTROL: dosarul numeste alt semnatar al agentului decat cel din codul de pe lant -> INVALID');
|
|
const r14 = copie(inreg2); delete r14.code;
|
|
cer(pica(await judeca(dovada2, r14), e2), '14. CONTROL: la adresa portofelului nu e niciun cod (un cont cu o cheie), dar dosarul spune 2-din-2 -> INVALID');
|
|
} catch (e) { cer(false, `proba s-a oprit: ${String(e.message || e).slice(0, 160)}`); }
|
|
console.log(`\nverifica-plati: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`);
|
|
process.exitCode = rau ? 1 : 0;
|