Add proof-of-work grinding; bind combined_row into Fiat-Shamir#210
Open
latifkasuli wants to merge 9 commits into
Open
Add proof-of-work grinding; bind combined_row into Fiat-Shamir#210latifkasuli wants to merge 9 commits into
latifkasuli wants to merge 9 commits into
Conversation
This was referenced Jun 10, 2026
FRI/WHIR-style grinding for Fiat-Shamir soundness amplification:
- zinc-transcript: new `pow` module — seed-bound blake3 PoW search and
check, deterministic (smallest nonce), parallelized under the new
`parallel` feature — plus `Transcript::{pow_seed, pow_grind, pow_check}`.
- zip-plus: `PcsProverTranscript::grind` searches the nonce, absorbs it,
and writes it to the proof stream; `PcsVerifierTranscript::check_grinding`
reads and verifies it. Both are no-ops at 0 bits, keeping current
proofs byte-identical. New `ZipError::Grinding` variant.
Groundwork for reducing PCS column openings at the same security
target; not yet wired into the PCS phases.
- Add `ZipTypes::GRINDING_BITS` (default 0). - Prover (`prove_f`) grinds after writing `combined_row` and before squeezing column indices; verifier (`verify_with_alphas`) checks the nonce at the same point. Both are no-ops at 0 bits, so default proofs are byte-identical and Fiat-Shamir stays in sync. - Make the proof-size accounting tests grinding-aware (account for the 8-byte nonce when enabled).
- Add GrindZipTypes test helper + a full PCS prove→verify roundtrip test that also corrupts the nonce and asserts verification fails at the grinding check (covers grind/check_grinding wired into the PCS). - Turn on grinding (8 bits) in the protocol e2e test configs so the whole prover/verifier pipeline is exercised with grinding active. - Recalibrate the e2e benchmark config to 84 openings + 16 grinding bits (≈ 100 openings without grinding at rate 1/8 / ~100-bit), demonstrating the proof-size reduction. Closes NethermindEth#149.
CI runs miri on zinc-transcript, zinc-protocol, and the pcs_transcript/ phase_verify chunks of zip-plus. Nonce searches are ~2^bits interpreted blake3 hashes there, so: cap unit-test grinding at 8 bits, gate the 16-bit tamper test behind cfg(miri, ignore), and drop test-config GRINDING_BITS to 2 under miri (keeping the grinding code path covered).
cargo run --release --example sweet_spot_sweep --features "simd parallel
unchecked" sweeps IPRS rate {1/4, 1/8, 1/16} x grinding {0, 8, 16, 20}
bits with column openings recalibrated to hold the ~100-bit target,
printing CSV of average prover time and raw/zstd proof sizes (with a
verifier pass so invalid parameter combinations fail loudly).
On main, combined_row is written to the proof stream via write_const_many, which does not absorb into the Fiat-Shamir transcript, so the column-opening indices were derived independently of it. The IOPP samples the spot checks after the prover sends this row; with predictable indices a malicious prover could solve for a row that passes the spot checks against an invalid commitment. Add write_const_many_bound / read_const_many_bound (write/read plus absorbing the serialized bytes) and use them for combined_row on both sides, so the grinding seed and the column indices are bound to it. A regression test asserts bound messages influence subsequent challenges and that prover/verifier stay in sync. Note: this re-derives all downstream challenges, so proofs differ from pre-fix proofs even with grinding disabled.
Protocol benches and examples built with --features parallel previously left zip-plus fully sequential (encoding, Merkle, and the grinding nonce search), since the feature was never propagated.
grind/check_grinding now return ZipError::InvalidPcsParam for bits >= 64 instead of panicking in the nonce search (the verifier rejects before touching the proof stream), and prove_f / verify_with_alphas turn a misconfigured ZipTypes::GRINDING_BITS into a compile-time error via an inline const assert.
f2118d6 to
86c339e
Compare
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.
Closes #149.
Plan / end state
Adds FRI/STIR/WHIR-style grinding so Zip+ can trade a small amount of prover hashing for fewer column openings — the dominant proof-size term. Ships as: (1) a transcript PoW primitive, (2)
ZipTypes::GRINDING_BITS(default 0 = off) wired into the open/verify phases, (3) e2e bench recalibrated to 84 openings + 16 grinding bits at the same ~100-bit target, plus a sweep example to reproduce the parameter study. Happy to split into smaller PRs if preferred — the commits are structured for it.Soundness fix included (pre-existing, found during this work)
On
main,combined_rowis written to the proof stream viawrite_const_many, which does not absorb into Fiat-Shamir — so the column-opening indices were derived independently ofw. Algorithm 2 samples the spot checks after the prover sendsw; with predictable indices a prover can solve the underdetermined linear system for awpassing all spot checks against an invalid commitment. Commit626a7a6addswrite_const_many_bound/read_const_many_boundand uses them forcombined_rowon both sides, binding the grinding seed and the indices to it. This re-derives downstream challenges, so proofs differ from pre-fix proofs even with grinding disabled. cc @albert-garreta for the soundness review.How grinding trades
At rate 1/8 the per-query soundness term is
(1−β)^Cwith β at the 1.5-Johnson boundrate^{1/3} = 1/2, i.e. 1 bit per opening — soggrinding bits allow100−gopenings at the same target. Caveat: grinding amplifies only this final query term; the MCA/list-decoding and algebraic terms are unchanged (they were engineered to the target independently and are unaffected by loweringC). Parameter choice is open for discussion —16bits follows the knee in the data below.Numbers (
sweet_spot_sweepexample, 5 iterations, nvars=10,-C target-cpu=native, simd+parallel+unchecked)20 grinding bits is past the knee (+34–60% prove for ~−1.6% more size). Rate 1/16 with 16 bits gives −14.4% raw (−18.9% compressed) for +35% prove — left out of this PR; can follow up with verifier-time data if there's interest. Reproduce:
SWEEP_ITERS=5 SWEEP_NVARS=10 RUSTFLAGS='-C target-cpu=native' cargo run -p zinc-protocol --release --features 'parallel simd unchecked' --example sweet_spot_sweep.Details
GRINDING_BITSleading zero bits inblake3(seed ‖ nonce); deterministic with or withoutparallel(chunkedfind_first). Verifier check is one hash.GRINDING_BITS >= 64is a compile-time error in the phases and a gracefulInvalidPcsParamon the transcript methods.zinc-protocol/parallelnow forwardszip-plus/parallel(benches previously ran zip-plus sequential).