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.
149 lines
5.8 KiB
JavaScript
149 lines
5.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// scan.js, Aere PQC-readiness scanner CLI.
|
|
//
|
|
// node scan.js <address> --rpc <url> live scan over JSON-RPC [MEASURE]
|
|
// node scan.js <address> --bytecode 0x.. offline scan of given code
|
|
// node scan.js --fixture <name> offline scan of a bundled fixture
|
|
// node scan.js --list-fixtures list bundled fixtures
|
|
//
|
|
// Reports RED (classical-only) / YELLOW (hybrid) / GREEN (PQC-capable) for an
|
|
// EVM address on Aere (chain 2800) or any EVM chain reachable by RPC.
|
|
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import { scanLive, scanBytecode, isValidAddress } from './lib/scanner.js';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const FIXTURE_DIR = join(__dirname, 'fixtures');
|
|
|
|
const C = process.stdout.isTTY
|
|
? { red: (s) => `\x1b[31m${s}\x1b[0m`, yellow: (s) => `\x1b[33m${s}\x1b[0m`, green: (s) => `\x1b[32m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m`, b: (s) => `\x1b[1m${s}\x1b[0m` }
|
|
: { red: (s) => s, yellow: (s) => s, green: (s) => s, dim: (s) => s, b: (s) => s };
|
|
|
|
function usage(code = 0) {
|
|
console.log(`Aere PQC-readiness scanner
|
|
|
|
Usage
|
|
node scan.js <address> --rpc <url> live scan over JSON-RPC [MEASURE]
|
|
node scan.js <address> --bytecode 0x.. offline scan of given runtime code
|
|
node scan.js --fixture <name> offline scan of a bundled fixture
|
|
node scan.js --list-fixtures list bundled fixtures
|
|
node scan.js <address> --rpc <url> --json machine-readable output
|
|
|
|
Readiness
|
|
RED classical-only (EOA, or a contract with no PQC precompile usage)
|
|
YELLOW hybrid (uses both a classical primitive and the PQC precompile band)
|
|
GREEN PQC-capable (uses Aere's live native PQC precompiles 0x0AE1..0x0AE5)
|
|
|
|
Scope: this classifies ACCOUNT / APPLICATION quantum exposure. It does not, and
|
|
cannot, make Aere consensus post-quantum (chain 2800 seals with secp256k1 QBFT).`);
|
|
process.exit(code);
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const a = { _: [] };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const t = argv[i];
|
|
if (t === '--rpc') a.rpc = argv[++i];
|
|
else if (t === '--bytecode') a.bytecode = argv[++i];
|
|
else if (t === '--fixture') a.fixture = argv[++i];
|
|
else if (t === '--list-fixtures') a.listFixtures = true;
|
|
else if (t === '--json') a.json = true;
|
|
else if (t === '--help' || t === '-h') a.help = true;
|
|
else a._.push(t);
|
|
}
|
|
return a;
|
|
}
|
|
|
|
function listFixtures() {
|
|
return readdirSync(FIXTURE_DIR).filter((f) => f.endsWith('.hex')).map((f) => f.replace(/\.hex$/, ''));
|
|
}
|
|
|
|
function badge(readiness) {
|
|
if (readiness === 'GREEN') return C.green(C.b(' GREEN '));
|
|
if (readiness === 'YELLOW') return C.yellow(C.b(' YELLOW '));
|
|
return C.red(C.b(' RED '));
|
|
}
|
|
|
|
function printReport(r) {
|
|
console.log('');
|
|
console.log(`${badge(r.readiness)} ${C.b(r.address)}`);
|
|
console.log(` kind: ${r.kind}${r.live ? ` chainId: ${r.live.chainId ?? 'unknown'} code: ${r.live.codeBytes} bytes` : ''}`);
|
|
if (r.kind === 'contract') console.log(` bytecode: ${r.bytecodeLength} bytes`);
|
|
if (r.reason) console.log(` ${C.dim(r.reason)}`);
|
|
|
|
const s = r.signals || {};
|
|
const pqc = s.pqcLivePrecompiles || [];
|
|
if (pqc.length) {
|
|
console.log(' post-quantum precompiles (live band 0x0AE1..0x0AE5):');
|
|
for (const p of pqc) console.log(` - ${p.address} ${p.name} ${C.dim('[' + p.confidence + (p.callProximate ? ', call-proximate' : '') + ']')}`);
|
|
}
|
|
if (s.p256) console.log(` classical: ${s.p256.address} ${s.p256.name} ${C.dim('[' + s.p256.confidence + ']')}`);
|
|
if (s.ecrecover) console.log(` classical: ${s.ecrecover.address} ${s.ecrecover.name} ${C.dim('[' + s.ecrecover.confidence + ']')}`);
|
|
if ((s.pqcTestnetPrecompiles || []).length) {
|
|
for (const p of s.pqcTestnetPrecompiles) console.log(` ${C.yellow('testnet-only')}: ${p.address} ${p.name}`);
|
|
}
|
|
if (!pqc.length && !s.p256 && !s.ecrecover) console.log(' no signature-verification precompile usage detected in bytecode.');
|
|
|
|
if (r.migration) console.log(` ${C.b('migration')}: ${r.migration}`);
|
|
if ((r.flags || []).length) {
|
|
console.log(' notes:');
|
|
for (const f of r.flags) console.log(` ${C.dim('*')} ${f}`);
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) usage(0);
|
|
|
|
if (args.listFixtures) {
|
|
console.log(listFixtures().join('\n'));
|
|
return;
|
|
}
|
|
|
|
let report;
|
|
if (args.fixture) {
|
|
const path = join(FIXTURE_DIR, args.fixture + '.hex');
|
|
let code;
|
|
try {
|
|
code = readFileSync(path, 'utf8').trim();
|
|
} catch {
|
|
console.error(`fixture not found: ${args.fixture}. Available: ${listFixtures().join(', ')}`);
|
|
process.exit(1);
|
|
}
|
|
report = scanBytecode(`fixture:${args.fixture}`, code);
|
|
} else if (args.bytecode) {
|
|
const addr = args._[0] || '(provided bytecode)';
|
|
report = scanBytecode(addr, args.bytecode);
|
|
} else if (args._[0]) {
|
|
const addr = args._[0];
|
|
if (!isValidAddress(addr)) {
|
|
console.error(`invalid address: ${addr}`);
|
|
process.exit(1);
|
|
}
|
|
if (!args.rpc) {
|
|
console.error('a live address scan requires --rpc <url>. [MEASURE] Provide an RPC, or use --bytecode / --fixture for an offline scan.');
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
report = await scanLive(addr, args.rpc);
|
|
} catch (e) {
|
|
console.error(`[MEASURE] live scan failed against ${args.rpc}: ${e.message}`);
|
|
console.error('The RPC path is the only part of this tool that needs network access; offline --bytecode / --fixture scans still work.');
|
|
process.exit(2);
|
|
}
|
|
} else {
|
|
usage(1);
|
|
}
|
|
|
|
if (args.json) {
|
|
console.log(JSON.stringify(report, (_k, v) => (typeof v === 'bigint' ? '0x' + v.toString(16) : v), 2));
|
|
} else {
|
|
printReport(report);
|
|
}
|
|
}
|
|
|
|
main().catch((e) => { console.error(e); process.exit(1); });
|