/* * 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 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 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 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 {@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 (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
}
}
}
*
*
*