# Component (f): Fiat-Shamir transcript / Poseidon2 duplex challenger (0x0AE8) - CONFORMANCE-CONFIRMED > **Scope caveat (2026-07-19 finding).** This transcript is conformance-confirmed against Plonky3 > `p3-challenger` / `p3-fri` `0.4.3-succinct` (Aere's OWN BabyBear + FRI STARK stack), NOT against SP1 > 6.1.0. SP1 6.1.0 is a **Hypercube** release (KoalaBear multilinear: BaseFold + Jagged + > sumcheck-zerocheck + LogUp-GKR), so FRI and DEEP-ALI do not apply to it and this skeleton does not > verify SP1 6.1.0 proofs. (The duplex-challenger SHAPE carries over to SP1's Hypercube stack, but > over the KoalaBear field with KoalaBear constants and a different observe/sample order.) Replacing > the SP1 BN254 Groth16 wrap is a separate ~22 to 32 person-week retarget. See > `AERE-STARK-SP1-RECURSION-AIR-PORT-SPEC-SUMMARY.md`. Date: 2026-07-19. Precompile: Aere PQ STARK verifier at 0x0AE8. Scope: port-spec component (f). ## Result headline - Transcript conformance KAT: **PASSED** (PASS=44 FAIL=0 across Python + Node + standalone Java; standalone `ChallengerSelfTest` PASS=31 including a driver fail-closed gate model). - It **closed the FRI betas/indices loop** that component (d) left open: transcript -> derived betas/indices -> confirmed `Fri.verifyQuery` ACCEPTs the real FRI ground-truth proof and REJECTs a tampered beta. - `Challenger.spongePorted` flipped **true**. Top level stays **FAIL-CLOSED**: `StarkConstraints. evaluateAtZeta` still returns UNAVAILABLE (component (e), the AIR, un-ported), gating ACCEPT before the query loop and before the single `return ACCEPT`. `computePrecompile` returns EMPTY for every input; ACCEPT is unreachable by construction. DO NOT ACTIVATE. ## What was confirmed from pinned source (p3-challenger / p3-fri 0.4.3-succinct) `DuplexChallenger` (duplex_challenger.rs) + `GrindingChallenger` (grinding_challenger.rs), transcript order from p3-fri verifier.rs `verify_shape_and_sample_challenges`: - `observe(felt)`: clear output buffer, push to input buffer, duplex when input buffer reaches RATE=8. - `duplexing`: overwrite the first `input_buffer.len()` (<=RATE) of 16 lanes, permute (confirmed Poseidon2), set output buffer = lanes [0..8). - `sample`: duplex if inputs buffered OR outputs empty, then `Vec::pop` (LIFO, from the END). An F_{p^4} sample = 4 sequential base pops [c0,c1,c2,c3] (`sample_ext_element` = `sample_vec(4)` then `from_base_slice`). - `sample_bits(bits)`: LOW `bits` of `as_canonical_u64` (confirmed LSB). - `check_witness(pow_bits, witness)`: `observe(witness)` then `sample_bits(pow_bits) == 0`. - FRI transcript order: per commit -> observe 8-elem digest, sample beta (F_{p^4}); then observe final_poly (4 coords); then check_witness(pow_bits, pow_witness); then num_queries x sample_bits(log_max_height). log_max_height = commits.len() + log_blowup. Montgomery/serialization: all arithmetic canonical (the confirmed Poseidon2 reference already carries the internal-layer R^-1 factor); the extractor emits `as_canonical_u32` and `as_base_slice` coords, and the references reproduce them exactly. ## Ground truth (executed the pinned crates) `pqc-fork/pq-stark/challenger-extractor/` (Cargo.toml + copied fri-extractor Cargo.lock, byte-identical pins) compiled and ran (target dir in scratch, no repo pollution, live infra untouched). It emits `challenger_ground_truth.json` with two blocks: 1. `duplex_kat`: a fully-scripted observe/sample transcript -> base samples, F_{p^4} ext samples, sample_bits, the full 16-lane sponge state at a checkpoint, and a check_witness accept/reject table (real grinding witness accepts, fixed non-witnesses reject). 2. `fri_transcript`: the SAME three FRI cases as `fri_ground_truth.json`, rebuilt (same seeds) so the commit_phase_commits / final_poly / pow_witness / betas / query_indices match; the references derive betas+indices+PoW-accept from (commits, final_poly, pow_witness) and match BOTH the extractor AND fri_ground_truth.json. The references reproduce every value byte-for-byte (all 3 languages identical), and the derived betas/indices, fed to the confirmed `Fri.verifyQuery`, accept the real proof and reject a tampered beta. ## Files added/changed Added: `challenger_reference.py`, `challenger_reference.mjs`, `ChallengerSelfTest.java`, `test_challenger.py`, `challenger-extractor/{Cargo.toml,Cargo.lock,src/main.rs}`, `challenger_ground_truth.json`, `../results/kat-results-challenger.json`. Changed: - `pqc-fork/precompiles/Sp1StarkVerifierPrecompiledContract.java`: `Challenger` class rewritten as the confirmed duplex sponge over `Poseidon2Bb.permute` (observeField/sampleBaseField/sampleExtField/ sampleBits/checkWitnessField + byte-facing driver adapters); `spongePorted = true`; driver stage-4 comment clarifies the AIR gate; stale "spongePorted=false" docs updated. `StarkConstraints. evaluateAtZeta` UNCHANGED (still returns UNAVAILABLE at line ~1077). - `docs/AERE-STARK-VERIFIER-PORT-SPEC.md`: status header, section 5 note, section 7 rewritten (IMPLEMENTED + CONFORMANCE-CONFIRMED), section 9 table row (f), section 10 files. - `pqc-fork/pq-stark/README.md`: HONEST STATUS, sections (b)/(c)/(d) closings, new section (f), layout. - `pqc-fork/pq-stark/test_fri_verify.py`: descriptive notes updated (f now confirmed; loop closed). ## Fail-closed proof verify() control flow: ACCEPT requires stage 3 (PoW) AND stage 4 (AIR) AND stage 5 (queries) all pass. Stage 4 = `StarkConstraints.evaluateAtZeta` ALWAYS returns UNAVAILABLE (component (e) un-ported) and is checked (line 196) BEFORE the query loop and BEFORE the single `return ACCEPT` (line 228). So verify() returns UNAVAILABLE for every input reaching stage 4; computePrecompile returns EMPTY. Flipping spongePorted cannot create an ACCEPT path. Corroborated at runtime by `ChallengerSelfTest.gateModel` (driver.gate.ACCEPT_unreachable_for_every_input PASS). The precompile is not compiled here (it is applied via patch to a Besu build, no local Besu jar); the challenger algorithm is verified by the byte-identical standalone `ChallengerSelfTest`. ## Remaining Component (e), the SP1 recursion AIR (StarkConstraints, ~3-4 person-weeks, HARDEST), is the one remaining un-ported crypto-core component. Real exported SP1 v6.1.0 transcript order is [MEASURE]. On-chain ZK verifiers remain classical BN254 until all 6 components + integration + a real SP1-proof end-to-end KAT + audit + founder activation.