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.
144 lines
5.8 KiB
JavaScript
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()));
|
|
}
|