aere-research/pq-stark/spec-fri-babybear.md
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

11 KiB

FRI verifier over BabyBear: config + fold/opening relation + conformance KAT (component (d), 0x0AE8)

Scope caveat (2026-07-19 finding). "Plonky3/SP1" wording below means Plonky3 0.4.3-succinct (Aere's OWN BabyBear + FRI STARK stack). It does NOT mean the pinned SP1 6.1.0, which is a Hypercube release whose inner recursion proof uses BaseFold, NOT FRI. This FRI low-degree test does not apply to SP1 6.1.0 at all; it is conformance-confirmed for a BabyBear + FRI STARK verifier (Aere's own Plonky3 circuits), not for an SP1 6.1.0 verifier. See ../../docs/AERE-STARK-SP1-RECURSION-AIR-PORT-SPEC-SUMMARY.md.

Date: 2026-07-19. Status: FRI config CONFIRMED-FROM-SOURCE; fold + opening relation (verify_query) IMPLEMENTED; real conformance KAT PASSED (accept genuine + reject tampered).

This records the exact FRI low-degree test Plonky3/SP1 use over BabyBear, traced from the pinned p3-fri source, the fold + opening relation the verifier checks, and the real known-answer test that confirms conformance. The top-level precompile 0x0AE8 stays FAIL-CLOSED (returns EMPTY for every input) regardless: this component being confirmed does not port the duplex-sponge Fiat-Shamir challenger (component (f)) that binds the transcript, nor the SP1 recursion-AIR (component (e)).

Pinned target (exact, checksum-matched)

The FRI verifier is p3_fri::verifier from p3-fri, its commit-phase MMCS is ExtensionMmcs from p3-commit wrapping the CONFIRMED FieldMerkleTreeMmcs (component (c)), and the challenger used to emit the ground truth is DuplexChallenger from p3-challenger. All crates.io version 0.4.3-succinct (the Succinct Plonky3 fork), the revision pinned in the repo's Cargo.lock files:

  • p3-fri 0.4.3-succinct, sha256 5cbc4965ee488f3247867b7ec4bb005b8afa72cb0d461a4dcb1387ecab6426d5
  • p3-challenger 0.4.3-succinct, sha256 b6a908924d43e4cfb93fb41c8346cac211b70314385a9037e9241f5b7f3eaf77
  • p3-dft 0.4.3-succinct, sha256 be6408b10a2c27eb13a7d5580c546c2179a8dc7dbc10a990657311891f9b41c0
  • p3-commit 0.4.3-succinct, sha256 50acacc7219fce6c01db938f82c1b21b5e7133990b7fff861f91534aeb569419

Found in aerenew/zk-circuits/*/Cargo.lock and aerenew/rollup-evm-validity/*/Cargo.lock. A fresh cargo build of the extractor (below) resolved the SAME checksums, so the crates executed are byte-identical to the ones the AERE prover pins.

The confirmed FRI config (traced from p3-fri source)

FriConfig (p3-fri config.rs) has exactly four fields: { log_blowup, num_queries, proof_of_work_bits, mmcs }. Traced from verifier.rs:

  • Folding arity is 2 (hard-coded): each layer opens the sibling pair (index, index ^ 1) and folds index_pair = index >> 1. The comment in proof.rs says a non-2 arity would need multiple siblings; this pinned version is arity 2.
  • log_max_height = commit_phase_commits.len() + log_blowup. The commit phase runs for log_max_height - log_blowup layers (for log_folded_height in (log_blowup..log_max_height).rev() in prover.rs commit_phase), so num_fold_rounds = log_max_height - log_blowup.
  • The FINAL polynomial is a single CONSTANT final_poly: F (F = the challenge field EF). Folding runs all the way down; after the last fold the remaining blowup() evaluations are all equal, and the verifier checks folded_eval == final_poly for EVERY query (verify_challenges). (The source comment notes it "could become Vec<...> if generalized to support non-constant final polynomials"; in this pinned version it is a constant, so log_final_poly_len = 0.)
  • The commit-phase MMCS is ExtensionMmcs<Val, Challenge, ValMmcs> (the FRI codewords live in the degree-4 extension EF = BinomialExtensionField<BabyBear, 4>). Each commit-phase leaf is a PAIR of EF evaluations; ExtensionMmcs::verify_batch flattens each EF element to its 4 base coords (as_base_slice) and concatenates, so a leaf is 8 BabyBear coords, a single width-8 row of a height-2^log_folded_height tree, checked by the CONFIRMED base FieldMerkleTreeMmcs::verify_batch (component (c)).
  • Proof-of-work / grinding (proof_of_work_bits, check_witness) is part of the transcript phase (verify_shape_and_sample_challenges), i.e. component (f); it is NOT part of the per-query fold + opening relation this component ports.
  • num_queries, log_blowup, proof_of_work_bits are proximity/soundness parameters read from the frozen SP1 config; the values are a [MEASURE] against a real exported proof (see the port spec). The KAT here exercises several (log_blowup, log_max_height, num_queries) shapes to validate the relation independently of the exact frozen numbers.

The fold + opening relation (p3-fri verify_query, ported)

For one query at index (from verifier.rs verify_query, reproduced exactly by the references and by Fri.verifyQuery in the precompile):

folded_eval = 0 in EF
x = two_adic_generator(log_max_height)^reverse_bits_len(index, log_max_height)   # coset point, in EF
for each layer (log_folded_height = log_max_height-1 down to log_blowup):
    folded_eval += reduced_openings[log_folded_height + 1]     # the injected input opening at this height
    index_sibling = index ^ 1;  index_pair = index >> 1
    evals = [folded_eval, folded_eval];  evals[index_sibling % 2] = sibling_value   # the opened sibling
    MMCS.verify_batch(commit_layer, dims={width:2(EF)->8(base), height:2^log_folded_height},
                      index_pair, [evals flattened to 8 base coords], opening_proof)   # component (c)
    xs = [x, x];  xs[index_sibling % 2] *= two_adic_generator(1)   # the sibling point is -x
    folded_eval = evals[0] + (beta - xs[0]) * (evals[1] - evals[0]) / (xs[1] - xs[0])   # interpolate at beta
    index = index_pair;  x = x^2
return folded_eval    # verify_challenges checks this == final_poly for every query
  • two_adic_generator(bits) for bits <= 27 is the BabyBear base generator embedded in EF (confirmed from p3-baby-bear extension.rs ext_two_adic_generator), so x is a base-field element embedded in EF; two_adic_generator(1) = -1 (the order-2 root). The arithmetic uses F_{p^4} (component (a)).
  • The reduced openings come from the input polynomials in a full STARK (component (e)); for a standalone single-codeword low-degree test they are zero except reduced_openings[log_max_height] = codeword_value_at_index, which the extractor emits as ro_top.

Montgomery / canonical note

Field elements are serialized as canonical u32 (PrimeField32::as_canonical_u32 for base coords). The F_{p^4} fold arithmetic is canonical (component (a)); the Montgomery R^{-1} factor lives entirely inside the Poseidon2 internal layer (component (b), resolved there) and does not surface in the FRI or MMCS layers. As a cross-check, the extractor's permute([0;16]) (emitted as perm_zeros) equals the confirmed Poseidon2 "zeros" vector, and the extractor's two_adic_generator(bits) for bits 0..=27 equals component (a)'s twoAdicGenerator(bits) (this is what makes the fold's coset points come out right, closing the old [VERIFY] on the generator).

How the KAT vectors were obtained (primary source)

A small Rust extractor (pq-stark/fri-extractor, main.rs + Cargo.toml + Cargo.lock) depending on the pinned crates was compiled and run (cargo 1.97.0). It builds the exact SP1 inner FRI config (BabyBear + BinomialExtensionField<_,4> + the seed_from_u64(1) Poseidon2 permutation + FieldMerkleTreeMmcs + ExtensionMmcs + DuplexChallenger<_,_,16,8>), constructs genuinely low-degree codewords (an RS/LDE of a random low-degree polynomial via Radix2Dit, bit-reversed), runs p3_fri::prover::prove to emit a real FriProof, then runs p3_fri::verifier::{verify_shape_and_sample_challenges, verify_challenges} to confirm the pinned LIBRARY itself accepts. It emits everything the reference verifier needs (commit-phase roots, betas, query indices, reduced openings, per-query per-layer sibling values + MMCS opening proofs, final poly), each field element as canonical u32. The output (pq-stark/fri_ground_truth.json) is the authoritative source. Three FRI cases are emitted:

case log_blowup log_max_height num_queries fold layers
blowup1_h6_q4 1 6 4 5
blowup2_h7_q5 2 7 5 5
blowup1_h8_q6 1 8 6 7

Conformance KAT (real known-answer test, executed from the pinned crates)

The reference (fri_verify_reference.py, .mjs, standalone FriVerifySelfTest.java, and Fri.verifyQuery in the precompile) re-verifies each emitted proof:

  • ACCEPT: every query's MMCS openings pass AND every query's folded constant equals final_poly.
  • REJECT (four tamper variants per case): a corrupted opened value (breaks the MMCS opening), a corrupted MMCS sibling digest (breaks the opening), a wrong fold challenge beta (breaks the interpolation so folded != final_poly, i.e. a wrong fold), and a non-matching final_poly (the final-poly check, the mechanism that catches a non-low-degree witness).

Result (ran 2026-07-19):

  • python test_fri_verify.py -> PASS=55 FAIL=0 (sanity perm + 28 generators; 3 cases x (accept + 4 rejects); Python + Node + Java byte-identical per-query folded values; recorded in ../results/kat-results-fri-verify.json).
  • java FriVerifySelfTest fri_ground_truth.json -> PASS=44 FAIL=0.
  • The PRECOMPILE's own Sp1StarkVerifierPrecompiledContract.Fri.verifyQuery (driven directly) reproduces accept + reject for all 3 cases (15/15), so the wired code, not just the parallel references, is conformant.

Status summary

  • FRI config: CONFIRMED-FROM-SOURCE (arity 2; num_fold_rounds = log_max_height - log_blowup; constant final poly; ExtensionMmcs-over-FieldMerkleTreeMmcs commit phase; traced from p3-fri 0.4.3-succinct; checksums matched Cargo.lock).
  • Fold + opening relation (verify_query): IMPLEMENTED (Python/Node/Java + Fri.verifyQuery).
  • Conformance KAT: PASSED (3 real FRI proofs accepted, 4 tamper variants each rejected; three-language byte-identical; the precompile's own method also conformant).
  • Closed by this KAT (were [VERIFY]): arity-2 fold, num_fold_rounds, reverse-bits index-to-coset-point mapping, two_adic_generator(bits) = Plonky3's, and the F_{p^4} non-residue W = 11 = Plonky3's (the EF fold arithmetic reproduces the interpolation).
  • Fri.foldRelationConfirmed = true (the fold + opening relation is confirmed conformant).
  • [MEASURE] remaining: conformance against a REAL exported SP1 v6.1.0 inner proof's FRI section (needs the exported proof + the challenger, component (f)) and the AIR reduced openings (component (e)).
  • Top-level 0x0AE8: still FAIL-CLOSED (EMPTY for every input). The duplex-sponge challenger (Challenger.spongePorted = false, component (f)) that derives the betas/indices/grinding, and the SP1 recursion-AIR (StarkConstraints = UNAVAILABLE, component (e)) that supplies the reduced openings, are un-ported, so Fri.checkQuery cannot be driven and returns UNAVAILABLE. ACCEPT is unreachable. DO NOT ACTIVATE.