pqc-migration-toolkit/lib/readiness.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

199 lines
11 KiB
JavaScript

// readiness.js, the quantum-readiness cost simulator. Given an account (its scan
// result and, optionally, its enumerated assets) and a target post-quantum
// scheme, it produces a readiness REPORT: the color, which assets need moving,
// the one-time migration gas, and the ongoing per-authorization cost.
//
// SOURCING DISCIPLINE (load-bearing, not decoration):
// - PQC verify / auth / account-deploy gas is CITED from committed artifacts
// (AERE-BENCHMARK-REPORT.md Part C.1, pqc-account.json). Never invented.
// - Asset-MOVE gas (ERC-20 transfer/approve) is NOT in any Aere benchmark and
// is NOT measured here, so it is a clearly-labeled [VERIFY] planning
// estimate, anchored to the one asset-move number this session DID measure
// live: a native AERE send estimated at 21,246 gas via eth_estimateGas on
// https://rpc.aere.network (chain 2800, ~block 10,416,783, 2026-07-19).
// - Gas price is the live 1 Gwei base-fee floor on 2800.
import { PQC, CLASSICAL, FALCON512_SOLIDITY_VERIFIER_GAS, AERE_PQC_ACCOUNT_MEASURED_USEROP_GAS } from './precompiles.js';
import { ACCOUNT_DEPLOY_GAS, GAS_PRICE_GWEI } from './simulate.js';
// Native AERE transfer. 21,000 is the fixed EVM base cost for a value-only tx;
// the live estimate came back 21,246 (a touch above 21,000 for warm-account
// bookkeeping). We use the measured live figure as the planning number.
export const NATIVE_SEND_GAS = 21246; // [MEASURED-FRESH] eth_estimateGas, rpc.aere.network, 2026-07-19
export const NATIVE_SEND_GAS_SOURCE =
'[MEASURED-FRESH] eth_estimateGas for a native AERE send on https://rpc.aere.network (chain 2800), 2026-07-19 => 21,246 gas (21,000 protocol base + warm-account overhead).';
// ERC-20 move gas: NOT measured on Aere, NOT in the benchmark report. Planning
// estimates only, flagged [VERIFY]. Refine per token with eth_estimateGas.
export const ERC20_TRANSFER_GAS = 65000; // [VERIFY] cold-recipient transfer, planning estimate
export const ERC20_APPROVE_GAS = 46000; // [VERIFY] approve, planning estimate
export const MIGRATOR_PER_TOKEN_GAS = 40000; // [VERIFY] one safeTransferFrom hop inside migrate()
export const MIGRATOR_BASE_GAS = 30000; // [VERIFY] migrate() call overhead (checks, event)
export const ERC20_MOVE_GAS_SOURCE =
'[VERIFY] ERC-20 move gas is a planning estimate, NOT measured on Aere and NOT in AERE-BENCHMARK-REPORT.md. Refine with eth_estimateGas against the real token+allowance.';
// Per-scheme ongoing authorization gas. verifyTxGas is the native-precompile
// verify-and-record receipt [CITED: AERE-BENCHMARK-REPORT.md Part C.1].
const SCHEME_AUTH = {
falcon512: { label: 'Falcon-512 (native precompile)', authGas: PQC.falcon512.verifyAndRecordTxGas, cited: 'AERE-BENCHMARK-REPORT.md Part C.1 (86,336)' },
falcon1024: { label: 'Falcon-1024 (native precompile)', authGas: PQC.falcon1024.verifyAndRecordTxGas, cited: 'AERE-BENCHMARK-REPORT.md Part C.1 (145,496)' },
mldsa44: { label: 'ML-DSA-44 (native precompile)', authGas: PQC.mldsa44.verifyAndRecordTxGas, cited: 'AERE-BENCHMARK-REPORT.md Part C.1 (351,050)' },
slhdsa128s: { label: 'SLH-DSA-128s (native precompile)', authGas: PQC.slhdsa128s.verifyAndRecordTxGas, cited: 'AERE-BENCHMARK-REPORT.md Part C.1 (558,276)' },
hybrid: { label: 'Hybrid ECDSA + Falcon-512 (native)', authGas: PQC.falcon512.verifyAndRecordTxGas + CLASSICAL.ecrecover.marginalGas, cited: 'Part C.1 Falcon-512 86,336 + ecrecover 3,000' },
// The path a currently-DEPLOYED AerePQCAccount actually takes (Solidity Falcon
// verifier), measured on-chain. This is the honest default: it is what a
// migration TODAY costs per auth, ~122x the native projection.
falcon512_solidity: { label: 'Falcon-512 (deployed AerePQCAccount, Solidity verifier)', authGas: AERE_PQC_ACCOUNT_MEASURED_USEROP_GAS, cited: 'pqc-account.json handleOpsGasUsed 10,278,313 (real on-chain receipt)' },
};
export function readinessSchemeList() {
return Object.keys(SCHEME_AUTH);
}
function gasToAere(gas, gwei = GAS_PRICE_GWEI) {
return gas * gwei * 1e-9;
}
/**
* Compute the one-time asset-move gas for a set of assets on a given path.
* @param {{nativeNeedsMove:boolean, erc20Count:number}} assets
* @param {'direct'|'migrator'} path
*/
export function assetMoveGas(assets, path = 'direct') {
const { nativeNeedsMove = false, erc20Count = 0 } = assets;
const nativeGas = nativeNeedsMove ? NATIVE_SEND_GAS : 0;
let erc20Gas = 0;
const breakdown = [];
if (nativeNeedsMove) breakdown.push({ item: 'native AERE send', gas: NATIVE_SEND_GAS, source: NATIVE_SEND_GAS_SOURCE });
if (path === 'direct') {
erc20Gas = erc20Count * ERC20_TRANSFER_GAS;
if (erc20Count) breakdown.push({ item: `${erc20Count} x ERC-20 transfer`, gas: erc20Gas, source: ERC20_MOVE_GAS_SOURCE });
} else if (path === 'migrator') {
const approveGas = erc20Count * ERC20_APPROVE_GAS;
const migrateGas = erc20Count > 0 || nativeNeedsMove ? MIGRATOR_BASE_GAS + erc20Count * MIGRATOR_PER_TOKEN_GAS : 0;
erc20Gas = approveGas + migrateGas;
if (erc20Count) breakdown.push({ item: `${erc20Count} x ERC-20 approve`, gas: approveGas, source: ERC20_MOVE_GAS_SOURCE });
if (erc20Count > 0 || nativeNeedsMove) breakdown.push({ item: 'atomic migrate() sweep', gas: migrateGas, source: ERC20_MOVE_GAS_SOURCE });
} else {
throw new Error(`unknown path "${path}"`);
}
return { total: nativeGas + erc20Gas, nativeGas, erc20Gas, breakdown };
}
/**
* Build a quantum-readiness report for ONE account.
*
* @param {object} p
* @param {object} p.scan a scan report ({ address, kind, readiness, ... }).
* @param {object} [p.assets] enumerateAssets() output (native + tokens + movable).
* @param {string} [p.scheme] target scheme (default falcon512_solidity, the
* honest deployed-reality path). Native projection also shown.
* @param {'direct'|'migrator'} [p.path] asset-move path (default direct).
* @param {boolean} [p.deployAccount] count the CREATE2 deploy (default: only if
* the account is an EOA, since a contract may already be PQC-capable).
* @param {number} [p.projectedAuths] ongoing auths to project the running cost.
* @param {number} [p.gwei] gas price (default 1).
*/
export function buildReadinessReport({
scan, assets, scheme = 'falcon512_solidity', path = 'direct', deployAccount, projectedAuths = 1, gwei = GAS_PRICE_GWEI,
} = {}) {
if (!scan) throw new Error('buildReadinessReport requires a scan report');
const model = SCHEME_AUTH[scheme];
if (!model) throw new Error(`unknown scheme "${scheme}". Known: ${readinessSchemeList().join(', ')}`);
const isEoa = scan.kind === 'eoa';
const needDeploy = deployAccount ?? isEoa;
// asset picture
let nativeNeedsMove = false;
let erc20ToMove = [];
if (assets) {
nativeNeedsMove = !!(assets.native && assets.native.needsMove);
erc20ToMove = (assets.tokens || []).filter((t) => t.needsMove);
}
const erc20Count = erc20ToMove.length;
const deployGas = needDeploy ? ACCOUNT_DEPLOY_GAS : 0;
const move = assetMoveGas({ nativeNeedsMove, erc20Count }, path);
const oneTimeGas = deployGas + move.total;
const ongoingAuthGas = model.authGas * projectedAuths;
// native-precompile projection for the same scheme (context, not the deployed cost)
const nativeProjection = scheme === 'falcon512_solidity'
? { label: SCHEME_AUTH.falcon512.label, authGas: SCHEME_AUTH.falcon512.authGas, cited: SCHEME_AUTH.falcon512.cited }
: null;
return {
address: scan.address,
kind: scan.kind,
readiness: scan.readiness,
readinessReason: scan.reason,
assetsToMove: {
native: nativeNeedsMove ? { symbol: assets.native.symbol, balanceWei: assets.native.balanceWei } : null,
erc20: erc20ToMove.map((t) => ({ token: t.token, symbol: t.symbol, balance: t.balance })),
count: (nativeNeedsMove ? 1 : 0) + erc20Count,
note: assets ? null : 'No asset enumeration supplied. Pass --rpc (and optional --tokens) to enumerate movable assets.',
},
scheme: { key: scheme, ...model },
nativeProjection,
gas: {
deploy: deployGas,
deploySource: needDeploy ? 'pqc-account.json createGasUsed 1,493,084 [CITED]' : 'no deploy (contract already exists / not an EOA)',
assetMove: move.total,
assetMoveBreakdown: move.breakdown,
oneTimeTotal: oneTimeGas,
perAuth: model.authGas,
perAuthSource: '[CITED: ' + model.cited + ']',
projectedAuths,
ongoingAuthTotal: ongoingAuthGas,
},
cost: {
gwei,
oneTimeAere: gasToAere(oneTimeGas, gwei),
ongoingAuthAere: gasToAere(ongoingAuthGas, gwei),
},
scope: 'ACCOUNT/APPLICATION quantum-readiness. This does not, and cannot, make Aere consensus post-quantum (chain 2800 seals with classical secp256k1 QBFT). BN254 ZK verifiers remain classical.',
};
}
/** Render a readiness report as plain text for the CLI. */
export function formatReadinessReport(r) {
const L = [];
const aere = (n) => n.toFixed(9) + ' AERE';
L.push('');
L.push(`Quantum-readiness report ${r.address}`);
L.push(` status: ${r.readiness} (${r.kind})`);
if (r.readinessReason) L.push(` reason: ${r.readinessReason}`);
L.push('');
L.push(' Assets to move:');
if (r.assetsToMove.note) {
L.push(` - ${r.assetsToMove.note}`);
} else if (r.assetsToMove.count === 0) {
L.push(' - none detected (nothing to migrate)');
} else {
if (r.assetsToMove.native) L.push(` - native AERE: ${r.assetsToMove.native.balanceWei} wei`);
for (const t of r.assetsToMove.erc20) L.push(` - ERC-20 ${t.symbol || ''} ${t.token}: ${t.balance} (base units)`);
}
L.push('');
L.push(' One-time migration gas:');
L.push(` deploy target account: ${r.gas.deploy.toLocaleString().padStart(12)} (${r.gas.deploySource})`);
for (const b of r.gas.assetMoveBreakdown) L.push(` ${(b.item + ':').padEnd(24)}${b.gas.toLocaleString().padStart(12)}`);
L.push(` one-time TOTAL: ${r.gas.oneTimeTotal.toLocaleString().padStart(12)} = ${aere(r.cost.oneTimeAere)} at ${r.cost.gwei} Gwei`);
L.push('');
L.push(' Ongoing per-authorization cost (post-migration):');
L.push(` scheme: ${r.scheme.label}`);
L.push(` per auth: ${r.gas.perAuth.toLocaleString()} gas ${r.gas.perAuthSource}`);
L.push(` ${r.gas.projectedAuths} projected auth(s): ${r.gas.ongoingAuthTotal.toLocaleString()} gas = ${aere(r.cost.ongoingAuthAere)}`);
if (r.nativeProjection) {
L.push(` (native-precompile projection for the same scheme: ${r.nativeProjection.authGas.toLocaleString()} gas/auth [CITED: ${r.nativeProjection.cited}], available once the account verifies Falcon via the native 0x0AE1 precompile instead of the Solidity verifier.)`);
}
L.push('');
const hasErc20Estimate = r.gas.assetMoveBreakdown.some((b) => b.source && b.source.startsWith('[VERIFY]'));
L.push(hasErc20Estimate
? ' Note: ERC-20 move gas figures are [VERIFY] planning estimates, not measured on Aere; native-send gas is a fresh live measurement (21,246). PQC auth gas is [CITED].'
: ' Note: PQC auth gas is [CITED]; native-send gas is a fresh live measurement (21,246).');
L.push(` Scope: ${r.scope}`);
L.push('');
return L.join('\n');
}