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.
159 lines
11 KiB
Markdown
159 lines
11 KiB
Markdown
# 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.
|