The published repository served the 0.3.0 line from May while the install page already pointed here. This carries the uncommitted line: 35+ typed contract clients over the canonical address book, the compliance surface, and the hybrid PQC wallet helpers.
166 lines
10 KiB
JavaScript
166 lines
10 KiB
JavaScript
// 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.');
|