// AERE post-quantum identity — end-to-end example (chain 2800). // // One runnable script that stitches the shipped PQC pieces into a single front // door and DRIVES it against the LIVE read-only RPC: // // 1. Generate a Falcon-512 quantum-durable ROOT keypair client-side, and // client-side-encrypt the secret under a passphrase (no seed phrase, the // secret never leaves this process unencrypted). // 2. Register the root public key in AerePQCKeyRegistry with on-chain // PROOF-OF-POSSESSION — derive the PoP challenge locally and CROSS-CHECK it // against the live `popChallenge` view (SDK<->contract parity, proven live). // 3. Open an AereAgentDID rooted in that Falcon key, then issue a short-lived, // revocable secp256k1 SESSION key (scope + spend cap + expiry), gated by a // Falcon proof-of-possession. Authorize an action with the session key. // 4. Set a quantum-durable M-of-N guardian committee (AerePQCSocialRecoveryModule) // and derive the recovery challenge a guardian would PQC-sign. // // READ-ONLY / DRY-RUN: every write flow is CONSTRUCTED and PRINTED (the exact // calldata a wallet would broadcast) but nothing is signed with a funded key and // nothing is sent. The live RPC is used only for eth_call reads and the parity // cross-checks. No private key is ever committed or transmitted. // // Run: // cd aerenew/sdk-js && npm run build // node examples/pqc-identity-e2e.mjs // AERE_RPC=https://rpc.aere.network node examples/pqc-identity-e2e.mjs import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes, hexlify, getBytes, } from 'ethers'; import { AERE_MAINNET } from '../dist/addresses.js'; import { generateFalconRoot, encryptFalconSecretKey } from '../dist/wallet/falconKeystore.js'; import { AerePQCKeyRegistryClient } from '../dist/pqc/AerePQCKeyRegistryClient.js'; import { AereAgentDIDClient } from '../dist/pqc/AereAgentDIDClient.js'; import { AerePQCSocialRecoveryClient, recoveryChallenge as deriveRecoveryChallenge } from '../dist/account/AerePQCSocialRecoveryClient.js'; import { SCHEME, SCHEME_NAME, verifyLocal } from '../dist/pqc/index.js'; const RPC = process.env.AERE_RPC || AERE_MAINNET.rpc; const line = (s = '') => console.log(s); const short = (h, n = 10) => (h.length > 2 * n + 2 ? `${h.slice(0, n + 2)}…${h.slice(-n)}` : h); const ok = (b) => (b ? 'OK' : 'MISMATCH'); const provider = new JsonRpcProvider(RPC); const net = await provider.getNetwork(); const block = await provider.getBlockNumber(); line('AERE post-quantum identity — end-to-end (read-only)'); line(`RPC ${RPC} · chainId ${net.chainId} · block ${block}`); line('This script sends NO transactions. It reads live state and prints the txs a wallet WOULD send.'); // The identity the keys bind to. Any address works for the read-only flow — the // PoP challenge commits to this address, and the account that pays gas is the owner. // Override with AERE_OWNER=0x... to bind to your own address. const OWNER = process.env.AERE_OWNER || Wallet.createRandom().address; line(`\nowner (identity) ${OWNER}`); // ── 1. Falcon-512 root, generated + encrypted client-side ──────────────────── line('\n=== 1. Falcon-512 quantum-durable root (client-side) ==='); const root = generateFalconRoot(); line(`scheme ${SCHEME_NAME[root.scheme]} (id ${root.scheme})`); line(`public key ${root.publicKey.length} bytes ${short(hexlify(root.publicKey))}`); const keystore = await encryptFalconSecretKey(root, 'correct horse battery staple'); line(`encrypted keystore ${keystore.cipher} / ${keystore.kdf} (${keystore.kdfIterations} iters)`); line(` ciphertext ${short(keystore.ciphertext)} (secret key never stored in the clear)`); // ── 2. Register the root key with on-chain proof-of-possession ─────────────── line('\n=== 2. Register root key with proof-of-possession (AerePQCKeyRegistry) ==='); const reg = new AerePQCKeyRegistryClient(provider); line(`registry ${reg.address}`); line(`keyCount [live] ${await reg.keyCount()}`); const idNonce = await reg.identityNonce(OWNER); line(`identityNonce(owner) ${idNonce}`); // Local PoP challenge, then cross-check it against the live contract view. const localPop = reg.derivePopChallenge(OWNER, root.scheme, root.publicKey, idNonce); const chainPop = await reg.popChallenge(OWNER, root.scheme, root.publicKey, idNonce); line(`PoP challenge (local) ${short(localPop)}`); line(`PoP challenge (on-chain)${short(chainPop)} parity: ${ok(localPop.toLowerCase() === chainPop.toLowerCase())}`); // Build + locally verify the PoP envelope, then show the registerKey tx. const pop = reg.buildRegisterKeyPoP(OWNER, root.scheme, root.secretKey, root.publicKey, idNonce); const popValid = verifyLocal(root.scheme, getBytes(pop.challenge), pop.signature, root.publicKey); line(`PoP signature ${pop.signature.length} bytes verifyLocal: ${ok(popValid)}`); const registerCalldata = reg.contract.interface.encodeFunctionData( 'registerKey', [root.scheme, hexlify(root.publicKey), hexlify(pop.signature)], ); line(`registerKey tx to ${reg.address}`); line(` calldata ${(registerCalldata.length - 2) / 2} bytes ${short(registerCalldata)}`); line(' (broadcast: reg.registerKeyWithPoP(signer, scheme, secretKey, pubKey) — signer is the owner)'); // The keyId this registration would take (registry is append-only from keyCount). const rootKeyId = await reg.keyCount(); line(` -> would take keyId ${rootKeyId}`); // ── 3. Agent DID: Falcon root + revocable secp256k1 session key ────────────── line('\n=== 3. Agent DID + session key (AereAgentDID) ==='); const did = new AereAgentDIDClient(provider); line(`agent DID ${did.address}`); line(`sessionCount [live] ${await did.sessionCount()}`); line(`agentExists(keyId ${rootKeyId}) ${await did.agentExists(rootKeyId)}`); const createAgentCalldata = did.contract.interface.encodeFunctionData('createAgent', [rootKeyId]); line(`createAgent tx to ${did.address} calldata ${short(createAgentCalldata)}`); // Fresh secp256k1 session key (hot key, disposable). Generated locally. const session = Wallet.createRandom(); const sessionParams = { rootKeyId, sessionAddr: session.address, scopeHash: keccak256(toUtf8Bytes('aere.agent.session.v1:inference')), spendCap: 10n ** 18n, // 1 AERE cumulative expiry: BigInt(Math.floor(Date.now() / 1000) + 30 * 24 * 3600), // +30 days }; line(`session key ${session.address} (secp256k1, revocable)`); line(` scope / cap / expiry ${short(sessionParams.scopeHash)} / 1 AERE / ${sessionParams.expiry}`); // Session issuance is Falcon-PoP-gated. Cross-check the issuance challenge (explicit nonce 0). const localSess = did.deriveSessionChallenge(sessionParams, 0n); const chainSess = await did.sessionChallenge(sessionParams, 0n); line(`issuance challenge local ${short(localSess)} parity: ${ok(localSess.toLowerCase() === chainSess.toLowerCase())}`); const issue = did.buildIssueSessionPoP(sessionParams, root.secretKey, root.scheme, 0n); line(`issueSession popSig ${issue.popSig.length} bytes (Falcon PoP by the root)`); const issueCalldata = did.contract.interface.encodeFunctionData('issueSession', [ sessionParams.rootKeyId, sessionParams.sessionAddr, sessionParams.scopeHash, sessionParams.spendCap, sessionParams.expiry, hexlify(issue.popSig), ]); line(`issueSession tx to ${did.address} calldata ${(issueCalldata.length - 2) / 2} bytes`); // Authorize an action with the SESSION key (cheap ecrecover hot path). const sessionId = await did.sessionCount(); // append-only const actionHash = keccak256(toUtf8Bytes('POST /v1/inference {"model":"aere-1"}')); const amount = 5n * 10n ** 17n; // 0.5 AERE const localDigest = did.deriveActionDigest(sessionId, sessionParams.scopeHash, actionHash, amount, 0n); const chainDigest = await did.actionDigest(sessionId, sessionParams.scopeHash, actionHash, amount, 0n); line(`action digest local ${short(localDigest)} parity: ${ok(localDigest.toLowerCase() === chainDigest.toLowerCase())}`); const auth = did.buildAuthorization(sessionId, sessionParams.scopeHash, actionHash, amount, 0n, session.privateKey); line(`authorize sig ${short(auth.signature)} (secp256k1 over the raw digest)`); const authCalldata = did.contract.interface.encodeFunctionData('authorize', [ sessionId, sessionParams.scopeHash, actionHash, amount, auth.signature, ]); line(`authorize tx to ${did.address} calldata ${short(authCalldata)}`); // ── 4. Quantum-durable M-of-N guardians (AerePQCSocialRecoveryModule) ──────── line('\n=== 4. PQC social-recovery guardians (AerePQCSocialRecoveryModule) ==='); // The module is a repo contract + client but is NOT yet deployed to mainnet, so // there is no live address to read; MODULE_ADDR is a placeholder for the calldata. const MODULE_ADDR = process.env.AERE_PQC_RECOVERY_MODULE || '0x' + '00'.repeat(20); const rec = new AerePQCSocialRecoveryClient(MODULE_ADDR, provider); // Three guardian keys, each a DISTINCT active AerePQCKeyRegistry key NOT owned by // the account (here illustrative keyIds; on-chain they must be registered first). const guardianKeyIds = [11n, 12n, 13n]; const threshold = 2n; line(`module (undeployed) ${MODULE_ADDR}`); line(`committee ${threshold}-of-${guardianKeyIds.length} guardian keyIds [${guardianKeyIds.join(', ')}]`); const installData = rec.encodeOnInstall(guardianKeyIds, threshold); line(`onInstall tx calldata ${(installData.length - 2) / 2} bytes ${short(installData)}`); line(`addGuardianKey(14) tx ${short(rec.encodeAddGuardianKey(14n))}`); line(`setThreshold(3) tx ${short(rec.encodeSetThreshold(3n))}`); // A recovery: the challenge each guardian PQC-signs to swap the account's root owner. const account = OWNER; const newOwner = Wallet.createRandom().address; const round = 0n; // first round for a fresh account const chal = deriveRecoveryChallenge(MODULE_ADDR, account, newOwner, round, net.chainId); line(`recovery: newOwner ${newOwner} round ${round}`); line(`recovery challenge ${short(chal)} (each of ${threshold} guardians PQC-signs this)`); line(' legs are built with rec.buildLegs(account, newOwner, [{guardianIndex, scheme, secretKey}...])'); line(' then submitted via rec.scheduleRecovery(...) — permissionless, 48h timelock, then executeRecovery'); line('\nDone. Live cross-checks (PoP / issuance / action digest) matched the on-chain views.'); line('Nothing was signed with a funded key and no transaction was sent.');