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