Skip to content

zkWHIR 3.0 : Code Switch Protocol (part 1)#249

Merged
Bisht13 merged 15 commits into
worldfnd:mainfrom
ocdbytes:aj/code-switch-protocol
Apr 28, 2026
Merged

zkWHIR 3.0 : Code Switch Protocol (part 1)#249
Bisht13 merged 15 commits into
worldfnd:mainfrom
ocdbytes:aj/code-switch-protocol

Conversation

@ocdbytes

@ocdbytes ocdbytes commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the code-switching IOR from Section 9.4 as a standalone sub-protocol in src/protocols/code_switch.rs.
Code switching reduces a proximity claim about oracle f w.r.t. source code C to a proximity claim about oracle g w.r.t. target code C', enabling the polylogarithmic-verifier protocol (Theorem 4) where the tested code shrinks each round.

What's implemented

  • Config: Derives target code C' from source (ℓ' = ℓ, m' from rate), optional ZK mask code C_zk, OOD sample computation from Lemma 9.9. ZK query budgets passed via Option.
  • Prover (prove): Commits target oracle g, optionally commits mask oracle s = Enc_{C_zk}((r, pad), r''), sends OOD answers, opens source oracle. Reuses irs_commit::commit and irs_commit::open with zero duplicated logic.
  • Verifier (verify): Mirrors prover transcript, computes batched target μ', builds constraint weights for output sl'. Mask weights have pre-shifted RLC coefficients via MaskClaimInfo.
  • Shared batching_challenge helper ensures prover/verifier transcript sync.
  • Shared num_ood_samples extracted to irs_commit — used by both irs_commit::Config::new and code switch.
  • Tests: Proptest over field types, sizes, rates, and ZK/non-ZK mode. Tests here just check the code switch transcript match. (Tests will be added in future PRs).

What's NOT in this PR (next PRs)

  • Integration into whir's round structure as an alternative to sumcheck-only folding
  • Implement zk sumcheck according to the construction 6.3 to include masks
  • Refactoring irs_commit to separate commitment from OOD (deferred to whir integration)
  • Parameter selection
  • Mask resolution per iteration (this makes the basecase verification simple and whole flow clean and contained)

@codspeed-hq

codspeed-hq Bot commented Apr 3, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 10 untouched benchmarks
⏩ 22 skipped benchmarks1


Comparing ocdbytes:aj/code-switch-protocol (3f04faa) with main (0aeaa7f)

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 changed the title zkWHIR 3.0 : Code Switch Protocol zkWHIR 3.0 : Code Switch Protocol (part 1) Apr 7, 2026
@ocdbytes ocdbytes marked this pull request as ready for review April 7, 2026 21:29
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs Outdated

@recmo recmo left a comment

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.

Most import is we need need a proptest based on arbitrary config testing all invariants.

Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs Outdated
Comment thread src/protocols/code_switch.rs
Comment thread src/protocols/code_switch.rs

@WizardOfMenlo WizardOfMenlo left a comment

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.

Approved, left some comments. The most annoying footgun is that we should check what is the right zero-evader to use.

Here is what AI had to say about it:

[P2] src/protocols/code_switch.rs:207 — target_interleaving_depth > 1 makes proving panic

                                                                                Config::new allows any positive target_interleaving_depth and sets the target IRS

vector_size to source_message_length * target_interleaving_depth, but prove still passes a vector whose length is only source_message_length into
self.target.commit. For any caller using the advertised target_interleaving_depth > 1, IrsConfig::commit rejects the input length and panics. Either restrict this
config to depth 1 or pass a target vector with the length/layout expected by the target IRS config.
### [P2] src/protocols/code_switch.rs:118 — ZK mask lengths can create invalid
IRS configs
In ZK mode, target_config.mask_length and mask_config.mask_length are assigned
after IrsConfig::new has already chosen codeword_length from the unmasked message length. If budget.target or budget.mask exceeds the spare capacity implied by the
original rate, masked_message_length() > codeword_length, so later commit/open calls panic in the NTT evaluation path; even before that, reparameterise_security
computes rates above 1. This arises for valid-looking inputs such as rate 1/2 with a mask budget larger than the message length. The constructor should either
size codeword_length for message_length + mask_length or reject budgets that do not fit.

[P2] src/protocols/code_switch.rs:267 — ZK covector update omits the

mask-oracle coefficients

In ZK mode, the verifier updates sum using OOD answers and source openings for
the combined polynomial f(x) + x^n s(x), but the prover-side covector update only
mutates the caller-provided single covector and does not require or return the
mask portion corresponding to s. With the f-length covector used by the current
test and implied by message.len(), honest proofs with nonzero source masks will
produce a verifier sum that includes mask terms while the downstream target
relation only has weights for f. Require/update a covector of length
message.len() + mask_message_len and expose the split for target/mask, or keep
the verifier update f-only.

Overall Verdict

needs attention

let matrix_witness = self.matrix_commit.commit(prover_state, &matrix);

// Handle out-of-domain points and values
// TODO : Remove this logic after main whir protocol is updated

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.

Not sure we can actually remove this TODO unfortunately

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.

Why ? we can remove this right we dont need ood points in commitment after we update the protocol as this should be handled by code switch. Here it is just revealing more info about the vector.

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.

Comment thread src/protocols/irs_commit.rs Outdated

/// Recompute `johnson_slack`, `in_domain_samples`, and `out_domain_samples`
/// for the current rate (which accounts for `mask_length`).
pub fn reparameterise_security(&mut self, security_target: f64, unique_decoding: bool) {

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.

Can we rename this to something more informative? This basically computes the parameters for this step right?

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.

how about : recompute_security_parameters ?

Comment thread src/protocols/code_switch.rs Outdated
source_config.rate(),
);
mask_config.mask_length = budget.mask;
mask_config.reparameterise_security(security_target, unique_decoding);

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.

Can we make reparametrise security be done in the new of mask_config? This makes it harder to misuse

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.

Yeah so the thing is this work will be cleaned up in parameter selection PR. here I just added for the correctness of the protocol. The configs are very messed up in each of the sub protocols for eg :
in IRS commit the way mask length is being handled is bad design. Will fix this I have noted these things on my end.


// Step 1b: s := Enc_{C_zk}((r || padding), r'') — Construction 9.7 Step 1, p.55
#[allow(clippy::option_if_let_else)]
let mask = if let Some(mask_config) = &self.mask_commit {

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.

Can we make this a map instead? (Instead of if else)

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.

It is a design choice, initially it was map and remco pointed out to use the if let pattern. Ig this looks clean even when we need to suppress clippy suggestion of map. with map it looked like :

        let (mask_message, mask_witness) =
            self.mask_commit
                .as_ref()
                .map_or((None, None), |mask_config| {
                    let mask_msg_len = mask_config.message_length();
                    let r_embedded = lift(self.source.embedding(), source_randomness);
                    let embedded_randomness_len = r_embedded.len();
                    let mut mask_msg = Vec::with_capacity(mask_msg_len);
                    mask_msg.extend_from_slice(&r_embedded);
                    let random_padding: Vec<M::Target> =
                        random_vector(prover_state.rng(), mask_msg_len - embedded_randomness_len);
                    mask_msg.extend_from_slice(&random_padding);
                    let witness = mask_config.commit(prover_state, &[&mask_msg]);
                    (Some(mask_msg), Some(witness))
                });

None
};

// Step 2-3: OOD challenge + answers — Construction 9.7 Steps 2-3, p.55

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.

We should double check that the current univariate_eval satisfies the private zero-evader conditions that we have in the paper, I am not sure if they do

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.

Let's leave a comment about it

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.

makes sense

Comment thread src/protocols/code_switch.rs Outdated
// Step 4: in-domain queries — Construction 9.7 Step 4, p.55
let source_evaluations = self.source.open(prover_state, &[witness]);

// Step 5: batching — Construction 9.7 Step 5, p.55

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 still step 4 right?

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.

yup according to the paper yes. I just divided into different step for batching part.

@ocdbytes

ocdbytes commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author

@WizardOfMenlo

Agreed on the deterministic evader — paper has a private ze_ood(ρ) but the geometric progression (1, α, α², …) works fine: applied to [f; (r,s)] it just evaluates the concatenated polynomial at α. ZK still holds because y = f(α) + α^ℓ · (r,s)(α) is uniform whenever (r,s) has fresh randomness and α ≠ 0. The α = 0 event is 1/|F| and folds into the soundness budget. So as long as ℓ_zk - r ≥ t_ood we get perfect privacy ζ_ze = 0.

Soundness parameter derivation pending.

@Bisht13 Bisht13 merged commit ec7aa32 into worldfnd:main Apr 28, 2026
7 checks passed
latifkasuli added a commit to latifkasuli/Plonky3 that referenced this pull request May 3, 2026
Add the `code_switch` module with:
- `RoundZkConfig`: per-round ZK parameters (query budgets, mask
  dimensions, OOD sample count) that extend `RoundConfig` without
  touching the non-ZK path.
- `ZkMaskClaim`: coefficient carrier for mask/target batching weights,
  preventing the covector mismatch bug from worldfnd/whir#249.
- `WhirRoundZkProof`: optional per-round ZK proof data (mask
  commitment + private OOD answers).
- Construction 9.7 `mu'` identity unit tests over BabyBear, covering
  both n=0 and n=2 auxiliary masks, plus OOD and batching helpers.

All 187 existing WHIR tests pass unchanged. The implementation itself
is blocked on Plonky3#1584 (ZK encoding), Plonky3#1585 (zero-evader helpers), and
Plonky3#1586 (HVZK sumcheck).
latifkasuli added a commit to latifkasuli/Plonky3 that referenced this pull request May 14, 2026
Add the `code_switch` module with:
- `RoundZkConfig`: per-round ZK parameters (query budgets, mask
  dimensions, OOD sample count) that extend `RoundConfig` without
  touching the non-ZK path.
- `ZkMaskClaim`: coefficient carrier for mask/target batching weights,
  preventing the covector mismatch bug from worldfnd/whir#249.
- `WhirRoundZkProof`: optional per-round ZK proof data (mask
  commitment + private OOD answers).
- Construction 9.7 `mu'` identity unit tests over BabyBear, covering
  both n=0 and n=2 auxiliary masks, plus OOD and batching helpers.

All 187 existing WHIR tests pass unchanged. The implementation itself
is blocked on Plonky3#1584 (ZK encoding), Plonky3#1585 (zero-evader helpers), and
Plonky3#1586 (HVZK sumcheck).
latifkasuli added a commit to latifkasuli/Plonky3 that referenced this pull request Jun 3, 2026
Add the `code_switch` module with:
- `RoundZkConfig`: per-round ZK parameters (query budgets, mask
  dimensions, OOD sample count) that extend `RoundConfig` without
  touching the non-ZK path.
- `ZkMaskClaim`: coefficient carrier for mask/target batching weights,
  preventing the covector mismatch bug from worldfnd/whir#249.
- `WhirRoundZkProof`: optional per-round ZK proof data (mask
  commitment + private OOD answers).
- Construction 9.7 `mu'` identity unit tests over BabyBear, covering
  both n=0 and n=2 auxiliary masks, plus OOD and batching helpers.

All 187 existing WHIR tests pass unchanged. The implementation itself
is blocked on Plonky3#1584 (ZK encoding), Plonky3#1585 (zero-evader helpers), and
Plonky3#1586 (HVZK sumcheck).
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.

4 participants