aere-quantum/agents/x402/verifica-plati.mjs
Liviu 8b608fe57f agents/x402: a 2-of-2 contract wallet (ERC-1271) that neither the agent nor its owner can spend from alone
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.
2026-09-30 00:48:10 +03:00

86 lines
7.3 KiB
JavaScript

#!/usr/bin/env node
// verifica-plati.mjs: dovada ca platile de pe lant ale unui portofel de agent au fost PERMISE de politica agentului, verificata de
// oricine, din afara (2026-09-29, punctele 23, 25). Primeste o dosar-dovada {policy, policyHash, ledger, wallet, network, token,
// payments:[{entrySeq, entryHash, transaction}], fromBlock?} si un RPC, si cere:
// 1. registrul verifica fara incredere sub politica (verifyLedger);
// 2. fiecare plata numeste o intrare a registrului: plata permisa, din portofel, catre destinatar, cu suma si activul;
// 3. pe lant, tranzactia platii are status 1 si emite, din contractul activului, AuthorizationUsed(portofel, nonce) cu nonce =
// sha256(hash-ul intrarii) si Transfer(portofel, destinatar, suma) - deci plata de pe lant e chiar cea din registru;
// 4. cu --all-transfers: ORICE Transfer din portofel in intervalul de blocuri e una din platile de mai sus (nicio plata fara intrare);
// 5. cand dosarul numeste un portofel-contract 2-din-2 (walletContract: {agentSigner, policySigner}): codul de pe lant al portofelului e
// EXACT codul compilat din AereAgentWallet2of2.sol (artefactul de langa verificator) cu cei doi semnatari in locul imutabilelor, deci
// din portofel nu a putut plati nici agentul singur, nici serviciul de politica singur.
// Ce nu dovedeste: ca serviciul cumparat a fost livrat; ca portofelul nu a semnat si autorizari nedecontate (acelea nu misca bani).
// node verifica-plati.mjs <dovada.json> --rpc <url> [--all-transfers] iesire 0 VALID, 1 INVALID, 2 NEMASURAT
import fs from 'node:fs';
import { pathToFileURL } from 'node:url';
import { verifyLedger } from '../agent-ledger.mjs';
import { incarcaEthers, nonceForEntry, assetId } from './wallet.mjs';
import { incarcaArtefact, codulAsteptat, amprentaSursei } from './contract-2of2.mjs';
export async function verificaPlati(d, { rpc, allTransfers = false, fetchImpl = fetch } = {}) {
const ethers = incarcaEthers();
const rez = []; const ok = (name, pass, detail = '') => { rez.push({ name, pass, detail }); return pass; };
const apel = async (method, params) => {
const r = await fetchImpl(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
const j = await r.json(); if (j.error) throw new Error(`${method}: ${String(j.error.message).slice(0, 80)}`); return j.result;
};
const chain = Number(String(d.network).split(':')[1]);
let lantul;
try { lantul = Number(await apel('eth_chainId', [])); } catch (e) { return { verdict: 'UNMEASURED', checks: [{ name: 'rpc', pass: null, detail: e.message }] }; }
if (!ok(`the RPC serves chain ${chain}`, lantul === chain, `it serves ${lantul}`)) return { verdict: 'UNMEASURED', checks: rez };
const v = verifyLedger(d.ledger, { policy: d.policy, policyHash: d.policyHash, revocations: d.revocations || [] });
ok(`the ledger verifies under the policy (${d.ledger.entries.length} entries)`, v.ok, v.error || '');
const token = String(d.token).toLowerCase(), wallet = String(d.wallet).toLowerCase(), asset = assetId(d.network, token);
const T_AUTH = ethers.id('AuthorizationUsed(address,bytes32)'), T_TRANSFER = ethers.id('Transfer(address,address,uint256)');
const topicAdresa = (a) => '0x' + '0'.repeat(24) + String(a).toLowerCase().slice(2);
const txPlati = new Set();
for (const p of d.payments || []) {
const e = d.ledger.entries[p.entrySeq];
if (!ok(`payment ${p.entrySeq}: the entry is in the ledger`, !!e && e.hash === p.entryHash)) continue;
const a = e.body.action;
ok(`payment ${p.entrySeq}: the entry is an allowed payment from the wallet in the asset`, e.body.decision.allowed && a.kind === 'payment' && String(a.from).toLowerCase() === wallet && a.asset === asset);
let rc;
try { rc = await apel('eth_getTransactionReceipt', [p.transaction]); } catch (x) { ok(`payment ${p.entrySeq}: receipt readable`, false, x.message); continue; }
if (!ok(`payment ${p.entrySeq}: transaction ${String(p.transaction).slice(0, 12)} is on chain with status 1`, !!rc && rc.status === '0x1')) continue;
txPlati.add(String(p.transaction).toLowerCase());
const logs = rc.logs.filter((l) => l.address.toLowerCase() === token);
const nonce = nonceForEntry(e.hash).toLowerCase();
ok(`payment ${p.entrySeq}: AuthorizationUsed(wallet, sha256(entry hash)) is emitted by the asset`, logs.some((l) => l.topics[0] === T_AUTH && l.topics[1].toLowerCase() === topicAdresa(wallet) && l.topics[2].toLowerCase() === nonce));
ok(`payment ${p.entrySeq}: Transfer(wallet, ${String(a.to).slice(0, 10)}.., ${a.amount}) is emitted by the asset`, logs.some((l) => l.topics[0] === T_TRANSFER && l.topics[1].toLowerCase() === topicAdresa(wallet)
&& l.topics[2].toLowerCase() === topicAdresa(a.to) && BigInt(l.data) === BigInt(a.amount)));
}
if (allTransfers) {
try {
const logs = await apel('eth_getLogs', [{ address: token, fromBlock: '0x' + Number(d.fromBlock || 0).toString(16), toBlock: 'latest', topics: [T_TRANSFER, topicAdresa(wallet)] }]);
const straine = logs.filter((l) => !txPlati.has(l.transactionHash.toLowerCase()));
ok(`every Transfer out of the wallet since block ${d.fromBlock || 0} is a payment in the ledger (${logs.length} transfers)`, straine.length === 0, straine.map((l) => l.transactionHash.slice(0, 12)).join(', '));
} catch (x) { rez.push({ name: 'all transfers out of the wallet', pass: null, detail: x.message }); }
}
if (d.walletContract) {
const wc = d.walletContract; let art = null, asteptat = null;
try { art = incarcaArtefact(); asteptat = codulAsteptat(art, wc); } catch (x) { rez.push({ name: 'the 2-of-2 wallet artifact', pass: null, detail: x.message }); }
if (asteptat) {
try {
const cod = String(await apel('eth_getCode', [d.wallet, 'latest'])).toLowerCase();
ok(`the wallet is the 2-of-2 contract (AereAgentWallet2of2, source ${String(amprentaSursei(art)).slice(0, 12)}..): its code on chain is the compiled code with agent signer ${String(wc.agentSigner).slice(0, 10)}.. and policy signer ${String(wc.policySigner).slice(0, 10)}..`,
cod === asteptat, cod === asteptat ? '' : cod === '0x' ? 'no code at the wallet address' : `the code on chain differs: ${(cod.length - 2) / 2} bytes, expected ${(asteptat.length - 2) / 2}`);
} catch (x) { rez.push({ name: 'the wallet code on chain', pass: null, detail: x.message }); }
}
}
const verdict = rez.some((c) => c.pass === false) ? 'INVALID' : rez.some((c) => c.pass === null) ? 'UNMEASURED' : 'VALID';
return { verdict, checks: rez };
}
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
const a = process.argv.slice(2); const get = (f) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : undefined; };
const f = a.find((x) => !x.startsWith('--') && x !== get('--rpc'));
if (!f || !get('--rpc')) { console.error('usage: node verifica-plati.mjs <evidence.json> --rpc <url> [--all-transfers]'); process.exitCode = 2; }
else {
const r = await verificaPlati(JSON.parse(fs.readFileSync(f, 'utf8')), { rpc: get('--rpc'), allTransfers: a.includes('--all-transfers') });
for (const c of r.checks) console.log(` ${c.pass === true ? 'OK ' : c.pass === false ? 'FAIL' : '-- '} ${c.name}${c.detail ? ' (' + c.detail + ')' : ''}`);
console.log(r.verdict);
process.exitCode = r.verdict === 'VALID' ? 0 : r.verdict === 'INVALID' ? 1 : 2;
}
}