Skip to content

Commit 08de38b

Browse files
committed
Clarify satisfying small-value sumcheck contract
1 parent 8736aed commit 08de38b

4 files changed

Lines changed: 69 additions & 50 deletions

File tree

src/lagrange_accumulator/accumulator_builder.rs

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
//! Builder functions for constructing Lagrange accumulators (Procedure 9).
88
//!
99
//! This module provides:
10-
//! - [`build_accumulators_spartan`]: Optimized builder for Spartan's cubic relation
10+
//! - [`build_accumulators_spartan_satisfying`]: Optimized builder for Spartan's
11+
//! satisfying-witness cubic relation
1112
1213
use super::{
1314
accumulator::LagrangeAccumulators, csr::Csr, domain::LagrangeIndex,
@@ -93,10 +94,16 @@ pub(crate) fn build_beta_cache<const D: usize>(l0: usize) -> BetaPrefixCache {
9394
BetaPrefixCache { cache, num_betas }
9495
}
9596

96-
/// Procedure 9: Build accumulators A_i(v, u) for Spartan's first sum-check (Algorithm 6).
97+
/// Procedure 9: Build accumulators A_i(v, u) for Spartan's satisfying-witness
98+
/// first sum-check (Algorithm 6).
9799
///
98100
/// Computes accumulators for: g(X) = eq(τ, X) · (Az(X) · Bz(X) - Cz(X))
99101
///
102+
/// This specialized builder assumes `Az * Bz = Cz` on `{0,1}^n`. Under that
103+
/// satisfying-witness invariant, all binary-β contributions vanish, so the
104+
/// optimized prefix rounds can omit explicit `C` handling and only accumulate
105+
/// the non-binary β values containing `∞`.
106+
///
100107
/// D is the degree bound of t_i(X) (not s_i); for Spartan, D = 2.
101108
///
102109
/// # Type Parameters
@@ -124,7 +131,7 @@ pub(crate) fn build_beta_cache<const D: usize>(l0: usize) -> BetaPrefixCache {
124131
///
125132
/// - Skip binary betas: for satisfying witnesses, Az·Bz = Cz on {0,1}^n, so Az·Bz - Cz = 0
126133
/// - Only process betas containing ∞ (non-binary evaluations where contributions are non-zero)
127-
pub fn build_accumulators_spartan<F, SV>(
134+
pub fn build_accumulators_spartan_satisfying<F, SV>(
128135
az: &MultilinearPolynomial<SV>,
129136
bz: &MultilinearPolynomial<SV>,
130137
taus: &[F],
@@ -161,7 +168,8 @@ where
161168
} = build_beta_cache::<2>(l0);
162169

163170
// Only betas containing at least one ∞ coordinate contribute non-zero values.
164-
// On binary inputs {0,1}^n, Az·Bz = Cz (R1CS identity), so Az·Bz - Cz = 0.
171+
// This builder intentionally omits explicit C terms: on satisfying witnesses,
172+
// Az·Bz = Cz on {0,1}^n, so all binary-β contributions are zero already.
165173
let betas_with_infty: Vec<usize> = (0..num_betas)
166174
.filter(|&i| (0..l0).any(|d| (i / base.pow(d as u32)) % base == 0))
167175
.collect();
@@ -223,7 +231,8 @@ where
223231
);
224232
let bz_ext = &state.bz_extended_evals[..bz_size];
225233

226-
// Only process betas with ∞ - binary betas contribute 0 for satisfying witnesses
234+
// Only process betas with ∞. Omitting explicit C terms is intentional here:
235+
// the satisfying-witness invariant makes every binary-β contribution vanish.
227236
// Uses delayed modular reduction: accumulates into unreduced wide-limb form.
228237
for &beta_idx in &betas_with_infty {
229238
let prod = SV::wide_mul(az_ext[beta_idx], bz_ext[beta_idx]);
@@ -301,7 +310,7 @@ mod tests {
301310
/// Binary-β zero shortcut: Az=Bz=Cz=first variable (x0), so Az·Bz−Cz=0 on binary β.
302311
/// Non-binary β (∞) should yield non-zero in some bucket.
303312
#[test]
304-
fn test_binary_beta_zero_shortcut_behavior() {
313+
fn test_binary_beta_zero_shortcut_behavior_for_satisfying_witness() {
305314
// Use l0=1 so round 0 buckets are fed only by β of length 1 (easy to reason about).
306315
let l0 = 1;
307316
let l = 2;
@@ -316,7 +325,7 @@ mod tests {
316325

317326
let taus: Vec<Scalar> = vec![Scalar::from(3u64), Scalar::from(5u64)];
318327

319-
let (acc, _, _) = build_accumulators_spartan(&az, &bz, &taus, l0);
328+
let (acc, _, _) = build_accumulators_spartan_satisfying(&az, &bz, &taus, l0);
320329

321330
// Only round 0 exists (v is empty). β ranges over U_d with binary {0,1} and non-binary {∞}.
322331
// Buckets for u = 0 should be zero (binary β), bucket for u = ∞ should be non-zero.
@@ -333,11 +342,12 @@ mod tests {
333342
);
334343
}
335344

336-
/// Test build_accumulators_spartan with i32 witnesses produces consistent results.
345+
/// Test `build_accumulators_spartan_satisfying` with i32 satisfying witnesses
346+
/// produces consistent results.
337347
///
338348
/// Verifies that running the same computation twice produces the same output.
339349
#[test]
340-
fn test_build_accumulators_spartan_small_consistent() {
350+
fn test_build_accumulators_spartan_satisfying_small_consistent() {
341351
let l0 = 2;
342352

343353
// Define deterministic Az, Bz over {0,1}^4 using small values
@@ -364,8 +374,8 @@ mod tests {
364374
];
365375

366376
// Build accumulators twice
367-
let (acc1, _, _) = build_accumulators_spartan(&az, &bz, &taus, l0);
368-
let (acc2, _, _) = build_accumulators_spartan(&az, &bz, &taus, l0);
377+
let (acc1, _, _) = build_accumulators_spartan_satisfying(&az, &bz, &taus, l0);
378+
let (acc2, _, _) = build_accumulators_spartan_satisfying(&az, &bz, &taus, l0);
369379

370380
// Compare all buckets
371381
for round in 0..l0 {
@@ -384,9 +394,10 @@ mod tests {
384394
}
385395
}
386396

387-
/// Test build_accumulators_spartan with i32 witnesses using larger inputs to stress test.
397+
/// Test `build_accumulators_spartan_satisfying` with i32 satisfying witnesses
398+
/// using larger inputs to stress the satisfying-witness shortcut.
388399
#[test]
389-
fn test_build_accumulators_spartan_small_larger() {
400+
fn test_build_accumulators_spartan_satisfying_small_larger() {
390401
let l0 = 3;
391402
let l = 10;
392403
let n = 1 << l;
@@ -402,8 +413,8 @@ mod tests {
402413
let taus: Vec<Scalar> = (0..l).map(|i| Scalar::from((i * 7 + 3) as u64)).collect();
403414

404415
// Build accumulators twice to verify consistency
405-
let (acc1, _, _) = build_accumulators_spartan(&az, &bz, &taus, l0);
406-
let (acc2, _, _) = build_accumulators_spartan(&az, &bz, &taus, l0);
416+
let (acc1, _, _) = build_accumulators_spartan_satisfying(&az, &bz, &taus, l0);
417+
let (acc2, _, _) = build_accumulators_spartan_satisfying(&az, &bz, &taus, l0);
407418

408419
for round in 0..l0 {
409420
let num_v = (D + 1).pow(round as u32);

src/lagrange_accumulator/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub use basis::{LagrangeBasisFactory, LagrangeCoeff};
4747
pub use accumulator::{LagrangeAccumulators, RoundAccumulator};
4848

4949
// Builder functions
50-
pub use accumulator_builder::{SPARTAN_T_DEGREE, build_accumulators_spartan};
50+
pub use accumulator_builder::{SPARTAN_T_DEGREE, build_accumulators_spartan_satisfying};
5151

5252
// Eq round factor and derivation
5353
pub use eq_round::{EqRoundFactor, derive_t1};

src/lagrange_accumulator/thread_state.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
//! Thread-local scratch buffers for accumulator building.
88
//!
99
//! These structs eliminate per-iteration heap allocations in the parallel fold loops
10-
//! of `build_accumulators_spartan`. By hoisting buffer allocations to the fold identity
10+
//! of `build_accumulators_spartan_satisfying`. By hoisting buffer allocations to the fold identity
1111
//! closure (called once per Rayon thread subdivision), we reduce allocations from
1212
//! O(num_x_out) to O(num_threads).
1313
1414
use super::accumulator::LagrangeAccumulators;
1515
use crate::big_num::{DelayedReduction, SmallValue, SmallValueEngine};
1616
use num_traits::Zero;
1717

18-
/// Thread-local scratch buffers for `build_accumulators_spartan`.
18+
/// Thread-local scratch buffers for `build_accumulators_spartan_satisfying`.
1919
///
2020
/// # Motivation
2121
///

src/small_sumcheck.rs

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@
1616
//!
1717
//! # Overview
1818
//!
19-
//! The main entry point is [`prove_cubic_small_value`], which implements
19+
//! The main entry point is [`prove_cubic_small_value_satisfying`], which implements
20+
//! the satisfying-witness-only version of the small-value optimization.
2021
2122
use crate::{
2223
big_num::{DelayedReduction, SmallValue, SmallValueEngine},
2324
errors::SpartanError,
2425
lagrange_accumulator::{
2526
EqRoundFactor, LagrangeAccumulators, LagrangeBasisFactory, LagrangeCoeff, LagrangeEvals,
26-
LagrangeHatEvals, SPARTAN_T_DEGREE, build_accumulators_spartan, derive_t1,
27+
LagrangeHatEvals, SPARTAN_T_DEGREE, build_accumulators_spartan_satisfying, derive_t1,
2728
},
2829
polys::{eq::EqPolynomial, multilinear::MultilinearPolynomial, univariate::UniPoly},
2930
start_span,
@@ -202,6 +203,8 @@ where
202203
///
203204
/// Field-element polynomials are created internally via batched eq-weighted binding
204205
/// from the small-value inputs, eliminating the need for pre-allocated field polys.
206+
/// This specialized prover assumes a satisfying witness, so `Az * Bz = Cz` on
207+
/// `{0,1}^n` and the incoming `claim` is expected to be zero.
205208
///
206209
/// Generic over `SmallValue` to support both i32/i64 and i64/i128 configurations.
207210
///
@@ -211,7 +214,7 @@ where
211214
/// `min(LB, num_rounds / 2)`. Caller should ensure input values are bounded by
212215
/// `SV::MAX / 3^LB` for safe Lagrange extension (see [`ExtensionBound`]).
213216
/// Typical values are 3-4 for practical instances (3^4 = 81× growth factor).
214-
pub fn prove_cubic_small_value<E, SV, const LB: usize>(
217+
pub fn prove_cubic_small_value_satisfying<E, SV, const LB: usize>(
215218
claim: &E::Scalar,
216219
taus: Vec<E::Scalar>,
217220
poly_A_small: &MultilinearPolynomial<SV>,
@@ -242,13 +245,15 @@ where
242245
}
243246

244247
// ===== Pre-computation Phase =====
245-
// Build accumulators A_i(v, u) for all i ∈ [ℓ₀] using small-value arithmetic.
248+
// Build satisfying-witness accumulators A_i(v, u) for all i ∈ [ℓ₀] using
249+
// small-value arithmetic. This optimized prefix path intentionally omits
250+
// explicit C handling because Az·Bz - Cz vanishes on binary points.
246251
// Also builds eq pyramids for reuse by EqSumCheckInstance.
247252
// Internally computes eq tables with balanced split and precomputed eq_cache.
248253
// Uses: small × small → intermediate (for Az·Bz products),
249254
// then intermediate × field (for eq weighting via DelayedReduction).
250255
let (accumulators, mut e_in_pyramid, e_xout_pyramid) =
251-
build_accumulators_spartan(poly_A_small, poly_B_small, &taus, l0);
256+
build_accumulators_spartan_satisfying(poly_A_small, poly_B_small, &taus, l0);
252257

253258
let mut small_value_sumcheck =
254259
SmallValueSumCheck::<E::Scalar, SPARTAN_T_DEGREE>::from_accumulators(accumulators);
@@ -300,9 +305,10 @@ where
300305
}
301306

302307
// ===== Transition Phase =====
303-
// Create bound field-element polynomials via batched eq-weighted small-value accumulation.
304-
// Binds all completed small-value rounds in a single pass using field×int
305-
// unreduced accumulation.
308+
// Create bound field-element polynomials via batched eq-weighted small-value
309+
// accumulation. The satisfying-witness shortcut only applies to the prefix
310+
// accumulators above; we still carry `poly_C_small` explicitly through this
311+
// transition and all remaining rounds.
306312
let (_bind_span, bind_t) = start_span!("bind_poly_vars_transition");
307313
let (mut poly_A, mut poly_B, mut poly_C) =
308314
bind_three_polys_batched_small_value(
@@ -323,7 +329,8 @@ where
323329
// This matches the Nova optimization where τ[l₀] is tracked in eval_eq_left.
324330
e_in_pyramid.pop();
325331

326-
// Reuse the precomputed eq pyramids when all planned small-value rounds succeeded.
332+
// Reuse the precomputed eq pyramids when all planned small-value rounds
333+
// succeeded under the satisfying-witness shortcut.
327334
eq_sumcheck::EqSumCheckInstance::<E>::from_pyramids(
328335
e_in_pyramid,
329336
e_xout_pyramid,
@@ -407,9 +414,9 @@ mod tests {
407414
type E = PallasHyraxEngine;
408415
type F = <E as Engine>::Scalar;
409416

410-
/// Generic helper to test that SmallValueSumCheck produces the same polynomial
411-
/// evaluations as EqSumCheckInstance across multiple rounds.
412-
fn run_smallvalue_round_test<SV>()
417+
/// Generic helper to test that `SmallValueSumCheck` produces the same
418+
/// polynomial evaluations as `EqSumCheckInstance` on satisfying inputs.
419+
fn run_satisfying_smallvalue_round_test<SV>()
413420
where
414421
SV: SmallValue + Mul<Output = SV> + TryFrom<usize>,
415422
<SV as TryFrom<usize>>::Error: Debug,
@@ -423,7 +430,7 @@ mod tests {
423430
.map(|i| F::from((i + 2) as u64))
424431
.collect::<Vec<_>>();
425432

426-
// Small-value polynomials for build_accumulators_spartan
433+
// Small-value polynomials for build_accumulators_spartan_satisfying
427434
let az_small: Vec<SV> = (0..n).map(|i| SV::try_from(i + 1).unwrap()).collect();
428435
let bz_small: Vec<SV> = (0..n).map(|i| SV::try_from(i + 3).unwrap()).collect();
429436
let cz_small: Vec<SV> = az_small
@@ -447,8 +454,9 @@ mod tests {
447454
// Claim = 0 for satisfying witness (Az·Bz = Cz)
448455
let mut claim = F::ZERO;
449456

450-
// Build accumulators using the simplified API
451-
let (accs, _, _) = build_accumulators_spartan(&az_poly, &bz_poly, &taus, SMALL_VALUE_ROUNDS);
457+
// Build accumulators using the satisfying-witness API.
458+
let (accs, _, _) =
459+
build_accumulators_spartan_satisfying(&az_poly, &bz_poly, &taus, SMALL_VALUE_ROUNDS);
452460
let mut small_value = SmallValueSumCheck::from_accumulators(accs);
453461

454462
// Full eq_instance for verification against standard sumcheck
@@ -512,17 +520,17 @@ mod tests {
512520
}
513521

514522
#[test]
515-
fn test_smallvalue_round_matches_eq_instance_evals_i32() {
516-
run_smallvalue_round_test::<i32>();
523+
fn test_satisfying_smallvalue_round_matches_eq_instance_evals_i32() {
524+
run_satisfying_smallvalue_round_test::<i32>();
517525
}
518526

519527
#[test]
520-
fn test_smallvalue_round_matches_eq_instance_evals_i64() {
521-
run_smallvalue_round_test::<i64>();
528+
fn test_satisfying_smallvalue_round_matches_eq_instance_evals_i64() {
529+
run_satisfying_smallvalue_round_test::<i64>();
522530
}
523531

524-
/// Test that prove_cubic_small_value produces identical
525-
/// output to prove_cubic_with_three_inputs using synthetic small-value polynomials.
532+
/// Test that `prove_cubic_small_value_satisfying` produces identical output
533+
/// to `prove_cubic_with_three_inputs` on satisfying synthetic inputs.
526534
///
527535
/// Uses synthetic Az, Bz values in a small range and computes Cz = Az * Bz.
528536
fn run_equivalence_test<SV>(num_vars: usize)
@@ -599,7 +607,7 @@ mod tests {
599607
.expect("standard prove should succeed");
600608

601609
// Run small-value method
602-
let (proof2, r2, evals2) = prove_cubic_small_value::<E, SV, 3>(
610+
let (proof2, r2, evals2) = prove_cubic_small_value_satisfying::<E, SV, 3>(
603611
&claim,
604612
taus.clone(),
605613
&az_small_poly,
@@ -633,7 +641,7 @@ mod tests {
633641
assert_eq!(final_claim, expected, "final claim mismatch");
634642
}
635643

636-
/// Test small-value sumcheck equivalence with synthetic polynomials.
644+
/// Test satisfying small-value sumcheck equivalence with synthetic polynomials.
637645
///
638646
/// Tests multiple sizes to ensure equivalence holds for various l0 values.
639647
/// With LB=3, l0 = min(3, num_vars - 1):
@@ -643,21 +651,21 @@ mod tests {
643651
/// - num_vars=6: l0=3, suffix_vars=3
644652
/// - num_vars=10: l0=3, suffix_vars=7
645653
#[test]
646-
fn test_sumcheck_equivalence_with_synthetic_i32() {
654+
fn test_satisfying_sumcheck_equivalence_with_synthetic_i32() {
647655
for num_vars in [2, 3, 4, 6, 10] {
648656
run_equivalence_test::<i32>(num_vars);
649657
}
650658
}
651659

652660
#[test]
653-
fn test_sumcheck_equivalence_with_synthetic_i64() {
661+
fn test_satisfying_sumcheck_equivalence_with_synthetic_i64() {
654662
for num_vars in [2, 3, 4, 6, 10] {
655663
run_equivalence_test::<i64>(num_vars);
656664
}
657665
}
658666

659667
#[test]
660-
fn test_sumcheck_equivalence_when_first_tau_is_zero() {
668+
fn test_satisfying_sumcheck_equivalence_when_first_tau_is_zero() {
661669
const NUM_VARS: usize = 6;
662670
const SEED: u64 = 0xDEADBEEF;
663671
let mut rng = StdRng::seed_from_u64(SEED);
@@ -697,7 +705,7 @@ mod perf_tests {
697705
#[cfg(not(debug_assertions))]
698706
const TEST_SIZES: &[usize] = &[16, 18, 20, 22, 24];
699707

700-
fn test_small_value_sumcheck_with<E: Engine>()
708+
fn test_satisfying_small_value_sumcheck_with<E: Engine>()
701709
where
702710
E::Scalar: SmallValueEngine<i64>,
703711
{
@@ -733,7 +741,7 @@ mod perf_tests {
733741
num_vars = num_vars
734742
);
735743

736-
let (proof, _r, _evals) = prove_cubic_small_value::<E, _, 3>(
744+
let (proof, _r, _evals) = prove_cubic_small_value_satisfying::<E, _, 3>(
737745
&E::Scalar::ZERO,
738746
taus.clone(),
739747
&az_poly,
@@ -754,7 +762,7 @@ mod perf_tests {
754762
}
755763

756764
#[test]
757-
fn test_small_value_sumcheck_perf() {
765+
fn test_satisfying_small_value_sumcheck_perf() {
758766
let _ = tracing_subscriber::fmt()
759767
.with_target(false)
760768
.with_ansi(true)
@@ -764,14 +772,14 @@ mod perf_tests {
764772
use crate::provider::Bn254Engine;
765773

766774
// Always test with BN254
767-
test_small_value_sumcheck_with::<Bn254Engine>();
775+
test_satisfying_small_value_sumcheck_with::<Bn254Engine>();
768776

769777
// Additional engines only in release builds
770778
#[cfg(not(debug_assertions))]
771779
{
772780
use crate::provider::{PallasHyraxEngine, T256HyraxEngine};
773-
test_small_value_sumcheck_with::<PallasHyraxEngine>();
774-
test_small_value_sumcheck_with::<T256HyraxEngine>();
781+
test_satisfying_small_value_sumcheck_with::<PallasHyraxEngine>();
782+
test_satisfying_small_value_sumcheck_with::<T256HyraxEngine>();
775783
}
776784
}
777785
}

0 commit comments

Comments
 (0)