aere-research/pq-stark/fri_verify_reference.mjs
Aere Network 4a0b48588c 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 01:02:30 +03:00

144 lines
5.8 KiB
JavaScript

// Independent (second-language) reference for the FRI VERIFIER (component (d) of the PQ STARK-verify
// precompile 0x0AE8). Mirrors fri_verify_reference.py and the standalone Java FriVerifySelfTest. It
// verifies real FRI proofs emitted by the pinned Plonky3 p3-fri 0.4.3-succinct prover (the ground-truth
// extractor, pq-stark/fri-extractor), and it verifies NOTHING trustless: the transcript that derives
// the betas/query-indices is component (f) and remains un-ported, so the top-level 0x0AE8 stays
// fail-closed. See docs/AERE-STARK-VERIFIER-PORT-SPEC.md section 5 and spec-fri-babybear.md.
//
// It reuses the CONFIRMED sub-components: F_{p^4} arithmetic (component (a),
// babybear_field_reference.mjs) and the FieldMerkleTreeMmcs verify_batch (component (c),
// mmcs_babybear_reference.mjs, whose leaf hash/compress is the CONFIRMED Poseidon2). BigInt throughout.
//
// node fri_verify_reference.mjs # emits the shared cross-language vector set as JSON
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import {
P, twoAdicGenerator, powF, extFromBase, extAdd, extSub, extMul, extInv,
} from "./babybear_field_reference.mjs";
import { permute as p2permute, verifyBatch } from "./mmcs_babybear_reference.mjs";
const HERE = dirname(fileURLToPath(import.meta.url));
const GROUND_TRUTH = join(HERE, "fri_ground_truth.json");
const B = (x) => BigInt(x);
const asBig = (a) => a.map(B);
const asNum = (a) => a.map(Number);
function reverseBitsLen(x, bitLen) {
let r = 0;
for (let i = 0; i < bitLen; i++) r = (r << 1) | ((x >> i) & 1);
return r;
}
// verify one query: reproduce p3-fri verifier::verify_query. Returns folded eval (BigInt[4]) or null
// if a commit-phase MMCS opening fails. betas/index/roFull are supplied inputs (transcript = comp (f)).
function verifyQuery(logBlowup, logMaxHeight, commits, betas, index, roFull, layers) {
let folded = [0n, 0n, 0n, 0n];
const g = twoAdicGenerator(B(logMaxHeight));
let x = extFromBase(powF(g, B(reverseBitsLen(index, logMaxHeight))));
const gen1 = extFromBase(twoAdicGenerator(1n)); // order-2 root = -1, embedded
let idx = index;
const numLayers = logMaxHeight - logBlowup;
for (let layer = 0; layer < numLayers; layer++) {
const lfh = logMaxHeight - 1 - layer;
folded = extAdd(folded, roFull[lfh + 1]);
const isib = idx ^ 1;
const ipair = idx >> 1;
const evals = [folded.slice(), folded.slice()];
evals[isib % 2] = asBig(layers[layer].sibling_value);
const row = evals[0].concat(evals[1]); // 8 base coords (ExtensionMmcs flatten)
const height = 1 << lfh;
if (!verifyBatch(commits[layer], [[8, height]], ipair, [row], layers[layer].opening_proof)) {
return null; // commit-phase MMCS opening failed -> reject
}
const xs = isib % 2 === 1 ? [x, extMul(x, gen1)] : [extMul(x, gen1), x];
const beta = asBig(betas[layer]);
const num = extMul(extSub(beta, xs[0]), extSub(evals[1], evals[0]));
const den = extSub(xs[1], xs[0]);
folded = extAdd(evals[0], extMul(num, extInv(den)));
idx = ipair;
x = extMul(x, x);
}
return folded;
}
function buildRoFull(logMaxHeight, roTop) {
const ro = [];
for (let i = 0; i < logMaxHeight + 2; i++) ro.push([0n, 0n, 0n, 0n]);
ro[logMaxHeight] = asBig(roTop);
return ro;
}
const eqArr = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
function verifyCase(cas, tamper) {
const logBlowup = cas.log_blowup;
const logMaxHeight = cas.log_max_height;
const commits = cas.commit_phase_commits;
let finalPoly = asBig(cas.final_poly);
let betas = cas.betas.map((b) => b.slice());
if (tamper === "final") finalPoly = [(finalPoly[0] + 1n) % P, finalPoly[1], finalPoly[2], finalPoly[3]];
if (tamper === "beta") { betas = betas.map((b) => b.slice()); betas[0] = betas[0].slice(); betas[0][0] = Number((B(betas[0][0]) + 1n) % P); }
for (const q of cas.queries) {
const layers = q.layers.map((s) => ({
sibling_value: s.sibling_value.slice(),
opening_proof: s.opening_proof.map((d) => d.slice()),
}));
if (tamper === "sibling") layers[0].sibling_value[0] = Number((B(layers[0].sibling_value[0]) + 1n) % P);
if (tamper === "proof") layers[0].opening_proof[0][0] = Number((B(layers[0].opening_proof[0][0]) + 1n) % P);
const roFull = buildRoFull(logMaxHeight, q.ro_top);
const folded = verifyQuery(logBlowup, logMaxHeight, commits, betas, q.index, roFull, layers);
if (folded === null) return false; // MMCS opening failed
if (!eqArr(folded, finalPoly)) return false; // FinalPolyMismatch
}
return true;
}
function caseFolded(cas) {
const commits = cas.commit_phase_commits;
const betas = cas.betas;
const out = [];
for (const q of cas.queries) {
const roFull = buildRoFull(cas.log_max_height, q.ro_top);
const folded = verifyQuery(cas.log_blowup, cas.log_max_height, commits, betas, q.index, roFull, q.layers);
out.push(asNum(folded));
}
return out;
}
export function sharedVectors() {
const gt = JSON.parse(readFileSync(GROUND_TRUTH, "utf8"));
const cases = gt.cases.map((cas) => ({
name: cas.name,
logBlowup: cas.log_blowup,
logMaxHeight: cas.log_max_height,
numQueries: cas.num_queries,
folded: caseFolded(cas),
finalPoly: cas.final_poly.slice(),
accept: verifyCase(cas, null),
rejectSibling: !verifyCase(cas, "sibling"),
rejectProof: !verifyCase(cas, "proof"),
rejectBeta: !verifyCase(cas, "beta"),
rejectFinal: !verifyCase(cas, "final"),
}));
return {
permZeros: asNum(p2permute(Array(16).fill(0n))),
twoAdicGenerators: Array.from({ length: 28 }, (_, b) => Number(twoAdicGenerator(B(b)))),
cases,
};
}
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1] === fileURLToPath(import.meta.url)) {
process.stdout.write(JSON.stringify(sharedVectors()));
}