The agent does not hold the payment key: the wallet holds it for the owner and signs an EIP-3009 authorization only for a payment the agent wrote into its signed ledger, verified without trusting the agent under the policy the owner pinned and against the ledger heads the wallet itself saw (a branch is refused with a proof of equivocation, a backdated entry is refused), naming exactly this purchase, written now, and within the limit judged also against what the wallet itself has signed. The authorization nonce is sha256(entry hash), so the on-chain payment names the ledger entry. The policy gains an optional `wallet` field. Also: an x402 v2 client, a minimal resource server, a local facilitator for tests, and verifica-plati.mjs, which proves from outside that a wallet's on-chain payments were allowed by the agent's policy (with --all-transfers, that no payment left the wallet without a ledger entry). Tests: wallet 25/25 and payment verifier 10/10 without a network (the verifier on chain responses recorded on testnet 28001), negative control 21/21; policy 27/27, agents control 26/26. On the public testnet 28001 through its x402 facilitator: 9/9, with the evidence in agents/x402/dovezi-28001/. Needs ethers (npm install in agents/x402).
64 lines
4.9 KiB
JavaScript
64 lines
4.9 KiB
JavaScript
// Proba verificatorului de plati (verifica-plati.mjs), fara retea: un RPC local care serveste raspunsurile INREGISTRATE de pe testnetul
|
|
// 28001 pentru dosarul-dovada din dovezi-28001/ (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');
|
|
const dovadaF = fs.readdirSync(D).filter((n) => n.startsWith('plati-agent-') && n.endsWith('.json')).sort().pop();
|
|
const inregF = fs.readdirSync(D).filter((n) => n.startsWith('rpc-inregistrat-') && n.endsWith('.json')).sort().pop();
|
|
let ok = 0, rau = 0;
|
|
const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; };
|
|
if (!dovadaF || !inregF) { console.log(`NEMASURAT: lipseste dosarul-dovada sau inregistrarea 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 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;
|
|
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}`);
|
|
} 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;
|