aere-research/pq-stark/air_quotient_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

203 lines
7.0 KiB
JavaScript

// Independent Node reference for the GENERIC AIR constraint / quotient-consistency check (component
// (e)) that a p3-uni-stark STARK verifier performs, for the PQ STARK-verify precompile 0x0AE8. Mirrors
// air_quotient_reference.py and AirQuotientSelfTest.java. See docs/AERE-STARK-VERIFIER-PORT-SPEC.md
// section 6, spec-stark-air.md.
//
// HONEST SCOPE: this implements the GENERIC quotient-consistency MECHANISM (p3-uni-stark verifier.rs
// lines 90-141) for a SUPPLIED AIR constraint evaluator: fold the AIR's constraints with alpha (Horner),
// reconstruct quotient(zeta) from the chunk openings + split-domain zps, compute Z_H(zeta) and the
// first/last/transition selectors, and check folded_constraints(zeta) == Z_H(zeta) * quotient(zeta),
// over F_{p^4}. The SP1 recursion AIR (its specific constraint set + vkey binding) is NOT ported, so the
// top-level 0x0AE8 STAYS FAIL-CLOSED for real SP1 proofs. Ground truth: real p3-uni-stark 0.4.3-succinct
// proofs of two KNOWN example AIRs (Fibonacci from tests/fib_air.rs; a degree-3 multiply AIR matching
// tests/mul_air.rs), accepted by the library and re-checked here (accept + reject tampered).
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import {
P,
GENERATOR,
mul,
inv,
powF,
twoAdicGenerator,
extFromBase,
extAdd,
extSub,
extMul,
extInv,
} from "./babybear_field_reference.mjs";
const HERE = dirname(fileURLToPath(import.meta.url));
const GROUND_TRUTH = join(HERE, "air_quotient_ground_truth.json");
const ONE = extFromBase(1n);
const B = (x) => BigInt(x);
const extEq = (a, b) => a.every((_, i) => ((a[i] % P) + P) % P === ((b[i] % P) + P) % P);
const extDiv = (a, b) => extMul(a, extInv(b));
function extExpPow2(x, logN) {
let r = x.slice();
for (let k = 0; k < logN; k++) r = extMul(r, r);
return r;
}
// Z_D(point) for a coset of size 2^logN and (base-field) shift: (point*shift^-1)^(2^logN) - 1.
function zpAtPoint(logN, shiftBase, pointExt) {
const shiftInv = extFromBase(inv(shiftBase));
return extSub(extExpPow2(extMul(pointExt, shiftInv), logN), ONE);
}
// domain.rs selectors_at_point for the trace domain (shift=1, logN=degreeBits).
function selectorsAtPoint(degreeBits, zeta) {
const g = twoAdicGenerator(B(degreeBits));
const gInv = extFromBase(inv(g));
const unshifted = zeta.slice();
const zH = extSub(extExpPow2(unshifted, degreeBits), ONE);
return {
isFirst: extDiv(zH, extSub(unshifted, ONE)),
isLast: extDiv(zH, extSub(unshifted, gInv)),
isTransition: extSub(unshifted, gInv),
invZeroifier: extInv(zH),
};
}
const monomial = (e) => {
const m = [0n, 0n, 0n, 0n];
m[e] = 1n;
return m;
};
function reconstructQuotient(degreeBits, chunks, zeta) {
const quotientDegree = chunks.length;
const logQuotientDegree = Math.round(Math.log2(quotientDegree));
const logNq = degreeBits + logQuotientDegree;
const genQ = twoAdicGenerator(B(logNq));
const shifts = [];
for (let i = 0; i < quotientDegree; i++) shifts.push(mul(GENERATOR, powF(genQ, B(i))));
const zps = [];
for (let i = 0; i < quotientDegree; i++) {
let acc = ONE.slice();
const firstPointI = extFromBase(shifts[i]);
for (let j = 0; j < quotientDegree; j++) {
if (j === i) continue;
const num = zpAtPoint(degreeBits, shifts[j], zeta);
const den = zpAtPoint(degreeBits, shifts[j], firstPointI);
acc = extMul(acc, extMul(num, extInv(den)));
}
zps.push(acc);
}
let quotient = [0n, 0n, 0n, 0n];
for (let chI = 0; chI < chunks.length; chI++) {
for (let eI = 0; eI < chunks[chI].length; eI++) {
quotient = extAdd(quotient, extMul(extMul(zps[chI], monomial(eI)), chunks[chI][eI]));
}
}
return quotient;
}
// ---- example AIR constraint evaluators (transcribed from their eval()) ----
function horner(constraints, alpha) {
let acc = [0n, 0n, 0n, 0n];
for (const c of constraints) acc = extAdd(extMul(acc, alpha), c);
return acc;
}
function foldFibonacci(tl, tn, pis, sel, alpha) {
const a = extFromBase(pis[0]);
const b = extFromBase(pis[1]);
const x = extFromBase(pis[2]);
const [left, right] = [tl[0], tl[1]];
const [nleft, nright] = [tn[0], tn[1]];
const c1 = extMul(sel.isFirst, extSub(left, a));
const c2 = extMul(sel.isFirst, extSub(right, b));
const c3 = extMul(sel.isTransition, extSub(right, nleft));
const c4 = extMul(sel.isTransition, extSub(extAdd(left, right), nright));
const c5 = extMul(sel.isLast, extSub(right, x));
return horner([c1, c2, c3, c4, c5], alpha);
}
function foldMulDeg3(tl, tn, pis, sel, alpha) {
const [a, b, c] = [tl[0], tl[1], tl[2]];
const nextA = tn[0];
const c1 = extSub(extMul(extMul(a, a), b), c);
const c2 = extMul(sel.isFirst, extSub(extAdd(extMul(a, a), ONE), b));
const c3 = extMul(sel.isTransition, extSub(extAdd(a, ONE), nextA));
return horner([c1, c2, c3], alpha);
}
const AIR_FOLDERS = { fibonacci: foldFibonacci, mul_deg3: foldMulDeg3 };
// ---- the generic quotient-consistency check ----
function toBigCase(cas) {
return {
name: cas.name,
air: cas.air,
degreeBits: cas.degree_bits,
quotientDegree: cas.quotient_degree,
alpha: cas.alpha.map(B),
zeta: cas.zeta.map(B),
pis: cas.public_values.map(B),
tl: cas.trace_local.map((v) => v.map(B)),
tn: cas.trace_next.map((v) => v.map(B)),
chunks: cas.quotient_chunks.map((ch) => ch.map((v) => v.map(B))),
libraryAccept: cas.library_accept,
};
}
function derive(c) {
const sel = selectorsAtPoint(c.degreeBits, c.zeta);
const quotient = reconstructQuotient(c.degreeBits, c.chunks, c.zeta);
const folded = AIR_FOLDERS[c.air](c.tl, c.tn, c.pis, sel, c.alpha);
return { quotient, folded, invZeroifier: sel.invZeroifier };
}
function checkIdentity(c) {
const d = derive(c);
return extEq(extMul(d.folded, d.invZeroifier), d.quotient);
}
function checkCase(c, tamper) {
const cc = {
...c,
alpha: c.alpha.slice(),
tl: c.tl.map((v) => v.slice()),
chunks: c.chunks.map((ch) => ch.map((v) => v.slice())),
};
if (tamper === "trace") cc.tl[0][0] = (cc.tl[0][0] + 1n) % P;
else if (tamper === "quotient") cc.chunks[0][0][0] = (cc.chunks[0][0][0] + 1n) % P;
else if (tamper === "alpha") cc.alpha[0] = (cc.alpha[0] + 1n) % P;
return checkIdentity(cc);
}
const asNum = (arr) => arr.map((x) => Number(((x % P) + P) % P));
export function sharedVectors() {
const gt = JSON.parse(readFileSync(GROUND_TRUTH, "utf8"));
const cases = gt.cases.map((raw) => {
const c = toBigCase(raw);
const d = derive(c);
return {
name: c.name,
air: c.air,
degreeBits: c.degreeBits,
quotientDegree: c.quotientDegree,
quotient: asNum(d.quotient),
folded: asNum(d.folded),
invZeroifier: asNum(d.invZeroifier),
accept: checkCase(c, null),
rejectTrace: !checkCase(c, "trace"),
rejectQuotient: !checkCase(c, "quotient"),
rejectAlpha: !checkCase(c, "alpha"),
};
});
return { valGenerator: Number(GENERATOR), cases };
}
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1] === fileURLToPath(import.meta.url)) {
process.stdout.write(JSON.stringify(sharedVectors()));
}