aere-quantum/agents/x402/proba-cosign-testnet.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

146 lines
13 KiB
JavaScript

#!/usr/bin/env node
// Proba cap la cap a portofelului-contract 2-din-2 pe testnetul PUBLIC 28001 (2026-09-29, punctele 23, 25): banii agentului stau intr-un
// AereAgentWallet2of2 desfasurat aici, si ies numai cu semnatura agentului SI a serviciului de politica, prin facilitatorul x402 al
// testnetului (ERC-1271) si prin tokenul EIP-3009 (SignatureChecker).
// 1. contractul se desfasoara din artefactul publicat (AereAgentWallet2of2.json), cu doua chei NOI tinute numai in memorie (agentul,
// serviciul de politica); codul de pe lant trebuie sa fie EXACT codul compilat cu cei doi semnatari;
// 2. primeste 0,05 tUSD de la cheia de dezvoltator a testnetului;
// 3. contractul, masurat pe lant (eth_call isValidSignature): DA numai pentru agent || politica; NU pentru fiecare jumatate singura,
// dublata, in ordine inversa, sau cu o cheie straina;
// 4. trei cumparaturi de 0,01 platite din contract si servite; a patra refuzata de politica;
// 5. ATACURI prin facilitatorul testnetului: agentul singur si serviciul de politica singur nu pot plati (402); si TOKENUL insusi,
// intrebat pe lant fara facilitator (eth_call transferWithAuthorization): refuza jumatatea agentului, primeste perechea;
// 6. pe lant: soldurile, nonce-urile folosite (sha256 al intrarii), iar nonce-ul atacului nefolosit;
// 7. dosarul-dovada (cu walletContract) verificat cu verifica-plati.mjs: VALID, inclusiv codul contractului; un dosar care numeste
// alt semnatar de politica: INVALID.
// Tranzactii trimise de proba: desfasurarea si finantarea (cheia de dezvoltator); decontarile le plateste facilitatorul. Refuza orice
// alt lant decat 28001. Cheia de dezvoltator se citeste din AERE_TESTNET_KEY_FILE (d=<hex> sau PRIVATE_KEY=0x<hex>) si nu se tipareste.
// AERE_TESTNET_KEY_FILE=<fisier> node proba-cosign-testnet.mjs [--rpc URL] [--facilitator URL]
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { definePolicy } from '../agent-policy.mjs';
import { newAgentIdentity, openLedger } from '../agent-ledger.mjs';
import { createWallet, assetId, nonceForEntry, incarcaEthers, EIP3009_TYPES } from './wallet.mjs';
import { payWithAgent } from './client.mjs';
import { createResourceServer } from './resource-server.mjs';
import { verificaPlati } from './verifica-plati.mjs';
import { incarcaArtefact, codulAsteptat, amprentaSursei } from './contract-2of2.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const a = process.argv.slice(2); const get = (f, d) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : d; };
const RPC = get('--rpc', 'https://testnet-rpc.aere.network');
const FAC = get('--facilitator', 'https://testnet-rpc.aere.network/x402');
const NET = 'eip155:28001';
const TOKEN = { address: '0x8215bA247a3574af8EBC36606eB437811E318FBd', name: 'AereTestUSD', version: '2' };
const MAGIC = '0x1626ba7e', INVALID = '0xffffffff';
const ethers = incarcaEthers();
let ok = 0, rau = 0;
const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; return c; };
const taie = (s) => String(s || '').replace(/(0x)?[0-9a-fA-F]{60,}/g, '<hex>').slice(0, 160);
const kf = process.env.AERE_TESTNET_KEY_FILE;
if (!kf || !fs.existsSync(kf)) { console.log('NEMASURAT: AERE_TESTNET_KEY_FILE nu numeste un fisier cu cheia de dezvoltator a testnetului'); process.exit(2); }
const rand = fs.readFileSync(kf, 'utf8').split(/\r?\n/).map((l) => l.trim()).find((l) => /^(d|PRIVATE_KEY)=/.test(l)) || '';
const hex = rand.replace(/^(d|PRIVATE_KEY)=/, '').replace(/^0x/, '').trim();
if (!/^[0-9a-fA-F]{1,64}$/.test(hex)) { console.log('NEMASURAT: fisierul cheii nu are un rand d=<hex> sau PRIVATE_KEY=0x<hex>'); process.exit(2); }
const provider = new ethers.JsonRpcProvider(RPC, undefined, { staticNetwork: false });
const lant = Number((await provider.getNetwork()).chainId);
if (lant !== 28001) { console.log(`REFUZ: RPC-ul serveste lantul ${lant}; proba ruleaza numai pe testnetul 28001`); process.exit(2); }
const dev = new ethers.Wallet('0x' + hex.padStart(64, '0'), provider);
const art = incarcaArtefact();
const tUSD = new ethers.Contract(TOKEN.address, ['function transfer(address,uint256) returns (bool)', 'function balanceOf(address) view returns (uint256)',
'function authorizationState(address,bytes32) view returns (bool)',
'function transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,bytes)'], dev);
const DOM = { name: TOKEN.name, version: TOKEN.version, chainId: 28001, verifyingContract: TOKEN.address };
const digestul = (au) => ethers.TypedDataEncoder.hash(DOM, EIP3009_TYPES, { ...au, value: BigInt(au.value), validAfter: BigInt(au.validAfter), validBefore: BigInt(au.validBefore) });
const deschise = [];
try {
// 1. desfasurarea, din artefactul publicat
const agentEvm = ethers.Wallet.createRandom(), politica = ethers.Wallet.createRandom(), strain = ethers.Wallet.createRandom();
const payee = ethers.Wallet.createRandom().address;
const c = await new ethers.ContractFactory(art.abi, art.bytecode, dev).deploy(agentEvm.address, politica.address);
const rcD = await c.deploymentTransaction().wait(1, 120000);
const portofel = await c.getAddress();
const cod = String(await provider.getCode(portofel)).toLowerCase();
cer(rcD && rcD.status === 1 && cod === codulAsteptat(art, { agentSigner: agentEvm.address, policySigner: politica.address }),
`1. AereAgentWallet2of2 desfasurat la ${portofel} (bloc ${rcD && rcD.blockNumber}): codul de pe lant e exact codul compilat din sursa publicata (sha256 ${String(amprentaSursei(art)).slice(0, 16)}), cu cei doi semnatari`);
// 2. finantarea
const rc0 = await (await tUSD.transfer(portofel, 50000n)).wait(1, 120000);
cer(rc0 && rc0.status === 1 && (await tUSD.balanceOf(portofel)) === 50000n, `2. portofelul-contract primeste 0,05 tUSD (bloc ${rc0 && rc0.blockNumber})`);
// 3. contractul, pe lant
const w = new ethers.Contract(portofel, art.abi, provider);
const h = ethers.hexlify(ethers.randomBytes(32));
const sA = agentEvm.signingKey.sign(h).serialized, sP = politica.signingKey.sign(h).serialized, sS = strain.signingKey.sign(h).serialized;
const raspunsuri = {
'agent || politica': await w.isValidSignature(h, ethers.concat([sA, sP])),
'agentul singur (65 de octeti)': await w.isValidSignature(h, sA), 'politica singura (65 de octeti)': await w.isValidSignature(h, sP),
'agentul de doua ori': await w.isValidSignature(h, ethers.concat([sA, sA])), 'politica de doua ori': await w.isValidSignature(h, ethers.concat([sP, sP])),
'ordinea inversa': await w.isValidSignature(h, ethers.concat([sP, sA])), 'agent || cheie straina': await w.isValidSignature(h, ethers.concat([sA, sS])),
'cheie straina || politica': await w.isValidSignature(h, ethers.concat([sS, sP])),
};
const [primul, ...restul] = Object.entries(raspunsuri);
cer(primul[1] === MAGIC && restul.every(([, r]) => r === INVALID),
`3. pe lant, isValidSignature: DA numai pentru agent || politica; NU pentru ${restul.length} alte forme (${restul.filter(([, r]) => r !== INVALID).map(([k]) => k).join(', ') || 'toate refuzate'})`);
// 4. cumparaturile, prin facilitatorul testnetului
const agent = newAgentIdentity();
const { policy, policyHash } = definePolicy({ agentId: agent.agentId, spend: { amount: '30000', windowSeconds: 3600, asset: assetId(NET, TOKEN.address) },
recipients: [payee], wallet: portofel });
const serviciu = createWallet({ policy, policyHash, evmPrivateKey: politica.privateKey, contractWallet: portofel, network: NET, token: TOKEN });
const L = openLedger({ identity: agent, policy, policyHash });
const cerinta = { scheme: 'exact', network: NET, amount: '10000', asset: TOKEN.address, payTo: payee, maxTimeoutSeconds: 120, extra: { name: TOKEN.name, version: TOKEN.version } };
const rs = createResourceServer({ requirement: cerinta, facilitator: FAC, content: 'the paid content' });
const url = await rs.listen(); deschise.push(rs.server);
const plati = [];
for (let i = 0; i < 3; i++) plati.push(await payWithAgent({ url, ledger: L, wallet: serviciu, agentEvmKey: agentEvm.privateKey }));
cer(plati.every((p) => p.paid && p.status === 200 && p.body === 'the paid content' && /^0x[0-9a-f]{64}$/.test(p.settlement.transaction || '')),
`4. trei cumparaturi de 0,01 tUSD platite din portofelul 2-din-2 prin facilitatorul testnetului si servite (${plati.map((p) => p.status + (p.reason ? ' ' + taie(p.reason) : '')).join('; ')})`);
const s0 = rs.lastPayloads[0] && rs.lastPayloads[0].payload.signature;
cer(!!s0 && ethers.dataLength(s0) === 130, '4. fiecare plata poarta 130 de octeti de semnatura: jumatatea agentului, apoi a serviciului de politica');
const p4 = await payWithAgent({ url, ledger: L, wallet: serviciu, agentEvmKey: agentEvm.privateKey });
cer(!p4.paid && /policy refused.*over the limit/.test(p4.reason || '') && serviciu.status().signed === 3, `4. CONTROL: a patra depaseste 0,03 pe ora: refuzata de politica, serviciul a semnat tot 3 (${taie(p4.reason)})`);
// 5. atacurile: o autorizare noua catre vanzator, semnata de o singura parte
const acum = Math.floor(Date.now() / 1000);
const au = { from: portofel, to: payee, value: '10000', validAfter: String(acum - 5), validBefore: String(acum + 100), nonce: ethers.hexlify(ethers.randomBytes(32)) };
const d5 = digestul(au);
const a5 = agentEvm.signingKey.sign(d5).serialized, p5 = politica.signingKey.sign(d5).serialized;
const trimite = async (sig) => { const pl = { x402Version: 2, accepted: cerinta, payload: { signature: sig, authorization: au } };
const r = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(pl)).toString('base64') } }); return [r.status, await r.text()]; };
const [cA, tA] = await trimite(ethers.concat([a5, a5]));
cer(cA === 402 && /invalid_signature/.test(tA), `5. ATAC: agentul singur (jumatatea lui de doua ori) prin facilitatorul testnetului -> ${cA} (${taie(tA.match(/payment not valid: [a-z_]+/) || tA)})`);
const [cP, tP] = await trimite(ethers.concat([p5, p5]));
cer(cP === 402 && /invalid_signature/.test(tP), `5. ATAC: serviciul de politica singur (proprietarul) prin facilitatorul testnetului -> ${cP} (${taie(tP.match(/payment not valid: [a-z_]+/) || tP)})`);
const apelTok = (sig) => tUSD.transferWithAuthorization.staticCall(au.from, au.to, BigInt(au.value), BigInt(au.validAfter), BigInt(au.validBefore), au.nonce, sig, { from: dev.address });
let refuzAgent = false; try { await apelTok(ethers.concat([a5, a5])); } catch { refuzAgent = true; }
let refuzPol = false; try { await apelTok(ethers.concat([p5, p5])); } catch { refuzPol = true; }
let primestePerechea = false; try { await apelTok(ethers.concat([a5, p5])); primestePerechea = true; } catch { primestePerechea = false; }
cer(refuzAgent && refuzPol && primestePerechea, `5. pe lant, fara facilitator (eth_call transferWithAuthorization): tokenul refuza agentul singur (${refuzAgent}) si politica singura (${refuzPol}), primeste perechea (${primestePerechea})`);
rs.server.close();
// 6. pe lant
await new Promise((r) => setTimeout(r, 2000));
const soldPayee = await tUSD.balanceOf(payee), soldPortofel = await tUSD.balanceOf(portofel);
cer(soldPayee === 30000n && soldPortofel === 20000n, `6. pe lant: vanzatorul are 30000, portofelul-contract 20000 (${soldPayee}, ${soldPortofel})`);
const folosite = await Promise.all(plati.map((p) => tUSD.authorizationState(portofel, nonceForEntry(p.entry.hash))));
const atacFolosit = await tUSD.authorizationState(portofel, au.nonce);
cer(folosite.every(Boolean) && !atacFolosit, '6. pe lant: nonce-ul sha256(hash-ul intrarii) e folosit pentru fiecare plata; nonce-ul atacurilor nu');
// 7. dosarul-dovada, verificat din afara
const dovada = { v: 1, kind: 'aere-agent-x402-payments', network: NET, token: TOKEN.address, wallet: portofel, fromBlock: rcD.blockNumber,
walletContract: { contract: 'AereAgentWallet2of2', sourceSha256: amprentaSursei(art), agentSigner: agentEvm.address, policySigner: politica.address, deployTransaction: rcD.hash },
facilitator: FAC, policy, policyHash, ledger: L.export(),
payments: plati.map((p) => ({ entrySeq: p.entry.seq, entryHash: p.entry.hash, transaction: p.settlement.transaction })), createdAt: new Date().toISOString() };
const dir = path.join(AICI, 'dovezi-28001'); fs.mkdirSync(dir, { recursive: true });
const f = path.join(dir, `plati-agent-2of2-${dovada.createdAt.slice(0, 19).replace(/[:T]/g, '-')}.json`);
fs.writeFileSync(f, JSON.stringify(dovada, null, 1) + '\n');
const v = await verificaPlati(dovada, { rpc: RPC, allTransfers: true });
cer(v.verdict === 'VALID' && v.checks.some((x) => x.pass === true && /is the 2-of-2 contract/.test(x.name)),
`7. verifica-plati pe dosarul-dovada: ${v.verdict} (${v.checks.length} verificari, printre ele: codul portofelului e contractul 2-din-2)`);
const alt = JSON.parse(JSON.stringify(dovada)); alt.walletContract.policySigner = strain.address;
const va = await verificaPlati(alt, { rpc: RPC });
cer(va.verdict === 'INVALID', `7. CONTROL: un dosar care numeste alt semnatar de politica -> ${va.verdict} (codul de pe lant nu e cel cu acel semnatar)`);
console.log(` dosarul-dovada: ${path.relative(process.cwd(), f)}`);
} catch (e) { cer(false, `proba s-a oprit: ${taie(e.shortMessage || e.message)}`); }
finally { for (const s of deschise) { try { s.closeAllConnections && s.closeAllConnections(); s.close(); } catch { /* inchis */ } } }
console.log(`\nagent-cosign-testnet: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`);
process.exitCode = rau ? 1 : 0;