pqc-migration-toolkit/lib/migrate.js
Aere Network de9a79298c Aere PQC migration toolkit: measure an address quantum exposure from its bytecode
Zero dependencies, no account, no API. Every command takes --rpc, so it runs
against a node you operate and never needs ours. That is the point: an exposure
measurement you cannot re-run is an opinion with a logo on it.

Ships with two self-checks meant to be run by you, not just by us. The second,
proba-vocabular.mjs, exists because of a real defect found on 2026-08-16: the
scanner emits RED / YELLOW / GREEN, two downstream files each wrote their own
copy of that list, and both wrote AMBER, so a hybrid contract, exactly a client
who has already started migrating, was reported as unmeasurable. Both sides were
self-consistent, so nothing we had could see it. The vocabulary now has one
exported source, and the test walks the whole path with a negative control that
makes it able to fail.

The same scanner returns RED about our own contracts. See VERIFY-US.md.
2026-08-17 10:31:31 +03:00

246 lines
13 KiB
JavaScript

// 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]);
}