161 lines
15 KiB
JavaScript
161 lines
15 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();
|
|
// 2026-10-21: tiparul EOA cere ANUL dupa prefix (\d{4}-); `\d.*` se potrivea si cu "2of2-", care sorteaza ultimul, deci randurile 1-10
|
|
// judecau tot dosarul 2-din-2 si dosarul portofelului EOA nu era judecat de nicio proba, desi tabelul spunea "both testnet runs"
|
|
const dovadaF = ultimul(/^plati-agent-\d{4}-.*\.json$/), inregF = ultimul(/^rpc-inregistrat-\d{4}-.*\.json$/);
|
|
const dovada2F = ultimul(/^plati-agent-2of2-.*\.json$/), inreg2F = ultimul(/^rpc-inregistrat-2of2-.*\.json$/);
|
|
if (dovadaF && dovadaF === dovada2F) { console.log('NEMASURAT: tiparul dosarului EOA a ales dosarul 2-din-2'); process.exit(2); }
|
|
console.log(` dosare: EOA ${dovadaF} / ${inregF}; 2-din-2 ${dovada2F} / ${inreg2F}`);
|
|
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)
|
|
// 2026-10-21 (trecerea straina, D1): macheta serveste si eth_blockNumber (implicit cel mai inalt bloc din inregistrare: chitante si
|
|
// transferuri; `o.cap` il muta), filtreaza transferurile pe [fromBlock, toBlock] ca o usa reala, refuza ca usa testnetului un interval
|
|
// mai lung de `o.limita` blocuri (-32005), si raspunde stricat pe ferestrele pe care `o.strica(a, b)` le numeste. Numara cererile eth_getLogs.
|
|
const nrBloc = (x) => parseInt(x.blockNumber, 16);
|
|
async function rpcDin(rec, o = {}) {
|
|
const cap = o.cap ?? Math.max(...rec.transfersOut.map(nrBloc), ...Object.values(rec.receipts).filter(Boolean).map(nrBloc));
|
|
const limita = o.limita ?? Infinity;
|
|
const stat = { getLogs: 0, maxInterval: 0 };
|
|
const trimite = (res, corp, cod = 200) => { res.writeHead(cod, { 'content-type': 'application/json' }); res.end(typeof corp === 'string' ? corp : JSON.stringify(corp)); };
|
|
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_blockNumber') result = '0x' + cap.toString(16);
|
|
else if (q.method === 'eth_getTransactionReceipt') result = rec.receipts[String(q.params[0]).toLowerCase()] || null;
|
|
else if (q.method === 'eth_getLogs') {
|
|
stat.getLogs++;
|
|
const f = q.params[0], a = parseInt(f.fromBlock, 16), b = f.toBlock === 'latest' ? cap : parseInt(f.toBlock, 16);
|
|
stat.maxInterval = Math.max(stat.maxInterval, b - a + 1);
|
|
if (b - a + 1 > limita) { trimite(res, { jsonrpc: '2.0', id: q.id, error: { code: -32005, message: 'Requested range exceeds maximum RPC range limit' } }); return; }
|
|
const stricat = o.strica && o.strica(a, b);
|
|
if (stricat) { trimite(res, stricat.corp(q.id), stricat.cod || 200); return; }
|
|
result = rec.transfersOut.filter((l) => nrBloc(l) >= a && nrBloc(l) <= b);
|
|
}
|
|
// 2026-10-11 (D-FAZA2-04b, R2): macheta serveste numai ce e INREGISTRAT; o inregistrare fara `code` nu devine "fara cod",
|
|
// ci o eroare JSON-RPC, pe care verificatorul trebuie sa o trateze ca necunoscut (inregistrarile EOA poarta explicit "0x")
|
|
else if (q.method === 'eth_getCode') { if (rec.code === undefined) { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: q.id, error: { code: -32000, message: 'World state unavailable' } })); return; } result = rec.code; }
|
|
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(), stat };
|
|
}
|
|
async function judeca(dov, rec, allTransfers = true) { const r = await rpcDin(rec); try { return await verificaPlati(dov, { rpc: r.url, allTransfers }); } finally { r.close(); } }
|
|
// aceeasi judecata pe o usa cu forma data (o), cu fereastra si plafonul date verificatorului (v); intoarce si ce a numarat macheta
|
|
async function judecaUsa(dov, rec, o = {}, v = {}) { const r = await rpcDin(rec, o); try { return { ...(await verificaPlati(dov, { rpc: r.url, allTransfers: true, ...v })), stat: r.stat }; } finally { r.close(); } }
|
|
const pica = (v, re) => v.checks.some((c) => c.pass === false && re.test(c.name));
|
|
const toate = /all transfers out of the wallet|every Transfer out of the wallet/;
|
|
const verificarea = (v) => v.checks.find((c) => toate.test(c.name)) || { name: '(lipseste)', pass: undefined, detail: '' };
|
|
|
|
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); r14.code = '0x'; // 2026-10-11: controlul spune EXPLICIT "cont fara cod"; o inregistrare fara camp e tacere, nu absenta (R2)
|
|
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');
|
|
|
|
// 2026-10-21 (trecerea straina, D1): --all-transfers pe o usa care refuza intervalele mari. Pe 28001 capul era 7.200.317, deci de la
|
|
// fromBlock-ul dosarului EOA (4.022.094) sunt 3.178.224 de blocuri; usa testnetului accepta 5.001 si refuza 10.000.
|
|
// Ferestrele si blocurile se DERIVA din dosarul si inregistrarea alese mai sus, nu se scriu fix.
|
|
const from = Number(dovada.fromBlock), capViu = from + 3178223;
|
|
const blocuri = inreg.transfersOut.map(nrBloc), capInreg = Math.max(...blocuri, ...Object.values(inreg.receipts).map(nrBloc));
|
|
const W = 8, nrFerestre = Math.ceil((capInreg - from + 1) / W), ferestreCuTransfer = new Set(blocuri.map((b) => Math.floor((b - from) / W))).size;
|
|
const transfer = (l, bloc, tx) => ({ ...l, blockNumber: '0x' + bloc.toString(16), transactionHash: tx });
|
|
const strain = (bloc) => { const r = copie(inreg); r.transfersOut.push(transfer(r.transfersOut[0], bloc, '0x' + 'ee'.repeat(32))); return r; };
|
|
const v15 = await judecaUsa(dovada, inreg, { cap: capViu, limita: 5000 });
|
|
const c15 = verificarea(v15);
|
|
cer(v15.verdict === 'VALID' && c15.pass === true && v15.stat.getLogs === 636 && v15.stat.maxInterval <= 5000 && new RegExp(`${blocuri.length} transfers, 636 windows`).test(c15.name),
|
|
`15. usa cu limita de 5.000 de blocuri, 3.178.224 de blocuri de citit (ca 28001 pe 2026-10-21): VALID, ${v15.stat.getLogs} cereri, cel mai lung interval ${v15.stat.maxInterval} (${c15.name.slice(0, 110)})`);
|
|
// ferestre de W blocuri pe [fromBlock, capul inregistrarii]: transferurile cad in mai multe ferestre DIFERITE
|
|
const v16 = await judecaUsa(dovada, inreg, { limita: 10 }, { window: W });
|
|
const c16 = verificarea(v16);
|
|
cer(ferestreCuTransfer >= 2 && v16.verdict === 'VALID' && c16.pass === true && v16.stat.getLogs === nrFerestre
|
|
&& new RegExp(`${blocuri.length} transfers, ${nrFerestre} windows of at most ${W} blocks\\)`).test(c16.name),
|
|
`16. ferestre de ${W} blocuri pe o usa cu limita 10: ${blocuri.length} transferuri gasite in ${ferestreCuTransfer} ferestre diferite din ${nrFerestre}, VALID (${c16.name.slice(0, 110)})`);
|
|
const v16b = await judecaUsa(dovada, strain(capInreg), { limita: 10 }, { window: W });
|
|
cer(v16b.verdict === 'INVALID' && verificarea(v16b).pass === false, `16b. CONTROL: un Transfer strain in ULTIMA fereastra (blocul capului) -> ${v16b.verdict}`);
|
|
// fara paginare: o singura cerere peste tot intervalul (fereastra mai mare decat intervalul), pe usa cu limita
|
|
const v17 = await judecaUsa(dovada, inreg, { cap: capViu, limita: 5000 }, { window: 10000000 });
|
|
const c17 = verificarea(v17);
|
|
cer(v17.verdict === 'UNMEASURED' && c17.pass === null && /Requested range exceeds/.test(c17.detail) && !/\d+ transfers/.test(c17.name),
|
|
`17. CONTROL: fara paginare (o singura cerere de la fromBlock la cap) pe usa cu limita -> ${v17.verdict}, cu refuzul usii, niciodata "0 transferuri"`);
|
|
// o fereastra cu eroare nu se numara drept goala: Transferul strain sta exact in fereastra necitita (prima, care incepe la fromBlock)
|
|
const strica1 = (forma) => ({ strica: (a) => a === from ? forma : null, limita: 10 });
|
|
const eroare = { corp: (id) => ({ jsonrpc: '2.0', id, error: { code: -32000, message: 'internal error' } }) };
|
|
const unaDin = new RegExp(`1 of ${nrFerestre} windows`);
|
|
const v18 = await judecaUsa(dovada, strain(from + 1), strica1(eroare), { window: W });
|
|
const c18 = verificarea(v18);
|
|
cer(v18.verdict === 'UNMEASURED' && c18.pass === null && unaDin.test(c18.detail),
|
|
`18. CONTROL: fereastra cu Transferul strain raspunde cu o eroare JSON-RPC -> ${v18.verdict} (${String(c18.detail).slice(0, 70)}), nu VALID`);
|
|
const v19 = await judecaUsa(dovada, strain(from + 1), strica1({ corp: (id) => ({ jsonrpc: '2.0', id, result: null }) }), { window: W });
|
|
cer(v19.verdict === 'UNMEASURED' && unaDin.test(verificarea(v19).detail) && /not a list/.test(verificarea(v19).detail),
|
|
`19. CONTROL: aceeasi fereastra raspunde cu un rezultat care nu e lista (null) -> ${v19.verdict}, nu "goala"`);
|
|
const v20 = await judecaUsa(dovada, strain(from + 1), strica1({ cod: 429, corp: () => 'Too Many Requests' }), { window: W });
|
|
cer(v20.verdict === 'UNMEASURED' && unaDin.test(verificarea(v20).detail),
|
|
`20. CONTROL: aceeasi fereastra raspunde HTTP 429 cu text, nu JSON -> ${v20.verdict}`);
|
|
// un Transfer strain intr-o fereastra CITITA da INVALID chiar daca alta fereastra e necitita
|
|
const v21 = await judecaUsa(dovada, strain(capInreg), strica1(eroare), { window: W });
|
|
cer(v21.verdict === 'INVALID' && verificarea(v21).pass === false && /1 unreadable/.test(verificarea(v21).name),
|
|
`21. CONTROL: Transfer strain in ultima fereastra, prima fereastra necitita -> ${v21.verdict} (INVALID bate NEMASURAT)`);
|
|
// plafonul de cereri: 20 de milioane de blocuri la 5.000 cer 4.001 ferestre, peste plafonul implicit de 2.000 -> nu se cere nimic
|
|
const v22 = await judecaUsa(dovada, inreg, { cap: from + 20000000, limita: 5000 });
|
|
cer(v22.verdict === 'UNMEASURED' && v22.stat.getLogs === 0 && /4001 requests .* above the cap of 2000/.test(verificarea(v22).detail),
|
|
`22. CONTROL: intervalul cere 4.001 ferestre, peste plafonul de 2.000 -> ${v22.verdict}, ${v22.stat.getLogs} cereri eth_getLogs`);
|
|
// un cap SUB fromBlock (o usa ramasa in urma) nu e un interval gol
|
|
const v23 = await judecaUsa(dovada, inreg, { cap: from - 1 });
|
|
cer(v23.verdict === 'UNMEASURED' && v23.stat.getLogs === 0 && /is not at or above the evidence's fromBlock/.test(verificarea(v23).detail),
|
|
`23. CONTROL: capul usii e sub fromBlock-ul dosarului -> ${v23.verdict}, nu "0 transferuri"`);
|
|
} 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;
|