// Standalone (no-Besu-classpath) reference + self-test for the FRI VERIFIER, component (d) of the PQ // STARK-verify precompile 0x0AE8 (direct SP1/Plonky3 inner FRI/STARK verify). Mirrors // fri_verify_reference.py and fri_verify_reference.mjs. 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). // // HONEST SCOPE: this implements the FOLD + OPENING relations (verify_query), taking the transcript // derived betas / query indices / reduced openings as INPUTS. The transcript BINDING that derives them // in-circuit is component (f) (the duplex-sponge challenger) and remains un-ported, so even with this // passing KAT the top-level 0x0AE8 stays FAIL-CLOSED. The Poseidon2 permutation (component (b)) and the // FieldMerkleTreeMmcs verify_batch (component (c)) below are copied verbatim from the CONFIRMED // MmcsBabyBearSelfTest; the F_{p^4} arithmetic (component (a)) matches the precompile's BabyBearExt4. // // javac -d out FriVerifySelfTest.java // java -cp out FriVerifySelfTest [ground_truth.json] # run the conformance self-test // java -cp out FriVerifySelfTest --emit [ground_truth.json] # emit the shared vector set (JSON) import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.HashMap; public final class FriVerifySelfTest { static final long P = 2013265921L; // BabyBear 2^31 - 2^27 + 1 static final long R_INV = 943718400L; // (2^32)^{-1} mod p (Poseidon2 internal-layer Montgomery factor) static final long GENERATOR = 31L; // BabyBear multiplicative generator (order p-1) static final long EXT_W = 11L; // F_{p^4} = F_p[x]/(x^4 - 11) non-residue static final int WIDTH = 16; static final int RATE = 8; static final int OUT = 8; static final int DIGEST_ELEMS = 8; static final long[][] M4 = {{2, 3, 1, 1}, {1, 2, 3, 1}, {1, 1, 2, 3}, {3, 1, 1, 2}}; static final long[] INTERNAL_DIAG_M1_16 = { P - 2, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 32768 }; static final long[][] RC_EXTERNAL = { {1321363468L, 285374923L, 858595076L, 131742120L, 550898981L, 109281027L, 1548327248L, 299186948L, 1198120888L, 1302311359L, 568137078L, 1484856917L, 1301979945L, 725688886L, 941758026L, 323341913L}, {1049323172L, 822409348L, 1406080127L, 1279024384L, 214862539L, 904628921L, 1320747287L, 11578228L, 1036373712L, 1474430466L, 1430509860L, 111174484L, 1124450171L, 85382027L, 679880882L, 243277213L}, {1338495990L, 1523013347L, 1841068573L, 578194469L, 47683837L, 1790441672L, 1628061601L, 1716216090L, 1635810049L, 1115145248L, 1117524270L, 678640014L, 1962751651L, 1367401392L, 11688709L, 1950824358L}, {528649031L, 1937116923L, 1460949223L, 1193074357L, 1221801411L, 1183923117L, 433505619L, 1928933309L, 505759755L, 285671663L, 1047265910L, 909281502L, 1258966486L, 864761693L, 307024510L, 504858517L}, {1467478033L, 1754565867L, 432187324L, 1452390672L, 881974300L, 550050336L, 1447309270L, 939419487L, 1783112406L, 1166910332L, 107514714L, 580516863L, 2003318760L, 854475946L, 934896823L, 994783668L}, {1841107561L, 438269126L, 1550523825L, 913322122L, 600932628L, 583000098L, 1262690949L, 105797869L, 277542016L, 170491952L, 365854467L, 1479645308L, 1457660602L, 1635879552L, 499155053L, 741227047L}, {651389942L, 464828001L, 89696107L, 360044673L, 230330371L, 1773129416L, 1380150763L, 745014723L, 793475694L, 1361274828L, 1443741698L, 51616650L, 731414218L, 1087554954L, 1273943885L, 311581717L}, {702702762L, 1473247301L, 132108357L, 1348260424L, 476775430L, 1438949459L, 2434448L, 1349232398L, 1954471898L, 1762138591L, 1271221795L, 1593266476L, 864488771L, 139147729L, 1053373910L, 422842363L}, }; static final long[] RC_INTERNAL = { 402771160L, 320708227L, 1122772462L, 100431997L, 202594011L, 1226485372L, 1088619034L, 64118538L, 109828860L, 724723599L, 1662837151L, 797753907L, 1075635743L, }; static final int ROUNDS_F = 8; static final int ROUNDS_P = 13; // ================= base field F_p ================= static long reduce(final long a) { long m = a % P; if (m < 0) m += P; return m; } static long add(final long a, final long b) { long s = a + b; if (s >= P) s -= P; return s; } static long sub(final long a, final long b) { long s = a - b; if (s < 0) s += P; return s; } static long mul(final long a, final long b) { return (reduce(a) * reduce(b)) % P; } static long powBase(final long base, long e) { long b = reduce(base); long acc = 1L; while (e > 0) { if ((e & 1L) == 1L) acc = mul(acc, b); b = mul(b, b); e >>= 1; } return acc; } static long twoAdicGenerator(final int bits) { return powBase(GENERATOR, (P - 1) >> bits); } // ================= F_{p^4} = F_p[x]/(x^4 - 11), elements long[4] = [c0,c1,c2,c3] ================= static long[] extFromBase(final long a) { return new long[]{reduce(a), 0, 0, 0}; } static long[] extAdd(final long[] a, final long[] b) { return new long[]{add(a[0], b[0]), add(a[1], b[1]), add(a[2], b[2]), add(a[3], b[3])}; } static long[] extSub(final long[] a, final long[] b) { return new long[]{sub(a[0], b[0]), sub(a[1], b[1]), sub(a[2], b[2]), sub(a[3], b[3])}; } static long[] extMul(final long[] a, final long[] b) { final long[] t = new long[7]; for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) t[i + j] = add(t[i + j], mul(a[i], b[j])); return new long[]{add(t[0], mul(EXT_W, t[4])), add(t[1], mul(EXT_W, t[5])), add(t[2], mul(EXT_W, t[6])), t[3]}; } static long[] extPow(final long[] a, final BigInteger e) { long[] base = a.clone(); long[] acc = new long[]{1, 0, 0, 0}; BigInteger ee = e; while (ee.signum() > 0) { if (ee.testBit(0)) acc = extMul(acc, base); base = extMul(base, base); ee = ee.shiftRight(1); } return acc; } static long[] extInv(final long[] a) { if (a[0] == 0 && a[1] == 0 && a[2] == 0 && a[3] == 0) return new long[]{0, 0, 0, 0}; final BigInteger p = BigInteger.valueOf(P); return extPow(a, p.pow(4).subtract(BigInteger.TWO)); } static boolean extEq(final long[] a, final long[] b) { for (int i = 0; i < 4; i++) if (reduce(a[i]) != reduce(b[i])) return false; return true; } // ================= Poseidon2 (component (b), CONFIRMED) ================= static long sbox(final long x) { final long x2 = mul(x, x); final long x4 = mul(x2, x2); return mul(x4, mul(x2, x)); } static long[] m4Apply(final long a, final long b, final long c, final long d) { final long[] out = new long[4]; for (int row = 0; row < 4; row++) out[row] = reduce(M4[row][0] * a + M4[row][1] * b + M4[row][2] * c + M4[row][3] * d); return out; } static void externalLayer(final long[] s) { final long[][] blk = new long[4][]; for (int b = 0; b < 4; b++) blk[b] = m4Apply(s[4 * b], s[4 * b + 1], s[4 * b + 2], s[4 * b + 3]); final long[] colSum = new long[4]; for (int j = 0; j < 4; j++) colSum[j] = reduce(blk[0][j] + blk[1][j] + blk[2][j] + blk[3][j]); for (int b = 0; b < 4; b++) for (int j = 0; j < 4; j++) s[4 * b + j] = add(blk[b][j], colSum[j]); } static void internalLayer(final long[] s) { long sum = 0; for (final long v : s) sum = add(sum, v); for (int i = 0; i < WIDTH; i++) s[i] = mul(R_INV, add(sum, mul(s[i], INTERNAL_DIAG_M1_16[i]))); } static long[] permute(final long[] state16) { final long[] s = new long[WIDTH]; for (int i = 0; i < WIDTH; i++) s[i] = reduce(state16[i]); externalLayer(s); final int half = ROUNDS_F / 2; for (int r = 0; r < half; r++) { for (int i = 0; i < WIDTH; i++) s[i] = add(s[i], RC_EXTERNAL[r][i]); for (int i = 0; i < WIDTH; i++) s[i] = sbox(s[i]); externalLayer(s); } for (int r = 0; r < ROUNDS_P; r++) { s[0] = add(s[0], RC_INTERNAL[r]); s[0] = sbox(s[0]); internalLayer(s); } for (int r = half; r < ROUNDS_F; r++) { for (int i = 0; i < WIDTH; i++) s[i] = add(s[i], RC_EXTERNAL[r][i]); for (int i = 0; i < WIDTH; i++) s[i] = sbox(s[i]); externalLayer(s); } return s; } // ================= MMCS verify_batch (component (c), CONFIRMED) ================= static long[] hashIter(final long[] elems) { final long[] state = new long[WIDTH]; int i = 0; while (i < elems.length) { final int end = Math.min(i + RATE, elems.length); for (int j = i; j < end; j++) state[j - i] = reduce(elems[j]); final long[] permuted = permute(state); System.arraycopy(permuted, 0, state, 0, WIDTH); i += RATE; } final long[] out = new long[OUT]; System.arraycopy(state, 0, out, 0, OUT); return out; } static long[] compress2to1(final long[] left8, final long[] right8) { final long[] pre = new long[WIDTH]; for (int i = 0; i < DIGEST_ELEMS; i++) pre[i] = reduce(left8[i]); for (int i = 0; i < DIGEST_ELEMS; i++) pre[DIGEST_ELEMS + i] = reduce(right8[i]); final long[] post = permute(pre); final long[] out = new long[DIGEST_ELEMS]; System.arraycopy(post, 0, out, 0, DIGEST_ELEMS); return out; } static int nextPow2(final int n) { if (n <= 1) return 1; return Integer.highestOneBit(n - 1) << 1; } static boolean eqDigest(final long[] a, final long[] b) { if (a.length != b.length) return false; for (int i = 0; i < a.length; i++) if (reduce(a[i]) != reduce(b[i])) return false; return true; } static long[] concatOpened(final Integer[] order, final int[] heights, final int[] ptr, final int padded, final long[][] opened) { final List buf = new ArrayList<>(); while (ptr[0] < heights.length && nextPow2(heights[ptr[0]]) == padded) { for (final long v : opened[order[ptr[0]]]) buf.add(v); ptr[0]++; } final long[] out = new long[buf.size()]; for (int i = 0; i < out.length; i++) out[i] = buf.get(i); return out; } static boolean verifyBatch(final long[] commit, final int[][] dims, final int index, final long[][] opened, final long[][] pf) { final int n = dims.length; if (n == 0) return false; final Integer[] order = new Integer[n]; for (int i = 0; i < n; i++) order[i] = i; java.util.Arrays.sort(order, (a, b) -> dims[b][1] != dims[a][1] ? dims[b][1] - dims[a][1] : a - b); final int[] heights = new int[n]; for (int i = 0; i < n; i++) heights[i] = dims[order[i]][1]; final int[] ptr = {0}; int currHeightPadded = nextPow2(heights[0]); long[] root = hashIter(concatOpened(order, heights, ptr, currHeightPadded, opened)); int idx = index; for (final long[] sibling : pf) { final long[] left; final long[] right; if ((idx & 1) == 0) { left = root; right = sibling; } else { left = sibling; right = root; } root = compress2to1(left, right); idx >>= 1; currHeightPadded >>= 1; if (ptr[0] < n && nextPow2(heights[ptr[0]]) == currHeightPadded) { root = compress2to1(root, hashIter(concatOpened(order, heights, ptr, currHeightPadded, opened))); } } return eqDigest(root, commit); } // ================= FRI verify_query (component (d)) ================= static int reverseBitsLen(final int x, final int bitLen) { int r = 0; for (int i = 0; i < bitLen; i++) r = (r << 1) | ((x >> i) & 1); return r; } static final class Layer { long[] sibling; long[][] openingProof; } /** Reproduce p3-fri verifier::verify_query for one query. Returns folded eval (F_{p^4}) or null if a * commit-phase MMCS opening fails. */ static long[] verifyQuery(final int logBlowup, final int logMaxHeight, final long[][] commits, final long[][] betas, final int index, final long[][] roFull, final Layer[] layers) { long[] folded = new long[]{0, 0, 0, 0}; final long g = twoAdicGenerator(logMaxHeight); long[] x = extFromBase(powBase(g, reverseBitsLen(index, logMaxHeight))); final long[] gen1 = extFromBase(twoAdicGenerator(1)); // order-2 root = -1, embedded int idx = index; final int numLayers = logMaxHeight - logBlowup; for (int layer = 0; layer < numLayers; layer++) { final int lfh = logMaxHeight - 1 - layer; folded = extAdd(folded, roFull[lfh + 1]); final int isib = idx ^ 1; final int ipair = idx >> 1; final long[][] evals = {folded.clone(), folded.clone()}; evals[isib % 2] = layers[layer].sibling.clone(); final long[] row = new long[8]; System.arraycopy(evals[0], 0, row, 0, 4); System.arraycopy(evals[1], 0, row, 4, 4); final int height = 1 << lfh; if (!verifyBatch(commits[layer], new int[][]{{8, height}}, ipair, new long[][]{row}, layers[layer].openingProof)) { return null; } final long[] xSib = extMul(x, gen1); final long[][] xs = (isib % 2 == 1) ? new long[][]{x, xSib} : new long[][]{xSib, x}; final long[] beta = betas[layer]; final long[] num = extMul(extSub(beta, xs[0]), extSub(evals[1], evals[0])); final long[] den = extSub(xs[1], xs[0]); folded = extAdd(evals[0], extMul(num, extInv(den))); idx = ipair; x = extMul(x, x); } return folded; } // ================= case model + verify (accept / reject) ================= static final class Query { int index; long[] roTop; Layer[] layers; } static final class FriCase { String name; int logBlowup, logMaxHeight, numQueries; long[][] commits; long[][] betas; long[] finalPoly; Query[] queries; } static long[][] buildRoFull(final int logMaxHeight, final long[] roTop) { final long[][] ro = new long[logMaxHeight + 2][4]; ro[logMaxHeight] = roTop.clone(); return ro; } static boolean verifyCase(final FriCase c, final String tamper) { long[] finalPoly = c.finalPoly.clone(); long[][] betas = new long[c.betas.length][]; for (int i = 0; i < betas.length; i++) betas[i] = c.betas[i].clone(); if ("final".equals(tamper)) finalPoly[0] = (finalPoly[0] + 1) % P; if ("beta".equals(tamper)) betas[0][0] = (betas[0][0] + 1) % P; for (final Query q : c.queries) { final Layer[] layers = new Layer[q.layers.length]; for (int i = 0; i < layers.length; i++) { layers[i] = new Layer(); layers[i].sibling = q.layers[i].sibling.clone(); layers[i].openingProof = new long[q.layers[i].openingProof.length][]; for (int j = 0; j < layers[i].openingProof.length; j++) layers[i].openingProof[j] = q.layers[i].openingProof[j].clone(); } if ("sibling".equals(tamper)) layers[0].sibling[0] = (layers[0].sibling[0] + 1) % P; if ("proof".equals(tamper)) layers[0].openingProof[0][0] = (layers[0].openingProof[0][0] + 1) % P; final long[][] roFull = buildRoFull(c.logMaxHeight, q.roTop); final long[] folded = verifyQuery(c.logBlowup, c.logMaxHeight, c.commits, betas, q.index, roFull, layers); if (folded == null) return false; if (!extEq(folded, finalPoly)) return false; } return true; } // ================= minimal JSON parser (numbers/strings/arrays/objects/bools) ================= static final class JP { final String s; int i; JP(final String s) { this.s = s; } void ws() { while (i < s.length() && Character.isWhitespace(s.charAt(i))) i++; } Object val() { ws(); final char c = s.charAt(i); if (c == '{') return obj(); if (c == '[') return arr(); if (c == '"') return str(); if (c == 't') { i += 4; return Boolean.TRUE; } if (c == 'f') { i += 5; return Boolean.FALSE; } if (c == 'n') { i += 4; return null; } return num(); } Map obj() { final Map m = new HashMap<>(); i++; ws(); if (s.charAt(i) == '}') { i++; return m; } while (true) { ws(); final String k = str(); ws(); i++; // ':' m.put(k, val()); ws(); if (s.charAt(i) == ',') { i++; continue; } i++; break; // '}' } return m; } List arr() { final List a = new ArrayList<>(); i++; ws(); if (s.charAt(i) == ']') { i++; return a; } while (true) { a.add(val()); ws(); if (s.charAt(i) == ',') { i++; continue; } i++; break; // ']' } return a; } String str() { final StringBuilder b = new StringBuilder(); i++; // opening quote while (s.charAt(i) != '"') { if (s.charAt(i) == '\\') i++; b.append(s.charAt(i++)); } i++; return b.toString(); } Long num() { final int start = i; while (i < s.length() && "+-0123456789.eE".indexOf(s.charAt(i)) >= 0) i++; return Long.parseLong(s.substring(start, i)); } } @SuppressWarnings("unchecked") static long[] la(final Object o) { final List l = (List) o; final long[] out = new long[l.size()]; for (int i = 0; i < out.length; i++) out[i] = (Long) l.get(i); return out; } @SuppressWarnings("unchecked") static long[][] laa(final Object o) { final List l = (List) o; final long[][] out = new long[l.size()][]; for (int i = 0; i < out.length; i++) out[i] = la(l.get(i)); return out; } @SuppressWarnings("unchecked") static FriCase[] parseGroundTruth(final String json, final long[][] permOut, final long[][] gensOut) { final JP jp = new JP(json); final Map root = (Map) jp.val(); permOut[0] = la(root.get("perm_zeros")); gensOut[0] = la(root.get("two_adic_generators")); final List cases = (List) root.get("cases"); final FriCase[] out = new FriCase[cases.size()]; for (int ci = 0; ci < out.length; ci++) { final Map cm = (Map) cases.get(ci); final FriCase c = new FriCase(); c.name = (String) cm.get("name"); c.logBlowup = (int) (long) (Long) cm.get("log_blowup"); c.logMaxHeight = (int) (long) (Long) cm.get("log_max_height"); c.numQueries = (int) (long) (Long) cm.get("num_queries"); c.commits = laa(cm.get("commit_phase_commits")); c.betas = laa(cm.get("betas")); c.finalPoly = la(cm.get("final_poly")); final List qs = (List) cm.get("queries"); c.queries = new Query[qs.size()]; for (int qi = 0; qi < c.queries.length; qi++) { final Map qm = (Map) qs.get(qi); final Query q = new Query(); q.index = (int) (long) (Long) qm.get("index"); q.roTop = la(qm.get("ro_top")); final List ls = (List) qm.get("layers"); q.layers = new Layer[ls.size()]; for (int li = 0; li < q.layers.length; li++) { final Map lm = (Map) ls.get(li); final Layer L = new Layer(); L.sibling = la(lm.get("sibling_value")); L.openingProof = laa(lm.get("opening_proof")); q.layers[li] = L; } c.queries[qi] = q; } out[ci] = c; } return out; } // ================= JSON emit (shared cross-language vector set) ================= static String ja(final long[] v) { final StringBuilder b = new StringBuilder("["); for (int i = 0; i < v.length; i++) { if (i > 0) b.append(','); b.append(reduce(v[i])); } return b.append(']').toString(); } static String jaa(final long[][] v) { final StringBuilder b = new StringBuilder("["); for (int i = 0; i < v.length; i++) { if (i > 0) b.append(','); b.append(ja(v[i])); } return b.append(']').toString(); } static void emit(final String path) throws Exception { final long[][] permOut = new long[1][]; final long[][] gensOut = new long[1][]; final FriCase[] cases = parseGroundTruth(new String(Files.readAllBytes(Paths.get(path))), permOut, gensOut); final long[] permZeros = permute(new long[16]); final long[] gens = new long[28]; for (int b = 0; b < 28; b++) gens[b] = twoAdicGenerator(b); final StringBuilder sb = new StringBuilder(); sb.append("{\"permZeros\":").append(ja(permZeros)); sb.append(",\"twoAdicGenerators\":").append(ja(gens)); sb.append(",\"cases\":["); for (int ci = 0; ci < cases.length; ci++) { final FriCase c = cases[ci]; if (ci > 0) sb.append(','); final long[][] folded = new long[c.queries.length][]; for (int qi = 0; qi < c.queries.length; qi++) { final long[][] roFull = buildRoFull(c.logMaxHeight, c.queries[qi].roTop); folded[qi] = verifyQuery(c.logBlowup, c.logMaxHeight, c.commits, c.betas, c.queries[qi].index, roFull, c.queries[qi].layers); } sb.append("{\"name\":\"").append(c.name).append("\""); sb.append(",\"logBlowup\":").append(c.logBlowup); sb.append(",\"logMaxHeight\":").append(c.logMaxHeight); sb.append(",\"numQueries\":").append(c.numQueries); sb.append(",\"folded\":").append(jaa(folded)); sb.append(",\"finalPoly\":").append(ja(c.finalPoly)); sb.append(",\"accept\":").append(verifyCase(c, null)); sb.append(",\"rejectSibling\":").append(!verifyCase(c, "sibling")); sb.append(",\"rejectProof\":").append(!verifyCase(c, "proof")); sb.append(",\"rejectBeta\":").append(!verifyCase(c, "beta")); sb.append(",\"rejectFinal\":").append(!verifyCase(c, "final")); sb.append("}"); } sb.append("]}"); System.out.print(sb); } // ================= self-test (conformance) ================= static int pass = 0, fail = 0; static void check(final String name, final boolean cond) { if (cond) pass++; else { fail++; System.out.println("FAIL " + name); } } static void selfTest(final String path) throws Exception { final long[][] permOut = new long[1][]; final long[][] gensOut = new long[1][]; final FriCase[] cases = parseGroundTruth(new String(Files.readAllBytes(Paths.get(path))), permOut, gensOut); // sanity: perm + generators match the confirmed sub-components check("sanity.perm_zeros", java.util.Arrays.equals(permute(new long[16]), permOut[0])); for (int b = 0; b < 28; b++) check("sanity.gen." + b, twoAdicGenerator(b) == gensOut[0][b]); for (final FriCase c : cases) { check("accept." + c.name, verifyCase(c, null)); check("reject.sibling." + c.name, !verifyCase(c, "sibling")); check("reject.proof." + c.name, !verifyCase(c, "proof")); check("reject.beta." + c.name, !verifyCase(c, "beta")); check("reject.final." + c.name, !verifyCase(c, "final")); } System.out.println("FriVerifySelfTest PASS=" + pass + " FAIL=" + fail); if (fail != 0) { System.out.println("FRI_VERIFY_SELFTEST_FAILED"); System.exit(1); } System.out.println("FRI_VERIFY_SELFTEST_OK"); } public static void main(final String[] args) throws Exception { boolean emit = false; String path = "fri_ground_truth.json"; for (final String a : args) { if (a.equals("--emit")) emit = true; else if (!a.startsWith("--")) path = a; } if (emit) emit(path); else selfTest(path); } }