Skip to content

Commit 8736aed

Browse files
committed
addressed comments from copilot
1 parent a9eb6b9 commit 8736aed

3 files changed

Lines changed: 99 additions & 25 deletions

File tree

src/polys/multilinear.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,12 @@ impl<T> MultilinearPolynomial<T> {
4242
/// Creates a new `MultilinearPolynomial` from the given evaluations.
4343
///
4444
/// # Panics
45-
/// The number of evaluations must be a power of two.
45+
/// Panics if the number of evaluations is empty or not a power of two.
4646
pub fn new(Z: Vec<T>) -> Self {
47+
assert!(
48+
!Z.is_empty() && Z.len().is_power_of_two(),
49+
"Vector Z must be non-empty and its length must be a power of two."
50+
);
4751
MultilinearPolynomial { Z }
4852
}
4953
}
@@ -391,4 +395,16 @@ mod tests {
391395
assert_ne!(poly.Z[0], Scalar::from(6u64));
392396
assert_ne!(poly.Z[1], Scalar::from(8u64));
393397
}
398+
399+
#[test]
400+
#[should_panic(expected = "Vector Z must be non-empty and its length must be a power of two.")]
401+
fn test_new_rejects_empty() {
402+
let _: MultilinearPolynomial<Scalar> = MultilinearPolynomial::new(vec![]);
403+
}
404+
405+
#[test]
406+
#[should_panic(expected = "Vector Z must be non-empty and its length must be a power of two.")]
407+
fn test_new_rejects_non_power_of_two_length() {
408+
let _ = MultilinearPolynomial::new(vec![Scalar::ONE, Scalar::ZERO, Scalar::ONE]);
409+
}
394410
}

src/small_sumcheck.rs

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ where
252252

253253
let mut small_value_sumcheck =
254254
SmallValueSumCheck::<E::Scalar, SPARTAN_T_DEGREE>::from_accumulators(accumulators);
255+
let mut transition_round = l0;
255256

256257
// ===== Small-Value Rounds (0 to ℓ₀-1) =====
257258
// During these rounds, we use the precomputed accumulators. Polynomials are NOT bound
@@ -268,9 +269,13 @@ where
268269
// 2. Get eq factor values ℓ_i(0), ℓ_i(1), ℓ_i(∞)
269270
let li = small_value_sumcheck.eq_round_values(taus[round]);
270271

271-
// 3. Derive t(1) from sumcheck constraint: s(0) + s(1) = claim
272-
let t1 = derive_t1(li.at_zero(), li.at_one(), claim_per_round, t0)
273-
.ok_or(SpartanError::InvalidSumcheckProof)?;
272+
// 3. Derive t(1) from the sumcheck constraint. If ℓ_i(1)=0, the optimized
273+
// path cannot recover t_i(1), so we fall back to the standard prover from
274+
// this round onward.
275+
let Some(t1) = derive_t1(li.at_zero(), li.at_one(), claim_per_round, t0) else {
276+
transition_round = round;
277+
break;
278+
};
274279

275280
// 4. Build round polynomial s_i(X) = ℓ_i(X) · t_i(X)
276281
let poly = build_univariate_round_polynomial(&li, t0, t1, t_inf);
@@ -296,35 +301,47 @@ where
296301

297302
// ===== Transition Phase =====
298303
// Create bound field-element polynomials via batched eq-weighted small-value accumulation.
299-
// Binds all l0 variables in a single pass using field×int unreduced accumulation.
304+
// Binds all completed small-value rounds in a single pass using field×int
305+
// unreduced accumulation.
300306
let (_bind_span, bind_t) = start_span!("bind_poly_vars_transition");
301307
let (mut poly_A, mut poly_B, mut poly_C) =
302-
bind_three_polys_batched_small_value(poly_A_small, poly_B_small, poly_C_small, &r[..l0]);
308+
bind_three_polys_batched_small_value(
309+
poly_A_small,
310+
poly_B_small,
311+
poly_C_small,
312+
&r[..transition_round],
313+
);
303314
info!(
304315
elapsed_ms = %bind_t.elapsed().as_millis(),
305316
"bind_poly_vars_transition"
306317
);
307318

308319
// ===== Remaining Rounds (ℓ₀ to ℓ-1) =====
309-
// Pop the top layer from e_in_pyramid. The top layer was used by the accumulator;
310-
// EqSumCheckInstance needs the remaining layers (without the first suffix tau τ[l₀]).
311-
// This matches the Nova optimization where τ[l₀] is tracked in eval_eq_left.
312-
e_in_pyramid.pop();
313-
314-
// Create EqSumCheckInstance from precomputed pyramids, avoiding redundant eq computation.
315-
// The accumulated eq factor from small-value rounds is passed directly.
316-
let mut eq_instance = eq_sumcheck::EqSumCheckInstance::<E>::from_pyramids(
317-
e_in_pyramid,
318-
e_xout_pyramid,
319-
&taus[l0..],
320-
small_value_sumcheck.eq_alpha(),
321-
);
320+
let mut eq_instance = if transition_round == l0 {
321+
// Pop the top layer from e_in_pyramid. The top layer was used by the accumulator;
322+
// EqSumCheckInstance needs the remaining layers (without the first suffix tau τ[l₀]).
323+
// This matches the Nova optimization where τ[l₀] is tracked in eval_eq_left.
324+
e_in_pyramid.pop();
325+
326+
// Reuse the precomputed eq pyramids when all planned small-value rounds succeeded.
327+
eq_sumcheck::EqSumCheckInstance::<E>::from_pyramids(
328+
e_in_pyramid,
329+
e_xout_pyramid,
330+
&taus[l0..],
331+
small_value_sumcheck.eq_alpha(),
332+
)
333+
} else {
334+
eq_sumcheck::EqSumCheckInstance::<E>::new_with_eval_eq_left(
335+
&taus[transition_round..],
336+
small_value_sumcheck.eq_alpha(),
337+
)
338+
};
322339

323340
// Continue with the remaining rounds using the eq instance built from pyramids.
324341
// The eq_instance was created with taus[l0..], so its internal round counter tracks
325342
// local rounds. We pass global round index for the round_idx parameter since
326343
// eval_one_case_cubic_three_inputs uses it for a round-0 optimization.
327-
for round in l0..num_rounds {
344+
for round in transition_round..num_rounds {
328345
let (_round_span, round_t) = start_span!("sumcheck_round", round = round);
329346

330347
let poly = {
@@ -533,14 +550,26 @@ mod tests {
533550
.map(|(&a, &b)| a * b)
534551
.collect();
535552

536-
// Convert to field elements
553+
// Random taus
554+
let taus: Vec<F> = (0..num_vars).map(|_| F::random(&mut rng)).collect();
555+
556+
run_equivalence_test_with_taus::<SV>(taus, az_small, bz_small, cz_small);
557+
}
558+
559+
fn run_equivalence_test_with_taus<SV>(
560+
taus: Vec<F>,
561+
az_small: Vec<SV>,
562+
bz_small: Vec<SV>,
563+
cz_small: Vec<SV>,
564+
) where
565+
SV: SmallValue + Mul<Output = SV>,
566+
F: SmallValueEngine<SV>,
567+
{
568+
let num_vars = taus.len();
537569
let az_vals: Vec<F> = az_small.iter().map(|&v| F::small_to_field(v)).collect();
538570
let bz_vals: Vec<F> = bz_small.iter().map(|&v| F::small_to_field(v)).collect();
539571
let cz_vals: Vec<F> = cz_small.iter().map(|&v| F::small_to_field(v)).collect();
540572

541-
// Random taus
542-
let taus: Vec<F> = (0..num_vars).map(|_| F::random(&mut rng)).collect();
543-
544573
// Claim = 0 for satisfying witness (Az·Bz = Cz on {0,1}^n)
545574
let claim: F = F::ZERO;
546575

@@ -626,6 +655,27 @@ mod tests {
626655
run_equivalence_test::<i64>(num_vars);
627656
}
628657
}
658+
659+
#[test]
660+
fn test_sumcheck_equivalence_when_first_tau_is_zero() {
661+
const NUM_VARS: usize = 6;
662+
const SEED: u64 = 0xDEADBEEF;
663+
let mut rng = StdRng::seed_from_u64(SEED);
664+
let n = 1usize << NUM_VARS;
665+
666+
let az_small: Vec<i32> = (0..n).map(|_| rng.gen_range(-100i32..=100i32)).collect();
667+
let bz_small: Vec<i32> = (0..n).map(|_| rng.gen_range(-100i32..=100i32)).collect();
668+
let cz_small: Vec<i32> = az_small
669+
.iter()
670+
.zip(&bz_small)
671+
.map(|(&a, &b)| a * b)
672+
.collect();
673+
674+
let mut taus: Vec<F> = (0..NUM_VARS).map(|_| F::random(&mut rng)).collect();
675+
taus[0] = F::ZERO;
676+
677+
run_equivalence_test_with_taus::<i32>(taus, az_small, bz_small, cz_small);
678+
}
629679
}
630680

631681
#[cfg(test)]

src/sumcheck.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,14 @@ pub(crate) mod eq_sumcheck {
895895
/// to simplify indexing into precomputed arrays, as the first evaluation happens before
896896
/// any binding operations occur.
897897
pub fn new(taus: &[E::Scalar]) -> Self {
898+
Self::new_with_eval_eq_left(taus, E::Scalar::ONE)
899+
}
900+
901+
/// Creates a new EqSumCheckInstance with an externally supplied prefix eq factor.
902+
///
903+
/// This is used when another prover path has already consumed a prefix of rounds and
904+
/// we need to continue with the standard eq-sumcheck from the first remaining round.
905+
pub fn new_with_eval_eq_left(taus: &[E::Scalar], eval_eq_left: E::Scalar) -> Self {
898906
let l = taus.len();
899907
let first_half = l / 2;
900908

@@ -950,7 +958,7 @@ pub(crate) mod eq_sumcheck {
950958
second_half: l - first_half,
951959
round: 1, // Start at 1 to simplify array indexing (round-1 gives 0-based index)
952960
taus: taus.to_vec(),
953-
eval_eq_left: E::Scalar::ONE,
961+
eval_eq_left,
954962
poly_eq_left,
955963
poly_eq_right,
956964
eq_tau_0_2_3,

0 commit comments

Comments
 (0)