Skip to content

feat: move to upstream-main Plonky3 and harden the outer MultiField32 transcript - #581

Merged
eigmax merged 8 commits into
ProjectZKM:mainfrom
felicityin:feat/plonky3-upstream
Oct 30, 2026
Merged

eigmax merged 8 commits into
ProjectZKM:mainfrom
felicityin:feat/plonky3-upstream

Conversation

@felicityin

Copy link
Copy Markdown
Contributor

Summary

  • Moves Ziren onto the ProjectZKM Plonky3 fork's main (5ccd557f): upstream Plonky3 main plus the fork's remaining changes.
  • Makes the BN254 wrap side match the hardened MultiField32Challenger (GHSA-vj64-rjf3-w3v7).

Changes

Outer transcript (GHSA-vj64-rjf3-w3v7)

  • The old radix-2^32 packing made absorbs non-injective and let two distinct leaves share an OuterHash digest.
  • 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.
  • New CircuitVar2FieldOrderLimbs op; the gnark side range-checks the digits and the high part, so the split is canonical.

Plonky3 migration

  • AIR builders implement PeriodicVar / is_transition; preprocessed_width moves to BaseAir.
  • Constraint degree and counts go through capture_machine_constraints, which keeps the PROOF_MAX_NUM_PVS public-value layout.
  • DuplexChallengerVariable follows upstream's length-tagged absorb: unused rate slots are zeroed and the absorb length is added to state[RATE].
  • GrindingChallenger bounds add Clone + Sync; FRI parameters set batch_proof_of_work_bits; SepticExtension implements AlgebraIdentity;
    num-bigint moves to 0.5.
  • Plonky3 is a git dependency on the fork's default branch. A commented [patch] points at ../Plonky3 for local work.

Everything else in the transcript is bit-identical to the previous fork: permutations, sponge hashes, compression, Merkle roots and openings.

Not included

  • The recursion vks and the wrap vk change. vk_map.bin / vk_map_profile.txt, the wrap vk, the Groth16/PLONK artifacts and crates/verifier/bn254-vk/*.bin will be regenerated separately.
  • Until then, proving needs VERIFY_VK=false.

Testing

  • cargo clippy --workspace --all-targets -- -D warnings
  • Recursion circuit vs. native challengers: inner duplex across buffer boundaries and a grind; outer challenger and OuterHash across buffer boundaries and multi-block inputs; the old collision no longer collides.
  • zkm-pcs lib tests (226 passed); real-ELF core prove/verify.
  • Through GPU: reth (420M cycles, 81 shards) proves through the BN254 wrap and the proof verifies.

…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.
@felicityin felicityin self-assigned this Oct 29, 2026
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.
self.sponge_state[..OUTER_MULTI_FIELD_CHALLENGER_RATE].iter_mut().enumerate()
{
*slot = match values.get(i) {
Some(&value) => value,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread Cargo.toml
# 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" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add the latest rev for Plonky3 deps?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need

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.
@eigmax
eigmax merged commit d3224e4 into ProjectZKM:main Oct 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants