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.
170 lines
6.9 KiB
JavaScript
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);
|
|
});
|