@@ -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}
0 commit comments