Skip to content

zkWHIR 3.0 : Updated protocols for whir round integration and added mask proximity protocol (part 2)#251

Merged
WizardOfMenlo merged 22 commits into
worldfnd:mainfrom
ocdbytes:aj/zk-whir-3-2
May 25, 2026
Merged

zkWHIR 3.0 : Updated protocols for whir round integration and added mask proximity protocol (part 2)#251
WizardOfMenlo merged 22 commits into
worldfnd:mainfrom
ocdbytes:aj/zk-whir-3-2

Conversation

@ocdbytes
Copy link
Copy Markdown
Contributor

Summary

  • Add mask proximity protocol (Construction 7.2) — verifies mask polynomials are close to C_zk codewords via shared Merkle tree
  • Refactor code-switch (Construction 9.7) — ZK-correct OOD with [f; r; s], covector split, mask oracle externalized to orchestrator
  • Update sumcheck (Construction 6.3) — unified output type, ZK masking support
  • Update basecase — structured Opening output type
  • Prepare all sub-protocols for orchestrator integration with shared mask tree per round

Mask Proximity Protocol

Implements Construction 7.2 from the paper (§7, p.43-48), specialized for zero-constraint masks (μ_i = 0, sl_{o,i} = 0).

Protocol flow:

  1. Prover commits 2n vectors in one tree: n original masks ξ_i and n fresh mask-of-masks s_i
  2. Verifier sends γ (combination randomness)
  3. Prover sends combined polynomials ξ*_i = s_i + γ·ξ_i and combined IRS randomness r*_i = r'_i + γ·r_i
  4. Shared tree is opened at random positions
  5. Verifier spot-checks: Enc(ξ*_i, r*_i)(y_j) = s_i(y_j) + γ·ξ_i(y_j) using linearity of RS encoding

Important

Soundness (Lemma 7.4, p.45): If ξ_i is δ-far from every C_zk codeword, then ξ*_i = s_i + γ·ξ_i is also far from s_i + γ·c for every codeword c, with high probability over γ. The spot-check catches disagreement at random positions with probability ≥ 1 − (1−δ)^{t_zk}.

Important

ZK safety (Lemma 7.3, p.44): Only ξ*_i = s_i + γ·ξ_i is revealed in full. Since s_i is uniformly random, ξ*_i is uniform regardless of ξ_i — the original mask is never exposed. The tree is opened at ≤ t_zk positions, within the ZK query budget of C_zk's encoding.

[MISSED IN DISCUSSIONS]
Why not just irs_commit.open/verify: irs_commit.verify() only checks Merkle binding (authentication paths). It does NOT verify proximity to C_zk — the doc comment in irs_commit confirms: "This does not verify the out-of-domain evaluations." Proximity requires the γ-combination: send the combined polynomial in full, then spot-check against the committed tree.

Next Steps

  • Protocol Param Selection
  • Wiring of protocol

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 29, 2026

Merging this PR will not alter performance

✅ 10 untouched benchmarks
⏩ 22 skipped benchmarks1


Comparing ocdbytes:aj/zk-whir-3-2 (a1190bd) with main (ec7aa32)

Open in CodSpeed

Footnotes

  1. 22 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@ocdbytes ocdbytes marked this pull request as ready for review April 30, 2026 07:12
target_config.interleaving_depth.is_power_of_two(),
"target.interleaving_depth must be a power of 2"
);
// Theorem 9.6: ℓ_zk ≥ r (mask oracle must cover source randomness).
Copy link
Copy Markdown
Collaborator

@Bisht13 Bisht13 May 4, 2026

Choose a reason for hiding this comment

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

This validates only ℓ_zk ≥ r. For the current OOD map f(α) + α^ℓ·mask(α), Lemma 9.8 also needs a checked privacy/rank condition for ze_ood; otherwise configs with too little fresh s padding can be accepted. Please reject under-padded ZK configs here, or make this constructor explicitly unchecked until parameter selection enforces the condition.

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.

Added the assert here but would remove it in the parameter selection PR

/// It must match what the prover received from the same transcript —
/// not caller-supplied randomness. See `prove` doc for details.
///
/// Returns the target commitment. In ZK mode, the caller **must**
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is soundness-critical and should not be a doc-only caller obligation. Construction 9.7’s next relation includes the mask oracle s; code_switch::verify currently returns only g. Please make ZK code-switch return/accept a typed next-claim including the mask commitment, or provide a composed verifier that runs code-switch + mask-proximity in a fixed transcript order.

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.

The idea is to have orchestrator own the masks and commit them. pass the refs in sub protocols and if there is tampering the mask proximity check should fail.

Comment thread src/protocols/code_switch.rs
Comment thread src/protocols/mask_proximity.rs Outdated
Comment thread src/protocols/sumcheck.rs Outdated
Comment thread src/protocols/mask_proximity.rs Outdated
// Step 2: compute and send combined polynomials + IRS randomness
let irs_masks_per_vector =
self.c_zk_commit.mask_length * self.c_zk_commit.interleaving_depth;
debug_assert_eq!(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use assert_eq!

Comment thread src/protocols/code_switch.rs Outdated
@@ -309,65 +292,60 @@ impl<M: Embedding> Config<M> {
Hash: ProverMessage<[H::U]>,
{
assert_eq!(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

verify uses assert_eq! for folding_randomness shape. Library verifiers should return VerificationError, not panic. Convert to verify! or structured verifier error.

Comment thread src/protocols/basecase.rs Outdated
};
}

// Even more trivial non-zk protocol: send f an r directly.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: “send f an r directly” → “send f and r directly”


/// Prover output from the commit phase.
#[must_use]
pub struct Witness<F: Field> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Witness::mask_witness and Witness::fresh_msgs are public. External mutation between commit and prove can cause invalid proofs. Not a soundness leak, but avoidable API footgun. Make fields private or pub(crate).

Comment thread src/protocols/basecase.rs
.prove(prover_state, &mut vector, &mut covector, &mut sum, &[])
.0;
.round_challenges;
assert!(!vector[0].is_zero(), "Proof failed");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here and at :107, :135. These three assert!(_, "Proof failed") panic the prover on (a) all-zero input vector, (b) mask_rlc == 0 (1-in-2^64), (c) zero post-fold masked vector. An honest prover hitting any of these in deployment should Err and let upstream re-randomize, not crash. Consider a pub enum ProverError { Degenerate, ChallengeIsZero } and return Result<Opening<F>, ProverError>.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wait why? The all-zero input vector is a valid input vector.

Comment thread src/protocols/sumcheck.rs Outdated
// Send mask sum and get combination randomness.
let mut mask_sum = F::ZERO;
let mut mask_rlc = F::ONE;
if !masks.is_empty() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here and at :183. Prover gates ZK path on !masks.is_empty() here; verifier gates on mask_length > 0 && num_rounds > 0 at line 183. Equivalent only because of the assert_eq!(masks.len(), num_rounds * mask_length) at line 92. Can we pick one expression and use it on both sides, preferably mask_length > 0 && num_rounds > 0 since the verifier doesn't have masks in scope. Reduces the chance of a future refactor desynchronizing them silently.

}

/// Length of the covector for this code-switch.
pub fn covector_length(&self) -> usize {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: the .max() is dead code given the asserts at lines 95 (message_mask_length >= source.mask_length when ZK) and 110 (source.mask_length == 0 when not ZK). Either of these forces message_mask_length >= source.mask_length always, so this can be + self.message_mask_length.

Comment thread src/protocols/basecase.rs
.prove(prover_state, &mut vector, &mut covector, &mut sum, &[])
.0;
.round_challenges;
assert!(!vector[0].is_zero(), "Proof failed");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wait why? The all-zero input vector is a valid input vector.

@WizardOfMenlo WizardOfMenlo merged commit 655783c into worldfnd:main May 25, 2026
7 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