aere-node/scripts/verify-live-precompiles.mjs
Aere Network 48416dfe73 Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 10:25:45 +03:00

170 lines
6.9 KiB
JavaScript

#!/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);
});