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.
158 lines
9.7 KiB
JavaScript
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);
|