#!/usr/bin/env node // --------------------------------------------------------------------------- // verify-live-pqc-precompiles.mjs // // READ-ONLY verification that the Aere PQC precompiles are genuinely live on // mainnet chain 2800 and produce correct results against known answer vectors. // // Sends ONLY eth_call / eth_chainId / eth_blockNumber. Sends NO transactions, // signs nothing, touches no validator, holds no key. // // node scripts/verify-live-precompiles.mjs // // Exit 0 only if every live-band check passes AND the testnet-only band // (0x0AE6 ML-KEM-768, 0x0AE7 Falcon HashToPoint) is confirmed NOT live on // mainnet, which is itself a published claim that should be falsifiable. // // Ground truth this script does NOT assert: consensus on chain 2800 is // classical secp256k1 ECDSA QBFT. These precompiles are an application and // account layer capability only. // --------------------------------------------------------------------------- import { readFileSync } from "node:fs"; import { createHash } from "node:crypto"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const RPC = process.env.AERE_RPC || "https://rpc.aere.network"; const HERE = dirname(fileURLToPath(import.meta.url)); const VECTORS = join(HERE, "..", "vectors"); const PRECOMPILES = { FALCON512: "0x0000000000000000000000000000000000000ae1", FALCON1024: "0x0000000000000000000000000000000000000ae2", MLDSA44: "0x0000000000000000000000000000000000000ae3", SLHDSA128S: "0x0000000000000000000000000000000000000ae4", SHAKE256: "0x0000000000000000000000000000000000000ae5", MLKEM768: "0x0000000000000000000000000000000000000ae6", // testnet-only HASHTOPOINT: "0x0000000000000000000000000000000000000ae7", // testnet-only }; const strip = (h) => (h || "").replace(/^0x/, ""); const hex = (h) => "0x" + strip(h); const word = (n) => n.toString(16).padStart(64, "0"); let id = 0; async function rpc(method, params) { const r = await fetch(RPC, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: ++id, method, params }), }); const j = await r.json(); if (j.error) throw new Error(`${method}: ${JSON.stringify(j.error)}`); return j.result; } const call = (to, data) => rpc("eth_call", [{ to, data: hex(data) }, "latest"]); const results = []; function record(name, expected, got, pass, note) { results.push({ name, expected, got, pass, note }); console.log( `${pass ? "PASS" : "FAIL"} ${name}\n expected: ${expected}\n got: ${got}` + (note ? `\n note: ${note}` : "") ); } const TRUE_WORD = "0x" + word(1); const FALSE_WORD = "0x" + word(0); async function main() { const chainId = await rpc("eth_chainId", []); const block = await rpc("eth_blockNumber", []); console.log(`RPC: ${RPC}`); console.log(`chainId: ${chainId} (${parseInt(chainId, 16)})`); console.log(`block: ${block} (${parseInt(block, 16)})`); console.log(""); if (parseInt(chainId, 16) !== 2800) throw new Error("not chain 2800"); // -- 0x0AE5 SHAKE256 ----------------------------------------------------- // Shipped framing: 32-byte big-endian outLen word, then the data to absorb. for (const [label, msg] of [["abc", Buffer.from("abc")], ["empty", Buffer.alloc(0)]]) { const want = "0x" + createHash("shake256", { outputLength: 32 }).update(msg).digest("hex"); const got = await call(PRECOMPILES.SHAKE256, word(32) + msg.toString("hex")); record(`0x0AE5 SHAKE256(${label}) outLen=32 vs FIPS 202`, want, got, got === want); } { // XOF property: the first 32 bytes of a 64-byte squeeze must equal the 32-byte squeeze. const want = "0x" + createHash("shake256", { outputLength: 64 }).update("abc").digest("hex"); const got = await call(PRECOMPILES.SHAKE256, word(64) + Buffer.from("abc").toString("hex")); record("0x0AE5 SHAKE256(abc) outLen=64 (true XOF squeeze)", want, got, got === want); } // -- 0x0AE1 / 0x0AE2 Falcon --------------------------------------------- // Shipped framing for the verify precompiles: pk || sm (NIST signed-message). const falcon = [ ["0x0AE1 Falcon-512", PRECOMPILES.FALCON512, "falcon512_kat0.json"], ["0x0AE2 Falcon-1024", PRECOMPILES.FALCON1024, "falcon1024_kat0.json"], ]; for (const [label, addr, file] of falcon) { const k = JSON.parse(readFileSync(join(VECTORS, file), "utf8")); const good = await call(addr, strip(k.pk) + strip(k.sm)); record(`${label} official NIST KAT vector 0 ACCEPTS`, TRUE_WORD, good, good === TRUE_WORD); // Negative control: flip one byte of the signed message. A precompile that // returns 0x01 for everything would pass the line above and fail here. const b = Buffer.from(strip(k.sm), "hex"); b[Math.floor(b.length / 2)] ^= 0x01; const bad = await call(addr, strip(k.pk) + b.toString("hex")); record( `${label} tampered signed-message REJECTS (negative control)`, FALSE_WORD, bad, bad === FALSE_WORD ); } // -- 0x0AE3 ML-DSA-44 / 0x0AE4 SLH-DSA-128s against NIST ACVP ------------ // ACVP fixtures carry pk / message / signature; NIST sm = signature || message. const acvp = [ ["0x0AE3 ML-DSA-44", PRECOMPILES.MLDSA44, "mldsa44-acvp-tg8.json"], ["0x0AE4 SLH-DSA-128s", PRECOMPILES.SLHDSA128S, "sphincs-sha2-128s-acvp-tg31.json"], ]; for (const [label, addr, file] of acvp) { const k = JSON.parse(readFileSync(join(VECTORS, file), "utf8")); const pos = k.tests.find((t) => t.expectedPass === true); const neg = k.tests.find((t) => t.expectedPass === false); for (const [t, want] of [[pos, TRUE_WORD], [neg, FALSE_WORD]]) { const sm = strip(t.signature) + strip(t.message); const got = await call(addr, strip(t.pk) + sm); record( `${label} ACVP tc${t.tcId} expectedPass=${t.expectedPass}`, want, got, got === want, t.reason || "" ); } } // -- 0x0AE6 / 0x0AE7 must NOT be live on mainnet ------------------------- // Docs say these are testnet-only. An empty (codeless) address returns 0x. for (const [label, addr] of [ ["0x0AE6 ML-KEM-768", PRECOMPILES.MLKEM768], ["0x0AE7 Falcon HashToPoint", PRECOMPILES.HASHTOPOINT], ]) { const code = await rpc("eth_getCode", [addr, "latest"]); const out = await call(addr, word(32) + Buffer.from("abc").toString("hex")); const notLive = out === "0x" && code === "0x"; record( `${label} is NOT live on mainnet 2800 (docs say testnet-only)`, "0x (empty: no precompile, no code)", `eth_call=${out} eth_getCode=${code}`, notLive ); } const failed = results.filter((r) => !r.pass); console.log(`\n${results.length - failed.length}/${results.length} checks passed`); if (failed.length) { console.log("FAILED:"); for (const f of failed) console.log(` - ${f.name}`); } process.exit(failed.length ? 1 : 0); } main().catch((e) => { console.error("ERROR:", e.message); process.exit(2); });