aere-research/precompiles/Sp1StarkVerifierPrecompiledContract.java
Aere Network 6cb0140fae Republished from a clean root: the compiled artifact is gone from history, and the local line of work joins the sanitized public line
The public history carried kat/__pycache__/mlkem768_reference.cpython-314.pyc,
a compiled Python artifact embedding the operator's absolute local path. Text
secret scanners do not read compiled binaries, which is exactly how it slipped
through, and removing it from the tip would have left it reachable through the
old root commits. So this repository is republished from a single clean root.

This root also carries, from the previously unpublished line of work:
- corrected LICENSE year, LICENSING.md, VERIFY-POLICY.md, and
  CITATIONS-UNRESOLVED.md remeasured 2026-08-11 (101 paths, README aligned)
- O-018: run_consensus_verification.py ran 19 of 29 models and reported PASS;
  it now runs all 29, and computemarket_smt.py gains resolveByTimeout /
  reclaimUnsettled cases plus a negative control
- O-006: the word 'audited' removed from next to Bouncy Castle, twice, after a
  concurrent edit resurrected it
- O-014: prior art named and dated - Algorand's native falcon_verify shipped
  about ten months before AERE's precompiles; the primacy claim is withdrawn
  where it was implied
- bench/ scripts parametrized so they actually run for an outsider (the
  earlier textual sanitization left $STAGING unexpanded inside Python strings)
- AIP-2/AIP-3 errata with measured figures, spec remeasurements at 2026-08-01,
  and the spec-zk-stack retractions (owner is an operational key, not the
  Foundation; 'maximally sound' withdrawn; aggregator V1 deprecated)
The redacted bench-host environment files from the sanitized line are kept
exactly as published; the unredacted local variants are not carried.
2026-08-15 13:52:14 +03:00

1515 lines
77 KiB
Java

/*
* 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.
*
* <p>==================================================================================
* REFERENCE SKELETON - NOT A WORKING VERIFIER. DO NOT ACTIVATE ON MAINNET.
* ==================================================================================
*
* <p>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.
*
* <p><b>Honest status.</b> 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).
*
* <p><b>Wire contract (v1, see spec doc section 3).</b> Input is a self-describing, length-checked
* blob:
*
* <pre>
* 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
* </pre>
*
* 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).
*
* <p><b>Output.</b> 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.
*
* <p>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<BabyBear, 4>} 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].
*
* <p>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<Bytes> 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).
*
* <p><b>CONFORMANCE IS CONFIRMED (2026-07-19), SO {@link #available} IS true.</b> 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):
* <ul>
* <li>{@link #RC_EXTERNAL}/{@link #RC_INTERNAL} are the CONFIRMED Xoroshiro128Plus::seed_from_u64(1)
* / new_from_rng_128 round constants (canonical), extracted from the pinned crates;
* <li>{@link #INTERNAL_DIAG_M1_16} is CONFIRMED (canonical) and the internal layer is
* {@code M_I = R^{-1} * (J + diag(D))} with {@link #R_INV} = 943718400 (the Montgomery-form
* artifact recovered from DiffusionMatrixBabyBear's exact 16x16 canonical matrix; the earlier
* textbook {@code state[i]*D[i] + sum} was self-consistent but WRONG, the "two wrong copies
* agree" trap);
* <li>{@link #ROUNDS_F} = 8, {@link #ROUNDS_P} = 13 are CONFIRMED (poseidon2_round_numbers_128(16, 7)).
* </ul>
*
* <p><b>This does NOT make the precompile verify anything.</b> 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 -&gt; 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.
*
* <p><b>CONFIRMED conformant (2026-07-19), SO {@link #available} IS true.</b> The construction is
* the exact SP1 inner config, verbatim from {@code p3-merkle-tree}'s own {@code mmcs.rs} tests:
* {@code PaddingFreeSponge<Perm, WIDTH=16, RATE=8, OUT=8>} as the leaf hasher, {@code
* TruncatedPermutation<Perm, N=2, CHUNK=8, WIDTH=16>} as the 2-to-1 node compressor (the {@link
* Poseidon2Bb#compress2to1} already used here), and {@code
* FieldMerkleTreeMmcs<Packing, Packing, MyHash, MyCompress, 8>} 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.
*
* <p><b>This does NOT make the precompile verify anything.</b> 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&lt;Perm, 16, 8, 8&gt; 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&lt;Perm, 2, 8, 16&gt; 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 &gt;= n (0 for n &lt;= 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.
*
* <p>{@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<Long> 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<BabyBear, Perm, WIDTH=16, RATE=8>} 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 -&gt; betas/indices -&gt; 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.
*
* <p><b>{@link #spongePorted} is now true, but the top level stays FAIL-CLOSED.</b> 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<F>).
private final long[] spongeState = new long[WIDTH];
private final java.util.List<Long> inputBuffer = new java.util.ArrayList<>();
private final java.util.List<Long> 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<F>: 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<F>: 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:
*
* <p><b>(1) The GENERIC quotient-consistency MECHANISM</b> ({@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).
*
* <p><b>(2) The SPECIFIC SP1 RECURSION AIR</b> (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.
*
* <p>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.
*
* <p><b>This does NOT make the precompile verify anything.</b> 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.
*
* <p>[VERIFY] Plonky3 masks the LOW bits of as_canonical_u32(); confirm LSB (not MSB) against
* p3-challenger. {@code bits} must be &lt; 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&gt;&gt;1, parity = index&amp;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).
*
* <p><b>{@link #checkQuery} stays gated / UNAVAILABLE and the top level stays FAIL-CLOSED.</b> 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.
*
* <p>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.
*
* <p>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
}
}
}