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

341 lines
12 KiB
JavaScript

// Independent (second-language) reference for the Poseidon2 permutation over BabyBear at width 16,
// component (b) of the PQ STARK-verify precompile 0x0AE8. Mirrors poseidon2_babybear_reference.py
// and the standalone Java Poseidon2BabyBearSelfTest. It serves two purposes: cross-language agreement
// (three independent implementations produce byte-identical output) AND conformance (it reproduces
// real known-answer vectors from the pinned Plonky3 crates; see CONFORMANCE_KATS). 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 3.
//
// HONEST SCOPE (CONFIRMED 2026-07-19): the 141 round constants, internal diagonal, M4, round counts,
// x^7 S-box, and layer order all match p3-baby-bear / p3-poseidon2 0.4.3-succinct (the exact crates
// pinned in aerenew Cargo.lock; lockfile checksums matched). A real known-answer test PASSES.
// MONTGOMERY SUBTLETY (resolved): the internal layer as a canonical map is R^{-1} * (J + diag(D)),
// R^{-1} = 943718400; a prior pass omitted the R^{-1} (textbook state[i]*D[i] + sum) which is
// self-consistent but disagrees with the prover. See the .py header for the full source citation.
//
// Uses BigInt throughout (x^7 and the linear layers exceed 2^53). Run standalone to emit JSON:
// node poseidon2_babybear_reference.mjs
export const P = 2013265921n;
// Montgomery inverse factor: p3-baby-bear stores field elements in Montgomery form with R = 2^32; its
// internal diffusion layer, as a canonical linear map, carries a factor of R^{-1} = (2^32)^{-1} mod p.
export const R_INV = 943718400n; // = (2^32)^{-1} mod p, verified against the pinned library's matrix
export const WIDTH = 16;
export const SBOX_DEGREE = 7n;
export const ROUNDS_F = 8;
export const ROUNDS_P = 13;
export const M4 = [
[2n, 3n, 1n, 1n],
[1n, 2n, 3n, 1n],
[1n, 1n, 2n, 3n],
[3n, 1n, 1n, 2n],
];
export const INTERNAL_DIAG_M1_16 = [
P - 2n, 1n, 2n, 4n, 8n, 16n, 32n, 64n, 128n, 256n, 512n, 1024n, 2048n, 4096n, 8192n, 32768n,
];
// ---- base-field helpers ----
const mod = (a) => ((a % P) + P) % P;
export const add = (a, b) => mod(a + b);
export const mul = (a, b) => mod(a * b);
export function sboxMono(x) {
x = mod(x);
const x2 = mul(x, x);
const x4 = mul(x2, x2);
return mul(x4, mul(x2, x)); // x^7
}
function sboxInvExponent() {
// 7^{-1} mod (p-1) via BigInt extended Euclid (p-1 = 2013265920).
const m = P - 1n;
let [oldR, r] = [SBOX_DEGREE % m, m];
let [oldS, s] = [1n, 0n];
while (r !== 0n) {
const q = oldR / r;
[oldR, r] = [r, oldR - q * r];
[oldS, s] = [s, oldS - q * s];
}
return ((oldS % m) + m) % m;
}
const SBOX_INV_E = sboxInvExponent();
export function powF(base, e) {
let b = mod(base);
let acc = 1n;
let ee = e;
while (ee > 0n) {
if (ee & 1n) acc = mul(acc, b);
b = mul(b, b);
ee >>= 1n;
}
return acc;
}
export const sboxMonoInv = (y) => powF(mod(y), SBOX_INV_E);
// ---- CONFIRMED round constants (from p3-baby-bear/p3-poseidon2 0.4.3-succinct via seed_from_u64(1)) ----
export const RC_EXTERNAL = [
[
1321363468n, 285374923n, 858595076n, 131742120n, 550898981n, 109281027n,
1548327248n, 299186948n, 1198120888n, 1302311359n, 568137078n, 1484856917n,
1301979945n, 725688886n, 941758026n, 323341913n,
],
[
1049323172n, 822409348n, 1406080127n, 1279024384n, 214862539n, 904628921n,
1320747287n, 11578228n, 1036373712n, 1474430466n, 1430509860n, 111174484n,
1124450171n, 85382027n, 679880882n, 243277213n,
],
[
1338495990n, 1523013347n, 1841068573n, 578194469n, 47683837n, 1790441672n,
1628061601n, 1716216090n, 1635810049n, 1115145248n, 1117524270n, 678640014n,
1962751651n, 1367401392n, 11688709n, 1950824358n,
],
[
528649031n, 1937116923n, 1460949223n, 1193074357n, 1221801411n, 1183923117n,
433505619n, 1928933309n, 505759755n, 285671663n, 1047265910n, 909281502n,
1258966486n, 864761693n, 307024510n, 504858517n,
],
[
1467478033n, 1754565867n, 432187324n, 1452390672n, 881974300n, 550050336n,
1447309270n, 939419487n, 1783112406n, 1166910332n, 107514714n, 580516863n,
2003318760n, 854475946n, 934896823n, 994783668n,
],
[
1841107561n, 438269126n, 1550523825n, 913322122n, 600932628n, 583000098n,
1262690949n, 105797869n, 277542016n, 170491952n, 365854467n, 1479645308n,
1457660602n, 1635879552n, 499155053n, 741227047n,
],
[
651389942n, 464828001n, 89696107n, 360044673n, 230330371n, 1773129416n,
1380150763n, 745014723n, 793475694n, 1361274828n, 1443741698n, 51616650n,
731414218n, 1087554954n, 1273943885n, 311581717n,
],
[
702702762n, 1473247301n, 132108357n, 1348260424n, 476775430n, 1438949459n,
2434448n, 1349232398n, 1954471898n, 1762138591n, 1271221795n, 1593266476n,
864488771n, 139147729n, 1053373910n, 422842363n,
],
];
export const RC_INTERNAL = [
402771160n, 320708227n, 1122772462n, 100431997n, 202594011n, 1226485372n,
1088619034n, 64118538n, 109828860n, 724723599n, 1662837151n, 797753907n,
1075635743n,
];
// ---- linear layers ----
function m4Apply(t) {
const out = [];
for (let row = 0; row < 4; row++) {
out.push(mod(M4[row][0] * t[0] + M4[row][1] * t[1] + M4[row][2] * t[2] + M4[row][3] * t[3]));
}
return out;
}
export function externalLayer(state) {
const blocks = [];
for (let b = 0; b < 4; b++) blocks.push(m4Apply(state.slice(4 * b, 4 * b + 4)));
const colSum = [];
for (let j = 0; j < 4; j++) colSum.push(mod(blocks[0][j] + blocks[1][j] + blocks[2][j] + blocks[3][j]));
const out = new Array(WIDTH);
for (let b = 0; b < 4; b++) for (let j = 0; j < 4; j++) out[4 * b + j] = mod(blocks[b][j] + colSum[j]);
return out;
}
export function internalLayer(state) {
// p3-baby-bear DiffusionMatrixBabyBear as a canonical map: M_I = R^{-1} * (J + diag(D)),
// i.e. out[i] = R_INV * (sum + D[i]*state[i]). The R_INV factor is the Montgomery-form artifact.
let s = 0n;
for (const v of state) s += v;
s = mod(s);
return state.map((v, i) => mod(R_INV * mod(s + INTERNAL_DIAG_M1_16[i] * v)));
}
// ---- explicit matrices (for invertibility / inverse round-trip only) ----
export function externalMatrix() {
const m = Array.from({ length: WIDTH }, () => new Array(WIDTH).fill(0n));
for (let bi = 0; bi < 4; bi++)
for (let bj = 0; bj < 4; bj++) {
const coef = bi === bj ? 2n : 1n;
for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) m[4 * bi + r][4 * bj + c] = mod(coef * M4[r][c]);
}
return m;
}
export function internalMatrix() {
// M_I = R^{-1} * (J + diag(D)): off-diagonal = R_INV, diagonal = R_INV*(1 + D[i]).
const m = Array.from({ length: WIDTH }, () => new Array(WIDTH).fill(R_INV));
for (let i = 0; i < WIDTH; i++) m[i][i] = mod(R_INV * (1n + INTERNAL_DIAG_M1_16[i]));
return m;
}
export function matVec(m, v) {
return m.map((row) => {
let acc = 0n;
for (let j = 0; j < WIDTH; j++) acc = mod(acc + mul(row[j], v[j]));
return acc;
});
}
export function matInverse(m) {
const n = WIDTH;
const a = m.map((row, i) => [...row.map(mod), ...Array.from({ length: n }, (_, j) => (j === i ? 1n : 0n))]);
for (let col = 0; col < n; col++) {
let piv = -1;
for (let r = col; r < n; r++) if (mod(a[r][col]) !== 0n) { piv = r; break; }
if (piv < 0) throw new Error("singular matrix");
[a[col], a[piv]] = [a[piv], a[col]];
const invP = powF(a[col][col], P - 2n);
a[col] = a[col].map((x) => mul(x, invP));
for (let r = 0; r < n; r++) {
if (r !== col && mod(a[r][col]) !== 0n) {
const f = a[r][col];
a[r] = a[r].map((x, k) => mod(x - mul(f, a[col][k])));
}
}
}
return a.map((row) => row.slice(n));
}
// ---- the permutation ----
export function permute(state) {
if (state.length !== WIDTH) throw new Error("state must have WIDTH elements");
let s = state.map(mod);
s = externalLayer(s);
const half = ROUNDS_F / 2;
for (let r = 0; r < half; r++) {
s = s.map((v, i) => add(v, RC_EXTERNAL[r][i]));
s = s.map(sboxMono);
s = externalLayer(s);
}
for (let r = 0; r < ROUNDS_P; r++) {
s[0] = add(s[0], RC_INTERNAL[r]);
s[0] = sboxMono(s[0]);
s = internalLayer(s);
}
for (let r = half; r < ROUNDS_F; r++) {
s = s.map((v, i) => add(v, RC_EXTERNAL[r][i]));
s = s.map(sboxMono);
s = externalLayer(s);
}
return s;
}
export function permuteInverse(state) {
let s = state.map(mod);
const extInv = matInverse(externalMatrix());
const intInv = matInverse(internalMatrix());
const half = ROUNDS_F / 2;
for (let r = ROUNDS_F - 1; r >= half; r--) {
s = matVec(extInv, s);
s = s.map(sboxMonoInv);
s = s.map((v, i) => mod(v - RC_EXTERNAL[r][i]));
}
for (let r = ROUNDS_P - 1; r >= 0; r--) {
s = matVec(intInv, s);
s[0] = sboxMonoInv(s[0]);
s[0] = mod(s[0] - RC_INTERNAL[r]);
}
for (let r = half - 1; r >= 0; r--) {
s = matVec(extInv, s);
s = s.map(sboxMonoInv);
s = s.map((v, i) => mod(v - RC_EXTERNAL[r][i]));
}
s = matVec(extInv, s);
return s;
}
// ---- CONFIRMED conformance KATs (from the pinned p3-baby-bear/p3-poseidon2 0.4.3-succinct crates) ----
export const CONFORMANCE_KATS = [
{
name: "zeros",
in: new Array(16).fill(0n),
out: [1787823396n, 953829438n, 89382455n, 347481625n, 1754527224n, 916217775n, 1056029082n,
410644796n, 1169123478n, 1854704276n, 1195829987n, 1485264906n, 1824644035n, 1948268315n,
847945433n, 190591038n],
},
{
name: "iota",
in: Array.from({ length: 16 }, (_, i) => BigInt(i)),
out: [157639285n, 1851003038n, 1852457045n, 1920360618n, 779990819n, 1080011039n, 585017685n,
1093051731n, 249426030n, 967262243n, 623744062n, 280332881n, 1995600430n, 1751988435n,
317724737n, 1895035071n],
},
{
name: "testvec",
in: [894848333n, 1437655012n, 1200606629n, 1690012884n, 71131202n, 1749206695n, 1717947831n,
120589055n, 19776022n, 42382981n, 1831865506n, 724844064n, 171220207n, 1299207443n,
227047920n, 1783754913n],
out: [512585766n, 975869435n, 1921378527n, 1238606951n, 899635794n, 132650430n, 1426417547n,
1734425242n, 57415409n, 67173027n, 1535042492n, 1318033394n, 1070659233n, 17258943n,
856719028n, 1500534995n],
},
];
export function conformanceKats() {
let passed = 0;
for (const kat of CONFORMANCE_KATS) {
const out = permute(kat.in.slice());
if (out.every((v, i) => mod(v) === mod(kat.out[i]))) passed++;
}
return { passed, total: CONFORMANCE_KATS.length };
}
// ---- shared cross-language vector set ----
function inputStates() {
const states = [
new Array(WIDTH).fill(0n),
Array.from({ length: WIDTH }, (_, i) => BigInt(i)),
new Array(WIDTH).fill(P - 1n),
Array.from({ length: WIDTH }, (_, i) => (BigInt(i) * 2654435761n) % P),
[11n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 1n],
];
// BigInt LCG (1103515245*x overflows JS Number's 2^53; BigInt keeps it exact and matching Py/Java)
let x = 123456789n;
for (let k = 0; k < 8; k++) {
const st = [];
for (let i = 0; i < WIDTH; i++) {
x = (1103515245n * x + 12345n) % 2147483648n;
st.push(x % P);
}
states.push(st);
}
return states;
}
const nlist = (a) => a.map((v) => Number(v));
export function sharedVectors() {
const rcExtFlat = [];
for (let r = 0; r < ROUNDS_F; r++) for (let i = 0; i < WIDTH; i++) rcExtFlat.push(Number(RC_EXTERNAL[r][i]));
const states = inputStates();
return {
constants: {
P: Number(P),
rInv: Number(R_INV),
width: WIDTH,
sboxDegree: Number(SBOX_DEGREE),
roundsF: ROUNDS_F,
roundsP: ROUNDS_P,
internalDiagM1: nlist(INTERNAL_DIAG_M1_16),
m4: [2, 3, 1, 1, 1, 2, 3, 1, 1, 1, 2, 3, 3, 1, 1, 2],
rcExternalFlat: rcExtFlat,
rcInternal: nlist(RC_INTERNAL),
},
permute: states.map((st) => {
const out = permute(st);
const back = permuteInverse(out);
const rt = back.every((v, i) => mod(v) === mod(st[i]));
return { in: nlist(st), out: nlist(out), roundtrip: rt };
}),
conformanceKats: CONFORMANCE_KATS.map((k) => {
const out = permute(k.in.slice());
return { name: k.name, in: nlist(k.in), out: nlist(out), match: out.every((v, i) => mod(v) === mod(k.out[i])) };
}),
};
}
import { pathToFileURL } from "node:url";
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.stdout.write(JSON.stringify(sharedVectors()));
}