// migrate.js, the migration SDK. Zero-dependency helpers to move an // ECDSA-controlled account toward a post-quantum account using Aere's EXISTING, // live primitives: // // AerePQCAccountFactory (CREATE2 factory, LIVE) 0xd5315Ea7...CE58 // -> AerePQCAccount (Falcon-512 owned ERC-4337 account, LIVE sample) // AereHybridAuth (ECDSA + Falcon-512, repo source; NOT yet on mainnet) // AereFalcon512Verifier (Solidity verifier the two accounts delegate to, LIVE) // AerePQCKeyRegistry (proof-of-possession key registry, LIVE) // EIP-7702 (EOA delegation, supported at protocol level) // // This file derives counterfactual account addresses, builds the calldata for // the on-chain steps, and returns an honest, step-by-step migration plan. It // does NOT sign or broadcast anything. import { keccak256, hexToBytes, bytesToHex, toChecksumAddress } from './keccak.js'; // ── canonical addresses (chain 2800) ───────────────────────────────────────── // live=true means confirmed on mainnet 2800 (address book / deployment json). // live=false means the contract exists in the repo but has no mainnet address // in the address book; deploying it is a separate, deployer/founder-gated step. // [CITED: aerenew/sdk-js/src/addresses.ts; aerenew/contracts/deployments/pqc-account.json] export const ADDRESSES = { AerePQCAccountFactory: { address: '0xd5315Ea7caa60d320c4f34b1bEd70dd9cc02CE58', live: true }, AerePQCAccount_sample: { address: '0xa42a5e7F72E46BadC11367650Ec34D676194326f', live: true }, AereEntryPointV2: { address: '0x8D6f40598d552fF0Cb358b6012cF4227B86aF770', live: true }, AereFalcon512Verifier: { address: '0x4E8e9682329e646784fB3bd01430aA4bA54D8fFC', live: true }, AerePQCKeyRegistry: { address: '0x1eCa3c5ADcBD0b22636D8672b00faC6D89363691', live: true }, AereHybridAuthorizer: { address: '0x168F2A6a3071e7654CF1784a6f5d7BC8e1a582E0', live: true }, // Repo source only: no mainnet address in the address book as of 2026-07-19. AereHybridAuth: { address: null, live: false, note: 'repo source (contracts/pqc/AereHybridAuth.sol); not deployed to mainnet. [VERIFY]' }, }; // CREATE2 init-code hash of AerePQCAccount (no constructor args), read from the // live factory deploy record. [CITED: contracts/deployments/pqc-account.json] export const ACCOUNT_INIT_CODE_HASH = '0x05dd938d3980ee22354b9635703e916187d0323259d2c9468954382245bb10ce'; export const FALCON512_PK_LEN = 897; export const FALCON512_PK_HEADER = 0x09; // ── minimal ABI encoder (only the types this SDK needs) ────────────────────── // Supports: 'uint256', 'address', 'bytes32', 'bytes'. Matches Solidity abi.encode. function encode32Uint(value) { let hex = BigInt(value).toString(16); return hex.padStart(64, '0'); } function encode32Address(addr) { const a = (addr.startsWith('0x') ? addr.slice(2) : addr).toLowerCase(); return a.padStart(64, '0'); } function encode32Bytes32(b) { const h = (b.startsWith('0x') ? b.slice(2) : b).toLowerCase(); if (h.length !== 64) throw new Error('bytes32 must be 32 bytes'); return h; } function encodeDynBytes(hex) { const h = (hex.startsWith('0x') ? hex.slice(2) : hex).toLowerCase(); const byteLen = h.length / 2; const lenWord = encode32Uint(byteLen); const padded = h.padEnd(Math.ceil(h.length / 64) * 64, '0'); return lenWord + padded; } /** * abi.encode(types, values) for the supported type set. Returns 0x-hex. */ export function abiEncode(types, values) { const isDynamic = (t) => t === 'bytes'; const headParts = []; const tailParts = []; let tailOffset = types.length * 32; for (let k = 0; k < types.length; k++) { const t = types[k]; const v = values[k]; if (isDynamic(t)) { headParts.push(encode32Uint(tailOffset)); const enc = encodeDynBytes(v); tailParts.push(enc); tailOffset += enc.length / 2; } else if (t === 'uint256') headParts.push(encode32Uint(v)); else if (t === 'address') headParts.push(encode32Address(v)); else if (t === 'bytes32') headParts.push(encode32Bytes32(v)); else throw new Error(`unsupported abi type: ${t}`); } return '0x' + headParts.join('') + tailParts.join(''); } /** 4-byte function selector (8 hex chars, NO 0x prefix) from a signature string. */ export function selector(signature) { const h = keccak256(new TextEncoder().encode(signature)); return bytesToHex(h.slice(0, 4)).slice(2); } /** Build 0x-prefixed calldata: selector(sig) ++ abiEncode(types, values). */ export function encodeCall(signature, types, values) { const sel = selector(signature); const args = abiEncode(types, values).slice(2); return '0x' + sel + args; } // ── Falcon public-key validation ───────────────────────────────────────────── export function validateFalconPubKey(pubKeyHex) { const bytes = hexToBytes(pubKeyHex); if (bytes.length !== FALCON512_PK_LEN) { throw new Error(`Falcon-512 public key must be ${FALCON512_PK_LEN} bytes, got ${bytes.length}`); } if (bytes[0] !== FALCON512_PK_HEADER) { throw new Error(`Falcon-512 public key must start with header byte 0x09, got 0x${bytes[0].toString(16)}`); } return true; } // ── CREATE2 account-address derivation ─────────────────────────────────────── // Mirrors AerePQCAccountFactory.predictAddress exactly: // saltMix = keccak256(abi.encode(falconPubKey, salt)) // account = keccak256(0xff ++ factory ++ saltMix ++ ACCOUNT_INIT_CODE_HASH)[12:] /** saltMix = keccak256(abi.encode(bytes falconPubKey, uint256 salt)). */ export function computeSaltMix(falconPubKeyHex, salt) { const encoded = abiEncode(['bytes', 'uint256'], [falconPubKeyHex, salt]); return bytesToHex(keccak256(hexToBytes(encoded))); } /** * Predict the counterfactual AerePQCAccount address for (falconPubKey, salt). * @param {object} p * @param {string} p.falconPubKey 897-byte Falcon-512 public key (0x-hex, 0x09 header) * @param {number|bigint} p.salt CREATE2 salt * @param {string} [p.factory] factory address (defaults to the live factory) * @param {string} [p.initCodeHash] account init-code hash (defaults to the live one) * @returns {string} checksummed account address */ export function predictPqcAccountAddress({ falconPubKey, salt = 0, factory, initCodeHash } = {}) { validateFalconPubKey(falconPubKey); const fac = factory || ADDRESSES.AerePQCAccountFactory.address; const ich = initCodeHash || ACCOUNT_INIT_CODE_HASH; const saltMix = computeSaltMix(falconPubKey, salt); const pre = new Uint8Array(1 + 20 + 32 + 32); pre[0] = 0xff; pre.set(hexToBytes(fac), 1); pre.set(hexToBytes(saltMix), 21); pre.set(hexToBytes(ich), 53); const hash = keccak256(pre); return toChecksumAddress(bytesToHex(hash.slice(12))); } // ── on-chain step calldata builders ────────────────────────────────────────── /** AerePQCAccountFactory.createAccount(bytes falconPubKey, uint256 salt). */ export function encodeCreateAccount(falconPubKeyHex, salt = 0) { validateFalconPubKey(falconPubKeyHex); return encodeCall('createAccount(bytes,uint256)', ['bytes', 'uint256'], [falconPubKeyHex, salt]); } /** AereHybridAuth.registerIdentity(bytes32 identityId, address ecdsaSigner, bytes falconPubKey). */ export function encodeHybridRegisterIdentity(identityId, ecdsaSigner, falconPubKeyHex) { validateFalconPubKey(falconPubKeyHex); return encodeCall( 'registerIdentity(bytes32,address,bytes)', ['bytes32', 'address', 'bytes'], [identityId, ecdsaSigner, falconPubKeyHex], ); } /** AerePQCKeyRegistry.registerKey(uint8 scheme, bytes pubKey, bytes signature) selector. */ export function encodeRegisterKeySelector() { return '0x' + selector('registerKey(uint8,bytes,bytes)'); } /** * Produce a documented, honest migration plan for moving an ECDSA-controlled * account to a post-quantum account. Returns structured steps; nothing is * signed or broadcast. * * @param {object} opts * @param {'pqc'|'hybrid'} opts.mode 'pqc' = Falcon-512-only account; 'hybrid' = ECDSA + Falcon. * @param {string} opts.falconPubKey 897-byte Falcon-512 public key (0x-hex). * @param {string} [opts.ecdsaSigner] the classical address keeping a leg (hybrid mode). * @param {number|bigint} [opts.salt] CREATE2 salt (pqc mode). */ export function buildMigrationPlan({ mode = 'pqc', falconPubKey, ecdsaSigner, salt = 0 } = {}) { validateFalconPubKey(falconPubKey); const steps = []; const caveats = [ 'SCOPE: this migrates ACCOUNT AUTHENTICATION to a post-quantum scheme. It does NOT make Aere consensus post-quantum; mainnet 2800 still seals blocks with classical secp256k1 QBFT.', 'The deployed AerePQCAccount / AereHybridAuth verify Falcon via the SOLIDITY AereFalcon512Verifier (0x4E8e...D8fFC), so one authorization costs ~10.5M gas today (measured 10,278,313 in the live userOp receipt), under the EIP-7825 2^24 per-tx cap. A native SHAKE precompile would cut this sharply (separate roadmap item).', ]; if (mode === 'pqc') { const predicted = predictPqcAccountAddress({ falconPubKey, salt }); steps.push({ n: 1, title: 'Derive the counterfactual PQC account address (off-chain, free)', how: 'predictPqcAccountAddress({ falconPubKey, salt }), pure CREATE2 math, no RPC.', result: predicted, }); steps.push({ n: 2, title: 'Deploy the PQC account via the LIVE factory (permissionless)', how: 'Send AerePQCAccountFactory.createAccount(falconPubKey, salt) to ' + ADDRESSES.AerePQCAccountFactory.address + '.', calldata: encodeCreateAccount(falconPubKey, salt), note: 'createAccount is permissionless and idempotent; anyone can deploy the counterfactual account, and re-deploying returns the existing one. Sample deploy cost was ~1,493,084 gas. [CITED: pqc-account.json createGasUsed]', }); steps.push({ n: 3, title: 'Move control: fund the PQC account, then move assets off the old EOA into it', how: 'From the old ECDSA EOA, transfer AERE / tokens / roles to the PQC account address. After this, spending authority is the Falcon-512 key only (no ECDSA fallback).', note: 'Optionally use EIP-7702 (supported on Aere) to have the EOA temporarily delegate execution to account code during the transition, so a single EOA transaction can batch the move.', }); caveats.push('The AerePQCAccount is Falcon-512-ONLY (no ECDSA fallback). Losing the Falcon key loses the account. Register the key in AerePQCKeyRegistry (proof-of-possession) and set PQC social-recovery guardians BEFORE moving material value.'); } else if (mode === 'hybrid') { if (!ecdsaSigner) throw new Error('hybrid mode requires ecdsaSigner'); const identityId = bytesToHex(keccak256(hexToBytes(encodeHybridIdentitySeed(ecdsaSigner, falconPubKey)))); steps.push({ n: 1, title: 'Deploy AereHybridAuth (deployer/founder-gated)', how: 'AereHybridAuth is repo source with no mainnet address yet. Deploying it (constructor arg = AereFalcon512Verifier ' + ADDRESSES.AereFalcon512Verifier.address + ') is a one-time deploy tx.', note: 'FOUNDER-GATED: a fresh mainnet deploy (or any corrected V2 redeploy of an account factory) is signed by the Foundation/deployer key, not by this toolkit. [VERIFY] AereHybridAuth not currently deployed to 2800.', }); steps.push({ n: 2, title: 'Register the hybrid identity (permissionless, append-only)', how: 'AereHybridAuth.registerIdentity(identityId, ecdsaSigner, falconPubKey).', identityId, calldata: encodeHybridRegisterIdentity(identityId, ecdsaSigner, falconPubKey), note: 'Binds your existing classical address to a Falcon-512 key. An identityId can be registered exactly once.', }); steps.push({ n: 3, title: 'Authorize under both legs during the transition window', how: 'AereHybridAuth.authorize(identityId, messageHash, ecdsaSig, falconNonce, falconCompSig) requires BOTH a valid ECDSA and a valid Falcon-512 signature over the same 32-byte hash.', note: 'Defense-in-depth: an attacker must break BOTH secp256k1 AND Falcon-512. This is the transition-era primitive; once you trust the PQC leg alone, move to a Falcon-only AerePQCAccount.', }); caveats.push('Hybrid marginal verify cost is ~43,000 gas at the native-precompile level (Falcon-512 40,000 + ecrecover 3,000); the deployed AereHybridAuth uses the Solidity Falcon verifier, so its authorize() is dominated by the ~10.5M Solidity Falcon path.'); } else { throw new Error(`unknown mode: ${mode}`); } return { mode, falconPubKey: '0x' + hexToBytes(falconPubKey).length + '-byte key', steps, caveats }; } // Deterministic identity seed for the hybrid example (not consensus-critical). function encodeHybridIdentitySeed(ecdsaSigner, falconPubKeyHex) { return abiEncode(['address', 'bytes'], [ecdsaSigner, falconPubKeyHex]); }