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

158 lines
9.7 KiB
JavaScript

#!/usr/bin/env node
// selftest.js, the runnable proof that this toolkit really works, with NO live
// RPC required. It:
// 1. checks keccak256 against the empty-string NIST/Ethereum vector,
// 2. re-derives the LIVE sample AerePQCAccount address from its real Falcon
// key and asserts it equals the on-chain address (proves CREATE2 math),
// 3. runs the scanner against the bundled REAL bytecode fixtures and asserts
// the RED / YELLOW / GREEN classifications,
// 4. runs a small migration-cost simulation.
// Exits non-zero on any failure.
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { keccak256Hex, bytesToHex } from './lib/keccak.js';
import { predictPqcAccountAddress } from './lib/migrate.js';
import { scanBytecode } from './lib/scanner.js';
import { simulateMigration } from './lib/simulate.js';
import {
encodeMigrateCalldata, encodeApproveCalldata, encodeTransferCalldata, encodeBalanceOfCalldata,
buildMigrationTransactions,
} from './lib/migrator.js';
import { buildReadinessReport } from './lib/readiness.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
let failures = 0;
const ok = (name, cond, detail = '') => {
console.log(` ${cond ? 'PASS' : 'FAIL'} ${name}${detail ? ' ' + detail : ''}`);
if (!cond) failures++;
};
console.log('Aere PQC migration toolkit, self test\n');
// 1. keccak256 correctness
console.log('[1] keccak256 zero-dependency implementation');
const empty = keccak256Hex('0x');
ok('keccak256("") == c5d2460186f7233c...', empty === '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470', empty);
// 2. CREATE2 address re-derivation against the LIVE sample account
console.log('\n[2] CREATE2 account-address derivation (vs the live on-chain sample)');
const dep = JSON.parse(readFileSync(join(__dirname, '..', 'contracts', 'deployments', 'pqc-account.json'), 'utf8'));
const sampleKey = dep.sampleAccount.falconPubKey;
const expected = dep.sampleAccount.address;
const derived = predictPqcAccountAddress({
falconPubKey: sampleKey,
salt: dep.sampleAccount.salt,
factory: dep.factory,
initCodeHash: dep.accountInitCodeHash,
});
ok(`predictPqcAccountAddress == ${expected}`, derived.toLowerCase() === expected.toLowerCase(), derived);
// 3. scanner classifications against REAL bytecode fixtures
console.log('\n[3] scanner on bundled REAL bytecode fixtures');
const fx = (name) => readFileSync(join(__dirname, 'fixtures', name + '.hex'), 'utf8').trim();
const attest = scanBytecode('fixture:AerePQCAttestation', fx('AerePQCAttestation'));
ok('AerePQCAttestation => GREEN (native PQC precompiles)', attest.readiness === 'GREEN',
attest.readiness + ' [' + attest.signals.pqcLivePrecompiles.map((p) => p.address).join(',') + ']');
const txAcct = scanBytecode('fixture:AerePQCTxAccount', fx('AerePQCTxAccount'));
ok('AerePQCTxAccount => GREEN (native PQC precompiles)', txAcct.readiness === 'GREEN',
txAcct.readiness + ' [' + txAcct.signals.pqcLivePrecompiles.map((p) => p.address).join(',') + ']');
// AereHybridAuth uses ecrecover but delegates Falcon to the SOLIDITY verifier
// (a contract, not the native precompile), so bytecode scanning reads it RED.
// This is the documented honest limitation, asserted here so it cannot regress.
const hybrid = scanBytecode('fixture:AereHybridAuth', fx('AereHybridAuth'));
ok('AereHybridAuth => RED (delegates Falcon to Solidity verifier; scope limit)', hybrid.readiness === 'RED', hybrid.readiness);
const falconV = scanBytecode('fixture:AereFalcon512Verifier', fx('AereFalcon512Verifier'));
ok('AereFalcon512Verifier => RED (heavy math; 0x0100 constants are literals, not P-256)',
falconV.readiness === 'RED' && !falconV.signals.p256, falconV.readiness);
// synthetic hybrid: real EVM ops calling BOTH 0x01 (ecrecover) and 0x0AE1
// (Falcon-512) via STATICCALL. Proves the YELLOW path deterministically.
// Sequence per call: PUSH1 retSize, PUSH1 retOff, PUSH1 argsSize, PUSH1 argsOff,
// PUSH2/PUSH1 addr, GAS, STATICCALL, POP.
const synthetic =
'0x' +
'6020' + '6000' + '6040' + '6000' + '6001' + '5a' + 'fa' + '50' + // staticcall 0x01 (ecrecover)
'6020' + '6000' + '6040' + '6000' + '610ae1' + '5a' + 'fa' + '50'; // staticcall 0x0ae1 (Falcon-512)
const synth = scanBytecode('synthetic:hybrid', synthetic);
ok('synthetic 0x01 + 0x0AE1 => YELLOW (hybrid)', synth.readiness === 'YELLOW',
synth.readiness + ' pqc=' + synth.signals.pqcLivePrecompiles.length + ' ecrecover=' + (synth.signals.ecrecover ? 'yes' : 'no'));
// EOA path
const eoa = scanBytecode('0x0000000000000000000000000000000000000001', '0x');
ok('empty code => RED (EOA)', eoa.readiness === 'RED' && eoa.kind === 'eoa', eoa.kind);
// 4. simulation sanity
console.log('\n[4] migration cost simulation');
const sim = simulateMigration(
[{ scheme: 'falcon512', authsPerAccount: 1 }, { scheme: 'hybrid', authsPerAccount: 2 }],
{ gweiPrice: 1 },
);
ok('simulation totals are positive and finite',
sim.totals.totalGas > 0 && Number.isFinite(sim.totals.estimatedAere),
`${sim.totals.totalGas.toLocaleString()} gas, ${sim.totals.estimatedAere.toFixed(6)} AERE`);
// 5. ABI calldata builders vs ethers v6 ground truth (hardcoded expected values,
// each independently generated by ethers 6.16.0; see the toolkit README). This
// proves the zero-dependency encoder emits byte-identical calldata to a full ABI
// library, so what a user is handed to sign is correct.
console.log('\n[5] migration SDK calldata (vs ethers v6 ground truth)');
const dest = '0xa42a5e7F72E46BadC11367650Ec34D676194326f';
const tokenA = '0x1111111111111111111111111111111111111111';
const tokenB = '0x2222222222222222222222222222222222222222';
const EXP = {
migrate: '0xe015b041000000000000000000000000a42a5e7f72e46badc11367650ec34d676194326f000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000011111111111111111111111111111111111111110000000000000000000000002222222222222222222222222222222222222222000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000007d0',
approve: '0x095ea7b3000000000000000000000000a42a5e7f72e46badc11367650ec34d676194326f00000000000000000000000000000000000000000000000000000000000003e8',
transfer: '0xa9059cbb000000000000000000000000a42a5e7f72e46badc11367650ec34d676194326f00000000000000000000000000000000000000000000000000000000000003e8',
balanceOf: '0x70a08231000000000000000000000000a42a5e7f72e46badc11367650ec34d676194326f',
};
const gotMigrate = encodeMigrateCalldata({ destination: dest, tokens: [tokenA, tokenB], amounts: [1000n, 2000n], moveNative: true });
ok('encodeMigrateCalldata == ethers migrate(...)', gotMigrate.toLowerCase() === EXP.migrate, `${(gotMigrate.length - 2) / 2} bytes`);
ok('encodeApproveCalldata == ethers approve(...)', encodeApproveCalldata(dest, 1000n).toLowerCase() === EXP.approve);
ok('encodeTransferCalldata == ethers transfer(...)', encodeTransferCalldata(dest, 1000n).toLowerCase() === EXP.transfer);
ok('encodeBalanceOfCalldata == ethers balanceOf(...)', encodeBalanceOfCalldata(dest).toLowerCase() === EXP.balanceOf);
// 6. buildMigrationTransactions: unsigned, correct target, no signature anywhere.
console.log('\n[6] unsigned migration transaction plan');
const plan = buildMigrationTransactions({
path: 'direct', eoa: '0x000000000000000000000000000000000000dEaD', falconPubKey: sampleKey,
tokens: [{ token: tokenA, amountWei: 5n }], nativeWei: 100n,
});
ok('plan.signed === false (nothing signed)', plan.signed === false);
ok('plan.target matches CREATE2 derivation', plan.target.toLowerCase() === expected.toLowerCase(), plan.target);
ok('every tx object is unsigned (signed:false, no r/s/v/signature field)',
plan.transactions.length > 0 && plan.transactions.every((t) => t.signed === false && !('signature' in t.tx) && !('r' in t.tx)),
`${plan.transactions.length} txs`);
ok('direct path builds deploy + erc20 transfer + native send (3 steps)',
plan.transactions.length === 3 &&
plan.transactions[0].kind === 'createAccount' &&
plan.transactions[1].kind === 'erc20Transfer' &&
plan.transactions[2].kind === 'nativeTransfer',
plan.transactions.map((t) => t.kind).join(','));
let migratorThrew = false;
try { buildMigrationTransactions({ path: 'migrator', eoa: '0x000000000000000000000000000000000000dEaD', falconPubKey: sampleKey, tokens: [{ token: tokenA, amountWei: 5n }] }); }
catch { migratorThrew = true; }
ok('migrator path without an explicit migrator address is refused (not deployed)', migratorThrew);
// 7. readiness report composes cited gas + honest asset-move estimate.
console.log('\n[7] quantum-readiness report');
const rr = buildReadinessReport({
scan: { address: dest, kind: 'eoa', readiness: 'RED', reason: 'EOA' },
assets: { native: { symbol: 'AERE', balanceWei: '100', needsMove: true }, tokens: [{ token: tokenA, symbol: 'TKA', balance: '5', needsMove: true }], movable: 2 },
scheme: 'falcon512_solidity', path: 'direct', projectedAuths: 1, gwei: 1,
});
ok('readiness one-time gas = deploy + native + erc20 (positive, finite)',
rr.gas.oneTimeTotal > rr.gas.deploy && Number.isFinite(rr.cost.oneTimeAere),
`${rr.gas.oneTimeTotal.toLocaleString()} gas one-time`);
ok('readiness ongoing per-auth uses the CITED deployed-account figure (10,278,313)',
rr.gas.perAuth === 10278313, `${rr.gas.perAuth.toLocaleString()} gas/auth`);
ok('readiness counts 2 movable assets', rr.assetsToMove.count === 2);
console.log(`\n${failures === 0 ? 'ALL PASS' : failures + ' FAILURE(S)'}`);
process.exit(failures === 0 ? 0 : 1);