/* * Copyright contributors to the AERE Network. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * * SPDX-License-Identifier: Apache-2.0 */ package org.hyperledger.besu.evm.precompile; import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.gascalculator.GasCalculator; import jakarta.validation.constraints.NotNull; import org.apache.tuweni.bytes.Bytes; /** * AERE PQC precompile: DIRECT post-quantum verification of an SP1 (Plonky3) INNER hash-based * FRI/STARK proof, at 0x0AE8. * *

================================================================================== * REFERENCE SKELETON - NOT A WORKING VERIFIER. DO NOT ACTIVATE ON MAINNET. * ================================================================================== * *

This class is the fork EVM precompile STRUCTURE for verifying an SP1 v6.1.0 inner STARK proof * (the "shrink"/compress Plonky3 STARK over BabyBear + Poseidon2 + FRI) DIRECTLY on-chain, WITHOUT * the BN254 Groth16 outer wrap. The Groth16 wrap that the live SP1VerifierGateway * (0x9ca479C8c52C0EbB4599319a36a5a017BCC70628, route 0x4388a21c) uses is pairing-based over BN254 * and is broken by Shor's algorithm; the inner FRI/STARK it wraps rests only on the * collision-resistance of a hash (Poseidon2) and Reed-Solomon proximity gaps, so verifying it * directly removes the only quantum-vulnerable link in the SP1 proof chain. See * docs/PQ-STARK-VERIFIER-PRECOMPILE-2026-07-18.md for the full spec, soundness parameters, and * the port plan. * *

Honest status. The pieces that are pure, deterministic, and safe to write by hand are * implemented and testable here: BabyBear prime-field arithmetic ({@link BabyBear}) and its * complete degree-4 extension F_p4 ({@link BabyBearExt4}: add/sub/neg/mul/inv/Frobenius, with W * pinned to a proven-irreducible non-residue), the versioned * wire-format parser/validator, a generic binary-Merkle path-check structure, the Fiat-Shamir * challenger skeleton, and the FRI query-index derivation ({@link FriQueryIndex}: sample_bits + the * folding-index walk, unit-tested and cross-checked across Python, Node, and Java; see * docs/AERE-STARK-VERIFIER-PORT-SPEC.md section 8). The Poseidon2-BabyBear permutation * ({@link Poseidon2Bb}) is now CONFIRMED conformant to the pinned Plonky3 (p3-baby-bear / * p3-poseidon2 0.4.3-succinct) via a real known-answer test, so {@link Poseidon2Bb#available} is * true. The MMCS ({@link Mmcs}: the {@code FieldMerkleTreeMmcs} vector commitment with its * PaddingFreeSponge leaf hasher, TruncatedPermutation compressor, and mixed-height {@code verifyBatch}) * is likewise now CONFIRMED conformant to the pinned Plonky3 (p3-merkle-tree / p3-symmetric / * p3-commit 0.4.3-succinct) via a real known-answer test, so {@link Mmcs#available} is true. The FRI * FOLD + OPENING relation ({@link Fri#verifyQuery}, the low-degree test's per-query check, component * (d)) is now IMPLEMENTED and CONFIRMED conformant to the pinned Plonky3 {@code p3-fri} 0.4.3-succinct * via a real known-answer test (real FRI proofs from the p3-fri prover are re-verified byte-for-byte, * accept-genuine + reject-tampered; see docs/AERE-STARK-VERIFIER-PORT-SPEC.md section 5 and * pq-stark/test_fri_verify.py), so {@link Fri#foldRelationConfirmed} is true. The duplex-sponge * Fiat-Shamir challenger ({@link Challenger}, component (f)) that derives the FRI betas / query indices / * grinding is now likewise CONFIRMED conformant to the pinned Plonky3 {@code p3-challenger} 0.4.3-succinct * ({@code DuplexChallenger} + {@code GrindingChallenger}) via a real known-answer test (a scripted * transcript reproduces every sampled value, the full 16-lane sponge state, and the grinding * accept/reject table byte-for-byte, and the DERIVED betas/indices close the FRI loop into the confirmed * {@link Fri#verifyQuery}; see pq-stark/test_challenger.py, PASS=44), so {@link Challenger#spongePorted} * is true. Component (e), the AIR constraint / quotient-consistency check, is now SPLIT: its GENERIC * quotient-consistency MECHANISM ({@link StarkConstraints#checkGenericQuotient}) is IMPLEMENTED and * CONFORMANCE-CONFIRMED against the pinned Plonky3 {@code p3-uni-stark} 0.4.3-succinct via a real * known-answer test on two KNOWN example AIRs (the Fibonacci AIR from Plonky3's own tests/fib_air.rs and * a degree-3 multiply AIR matching tests/mul_air.rs: the pinned prover+verifier accept, and the mechanism * reproduces the accept + rejects a tampered trace opening / quotient chunk / alpha), so * {@link StarkConstraints#GENERIC_QUOTIENT_CHECK_CONFIRMED} is true. But the SPECIFIC SP1 RECURSION AIR * (its exact multi-thousand-constraint set, interactions/permutation argument, public-value layout) and * the verifying-key digest that binds it are NOT ported (a multi-week piece needing the SP1 toolchain; * Plonky3 {@code p3-uni-stark} + SP1 {@code sp1-stark}): {@link StarkConstraints#SP1_RECURSION_AIR_PORTED} * is false. Because that gate is false, {@link StarkConstraints#evaluateAtZeta} (the REAL verify path) * returns UNAVAILABLE, which the driver checks at stage 4 (before any query loop and before the single * {@code return ACCEPT}), and {@link WireReader} still does not parse the proof body: {@link Fri#checkQuery} * stays gated and returns UNAVAILABLE. Those methods return "unavailable", which makes * {@link #computePrecompile} FAIL-CLOSED: it returns EMPTY (0x, "not verified") for every input. It * CANNOT return a positive (verified) result in this build, by construction, so it can never yield a * false accept. The generic mechanism being confirmed does NOT change this: it is exercised only for a * SUPPLIED example AIR under the KAT, never on the real SP1-proof path. The ISP1Verifier Solidity shim * treats "not the 32-byte 0x..01 success word" as a * revert, so a consumer that swaps to this precompile before the core is completed simply denies * every proof (safe), never accepts a forged one. DO NOT ACTIVATE (see below). * *

Wire contract (v1, see spec doc section 3). Input is a self-describing, length-checked * blob: * *

 *   magic(4)="AS1\0" || version(1) || configId(1) || reserved(2)
 *   || vkeyDigest(32)            // Poseidon2 digest of the SP1 program vkey / recursion vk
 *   || publicValuesLen(4, BE) || publicValues(publicValuesLen)
 *   || proofLen(4, BE)          || friProof(proofLen)   // the serialized Plonky3 shard proof
 * 
* * The {@code friProof} body is itself parsed (commit-phase Merkle roots, per-query openings, final * polynomial, and the proof-of-work witness) by {@link WireReader}; its exact sub-layout is pinned * to the frozen SP1 v6.1.0 {@code ShardProof} serialization (spec doc section 3.2). * *

Output. On a fully verified proof: the 32-byte word {@code 0x00..01}. On ANY malformed * input, unsupported config, or (in this skeleton) any path that reaches the un-ported crypto core: * EMPTY (0x). Never faults, exactly like the other AERE PQC precompiles. */ public class Sp1StarkVerifierPrecompiledContract extends AbstractPrecompiledContract { // ---- wire constants ------------------------------------------------------------------------- private static final byte[] MAGIC = {'A', 'S', '1', 0}; private static final int VERSION = 1; private static final int HEADER_LEN = 4 + 1 + 1 + 2 + 32; // magic|ver|cfg|resv|vkeyDigest = 40 private static final int MIN_INPUT = HEADER_LEN + 4 + 4; // + publicValuesLen + proofLen fields /** 32-byte big-endian 1, the "verified" success word the ISP1Verifier shim expects. */ private static final Bytes VERIFIED = Bytes.fromHexString( "0x0000000000000000000000000000000000000000000000000000000000000001"); // ---- gas model (illustrative; MUST be re-benchmarked on the frozen artifact) ---------------- // // A FRI/STARK verify cost is dominated by Poseidon2-BabyBear permutations: one per Merkle node // on each query's authentication path, per FRI folding round, plus the constraint openings. The // model is BASE + numQueries * logDomain * POSEIDON2_GAS + constraint/quotient term. Numbers are // pre-benchmark placeholders pending the section-6 measurement in the spec doc. private static final long BASE_GAS = 250_000L; private static final long GAS_PER_QUERY = 40_000L; private static final long GAS_PER_BYTE = 3L; // calldata-proportional decode/hash-absorb term Sp1StarkVerifierPrecompiledContract(final GasCalculator gasCalculator) { super("AereSp1StarkVerify", gasCalculator); } @Override public long gasRequirement(final Bytes input) { // Gas must be a pure function of input and independent of the (skeleton) verify outcome. We // charge a conservative base plus a per-query term read from the (parsed) FRI config, plus a // byte term. If the header does not parse, charge only the byte term so a spam blob is cheap. final ParsedConfig cfg = ParsedConfig.tryParse(input); final long byteTerm = GAS_PER_BYTE * input.size(); if (cfg == null) { return BASE_GAS / 5 + byteTerm; // malformed: reduced base } return BASE_GAS + (long) cfg.numQueries * GAS_PER_QUERY + byteTerm; } @NotNull @Override public PrecompileContractResult computePrecompile( final Bytes input, @NotNull final MessageFrame messageFrame) { try { final VerifyResult r = verify(input); // FAIL-CLOSED: only an explicit ACCEPT returns the success word; everything else -> EMPTY. return PrecompileContractResult.success(r == VerifyResult.ACCEPT ? VERIFIED : Bytes.EMPTY); } catch (final Throwable t) { // Consensus rule for the non-signature PQC precompiles: never fault, deny on error. return PrecompileContractResult.success(Bytes.EMPTY); } } // ============================================================================================= // Verifier driver (structure complete; crypto core delegated -> currently returns UNAVAILABLE) // ============================================================================================= enum VerifyResult { ACCEPT, REJECT, /** A required piece of the crypto core is not yet ported in this reference build. */ UNAVAILABLE } static VerifyResult verify(final Bytes input) { final WireReader r = WireReader.open(input); if (r == null) { return VerifyResult.REJECT; // malformed header / lengths } // 1) Fiat-Shamir challenger, seeded with the vkey digest and public values. The duplex sponge over // Poseidon2 (component (f), Challenger) is now CONFIRMED conformant; the byte-facing observe here // absorbs the vkey/public values (the proof-body field elements come from WireReader, un-ported). final Challenger challenger = Challenger.newDuplex(); challenger.observe(r.vkeyDigest()); challenger.observe(r.publicValues()); // 2) Observe the FRI commit-phase Merkle roots to derive the folding challenges beta_i, and the // out-of-domain (DEEP) query point zeta. This is the transcript order Plonky3 fixes; it must // match the prover exactly or the sampled randomness diverges. for (final Bytes root : r.commitPhaseRoots()) { challenger.observe(root); } // final polynomial + PoW witness also enter the transcript before the query indices are drawn. challenger.observe(r.finalPolyBytes()); // 3) Proof-of-work / grinding check (part of FRI soundness; adds `pow_bits` to the query term). if (!challenger.checkProofOfWork(r.powWitness(), r.config().powBits)) { return VerifyResult.REJECT; } // 4) STARK constraint / quotient consistency at the DEEP point zeta. THE FAIL-CLOSED GATE (component // (e)). The GENERIC quotient-consistency mechanism is now ported + confirmed // (StarkConstraints.checkGenericQuotient, CONFIRMED vs p3-uni-stark on example AIRs), but the // SPECIFIC SP1 recursion AIR + its vkey binding are NOT (SP1_RECURSION_AIR_PORTED == false), so // evaluateAtZeta (the REAL path) ALWAYS returns UNAVAILABLE. This check sits BEFORE the query loop // and BEFORE the single `return ACCEPT` below, so even with the generic (e) mechanism, the // challenger (f), and the FRI fold (d) all confirmed, verify() returns UNAVAILABLE for every real // input and ACCEPT is unreachable. Without the SP1-recursion-specific AIR + vkey we cannot ACCEPT. final int constraintOk = StarkConstraints.evaluateAtZeta(r, challenger); if (constraintOk == StarkConstraints.UNAVAILABLE) { return VerifyResult.UNAVAILABLE; // -> EMPTY, fail-closed (SP1 recursion AIR + vkey un-ported) } if (constraintOk == StarkConstraints.FAIL) { return VerifyResult.REJECT; } // 5) FRI query phase: for each sampled index, check the Merkle openings against the commit-phase // roots and that each folding step is consistent (p_{i+1}(x^2) == fold(p_i, beta_i)). The // loop STRUCTURE is here; the per-step folding arithmetic + Poseidon2 Merkle hash are // delegated, so any query returning UNAVAILABLE forces fail-closed. final int[] queryIndices = challenger.sampleQueryIndices(r.config().numQueries, r.logMaxDomain()); // Fail-closed guard: a full verify MUST check exactly numQueries queries. If the sponge is not // ported (or otherwise yields too few indices), the query loop would be vacuous and fall through // to ACCEPT, a false accept. Deny instead. In this build sampleQueryIndices returns empty, so // this returns UNAVAILABLE -> EMPTY (belt-and-braces on top of the stage-4 UNAVAILABLE above). if (queryIndices.length != r.config().numQueries) { return VerifyResult.UNAVAILABLE; } for (int q = 0; q < queryIndices.length; q++) { final int step = Fri.checkQuery(r, queryIndices[q], challenger); if (step == Fri.UNAVAILABLE) { return VerifyResult.UNAVAILABLE; // -> EMPTY, fail-closed } if (step == Fri.FAIL) { return VerifyResult.REJECT; } } // If (and only if) every stage above genuinely passes will this return ACCEPT. In the current // reference build stage 4/5 return UNAVAILABLE, so control never reaches here. This is the // single line that a completed port makes reachable. return VerifyResult.ACCEPT; } // ============================================================================================= // BabyBear prime field p = 15 * 2^27 + 1 = 2013265921 = 0x78000001 (COMPLETE, testable) // ============================================================================================= /** * BabyBear canonical (non-Montgomery) arithmetic in {@code [0, p)} using 64-bit intermediates. * This is ordinary modular arithmetic (not a cryptographic primitive) and is implemented in full * so the field layer of the verifier is real and unit-testable. The degree-4 binomial extension * F_p[x]/(x^4 - 11) used by SP1's FRI lives in {@link BabyBearExt4}. */ static final class BabyBear { static final long P = 2013265921L; // 0x78000001 = 2^31 - 2^27 + 1 = 15 * 2^27 + 1 /** * A multiplicative generator of F_p^*. GENERATOR = 31 has order exactly p-1 (this is PROVEN * offline in pq-stark/test_babybear_field.py via the known factorization p-1 = 2^27 * 3 * 5). * [VERIFY] that 31 is Plonky3 p3-baby-bear's chosen generator at the pinned SP1 v6.1.0 revision; * a wrong generator does not break field arithmetic but would give the wrong stored roots of * unity if those are ever read from this constant rather than the proof. */ static final long GENERATOR = 31L; /** v2(p-1): p-1 = 2^27 * 3 * 5, so the largest power-of-two subgroup has order 2^27. */ static final int TWO_ADICITY = 27; private BabyBear() {} 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; } /** Additive inverse (negation). */ static long neg(final long a) { final long r = reduce(a); return r == 0 ? 0 : P - r; } static long mul(final long a, final long b) { return (reduce(a) * reduce(b)) % P; // both < 2^31 so product < 2^62, safe in a signed 64-bit } /** Exponentiation by squaring; used for inverse via Fermat (a^(p-2)). */ static long pow(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; } /** Multiplicative inverse; inv(0) is defined as 0 here (callers must guard division by zero). */ static long inv(final long a) { final long r = reduce(a); if (r == 0) return 0; return pow(r, P - 2); } /** * A generator of the order-2^bits subgroup of F_p^*, derived as GENERATOR^((p-1)/2^bits). Its * order is exactly 2^bits (PROVEN offline for bits = 1..27 in test_babybear_field.py). These are * the roots of unity FRI evaluation domains use. [VERIFY] the exact stored value against * Plonky3's two_adic_generator(bits); the order is a proven field fact, the exact value is not. */ static long twoAdicGenerator(final int bits) { if (bits < 0 || bits > TWO_ADICITY) { throw new IllegalArgumentException("bits out of range [0, 27]"); } return pow(GENERATOR, (P - 1) >> bits); } } /** * Degree-4 binomial extension of BabyBear, F_p[x]/(x^4 - W). Plonky3's BabyBear "quartic" * extension (|F| = p^4 ~= 2^124.05) is the field FRI queries and the DEEP point live in. * *

The extension NON-RESIDUE {@code W} is pinned to 11. This choice is PROVEN offline to yield a * genuine field: 11 is a quadratic non-residue mod p and p == 1 mod 4, so x^4 - 11 is irreducible * over F_p (Lidl-Niederreiter, irreducible-binomial criterion; checked in * pq-stark/test_babybear_field.py). [VERIFY] that 11 is Plonky3's exact BabyBear * {@code BinomialExtensionField} non-residue at the pinned SP1 v6.1.0 revision: a * wrong-but-still-irreducible W builds a valid field that silently DISAGREES with the prover, so * the field being real is necessary but not sufficient; conformance is [MEASURE]. * *

All operations are complete: add/sub/neg are component-wise, mul is the schoolbook * convolution reduced by x^4 = W, inv is Fermat a^(p^4-2), and Frobenius is a^p (so Frobenius^4 is * the identity and the base field is fixed). */ static final class BabyBearExt4 { // W = 11: PROVEN to give an irreducible x^4 - 11 (real field). [VERIFY] it is Plonky3's W. static final long W = 11L; final long[] c = new long[4]; // c0 + c1 x + c2 x^2 + c3 x^3 static BabyBearExt4 of(final long c0, final long c1, final long c2, final long c3) { final BabyBearExt4 e = new BabyBearExt4(); e.c[0] = BabyBear.reduce(c0); e.c[1] = BabyBear.reduce(c1); e.c[2] = BabyBear.reduce(c2); e.c[3] = BabyBear.reduce(c3); return e; } /** Base-field embedding a |-> a + 0x + 0x^2 + 0x^3. */ static BabyBearExt4 fromBase(final long a) { return of(a, 0, 0, 0); } boolean isZero() { return c[0] == 0 && c[1] == 0 && c[2] == 0 && c[3] == 0; } BabyBearExt4 add(final BabyBearExt4 o) { return of( BabyBear.add(c[0], o.c[0]), BabyBear.add(c[1], o.c[1]), BabyBear.add(c[2], o.c[2]), BabyBear.add(c[3], o.c[3])); } BabyBearExt4 sub(final BabyBearExt4 o) { return of( BabyBear.sub(c[0], o.c[0]), BabyBear.sub(c[1], o.c[1]), BabyBear.sub(c[2], o.c[2]), BabyBear.sub(c[3], o.c[3])); } BabyBearExt4 neg() { return of(BabyBear.neg(c[0]), BabyBear.neg(c[1]), BabyBear.neg(c[2]), BabyBear.neg(c[3])); } BabyBearExt4 mul(final BabyBearExt4 o) { final long[] t = new long[7]; for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { t[i + j] = BabyBear.add(t[i + j], BabyBear.mul(c[i], o.c[j])); } } // reduce x^4=W, x^5=Wx, x^6=Wx^2 final long r0 = BabyBear.add(t[0], BabyBear.mul(W, t[4])); final long r1 = BabyBear.add(t[1], BabyBear.mul(W, t[5])); final long r2 = BabyBear.add(t[2], BabyBear.mul(W, t[6])); final long r3 = t[3]; return of(r0, r1, r2, r3); } /** Exponentiation by squaring in F_{p^4} with a (possibly large) BigInteger exponent. */ BabyBearExt4 pow(final java.math.BigInteger e) { if (e.signum() < 0) { throw new IllegalArgumentException("use inv for negative exponents"); } BabyBearExt4 base = of(c[0], c[1], c[2], c[3]); BabyBearExt4 acc = fromBase(1); java.math.BigInteger ee = e; while (ee.signum() > 0) { if (ee.testBit(0)) acc = acc.mul(base); base = base.mul(base); ee = ee.shiftRight(1); } return acc; } /** Multiplicative inverse via Fermat a^(p^4 - 2); inv(0) := 0 (callers guard div-by-zero). */ BabyBearExt4 inv() { if (isZero()) return fromBase(0); final java.math.BigInteger p = java.math.BigInteger.valueOf(BabyBear.P); return pow(p.pow(4).subtract(java.math.BigInteger.TWO)); } /** Frobenius pi(a) = a^p; Frobenius^4 is the identity and it fixes the base field. */ BabyBearExt4 frobenius() { return pow(java.math.BigInteger.valueOf(BabyBear.P)); } } // ============================================================================================= // Wire parsing (COMPLETE, testable) + config // ============================================================================================= /** FRI/STARK config (query count, blowup, PoW bits) resolved from the 1-byte configId. */ static final class ParsedConfig { final int configId; final int numQueries; final int logBlowup; final int powBits; private ParsedConfig(final int id, final int nq, final int lb, final int pow) { this.configId = id; this.numQueries = nq; this.logBlowup = lb; this.powBits = pow; } /** * Resolve the frozen FRI config for a configId. The canonical values MUST be copied from the * pinned SP1 v6.1.0 shrink/wrap {@code FriConfig}; the entries below are the SHAPE with the * fields to fill (spec doc section 4). configId 1 = SP1 inner/shrink, 2 = SP1 wrap. */ static ParsedConfig forId(final int id) { switch (id) { case 1: // TODO(port): confirm against pinned SP1 v6.1.0 shrink FriConfig. return new ParsedConfig(1, /*numQueries*/ 100, /*logBlowup*/ 1, /*powBits*/ 16); case 2: // TODO(port): confirm against pinned SP1 v6.1.0 wrap FriConfig. return new ParsedConfig(2, /*numQueries*/ 25, /*logBlowup*/ 4, /*powBits*/ 16); default: return null; } } static ParsedConfig tryParse(final Bytes input) { if (input == null || input.size() < MIN_INPUT) return null; for (int i = 0; i < MAGIC.length; i++) { if (input.get(i) != MAGIC[i]) return null; } if ((input.get(4) & 0xff) != VERSION) return null; return forId(input.get(5) & 0xff); } } /** * Parses and length-validates the outer envelope and (structurally) the FRI proof body. This is * complete and deterministic; it never touches cryptographic material, only bounds and offsets. * The per-field crypto sub-parsers ({@link #commitPhaseRoots()} etc.) return the raw slices for * the (delegated) core to interpret. */ static final class WireReader { private final Bytes in; private final ParsedConfig cfg; private final Bytes vkeyDigest; private final Bytes publicValues; private final Bytes friProof; private WireReader( final Bytes in, final ParsedConfig cfg, final Bytes vkeyDigest, final Bytes publicValues, final Bytes friProof) { this.in = in; this.cfg = cfg; this.vkeyDigest = vkeyDigest; this.publicValues = publicValues; this.friProof = friProof; } static WireReader open(final Bytes input) { final ParsedConfig cfg = ParsedConfig.tryParse(input); if (cfg == null) return null; int off = HEADER_LEN - 32; // start of vkeyDigest = 8 final Bytes vkeyDigest = input.slice(off, 32); off += 32; // = 40 = HEADER_LEN // publicValues if (input.size() < off + 4) return null; final long pvLen = readU32(input, off); off += 4; if (pvLen < 0 || input.size() < off + pvLen) return null; final Bytes pv = input.slice(off, (int) pvLen); off += (int) pvLen; // friProof if (input.size() < off + 4) return null; final long pfLen = readU32(input, off); off += 4; if (pfLen < 0 || input.size() != off + pfLen) return null; // must consume input exactly final Bytes proof = input.slice(off, (int) pfLen); return new WireReader(input, cfg, vkeyDigest, pv, proof); } private static long readU32(final Bytes b, final int off) { return ((long) (b.get(off) & 0xff) << 24) | ((long) (b.get(off + 1) & 0xff) << 16) | ((long) (b.get(off + 2) & 0xff) << 8) | (b.get(off + 3) & 0xff); } ParsedConfig config() { return cfg; } Bytes vkeyDigest() { return vkeyDigest; } Bytes publicValues() { return publicValues; } // ---- FRI proof sub-fields (structural slices; interpreted by the delegated core) ----------- // TODO(port): implement the exact SP1 v6.1.0 ShardProof sub-layout offsets. Until then these // return safe empties, which keeps the core UNAVAILABLE and the precompile fail-closed. java.util.List commitPhaseRoots() { return java.util.Collections.emptyList(); // TODO(port) } Bytes finalPolyBytes() { return Bytes.EMPTY; // TODO(port) } Bytes powWitness() { return Bytes.EMPTY; // TODO(port) } int logMaxDomain() { return 0; // TODO(port): read from the shard's degree bits } } // ============================================================================================= // Delegated crypto core (STUBS -> fail-closed). Port targets cited in each method. // ============================================================================================= /** * Poseidon2 over BabyBear (width 16, the Plonky3 "Poseidon2BabyBear"). Component (b), port spec * section 3: the x^7 S-box, the 8 external (full) rounds with the MDS-light external layer built * from M4, the 13 internal (partial) rounds with the internal diffusion layer, and the 141-constant * round schedule. It is a real, deterministic bijection, cross-checked byte-for-byte against the * Python/Node/standalone-Java references in pqc-fork/pq-stark * (poseidon2_babybear_reference.{py,mjs}, Poseidon2BabyBearSelfTest.java; harness * test_poseidon2_babybear.py, PASS=47017 FAIL=0). * *

CONFORMANCE IS CONFIRMED (2026-07-19), SO {@link #available} IS true. The constants and * structure are confirmed against p3-poseidon2 / p3-baby-bear 0.4.3-succinct (the exact crates * pinned in the repo Cargo.lock, checksums * 522986377b2164c5f94f2dae88e0e0a3d169cc6239202ef4aeb4322d60feffd0 / * d69e6e9af4eaaaa60f7bb9f0e0f73ebcbaefe7e00974d97ad0fa542d6a4f0890), and a real known-answer test * PASSES (zeros / [0..15] / testvec, emitted by executing the pinned crates via cargo; lockfile * checksums matched): *

* *

This does NOT make the precompile verify anything. The permutation being confirmed is * necessary but not sufficient: the SP1 recursion-AIR ({@link StarkConstraints}, component (e)) is * un-ported (the duplex-sponge challenger (f), the MMCS (c), and the FRI fold+opening (d) are all now * confirmed), so {@link StarkConstraints#evaluateAtZeta} returns UNAVAILABLE and the top-level verifier * still returns EMPTY for every input. DO NOT ACTIVATE (see the class banner). */ static final class Poseidon2Bb { /** * CONFORMANCE GATE for the raw permutation. Now true: the constants + structure reproduce real * p3-baby-bear / p3-poseidon2 0.4.3-succinct known-answer vectors (zeros / [0..15] / testvec). * This flags ONLY that the permutation is confirmed conformant; it does NOT port the SP1 * recursion-AIR (component (e)), so the top-level precompile stays fail-closed (see * {@link StarkConstraints#evaluateAtZeta}). */ static final boolean available = true; static final int WIDTH = 16; static final int ROUNDS_F = 8; // external (full) rounds, 4 initial + 4 terminal. CONFIRMED. static final int ROUNDS_P = 13; // internal (partial) rounds for BabyBear width 16. CONFIRMED. /** * 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 = 943718400 (verified against the pinned library's exact 16x16 matrix). */ static final long R_INV = 943718400L; // Poseidon2 external 4x4 MDS matrix M4 (paper). CONFIRMED: the full 16x16 external matrix // recovered from Plonky3's Poseidon2ExternalMatrixGeneral equals the block(2*M4, M4) form. static final long[][] M4 = { {2, 3, 1, 1}, {1, 2, 3, 1}, {1, 1, 2, 3}, {3, 1, 1, 2} }; // diag(M_I - I) = D; internal layer M_I = R^{-1} * (J + diag(D)). CONFIRMED (canonical) vs // p3-baby-bear 0.4.3-succinct POSEIDON2_INTERNAL_MATRIX_DIAG_16_BABYBEAR_MONTY. static final long[] INTERNAL_DIAG_M1_16 = { BabyBear.P - 2, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 32768 }; // CONFIRMED round constants (canonical), from Xoroshiro128Plus::seed_from_u64(1) via // new_from_rng_128, extracted from the pinned p3-baby-bear / p3-poseidon2 0.4.3-succinct crates. static final long[][] RC_EXTERNAL = { // [ROUNDS_F][WIDTH], added to all lanes { 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 = { // [ROUNDS_P], added to lane 0 only 402771160L, 320708227L, 1122772462L, 100431997L, 202594011L, 1226485372L, 1088619034L, 64118538L, 109828860L, 724723599L, 1662837151L, 797753907L, 1075635743L, }; private Poseidon2Bb() {} /** The x^7 S-box (BabyBear: 7 is the smallest d>1 with gcd(d, p-1)=1, so it is a bijection). */ private static long sbox(final long x) { final long x2 = BabyBear.mul(x, x); final long x4 = BabyBear.mul(x2, x2); return BabyBear.mul(x4, BabyBear.mul(x2, x)); // x^4 * x^2 * x } private 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] = BabyBear.reduce(M4[row][0] * a + M4[row][1] * b + M4[row][2] * c + M4[row][3] * d); } return out; } /** Poseidon2 external MDS-light layer: apply M4 per block of 4, then add across-block col sums. */ private 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] = BabyBear.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] = BabyBear.add(blk[b][j], colSum[j]); } /** * Poseidon2 internal layer, matching p3-baby-bear DiffusionMatrixBabyBear as a canonical map: * M_I = R^{-1} * (J + diag(D)), i.e. state[i] = R_INV * (sum + D[i]*state[i]). The R_INV factor * is the Montgomery-form artifact of the pinned library (CONFIRMED against its 16x16 matrix). */ private static void internalLayer(final long[] s) { long sum = 0; for (final long v : s) sum = BabyBear.add(sum, v); for (int i = 0; i < WIDTH; i++) { s[i] = BabyBear.mul(R_INV, BabyBear.add(sum, BabyBear.mul(s[i], INTERNAL_DIAG_M1_16[i]))); } } /** * The Poseidon2 permutation over BabyBear, width 16. CONFIRMED conformant to p3-baby-bear / * p3-poseidon2 0.4.3-succinct via a real known-answer test ({@link #available} is true). * Deterministic; a bijection. */ static long[] permute(final long[] state16) { if (state16 == null || state16.length != WIDTH) { throw new IllegalArgumentException("state must have 16 elements"); } final long[] s = new long[WIDTH]; for (int i = 0; i < WIDTH; i++) s[i] = BabyBear.reduce(state16[i]); externalLayer(s); // initial linear layer final int half = ROUNDS_F / 2; for (int r = 0; r < half; r++) { // initial external rounds for (int i = 0; i < WIDTH; i++) s[i] = BabyBear.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++) { // internal rounds s[0] = BabyBear.add(s[0], RC_INTERNAL[r]); s[0] = sbox(s[0]); internalLayer(s); } for (int r = half; r < ROUNDS_F; r++) { // terminal external rounds for (int i = 0; i < WIDTH; i++) s[i] = BabyBear.add(s[i], RC_EXTERNAL[r][i]); for (int i = 0; i < WIDTH; i++) s[i] = sbox(s[i]); externalLayer(s); } return s; } /** * 2-to-1 truncated-permutation compression (width 16 -> 8): permute l||r and take the first 8 * lanes. This is the COMMON Plonky3 TruncatedPermutation shape used for Merkle node compression * (component (c)); the EXACT SP1 convention (padding/rate, which 8 lanes) is [VERIFY]. The * underlying permutation is confirmed; the Merkle/MMCS wiring that would call this is still * un-ported, so it is unused on the (fail-closed) verify path. */ static long[] compress2to1(final long[] left8, final long[] right8) { if (left8 == null || right8 == null || left8.length != 8 || right8.length != 8) { throw new IllegalArgumentException("compress inputs must be length 8"); } final long[] in = new long[WIDTH]; System.arraycopy(left8, 0, in, 0, 8); System.arraycopy(right8, 0, in, 8, 8); final long[] out = permute(in); final long[] digest = new long[8]; System.arraycopy(out, 0, digest, 0, 8); return digest; } } /** * The Mixed Matrix Commitment Scheme (MMCS) over BabyBear: the Plonky3 {@code FieldMerkleTreeMmcs} * vector commitment FRI (component (d)) and the trace commitment are built on. Component (c), port * spec section 4. * *

CONFIRMED conformant (2026-07-19), SO {@link #available} IS true. The construction is * the exact SP1 inner config, verbatim from {@code p3-merkle-tree}'s own {@code mmcs.rs} tests: * {@code PaddingFreeSponge} as the leaf hasher, {@code * TruncatedPermutation} as the 2-to-1 node compressor (the {@link * Poseidon2Bb#compress2to1} already used here), and {@code * FieldMerkleTreeMmcs} so a digest is 8 BabyBear elements * (32 bytes). The root/opening/verify outputs reproduce real known-answer vectors emitted by * executing the pinned {@code p3-merkle-tree} / {@code p3-symmetric} / {@code p3-commit} * 0.4.3-succinct crates (checksums matched the repo Cargo.lock); the extractor's {@code * permute([0;16])} equals the confirmed Poseidon2 "zeros" vector, proving it uses the same confirmed * permutation. Cross-checked byte-for-byte against the Python/Node/standalone-Java references in * pqc-fork/pq-stark (mmcs_babybear_reference.{py,mjs}, MmcsBabyBearSelfTest.java; harness * test_mmcs_babybear.py, PASS=300 FAIL=0). See docs/AERE-STARK-VERIFIER-PORT-SPEC.md section 4 and * pqc-fork/pq-stark/spec-mmcs-babybear.md. * *

This does NOT make the precompile verify anything. The MMCS being confirmed is necessary * but not sufficient: the SP1 recursion-AIR ({@link StarkConstraints}, component (e)) is un-ported * (the duplex-sponge challenger (f) and the FRI fold+opening (d) are now confirmed), so {@link * StarkConstraints#evaluateAtZeta} returns UNAVAILABLE at stage 4 (before the query loop) and the * top-level verifier still returns EMPTY for every input. ACCEPT stays unreachable. DO NOT ACTIVATE * (see the class banner). */ static final class Mmcs { /** * CONFORMANCE GATE for the MMCS commitment/opening. Now true: the construction + outputs reproduce * real p3-merkle-tree / p3-symmetric / p3-commit 0.4.3-succinct known-answer vectors. This flags * ONLY that the commitment scheme is confirmed conformant; it does NOT port the sponge challenger, * the FRI fold arithmetic, or the AIR, so the top-level precompile stays fail-closed. */ static final boolean available = true; static final int WIDTH = Poseidon2Bb.WIDTH; // 16 static final int RATE = 8; // PaddingFreeSponge rate static final int OUT = 8; // digest size in field elements static final int DIGEST_ELEMS = 8; private static final long[] DEFAULT_DIGEST = new long[DIGEST_ELEMS]; // the zero (padding) digest private Mmcs() {} /** * PaddingFreeSponge<Perm, 16, 8, 8> leaf hasher (p3-symmetric sponge.rs): overwrite-mode, * padding-free. State starts all-zero; for each chunk of RATE elements, OVERWRITE the first * chunk-length lanes (leaving the rest at their prior value) and permute; the digest is the first * OUT lanes. An empty input yields the all-zero digest with no permutation. */ static long[] hashIter(final long[] elems) { final long[] state = new long[WIDTH]; // all-zero 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] = BabyBear.reduce(elems[j]); final long[] permuted = Poseidon2Bb.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; } /** TruncatedPermutation<Perm, 2, 8, 16> 2-to-1 node compression (the confirmed Poseidon2). */ static long[] compress2to1(final long[] left8, final long[] right8) { return Poseidon2Bb.compress2to1(left8, right8); } /** log2_ceil_usize: smallest k with 2^k >= n (0 for n <= 1). Matches p3-util. */ static int log2Ceil(final int n) { if (n <= 1) return 0; return 32 - Integer.numberOfLeadingZeros(n - 1); } private static int nextPow2(final int n) { if (n <= 1) return 1; return Integer.highestOneBit(n - 1) << 1; } /** * FieldMerkleTreeMmcs::verify_batch (p3-merkle-tree mmcs.rs): recompute the commitment root from * the opened leaf rows and the sibling path, and compare to {@code commit}. Returns true on match, * false otherwise; never throws for a well-formed but wrong opening. Handles the mixed-height MMCS * layout: opened rows are grouped by padded height (tallest first), the running root is seeded with * the tallest group's hashed rows, then the proof is walked, ordering (root, sibling) by the index * parity, compressing, and (when the next group's padded height is reached) compressing in that * group's hashed rows. * *

{@code dims[i] = {width, height}} in the SAME order as {@code opened[i]} (original matrix * order). This is the single method the on-chain verifier needs from the MMCS (the tree build and * open_batch are prover-side and live only in the offline references). It is CONFIRMED conformant * but is UNUSED on the (fail-closed) verify path in this build, since the commit-phase roots and * per-query openings are not parsed and the FRI/AIR/challenger above it are un-ported. */ static boolean verifyBatch( final long[] commit, final int[][] dims, final int index, final long[][] opened, final long[][] proof) { 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); // stable, tallest first 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(concatGroup(order, heights, ptr, currHeightPadded, opened)); int idx = index; for (final long[] sibling : proof) { 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) { final long[] nxt = hashIter(concatGroup(order, heights, ptr, currHeightPadded, opened)); root = compress2to1(root, nxt); } } if (root.length != commit.length) return false; for (int i = 0; i < root.length; i++) { if (BabyBear.reduce(root[i]) != BabyBear.reduce(commit[i])) return false; } return true; } /** Concatenate the opened rows of every matrix whose padded height matches; advances {@code ptr}. */ private static long[] concatGroup( final Integer[] order, final int[] heights, final int[] ptr, final int padded, final long[][] opened) { final java.util.List buf = new java.util.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; } } /** * Fiat-Shamir DUPLEX CHALLENGER over the confirmed Poseidon2 (component (f), port spec section 7): * {@code DuplexChallenger} plus the {@code GrindingChallenger} * proof-of-work check. IMPLEMENTED and CONFORMANCE-CONFIRMED against the pinned Plonky3 * {@code p3-challenger} 0.4.3-succinct ({@code duplex_challenger.rs} / {@code grinding_challenger.rs}) * and the {@code p3-fri} transcript order ({@code verify_shape_and_sample_challenges}) via a real * known-answer test: a scripted observe/sample transcript reproduces every sampled base + F_{p^4} * value, the full 16-lane sponge state, and a {@code check_witness} accept/reject table byte-for-byte; * and the FRI folding challenges (betas) + query indices + grinding acceptance are DERIVED from the * transcript and match BOTH the pinned p3-fri challenger AND the confirmed FRI ground truth, which, * fed into the confirmed {@link Fri#verifyQuery}, accept the real FRI proof and reject a tampered * beta (transcript -> betas/indices -> FRI verify, closing the loop the FRI KAT left open). See * pqc-fork/pq-stark/challenger_reference.{py,mjs}, ChallengerSelfTest.java, harness test_challenger.py * (PASS=44 FAIL=0), and pqc-fork/results/kat-results-challenger.json. * *

{@link #spongePorted} is now true, but the top level stays FAIL-CLOSED. Component (f) * being confirmed is necessary but NOT sufficient: the SP1 recursion-AIR constraint evaluation * (component (e), {@link StarkConstraints#evaluateAtZeta}) is un-ported and returns UNAVAILABLE, which * the driver checks at stage 4 (BEFORE any query loop and BEFORE the single {@code return ACCEPT}). * So {@link #verify} returns UNAVAILABLE for every input and {@link #computePrecompile} returns EMPTY. * ACCEPT is unreachable by construction. Also the WireReader proof-body parser still returns empties, * so the byte-facing driver adapters below cannot be DRIVEN on a real proof (the confirmed conformance * is at the field-element API, exercised by the offline KAT). DO NOT ACTIVATE (see the class banner). */ static final class Challenger { /** * CONFORMANCE flag for the DUPLEX SPONGE / Fiat-Shamir transcript (component (f)). Now true: the * observe / sample / sample_bits / check_witness operations reproduce real p3-challenger * 0.4.3-succinct ground truth (and close the FRI betas/indices loop). This flags ONLY that the * challenger is confirmed conformant; it does NOT port the SP1 recursion-AIR (component (e), * {@link StarkConstraints} = UNAVAILABLE), so the top-level precompile stays fail-closed. */ static final boolean spongePorted = true; static final int WIDTH = Poseidon2Bb.WIDTH; // 16 static final int RATE = 8; // DuplexChallenger RATE (absorb width + squeeze width). CONFIRMED. // The duplex sponge state (F::default() == 0), and the input/output rate buffers (Vec). private final long[] spongeState = new long[WIDTH]; private final java.util.List inputBuffer = new java.util.ArrayList<>(); private final java.util.List outputBuffer = new java.util.ArrayList<>(); static Challenger newDuplex() { return new Challenger(); } /** duplexing(): overwrite the first inputBuffer.len() lanes, permute, squeeze the first RATE lanes. */ private void duplexing() { for (int i = 0; i < inputBuffer.size(); i++) spongeState[i] = BabyBear.reduce(inputBuffer.get(i)); inputBuffer.clear(); final long[] permuted = Poseidon2Bb.permute(spongeState); System.arraycopy(permuted, 0, spongeState, 0, WIDTH); outputBuffer.clear(); for (int i = 0; i < RATE; i++) outputBuffer.add(spongeState[i]); } // ---- CONFIRMED field-element API (the real component (f), matched byte-for-byte by the KAT) ---- /** observe one BabyBear field element (CanObserve: overwrite-mode absorb; flush at RATE). */ void observeField(final long value) { outputBuffer.clear(); // any buffered output is now invalid inputBuffer.add(BabyBear.reduce(value)); if (inputBuffer.size() == RATE) duplexing(); } /** observe a slice / digest of field elements in order. */ void observeFields(final long[] felts) { for (final long v : felts) observeField(v); } /** sample one base field element (CanSample: duplex if needed, then pop the LAST output). */ long sampleBaseField() { if (!inputBuffer.isEmpty() || outputBuffer.isEmpty()) duplexing(); return outputBuffer.remove(outputBuffer.size() - 1); // Vec::pop -> last element } /** sample one F_{p^4} element = [c0,c1,c2,c3] = 4 sequential base samples (sample_ext_element). */ long[] sampleExtField() { return new long[] {sampleBaseField(), sampleBaseField(), sampleBaseField(), sampleBaseField()}; } /** CanSampleBits::sample_bits: low {@code bits} bits of a base sample's canonical rep. */ int sampleBits(final int bits) { return FriQueryIndex.sampleBits(sampleBaseField(), bits); } /** GrindingChallenger::check_witness: observe(witness) then sample_bits(bits) == 0. */ boolean checkWitnessField(final int bits, final long witness) { observeField(witness); return sampleBits(bits) == 0; } // ---- byte-facing adapters for the (fail-closed) driver ------------------------------------- // The BabyBear serialization used here (4-byte little-endian canonical words) is [VERIFY] against // the frozen SP1 v6.1.0 ShardProof format; this path is NOT the conformance-confirmed one (the // confirmed conformance is at the field-element API above, proven by the offline KAT). Because the // WireReader proof body returns empties, only the vkey digest + public values are absorbed on a real // input; the driver then returns UNAVAILABLE at the AIR stage (component (e)) and the precompile // returns EMPTY. These adapters can be DRIVEN end-to-end only once WireReader parses the proof body. void observe(final Bytes b) { observeFields(decodeFields(b)); } boolean checkProofOfWork(final Bytes witness, final int powBits) { // The grinding witness lives in the (un-ported) proof body; WireReader returns empty here, so // there is no field element to observe and grinding cannot be confirmed -> fail-closed (REJECT). if (witness == null || witness.size() < 4) return false; return checkWitnessField(powBits, decodeField(witness, 0)); } int[] sampleQueryIndices(final int numQueries, final int logDomain) { // Faithful sample_bits loop. UNREACHED in this build: the driver returns UNAVAILABLE at the AIR // stage (StarkConstraints.evaluateAtZeta, component (e)) BEFORE this is called. logDomain comes // from the un-ported WireReader (0 here), so this returns empty (fail-closed) until the proof body // is parsed; a completed port supplies a real logDomain and this samples the indices directly. if (logDomain <= 0 || logDomain >= 31) return new int[0]; final int[] out = new int[numQueries]; for (int q = 0; q < numQueries; q++) out[q] = sampleBits(logDomain); return out; } private static long[] decodeFields(final Bytes b) { if (b == null) return new long[0]; final int words = b.size() / 4; final long[] out = new long[words]; for (int i = 0; i < words; i++) out[i] = decodeField(b, i * 4); return out; } private static long decodeField(final Bytes b, final int off) { final long w = (b.get(off) & 0xffL) | ((b.get(off + 1) & 0xffL) << 8) | ((b.get(off + 2) & 0xffL) << 16) | ((b.get(off + 3) & 0xffL) << 24); return BabyBear.reduce(w); } } /** * AIR constraint evaluation + quotient consistency at the DEEP point zeta (component (e), port spec * section 6). TWO distinct things live here, and the distinction IS the fail-closed gate: * *

(1) The GENERIC quotient-consistency MECHANISM ({@link #checkGenericQuotient}) that any * p3-uni-stark STARK verifier runs AFTER the PCS opening argument (p3-uni-stark verifier.rs lines * 90-141): given the out-of-domain trace openings at zeta ({@code traceLocal}) and at g*zeta * ({@code traceNext}), the random challenge alpha, the quotient-chunk openings, and the trace-domain * degree, it (a) folds the AIR's constraints with alpha into a single value (Horner: * {@code acc = acc*alpha + constraint}, matching the VerifierConstraintFolder in p3-air folder.rs), * (b) reconstructs quotient(zeta) from the chunk openings and the split-domain normalization {@code * zps} (p3-commit domain.rs), (c) computes the vanishing polynomial Z_H(zeta) and the * first/last/transition selectors ({@code selectors_at_point}), and (d) checks the identity * {@code folded_constraints(zeta) == Z_H(zeta) * quotient(zeta)}, all over F_{p^4}. This mechanism is * IMPLEMENTED and CONFORMANCE-CONFIRMED against the pinned Plonky3 p3-uni-stark 0.4.3-succinct via a * real known-answer test on two KNOWN example AIRs (the Fibonacci AIR from Plonky3's own * tests/fib_air.rs, single quotient chunk; and a degree-3 multiply AIR matching tests/mul_air.rs, TWO * quotient chunks, exercising the zps product): the pinned p3-uni-stark PROVER+VERIFIER accept the * proofs and this mechanism reproduces the accept + rejects a tampered trace opening / quotient chunk / * alpha (pq-stark/airquotient-extractor, test_air_quotient.py PASS=18, plus Python/Node/standalone-Java * references). So {@link #GENERIC_QUOTIENT_CHECK_CONFIRMED} is true. It is exercised ONLY for a SUPPLIED * example AIR under the KAT; it is NOT reachable on the real verify path (see below). * *

(2) The SPECIFIC SP1 RECURSION AIR (its exact multi-thousand-constraint set, * interactions/permutation argument, and public-value layout) and the verifying-key digest that commits * to it. That is a large, program-specific, multi-week port needing the SP1 toolchain, and is NOT done: * {@link #SP1_RECURSION_AIR_PORTED} is false. Because it is false, {@link #evaluateAtZeta} (the REAL * verify path) returns UNAVAILABLE, which the driver checks at stage 4 (BEFORE the query loop and BEFORE * the single {@code return ACCEPT}). So the generic mechanism being confirmed does NOT make the * precompile verify a real SP1 proof: {@link #computePrecompile} returns EMPTY for every input and * ACCEPT is unreachable by construction. DO NOT ACTIVATE (see the class banner). */ static final class StarkConstraints { static final int OK = 1; static final int FAIL = 0; static final int UNAVAILABLE = -1; /** * The GENERIC quotient-consistency mechanism ({@link #checkGenericQuotient}) is CONFIRMED conformant * to the pinned p3-uni-stark 0.4.3-succinct via a real KAT on two known example AIRs. This flags ONLY * that the generic mechanism is confirmed; it does NOT port the SP1-recursion-specific AIR, so the * top-level precompile stays fail-closed (see {@link #SP1_RECURSION_AIR_PORTED}). */ static final boolean GENERIC_QUOTIENT_CHECK_CONFIRMED = true; /** * THE FAIL-CLOSED GATE. The SP1-recursion-specific AIR (its exact constraint set + interactions + * public-value layout) and the verifying-key digest that binds it are NOT ported (a multi-week piece * needing the SP1 toolchain, port spec sections 6 and 9). While this is false, {@link #evaluateAtZeta} * returns UNAVAILABLE on the real path and ACCEPT is unreachable. Flipping it to true is FORBIDDEN * until the SP1 recursion AIR + vkey binding are ported, a real end-to-end SP1-proof KAT is green, * and the whole verifier is externally audited and founder-activated. */ static final boolean SP1_RECURSION_AIR_PORTED = false; private StarkConstraints() {} static int evaluateAtZeta(final WireReader r, final Challenger ch) { // REAL verify path (component (e), the SP1 recursion AIR). The GENERIC quotient MECHANISM below is // confirmed, but the SP1-recursion-specific constraint set + the vkey digest that commits to it are // ABSENT (SP1_RECURSION_AIR_PORTED == false), and WireReader does not parse the proof body, so we // can neither evaluate the real recursion AIR's constraints nor bind its vkey. Fail-closed. if (!SP1_RECURSION_AIR_PORTED) { return UNAVAILABLE; // -> EMPTY, fail-closed (SP1 recursion AIR + vkey binding un-ported) } // Unreachable while the gate is false. A completed port would, on the real path: parse // opened_values (WireReader), derive alpha/zeta from the transcript (ch), evaluate the SP1 recursion // AIR + interactions, bind the vkey digest + public values, and finally call checkGenericQuotient // with the real AIR's folded constraints. Kept explicit so the gate is auditable. return UNAVAILABLE; } // ===== the GENERIC quotient-consistency mechanism (CONFIRMED vs p3-uni-stark; exercised under KAT) = // Elements of F_{p^4} are BabyBearExt4 (component (a)). Openings arrive as canonical long[4] limbs. private static BabyBearExt4 ext(final long[] v) { return BabyBearExt4.of(v[0], v[1], v[2], v[3]); } /** x^(2^logN) in F_{p^4} by repeated squaring (Field::exp_power_of_2). */ private static BabyBearExt4 extExpPow2(final BabyBearExt4 x, final int logN) { BabyBearExt4 r = x; for (int k = 0; k < logN; k++) r = r.mul(r); return r; } /** Z_D(point) for a coset of size 2^logN with (base-field) shift: (point*shift^-1)^(2^logN) - 1. */ private static BabyBearExt4 zpAtPoint(final int logN, final long shiftBase, final BabyBearExt4 point) { final BabyBearExt4 shiftInv = BabyBearExt4.fromBase(BabyBear.inv(shiftBase)); return extExpPow2(point.mul(shiftInv), logN).sub(BabyBearExt4.fromBase(1)); } /** domain.rs selectors_at_point for the trace domain (shift=1, log_n=degreeBits). Returns * {is_first_row, is_last_row, is_transition, inv_zeroifier}. */ static BabyBearExt4[] selectorsAtPoint(final int degreeBits, final BabyBearExt4 zeta) { final long g = BabyBear.twoAdicGenerator(degreeBits); final BabyBearExt4 gInv = BabyBearExt4.fromBase(BabyBear.inv(g)); final BabyBearExt4 one = BabyBearExt4.fromBase(1); final BabyBearExt4 zH = extExpPow2(zeta, degreeBits).sub(one); final BabyBearExt4 isFirst = zH.mul(zeta.sub(one).inv()); final BabyBearExt4 isLast = zH.mul(zeta.sub(gInv).inv()); final BabyBearExt4 isTransition = zeta.sub(gInv); final BabyBearExt4 invZeroifier = zH.inv(); return new BabyBearExt4[] {isFirst, isLast, isTransition, invZeroifier}; } private static BabyBearExt4 monomial(final int e) { final long[] m = new long[4]; m[e] = 1; return BabyBearExt4.of(m[0], m[1], m[2], m[3]); } /** Reconstruct quotient(zeta) from the chunk openings (verifier.rs lines 90-116). */ static BabyBearExt4 reconstructQuotient( final int degreeBits, final long[][][] chunks, final BabyBearExt4 zeta) { final int quotientDegree = chunks.length; final int logQuotientDegree = Integer.numberOfTrailingZeros(quotientDegree); final int logNq = degreeBits + logQuotientDegree; final long genQ = BabyBear.twoAdicGenerator(logNq); final long[] shifts = new long[quotientDegree]; for (int i = 0; i < quotientDegree; i++) shifts[i] = BabyBear.mul(BabyBear.GENERATOR, BabyBear.pow(genQ, i)); final BabyBearExt4[] zps = new BabyBearExt4[quotientDegree]; for (int i = 0; i < quotientDegree; i++) { BabyBearExt4 acc = BabyBearExt4.fromBase(1); final BabyBearExt4 firstPointI = BabyBearExt4.fromBase(shifts[i]); for (int j = 0; j < quotientDegree; j++) { if (j == i) continue; final BabyBearExt4 num = zpAtPoint(degreeBits, shifts[j], zeta); final BabyBearExt4 den = zpAtPoint(degreeBits, shifts[j], firstPointI); acc = acc.mul(num.mul(den.inv())); } zps[i] = acc; } BabyBearExt4 quotient = BabyBearExt4.of(0, 0, 0, 0); for (int chI = 0; chI < chunks.length; chI++) { for (int eI = 0; eI < chunks[chI].length; eI++) { quotient = quotient.add(zps[chI].mul(monomial(eI)).mul(ext(chunks[chI][eI]))); } } return quotient; } /** VerifierConstraintFolder Horner fold: acc = acc*alpha + constraint (p3-air folder.rs). */ private static BabyBearExt4 horner(final BabyBearExt4[] constraints, final BabyBearExt4 alpha) { BabyBearExt4 acc = BabyBearExt4.of(0, 0, 0, 0); for (final BabyBearExt4 c : constraints) acc = acc.mul(alpha).add(c); return acc; } /** tests/fib_air.rs eval(): columns [left,right], public values [a,b,x]. */ private static BabyBearExt4 foldFibonacci( final long[][] tl, final long[][] tn, final long[] pis, final BabyBearExt4[] sel, final BabyBearExt4 alpha) { final BabyBearExt4 a = BabyBearExt4.fromBase(pis[0]); final BabyBearExt4 b = BabyBearExt4.fromBase(pis[1]); final BabyBearExt4 x = BabyBearExt4.fromBase(pis[2]); final BabyBearExt4 left = ext(tl[0]); final BabyBearExt4 right = ext(tl[1]); final BabyBearExt4 nleft = ext(tn[0]); final BabyBearExt4 nright = ext(tn[1]); final BabyBearExt4 c1 = sel[0].mul(left.sub(a)); final BabyBearExt4 c2 = sel[0].mul(right.sub(b)); final BabyBearExt4 c3 = sel[2].mul(right.sub(nleft)); final BabyBearExt4 c4 = sel[2].mul(left.add(right).sub(nright)); final BabyBearExt4 c5 = sel[1].mul(right.sub(x)); return horner(new BabyBearExt4[] {c1, c2, c3, c4, c5}, alpha); } /** tests/mul_air.rs eval() (REPETITIONS=1, degree=3): columns [a,b,c]. */ private static BabyBearExt4 foldMulDeg3( final long[][] tl, final long[][] tn, final long[] pis, final BabyBearExt4[] sel, final BabyBearExt4 alpha) { final BabyBearExt4 a = ext(tl[0]); final BabyBearExt4 b = ext(tl[1]); final BabyBearExt4 c = ext(tl[2]); final BabyBearExt4 nextA = ext(tn[0]); final BabyBearExt4 one = BabyBearExt4.fromBase(1); final BabyBearExt4 c1 = a.mul(a).mul(b).sub(c); final BabyBearExt4 c2 = sel[0].mul(a.mul(a).add(one).sub(b)); final BabyBearExt4 c3 = sel[2].mul(a.add(one).sub(nextA)); return horner(new BabyBearExt4[] {c1, c2, c3}, alpha); } private static boolean extEq(final BabyBearExt4 a, final BabyBearExt4 b) { for (int i = 0; i < 4; i++) if (a.c[i] != b.c[i]) return false; return true; } /** * The GENERIC quotient-consistency check for a SUPPLIED example AIR: returns true iff * folded_constraints(zeta) * inv_zeroifier == quotient(zeta), reproducing p3-uni-stark verify()'s * OodEvaluationMismatch gate. CONFORMANCE-CONFIRMED (see the class doc). This is NOT on the real * verify path: {@link #evaluateAtZeta} is fail-closed while {@link #SP1_RECURSION_AIR_PORTED} is * false, so this method is reachable only from the KAT (with a supplied {@code airId}). */ static boolean checkGenericQuotient( final int degreeBits, final long[] alpha, final long[] zeta, final long[] pis, final long[][] traceLocal, final long[][] traceNext, final long[][][] quotientChunks, final String airId) { final BabyBearExt4 zetaE = ext(zeta); final BabyBearExt4 alphaE = ext(alpha); final BabyBearExt4[] sel = selectorsAtPoint(degreeBits, zetaE); final BabyBearExt4 quotient = reconstructQuotient(degreeBits, quotientChunks, zetaE); final BabyBearExt4 folded; if ("fibonacci".equals(airId)) { folded = foldFibonacci(traceLocal, traceNext, pis, sel, alphaE); } else if ("mul_deg3".equals(airId)) { folded = foldMulDeg3(traceLocal, traceNext, pis, sel, alphaE); } else { return false; // unknown supplied AIR } return extEq(folded.mul(sel[3]), quotient); } } /** * FRI query-index derivation: the one constant-free slice of FRI component (d) that is REAL, * unit-tested, and independently cross-checked (Python + Node + Java) in this build. It turns a * Fiat-Shamir challenger sample into a query index ({@link #sampleBits}) and produces the * per-layer folding-index walk each query follows ({@link #walk}). It depends on NO secret round * constants (unlike Poseidon2), so it can be validated offline against hand-computed golden * vectors and structural invariants. * *

Tests and references (see docs/AERE-STARK-VERIFIER-PORT-SPEC.md section 8): * pqc-fork/pq-stark/test_fri_query_index.py (harness), * pqc-fork/pq-stark/fri_query_index_reference.py / .mjs (independent references), * pqc-fork/pq-stark/FriQueryIndexSelfTest.java (a verbatim copy of this logic, compiled and run * without the Besu classpath). Result recorded in pqc-fork/results/kat-results-fri-query-index.json. * *

This does NOT make the precompile verify anything. Deriving indices is only the query * bookkeeping; the fold ARITHMETIC and the Merkle openings under Poseidon2 are still delegated * (return UNAVAILABLE), so {@link Fri#checkQuery} still returns UNAVAILABLE and the top-level * verifier stays fail-closed. Conformance of the derived indices to a real SP1 v6.1.0 trace is * [MEASURE] (it needs the Poseidon2 challenger + an exported proof). */ static final class FriQueryIndex { private FriQueryIndex() {} /** * Reduce a sampled BabyBear field element (its canonical u32 representative in [0, p)) to a * query index in [0, 2^bits) by taking the low {@code bits} bits. Mirrors Plonky3 * CanSampleBits::sample_bits for a DuplexChallenger over BabyBear. * *

[VERIFY] Plonky3 masks the LOW bits of as_canonical_u32(); confirm LSB (not MSB) against * p3-challenger. {@code bits} must be < 31 so a single 31-bit sample supplies them. */ static int sampleBits(final long canonicalU32, final int bits) { if (bits < 0 || bits >= 31) { throw new IllegalArgumentException("bits must be in [0,31): one BabyBear sample carries ~31 bits"); } if (canonicalU32 < 0 || canonicalU32 >= BabyBear.P) { throw new IllegalArgumentException("canonicalU32 must be canonical in [0, p)"); } return (int) (canonicalU32 & ((1L << bits) - 1L)); } /** * The per-query folding-index walk. Row r = {layer, index, sibling = index^1, index_pair = * index>>1, parity = index&1}. num_fold_rounds = logMaxHeight - logFinalPolyLen. This * is the index bookkeeping a FRI verifier follows to know, at each layer, which sibling pair to * open and which folded value is its own. It performs NO fold arithmetic and NO Merkle opening. */ static int[][] walk(final int index, final int logMaxHeight, final int logFinalPolyLen) { if (logMaxHeight < 0 || logFinalPolyLen < 0 || logFinalPolyLen > logMaxHeight) { throw new IllegalArgumentException("bad log heights"); } if (index < 0 || index >= (1 << logMaxHeight)) { throw new IllegalArgumentException("index out of range for log_max_height"); } final int rounds = logMaxHeight - logFinalPolyLen; final int[][] out = new int[rounds][5]; int cur = index; for (int layer = 0; layer < rounds; layer++) { out[layer][0] = layer; out[layer][1] = cur; out[layer][2] = cur ^ 1; out[layer][3] = cur >> 1; out[layer][4] = cur & 1; cur = cur >> 1; } return out; } /** Terminal index after all folds; must land in the final-poly domain [0, 2^logFinalPolyLen). */ static int finalIndex(final int index, final int logMaxHeight, final int logFinalPolyLen) { return index >> (logMaxHeight - logFinalPolyLen); } } /** * FRI query-consistency + Merkle-opening check for one sampled index (component (d), port spec * section 5). The FOLD + OPENING relation ({@link #verifyQuery}) is now IMPLEMENTED and * CONFORMANCE-CONFIRMED against the pinned Plonky3 p3-fri 0.4.3-succinct via a real known-answer test * (pq-stark/test_fri_verify.py: real FRI proofs emitted by the pinned p3-fri PROVER are re-verified * byte-for-byte here, ACCEPT for genuine + REJECT for tampered openings / folds / final polys; * cross-checked byte-identically across Python, Node, and standalone Java). * *

{@link #checkQuery} stays gated / UNAVAILABLE and the top level stays FAIL-CLOSED. The * fold+opening relation takes the folding challenges (betas), the query index, and the reduced * openings as INPUTS. The duplex-sponge Fiat-Shamir transcript that DERIVES the betas / query indices * in-circuit (component (f), {@link Challenger#spongePorted} = true) is now confirmed, and closing that * loop into {@link #verifyQuery} is KAT-proven offline (pq-stark/test_challenger.py). But the SP1 * recursion-AIR reduced-opening combination (component (e), {@link StarkConstraints} = UNAVAILABLE) is * still un-ported, and the commit-phase roots and per-query openings are not parsed in this build * ({@link WireReader} returns empties). So {@link #verifyQuery} cannot be driven on a real proof, * {@link #checkQuery} returns UNAVAILABLE, and {@link #computePrecompile} returns EMPTY for every input. * ACCEPT is unreachable. */ static final class Fri { static final int OK = 1; static final int FAIL = 0; static final int UNAVAILABLE = -1; /** * CONFORMANCE flag for the FRI fold+opening relation ({@link #verifyQuery}). Now true: it * reproduces real p3-fri 0.4.3-succinct proofs (accept genuine + reject tampered) in the offline * references/KAT. This flags ONLY that the fold+opening relation is confirmed; it does NOT port the * duplex-sponge challenger (component (f)) that derives the betas / query indices / reduced * openings, so {@link #checkQuery} stays gated and the top level stays fail-closed. */ static final boolean foldRelationConfirmed = true; /** Reverse the low {@code bitLen} bits of x (p3-util reverse_bits_len); maps a query index to its * coset-point exponent. */ 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; } /** * Reproduce Plonky3 p3-fri {@code verifier::verify_query} for one query: walk the arity-2 folding, * checking (1) each layer's commit-phase MMCS opening of the sibling pair ({@link Mmcs#verifyBatch}, * component (c), CONFIRMED) and (2) the fold interpolation in F_{p^4} * ({@link BabyBearExt4}, component (a)) layer to layer, down to a single constant. Returns the * folded value (4 canonical longs) on success, or {@code null} if a commit-phase MMCS opening fails * (the caller compares the return against the committed final polynomial). CONFORMANCE-CONFIRMED * against p3-fri 0.4.3-succinct. * *

Inputs: {@code commits} = commit-phase MMCS roots (one per fold layer, each 8 BabyBear); * {@code betas} = folding challenges (one per layer, each F_{p^4} as 4 longs); {@code index} in * [0, 2^logMaxHeight); {@code reducedOpenings[h]} = the reduced opening injected before the fold at * log_folded_height h-1 (F_{p^4}); {@code siblingValues[layer]} = the opened sibling value (F_{p^4}); * {@code openingProofs[layer]} = that layer's MMCS sibling path. The betas / index / reducedOpenings * come from the transcript (component (f)) and the AIR (component (e)), which are UN-PORTED, so this * method is exercised only by the offline conformance KAT, NOT on any real precompile verify path. * *

The commit-phase MMCS is ExtensionMmcs over the base FieldMerkleTreeMmcs: a leaf is a PAIR of * F_{p^4} evals flattened to 8 BabyBear coords (each element's 4 base coords, concatenated), a single * width-8 row of a height-2^log_folded_height tree. */ static long[] verifyQuery( final int logBlowup, final int logMaxHeight, final long[][] commits, final long[][] betas, final int index, final long[][] reducedOpenings, final long[][] siblingValues, final long[][][] openingProofs) { BabyBearExt4 folded = BabyBearExt4.of(0, 0, 0, 0); final long g = BabyBear.twoAdicGenerator(logMaxHeight); BabyBearExt4 x = BabyBearExt4.fromBase(BabyBear.pow(g, reverseBitsLen(index, logMaxHeight))); final BabyBearExt4 gen1 = BabyBearExt4.fromBase(BabyBear.twoAdicGenerator(1)); // order-2 root = -1 int idx = index; final int numLayers = logMaxHeight - logBlowup; for (int layer = 0; layer < numLayers; layer++) { final int lfh = logMaxHeight - 1 - layer; final long[] ro = reducedOpenings[lfh + 1]; folded = folded.add(BabyBearExt4.of(ro[0], ro[1], ro[2], ro[3])); final int indexSibling = idx ^ 1; final int indexPair = idx >> 1; final long[] fe = {folded.c[0], folded.c[1], folded.c[2], folded.c[3]}; final long[][] evals = {fe.clone(), fe.clone()}; evals[indexSibling % 2] = siblingValues[layer].clone(); final long[] row = new long[8]; // ExtensionMmcs flatten: two F_{p^4} evals -> 8 base coords System.arraycopy(evals[0], 0, row, 0, 4); System.arraycopy(evals[1], 0, row, 4, 4); final int height = 1 << lfh; if (!Mmcs.verifyBatch( commits[layer], new int[][] {{8, height}}, indexPair, new long[][] {row}, openingProofs[layer])) { return null; // commit-phase MMCS opening failed -> reject } final BabyBearExt4 e0 = BabyBearExt4.of(evals[0][0], evals[0][1], evals[0][2], evals[0][3]); final BabyBearExt4 e1 = BabyBearExt4.of(evals[1][0], evals[1][1], evals[1][2], evals[1][3]); final BabyBearExt4 xSib = x.mul(gen1); final BabyBearExt4 x0 = (indexSibling % 2 == 1) ? x : xSib; final BabyBearExt4 x1 = (indexSibling % 2 == 1) ? xSib : x; final BabyBearExt4 beta = BabyBearExt4.of(betas[layer][0], betas[layer][1], betas[layer][2], betas[layer][3]); // folded = e0 + (beta - x0) * (e1 - e0) / (x1 - x0) final BabyBearExt4 num = beta.sub(x0).mul(e1.sub(e0)); final BabyBearExt4 den = x1.sub(x0); folded = e0.add(num.mul(den.inv())); idx = indexPair; x = x.mul(x); } return new long[] {folded.c[0], folded.c[1], folded.c[2], folded.c[3]}; } static int checkQuery(final WireReader r, final int index, final Challenger ch) { // Index derivation is REAL (FriQueryIndex, tested). Derive the fold walk for this query so the // tested helper is genuinely on the verify path; guarded so it never throws (never fault). final int logMaxHeight = r.logMaxDomain(); if (logMaxHeight > 0 && index >= 0 && index < (1 << logMaxHeight)) { final int[][] foldWalk = FriQueryIndex.walk(index, logMaxHeight, /*logFinalPolyLen*/ 0); // foldWalk gives (index, sibling, index_pair, parity) per layer. The per-layer Merkle opening // (Mmcs.verifyBatch, component (c)) AND the fold interpolation (verifyQuery, component (d)) are // now REAL and CONFORMANCE-CONFIRMED against p3-fri 0.4.3-succinct, and the duplex challenger // that DERIVES the betas / query indices (component (f), Challenger.spongePorted=true) is now // confirmed too (the transcript->betas/indices->verifyQuery loop is KAT-proven offline). But this // query CANNOT be driven here: the commit-phase roots and per-query openings are not parsed // (WireReader returns empties) and the reduced openings come from the un-ported SP1 recursion-AIR // (component (e), StarkConstraints=UNAVAILABLE). So checkQuery returns UNAVAILABLE below and the // precompile stays fail-closed. assert foldWalk.length == logMaxHeight; // structural, walks to the final poly (len 1 here) assert Mmcs.available; // MMCS opening primitive confirmed assert foldRelationConfirmed; // FRI fold+opening relation (verifyQuery) confirmed; still gated } // Port target: the duplex challenger (component (f)) that samples betas/indices, and the AIR // reduced-opening combination (component (e)); the fold arithmetic + MMCS opening (verifyQuery, // this class) are DONE and confirmed. Not driveable end-to-end -> UNAVAILABLE (fail-closed). return UNAVAILABLE; // TODO(port): challenger (f) + AIR reduced openings (e); fold+opening are done } } }