Skip to content

Commit cf1599b

Browse files
fix: handle batch_size > 1 in prove_batch/verify_batch RLC combination (#205)
1 parent a5bc5c2 commit cf1599b

3 files changed

Lines changed: 204 additions & 13 deletions

File tree

src/whir/mod.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,4 +516,173 @@ mod tests {
516516
"Verifier should reject mismatched polynomial"
517517
);
518518
}
519+
520+
/// Test batch proving with batch_size > 1 (multiple polynomials per commitment).
521+
///
522+
/// This tests the case where each commitment contains multiple stacked polynomials
523+
/// (e.g., masked witness + random blinding for ZK), and we batch-prove multiple
524+
/// such commitments together.
525+
///
526+
/// This was a regression test for a bug where the RLC combination of stacked
527+
/// leaf answers was incorrect when batch_size > 1.
528+
fn make_whir_batch_with_batch_size(
529+
num_variables: usize,
530+
folding_factor: FoldingFactor,
531+
num_points_per_poly: usize,
532+
num_witnesses: usize,
533+
batch_size: usize,
534+
soundness_type: SoundnessType,
535+
pow_bits: usize,
536+
) {
537+
let num_coeffs = 1 << num_variables;
538+
let mut rng = ark_std::test_rng();
539+
540+
let (leaf_hash_params, two_to_one_params) =
541+
default_config::<EF, Blake3LeafHash<EF>, Blake3Compress>(&mut rng);
542+
543+
let mv_params = MultivariateParameters::new(num_variables);
544+
let whir_params = ProtocolParameters::<MerkleConfig, PowStrategy> {
545+
initial_statement: true,
546+
security_level: 32,
547+
pow_bits,
548+
folding_factor,
549+
leaf_hash_params,
550+
two_to_one_params,
551+
soundness_type,
552+
_pow_parameters: Default::default(),
553+
starting_log_inv_rate: 1,
554+
batch_size, // KEY: batch_size > 1
555+
deduplication_strategy: DeduplicationStrategy::Enabled,
556+
merkle_proof_strategy: MerkleProofStrategy::Compressed,
557+
};
558+
559+
let reed_solomon = Arc::new(RSDefault);
560+
let basefield_reed_solomon = reed_solomon.clone();
561+
let params = WhirConfig::new(reed_solomon, basefield_reed_solomon, mv_params, whir_params);
562+
563+
// Create polynomials for each witness
564+
// Each witness will contain batch_size polynomials committed together
565+
let mut all_polynomials: Vec<Vec<CoefficientList<F>>> = Vec::new();
566+
for w in 0..num_witnesses {
567+
let witness_polys: Vec<_> = (0..batch_size)
568+
.map(|b| {
569+
// Different polynomials: witness 0 batch 0 = all 1s, witness 0 batch 1 = all 2s, etc.
570+
CoefficientList::new(vec![
571+
F::from(((w * batch_size + b) + 1) as u64);
572+
num_coeffs
573+
])
574+
})
575+
.collect();
576+
all_polynomials.push(witness_polys);
577+
}
578+
579+
// Create statements - one per witness, based on the first polynomial in each batch
580+
// (the internally-batched polynomial is what the statement is about)
581+
let mut statements = Vec::new();
582+
for witness_polys in &all_polynomials {
583+
let poly = &witness_polys[0]; // Use first poly for statement (will be batched internally)
584+
let mut statement = Statement::new(num_variables);
585+
586+
for _ in 0..num_points_per_poly {
587+
let point = MultilinearPoint::rand(&mut rng, num_variables);
588+
let eval = poly.evaluate_at_extension(&point);
589+
statement.add_constraint(Weights::evaluation(point), eval);
590+
}
591+
592+
// Add a linear constraint
593+
let input = CoefficientList::new(
594+
(0..1 << num_variables)
595+
.map(<EF as Field>::BasePrimeField::from)
596+
.collect(),
597+
);
598+
let linear_claim_weight = Weights::linear(input.into());
599+
let poly_evals = EvaluationsList::from(poly.clone().to_extension());
600+
let sum = linear_claim_weight.weighted_sum(&poly_evals);
601+
statement.add_constraint(linear_claim_weight, sum);
602+
603+
statements.push(statement);
604+
}
605+
606+
// Set up domain separator
607+
let mut domainsep = DomainSeparator::new("🌪️");
608+
for _ in 0..num_witnesses {
609+
domainsep = domainsep.commit_statement(&params);
610+
}
611+
612+
let total_constraints = num_witnesses * params.committment_ood_samples
613+
+ statements
614+
.iter()
615+
.map(|s| s.constraints.len())
616+
.sum::<usize>();
617+
domainsep = domainsep.add_whir_batch_proof(&params, num_witnesses, total_constraints);
618+
let mut prover_state = domainsep.to_prover_state();
619+
620+
// Commit using commit_batch (stacks batch_size polynomials per witness)
621+
let committer = CommitmentWriter::new(params.clone());
622+
let mut witnesses = Vec::new();
623+
624+
for witness_polys in &all_polynomials {
625+
let poly_refs: Vec<_> = witness_polys.iter().collect();
626+
let witness = committer
627+
.commit_batch(&mut prover_state, &poly_refs)
628+
.unwrap();
629+
witnesses.push(witness);
630+
}
631+
632+
// Batch prove all witnesses together
633+
let prover = Prover::new(params.clone());
634+
let result = prover.prove_batch(&mut prover_state, &statements, &witnesses);
635+
assert!(
636+
result.is_ok(),
637+
"Batch proving with batch_size={} failed: {:?}",
638+
batch_size,
639+
result.err()
640+
);
641+
642+
// Verify
643+
let commitment_reader = CommitmentReader::new(&params);
644+
let verifier = Verifier::new(&params);
645+
let mut verifier_state = domainsep.to_verifier_state(prover_state.narg_string());
646+
647+
let mut parsed_commitments = Vec::new();
648+
for _ in 0..num_witnesses {
649+
let parsed_commitment = commitment_reader
650+
.parse_commitment(&mut verifier_state)
651+
.unwrap();
652+
parsed_commitments.push(parsed_commitment);
653+
}
654+
655+
let verify_result =
656+
verifier.verify_batch(&mut verifier_state, &parsed_commitments, &statements);
657+
assert!(
658+
verify_result.is_ok(),
659+
"Batch verification with batch_size={} failed: {:?}",
660+
batch_size,
661+
verify_result.err()
662+
);
663+
}
664+
665+
#[test]
666+
fn test_whir_batch_with_batch_size_2() {
667+
// This is the key regression test for the batch_size > 1 bug
668+
let batch_sizes = [2, 3];
669+
let num_witnesses = [2, 3];
670+
let folding_factors = [2, 3];
671+
672+
for batch_size in batch_sizes {
673+
for num_witness in num_witnesses {
674+
for folding_factor in folding_factors {
675+
make_whir_batch_with_batch_size(
676+
folding_factor * 2, // num_variables
677+
FoldingFactor::Constant(folding_factor),
678+
1, // num_points_per_poly
679+
num_witness,
680+
batch_size,
681+
SoundnessType::ConjectureList,
682+
0, // pow_bits
683+
);
684+
}
685+
}
686+
}
687+
}
519688
}

src/whir/prover.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -415,13 +415,14 @@ where
415415
)?;
416416

417417
let fold_size = 1 << self.config.folding_factor.at_round(0);
418+
let leaf_size = fold_size * self.config.batch_size;
418419
let mut all_witness_answers = Vec::with_capacity(witnesses.len());
419420

420421
// For each witness, provide Merkle proof opening at the challenge indices
421422
for witness in witnesses {
422423
let answers: Vec<_> = stir_indexes
423424
.iter()
424-
.map(|&i| witness.merkle_leaves[i * fold_size..(i + 1) * fold_size].to_vec())
425+
.map(|&i| witness.merkle_leaves[i * leaf_size..(i + 1) * leaf_size].to_vec())
425426
.collect();
426427
prover_state.hint::<Vec<Vec<F>>>(&answers)?;
427428
self.merkle_state.write_proof_hint(
@@ -438,10 +439,19 @@ where
438439
.map(|query_idx| {
439440
let mut combined = vec![F::ZERO; fold_size];
440441
let mut pow = F::ONE;
441-
for witness_answers in &all_witness_answers {
442-
for (dst, src) in combined.iter_mut().zip(&witness_answers[query_idx]) {
443-
*dst += pow * src;
442+
for (witness_idx, witness) in witnesses.iter().enumerate() {
443+
let answer = &all_witness_answers[witness_idx][query_idx];
444+
445+
// First, internally reduce stacked leaf using witness's batching_randomness
446+
let mut internal_pow = F::ONE;
447+
for poly_idx in 0..self.config.batch_size {
448+
let start = poly_idx * fold_size;
449+
for j in 0..fold_size {
450+
combined[j] += pow * internal_pow * answer[start + j];
451+
}
452+
internal_pow *= witness.batching_randomness;
444453
}
454+
445455
pow *= batching_randomness;
446456
}
447457
combined

src/whir/verifier.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ where
236236
// Step 1: Read the N×M constraint evaluation matrix from the transcript
237237

238238
// Collect all constraint weights to determine matrix dimensions
239-
let mut all_constraint_weights = Vec::new();
239+
let mut all_constraints_info: Vec<(Weights<F>, bool)> = Vec::new();
240240

241241
// OOD constraints from each commitment
242242
for commitment in parsed_commitments {
@@ -245,20 +245,21 @@ where
245245
*point,
246246
self.params.mv_parameters.num_variables,
247247
);
248-
all_constraint_weights.push(Weights::evaluation(ml_point));
248+
all_constraints_info.push((Weights::evaluation(ml_point), false));
249249
}
250250
}
251251

252252
// Statement constraints
253253
for statement in statements {
254254
for constraint in &statement.constraints {
255-
all_constraint_weights.push(constraint.weights.clone());
255+
all_constraints_info
256+
.push((constraint.weights.clone(), constraint.defer_evaluation));
256257
}
257258
}
258259

259260
// Read the N×M evaluation matrix from transcript
260261
let num_polynomials = parsed_commitments.len();
261-
let num_constraints = all_constraint_weights.len();
262+
let num_constraints = all_constraints_info.len();
262263
let mut constraint_evals_matrix = Vec::with_capacity(num_polynomials);
263264

264265
for _ in 0..num_polynomials {
@@ -279,7 +280,9 @@ where
279280
if self.params.initial_statement {
280281
let mut all_constraints = Vec::new();
281282

282-
for (constraint_idx, weights) in all_constraint_weights.into_iter().enumerate() {
283+
for (constraint_idx, (weights, defer_evaluation)) in
284+
all_constraints_info.into_iter().enumerate()
285+
{
283286
let mut combined_eval = F::ZERO;
284287
let mut pow = F::ONE;
285288
for poly_evals in &constraint_evals_matrix {
@@ -290,7 +293,7 @@ where
290293
all_constraints.push(Constraint {
291294
weights,
292295
sum: combined_eval,
293-
defer_evaluation: false,
296+
defer_evaluation,
294297
});
295298
}
296299

@@ -368,10 +371,19 @@ where
368371
.map(|query_idx| {
369372
let mut combined = vec![F::ZERO; fold_size];
370373
let mut pow = F::ONE;
371-
for witness_answers in &all_answers {
372-
for (dst, src) in combined.iter_mut().zip(&witness_answers[query_idx]) {
373-
*dst += pow * src;
374+
for (commitment, witness_answers) in parsed_commitments.iter().zip(&all_answers) {
375+
let stacked_answer = &witness_answers[query_idx];
376+
377+
// First, internally reduce stacked leaf using commitment's batching_randomness
378+
let mut internal_pow = F::ONE;
379+
for poly_idx in 0..self.params.batch_size {
380+
let start = poly_idx * fold_size;
381+
for j in 0..fold_size {
382+
combined[j] += pow * internal_pow * stacked_answer[start + j];
383+
}
384+
internal_pow *= commitment.batching_randomness;
374385
}
386+
375387
pow *= batching_randomness;
376388
}
377389
combined

0 commit comments

Comments
 (0)