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

156 lines
4.8 KiB
JavaScript

// Independent (second-language) reference for the BabyBear field F_p and its degree-4 extension
// F_{p^4}, the FOUNDATION layer (component (a)) of the PQ STARK-verify precompile 0x0AE8. Mirrors
// babybear_field_reference.py and the Java BabyBearFieldSelfTest. Its ONLY purpose is
// cross-language agreement: three independent implementations of the same public field definition
// should produce byte-identical output on a shared vector set. It verifies NOTHING about a real
// proof and the top-level 0x0AE8 verifier stays fail-closed. See
// docs/AERE-STARK-VERIFIER-PORT-SPEC.md section 2.
//
// Run standalone to emit the shared vector set as JSON on stdout:
// node babybear_field_reference.mjs
//
// Uses BigInt throughout so the extension Fermat inverse exponent (p^4 - 2, ~124 bits) is exact.
// JSON is emitted with Number values (every field element fits in a JS double, < 2^31).
export const P = 2013265921n; // 2^31 - 2^27 + 1 = 15 * 2^27 + 1
export const GENERATOR = 31n; // [VERIFY] Plonky3 generator; order proven = P-1 in the harness
export const TWO_ADICITY = 27n;
export const W = 11n; // [VERIFY] Plonky3 BabyBear quartic non-residue; irreducibility proven
// ---- base field F_p ----
const mod = (a) => ((a % P) + P) % P;
export const add = (a, b) => mod(a + b);
export const sub = (a, b) => mod(a - b);
export const neg = (a) => mod(-a);
export const mul = (a, b) => mod(a * b);
export function powF(base, e) {
if (e < 0n) throw new Error("use inv for negative exponents");
let b = mod(base);
let acc = 1n;
while (e > 0n) {
if (e & 1n) acc = mod(acc * b);
b = mod(b * b);
e >>= 1n;
}
return acc;
}
export function inv(a) {
const r = mod(a);
if (r === 0n) return 0n; // inv(0) := 0, matches the Java precompile convention
return powF(r, P - 2n);
}
export function twoAdicGenerator(bits) {
if (bits < 0n || bits > TWO_ADICITY) throw new Error("bits out of range [0,27]");
return powF(GENERATOR, (P - 1n) >> bits);
}
// ---- degree-4 extension F_{p^4} = F_p[x]/(x^4 - W), elements as [c0,c1,c2,c3] BigInt ----
export const extFromBase = (a) => [mod(a), 0n, 0n, 0n];
export const extAdd = (a, b) => a.map((_, i) => mod(a[i] + b[i]));
export const extSub = (a, b) => a.map((_, i) => mod(a[i] - b[i]));
export const extNeg = (a) => a.map((v) => mod(-v));
export function extMul(a, b) {
const t = [0n, 0n, 0n, 0n, 0n, 0n, 0n];
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
t[i + j] = mod(t[i + j] + a[i] * b[j]);
}
}
return [mod(t[0] + W * t[4]), mod(t[1] + W * t[5]), mod(t[2] + W * t[6]), mod(t[3])];
}
export function extPow(a, e) {
if (e < 0n) throw new Error("use extInv for negative exponents");
let base = a.map((v) => mod(v));
let acc = [1n, 0n, 0n, 0n];
while (e > 0n) {
if (e & 1n) acc = extMul(acc, base);
base = extMul(base, base);
e >>= 1n;
}
return acc;
}
// Frobenius pi(a) = a^p; Frobenius^4 == id, fixes the base field. Generic power = independent ref.
export const extFrobenius = (a) => extPow(a, P);
export function extInv(a) {
if (a.every((v) => mod(v) === 0n)) return [0n, 0n, 0n, 0n];
return extPow(a, P ** 4n - 2n);
}
// ---- shared cross-language vector set (must match the Python schema) ----
const BASE_UNARY = [0n, 1n, 2n, 31n, 1000000n, 123456789n, P - 1n];
const BASE_BINARY = [
[2n, 3n],
[P - 1n, 1n],
[1000000n, 999n],
[123456789n, 987654321n],
[0n, 5n],
];
const EXT_UNARY = [
[1n, 0n, 0n, 0n],
[0n, 1n, 0n, 0n],
[2n, 3n, 5n, 7n],
[P - 1n, P - 1n, P - 1n, P - 1n],
[11n, 0n, 0n, 0n],
[123n, 456n, 789n, 1011n],
];
const EXT_BINARY = [
[[1n, 2n, 3n, 4n], [5n, 6n, 7n, 8n]],
[[0n, 1n, 0n, 0n], [0n, 1n, 0n, 0n]],
[[2n, 3n, 5n, 7n], [11n, 0n, 0n, 0n]],
];
const POW_E = 12345n;
const n = (x) => Number(x); // every element < 2^31, safe as a JS Number
const arr = (a) => a.map(n);
export function sharedVectors() {
return {
constants: {
P: n(P),
generator: n(GENERATOR),
twoAdicity: n(TWO_ADICITY),
twoAdicGen27: n(twoAdicGenerator(27n)),
W: n(W),
powE: n(POW_E),
},
base_unary: BASE_UNARY.map((a) => ({
a: n(a),
neg: n(neg(a)),
inv: n(inv(a)),
pow: n(powF(a, POW_E)),
})),
base_binary: BASE_BINARY.map(([a, b]) => ({
a: n(a),
b: n(b),
add: n(add(a, b)),
sub: n(sub(a, b)),
mul: n(mul(a, b)),
})),
ext_unary: EXT_UNARY.map((a) => ({
a: arr(a),
neg: arr(extNeg(a)),
inv: arr(extInv(a)),
frob: arr(extFrobenius(a)),
})),
ext_binary: EXT_BINARY.map(([a, b]) => ({
a: arr(a),
b: arr(b),
add: arr(extAdd(a, b)),
sub: arr(extSub(a, b)),
mul: arr(extMul(a, b)),
})),
};
}
import { pathToFileURL } from "node:url";
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.stdout.write(JSON.stringify(sharedVectors()));
}