Repository navigation
feat: move to upstream-main Plonky3 and harden the outer MultiField32 transcript - #581
Merged
Merged
Conversation
…eld32 challenger The wrap's OuterChallenger (MultiField32Challenger over BN254) packed six KoalaBear limbs per BN254 element in radix 2^32 and squeezed 64-bit chunks reduced mod p, so absorbs were not injective and each squeezed element carried less entropy than a full field element (GHSA-vj64-rjf3-w3v7). OuterHash had the same radix-2^32 packing, so two distinct leaves could hash to the same BN254 digest. The hardened challenger and sponge pack in radix 2^31, length-tag every absorb, absorb digests natively and squeeze base-p digits. This change makes the gnark-side verifier follow them: - OuterHash takes rate 2, as the sponge now requires RATE < WIDTH. - MultiField32ChallengerVariable buffers sixteen felts per absorb, adds the length tag to the capacity, absorbs commitments as BN254 words and squeezes seven base-p limbs per rate element. - The in-circuit outer hash packs with shifted radix-2^31 limbs. - A new CircuitVar2FieldOrderLimbs op returns the low base-p digits of a BN254 variable; the gnark side range-checks every digit and the high part so that the decomposition is the canonical one. Tests compare the circuit against the native challenger and OuterHash across buffer boundaries and multi-block inputs, check the limb split at its edge values, and pin that the old radix-2^32 collision no longer collides. This needs the Plonky3 fork's hardened challenger; the dependency rev is bumped once that branch is pushed. The wrap vk and the Groth16/PLONK circuit artifacts change with it.
The Plonky3 dependencies follow the fork's zkm/whir-pcs branch instead of a fixed rev; Cargo.lock pins the commit, now cb1b31dc, which carries the hardened MultiField32Challenger and MultiField32PaddingFreeSponge.
Plonky3 resolves through a [patch] to a sibling ../Plonky3 checkout of the upstream-main port while that branch is not published. - AIR builders implement PeriodicVar and is_transition. - preprocessed_width now lives on BaseAir: chip overrides, the MachineAir derive and Chip forward through it, and MachineAir drops its copy. - Constraint degree and count go through capture_symbolic_constraints, which skips the layout check against BaseAir::num_public_values. - DuplexChallengerVariable follows the length-tagged duplex absorb: unused rate slots are zeroed and the absorb length is added to the first capacity element. A recursion test checks it against the native challenger across buffer boundaries and a grind. - GrindingChallenger bounds add Clone + Sync, FRI parameters set batch_proof_of_work_bits, SepticExtension implements AlgebraIdentity, and num-bigint moves to 0.5.
BaseAir::preprocessed_width defaults to zero, and an implementor that overrides preprocessed_trace must also override the width. Add assert_preprocessed_widths_match to zkm-pcs and call it from the MIPS and recursion chip sets, so a missing or wrong override fails in a test rather than in a proof.
The fibonacci guest reads its input from stdin, but the execute and prove commands ran it with empty stdin and failed with InvalidSyscallArgs. Both now get the fixture's input through fixture_stdin, and prove goes through run_test_io.
VanhGer
reviewed
Oct 30, 2026
| self.sponge_state[..OUTER_MULTI_FIELD_CHALLENGER_RATE].iter_mut().enumerate() | ||
| { | ||
| *slot = match values.get(i) { | ||
| Some(&value) => value, |
Contributor
There was a problem hiding this comment.
Some(&value) => value reuses the caller's Var instead of creating a new one, so the sponge slot and the commitment are the same variable. The gnark Permute writes its output back into its input variables (zkm.go:176-178). So after observe_commitment, this leads the commitment holds the Poseidon2 output instead of the root, which makes the honest wrap proofs fail in the PLONK/Groth16 circuit.
| # For Git dependencies, Cargo.lock records the revisions selected during resolution. | ||
| # Enabling the local path overrides below uses checkout contents instead. Dependency | ||
| # changes can change the transcript and verifying keys. | ||
| p3-air = { git = "https://github.com/ProjectZKM/Plonky3" } |
Contributor
There was a problem hiding this comment.
should we add the latest rev for Plonky3 deps?
absorb_rate_padded_with_tag copied the caller's variables into the sponge rate, so a sponge slot and the absorbed commitment word shared a variable. The Poseidon2 permute writes its output back into the sponge's variables, so the commitment word was overwritten by the permutation output. Each rate slot now gets a fresh variable, and a regression test checks that the sponge does not alias its inputs.
VanhGer
approved these changes
Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
main(5ccd557f): upstream Plonky3mainplus the fork's remaining changes.MultiField32Challenger(GHSA-vj64-rjf3-w3v7).Changes
Outer transcript (GHSA-vj64-rjf3-w3v7)
OuterHashdigest.OuterHash: rate 2, shifted radix-2^31 limbs.MultiField32ChallengerVariable: 16 felts per absorb, a length-tagged capacity, commitments absorbed as BN254 words, 7 base-p limbs squeezed per rate element.CircuitVar2FieldOrderLimbsop; the gnark side range-checks the digits and the high part, so the split is canonical.Plonky3 migration
PeriodicVar/is_transition;preprocessed_widthmoves toBaseAir.capture_machine_constraints, which keeps thePROOF_MAX_NUM_PVSpublic-value layout.DuplexChallengerVariablefollows upstream's length-tagged absorb: unused rate slots are zeroed and the absorb length is added tostate[RATE].GrindingChallengerbounds addClone + Sync; FRI parameters setbatch_proof_of_work_bits;SepticExtensionimplementsAlgebraIdentity;num-bigintmoves to 0.5.[patch]points at../Plonky3for local work.Everything else in the transcript is bit-identical to the previous fork: permutations, sponge hashes, compression, Merkle roots and openings.
Not included
vk_map.bin/vk_map_profile.txt, the wrap vk, the Groth16/PLONK artifacts andcrates/verifier/bn254-vk/*.binwill be regenerated separately.VERIFY_VK=false.Testing
cargo clippy --workspace --all-targets -- -D warningsOuterHashacross buffer boundaries and multi-block inputs; the old collision no longer collides.zkm-pcslib tests (226 passed); real-ELF core prove/verify.