From 8ff711a6bf6491a6ec604c21f08f7e4b8c2550c8 Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Wed, 25 Mar 2026 01:18:11 +0100 Subject: [PATCH 1/5] core: plonky3 integration --- Cargo.toml | 32 +- benches/eval_multilinear.rs | 20 +- benches/evaluate.rs | 10 +- benches/sumcheck.rs | 10 +- benches/whir.rs | 6 +- src/bin/main.rs | 6 +- src/constraints/evaluator.rs | 266 ------ src/constraints/mod.rs | 760 ---------------- src/constraints/statement/eq.rs | 777 ---------------- src/constraints/statement/initial.rs | 466 ---------- src/constraints/statement/mod.rs | 19 - src/constraints/statement/select.rs | 1228 -------------------------- src/fiat_shamir/domain_separator.rs | 317 ------- src/fiat_shamir/errors.rs | 13 - src/fiat_shamir/mod.rs | 5 - src/fiat_shamir/pattern.rs | 128 --- src/fiat_shamir/tests.rs | 54 -- src/lib.rs | 5 +- src/parameters/folding.rs | 266 ------ src/parameters/mod.rs | 56 -- src/parameters/soundness.rs | 551 ------------ src/parameters/whir.rs | 741 ---------------- src/sumcheck/data.rs | 634 ------------- src/sumcheck/error.rs | 17 - src/sumcheck/lagrange.rs | 354 -------- src/sumcheck/mod.rs | 37 - src/sumcheck/product_polynomial.rs | 1072 ---------------------- src/sumcheck/prover.rs | 532 ----------- src/sumcheck/svo.rs | 675 -------------- src/sumcheck/tests.rs | 588 ------------ src/whir/committer/reader.rs | 14 +- src/whir/committer/writer.rs | 14 +- src/whir/mod.rs | 11 +- src/whir/proof.rs | 4 +- src/whir/prover/mod.rs | 20 +- src/whir/prover/round_state/state.rs | 4 +- src/whir/prover/round_state/tests.rs | 33 +- src/whir/verifier/mod.rs | 14 +- 38 files changed, 94 insertions(+), 9665 deletions(-) delete mode 100644 src/constraints/evaluator.rs delete mode 100644 src/constraints/mod.rs delete mode 100644 src/constraints/statement/eq.rs delete mode 100644 src/constraints/statement/initial.rs delete mode 100644 src/constraints/statement/mod.rs delete mode 100644 src/constraints/statement/select.rs delete mode 100644 src/fiat_shamir/domain_separator.rs delete mode 100644 src/fiat_shamir/errors.rs delete mode 100644 src/fiat_shamir/mod.rs delete mode 100644 src/fiat_shamir/pattern.rs delete mode 100644 src/fiat_shamir/tests.rs delete mode 100644 src/parameters/folding.rs delete mode 100644 src/parameters/mod.rs delete mode 100644 src/parameters/soundness.rs delete mode 100644 src/parameters/whir.rs delete mode 100644 src/sumcheck/data.rs delete mode 100644 src/sumcheck/error.rs delete mode 100644 src/sumcheck/lagrange.rs delete mode 100644 src/sumcheck/mod.rs delete mode 100644 src/sumcheck/product_polynomial.rs delete mode 100644 src/sumcheck/prover.rs delete mode 100644 src/sumcheck/svo.rs delete mode 100644 src/sumcheck/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 6b861dff..0cf2709e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,22 +41,22 @@ serde = { version = "1.0", default-features = false, features = [ "alloc", ] } rayon = { version = "1.0", optional = true } -p3-field = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-baby-bear = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-koala-bear = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-merkle-tree = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-symmetric = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-matrix = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-commit = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-goldilocks = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-dft = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-util = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-maybe-rayon = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139", features = [ -] } -p3-interpolation = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-challenger = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-multilinear-util = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } -p3-keccak = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b0fa5139" } +p3-field = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-baby-bear = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-koala-bear = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-merkle-tree = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-symmetric = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-matrix = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-commit = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-goldilocks = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-dft = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-util = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-maybe-rayon = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-interpolation = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-challenger = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-multilinear-util = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-keccak = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-whir = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } hashbrown = "0.16" itertools = { version = "0.14.0", default-features = false, features = [ diff --git a/benches/eval_multilinear.rs b/benches/eval_multilinear.rs index 75289c62..2fb52c7c 100644 --- a/benches/eval_multilinear.rs +++ b/benches/eval_multilinear.rs @@ -1,7 +1,7 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use p3_baby_bear::BabyBear; use p3_field::{ExtensionField, Field, extension::BinomialExtensionField}; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; +use p3_multilinear_util::{evals::Poly, multilinear::Point}; use rand::{ RngExt, SeedableRng, distr::{Distribution, StandardUniform}, @@ -12,7 +12,7 @@ type F = BabyBear; type EF = BinomialExtensionField; // Generate random evaluation data and a random point. -fn setup(n_vars: usize) -> (EvaluationsList, MultilinearPoint) +fn setup(n_vars: usize) -> (Poly, Point) where F: Field, EF: ExtensionField, @@ -20,9 +20,9 @@ where { let mut rng = SmallRng::seed_from_u64(1); let evals: Vec = (0..1 << n_vars).map(|_| rng.random()).collect(); - let evals = EvaluationsList::new(evals); + let evals = Poly::new(evals); let point_vec: Vec = (0..n_vars).map(|_| rng.random()).collect(); - let point = MultilinearPoint::new(point_vec); + let point = Point::new(point_vec); (evals, point) } @@ -37,10 +37,8 @@ fn bench_eval_multilinear_base(c: &mut Criterion) { let bench_id = BenchmarkId::new("packed-split", num_vars); group.bench_with_input(bench_id, &num_vars, |b, &n_vars| { - let routine = |(evals, point): (EvaluationsList, MultilinearPoint)| { - let _ = std::hint::black_box( - evals.evaluate_hypercube_base(std::hint::black_box(&point)), - ); + let routine = |(evals, point): (Poly, Point)| { + let _ = std::hint::black_box(evals.eval_base(std::hint::black_box(&point))); }; b.iter_batched( || setup::(n_vars), @@ -63,10 +61,8 @@ fn bench_eval_multilinear_ext(c: &mut Criterion) { let bench_id = BenchmarkId::new("packed-split", num_vars); group.bench_with_input(bench_id, &num_vars, |b, &n_vars| { - let routine = |(evals, point): (EvaluationsList, MultilinearPoint)| { - let _ = std::hint::black_box( - evals.evaluate_hypercube_ext::(std::hint::black_box(&point)), - ); + let routine = |(evals, point): (Poly, Point)| { + let _ = std::hint::black_box(evals.eval_ext::(std::hint::black_box(&point))); }; b.iter_batched( || setup::(n_vars), diff --git a/benches/evaluate.rs b/benches/evaluate.rs index 498cac39..29f7c73d 100644 --- a/benches/evaluate.rs +++ b/benches/evaluate.rs @@ -1,18 +1,18 @@ use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; use p3_field::extension::BinomialExtensionField; use p3_koala_bear::KoalaBear; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; +use p3_multilinear_util::{evals::Poly, multilinear::Point}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; type F = KoalaBear; type EF4 = BinomialExtensionField; -fn generate_test_case(num_variables: usize) -> (EvaluationsList, MultilinearPoint) { +fn generate_test_case(num_variables: usize) -> (Poly, Point) { let mut rng = SmallRng::seed_from_u64(1); let num_coeffs = 1 << num_variables; let coeffs = (0..num_coeffs).map(|_| rng.random()).collect(); - let point = MultilinearPoint::new((0..num_variables).map(|_| rng.random()).collect()); - (EvaluationsList::new(coeffs), point) + let point = Point::new((0..num_variables).map(|_| rng.random()).collect()); + (Poly::new(coeffs), point) } fn benchmark_evaluate(c: &mut Criterion) { @@ -28,7 +28,7 @@ fn benchmark_evaluate(c: &mut Criterion) { b.iter_batched( || (poly.clone(), point.clone()), |(poly, point)| { - let _ = poly.evaluate_hypercube_base(&point); + let _ = poly.eval_base(&point); }, BatchSize::SmallInput, ); diff --git a/benches/sumcheck.rs b/benches/sumcheck.rs index 4a6f43d9..d72cd9f5 100644 --- a/benches/sumcheck.rs +++ b/benches/sumcheck.rs @@ -3,7 +3,7 @@ use p3_baby_bear::{BabyBear, Poseidon2BabyBear}; use p3_challenger::{DuplexChallenger, FieldChallenger}; use p3_field::{Field, extension::BinomialExtensionField}; use p3_merkle_tree::MerkleTreeMmcs; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; +use p3_multilinear_util::{evals::Poly, multilinear::Point}; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; use whir_p3::{ @@ -48,10 +48,10 @@ fn setup_challenger() -> MyChallenger { } /// Helper to generate a random multilinear polynomial. -fn generate_poly(num_vars: usize) -> EvaluationsList { +fn generate_poly(num_vars: usize) -> Poly { let mut rng = SmallRng::seed_from_u64(1); let evals = (0..1 << num_vars).map(|_| rng.random()).collect(); - EvaluationsList::new(evals) + Poly::new(evals) } /// Main benchmark function to test the sumcheck prover. @@ -85,7 +85,7 @@ fn bench_sumcheck_prover(c: &mut Criterion) { SumcheckStrategy::Classic, ); for _ in 0..3 { - let _ = initial_statement.evaluate(&MultilinearPoint::expand_from_univariate( + let _ = initial_statement.evaluate(&Point::expand_from_univariate( challenger.sample_algebra_element(), *num_vars, )); @@ -133,7 +133,7 @@ fn bench_sumcheck_prover(c: &mut Criterion) { SumcheckStrategy::Svo, ); for _ in 0..3 { - let _ = initial_statement.evaluate(&MultilinearPoint::expand_from_univariate( + let _ = initial_statement.evaluate(&Point::expand_from_univariate( challenger.sample_algebra_element(), *num_vars, )); diff --git a/benches/whir.rs b/benches/whir.rs index 509ece60..799cd6b1 100644 --- a/benches/whir.rs +++ b/benches/whir.rs @@ -4,7 +4,7 @@ use p3_dft::Radix2DFTSmallBatch; use p3_field::{Field, extension::BinomialExtensionField}; use p3_koala_bear::{KoalaBear, Poseidon2KoalaBear}; use p3_merkle_tree::MerkleTreeMmcs; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; +use p3_multilinear_util::{evals::Poly, multilinear::Point}; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; use whir_p3::{ @@ -95,11 +95,11 @@ fn prepare_inputs() -> ( let mut rng = SmallRng::seed_from_u64(0); // Sample a random multilinear polynomial over `F`, represented by its evaluations. - let polynomial = EvaluationsList::::new((0..num_coeffs).map(|_| rng.random()).collect()); + let polynomial = Poly::::new((0..num_coeffs).map(|_| rng.random()).collect()); // Build a simple initial statement with one evaluation constraint. let mut initial_statement = params.initial_statement(polynomial, SumcheckStrategy::Svo); - let _ = initial_statement.evaluate(&MultilinearPoint::rand(&mut rng, num_variables)); + let _ = initial_statement.evaluate(&Point::rand(&mut rng, num_variables)); // Fiat-Shamir setup diff --git a/src/bin/main.rs b/src/bin/main.rs index 998fd3e4..15284561 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -6,7 +6,7 @@ use p3_dft::Radix2DFTSmallBatch; use p3_field::{Field, extension::BinomialExtensionField}; use p3_koala_bear::{KoalaBear, Poseidon2KoalaBear}; use p3_merkle_tree::MerkleTreeMmcs; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; +use p3_multilinear_util::{evals::Poly, multilinear::Point}; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{ RngExt, SeedableRng, @@ -125,13 +125,13 @@ fn main() { let params = WhirConfig::::new(num_variables, whir_params.clone()); let mut rng = StdRng::seed_from_u64(0); - let polynomial = EvaluationsList::::new((0..num_coeffs).map(|_| rng.random()).collect()); + let polynomial = Poly::::new((0..num_coeffs).map(|_| rng.random()).collect()); let mut initial_statement = params.initial_statement(polynomial, SumcheckStrategy::default()); // Sample `num_points` random multilinear points in the Boolean hypercube // And add constraints for each sampled point (equality constraints) (0..num_evaluations).for_each(|_| { - let _ = initial_statement.evaluate(&MultilinearPoint::rand(&mut rng, num_variables)); + let _ = initial_statement.evaluate(&Point::rand(&mut rng, num_variables)); }); let verifier_statement = initial_statement.normalize(); diff --git a/src/constraints/evaluator.rs b/src/constraints/evaluator.rs deleted file mode 100644 index a3a1d7df..00000000 --- a/src/constraints/evaluator.rs +++ /dev/null @@ -1,266 +0,0 @@ -use p3_field::{ExtensionField, Field, TwoAdicField}; -use p3_multilinear_util::multilinear::MultilinearPoint; - -use crate::{constraints::Constraint, parameters::FoldingFactor}; - -/// Evaluate a single round's constraint. -fn eval_round + TwoAdicField>( - constraint: &Constraint, - point: &MultilinearPoint, -) -> EF { - let point = point - .get_subpoint_over_range(0..constraint.num_variables()) - .reversed(); - // Evaluate eq and sel constraints at the computed point - let eq_contribution = constraint - .iter_eqs() - .map(|(pt, coeff)| pt.eq_poly(&point) * coeff) - .sum::(); - - let sel_contribution = constraint - .iter_sels() - .map(|(&var, coeff)| { - let expanded = - MultilinearPoint::expand_from_univariate(var, constraint.num_variables()); - coeff * expanded.select_poly(&point) - }) - .sum::(); - - eq_contribution + sel_contribution -} - -/// Lightweight evaluator for the combined constraint polynomial W(r). -#[derive(Clone, Debug)] -pub struct ConstraintPolyEvaluator { - /// The folding factor. - pub folding_factor: FoldingFactor, -} - -impl ConstraintPolyEvaluator { - /// Creates a new `ConstraintPolyEvaluator` with the given parameters. - #[must_use] - pub const fn new(folding_factor: FoldingFactor) -> Self { - Self { folding_factor } - } - - /// Evaluate the combined constraint polynomial W(r). - /// - /// ## Key Insight - /// Constraint i needs evaluation point matching its polynomial's remaining variables. - /// This means using challenges from prover round i onwards + final sumcheck. - #[must_use] - pub fn eval_constraints_poly + TwoAdicField>( - &self, - constraints: &[Constraint], - point: &MultilinearPoint, - ) -> EF { - constraints - .iter() - .map(|constraint| eval_round(constraint, point)) - .sum() - } -} - -#[cfg(test)] -mod tests { - use alloc::{vec, vec::Vec}; - - use p3_baby_bear::BabyBear; - use p3_field::{PrimeCharacteristicRing, extension::BinomialExtensionField}; - use p3_multilinear_util::evals::EvaluationsList; - use proptest::prelude::*; - use rand::{RngExt, SeedableRng, rngs::SmallRng}; - - use super::*; - use crate::{ - constraints::statement::{EqStatement, SelectStatement}, - parameters::FoldingFactor, - }; - - type F = BabyBear; - type EF = BinomialExtensionField; - - #[test] - fn test_eval_constraints_poly() { - // -- Test Configuration -- - // We use 20 variables to ensure a non-trivial number of folding rounds. - let num_vars = 20; - // A constant folding factor of 5 is used. - let folding_factor = FoldingFactor::Constant(5); - // This configuration implies a 3-round folding schedule before the final polynomial: - // Round 0: 20 vars -> 15 vars - // Round 1: 15 vars -> 10 vars - // Round 2: 10 vars -> 5 vars (final polynomial) - - // We will add a varying number of constraints in each round. - let num_eq_constraints_per_round = &[2usize, 3, 1]; - let num_sel_constraints_per_round = &[31usize, 41, 51]; - - // Initialize a deterministic random number generator for reproducibility. - let mut rng = SmallRng::seed_from_u64(0); - - // -- Random Constraints and Challenges -- - // This block generates the inputs that the verifier would receive in a real proof. - let mut num_vars_at_round = num_vars; - let mut constraints = vec![]; - - // Generate eq and select constraints and challenges for each of the 3 rounds. - for (round_idx, (&num_eq, &num_sel)) in num_eq_constraints_per_round - .iter() - .zip(num_sel_constraints_per_round.iter()) - .enumerate() - { - // Generate a random combination challenge for this round. - let gamma = rng.random(); - // Create eq statement for the current domain size (20, then 15, then 10). - let mut eq_statement = EqStatement::initialize(num_vars_at_round); - (0..num_eq).for_each(|_| { - eq_statement.add_evaluated_constraint( - MultilinearPoint::rand(&mut rng, num_vars_at_round), - rng.random(), - ); - }); - - // Create select statement for the current domain size (20, then 15, then 10). - let mut sel_statement = SelectStatement::::initialize(num_vars_at_round); - (0..num_sel).for_each(|_| sel_statement.add_constraint(rng.random(), rng.random())); - constraints.push(Constraint::new(gamma, eq_statement, sel_statement)); - - // Shrink the number of variables for the next round. - num_vars_at_round -= folding_factor.at_round(round_idx); - } - - // Generate the final, full 20-dimensional challenge point `r`. - let final_point = MultilinearPoint::rand(&mut rng, num_vars); - - // Calculate W(r) using the function under test - let evaluator = ConstraintPolyEvaluator::new(folding_factor); - let result_from_eval_poly = evaluator.eval_constraints_poly(&constraints, &final_point); - - // Calculate W(r) by materializing and evaluating round-by-round - // This simpler, more direct method serves as our ground truth. - // Loop through each round to calculate its contribution to the final evaluation. - let expected_result = constraints - .iter() - .map(|constraint| { - let num_vars = constraint.num_variables(); - let mut combined = EvaluationsList::zero(num_vars); - let mut eval = EF::ZERO; - constraint.combine(&mut combined, &mut eval); - let point = final_point.get_subpoint_over_range(0..num_vars).reversed(); - combined.evaluate_hypercube_ext::(&point) - }) - .sum::(); - - // The result from the recursive function must match the materialized ground truth. - assert_eq!(result_from_eval_poly, expected_result); - } - - proptest! { - #[test] - fn prop_eval_constraints_poly( - (num_vars, folding_factor_val) in (10..=20usize) - .prop_flat_map(|n| ( - Just(n), - 2..=(n / 2) - )) - ) { - // `Tracks the number of variables remaining before each round. - let mut num_vars_current = num_vars; - // The folding factor is constant for all rounds. - let folding_factor = FoldingFactor::Constant(folding_factor_val); - // Will store the number of variables folded in each specific round. - let mut folding_factors_vec = vec![]; - // We simulate the folding process to build the schedule. - // - // The protocol folds variables until 0 remain. - while num_vars_current > 0 { - // In each round, we fold `folding_factor_val` variables. - // - // If this would leave fewer than 0 variables, we fold just enough to reach 0. - let num_to_fold = core::cmp::min(folding_factor_val, num_vars_current); - // This check avoids an infinite loop if `num_vars_current` gets stuck. - if num_to_fold == 0 { break; } - // Record the number of variables folded in this round. - folding_factors_vec.push(num_to_fold); - // Decrease the variable count for the next round. - num_vars_current -= num_to_fold; - } - // The total number of folding rounds. - let num_rounds = folding_factors_vec.len(); - - // Use a seeded RNG for a reproducible test run. - let mut rng = SmallRng::seed_from_u64(0); - // For each round, generate a random number of constraints (from 0 to 8). - let num_eq_constraints_per_round: Vec = (0..num_rounds) - .map(|_| rng.random_range(0..=2)) - .collect(); - let num_sel_constraints_per_round: Vec = (0..num_rounds) - .map(|_| rng.random_range(0..=2)) - .collect(); - - // -- Random Constraints and Challenges -- - // This block generates the inputs that the verifier would receive in a real proof. - let mut num_vars_current = num_vars; - let mut constraints = vec![]; - - // Generate eq and select constraints and alpha challenges for each of the 3 rounds. - for (round_idx, (&num_eq, &num_sel)) in num_eq_constraints_per_round - .iter() - .zip(num_sel_constraints_per_round.iter()) - .enumerate() - { - // Generate a random combination scalar (alpha) for this round. - let gamma = rng.random(); - // Create eq statement for the current domain size (20, then 15, then 10). - let mut eq_statement = EqStatement::initialize(num_vars_current); - (0..num_eq).for_each(|_| { - eq_statement.add_evaluated_constraint( - MultilinearPoint::rand(&mut rng, num_vars_current), - rng.random(), - ); - }); - - // Create select statement for the current domain size (20, then 15, then 10). - let mut sel_statement = SelectStatement::::initialize(num_vars_current); - (0..num_sel).for_each(|_| sel_statement.add_constraint(rng.random(), rng.random())); - constraints.push(Constraint::new(gamma, eq_statement, sel_statement)); - - // Shrink the number of variables for the next round. - num_vars_current -= folding_factors_vec[round_idx]; - } - - // Generate the final, full n-dimensional challenge point `r`. - let final_point = MultilinearPoint::rand(&mut rng, num_vars); - - - // Calculate W(r) using the function under test - // - // This is the recursive method we want to validate. - let evaluator = ConstraintPolyEvaluator::new(folding_factor); - let result_from_eval_poly = - evaluator.eval_constraints_poly(&constraints, &final_point); - - // Calculate W(r) by materializing and evaluating round-by-round - // - // This simpler, more direct method serves as our ground truth. - let mut num_vars_at_round = num_vars; - // Loop through each round to calculate its contribution to the final evaluation. - let expected_result = constraints - .iter() - .enumerate() - .map(|(round_idx, constraint)| { - let point = final_point.get_subpoint_over_range(0..num_vars_at_round).reversed(); - let mut combined = EvaluationsList::zero(constraint.num_variables()); - let mut eval = EF::ZERO; - constraint.combine(&mut combined, &mut eval); - num_vars_at_round -= folding_factors_vec[round_idx]; - combined.evaluate_hypercube_ext::(&point) - }) - .sum::(); - - // The result from the recursive function must match the materialized ground truth. - prop_assert_eq!(result_from_eval_poly, expected_result); - } - } -} diff --git a/src/constraints/mod.rs b/src/constraints/mod.rs deleted file mode 100644 index e22319f6..00000000 --- a/src/constraints/mod.rs +++ /dev/null @@ -1,760 +0,0 @@ -use p3_field::{ExtensionField, Field, PackedValue}; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; -use p3_util::log2_strict_usize; - -use crate::constraints::statement::{EqStatement, SelectStatement}; - -/// Constraint evaluation utilities. -pub mod evaluator; - -/// Statement types for polynomial evaluation constraints. -pub mod statement; - -/// A combined constraint system with equality and selection statements. -/// -/// This struct represents a unified constraint system that combines: -/// - **Equality constraints**: Polynomial evaluations at specific points -/// - **Select constraints**: Selection-based polynomial evaluations -/// -/// Both constraint types are batched using powers of a random challenge `γ`. -/// -/// # Mathematical Structure -/// -/// Given `n_eq` equality constraints and `n_sel` select constraints, the combined -/// constraint polynomial is: -/// -/// ```text -/// W(X) = Σ_{i=0}^{n_eq-1} γ^i · eq(X, z_eq_i) + Σ_{j=0}^{n_sel-1} γ^{n_eq+j} · select(pow(z_sel_j), X) -/// ``` -/// -/// The combined expected evaluation is: -/// -/// ```text -/// S = Σ_{i=0}^{n_eq-1} γ^i · s_eq_i + Σ_{j=0}^{n_sel-1} γ^{n_eq+j} · s_sel_j -/// ``` -#[derive(Clone, Debug)] -pub struct Constraint> { - /// Equality-based evaluation constraints of the form `p(z_i) = s_i`. - /// - /// Each constraint specifies a point `z_i` and expected evaluation `s_i`. - pub eq_statement: EqStatement, - - /// Selection-based evaluation constraints of the form `p(z_j) = s_j`. - /// - /// Each constraint specifies a univariate value `z_j` that is expanded - /// via the power map to create a multilinear evaluation point. - pub sel_statement: SelectStatement, - - /// Random challenge `γ` used for batching constraints. - /// - /// Powers of this challenge weight different constraints: - /// - Equality constraints use `γ^0, γ^1, ..., γ^{n_eq-1}` - /// - Select constraints use `γ^{n_eq}, γ^{n_eq+1}, ..., γ^{n_eq+n_sel-1}` - pub challenge: EF, -} - -impl> Constraint { - /// Creates a new constraint combining equality and select statements. - /// - /// This constructor initializes a unified constraint system that batches both - /// equality-based and selection-based polynomial evaluation constraints using - /// powers of the provided challenge. - /// - /// # Parameters - /// - /// - `challenge`: Random challenge `γ` for batching constraints - /// - `eq_statement`: Equality constraints `p(z_i) = s_i` - /// - `sel_statement`: Selection constraints via power map expansion - /// - /// # Panics - /// - /// Panics if the number of variables differs between statements. - /// - /// # Invariant - /// - /// Both statements must operate over the same number of variables to ensure - /// the combined weight polynomial is well-defined. - #[must_use] - pub const fn new( - challenge: EF, - eq_statement: EqStatement, - sel_statement: SelectStatement, - ) -> Self { - // Verify that both statements have the same number of variables. - // - // This ensures the combined polynomial has a consistent domain. - assert!(eq_statement.num_variables() == sel_statement.num_variables()); - - // Construct the combined constraint with both statement types. - Self { - eq_statement, - sel_statement, - challenge, - } - } - - /// Creates a constraint with only equality statements. - /// - /// This is a convenience constructor for the common case where only equality - /// constraints are needed. An empty select statement is automatically created - /// with the same number of variables. - /// - /// # Parameters - /// - /// - `challenge`: Random challenge `γ` for batching eq constraints - /// - `eq_statement`: Equality constraints `p(z_i) = s_i` - /// - /// # Returns - /// - /// A `Constraint` with the given equality statement and an empty select statement. - #[must_use] - pub const fn new_eq_only(challenge: EF, eq_statement: EqStatement) -> Self { - // Extract the number of variables from the equality statement. - let num_variables = eq_statement.num_variables(); - - // Create a constraint with an empty select statement that has - // the same number of variables as the equality statement. - Self::new( - challenge, - eq_statement, - SelectStatement::initialize(num_variables), - ) - } - - /// Returns the number of variables in the constraint polynomial. - /// - /// This value determines the dimension of the Boolean hypercube `{0,1}^k` - /// over which the constraint polynomial is evaluated. - /// - /// # Returns - /// - /// The number of variables `k` shared by both statement types. - #[must_use] - pub const fn num_variables(&self) -> usize { - // The number of variables is determined by the equality statement. - // - // By construction, the select statement has the same value. - self.eq_statement.num_variables() - } - - /// Combines expected evaluations using challenge powers. - /// - /// This accumulates the weighted sum of all expected constraint evaluations: - /// ```text - /// eval += Σ_{i=0}^{n_eq-1} γ^i · s_eq_i + Σ_{j=0}^{n_sel-1} γ^{n_eq+j} · s_sel_j - /// ``` - /// - /// # Parameters - /// - /// - `eval`: Mutable accumulator for the combined expected evaluation - /// - /// # Implementation Notes - /// - /// The equality statement uses challenge powers `γ^0, γ^1, ...` - /// The select statement continues with powers `γ^{n_eq}, γ^{n_eq+1}, ...` - /// to ensure distinct weights for all constraints. - pub fn combine_evals(&self, eval: &mut EF) { - // Accumulate equality constraint evaluations weighted by γ^i. - // - // This adds: Σ_{i=0}^{n_eq-1} γ^i · s_eq_i - self.eq_statement.combine_evals(eval, self.challenge); - - // Accumulate select constraint evaluations weighted by γ^{n_eq+j}. - // The shift ensures distinct challenge powers for each constraint. - // - // This adds: Σ_{j=0}^{n_sel-1} γ^{n_eq+j} · s_sel_j - self.sel_statement - .combine_evals(eval, self.challenge, self.eq_statement.len()); - } - - /// Combines constraint polynomials into weight polynomial and expected evaluation. - /// - /// This method accumulates both: - /// 1. The weight polynomial `W(X)` evaluated at all hypercube points - /// 2. The expected evaluation `S` as a scalar - /// - /// Both are added to the provided accumulators, allowing for incremental - /// combination across multiple constraints. - /// - /// # Parameters - /// - /// - `combined`: Accumulator for weight polynomial evaluations `W(b)` at all `b ∈ {0,1}^k` - /// - `eval`: Accumulator for the combined expected evaluation `S` - /// - /// # Mathematical Details - /// - /// Updates `combined[b]` for each `b ∈ {0,1}^k`: - /// ```text - /// combined[b] += Σ_i γ^i · eq(b, z_eq_i) + Σ_j γ^{n_eq+j} · select(pow(z_sel_j), b) - /// ``` - /// - /// Updates `eval`: - /// ```text - /// eval += Σ_i γ^i · s_eq_i + Σ_j γ^{n_eq+j} · s_sel_j - /// ``` - pub fn combine(&self, combined: &mut EvaluationsList, eval: &mut EF) { - // Combine equality constraints with accumulation enabled (INITIALIZED=true). - // This adds the equality portion of W(X) to the existing values in `combined`. - self.eq_statement - .combine_hypercube::(combined, eval, self.challenge); - - // Combine select constraints, continuing from where equality left off. - // The shift parameter ensures select constraints use distinct challenge powers. - self.sel_statement - .combine(combined, eval, self.challenge, self.eq_statement.len()); - } - - /// Combines constraint polynomials into weight polynomial and expected evaluation. - /// - /// This method accumulates both: - /// 1. The weight polynomial `W(X)` evaluated at all hypercube points - /// 2. The expected evaluation `S` as a scalar - /// - /// Both are added to the provided accumulators, allowing for incremental - /// combination across multiple constraints. - /// - /// # Parameters - /// - /// - `combined`: Accumulator for packed weight polynomial evaluations `W(b)` at all `b ∈ {0,1}^k` - /// - `eval`: Accumulator for the combined expected evaluation `S` - /// - /// # Mathematical Details - /// - /// Updates `combined[b]` for each `b ∈ {0,1}^k`: - /// ```text - /// combined[b] += Σ_i γ^i · eq(b, z_eq_i) + Σ_j γ^{n_eq+j} · select(pow(z_sel_j), b) - /// ``` - /// - /// Updates `eval`: - /// ```text - /// eval += Σ_i γ^i · s_eq_i + Σ_j γ^{n_eq+j} · s_sel_j - /// ``` - pub fn combine_packed( - &self, - combined: &mut EvaluationsList, - eval: &mut EF, - ) { - // Combine equality constraints with accumulation enabled (INITIALIZED=true). - // This adds the equality portion of W(X) to the existing values in `combined`. - self.eq_statement - .combine_hypercube_packed::(combined, eval, self.challenge); - - // Combine select constraints, continuing from where equality left off. - // The shift parameter ensures select constraints use distinct challenge powers. - self.sel_statement - .combine_packed(combined, eval, self.challenge, self.eq_statement.len()); - } - - /// Creates a new combined weight polynomial and expected evaluation. - /// - /// This is similar to [`combine`](Self::combine) but creates fresh accumulators - /// instead of adding to existing ones. - /// - /// # Returns - /// - /// A tuple `(W, S)` where: - /// - `W`: Weight polynomial evaluations at all points in `{0,1}^k` - /// - `S`: Combined expected evaluation scalar - /// - /// # Usage - /// - /// Use this method when starting a new constraint combination. - /// Use [`combine`](Self::combine) when accumulating multiple constraints. - pub fn combine_new(&self) -> (EvaluationsList, EF) { - // Initialize fresh accumulators for the weight polynomial and expected evaluation. - // The weight polynomial needs 2^k entries for the full Boolean hypercube. - let mut combined = EvaluationsList::zero(self.num_variables()); - let mut eval = EF::ZERO; - - // Combine equality constraints without accumulation (INITIALIZED=false). - // This directly writes the equality portion of W(X) to `combined`. - self.eq_statement - .combine_hypercube::(&mut combined, &mut eval, self.challenge); - - // Add select constraints to the weight polynomial and expected evaluation. - // The shift ensures select constraints use distinct challenge powers. - self.sel_statement.combine( - &mut combined, - &mut eval, - self.challenge, - self.eq_statement.len(), - ); - - // Return the completed weight polynomial and expected evaluation. - (combined, eval) - } - - /// Creates a new combined weight polynomial in packed form and expected evaluation. - /// - /// This is similar to [`combine_packed`](Self::combine_packed) but creates fresh accumulators - /// instead of adding to existing ones. - /// - /// # Returns - /// - /// A tuple `(W, S)` where: - /// - `W`: Weight polynomial evaluations at all points in `{0,1}^k` - /// - `S`: Combined expected evaluation scalar - /// - /// # Usage - /// - /// Use this method when starting a new constraint combination. - /// Use [`combine_packed`](Self::combine_packed) when accumulating multiple constraints. - pub fn combine_new_packed(&self) -> (EvaluationsList, EF) { - let k_pack = log2_strict_usize(F::Packing::WIDTH); - let k = self.num_variables(); - - // Initialize fresh accumulators for the weight polynomial and expected evaluation. - // The weight polynomial needs 2^(k-k_pack) packed entries for the full Boolean hypercube. - let mut combined = EvaluationsList::zero(k - k_pack); - let mut eval = EF::ZERO; - - // Combine equality constraints without accumulation (INITIALIZED=false). - // This directly writes the equality portion of W(X) to `combined`. - self.eq_statement.combine_hypercube_packed::( - &mut combined, - &mut eval, - self.challenge, - ); - - // Add select constraints to the weight polynomial and expected evaluation. - // The shift ensures select constraints use distinct challenge powers. - self.sel_statement.combine_packed( - &mut combined, - &mut eval, - self.challenge, - self.eq_statement.len(), - ); - - // Return the completed weight polynomial and expected evaluation. - (combined, eval) - } - - /// Iterates over equality constraints with their challenge weights. - /// - /// This produces pairs `(z_i, γ^i)` for each equality constraint where: - /// - `z_i` is the evaluation point - /// - `γ^i` is the challenge power for this constraint - /// - /// # Returns - /// - /// An iterator over `(&MultilinearPoint, EF)` pairs. - pub fn iter_eqs(&self) -> impl Iterator, EF)> { - // Pair each equality point with its corresponding challenge power. - // Points are weighted by γ^0, γ^1, γ^2, ... - self.eq_statement.points.iter().zip(self.challenge.powers()) - } - - /// Iterates over select constraints with their challenge weights. - /// - /// This produces pairs `(z_j, γ^{n_eq+j})` for each select constraint where: - /// - `z_j` is the univariate evaluation point (before power map expansion) - /// - `γ^{n_eq+j}` is the challenge power for this constraint - /// - /// # Returns - /// - /// An iterator over `(&F, EF)` pairs. - /// - /// # Implementation Notes - /// - /// Challenge powers are skipped by `n_eq` to ensure select constraints - /// use distinct powers from equality constraints. - pub fn iter_sels(&self) -> impl Iterator { - // Pair each select variable with its corresponding challenge power. - // Powers start at γ^{n_eq} to avoid overlap with equality constraints. - self.sel_statement - .vars - .iter() - .zip(self.challenge.powers().skip(self.eq_statement.len())) - } -} - -#[cfg(test)] -mod tests { - use alloc::{vec, vec::Vec}; - - use p3_baby_bear::BabyBear; - use p3_field::{PrimeCharacteristicRing, extension::BinomialExtensionField}; - - use super::*; - - /// Type alias for the base field used in tests - type F = BabyBear; - - /// Type alias for the extension field used in tests - type EF = BinomialExtensionField; - - #[test] - fn test_constraint_new() { - // Declare test parameters explicitly - - // Number of variables for the constraint system - let num_variables = 3; - - // Random challenge for batching constraints - let challenge = EF::from_u64(42); - - // Create an equality statement with 2 constraints - let eq_point_0 = - MultilinearPoint::new(vec![EF::from_u64(1), EF::from_u64(2), EF::from_u64(3)]); - let eq_eval_0 = EF::from_u64(10); - let eq_point_1 = - MultilinearPoint::new(vec![EF::from_u64(4), EF::from_u64(5), EF::from_u64(6)]); - let eq_eval_1 = EF::from_u64(20); - let eq_statement = - EqStatement::new_hypercube(vec![eq_point_0, eq_point_1], vec![eq_eval_0, eq_eval_1]); - - // Create a select statement with 1 constraint - let sel_var = F::from_u64(7); - let sel_eval = EF::from_u64(30); - let sel_statement = SelectStatement::new(num_variables, vec![sel_var], vec![sel_eval]); - - // Construct the combined constraint - let constraint: Constraint = Constraint::new(challenge, eq_statement, sel_statement); - - // Verify that the constraint was constructed with correct fields - assert_eq!(constraint.challenge, challenge); - assert_eq!(constraint.eq_statement.len(), 2); - assert_eq!(constraint.sel_statement.len(), 1); - assert_eq!(constraint.num_variables(), num_variables); - } - - #[test] - #[should_panic(expected = "assertion failed")] - fn test_constraint_new_mismatched_variables() { - // Create statements with different numbers of variables - - // Equality statement with 3 variables - let eq_point = - MultilinearPoint::new(vec![EF::from_u64(1), EF::from_u64(2), EF::from_u64(3)]); - let eq_eval = EF::from_u64(10); - let eq_statement = EqStatement::new_hypercube(vec![eq_point], vec![eq_eval]); - - // Select statement with 2 variables (different!) - let num_variables_sel = 2; - let sel_var = F::from_u64(7); - let sel_eval = EF::from_u64(30); - let sel_statement = SelectStatement::new(num_variables_sel, vec![sel_var], vec![sel_eval]); - - // Random challenge - let challenge = EF::from_u64(42); - - // This should panic due to mismatched variable counts - let _constraint = Constraint::new(challenge, eq_statement, sel_statement); - } - - #[test] - fn test_constraint_new_eq_only() { - // Declare test parameters explicitly - - // Number of variables - let num_variables = 2; - - // Random challenge - let challenge = EF::from_u64(99); - - // Create an equality statement with 3 constraints - let eq_point_0 = MultilinearPoint::new(vec![EF::from_u64(1), EF::from_u64(2)]); - let eq_eval_0 = EF::from_u64(10); - let eq_point_1 = MultilinearPoint::new(vec![EF::from_u64(3), EF::from_u64(4)]); - let eq_eval_1 = EF::from_u64(20); - let eq_point_2 = MultilinearPoint::new(vec![EF::from_u64(5), EF::from_u64(6)]); - let eq_eval_2 = EF::from_u64(30); - let eq_statement = EqStatement::new_hypercube( - vec![eq_point_0, eq_point_1, eq_point_2], - vec![eq_eval_0, eq_eval_1, eq_eval_2], - ); - - // Create constraint with only equality constraints - let constraint: Constraint = Constraint::new_eq_only(challenge, eq_statement); - - // Verify that the select statement is empty - assert_eq!(constraint.sel_statement.len(), 0); - assert!(constraint.sel_statement.is_empty()); - - // Verify that the equality statement is present - assert_eq!(constraint.eq_statement.len(), 3); - - // Verify that both statements have the same number of variables - assert_eq!(constraint.num_variables(), num_variables); - assert_eq!(constraint.sel_statement.num_variables(), num_variables); - } - - #[test] - fn test_constraint_num_variables() { - // Declare test parameters explicitly - - // Number of variables (determines hypercube dimension) - let num_variables = 4; - - // Random challenge - let challenge = EF::from_u64(42); - - // Create empty statements with the specified number of variables - let eq_statement = EqStatement::initialize(num_variables); - let sel_statement = SelectStatement::initialize(num_variables); - - // Create constraint - let constraint: Constraint = Constraint::new(challenge, eq_statement, sel_statement); - - // Verify that num_variables returns the correct value - assert_eq!(constraint.num_variables(), num_variables); - } - - #[test] - fn test_constraint_combine_evals() { - // Declare test parameters explicitly - - // Number of variables - let num_variables = 2; - - // Random challenge for batching - // We'll use γ = 2 for easy manual calculation - let gamma = EF::from_u64(2); - - // Create equality statement with 2 constraints - // Constraint 0: p(z_0) = 5, weighted by γ^0 = 1 - let eq_point_0 = MultilinearPoint::new(vec![EF::from_u64(1), EF::from_u64(1)]); - let eq_eval_0 = EF::from_u64(5); - - // Constraint 1: p(z_1) = 7, weighted by γ^1 = 2 - let eq_point_1 = MultilinearPoint::new(vec![EF::from_u64(0), EF::from_u64(1)]); - let eq_eval_1 = EF::from_u64(7); - - let eq_statement = - EqStatement::new_hypercube(vec![eq_point_0, eq_point_1], vec![eq_eval_0, eq_eval_1]); - - // Create select statement with 1 constraint - // Constraint 2: p(z_2) = 11, weighted by γ^2 = 4 - let sel_var = F::from_u64(3); - let sel_eval = EF::from_u64(11); - let sel_statement = SelectStatement::new(num_variables, vec![sel_var], vec![sel_eval]); - - // Create constraint - let constraint: Constraint = Constraint::new(gamma, eq_statement, sel_statement); - - // Initialize accumulator - let mut eval = EF::ZERO; - - // Combine evaluations - constraint.combine_evals(&mut eval); - - // Expected result: 1*5 + 2*7 + 4*11 = 5 + 14 + 44 = 63 - let expected_eval = EF::from_u64(5) - + EF::from_u64(2) * EF::from_u64(7) - + EF::from_u64(4) * EF::from_u64(11); - - assert_eq!(eval, expected_eval); - assert_eq!(eval, EF::from_u64(63)); - } - - #[test] - fn test_constraint_combine_evals_accumulation() { - // Test that combine_evals adds to existing values rather than overwriting - - // Random challenge - let challenge = EF::from_u64(3); - - // Create a simple equality-only constraint - let eq_point = MultilinearPoint::new(vec![EF::from_u64(1), EF::from_u64(1)]); - let eq_eval = EF::from_u64(10); - let eq_statement = EqStatement::new_hypercube(vec![eq_point], vec![eq_eval]); - let constraint: Constraint = Constraint::new_eq_only(challenge, eq_statement); - - // Start with a non-zero accumulator - let initial_value = EF::from_u64(100); - let mut eval = initial_value; - - // Combine evaluations (should add to existing value) - constraint.combine_evals(&mut eval); - - // Verify that the result is initial_value + (γ^0 * 10) = 100 + 10 = 110 - assert_eq!(eval, EF::from_u64(110)); - } - - #[test] - fn test_constraint_combine_new() { - // Declare test parameters explicitly - - // Number of variables (2 variables → 2^2 = 4 hypercube points) - let num_variables = 2; - - // Random challenge - let challenge = EF::from_u64(5); - - // Create a simple equality statement with 1 constraint - let eq_point = MultilinearPoint::new(vec![EF::ONE, EF::ZERO]); - let eq_eval = EF::from_u64(42); - let eq_statement = EqStatement::new_hypercube(vec![eq_point], vec![eq_eval]); - - // Create constraint (eq-only for simplicity) - let constraint: Constraint = Constraint::new_eq_only(challenge, eq_statement); - - // Combine into fresh accumulators - let (combined, eval) = constraint.combine_new(); - - // Verify that the combined weight polynomial has the correct size - // Should have 2^num_variables = 4 entries - assert_eq!(combined.num_evals(), 1 << num_variables); - assert_eq!(combined.num_evals(), 4); - - // Verify that the expected evaluation equals γ^0 * 42 = 42 - assert_eq!(eval, EF::from_u64(42)); - - // Verify that at least some entries in the weight polynomial are non-zero - let non_zero_count = combined.iter().filter(|&&x| x != EF::ZERO).count(); - assert!(non_zero_count > 0); - } - - #[test] - fn test_constraint_combine_vs_combine_new() { - // Verify that combine and combine_new produce the same results - - // Number of variables - let num_variables = 2; - - // Random challenge - let challenge = EF::from_u64(7); - - // Create a test constraint - let eq_point = MultilinearPoint::new(vec![EF::ZERO, EF::ONE]); - let eq_eval = EF::from_u64(15); - let eq_statement = EqStatement::new_hypercube(vec![eq_point], vec![eq_eval]); - let constraint: Constraint = Constraint::new_eq_only(challenge, eq_statement); - - // Method 1: Use combine_new - let (combined_new, eval_new) = constraint.combine_new(); - - // Method 2: Use combine with fresh accumulators - let mut combined_manual = EvaluationsList::zero(num_variables); - let mut eval_manual = EF::ZERO; - constraint.combine(&mut combined_manual, &mut eval_manual); - - // Verify that both methods produce identical results - assert_eq!(combined_new.num_evals(), combined_manual.num_evals()); - for (new_val, manual_val) in combined_new - .as_slice() - .iter() - .zip(combined_manual.as_slice().iter()) - { - assert_eq!(new_val, manual_val); - } - assert_eq!(eval_new, eval_manual); - } - - #[test] - fn test_constraint_iter_eqs() { - // Test iteration over equality constraints with challenge weights - - // Number of variables - let num_variables = 2; - - // Random challenge (γ = 3 for easy verification) - let gamma = EF::from_u64(3); - - // Create equality statement with 3 constraints - let eq_point_0 = MultilinearPoint::new(vec![EF::from_u64(1), EF::from_u64(2)]); - let eq_eval_0 = EF::from_u64(10); - - let eq_point_1 = MultilinearPoint::new(vec![EF::from_u64(3), EF::from_u64(4)]); - let eq_eval_1 = EF::from_u64(20); - - let eq_point_2 = MultilinearPoint::new(vec![EF::from_u64(5), EF::from_u64(6)]); - let eq_eval_2 = EF::from_u64(30); - - let eq_statement = EqStatement::new_hypercube( - vec![eq_point_0, eq_point_1, eq_point_2], - vec![eq_eval_0, eq_eval_1, eq_eval_2], - ); - - // Create constraint - let constraint: Constraint = Constraint::new_eq_only(gamma, eq_statement); - - // Collect iterator results - let results: Vec<_> = constraint.iter_eqs().collect(); - - // Verify that we have 3 pairs - assert_eq!(results.len(), 3); - - // Verify challenge weights: γ^0 = 1, γ^1 = 3, γ^2 = 9 - let expected_weights = [EF::from_u64(1), EF::from_u64(3), EF::from_u64(9)]; - - for (i, (point, coeff)) in results.iter().enumerate() { - // Verify challenge weight - assert_eq!(*coeff, expected_weights[i]); - - // Verify point reference matches original - assert_eq!(point.num_variables(), num_variables); - } - } - - #[test] - fn test_constraint_iter_sels() { - // Test iteration over select constraints with challenge weights - - // Number of variables - let num_variables = 2; - - // Random challenge (γ = 2 for easy verification) - let gamma = EF::from_u64(2); - - // Create equality statement with 2 constraints - // This will use challenge powers γ^0 and γ^1 - let eq_point_0 = MultilinearPoint::new(vec![EF::from_u64(1), EF::from_u64(2)]); - let eq_eval_0 = EF::from_u64(10); - let eq_point_1 = MultilinearPoint::new(vec![EF::from_u64(3), EF::from_u64(4)]); - let eq_eval_1 = EF::from_u64(20); - let eq_statement = - EqStatement::new_hypercube(vec![eq_point_0, eq_point_1], vec![eq_eval_0, eq_eval_1]); - - // Create select statement with 2 constraints - // These should use challenge powers γ^2 and γ^3 - let sel_var_0 = F::from_u64(5); - let sel_eval_0 = EF::from_u64(30); - let sel_var_1 = F::from_u64(6); - let sel_eval_1 = EF::from_u64(40); - let sel_statement = SelectStatement::new( - num_variables, - vec![sel_var_0, sel_var_1], - vec![sel_eval_0, sel_eval_1], - ); - - // Create constraint - let constraint: Constraint = Constraint::new(gamma, eq_statement, sel_statement); - - // Collect iterator results - let results: Vec<_> = constraint.iter_sels().collect(); - - // Verify that we have 2 pairs - assert_eq!(results.len(), 2); - - // Verify challenge weights: γ^2 = 4, γ^3 = 8 - // (skipping the first 2 powers used by equality constraints) - let expected_weights = [EF::from_u64(4), EF::from_u64(8)]; - let expected_vars = [sel_var_0, sel_var_1]; - - for (i, (var, coeff)) in results.iter().enumerate() { - // Verify challenge weight - assert_eq!(*coeff, expected_weights[i]); - - // Verify variable reference matches original - assert_eq!(**var, expected_vars[i]); - } - } - - #[test] - fn test_constraint_iter_sels_empty() { - // Test that iter_sels works correctly when there are no select constraints - - // Random challenge - let challenge = EF::from_u64(7); - - // Create equality-only constraint - let eq_point = MultilinearPoint::new(vec![EF::from_u64(1), EF::from_u64(2)]); - let eq_eval = EF::from_u64(10); - let eq_statement = EqStatement::new_hypercube(vec![eq_point], vec![eq_eval]); - let constraint: Constraint = Constraint::new_eq_only(challenge, eq_statement); - - // Verify that the iterator is empty - assert_eq!(constraint.iter_sels().count(), 0); - } -} diff --git a/src/constraints/statement/eq.rs b/src/constraints/statement/eq.rs deleted file mode 100644 index 10ead24e..00000000 --- a/src/constraints/statement/eq.rs +++ /dev/null @@ -1,777 +0,0 @@ -use alloc::vec::Vec; - -use itertools::Itertools; -use p3_field::{ - ExtensionField, Field, PackedFieldExtension, PackedValue, PrimeCharacteristicRing, dot_product, -}; -use p3_matrix::{ - Matrix, - dense::{RowMajorMatrix, RowMajorMatrixView}, -}; -use p3_maybe_rayon::prelude::*; -use p3_multilinear_util::{ - eq_batch::eval_eq_batch, evals::EvaluationsList, multilinear::MultilinearPoint, -}; -use p3_util::log2_strict_usize; -use tracing::instrument; - -/// Batched equality polynomial evaluation via butterfly expansion. -/// -/// # Overview -/// -/// Given `n` points as columns of a `k × n` matrix, computes -/// `eq(P_i, X)` for every `X in {0,1}^k` and every point `P_i`, -/// scaled by powers of `alpha`: -/// -/// ```text -/// mat[X, i] = alpha^i * eq(P_i, X) -/// ``` -/// -/// # Algorithm -/// -/// Uses the standard eq-polynomial butterfly. Each input row `i` -/// (variable) doubles the filled region: -/// -/// ```text -/// hi[j] = lo[j] * var[j] (contribution when X_i = 1) -/// lo[j] -= hi[j] (contribution when X_i = 0) -/// ``` -/// -/// The seed row is `[alpha^0, alpha^1, ..., alpha^{n-1}]`, which -/// bakes the batching weights directly into the expansion. -fn batch_eqs>( - points: RowMajorMatrixView<'_, EF>, - alpha: EF, -) -> RowMajorMatrix { - let k = points.height(); - let n = points.width(); - assert_ne!(n, 0); - - let mut mat = RowMajorMatrix::new(EF::zero_vec(n * (1 << k)), n); - - // Seed with alpha powers: column j starts with alpha^j. - mat.row_mut(0).copy_from_slice(&alpha.powers().collect_n(n)); - - // Butterfly: process one variable per step. - points.row_slices().enumerate().for_each(|(i, vars)| { - let (mut lo, mut hi) = mat.split_rows_mut(1 << i); - lo.rows_mut().zip(hi.rows_mut()).for_each(|(lo, hi)| { - vars.iter() - .zip(lo.iter_mut().zip(hi.iter_mut())) - .for_each(|(&var, (lo, hi))| { - // hi = lo * var (X_i = 1 branch) - *hi = *lo * var; - // lo = lo * (1 - var) = lo - hi (X_i = 0 branch) - *lo -= *hi; - }); - }); - }); - mat -} - -/// SIMD-packed variant of the batched equality polynomial evaluation. -/// -/// # Overview -/// -/// Produces the same result as the scalar version, but stores each -/// column's eq coefficients in packed form. One SIMD element covers -/// `Packing::WIDTH` consecutive hypercube entries. -/// -/// # Algorithm -/// -/// 1. **Packing phase** (first `k_pack` variables): For each column, -/// build the small scalar eq table over `{0,1}^{k_pack}`, then -/// pack into a single SIMD element. This fills one row of the -/// packed output. -/// -/// 2. **Butterfly phase** (remaining `k - k_pack` variables): Same -/// eq butterfly as the scalar version, but each multiply/subtract -/// operates on packed elements — all SIMD lanes in parallel. -/// -/// No alpha scaling is applied (the caller handles batching weights -/// separately via the dot product in `combine_hypercube_packed`). -fn packed_batch_eqs>( - points: RowMajorMatrixView<'_, EF>, -) -> RowMajorMatrix { - let k = points.height(); - let n = points.width(); - assert_ne!(n, 0); - let k_pack = log2_strict_usize(F::Packing::WIDTH); - assert!(k >= k_pack); - - let (init_vars, rest_vars) = points.split_rows(k_pack); - let mut mat = RowMajorMatrix::new(EF::ExtensionPacking::zero_vec(n * (1 << (k - k_pack))), n); - - if k_pack > 0 { - // Packing phase: build a scalar eq table per column over - // the first k_pack variables, then pack into one SIMD element. - // - // The transpose gives us one row per column of the input, - // containing the k_pack variable values for that point. - init_vars - .transpose() - .row_slices() - .zip(mat.values.iter_mut()) - .for_each(|(vars, packed)| { - // Reverse order: the butterfly processes variables from - // MSB to LSB, but new_from_point expects LSB-first. - let point = vars.iter().rev().copied().collect::>(); - *packed = EF::ExtensionPacking::from_ext_slice( - EvaluationsList::new_from_point(&point, EF::ONE).as_slice(), - ); - }); - } else { - // No packing needed: WIDTH = 1, seed row is all ones. - mat.row_mut(0).fill(EF::ExtensionPacking::ONE); - } - - // Butterfly phase: same eq expansion as the scalar version, - // but operating on packed elements for SIMD parallelism. - rest_vars.row_slices().enumerate().for_each(|(i, vars)| { - let (mut lo, mut hi) = mat.split_rows_mut(1 << i); - lo.rows_mut().zip(hi.rows_mut()).for_each(|(lo, hi)| { - vars.iter() - .zip(lo.iter_mut().zip(hi.iter_mut())) - .for_each(|(&var, (lo, hi))| { - // hi = lo * var (X_i = 1 branch) - *hi = *lo * var; - // lo = lo * (1 - var) = lo - hi (X_i = 0 branch) - *lo -= *hi; - }); - }); - }); - mat -} - -/// A batched system of evaluation constraints $p(z_i) = s_i$ on $\{0,1\}^m$. -/// -/// Each entry ties a Boolean point `z_i` to an expected polynomial evaluation `s_i`. -/// -/// Batching with a random challenge $\gamma$ produces a single combined weight -/// polynomial $W$ and a single scalar $S$ that summarize all constraints. -/// -/// Invariants -/// ---------- -/// - `points.len() == evaluations.len()`. -/// - Every `MultilinearPoint` in `points` has exactly `num_variables` coordinates. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct EqStatement { - /// Number of variables in the multilinear polynomial space. - num_variables: usize, - /// List of evaluation points. - pub(crate) points: Vec>, - /// List of target evaluations. - pub(crate) evaluations: Vec, -} - -impl EqStatement { - /// Creates an empty `EqStatement` for polynomials with `num_variables` variables. - #[must_use] - pub const fn initialize(num_variables: usize) -> Self { - Self { - num_variables, - points: Vec::new(), - evaluations: Vec::new(), - } - } - - /// Returns the number of variables defining the polynomial space. - #[must_use] - pub const fn num_variables(&self) -> usize { - self.num_variables - } - - /// Returns true if the statement contains no constraints. - #[must_use] - pub const fn is_empty(&self) -> bool { - debug_assert!(self.points.is_empty() == self.evaluations.is_empty()); - self.points.is_empty() - } - - /// Returns an iterator over the evaluation constraints in the statement. - pub fn iter(&self) -> impl Iterator, &F)> { - self.points.iter().zip(self.evaluations.iter()) - } - - /// Returns the number of constraints in the statement. - #[must_use] - pub const fn len(&self) -> usize { - debug_assert!(self.points.len() == self.evaluations.len()); - self.points.len() - } - - /// Verifies that a given polynomial satisfies all constraints in the statement. - #[must_use] - pub fn verify(&self, poly: &EvaluationsList) -> bool { - self.iter() - .all(|(point, &expected_eval)| poly.evaluate_hypercube_base(point) == expected_eval) - } - - /// Concatenates another statement's constraints into this one. - pub fn concatenate(&mut self, other: &Self) { - assert_eq!(self.num_variables, other.num_variables); - self.points.extend_from_slice(&other.points); - self.evaluations.extend_from_slice(&other.evaluations); - } - - /// Adds an evaluation constraint `p(z) = s` to the system. - /// - /// Assumes the evaluation `s` is already known. - /// - /// # Panics - /// Panics if the number of variables in the `point` does not match the statement. - pub fn add_evaluated_constraint(&mut self, point: MultilinearPoint, eval: F) { - assert_eq!(point.num_variables(), self.num_variables()); - self.points.push(point); - self.evaluations.push(eval); - } - - /// Combine all constraints into a single batched weight polynomial and expected sum. - /// - /// Computes `W(x) = sum_i gamma^i * eq(x, z_i)` for all `x in {0,1}^k` - /// using the Plonky3 `eval_eq_batch` kernel, and accumulates the - /// scalar sum `S = sum_i gamma^i * s_i`. - /// - /// The `INITIALIZED` const generic controls whether the accumulator - /// is added to (true) or overwritten (false). - #[instrument(skip_all, fields(num_constraints = self.len(), num_variables = self.num_variables()))] - pub fn combine_hypercube( - &self, - acc_weights: &mut EvaluationsList, - acc_sum: &mut F, - challenge: F, - ) where - Base: Field, - F: ExtensionField, - { - if self.points.is_empty() { - return; - } - - let num_constraints = self.len(); - - // Precompute challenge powers gamma^0, gamma^1, ..., gamma^{n-1}. - let challenges = challenge.powers().collect_n(num_constraints); - - // Transpose the points into a k × n matrix (rows = variables, - // columns = constraint points). Uses Plonky3's transpose which - // writes directly into a flat buffer. - let points_matrix = MultilinearPoint::transpose(&self.points, false); - - // Delegate to Plonky3's batched eq-polynomial kernel. - // This is the hot path — computes all 2^k evaluations in one pass. - eval_eq_batch::( - points_matrix.as_view(), - acc_weights.as_mut_slice(), - &challenges, - ); - - // Accumulate the scalar target sum: S += sum_i gamma^i * s_i. - *acc_sum += - dot_product::(self.evaluations.iter().copied(), challenges.into_iter()); - } - - /// SIMD-packed variant of constraint batching on the hypercube. - /// - /// Produces the same result as the scalar version but stores the - /// weight polynomial in packed form (one element per - /// `Packing::WIDTH` consecutive hypercube entries). - /// - /// # Algorithm - /// - /// For small `k` (where `2 * k_pack > k`), falls back to a - /// per-constraint naive loop that builds each eq polynomial - /// separately and packs it chunk-by-chunk. - /// - /// For larger `k`, uses the split-and-dot strategy: - /// - /// 1. Transpose the points and split at `k / 2`. - /// 2. Left half → `packed_batch_eqs` (SIMD lanes). - /// 3. Right half → `batch_eqs` (scalar, with alpha scaling). - /// 4. Parallel dot product: for each right-half row, dot all - /// left-half rows weighted by the scalar eq values. - #[instrument(skip_all, fields(num_constraints = self.len(), num_variables = self.num_variables()))] - pub fn combine_hypercube_packed( - &self, - weights: &mut EvaluationsList, - sum: &mut F, - challenge: F, - ) where - Base: Field, - F: ExtensionField, - { - if self.points.is_empty() { - return; - } - - let k = self.num_variables(); - let k_pack = log2_strict_usize(Base::Packing::WIDTH); - assert!(k >= k_pack); - assert_eq!(weights.num_variables() + k_pack, k); - - // Combine expected evaluations: S = ∑_i γ^i * s_i - self.combine_evals(sum, challenge); - - // Apply naive method if number of variables is too small for packed split method - if k_pack * 2 > k { - self.points - .iter() - .zip(challenge.powers()) - .enumerate() - .for_each(|(i, (point, challenge))| { - let eq = EvaluationsList::new_from_point(point.as_slice(), challenge); - weights - .as_mut_slice() - .iter_mut() - .zip_eq(eq.as_slice().chunks(Base::Packing::WIDTH)) - .for_each(|(out, chunk)| { - let packed = F::ExtensionPacking::from_ext_slice(chunk); - if INITIALIZED || i > 0 { - *out += packed; - } else { - *out = packed; - } - }); - }); - return; - } - - let points = MultilinearPoint::transpose(&self.points, true); - let (left, right) = points.split_rows(k / 2); - let left = packed_batch_eqs::(left); - let right = batch_eqs::(right, challenge); - - weights - .as_mut_slice() - .par_chunks_mut(left.height()) - .zip_eq(right.par_row_slices()) - .for_each(|(out, right)| { - out.iter_mut().zip(left.rows()).for_each(|(out, left)| { - if INITIALIZED { - *out += - dot_product::(left, right.iter().copied()); - } else { - *out = dot_product(left, right.iter().copied()); - } - }); - }); - } - - /// Combines a list of evals into a single linear combination using powers of `gamma`, - /// and updates the running claimed_eval in place. - /// - /// # Arguments - /// - `claimed_eval`: Mutable reference to the total accumulated claimed eval so far. Updated in place. - /// - `gamma`: A random extension field element used to weight the evals. - pub fn combine_evals(&self, claimed_eval: &mut F, gamma: F) { - *claimed_eval += dot_product(self.evaluations.iter().copied(), gamma.powers()); - } -} - -#[cfg(test)] -mod tests { - use alloc::vec; - - use p3_baby_bear::BabyBear; - use p3_field::{PrimeCharacteristicRing, extension::BinomialExtensionField}; - use proptest::prelude::*; - use rand::{RngExt, SeedableRng, rngs::SmallRng}; - - use super::*; - - impl EqStatement { - /// Creates a filled `EqStatement` for polynomials with `num_variables` variables. - /// - /// # Standard Hypercube Representation - /// - /// This constructor is for the standard case where the polynomial is represented as - /// evaluations over the Boolean hypercube `{0,1}^num_variables`, and will be evaluated - /// at arbitrary constraint points using standard multilinear interpolation. Each point - /// has exactly `num_variables` coordinates. - #[must_use] - pub fn new_hypercube(points: Vec>, evaluations: Vec) -> Self { - // Validate that we have one evaluation per point. - assert_eq!( - points.len(), - evaluations.len(), - "Number of points ({}) must match number of evaluations ({})", - points.len(), - evaluations.len() - ); - - // Validate that each point has the correct number of variables. - let num_variables = points - .iter() - .map(MultilinearPoint::num_variables) - .all_equal_value() - .unwrap(); - Self { - num_variables, - points, - evaluations, - } - } - } - - type F = BabyBear; - type EF = BinomialExtensionField; - - #[test] - fn test_statement_combine_single_constraint() { - let mut statement = EqStatement::initialize(1); - let point = MultilinearPoint::new(vec![F::ONE]); - let expected_eval = F::from_u64(7); - statement.add_evaluated_constraint(point.clone(), expected_eval); - - let challenge = F::from_u64(2); // This is unused with one constraint. - let mut combined_evals = EvaluationsList::zero(statement.num_variables()); - let mut combined_sum = F::ZERO; - statement.combine_hypercube::<_, false>(&mut combined_evals, &mut combined_sum, challenge); - - // Expected evals for eq_z(X) where z = (1). - // For x=0, eq=0. For x=1, eq=1. - let expected_combined_evals = EvaluationsList::new_from_point(point.as_slice(), F::ONE); - - assert_eq!(combined_evals, expected_combined_evals); - assert_eq!(combined_sum, expected_eval); - } - - #[test] - fn test_statement_with_multiple_constraints() { - let mut statement = EqStatement::initialize(2); - - // Constraint 1: evaluate at z1 = (1,0), expected value 5 - let point1 = MultilinearPoint::new(vec![F::ONE, F::ZERO]); - let eval1 = F::from_u64(5); - statement.add_evaluated_constraint(point1.clone(), eval1); - - // Constraint 2: evaluate at z2 = (0,1), expected value 7 - let point2 = MultilinearPoint::new(vec![F::ZERO, F::ONE]); - let eval2 = F::from_u64(7); - statement.add_evaluated_constraint(point2.clone(), eval2); - - let challenge = F::from_u64(2); - let mut combined_evals = EvaluationsList::zero(statement.num_variables()); - let mut combined_sum = F::ZERO; - statement.combine_hypercube::<_, false>(&mut combined_evals, &mut combined_sum, challenge); - - // Expected evals: W(X) = eq_z1(X) + challenge * eq_z2(X) - let expected_eq1 = EvaluationsList::new_from_point(point1.as_slice(), F::ONE); - let expected_eq2 = EvaluationsList::new_from_point(point2.as_slice(), challenge); - let expected_combined_evals = EvaluationsList::new( - expected_eq1 - .iter() - .zip(expected_eq2.iter()) - .map(|(&a, &b)| a + b) - .collect(), - ); - - // Expected sum: S = s1 + challenge * s2 - let expected_combined_sum = eval1 + challenge * eval2; - - assert_eq!(combined_evals, expected_combined_evals); - assert_eq!(combined_sum, expected_combined_sum); - } - - #[test] - fn test_compute_evaluation_weight() { - // Define an evaluation weight at a specific point - let point = MultilinearPoint::new(vec![F::from_u64(3)]); - - // Define a randomness point for folding - let folding_randomness = MultilinearPoint::new(vec![F::from_u64(2)]); - - // Expected result is the evaluation of eq_poly at the given randomness - let expected = point.eq_poly(&folding_randomness); - - assert_eq!(point.eq_poly(&folding_randomness), expected); - } - - #[test] - fn test_constructors_and_basic_properties() { - // Test new_hypercube constructor - let point = MultilinearPoint::new(vec![F::ONE]); - let eval = F::from_u64(42); - let statement = EqStatement::new_hypercube(vec![point], vec![eval]); - - assert_eq!(statement.num_variables(), 1); - assert_eq!(statement.len(), 1); - assert!(!statement.is_empty()); - - // Test initialize constructor - let empty_statement = EqStatement::::initialize(2); - assert_eq!(empty_statement.num_variables(), 2); - assert_eq!(empty_statement.len(), 0); - assert!(empty_statement.is_empty()); - } - - #[test] - fn test_verify_constraints() { - // Create polynomial with evaluations [1, 2] - let poly = EvaluationsList::new(vec![F::from_u64(1), F::from_u64(2)]); - let mut statement = EqStatement::::initialize(1); - - // Test matching constraint: f(0) = 1 - statement.add_evaluated_constraint(MultilinearPoint::new(vec![F::ZERO]), F::from_u64(1)); - assert!(statement.verify(&poly)); - - // Test mismatched constraint: f(1) = 5 (but poly has f(1) = 2) - statement.add_evaluated_constraint(MultilinearPoint::new(vec![F::ONE]), F::from_u64(5)); - assert!(!statement.verify(&poly)); - } - - #[test] - fn test_concatenate() { - // Test successful concatenation - let mut statement1 = EqStatement::::initialize(1); - let mut statement2 = EqStatement::::initialize(1); - statement1.add_evaluated_constraint(MultilinearPoint::new(vec![F::ZERO]), F::from_u64(10)); - statement2.add_evaluated_constraint(MultilinearPoint::new(vec![F::ONE]), F::from_u64(20)); - - statement1.concatenate(&statement2); - assert_eq!(statement1.len(), 2); - } - - #[test] - #[should_panic(expected = "assertion `left == right` failed")] - fn test_concatenate_mismatched_variables() { - let mut statement1 = EqStatement::::initialize(2); - let statement2 = EqStatement::::initialize(3); - statement1.concatenate(&statement2); // Should panic - } - - #[test] - fn test_add_evaluated_constraint() { - let poly = EvaluationsList::new(vec![F::from_u64(1), F::from_u64(2)]); - let point = MultilinearPoint::new(vec![F::ZERO]); - - let mut statement = EqStatement::::initialize(1); - - // Add constraint with pre-computed evaluation - let eval = poly.evaluate_hypercube_base(&point); - statement.add_evaluated_constraint(point, eval); - - // Statement should have one constraint - assert_eq!(statement.len(), 1); - - // Should verify against the polynomial - assert!(statement.verify(&poly)); - - // Points should be stored - assert_eq!(statement.points.len(), 1); - } - - #[test] - #[should_panic(expected = "assertion `left == right` failed")] - fn test_wrong_variable_count() { - let mut statement = EqStatement::::initialize(1); - let wrong_point = MultilinearPoint::new(vec![F::ONE, F::ZERO]); // 2 vars for 1-var statement - statement.add_evaluated_constraint(wrong_point, F::from_u64(5)); - } - - #[test] - fn test_combine_operations() { - // Test empty statement combine - let empty_statement = EqStatement::::initialize(1); - - let mut combined_evals = EvaluationsList::zero(empty_statement.num_variables()); - let mut combined_sum = F::ZERO; - empty_statement.combine_hypercube::<_, false>( - &mut combined_evals, - &mut combined_sum, - F::from_u64(42), - ); - assert_eq!(combined_sum, F::ZERO); - - // Test combine_evals with constraints - let mut statement = EqStatement::::initialize(1); - statement.add_evaluated_constraint(MultilinearPoint::new(vec![F::ZERO]), F::from_u64(3)); - statement.add_evaluated_constraint(MultilinearPoint::new(vec![F::ONE]), F::from_u64(7)); - - let mut claimed_eval = F::ZERO; - statement.combine_evals(&mut claimed_eval, F::from_u64(2)); - - // Verify: 3*1 + 7*2 = 17 - assert_eq!(claimed_eval, F::from_u64(17)); - } - - proptest! { - #[test] - fn prop_statement_workflow( - // Random 4-var polynomial: 16 evaluations (2^4) - poly_evals in prop::collection::vec(0u32..100, 16), - // Random challenge value - challenge in 1u32..50, - // Random constraint points (4 coords × 2 points) - point_coords in prop::collection::vec(0u32..10, 8), - ) { - // Create a 4-variable polynomial from random evaluations - let poly = EvaluationsList::new(poly_evals.into_iter().map(F::from_u32).collect()); - - // Create statement with random constraints that match the polynomial - let mut statement = EqStatement::::initialize(4); - let point1 = MultilinearPoint::new(vec![ - F::from_u32(point_coords[0]), F::from_u32(point_coords[1]), - F::from_u32(point_coords[2]), F::from_u32(point_coords[3]) - ]); - let point2 = MultilinearPoint::new(vec![ - F::from_u32(point_coords[4]), F::from_u32(point_coords[5]), - F::from_u32(point_coords[6]), F::from_u32(point_coords[7]) - ]); - - // Add constraints: poly(point1) = actual_eval1, poly(point2) = actual_eval2 - let eval1 = poly.evaluate_hypercube_base(&point1); - let eval2 = poly.evaluate_hypercube_base(&point2); - statement.add_evaluated_constraint(point1, eval1); - statement.add_evaluated_constraint(point2, eval2); - - // Statement should verify against polynomial (consistent constraints) - prop_assert!(statement.verify(&poly)); - - // Combine constraints with challenge - let gamma = F::from_u32(challenge); - let mut combined_poly = EvaluationsList::zero(statement.num_variables()); - let mut combined_sum = F::ZERO; - statement.combine_hypercube::<_, false>(&mut combined_poly, &mut combined_sum, gamma); - - // Combined polynomial should have same number of variables - prop_assert_eq!(combined_poly.num_variables(), 4); - - // Combined evaluations should match combine result - let mut claimed_eval = F::ZERO; - statement.combine_evals(&mut claimed_eval, gamma); - // Both methods should give same sum - prop_assert_eq!(combined_sum, claimed_eval); - - // Adding wrong constraint should break verification - let wrong_point = MultilinearPoint::new(vec![F::ZERO, F::ZERO, F::ZERO, F::ZERO]); - // Obviously wrong evaluation - let wrong_eval = F::from_u32(999); - let actual_eval = poly.evaluate_hypercube_base(&wrong_point); - // Only test if actually different - if wrong_eval != actual_eval { - statement.add_evaluated_constraint(wrong_point, wrong_eval); - // Should fail verification - prop_assert!(!statement.verify(&poly)); - } - } - } - - #[test] - #[should_panic(expected = "Number of points (2) must match number of evaluations (1)")] - fn test_new_mismatched_lengths() { - // Should panic when points.len() != evaluations.len() - let points = vec![ - MultilinearPoint::new(vec![F::ONE]), - MultilinearPoint::new(vec![F::ZERO]), - ]; - let evaluations = vec![F::from_u64(100)]; - - let _ = EqStatement::new_hypercube(points, evaluations); - } - - proptest! { - #[test] - fn prop_packed_combine_roundtrip( - // Number of variables (covers both naive and split paths). - k in 4usize..10, - // Number of constraints per batch. - n in 1usize..12, - // RNG seed for reproducible randomness. - seed in 0u64..100, - ) { - let k_pack = log2_strict_usize(::Packing::WIDTH); - if k < k_pack { - // Skip configurations where k is too small for packing. - return Ok(()); - } - - let mut rng = SmallRng::seed_from_u64(seed); - let challenge: EF = rng.random(); - - // Generate n random constraint points in {F}^k. - let points = (0..n) - .map(|_| MultilinearPoint::rand(&mut rng, k)) - .collect::>(); - // Generate n random expected evaluations. - let evals = (0..n).map(|_| rng.random()).collect::>(); - - let statement = EqStatement::::new_hypercube(points, evals); - - // Scalar path: combine into a 2^k evaluation list. - let mut scalar_weights = EvaluationsList::::zero(k); - let mut scalar_sum = EF::ZERO; - statement.combine_hypercube::( - &mut scalar_weights, &mut scalar_sum, challenge, - ); - - // Packed path: combine into a 2^{k - k_pack} packed list. - let mut packed_weights = - EvaluationsList::<>::ExtensionPacking>::zero(k - k_pack); - let mut packed_sum = EF::ZERO; - statement.combine_hypercube_packed::( - &mut packed_weights, &mut packed_sum, challenge, - ); - - // Unpack the packed result and compare element-by-element. - let unpacked = - <>::ExtensionPacking as PackedFieldExtension>::to_ext_iter( - packed_weights.as_slice().iter().copied(), - ) - .collect::>(); - prop_assert_eq!(scalar_weights.as_slice(), &unpacked[..]); - - // The scalar sums must match exactly. - prop_assert_eq!(scalar_sum, packed_sum); - } - - #[test] - fn prop_packed_combine_accumulation( - k in 4usize..10, - seed in 0u64..50, - ) { - let k_pack = log2_strict_usize(::Packing::WIDTH); - if k < k_pack { - return Ok(()); - } - - let mut rng = SmallRng::seed_from_u64(seed); - let challenge: EF = rng.random(); - - // First batch: INITIALIZED=false (overwrite). - let points1 = (0..3) - .map(|_| MultilinearPoint::rand(&mut rng, k)) - .collect::>(); - let evals1 = (0..3).map(|_| rng.random()).collect::>(); - let stmt1 = EqStatement::::new_hypercube(points1, evals1); - - let mut s_wt = EvaluationsList::::zero(k); - let mut s_sum = EF::ZERO; - stmt1.combine_hypercube::(&mut s_wt, &mut s_sum, challenge); - - let mut p_wt = - EvaluationsList::<>::ExtensionPacking>::zero(k - k_pack); - let mut p_sum = EF::ZERO; - stmt1.combine_hypercube_packed::(&mut p_wt, &mut p_sum, challenge); - - // Second batch: INITIALIZED=true (accumulate on top). - let points2 = (0..5) - .map(|_| MultilinearPoint::rand(&mut rng, k)) - .collect::>(); - let evals2 = (0..5).map(|_| rng.random()).collect::>(); - let stmt2 = EqStatement::::new_hypercube(points2, evals2); - - stmt2.combine_hypercube::(&mut s_wt, &mut s_sum, challenge); - stmt2.combine_hypercube_packed::(&mut p_wt, &mut p_sum, challenge); - - // Verify accumulated results match. - let unpacked = - <>::ExtensionPacking as PackedFieldExtension>::to_ext_iter( - p_wt.as_slice().iter().copied(), - ) - .collect::>(); - prop_assert_eq!(s_wt.as_slice(), &unpacked[..]); - prop_assert_eq!(s_sum, p_sum); - } - } -} diff --git a/src/constraints/statement/initial.rs b/src/constraints/statement/initial.rs deleted file mode 100644 index ad713c18..00000000 --- a/src/constraints/statement/initial.rs +++ /dev/null @@ -1,466 +0,0 @@ -//! Initial statement handling for the WHIR protocol. -//! -//! This module manages the polynomial and constraint system during the initial -//! phase of the protocol. It supports two execution strategies: -//! -//! - **Classic**: Standard sumcheck with explicit constraint batching. -//! -//! - **SVO**: Split-Value Optimization for faster proving on large polynomials. -//! -//! # Overview -//! -//! The prover commits to a multilinear polynomial `p: {0,1}^n → F` and must later -//! prove evaluations at verifier-chosen points. The initial statement captures: -//! -//! 1. The polynomial `p` in evaluation form over the Boolean hypercube. -//! -//! 2. A set of evaluation constraints `p(z_i) = s_i` accumulated during the protocol. - -use alloc::{vec, vec::Vec}; - -use p3_field::{ExtensionField, Field, PackedValue}; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; -use p3_util::log2_strict_usize; - -use crate::{ - constraints::statement::EqStatement, parameters::SumcheckStrategy, sumcheck::svo::SplitEq, -}; - -/// Manages the polynomial and constraints during the initial phase of WHIR. -#[derive(Clone, Debug)] -pub struct InitialStatement> { - /// The multilinear polynomial in evaluation form over `{0,1}^n`. - /// - /// Stores `2^n` evaluations on the Boolean hypercube. - pub(crate) poly: EvaluationsList, - - /// Inner constraint state, either classic or SVO-based. - pub(crate) inner: InitialStatementInner, -} - -/// Inner state for constraint tracking. -/// -/// Allows switching between two constraint representations: -/// -/// - **Classic**: Batches equality constraints explicitly. Each constraint `p(z_i) = s_i` -/// is stored as a (point, value) pair and combined using a random challenge. -/// -/// - **SVO**: Exploits the structure of the equality polynomial for faster proving. -/// Pre-computes accumulators to avoid materializing large intermediate tables. -/// Only beneficial when the polynomial is large enough. -#[derive(Clone, Debug)] -pub(crate) enum InitialStatementInner> { - /// Classic sumcheck representation. - /// - /// Stores evaluation constraints as explicit (point, value) pairs. - Classic(EqStatement), - - /// Split-Value Optimization representation. - /// - /// Uses pre-computed accumulators for faster sumcheck proving. - Svo { - /// Folding factor for the first SVO round. - l0: usize, - - /// Split equality polynomials for each constraint. - split_eqs: Vec>, - }, -} - -impl> InitialStatement { - /// Creates a statement using the classic sumcheck strategy. - /// - /// Initializes an empty constraint set that accumulates as the protocol progresses. - const fn new_classic(poly: EvaluationsList) -> Self { - let num_variables = poly.num_variables(); - Self { - poly, - inner: InitialStatementInner::new_classic(num_variables), - } - } - - /// Creates a statement using the SVO strategy. - /// - /// Initializes an empty SVO state that accumulates split equality constraints. - const fn new_svo(poly: EvaluationsList, l0: usize) -> Self { - Self { - poly, - inner: InitialStatementInner::new_svo(l0), - } - } - - /// Creates a new initial statement with the specified strategy. - /// - /// Automatically selects the appropriate internal representation based on the - /// strategy and polynomial size. For SVO, falls back to classic if the polynomial - /// is too small to benefit from the optimization. - /// - /// # SVO Requirements - /// - /// The SVO strategy requires: - /// - /// ```text - /// k > 2 * log2(SIMD_WIDTH) + l0 - /// ``` - /// - /// where `k` is the number of variables. This ensures enough parallelism - /// for packed field operations. - #[must_use] - pub const fn new(poly: EvaluationsList, l0: usize, strategy: SumcheckStrategy) -> Self { - match strategy { - SumcheckStrategy::Classic => Self::new_classic(poly), - SumcheckStrategy::Svo => { - let k = poly.num_variables(); - // Check if polynomial is large enough for SVO optimization. - if k > 2 * log2_strict_usize(F::Packing::WIDTH) + l0 { - Self::new_svo(poly, l0) - } else { - // Fallback to classic for small polynomials. - Self::new_classic(poly) - } - } - } - } - - /// Evaluates the polynomial at the given point and records the constraint. - /// - /// Steps: - /// - /// 1. Computes `p(point)` using multilinear interpolation. - /// - /// 2. Records the constraint `p(point) = eval` in the inner state. - /// - /// 3. Returns the computed evaluation. - /// - /// - For classic strategy, adds an explicit constraint. - /// - For SVO, creates a split equality polynomial with pre-computed accumulators. - /// - /// # Panics - /// - /// Panics if the point's dimension doesn't match the polynomial's. - #[must_use] - pub fn evaluate(&mut self, point: &MultilinearPoint) -> EF { - assert_eq!( - point.num_variables(), - self.num_variables(), - "Point has {} variables but statement expects {}", - point.num_variables(), - self.num_variables() - ); - self.inner.evaluate(point, &self.poly) - } - - /// Returns the number of variables in the polynomial. - /// - /// This is `n` where the polynomial is defined over `{0,1}^n`. - pub(crate) const fn num_variables(&self) -> usize { - self.poly.num_variables() - } - - /// Converts the statement to a normalized equality statement representation. - /// - /// - For classic statements, returns a clone of the inner state. - /// - For SVO statements, extracts the (point, evaluation) pairs from each - /// split equality polynomial. - /// - /// Useful for verification or when the explicit constraint form is needed. - #[must_use] - pub fn normalize(&self) -> EqStatement { - match &self.inner { - InitialStatementInner::Classic(statement) => statement.clone(), - InitialStatementInner::Svo { split_eqs, .. } => { - let points: Vec<_> = split_eqs.iter().map(|se| se.point.clone()).collect(); - let evals: Vec<_> = split_eqs.iter().map(|se| se.eval).collect(); - - let mut statement = EqStatement::initialize(self.num_variables()); - points - .iter() - .zip(evals.iter()) - .for_each(|(pt, &ev)| statement.add_evaluated_constraint(pt.clone(), ev)); - statement - } - } - } -} - -impl> InitialStatementInner { - /// Creates a new classic inner state with no constraints. - const fn new_classic(num_variables: usize) -> Self { - Self::Classic(EqStatement::initialize(num_variables)) - } - - /// Creates a new SVO inner state with no constraints. - const fn new_svo(l0: usize) -> Self { - Self::Svo { - split_eqs: vec![], - l0, - } - } - - /// Evaluates the polynomial and records the constraint. - /// - /// - For classic: computes the evaluation and adds it to the constraint set. - /// - For SVO: creates a split equality polynomial with pre-computed accumulators. - pub(crate) fn evaluate( - &mut self, - point: &MultilinearPoint, - poly: &EvaluationsList, - ) -> EF { - match self { - Self::Classic(statement) => { - let eval = poly.evaluate_hypercube_base(point); - statement.add_evaluated_constraint(point.clone(), eval); - eval - } - Self::Svo { split_eqs, l0 } => { - let split_eq = SplitEq::new(point, *l0, poly); - let eval = split_eq.eval; - split_eqs.push(split_eq); - eval - } - } - } -} - -#[cfg(test)] -mod tests { - use p3_baby_bear::BabyBear; - use p3_field::{PrimeCharacteristicRing, extension::BinomialExtensionField}; - - use super::*; - - impl> InitialStatement { - /// Returns true if no constraints have been added yet. - pub(crate) const fn is_empty(&self) -> bool { - match &self.inner { - InitialStatementInner::Classic(statement) => statement.is_empty(), - InitialStatementInner::Svo { split_eqs, .. } => split_eqs.is_empty(), - } - } - - /// Returns the number of constraints currently recorded. - pub(crate) const fn len(&self) -> usize { - match &self.inner { - InitialStatementInner::Classic(statement) => statement.len(), - InitialStatementInner::Svo { split_eqs, .. } => split_eqs.len(), - } - } - } - - type F = BabyBear; - type EF = BinomialExtensionField; - - /// Creates a simple test polynomial with known evaluations. - /// - /// Returns p(x_0, x_1) with evaluations: - /// p(0,0)=1, p(0,1)=2, p(1,0)=3, p(1,1)=4 - fn make_test_poly() -> EvaluationsList { - EvaluationsList::new(vec![ - F::from_u64(1), - F::from_u64(2), - F::from_u64(3), - F::from_u64(4), - ]) - } - - /// Creates a larger test polynomial for SVO testing. - /// - /// Returns a polynomial with 2^16 evaluations (16 variables). - fn make_large_poly() -> EvaluationsList { - let evals: Vec = (0..(1 << 16)).map(|i| F::from_u64(i as u64)).collect(); - EvaluationsList::new(evals) - } - - #[test] - fn test_new_classic_strategy() { - let poly = make_test_poly(); - let statement = InitialStatement::::new(poly, 2, SumcheckStrategy::Classic); - - assert_eq!(statement.num_variables(), 2); - assert_eq!(statement.poly.num_evals(), 4); - assert!(matches!(statement.inner, InitialStatementInner::Classic(_))); - } - - #[test] - fn test_new_svo_strategy_fallback_to_classic() { - // Small polynomial should fallback to classic even with SVO strategy. - let poly = make_test_poly(); - let statement = InitialStatement::::new(poly, 2, SumcheckStrategy::Svo); - - assert!(matches!(statement.inner, InitialStatementInner::Classic(_))); - - let normalized = statement.normalize(); - assert!(normalized.is_empty()); - } - - #[test] - fn test_new_svo_strategy_with_large_poly() { - let poly = make_large_poly(); - let statement = InitialStatement::::new(poly, 4, SumcheckStrategy::Svo); - - assert!(matches!(statement.inner, InitialStatementInner::Svo { .. })); - assert_eq!(statement.num_variables(), 16); - } - - #[test] - fn test_evaluate_classic() { - let poly = make_test_poly(); - let mut statement = InitialStatement::::new(poly, 2, SumcheckStrategy::Classic); - - // Evaluate at Boolean point (0, 1). - let point = MultilinearPoint::new(vec![EF::ZERO, EF::ONE]); - let eval = statement.evaluate(&point); - - // p(0, 1) = 2 - assert_eq!(eval, EF::from_u64(2)); - assert_eq!(statement.normalize().len(), 1); - } - - #[test] - fn test_evaluate_extension_point() { - let poly = make_test_poly(); - let mut statement = - InitialStatement::::new(poly.clone(), 2, SumcheckStrategy::Classic); - - // Evaluate at non-Boolean extension field point. - let point = MultilinearPoint::new(vec![EF::from_u64(3), EF::from_u64(7)]); - let eval = statement.evaluate(&point); - - let expected = poly.evaluate_hypercube_base(&point); - assert_eq!(eval, expected); - } - - #[test] - fn test_multiple_evaluations() { - let poly = make_test_poly(); - let mut statement = - InitialStatement::::new(poly.clone(), 2, SumcheckStrategy::Classic); - - let point1 = MultilinearPoint::new(vec![EF::ZERO, EF::ZERO]); - let point2 = MultilinearPoint::new(vec![EF::ONE, EF::ONE]); - let point3 = MultilinearPoint::new(vec![EF::from_u64(5), EF::from_u64(7)]); - - let eval1 = statement.evaluate(&point1); - let eval2 = statement.evaluate(&point2); - let eval3 = statement.evaluate(&point3); - - assert_eq!(eval1, EF::from_u64(1)); // p(0, 0) = 1 - assert_eq!(eval2, EF::from_u64(4)); // p(1, 1) = 4 - assert_eq!(eval3, poly.evaluate_hypercube_base(&point3)); - assert_eq!(statement.normalize().len(), 3); - } - - #[test] - #[should_panic(expected = "Point has 3 variables but statement expects 2")] - fn test_evaluate_wrong_variable_count() { - let poly = make_test_poly(); - let mut statement = InitialStatement::::new(poly, 2, SumcheckStrategy::Classic); - - let wrong_point = MultilinearPoint::new(vec![EF::ONE, EF::ZERO, EF::ONE]); - let _ = statement.evaluate(&wrong_point); - } - - #[test] - fn test_normalize_classic() { - let poly = make_test_poly(); - let mut statement = InitialStatement::::new(poly, 2, SumcheckStrategy::Classic); - - let point1 = MultilinearPoint::new(vec![EF::ZERO, EF::ONE]); - let point2 = MultilinearPoint::new(vec![EF::ONE, EF::ZERO]); - let eval1 = statement.evaluate(&point1); - let eval2 = statement.evaluate(&point2); - - let normalized = statement.normalize(); - assert_eq!(normalized.len(), 2); - assert_eq!(normalized.num_variables(), 2); - - let (stored_point1, &stored_eval1) = normalized.iter().next().unwrap(); - assert_eq!(*stored_point1, point1); - assert_eq!(stored_eval1, eval1); - - let (stored_point2, &stored_eval2) = normalized.iter().nth(1).unwrap(); - assert_eq!(*stored_point2, point2); - assert_eq!(stored_eval2, eval2); - } - - #[test] - fn test_normalize_svo() { - let poly = make_large_poly(); - let mut statement = InitialStatement::::new(poly, 4, SumcheckStrategy::Svo); - - let point = MultilinearPoint::new((0..16).map(|i| EF::from_u64(i as u64)).collect()); - let eval = statement.evaluate(&point); - - let normalized = statement.normalize(); - assert_eq!(normalized.len(), 1); - - let (stored_point, &stored_eval) = normalized.iter().next().unwrap(); - assert_eq!(*stored_point, point); - assert_eq!(stored_eval, eval); - } - - #[test] - fn test_num_variables() { - let poly = make_test_poly(); - let statement = InitialStatement::::new(poly, 2, SumcheckStrategy::Classic); - assert_eq!(statement.num_variables(), 2); - - let large_poly = make_large_poly(); - let large_statement = InitialStatement::::new(large_poly, 4, SumcheckStrategy::Svo); - assert_eq!(large_statement.num_variables(), 16); - } - - #[test] - fn test_inner_classic_new() { - let inner = InitialStatementInner::::new_classic(3); - - if let InitialStatementInner::Classic(statement) = inner { - assert_eq!(statement.num_variables(), 3); - assert_eq!(statement.len(), 0); - } else { - panic!("Expected Classic variant"); - } - } - - #[test] - fn test_inner_svo_new() { - let inner = InitialStatementInner::::new_svo(5); - - if let InitialStatementInner::Svo { l0, split_eqs } = inner { - assert_eq!(l0, 5); - assert!(split_eqs.is_empty()); - } else { - panic!("Expected Svo variant"); - } - } - - #[test] - fn test_inner_evaluate_classic() { - let poly = make_test_poly(); - let mut inner = InitialStatementInner::::new_classic(2); - - let point = MultilinearPoint::new(vec![EF::ONE, EF::ZERO]); - let eval = inner.evaluate(&point, &poly); - - // p(1, 0) = 3 - assert_eq!(eval, EF::from_u64(3)); - - if let InitialStatementInner::Classic(statement) = &inner { - assert_eq!(statement.len(), 1); - } - } - - #[test] - fn test_svo_creates_split_eq() { - let poly = make_large_poly(); - let mut inner = InitialStatementInner::::new_svo(4); - - let point = MultilinearPoint::new((0..16).map(|_| EF::from_u64(1)).collect()); - let eval = inner.evaluate(&point, &poly); - - if let InitialStatementInner::Svo { split_eqs, .. } = &inner { - assert_eq!(split_eqs.len(), 1); - assert_eq!(split_eqs[0].eval, eval); - } - } -} diff --git a/src/constraints/statement/mod.rs b/src/constraints/statement/mod.rs deleted file mode 100644 index faaa1d33..00000000 --- a/src/constraints/statement/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Polynomial evaluation constraint systems for the WHIR protocol. -//! -//! Three constraint representations, each targeting a different protocol phase: -//! -//! - **Equality**: explicit multilinear points `p(z_i) = s_i`, batched via eq polynomials. -//! - **Select**: univariate evaluation claims, batched via the power-map select function. -//! - **Initial**: mutable builder that accumulates constraints during commitment. - -/// Equality-based evaluation constraints with batched eq-polynomial combination. -pub mod eq; - -/// Initial statement builder for the commitment phase. -pub mod initial; - -/// Selection-based evaluation constraints using the power-map expansion. -pub mod select; - -pub use eq::EqStatement; -pub use select::SelectStatement; diff --git a/src/constraints/statement/select.rs b/src/constraints/statement/select.rs deleted file mode 100644 index 6e079ece..00000000 --- a/src/constraints/statement/select.rs +++ /dev/null @@ -1,1228 +0,0 @@ -use alloc::vec::Vec; - -use itertools::Itertools; -use p3_field::{ - ExtensionField, Field, PackedFieldExtension, PackedValue, PrimeCharacteristicRing, dot_product, -}; -use p3_matrix::{ - Matrix, - dense::{RowMajorMatrix, RowMajorMatrixView}, -}; -use p3_maybe_rayon::prelude::*; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; -use p3_util::log2_strict_usize; -use tracing::instrument; - -/// Expand powers-of-two seeds into the full power table via butterfly. -/// -/// # Input -/// -/// A `k × n` matrix where column `j` holds the squared powers of -/// variable `v_j` in descending exponent order: -/// -/// ```text -/// row 0: [v_1^{2^{k-1}}, v_2^{2^{k-1}}, …, v_n^{2^{k-1}}] -/// row 1: [v_1^{2^{k-2}}, v_2^{2^{k-2}}, …, v_n^{2^{k-2}}] -/// ⋮ -/// row k-1: [v_1^1, v_2^1, …, v_n^1 ] -/// ``` -/// -/// # Output -/// -/// A `2^k × n` matrix where entry `[b, j] = v_j^b` (the full -/// monomial power, not just a squared power). -/// -/// # Algorithm -/// -/// Uses a binary-tree butterfly. After processing row `i` of the -/// input, the first `2^{i+1}` rows of the output are filled. -/// Each step copies the existing rows and multiplies by the -/// current squared power to fill the new rows: -/// -/// ```text -/// mat[b + 2^i, j] = mat[b, j] * points[i, j] -/// ``` -fn batch_pows(points: RowMajorMatrixView<'_, F>) -> RowMajorMatrix { - let k = points.height(); - let n = points.width(); - - let mut mat = RowMajorMatrix::new(F::zero_vec(n * (1 << k)), n); - - // Base case: v_j^0 = 1 for all j. - mat.row_mut(0).fill(F::ONE); - - // Butterfly expansion: each input row doubles the number of filled rows. - points.row_slices().enumerate().for_each(|(i, vars)| { - let (lo, mut hi) = mat.split_rows_mut(1 << i); - lo.rows().zip(hi.rows_mut()).for_each(|(lo, hi)| { - // hi[j] = lo[j] * var[j], extending the power by 2^i. - vars.iter() - .zip(lo.zip(hi.iter_mut())) - .for_each(|(&var, (lo, hi))| *hi = lo * var); - }); - }); - mat -} - -/// SIMD-packed variant of the power-table butterfly expansion. -/// -/// # Overview -/// -/// Splits the `k` input variables into two phases: -/// -/// 1. **Packing phase** (first `k_pack` variables): Builds a small -/// scalar power table per column, then packs it into a single -/// SIMD lane. This fills the first row of the packed output. -/// -/// 2. **Butterfly phase** (remaining `k - k_pack` variables): Applies -/// the same butterfly as the scalar version, but operates on -/// packed elements — multiplying all SIMD lanes in one instruction. -/// -/// # Output -/// -/// A `2^{k - k_pack} × n` matrix of packed elements, where -/// unpacking row `r` column `j` yields the `F::Packing::WIDTH` -/// consecutive scalar entries `v_j^{r * WIDTH}, …, v_j^{r * WIDTH + WIDTH - 1}`. -fn packed_batch_pows(points: RowMajorMatrixView<'_, F>) -> RowMajorMatrix { - let k = points.height(); - let n = points.width(); - assert_ne!(n, 0); - let k_pack = log2_strict_usize(F::Packing::WIDTH); - assert!(k >= k_pack); - - let (init_vars, rest_vars) = points.split_rows(k_pack); - let mut mat = RowMajorMatrix::new(F::Packing::zero_vec(n * (1 << (k - k_pack))), n); - - if k_pack > 0 { - // Packing phase: build a scalar 2^{k_pack}-row power table - // per column and pack it into one SIMD element. - init_vars - .transpose() - .row_slices() - .zip(mat.values.iter_mut()) - .for_each(|(vars, packed)| { - let point = RowMajorMatrixView::new(vars, 1); - *packed = *F::Packing::from_slice(&batch_pows(point).values); - }); - } else { - // No packing needed: WIDTH = 1, seed row is all ones. - mat.row_mut(0).fill(F::Packing::ONE); - } - - // Butterfly phase: same expansion as the scalar version, - // but each multiply operates on WIDTH lanes simultaneously. - rest_vars.row_slices().enumerate().for_each(|(i, vars)| { - let (lo, mut hi) = mat.split_rows_mut(1 << i); - lo.rows().zip(hi.rows_mut()).for_each(|(lo, hi)| { - vars.iter() - .zip(lo.zip(hi.iter_mut())) - .for_each(|(&var, (lo, hi))| *hi = lo * var); - }); - }); - mat -} - -/// A batched system of `select`-based evaluation constraints for multilinear polynomials. -/// -/// This struct represents a collection of evaluation constraints of the form `p(z_i) = s_i` -/// for a multilinear polynomial `p` over the Boolean hypercube `{0,1}^k`. -/// -/// # The Select Function -/// -/// For vectors `X, Y ∈ F^k`, the select function is defined as: -/// -/// ```text -/// select(X, Y) = ∏_i (X_i · Y_i + (1 - Y_i)) -/// ``` -/// -/// **Key Property:** When `Y ∈ {0,1}^k` is a Boolean vector and `X = pow(z)`: -/// -/// ```text -/// select(pow(z), b) = z^{int(b)} -/// ``` -/// -/// where `pow(z) = (z, z^2, z^4, ..., z^{2^{k-1}})` and `int(b)` interprets the Boolean -/// vector `b` as an integer in binary. -/// -/// **Derivation:** -/// ```text -/// select(pow(z), b) = ∏_i (z^{2^i} · b_i + (1 - b_i)) -/// = ∏_{i: b_i=1} (z^{2^i}) [since b_i ∈ {0,1}] -/// = z^{Σ_{i: b_i=1} 2^i} -/// = z^{int(b)} -/// ``` -/// -/// # Verification Claims -/// -/// Each constraint `(z_i, s_i)` in this statement asserts: -/// -/// ```text -/// Σ_{b ∈ {0,1}^k} P(b) · select(pow(z_i), b) = s_i -/// ``` -/// -/// where `P(b)` are the evaluations of the polynomial over the Boolean hypercube. -/// -/// # Batching -/// -/// Multiple constraints are batched using random challenge `γ` to produce: -/// -/// - **Weight polynomial**: `W(b) = Σ_i γ^i · select(pow(z_i), b)` -/// - **Target sum**: `S = Σ_i γ^i · s_i` -/// -/// This reduces `n` separate verification claims to a single sumcheck: -/// -/// ```text -/// Σ_{b ∈ {0,1}^k} P(b) · W(b) = S -/// ``` -#[derive(Clone, Debug)] -pub struct SelectStatement { - /// Number of variables `k` defining the Boolean hypercube `{0,1}^k`. - /// - /// This determines the dimension of the multilinear polynomial space and the size - /// of the evaluation domain (2^k points). - num_variables: usize, - - /// Evaluation points `[z_1, z_2, ..., z_n]` where each constraint checks `p(z_i) = s_i`. - /// - /// Each `z_i ∈ F` is a base field element. The `pow` map will expand it to - /// `pow(z_i) = (z_i, z_i^2, z_i^4, ..., z_i^{2^{k-1}})` for the select function. - pub(crate) vars: Vec, - - /// Expected evaluation values `[s_1, s_2, ..., s_n]` corresponding to each constraint. - /// - /// Each `s_i ∈ EF` is an extension field element representing the claimed evaluation - /// of the polynomial at point `z_i`. - evaluations: Vec, -} - -impl> SelectStatement { - /// Creates an empty select statement for polynomials over `{0,1}^k`. - /// - /// # Parameters - /// - /// - `num_variables`: The dimension `k` of the Boolean hypercube - /// - /// # Returns - /// - /// An initialized statement with no constraints, ready to accept constraints. - #[must_use] - pub const fn initialize(num_variables: usize) -> Self { - Self { - num_variables, - vars: Vec::new(), - evaluations: Vec::new(), - } - } - - /// Creates a select statement pre-populated with constraints. - /// - /// # Parameters - /// - /// - `num_variables`: The dimension `k` of the Boolean hypercube - /// - `vars`: Evaluation points `[z_1, ..., z_n]` - /// - `evaluations`: Expected values `[s_1, ..., s_n]` - /// - /// # Panics - /// - /// Panics if the nu - #[must_use] - pub const fn new(num_variables: usize, vars: Vec, evaluations: Vec) -> Self { - assert!(vars.len() == evaluations.len()); - Self { - num_variables, - vars, - evaluations, - } - } - - /// Returns the number of variables `k` defining the polynomial space dimension. - /// - /// This is the dimension of the Boolean hypercube `{0,1}^k` over which polynomials - /// are defined, containing `2^k` evaluation points. - #[must_use] - pub const fn num_variables(&self) -> usize { - self.num_variables - } - - /// Returns `true` if no constraints have been added to this statement. - #[must_use] - pub const fn is_empty(&self) -> bool { - debug_assert!(self.vars.is_empty() == self.evaluations.is_empty()); - self.vars.is_empty() - } - - /// Returns an iterator over constraint pairs `(z_i, s_i)`. - /// - /// Each pair represents one evaluation constraint: `p(z_i) = s_i`. - pub fn iter(&self) -> impl Iterator { - self.vars.iter().zip(self.evaluations.iter()) - } - - /// Returns the number of evaluation constraints `n` in this statement. - #[must_use] - pub const fn len(&self) -> usize { - debug_assert!(self.vars.len() == self.evaluations.len()); - self.vars.len() - } - - /// Verifies that a given polynomial satisfies all constraints in the statement. - /// - /// For each constraint `(z_i, s_i)`, this method interprets the evaluation table as - /// coefficients of a univariate polynomial, evaluates it at `z_i` using Horner's method, - /// and checks if the result equals the expected value `s_i`. - /// - /// For a polynomial represented by evaluations `[c_0, c_1, ..., c_{2^k-1}]`: - /// - /// ```text - /// p(z) = c_0 + z(c_1 + z(c_2 + z(...))) - /// ``` - /// - /// This is computed right-to-left as: - /// ```text - /// acc = 0 - /// for i = 2^k-1 down to 0: - /// acc = acc * z + c_i - /// ``` - /// - /// # Parameters - /// - /// - `poly`: Evaluation table treated as univariate polynomial coefficients - /// - /// # Returns - /// - /// `true` if all constraints are satisfied, `false` otherwise. - #[must_use] - pub fn verify(&self, poly: &EvaluationsList) -> bool { - self.iter().all(|(&var, &expected_eval)| { - // Evaluate the polynomial at `var` using Horner's method. - // This computes: p(var) = c_0 + var(c_1 + var(c_2 + ...)) - poly.iter() - .rfold(EF::ZERO, |result, coeff| result * var + *coeff) - == expected_eval - }) - } - - /// Adds a single evaluation constraint `p(z) = s` to the statement. - /// - /// # Parameters - /// - /// - `var`: Evaluation point `z ∈ F` - /// - `eval`: Expected evaluation value `s ∈ EF` - pub fn add_constraint(&mut self, var: F, eval: EF) { - self.vars.push(var); - self.evaluations.push(eval); - } - - /// Batches all constraints into a single weighted polynomial and target sum for sumcheck. - /// - /// Given constraints `p(z_1) = s_1, ..., p(z_n) = s_n`, this method transforms them into - /// a single sumcheck claim using random challenge `γ`: - /// - /// ```text - /// Σ_{b ∈ {0,1}^k} P(b) · W(b) = S - /// ``` - /// - /// where: - /// - **Weight polynomial**: `W(b) = Σ_i γ^{i+shift} · select(pow(z_i), b)` - /// - **Target sum**: `S = Σ_i γ^{i+shift} · s_i` - /// - /// The method computes `W(b)` for all `b ∈ {0,1}^k` and `S`, adding them to the - /// provided accumulators. - /// - /// # Parameters - /// - /// - `acc_weights`: Accumulator for the weight polynomial `W(b)`. Must have `2^k` entries. - /// This method **adds** the batched weights to existing values. - /// - /// - `acc_sum`: Accumulator for the target sum `S`. This method **adds** the batched - /// evaluations to the existing value. - /// - /// - `challenge`: Random challenge `γ ∈ EF` used for batching. - /// - /// - `shift`: Power offset for challenge. Constraint `i` uses weight `γ^{i+shift}`. - /// Allows multiple statement types to use non-overlapping challenge powers. - /// Batches all constraints into a single weighted polynomial and target sum for sumcheck. - /// - /// # Algorithm - /// - /// Three stages: - /// - /// 1. **Power map**: Build a `k × n` matrix where row `i`, column `j` - /// holds `z_j^{2^i}`. Stored as a flat row-major buffer so each - /// butterfly step reads a contiguous row (cache-friendly). - /// - /// 2. **Butterfly expansion**: Expand the power map into the full - /// `2^k × n` select matrix using the same binary-tree doubling as - /// the scalar power table. Entry `[b, j] = z_j^b`. - /// - /// 3. **Challenge combination**: Dot each row of the select matrix - /// with the challenge power vector to produce the weight polynomial. - #[instrument(skip_all, fields(num_constraints = self.len(), num_variables = self.num_variables()))] - pub fn combine( - &self, - acc_weights: &mut EvaluationsList, - acc_sum: &mut EF, - challenge: EF, - shift: usize, - ) { - // Early return for empty statement: - // - // No constraints means no contribution to the batched claim. - if self.vars.is_empty() { - return; - } - - // Extract dimensions for clarity. - // - // Number of constraints - let n = self.len(); - // Dimension of Boolean hypercube - let k = self.num_variables(); - - // --------------------------------------------------------------- - // Stage 1: Build the k × n power-of-two matrix. - // --------------------------------------------------------------- - // - // Row i contains [z_1^{2^i}, z_2^{2^i}, ..., z_n^{2^i}]. - // Stored as a flat Vec of size k * n in row-major order. - let mut pow_matrix = F::zero_vec(k * n); - for (j, &var) in self.vars.iter().enumerate() { - let mut v = var; - for i in 0..k { - // pow_matrix[i * n + j] = z_j^{2^i} - pow_matrix[i * n + j] = v; - v = v.square(); - } - } - - // --------------------------------------------------------------- - // Stage 2: Butterfly expansion into the 2^k × n select matrix. - // --------------------------------------------------------------- - // - // After iteration i, the first 2^{i+1} rows are filled. - // Entry [b, j] = z_j^b. - let mut acc = F::zero_vec((1 << k) * n); - - // Base case: z_j^0 = 1 for all j. - acc[..n].fill(F::ONE); - - for i in 0..k { - let num_existing_rows = 1 << i; - let (lo, hi) = acc.split_at_mut(num_existing_rows * n); - - // Contiguous row slice — no per-iteration allocation. - let pow_row = &pow_matrix[i * n..(i + 1) * n]; - - // For each existing row, compute the new row: - // acc[b + 2^i, j] = acc[b, j] * z_j^{2^i} - lo.par_chunks_mut(n) - .zip(hi.par_chunks_mut(n)) - .for_each(|(lo_row, hi_row)| { - pow_row - .iter() - .zip(lo_row.iter()) - .zip(hi_row.iter_mut()) - .for_each(|((&z_pow, &lo_val), hi_val)| { - *hi_val = lo_val * z_pow; - }); - }); - } - - // --------------------------------------------------------------- - // Stage 3: Combine with challenge powers. - // --------------------------------------------------------------- - - // Precompute [gamma^shift, gamma^{shift+1}, ..., gamma^{shift+n-1}]. - let challenges = challenge.powers().skip(shift).take(n).collect::>(); - - // W(b) += sum_i gamma^{i+shift} * z_i^b - acc.par_chunks(n) - .zip(acc_weights.as_mut_slice().par_iter_mut()) - .for_each(|(row, weight_out)| { - *weight_out += - dot_product::(challenges.iter().copied(), row.iter().copied()); - }); - - // S += sum_i gamma^{i+shift} * s_i - *acc_sum += - dot_product::(challenges.into_iter(), self.evaluations.iter().copied()); - } - - /// SIMD-packed variant of constraint batching. - /// - /// # Overview - /// - /// Produces the same result as the scalar version, but stores the - /// weight polynomial in packed form (one SIMD element per - /// `Packing::WIDTH` consecutive hypercube entries). - /// - /// # Algorithm - /// - /// For small `k` (where `2 * k_pack > k`), falls back to a naive - /// per-constraint loop using shifted powers. - /// - /// For larger `k`, uses the split-and-dot approach: - /// - /// 1. Expand each evaluation point into its power-map representation. - /// 2. Transpose into a `k × n` matrix and split at `k / 2`. - /// 3. Build the packed left-half power table and the scalar right-half - /// power table. - /// 4. For each pair of rows (left packed, right scalar), compute the - /// weighted dot product with the challenge powers. - #[instrument(skip_all, fields(num_constraints = self.len(), num_variables = self.num_variables()))] - pub fn combine_packed( - &self, - weights: &mut EvaluationsList, - sum: &mut EF, - challenge: EF, - shift: usize, - ) { - if self.vars.is_empty() { - return; - } - - let n = self.len(); - let k = self.num_variables(); - let k_pack = log2_strict_usize(F::Packing::WIDTH); - assert!(k >= k_pack); - assert_eq!(weights.num_variables() + k_pack, k); - - // Accumulate the scalar target sum first. - self.combine_evals(sum, challenge, shift); - - // Naive fallback: when there aren't enough variables for the - // split approach, compute shifted powers directly per constraint. - if k_pack * 2 > k { - self.vars - .iter() - .zip(challenge.powers().skip(shift)) - .for_each(|(&var, challenge)| { - // gamma^{shift+i} * [1, z, z^2, ..., z^{2^k - 1}] - let pow = EF::from(var).shifted_powers(challenge).collect_n(1 << k); - weights - .as_mut_slice() - .iter_mut() - .zip_eq(pow.chunks(F::Packing::WIDTH)) - .for_each(|(out, chunk)| { - *out += EF::ExtensionPacking::from_ext_slice(chunk); - }); - }); - return; - } - - // Split approach: expand each var into its power-map form, - // transpose, and split into left (packed) and right (scalar) halves. - let points = self - .vars - .iter() - .map(|&var| MultilinearPoint::expand_from_univariate(var, k)) - .collect::>(); - let points = MultilinearPoint::transpose(&points, true); - let (left, right) = points.split_rows(k / 2); - - // Left half → packed power table (operates in SIMD lanes). - let left = packed_batch_pows(left); - // Right half → scalar power table. - let right = batch_pows(right); - - // Broadcast challenge powers into packed form for dot products. - let alphas = challenge - .powers() - .skip(shift) - .take(n) - .map(EF::ExtensionPacking::from) - .collect::>(); - - // For each right-half row, dot all left-half rows against it - // (weighted by the challenge powers) and accumulate into the - // packed weight polynomial. - weights - .as_mut_slice() - .par_chunks_mut(left.height()) - .zip(right.par_row_slices()) - .for_each(|(out, right)| { - out.iter_mut().zip(left.rows()).for_each(|(out, left)| { - *out += left - .zip(right.iter()) - .zip(alphas.iter()) - .map(|((left, &right), &alpha)| alpha * (left * right)) - .sum::(); - }); - }); - } - - /// Batches expected evaluation values into a single target sum using challenge powers. - /// - /// Computes and adds to `claimed_eval`: - /// - /// ```text - /// S = Σ_i γ^{i+shift} · s_i - /// ``` - /// - /// where `s_i` are the expected evaluation values in `self.evaluations`. - /// - /// # Parameters - /// - /// - `claimed_eval`: Accumulator for the target sum. This method **adds** the batched - /// evaluations to the existing value. - /// - /// - `challenge`: Random challenge `γ ∈ EF` used for batching. - /// - /// - `shift`: Power offset. Constraint `i` uses weight `γ^{i+shift}`. - pub fn combine_evals(&self, claimed_eval: &mut EF, challenge: EF, shift: usize) { - // Compute: Σ_i γ^{i+shift} · s_i - // This is equivalent to dot_product(evaluations, [γ^shift, γ^{shift+1}, ...]) - *claimed_eval += dot_product::( - self.evaluations.iter().copied(), - challenge.powers().skip(shift).take(self.len()), - ); - } -} - -#[cfg(test)] -mod tests { - use alloc::vec; - - use p3_baby_bear::BabyBear; - use p3_field::{ - PackedFieldExtension, PrimeCharacteristicRing, extension::BinomialExtensionField, - }; - use proptest::prelude::*; - use rand::{RngExt, SeedableRng, rngs::SmallRng}; - - use super::*; - - type F = BabyBear; - type EF = BinomialExtensionField; - - #[test] - fn test_select_statement_initialize() { - // Test that initialize creates an empty statement with correct num_variables. - let statement = SelectStatement::::initialize(3); - - // The statement should have 3 variables. - assert_eq!(statement.num_variables(), 3); - // The statement should be empty (no constraints). - assert!(statement.is_empty()); - // The length should be 0. - assert_eq!(statement.len(), 0); - } - - #[test] - fn test_select_statement_new() { - // Test that new creates a statement with pre-populated constraints. - let vars = vec![F::from_u64(5), F::from_u64(7)]; - let evaluations = vec![F::from_u64(10), F::from_u64(20)]; - - let statement = SelectStatement::new(2, vars.clone(), evaluations.clone()); - - // The statement should have 2 variables. - assert_eq!(statement.num_variables(), 2); - // The statement should not be empty. - assert!(!statement.is_empty()); - // The statement should have 2 constraints. - assert_eq!(statement.len(), 2); - // The vars and evaluations should match. - assert_eq!(statement.vars, vars); - assert_eq!(statement.evaluations, evaluations); - } - - #[test] - #[should_panic(expected = "assertion")] - fn test_select_statement_new_mismatched_lengths() { - // Test that new panics when vars.len() != evaluations.len(). - let vars = vec![F::from_u64(5)]; - let evaluations = vec![F::from_u64(10), F::from_u64(20)]; - - // This should panic due to length mismatch. - let _ = SelectStatement::new(2, vars, evaluations); - } - - #[test] - fn test_select_statement_add_constraint() { - // Test adding constraints one at a time. - let mut statement = SelectStatement::::initialize(2); - - // Initially empty. - assert!(statement.is_empty()); - assert_eq!(statement.len(), 0); - - // Add first constraint: p(5) = 10. - statement.add_constraint(F::from_u64(5), F::from_u64(10)); - assert!(!statement.is_empty()); - assert_eq!(statement.len(), 1); - - // Add second constraint: p(7) = 20. - statement.add_constraint(F::from_u64(7), F::from_u64(20)); - assert_eq!(statement.len(), 2); - - // Verify the constraints were added correctly. - let constraints: Vec<_> = statement.iter().collect(); - assert_eq!(constraints.len(), 2); - assert_eq!(*constraints[0].0, F::from_u64(5)); - assert_eq!(*constraints[0].1, F::from_u64(10)); - assert_eq!(*constraints[1].0, F::from_u64(7)); - assert_eq!(*constraints[1].1, F::from_u64(20)); - } - - #[test] - fn test_select_statement_verify_basic() { - // Test the verify method with a simple polynomial. - // - // Create a polynomial with evaluations [c0, c1, c2, c3] over {0,1}^2. - let c0 = F::from_u64(1); - let c1 = F::from_u64(2); - let c2 = F::from_u64(3); - let c3 = F::from_u64(4); - let poly = EvaluationsList::new(vec![c0, c1, c2, c3]); - - // Create a statement with k=2 variables. - let k = 2; - let mut statement = SelectStatement::::initialize(k); - - // The polynomial evaluations [c0, c1, c2, c3] can be interpreted as a univariate polynomial: - // p(z) = c0 + c1*z + c2*z^2 + c3*z^3 - // - // Test p(0) = c0 = 1. - let z0 = F::ZERO; - let eval0 = c0; - statement.add_constraint(z0, eval0); - assert!(statement.verify(&poly)); - - // Test p(1) = c0 + c1 + c2 + c3 - let mut statement2 = SelectStatement::::initialize(k); - let z1 = F::ONE; - let eval1 = c0 + c1 + c2 + c3; - statement2.add_constraint(z1, eval1); - assert!(statement2.verify(&poly)); - - // Test p(2) = c0 + c1*2 + c2*4 + c3*8 - let mut statement3 = SelectStatement::::initialize(k); - let z2 = F::from_u64(2); - let eval2 = c0 + c1 * z2 + c2 * z2 * z2 + c3 * z2 * z2 * z2; - statement3.add_constraint(z2, eval2); - assert!(statement3.verify(&poly)); - - // Test a failing verification: p(1) = wrong_eval - let mut statement4 = SelectStatement::::initialize(k); - let wrong_eval = F::from_u64(56765); - statement4.add_constraint(z1, wrong_eval); - assert!(!statement4.verify(&poly)); - } - - #[test] - fn test_select_statement_combine_single_constraint() { - // Test combining a single constraint. - // - // For k=2 variables, we have a 2^2 = 4-point domain. - let k = 2; - - // Create a statement with one constraint: p(z) = s. - let mut statement = SelectStatement::::initialize(k); - let z = F::from_u64(5); - let s = F::from_u64(100); - statement.add_constraint(z, s); - - // The challenge γ is unused for a single constraint (it would multiply by γ^0 = 1). - let gamma = F::from_u64(2); - let shift = 0; - - // Initialize accumulators. - let mut acc_weights = EvaluationsList::zero(k); - let mut acc_sum = F::ZERO; - - // Combine the constraints. - statement.combine(&mut acc_weights, &mut acc_sum, gamma, shift); - - // The target sum should be S = γ^0 · s = 1 · s = s. - let expected_sum = s; - assert_eq!(acc_sum, expected_sum); - - // The weight polynomial should be W(b) = select(pow(z), b) for all b ∈ {0,1}^k. - // - // Verify each entry manually using the property: select(pow(z), b) = z^b. - for (b, acc_weight) in acc_weights.as_slice().iter().enumerate() { - let expected_weight = z.exp_u64(b as u64); - assert_eq!(*acc_weight, expected_weight, "Weight mismatch at index {b}"); - } - } - - #[test] - fn test_select_statement_combine_multiple_constraints() { - // Test combining multiple constraints with batching. - // - // For k=2 variables, we have a 2^2 = 4-point domain. - let k = 2; - - // Create a statement with two constraints: - // - Constraint 0: p(z0) = s0 - // - Constraint 1: p(z1) = s1 - let mut statement = SelectStatement::::initialize(k); - let z0 = F::from_u64(3); - let s0 = F::from_u64(10); - let z1 = F::from_u64(7); - let s1 = F::from_u64(20); - statement.add_constraint(z0, s0); - statement.add_constraint(z1, s1); - - // Use challenge γ for batching. - let gamma = F::from_u64(2); - let shift = 0; - - // Initialize accumulators. - let mut acc_weights = EvaluationsList::zero(k); - let mut acc_sum = F::ZERO; - - // Combine the constraints. - statement.combine(&mut acc_weights, &mut acc_sum, gamma, shift); - - // The target sum should be: - // S = γ^0 · s0 + γ^1 · s1 = 1·s0 + γ·s1 = s0 + gamma*s1. - let expected_sum = s0 + gamma * s1; - assert_eq!(acc_sum, expected_sum); - - // The weight polynomial should be: - // W(b) = γ^0 · select(pow(z0), b) + γ^1 · select(pow(z1), b) - // = select(pow(z0), b) + gamma · select(pow(z1), b) - // Using the property: select(pow(z), b) = z^b. - for (b, acc_weight) in acc_weights.as_slice().iter().enumerate() { - let weight0 = z0.exp_u64(b as u64); - let weight1 = z1.exp_u64(b as u64); - let expected_weight = weight0 + gamma * weight1; - assert_eq!(*acc_weight, expected_weight, "Weight mismatch at index {b}"); - } - } - - #[test] - fn test_select_statement_combine_with_shift() { - // Test combining constraints with a non-zero shift parameter. - // - // The shift parameter allows multiple statement types to use non-overlapping - // challenge powers for batching. - let k = 1; - - // Create a statement with one constraint: p(z) = s. - let mut statement = SelectStatement::::initialize(k); - let z = F::from_u64(5); - let s = F::from_u64(100); - statement.add_constraint(z, s); - - // Use challenge γ with shift. - // This means the constraint will be weighted by γ^{0+shift} = γ^shift. - let gamma = F::from_u64(2); - let shift = 3; - - // Initialize accumulators. - let mut acc_weights = EvaluationsList::zero(k); - let mut acc_sum = F::ZERO; - - // Combine the constraints. - statement.combine(&mut acc_weights, &mut acc_sum, gamma, shift); - - // The target sum should be S = γ^shift · s. - let gamma_to_shift = gamma.exp_u64(shift as u64); - let expected_sum = gamma_to_shift * s; - assert_eq!(acc_sum, expected_sum); - - // The weight polynomial should be W(b) = γ^shift · select(pow(z), b). - // Using the property: select(pow(z), b) = z^b. - for (b, acc_weight) in acc_weights.as_slice().iter().enumerate() { - let select_val = z.exp_u64(b as u64); - let expected_weight = gamma_to_shift * select_val; - assert_eq!(*acc_weight, expected_weight, "Weight mismatch at index {b}"); - } - } - - #[test] - fn test_select_statement_combine_empty() { - // Test that combining an empty statement does nothing. - let k = 2; - let statement = SelectStatement::::initialize(k); - - // Initialize accumulators with non-zero values. - let w0 = F::from_u64(1); - let w1 = F::from_u64(2); - let w2 = F::from_u64(3); - let w3 = F::from_u64(4); - let mut acc_weights = EvaluationsList::new(vec![w0, w1, w2, w3]); - let initial_sum = F::from_u64(99); - let mut acc_sum = initial_sum; - - // Store original values. - let original_weights = acc_weights.clone(); - let original_sum = acc_sum; - - // Combine the empty statement. - let gamma = F::from_u64(2); - let shift = 0; - statement.combine(&mut acc_weights, &mut acc_sum, gamma, shift); - - // The accumulators should remain unchanged. - assert_eq!(acc_weights, original_weights); - assert_eq!(acc_sum, original_sum); - } - - #[test] - fn test_select_statement_combine_accumulation() { - // Test that combine properly accumulates (adds to) existing values. - // - // This is important for batching multiple statements together. - let k = 1; - - // Create first statement with constraint p(z1) = s1. - let mut statement1 = SelectStatement::::initialize(k); - let z1 = F::from_u64(2); - let s1 = F::from_u64(5); - statement1.add_constraint(z1, s1); - - // Create second statement with constraint p(z2) = s2. - let mut statement2 = SelectStatement::::initialize(k); - let z2 = F::from_u64(3); - let s2 = F::from_u64(7); - statement2.add_constraint(z2, s2); - - let gamma = F::from_u64(2); - let shift = 0; - - // Initialize accumulators. - let mut acc_weights = EvaluationsList::zero(k); - let mut acc_sum = F::ZERO; - - // Combine first statement. - statement1.combine(&mut acc_weights, &mut acc_sum, gamma, shift); - - // Store intermediate values. - let intermediate_weights = acc_weights.clone(); - let intermediate_sum = acc_sum; - - // Combine second statement (should add to existing values). - statement2.combine(&mut acc_weights, &mut acc_sum, gamma, shift); - - // The accumulated sum should be intermediate_sum + s2. - let expected_sum = intermediate_sum + s2; - assert_eq!(acc_sum, expected_sum); - - // The accumulated weights should be the sum of both select functions. - // Using the property: select(pow(z), b) = z^b. - let domain_size = 1 << k; - for b in 0..domain_size { - let weight2 = z2.exp_u64(b as u64); - let expected_weight = intermediate_weights.as_slice()[b] + weight2; - assert_eq!( - acc_weights.as_slice()[b], - expected_weight, - "Accumulated weight mismatch at index {b}" - ); - } - } - - #[test] - fn test_select_statement_combine_evals() { - // Test the combine_evals method. - let k = 2; - - // Create a statement with two constraints. - let mut statement = SelectStatement::::initialize(k); - let s0 = F::from_u64(10); - let s1 = F::from_u64(20); - statement.add_constraint(F::from_u64(3), s0); - statement.add_constraint(F::from_u64(7), s1); - - let gamma = F::from_u64(2); - let shift = 1; - - // Test combine_evals. - let mut claimed_eval = F::ZERO; - statement.combine_evals(&mut claimed_eval, gamma, shift); - - // Expected: S = γ^{shift} · s0 + γ^{shift+1} · s1 = γ^1·s0 + γ^2·s1. - let gamma_1 = gamma.exp_u64(shift as u64); - let gamma_2 = gamma.exp_u64((shift + 1) as u64); - let expected = gamma_1 * s0 + gamma_2 * s1; - assert_eq!(claimed_eval, expected); - } - - #[test] - fn test_select_statement_combine_evals_accumulation() { - // Test that combine_evals properly accumulates. - let k = 1; - - let mut statement = SelectStatement::::initialize(k); - let s = F::from_u64(10); - statement.add_constraint(F::from_u64(5), s); - - let gamma = F::from_u64(3); - let shift = 0; - - // Start with a non-zero claimed_eval. - let initial_eval = F::from_u64(42); - let mut claimed_eval = initial_eval; - - // Combine evals should add to the existing value. - statement.combine_evals(&mut claimed_eval, gamma, shift); - - // Expected: initial_eval + γ^0 · s = initial_eval + 1·s = initial_eval + s. - let expected = initial_eval + s; - assert_eq!(claimed_eval, expected); - } - - #[test] - fn test_select_combine_consistency_with_verify() { - // Test that combine and verify are consistent. - // - // If we create a polynomial that satisfies the constraints, then: - // 1. verify() should return true - // 2. The combined weights should correctly compute the polynomial evaluations - let k = 2; - - // Create a simple polynomial: evaluations [c0, c1, c2, c3]. - let c0 = F::from_u64(1); - let c1 = F::from_u64(2); - let c2 = F::from_u64(3); - let c3 = F::from_u64(4); - let poly = EvaluationsList::new(vec![c0, c1, c2, c3]); - - // Create constraints that match the polynomial. - // Using Horner evaluation: p(z) = c0 + c1*z + c2*z^2 + c3*z^3. - let mut statement = SelectStatement::::initialize(k); - - // Evaluate p(z) at z using Horner's method. - let z = F::from_u64(2); - let expected_eval = poly - .iter() - .rfold(F::ZERO, |result, &coeff| result * z + coeff); - statement.add_constraint(z, expected_eval); - - // Verify should pass. - assert!(statement.verify(&poly)); - - // Now combine and check that the weight polynomial correctly represents - // the select function. - let gamma = F::from_u64(3); - let shift = 0; - let mut acc_weights = EvaluationsList::zero(k); - let mut acc_sum = F::ZERO; - statement.combine(&mut acc_weights, &mut acc_sum, gamma, shift); - - // The sum should match the expected evaluation. - assert_eq!(acc_sum, expected_eval); - - // The weight polynomial should satisfy: - // Σ_{b ∈ {0,1}^k} poly(b) · W(b) = expected_eval - let mut computed_sum = F::ZERO; - for (poly_val, acc_weight) in poly.as_slice().iter().zip(acc_weights.as_slice().iter()) { - computed_sum += *poly_val * *acc_weight; - } - assert_eq!(computed_sum, expected_eval); - } - - proptest! { - #[test] - fn prop_select_statement_combine_sum( - // Number of variables (1 to 4 for reasonable test size). - k in 1usize..=4, - // Number of constraints (1 to 5). - num_constraints in 1usize..=5, - // Random evaluation points (avoiding 0 for better coverage). - // Generate exactly num_constraints values. - z_values in prop::collection::vec(1u32..100, 1..=5), - // Random expected evaluations. - s_values in prop::collection::vec(0u32..100, 1..=5), - // Random challenge. - challenge in 1u32..50, - ) { - // Ensure we have enough values for the test. - let actual_num_constraints = num_constraints.min(z_values.len()).min(s_values.len()); - if actual_num_constraints == 0 { - return Ok(()); - } - - let z_values = &z_values[..actual_num_constraints]; - let s_values = &s_values[..actual_num_constraints]; - - // Create statement with random constraints. - let mut statement = SelectStatement::::initialize(k); - for (&z, &s) in z_values.iter().zip(s_values.iter()) { - statement.add_constraint(F::from_u32(z), F::from_u32(s)); - } - - let gamma = F::from_u32(challenge); - - // Combine with shift=0. - let mut acc_weights = EvaluationsList::zero(k); - let mut acc_sum = F::ZERO; - statement.combine(&mut acc_weights, &mut acc_sum, gamma, 0); - - // Compute expected sum manually: S = Σ_i γ^i · s_i. - let mut expected_sum = F::ZERO; - for (i, &s) in s_values.iter().enumerate() { - expected_sum += gamma.exp_u64(i as u64) * F::from_u32(s); - } - - prop_assert_eq!(acc_sum, expected_sum); - } - } - - proptest! { - #[test] - fn prop_select_statement_verify( - // Polynomial evaluations (2^k values for k=3). - poly_evals in prop::collection::vec(0u32..100, 8), - // Evaluation point (avoiding 0 for better coverage). - z in 1u32..50, - ) { - let k = 3; // Fixed k=3 gives 2^3 = 8 evaluations. - let poly = EvaluationsList::new(poly_evals.into_iter().map(F::from_u32).collect()); - - // Compute expected evaluation using Horner's method. - let z_field = F::from_u32(z); - let expected_eval = poly - .iter() - .rfold(F::ZERO, |result, &coeff| result * z_field + coeff); - - // Create statement with correct constraint. - let mut statement = SelectStatement::::initialize(k); - statement.add_constraint(z_field, expected_eval); - - // Verify should pass. - prop_assert!(statement.verify(&poly)); - - // Add a wrong constraint (off by 1, unless it wraps to same value). - let wrong_eval = expected_eval + F::ONE; - if wrong_eval != expected_eval { - statement.add_constraint(z_field, wrong_eval); - // Verify should fail now. - prop_assert!(!statement.verify(&poly)); - } - } - } - - proptest! { - #[test] - fn prop_combine_evals_consistency( - // Number of constraints. - num_constraints in 1usize..=5, - // Random evaluations. - s_values in prop::collection::vec(0u32..100, 1..=5), - // Random challenge. - challenge in 1u32..50, - // Random shift. - shift in 0usize..3, - ) { - let s_values = &s_values[..num_constraints.min(s_values.len())]; - - // Create statement with arbitrary z values (they don't matter for this test). - let mut statement = SelectStatement::::initialize(2); - for &s in s_values { - statement.add_constraint(F::from_u32(1), F::from_u32(s)); - } - - let gamma = F::from_u32(challenge); - - // Method 1: Use combine_evals. - let mut claimed_eval1 = F::ZERO; - statement.combine_evals(&mut claimed_eval1, gamma, shift); - - // Method 2: Compute manually. - let mut claimed_eval2 = F::ZERO; - for (i, &s) in s_values.iter().enumerate() { - claimed_eval2 += gamma.exp_u64((i + shift) as u64) * F::from_u32(s); - } - - prop_assert_eq!(claimed_eval1, claimed_eval2); - } - } - - proptest! { - #[test] - fn prop_packed_combine_roundtrip( - // Number of variables (covers both naive and split paths). - k in 4usize..10, - // Number of select constraints per batch. - n in 1usize..12, - // Challenge power offset. - shift in 0usize..5, - // RNG seed for reproducible randomness. - seed in 0u64..100, - ) { - type PackedExt = >::ExtensionPacking; - - let k_pack = log2_strict_usize(::Packing::WIDTH); - if k < k_pack { - return Ok(()); - } - - let mut rng = SmallRng::seed_from_u64(seed); - let challenge: EF = rng.random(); - - // Generate n random evaluation points and expected values. - let vars = (0..n).map(|_| rng.random()).collect::>(); - let evals = (0..n).map(|_| rng.random()).collect::>(); - - let statement = SelectStatement::::new(k, vars, evals); - - // Scalar path: combine into a 2^k evaluation list. - let mut scalar_weights = EvaluationsList::::zero(k); - let mut scalar_sum = EF::ZERO; - statement.combine(&mut scalar_weights, &mut scalar_sum, challenge, shift); - - // Packed path: combine into a 2^{k - k_pack} packed list. - let mut packed_weights = EvaluationsList::::zero(k - k_pack); - let mut packed_sum = EF::ZERO; - statement.combine_packed(&mut packed_weights, &mut packed_sum, challenge, shift); - - // Unpack the packed result and compare element-by-element. - let unpacked = - >::to_ext_iter( - packed_weights.as_slice().iter().copied(), - ) - .collect::>(); - prop_assert_eq!(scalar_weights.as_slice(), &unpacked[..]); - - // The scalar sums must match exactly. - prop_assert_eq!(scalar_sum, packed_sum); - } - - #[test] - fn prop_packed_combine_accumulation( - k in 4usize..10, - seed in 0u64..50, - ) { - type PackedExt = >::ExtensionPacking; - - let k_pack = log2_strict_usize(::Packing::WIDTH); - if k < k_pack { - return Ok(()); - } - - let mut rng = SmallRng::seed_from_u64(seed); - let challenge: EF = rng.random(); - - let mut s_wt = EvaluationsList::::zero(k); - let mut p_wt = EvaluationsList::::zero(k - k_pack); - let mut s_sum = EF::ZERO; - let mut p_sum = EF::ZERO; - let mut shift = 0; - - // Two batches with different constraint counts. - for n in [3, 7] { - let vars = (0..n).map(|_| rng.random()).collect::>(); - let evals = (0..n).map(|_| rng.random()).collect::>(); - let stmt = SelectStatement::::new(k, vars, evals); - - stmt.combine(&mut s_wt, &mut s_sum, challenge, shift); - stmt.combine_packed(&mut p_wt, &mut p_sum, challenge, shift); - shift += stmt.len(); - } - - // Verify accumulated results match after both batches. - let unpacked = - >::to_ext_iter( - p_wt.as_slice().iter().copied(), - ) - .collect::>(); - prop_assert_eq!(s_wt.as_slice(), &unpacked[..]); - prop_assert_eq!(s_sum, p_sum); - } - } -} diff --git a/src/fiat_shamir/domain_separator.rs b/src/fiat_shamir/domain_separator.rs deleted file mode 100644 index 24df40b8..00000000 --- a/src/fiat_shamir/domain_separator.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! Domain separator construction for the WHIR Fiat-Shamir transcript. - -use alloc::vec::Vec; -use core::marker::PhantomData; - -use p3_challenger::{FieldChallenger, GrindingChallenger}; -use p3_commit::Mmcs; -use p3_field::{ExtensionField, Field, TwoAdicField}; - -use crate::{ - fiat_shamir::pattern::{Hint, Observe, Pattern, Sample}, - parameters::{FoldingFactor, WhirConfig}, -}; - -/// Configuration for a sumcheck phase in the protocol. -#[derive(Debug)] -pub(crate) struct SumcheckParams { - /// Number of sumcheck rounds. - /// - /// Each round corresponds to one prover polynomial and one verifier challenge. - pub rounds: usize, - - /// Proof-of-work difficulty in bits. - /// - /// - Zero disables PoW. - /// - Positive values insert a grinding step after each round. - pub pow_bits: usize, -} - -/// Encodes the structure of an interactive protocol as a sequence of field elements. -/// -/// # Overview -/// -/// Before any protocol execution, both prover and verifier build identical -/// domain separators that describe every transcript operation (observe, sample, -/// hint, PoW) in the exact order they occur. This sequence is absorbed into -/// the challenger at the start, binding the sponge state to the protocol -/// structure and preventing cross-protocol attacks. -/// -/// # Transcript Operation Encoding -/// -/// Each transcript step is encoded as a single field element: -/// -/// ```text -/// element = pattern_tag + sub_label + count -/// ``` -/// -/// where: -/// - `pattern_tag` distinguishes observe / sample / hint. -/// - `sub_label` identifies the semantic role (e.g., Merkle digest, folding randomness). -/// - `count` is the number of field elements involved (omitted for hints). -/// -/// # Protocol Structure -/// -/// The full WHIR proof transcript, as encoded by this separator, follows -/// this order (matching Construction 5.1 of the WHIR paper): -#[derive(Clone, Debug)] -pub struct DomainSeparator { - /// Field-element encoding of the protocol transcript pattern. - pattern: Vec, - - /// Phantom marker for the extension field type. - _extension_field: PhantomData, -} - -impl DomainSeparator -where - EF: ExtensionField, - F: Field, -{ - /// Create a domain separator from an existing pattern vector. - #[must_use] - pub const fn new(pattern: Vec) -> Self { - Self { - pattern, - _extension_field: PhantomData, - } - } - - /// Record that the prover observes `count` field elements into the sponge. - pub fn observe(&mut self, count: usize, pattern: Observe) { - self.pattern.push( - pattern.as_field_element::() - + F::from_usize(count) - + Pattern::Observe.as_field_element::(), - ); - } - - /// Record that the verifier samples `count` field elements from the sponge. - pub fn sample(&mut self, count: usize, pattern: Sample) { - self.pattern.push( - pattern.as_field_element::() - + F::from_usize(count) - + Pattern::Sample.as_field_element::(), - ); - } - - /// Encode a public protocol parameter into the domain separator. - /// - /// Pushes two field elements: - /// 1. A constant marker identifying this entry as a protocol parameter. - /// 2. The raw parameter value. - /// - /// This binds the Fiat-Shamir transcript to the specific protocol - /// configuration, preventing cross-protocol transcript reuse. - fn protocol_param(&mut self, value: usize) { - // Constant marker: observe tag + protocol-param sub-label. - self.pattern.push( - Observe::ProtocolParam.as_field_element::() - + Pattern::Observe.as_field_element::(), - ); - // Raw parameter value. - self.pattern.push(F::from_usize(value)); - } - - /// Record a non-binding hint from the prover. - pub fn hint(&mut self, pattern: Hint) { - self.pattern - .push(pattern.as_field_element::() + Pattern::Hint.as_field_element::()); - } - - /// Absorb the entire domain separator pattern into the challenger. - /// - /// Must be called before any protocol-specific transcript operations - /// so the sponge state is bound to the protocol structure. - pub fn observe_domain_separator(&self, challenger: &mut Challenger) - where - Challenger: FieldChallenger + GrindingChallenger, - { - challenger.observe_slice(&self.pattern); - } - - /// Append an out-of-domain (OOD) sampling step. - /// - /// Encodes sampling `num_samples` OOD evaluation points followed by - /// observing their answers. Skipped when `num_samples` is zero. - pub fn add_ood(&mut self, num_samples: usize) { - if num_samples > 0 { - self.sample(num_samples, Sample::OodQuery); - self.observe(num_samples, Observe::OodAnswers); - } - } - - /// Append the commitment phase of the protocol. - /// - /// # Algorithm - /// - /// 1. Encode public protocol parameters that uniquely identify this - /// protocol instance. This prevents an adversary from replaying a - /// proof generated for one parameter set against a verifier - /// configured with different parameters. - /// 2. Observe the Merkle root of the committed polynomial. - /// 3. Optionally, encode an OOD sampling step. - pub fn commit_statement, Challenger, const DIGEST_ELEMS: usize>( - &mut self, - params: &WhirConfig, - ) where - Challenger: FieldChallenger + GrindingChallenger, - { - // Bind the transcript to the protocol configuration. - self.protocol_param(params.num_variables); - self.protocol_param(params.security_level); - self.protocol_param(params.starting_log_inv_rate); - self.protocol_param(params.max_pow_bits); - - // Encode the soundness assumption as its discriminant. - self.protocol_param(params.soundness_type as usize); - - // Encode the folding strategy: discriminant followed by inner values. - match params.folding_factor { - FoldingFactor::Constant(f) => { - self.protocol_param(0); - self.protocol_param(f); - } - FoldingFactor::ConstantFromSecondRound(first, rest) => { - self.protocol_param(1); - self.protocol_param(first); - self.protocol_param(rest); - } - } - - self.observe(DIGEST_ELEMS, Observe::MerkleDigest); - if params.commitment_ood_samples > 0 { - self.add_ood(params.commitment_ood_samples); - } - } - - /// Append the full WHIR proof transcript to the domain separator. - /// - /// # Algorithm - /// - /// 1. Sample initial combination randomness and run the first sumcheck. - /// 2. For each intermediate round: - /// - Observe the new Merkle commitment and optional OOD answers. - /// - Perform PoW (before queries, per the WHIR security argument). - /// - Draw a transcript checkpoint, then STIR query positions. - /// - Record hints for query data and Merkle proofs. - /// - Sample combination randomness and run the next sumcheck. - /// 3. For the final round: - /// - Observe the final polynomial coefficients. - /// - Perform PoW, then draw final query positions. - /// - Record hints and run the final sumcheck. - /// - Record deferred weight evaluation hints. - pub fn add_whir_proof, Challenger, const DIGEST_ELEMS: usize>( - &mut self, - params: &WhirConfig, - ) where - Challenger: FieldChallenger + GrindingChallenger, - EF: TwoAdicField, - F: TwoAdicField, - { - // Initial combination randomness and first sumcheck phase. - self.sample(1, Sample::InitialCombinationRandomness); - self.add_sumcheck(&SumcheckParams { - rounds: params.folding_factor.at_round(0), - pow_bits: params.starting_folding_pow_bits, - }); - - // Intermediate rounds: commitment → OOD → PoW → checkpoint → queries → sumcheck. - let mut domain_size = params.starting_domain_size(); - for (round, r) in params.round_parameters.iter().enumerate() { - let folded_domain_size = domain_size >> params.folding_factor.at_round(round); - // Byte length needed to encode a position in the folded domain. - let domain_size_bytes = ((folded_domain_size * 2 - 1).ilog2() as usize).div_ceil(8); - - // Observe the new Merkle root and optional OOD evaluations. - self.observe(DIGEST_ELEMS, Observe::MerkleDigest); - self.add_ood(r.ood_samples); - - // PoW must precede query generation to prevent commitment shopping. - self.pow(r.pow_bits); - - // Transcript checkpoint: a dummy sample that synchronizes the - // domain separator with the prover/verifier `challenger.sample()` call - // that occurs between PoW and query generation. - self.sample(1, Sample::TranscriptCheckpoint); - - // Draw STIR query positions and provide opening data. - self.sample(r.num_queries * domain_size_bytes, Sample::StirQueries); - self.hint(Hint::StirQueries); - self.hint(Hint::MerkleProof); - - // Combination randomness for the next polynomial, then sumcheck. - self.sample(1, Sample::CombinationRandomness); - - self.add_sumcheck(&SumcheckParams { - rounds: params.folding_factor.at_round(round + 1), - pow_bits: r.folding_pow_bits, - }); - domain_size >>= params.rs_reduction_factor(round); - } - - // Final round: coefficients → PoW → queries → sumcheck → deferred hints. - let folded_domain_size = domain_size - >> params - .folding_factor - .at_round(params.round_parameters.len()); - let domain_size_bytes = ((folded_domain_size * 2 - 1).ilog2() as usize).div_ceil(8); - - // Observe all coefficients of the final folded polynomial. - self.observe(1 << params.final_sumcheck_rounds, Observe::FinalCoeffs); - - // PoW before final query generation (no transcript checkpoint in final round). - self.pow(params.final_pow_bits); - self.sample( - domain_size_bytes * params.final_queries, - Sample::FinalQueries, - ); - self.hint(Hint::StirAnswers); - self.hint(Hint::MerkleProof); - - // Final sumcheck and deferred weight evaluations. - self.add_sumcheck(&SumcheckParams { - rounds: params.final_sumcheck_rounds, - pow_bits: params.final_folding_pow_bits, - }); - self.hint(Hint::DeferredWeightEvaluations); - } - - /// Append a sumcheck sub-protocol to the domain separator. - /// - /// # Algorithm - /// - /// For each round: - /// 1. Observe 2 coefficients of the degree-2 round polynomial (c_0 and c_2). - /// The third coefficient c_1 = claimed_sum - c_0 is derived by the verifier. - /// 2. Sample one folding randomness challenge. - /// 3. Optionally perform a PoW step. - pub(crate) fn add_sumcheck(&mut self, params: &SumcheckParams) { - let SumcheckParams { rounds, pow_bits } = *params; - - for _ in 0..rounds { - // Absorb c_0 and c_2; the verifier reconstructs c_1. - self.observe(2, Observe::SumcheckPoly); - // Verifier draws the folding challenge for this variable. - self.sample(1, Sample::FoldingRandomness); - // Optional grinding step after each sumcheck round. - self.pow(pow_bits); - } - } - - /// Optionally append a proof-of-work challenge. - /// - /// When `bits` is positive, encodes: - /// 1. Sampling a 32-byte challenge from the sponge. - /// 2. Observing an 8-byte nonce that satisfies the grinding condition. - /// - /// When `bits` is zero, nothing is appended. - pub fn pow(&mut self, bits: usize) { - if bits > 0 { - // Sample a 32-byte PoW challenge preimage. - self.sample(32, Sample::PowQueries); - // Observe the nonce that solves the challenge. - self.observe(8, Observe::PowNonce); - } - } -} diff --git a/src/fiat_shamir/errors.rs b/src/fiat_shamir/errors.rs deleted file mode 100644 index 6c286f02..00000000 --- a/src/fiat_shamir/errors.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Fiat-Shamir transcript errors for WHIR protocol challenges. - -use thiserror::Error; - -/// Granular error types for Fiat-Shamir operations. -/// -/// Each variant represents a specific failure mode in the Fiat-Shamir transform. -#[derive(Error, Debug, Clone)] -pub enum FiatShamirError { - /// Proof-of-work witness fails difficulty requirement. - #[error("Invalid grinding witness: proof-of-work verification failed")] - InvalidGrindingWitness, -} diff --git a/src/fiat_shamir/mod.rs b/src/fiat_shamir/mod.rs deleted file mode 100644 index 18d0f2d0..00000000 --- a/src/fiat_shamir/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod domain_separator; -pub mod errors; -pub mod pattern; -#[cfg(test)] -mod tests; diff --git a/src/fiat_shamir/pattern.rs b/src/fiat_shamir/pattern.rs deleted file mode 100644 index 3fbc166b..00000000 --- a/src/fiat_shamir/pattern.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Transcript pattern labels for the Fiat-Shamir domain separator. - -use p3_field::Field; - -/// Top-level classification of a transcript operation. -/// -/// Each step in the protocol transcript is one of three kinds: -/// - Prover sends data (observed into the sponge). -/// - Verifier draws randomness (sampled from the sponge). -/// - Non-binding auxiliary data (hints, not absorbed). -/// -/// The discriminant is encoded as a field element and combined with a -/// sub-label and count to form a single domain separator entry. -#[derive(Debug, Clone, Copy)] -pub enum Pattern { - /// Verifier samples randomness from the transcript. - Sample, - /// Prover observes (absorbs) data into the transcript. - Observe, - /// Prover supplies non-binding auxiliary data. - Hint, -} - -impl Pattern { - /// Convert to a field element using the enum discriminant. - #[must_use] - pub fn as_field_element(self) -> F { - F::from_u8(self as u8) - } -} - -/// Sub-labels for sampled (verifier-drawn) transcript items. -/// -/// Each variant identifies the semantic role of a challenge or -/// randomness drawn from the sponge during the protocol. -#[derive(Debug, Clone, Copy)] -pub enum Sample { - /// Randomness for the initial linear combination of constraints. - InitialCombinationRandomness, - /// Per-round folding challenge in the sumcheck protocol. - FoldingRandomness, - /// Randomness for combining quotient polynomials after each round. - CombinationRandomness, - /// Byte-encoded query positions for STIR proximity tests. - StirQueries, - /// Byte-encoded query positions for the final proximity test. - FinalQueries, - /// Challenge bytes for proof-of-work grinding. - PowQueries, - /// Out-of-domain evaluation point. - OodQuery, - /// Dummy single-element sample used as a transcript checkpoint. - /// - /// Drawn between proof-of-work and query generation in non-final rounds - /// to keep the domain separator synchronized with the actual prover/verifier - /// transcript. - TranscriptCheckpoint, -} - -impl Sample { - /// Convert to a field element using the enum discriminant. - #[must_use] - pub fn as_field_element(self) -> F { - F::from_u8(self as u8) - } -} - -/// Sub-labels for observed (prover-absorbed) transcript items. -/// -/// Each variant identifies the semantic role of data the prover -/// commits into the sponge. -#[derive(Debug, Clone, Copy)] -pub enum Observe { - /// Merkle tree root digest for a committed polynomial. - MerkleDigest, - /// Evaluations at out-of-domain points. - OodAnswers, - /// Coefficients of the degree-2 sumcheck round polynomial. - /// - /// Only c_0 and c_2 are sent; c_1 is derived by the verifier - /// as `claimed_sum - c_0`. - SumcheckPoly, - /// Evaluation answers at STIR query positions. - StirAnswers, - /// Coefficients of the final folded polynomial. - FinalCoeffs, - /// Proof-of-work nonce solving the grinding challenge. - PowNonce, - /// A public protocol parameter value. - /// - /// Used in the domain separator header to bind the transcript - /// to the specific protocol configuration. Each parameter is - /// encoded as a (marker, value) pair. - ProtocolParam, -} - -impl Observe { - /// Convert to a field element using the enum discriminant. - #[must_use] - pub fn as_field_element(self) -> F { - F::from_u8(self as u8) - } -} - -/// Sub-labels for hint (non-binding auxiliary) transcript items. -/// -/// Hints are data the prover sends to help the verifier reconstruct -/// information but are not absorbed into the sponge. They do not -/// affect soundness. -#[derive(Debug, Clone, Copy)] -pub enum Hint { - /// Indices and positions of STIR query points. - StirQueries, - /// Evaluation values at STIR query positions. - StirAnswers, - /// Authentication paths for Merkle tree opening proofs. - MerkleProof, - /// Precomputed weight evaluations deferred from earlier rounds. - DeferredWeightEvaluations, -} - -impl Hint { - /// Convert to a field element using the enum discriminant. - #[must_use] - pub fn as_field_element(self) -> F { - F::from_u8(self as u8) - } -} diff --git a/src/fiat_shamir/tests.rs b/src/fiat_shamir/tests.rs deleted file mode 100644 index 03c3022a..00000000 --- a/src/fiat_shamir/tests.rs +++ /dev/null @@ -1,54 +0,0 @@ -use alloc::vec::Vec; - -use p3_baby_bear::{BabyBear, Poseidon2BabyBear}; -use p3_challenger::{CanObserve, CanSample, DuplexChallenger}; -use p3_field::extension::BinomialExtensionField; -use proptest::prelude::*; -use rand::{RngExt, SeedableRng, rngs::SmallRng}; - -use crate::fiat_shamir::domain_separator::DomainSeparator; - -type F = BabyBear; -type EF = BinomialExtensionField; -type Perm = Poseidon2BabyBear<16>; -type MyChallenger = DuplexChallenger; - -fn make_challenger() -> MyChallenger { - let perm = Perm::new_from_rng_128(&mut rand::rngs::SmallRng::seed_from_u64(42)); - DuplexChallenger::new(perm) -} - -proptest! { - /// Tests that `observe_domain_separator` produces the same challenger state - /// as manually observing the same field elements. - #[test] - fn test_observe_domain_separator(seed in any::(), pattern_len in 1usize..16) { - let mut rng = SmallRng::seed_from_u64(seed); - - // Create a domain separator with random pattern elements - let pattern: Vec = (0..pattern_len).map(|_| rng.random()).collect(); - let domsep = DomainSeparator::::new(pattern.clone()); - - // Create two identical challengers - let mut challenger1 = make_challenger(); - let mut challenger2 = make_challenger(); - - // Use observe_domain_separator on the first challenger - domsep.observe_domain_separator(&mut challenger1); - - // Manually observe the same field elements on the second challenger - challenger2.observe_slice(&pattern); - - // Verify that both challengers produce the same challenge values - let sample1: F = challenger1.sample(); - let sample2: F = challenger2.sample(); - prop_assert_eq!(sample1, sample2, "Challengers should produce identical challenges after observing the same domain separator"); - - // Sample a few more values to ensure consistency - for _ in 0..3 { - let s1: F = challenger1.sample(); - let s2: F = challenger2.sample(); - prop_assert_eq!(s1, s2, "All subsequent samples should match"); - } - } -} diff --git a/src/lib.rs b/src/lib.rs index b3f7e569..bcc2a2da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,5 @@ #![no_std] extern crate alloc; -pub mod constraints; -pub mod fiat_shamir; -pub mod parameters; -pub mod sumcheck; +pub use p3_whir::{constraints, fiat_shamir, parameters, sumcheck}; pub mod whir; diff --git a/src/parameters/folding.rs b/src/parameters/folding.rs deleted file mode 100644 index a932e65f..00000000 --- a/src/parameters/folding.rs +++ /dev/null @@ -1,266 +0,0 @@ -//! Folding factor strategies for variable reduction across WHIR rounds. - -use thiserror::Error; - -/// Each WHIR step folds the polynomial, reducing the number of variables. -/// Once the number of variables drops to this threshold or below, -/// the prover sends the polynomial coefficients directly. -const MAX_NUM_VARIABLES_TO_SEND_COEFFS: usize = 6; - -/// Errors that can occur when validating a folding factor. -#[derive(Debug, Error, PartialEq, Eq)] -pub enum FoldingFactorError { - /// The folding factor is larger than the number of variables. - #[error( - "Folding factor {0} is greater than the number of variables {1}. Polynomial too small, just send it directly." - )] - TooLarge(usize, usize), - - /// The folding factor cannot be zero. - #[error("Folding factor shouldn't be zero.")] - ZeroFactor, -} - -/// Defines the folding factor for polynomial commitments. -#[derive(Debug, Clone, Copy)] -pub enum FoldingFactor { - /// A fixed folding factor used in all rounds. - Constant(usize), - /// Uses a different folding factor for the first round and a fixed one for the rest. - ConstantFromSecondRound(usize, usize), -} - -impl FoldingFactor { - /// Retrieves the folding factor for a given round. - #[must_use] - pub const fn at_round(&self, round: usize) -> usize { - match self { - Self::Constant(factor) => *factor, - Self::ConstantFromSecondRound(first_round_factor, factor) => { - if round == 0 { - *first_round_factor - } else { - *factor - } - } - } - } - - /// Checks the validity of the folding factor against the number of variables. - pub const fn check_validity(&self, num_variables: usize) -> Result<(), FoldingFactorError> { - match self { - Self::Constant(factor) => { - if *factor > num_variables { - // A folding factor cannot be greater than the number of available variables. - Err(FoldingFactorError::TooLarge(*factor, num_variables)) - } else if *factor == 0 { - // A folding factor of zero is invalid since folding must reduce variables. - Err(FoldingFactorError::ZeroFactor) - } else { - Ok(()) - } - } - Self::ConstantFromSecondRound(first_round_factor, factor) => { - if *first_round_factor > num_variables { - // The first round folding factor must not exceed the available variables. - Err(FoldingFactorError::TooLarge( - *first_round_factor, - num_variables, - )) - } else if *factor > num_variables { - // Subsequent round folding factors must also not exceed the available - // variables. - Err(FoldingFactorError::TooLarge(*factor, num_variables)) - } else if *factor == 0 || *first_round_factor == 0 { - // Folding should occur at least once; zero is not valid. - Err(FoldingFactorError::ZeroFactor) - } else { - Ok(()) - } - } - } - } - - /// Computes the number of WHIR rounds and the number of rounds in the final sumcheck. - #[must_use] - pub fn compute_number_of_rounds(&self, num_variables: usize) -> (usize, usize) { - match self { - Self::Constant(factor) => { - if num_variables <= MAX_NUM_VARIABLES_TO_SEND_COEFFS { - // the first folding is mandatory in the current implem (TODO don't fold, send directly the polynomial) - return (0, num_variables - factor); - } - // Starting from `num_variables`, each round reduces the number of variables by `factor`. As soon as the - // number of variables is less of equal than `MAX_NUM_VARIABLES_TO_SEND_COEFFS`, we stop folding and the - // prover sends directly the coefficients of the polynomial. - let num_rounds = - (num_variables - MAX_NUM_VARIABLES_TO_SEND_COEFFS).div_ceil(*factor); - let final_sumcheck_rounds = num_variables - num_rounds * factor; - // The -1 accounts for the fact that the last round does not require another folding. - (num_rounds - 1, final_sumcheck_rounds) - } - Self::ConstantFromSecondRound(first_round_factor, factor) => { - // Compute the number of variables remaining after the first round. - let nv_except_first_round = num_variables - *first_round_factor; - if nv_except_first_round < MAX_NUM_VARIABLES_TO_SEND_COEFFS { - // This case is equivalent to Constant(first_round_factor) - // the first folding is mandatory in the current implem (TODO don't fold, send directly the polynomial) - return (0, nv_except_first_round); - } - // Starting from `num_variables`, the first round reduces the number of variables by `first_round_factor`, - // and the next ones by `factor`. As soon as the number of variables is less of equal than - // `MAX_NUM_VARIABLES_TO_SEND_COEFFS`, we stop folding and the prover sends directly the coefficients of the polynomial. - let num_rounds = - (nv_except_first_round - MAX_NUM_VARIABLES_TO_SEND_COEFFS).div_ceil(*factor); - let final_sumcheck_rounds = nv_except_first_round - num_rounds * factor; - // No need to minus 1 because the initial round is already excepted out - (num_rounds, final_sumcheck_rounds) - } - } - } - - /// Computes the total number of folding rounds over `n_rounds` iterations. - #[must_use] - pub const fn total_number(&self, n_rounds: usize) -> usize { - match self { - Self::Constant(factor) => { - // - Each round folds `factor` variables, - // - There are `n_rounds + 1` iterations (including the original input size). - *factor * (n_rounds + 1) - } - Self::ConstantFromSecondRound(first_round_factor, factor) => { - // - The first round folds `first_round_factor` variables, - // - Subsequent rounds fold `factor` variables each. - *first_round_factor + *factor * n_rounds - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_folding_factor_at_round() { - let factor = FoldingFactor::Constant(4); - assert_eq!(factor.at_round(0), 4); - assert_eq!(factor.at_round(5), 4); - - let variable_factor = FoldingFactor::ConstantFromSecondRound(3, 5); - assert_eq!(variable_factor.at_round(0), 3); // First round uses 3 - assert_eq!(variable_factor.at_round(1), 5); // Subsequent rounds use 5 - assert_eq!(variable_factor.at_round(10), 5); - } - - #[test] - fn test_folding_factor_check_validity() { - // Valid cases - assert!(FoldingFactor::Constant(2).check_validity(4).is_ok()); - assert!( - FoldingFactor::ConstantFromSecondRound(2, 3) - .check_validity(5) - .is_ok() - ); - - // Invalid cases - // Factor too large - assert_eq!( - FoldingFactor::Constant(5).check_validity(3), - Err(FoldingFactorError::TooLarge(5, 3)) - ); - // Zero factor - assert_eq!( - FoldingFactor::Constant(0).check_validity(3), - Err(FoldingFactorError::ZeroFactor) - ); - // First round factor too large - assert_eq!( - FoldingFactor::ConstantFromSecondRound(4, 2).check_validity(3), - Err(FoldingFactorError::TooLarge(4, 3)) - ); - // Second round factor too large - assert_eq!( - FoldingFactor::ConstantFromSecondRound(2, 5).check_validity(4), - Err(FoldingFactorError::TooLarge(5, 4)) - ); - // First round zero - assert_eq!( - FoldingFactor::ConstantFromSecondRound(0, 3).check_validity(4), - Err(FoldingFactorError::ZeroFactor) - ); - } - - #[test] - fn test_compute_number_of_rounds() { - let constant_factor = 3; - let factor = FoldingFactor::Constant(constant_factor); - assert_eq!( - factor.compute_number_of_rounds(MAX_NUM_VARIABLES_TO_SEND_COEFFS - 1), - (0, MAX_NUM_VARIABLES_TO_SEND_COEFFS - constant_factor - 1) - ); - assert_eq!( - factor.compute_number_of_rounds(MAX_NUM_VARIABLES_TO_SEND_COEFFS), - (0, MAX_NUM_VARIABLES_TO_SEND_COEFFS - constant_factor) - ); - assert_eq!( - factor.compute_number_of_rounds(MAX_NUM_VARIABLES_TO_SEND_COEFFS + 1), - (0, MAX_NUM_VARIABLES_TO_SEND_COEFFS - constant_factor + 1) - ); - assert_eq!( - factor.compute_number_of_rounds(MAX_NUM_VARIABLES_TO_SEND_COEFFS + constant_factor), - (0, MAX_NUM_VARIABLES_TO_SEND_COEFFS) - ); - assert_eq!( - factor.compute_number_of_rounds(MAX_NUM_VARIABLES_TO_SEND_COEFFS + constant_factor + 1), - (1, MAX_NUM_VARIABLES_TO_SEND_COEFFS - constant_factor + 1) - ); - assert_eq!( - factor.compute_number_of_rounds( - MAX_NUM_VARIABLES_TO_SEND_COEFFS + constant_factor * 2 + 1 - ), - (2, MAX_NUM_VARIABLES_TO_SEND_COEFFS - constant_factor + 1) - ); - - let initial_factor = 4; - let next_factor = 3; - let variable_factor = FoldingFactor::ConstantFromSecondRound(initial_factor, next_factor); - assert_eq!( - variable_factor.compute_number_of_rounds(MAX_NUM_VARIABLES_TO_SEND_COEFFS - 1), - (0, MAX_NUM_VARIABLES_TO_SEND_COEFFS - initial_factor - 1) - ); - assert_eq!( - variable_factor.compute_number_of_rounds(MAX_NUM_VARIABLES_TO_SEND_COEFFS), - (0, MAX_NUM_VARIABLES_TO_SEND_COEFFS - initial_factor) - ); - assert_eq!( - variable_factor.compute_number_of_rounds(MAX_NUM_VARIABLES_TO_SEND_COEFFS + 1), - (0, MAX_NUM_VARIABLES_TO_SEND_COEFFS - initial_factor + 1) - ); - assert_eq!( - variable_factor - .compute_number_of_rounds(MAX_NUM_VARIABLES_TO_SEND_COEFFS + initial_factor), - (0, MAX_NUM_VARIABLES_TO_SEND_COEFFS) - ); - assert_eq!( - variable_factor - .compute_number_of_rounds(MAX_NUM_VARIABLES_TO_SEND_COEFFS + initial_factor + 1), - (1, MAX_NUM_VARIABLES_TO_SEND_COEFFS - next_factor + 1) - ); - assert_eq!( - variable_factor.compute_number_of_rounds( - MAX_NUM_VARIABLES_TO_SEND_COEFFS + initial_factor + next_factor + 1 - ), - (2, MAX_NUM_VARIABLES_TO_SEND_COEFFS - next_factor + 1) - ); - } - - #[test] - fn test_total_number() { - let factor = FoldingFactor::Constant(2); - assert_eq!(factor.total_number(3), 8); // 2 * (3 + 1) - - let variable_factor = FoldingFactor::ConstantFromSecondRound(3, 2); - assert_eq!(variable_factor.total_number(3), 9); // 3 + 2 * 3 - } -} diff --git a/src/parameters/mod.rs b/src/parameters/mod.rs deleted file mode 100644 index 58ba46ee..00000000 --- a/src/parameters/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Protocol configuration for WHIR polynomial commitments. - -use core::fmt::Display; - -mod folding; -pub mod soundness; -pub mod whir; - -pub use folding::{FoldingFactor, FoldingFactorError}; -pub use soundness::SecurityAssumption; -pub use whir::{RoundConfig, SumcheckStrategy, WhirConfig}; - -/// Fallback proof-of-work difficulty when the user does not specify one. -/// -/// 16 bits strikes a balance between prover cost and verifier DoS resistance. -/// - Higher values slow down the prover; -/// - Lower values weaken the PoW contribution to soundness. -pub const DEFAULT_MAX_POW: usize = 16; - -/// Configuration parameters for WHIR proofs. -#[derive(Clone, Debug)] -pub struct ProtocolParameters { - /// The logarithmic inverse rate for sampling. - pub starting_log_inv_rate: usize, - /// The value v such that that the size of the Reed Solomon domain on which - /// our polynomial is evaluated gets divided by `2^v` at the first round. - /// RS domain size at commitment = 2^(num_variables + starting_log_inv_rate) - /// RS domain size after the first round = 2^(num_variables + starting_log_inv_rate - v) - /// The default value is 1 (halving the domain size, which is the behavior of the consecutive rounds). - pub rs_domain_initial_reduction_factor: usize, - /// The folding factor strategy. - pub folding_factor: FoldingFactor, - /// The type of soundness guarantee. - pub soundness_type: SecurityAssumption, - /// The security level in bits. - pub security_level: usize, - /// The number of bits required for proof-of-work (PoW). - pub pow_bits: usize, - /// Merkle tree configuration. - pub mmcs: MT, -} - -impl Display for ProtocolParameters { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - writeln!( - f, - "Targeting {}-bits of security with {}-bits of PoW - soundness: {:?}", - self.security_level, self.pow_bits, self.soundness_type - )?; - writeln!( - f, - "Starting rate: 2^-{}, folding_factor: {:?}", - self.starting_log_inv_rate, self.folding_factor, - ) - } -} diff --git a/src/parameters/soundness.rs b/src/parameters/soundness.rs deleted file mode 100644 index d52a52bb..00000000 --- a/src/parameters/soundness.rs +++ /dev/null @@ -1,551 +0,0 @@ -//! Soundness analysis and security assumption models for Reed-Solomon proximity testing. - -use alloc::{format, string::String}; -use core::{f64::consts::LOG2_10, fmt::Display, str::FromStr}; - -use serde::Serialize; - -/// Security assumptions determines which proximity parameters and conjectures are assumed by the error computation. -/// -/// # References -/// -/// The proximity gaps analysis is based on: -/// - **[BCI+20]**: Ben-Sasson, Carmon, Ishai, Kopparty, Saraf. "Proximity Gaps for Reed–Solomon Codes". -/// FOCS 2020. -/// - **[BCSS25]**: Ben-Sasson, Carmon, Haböck, Kopparty, Saraf. "On Proximity Gaps for Reed–Solomon Codes". -/// -/// -/// The [BCSS25] paper significantly improves the Johnson bound proximity gaps from `O(n^2/η^7)` to `O(n/η^5)`, -/// enabling provable 128-bit security with smaller extension fields (e.g., degree-5 extension of KoalaBear). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -pub enum SecurityAssumption { - /// Unique decoding assumes that the distance of each oracle is within the UDR of the code. - /// We refer to this configuration as UD for short. - /// This requires no conjectures. - UniqueDecoding, - - /// Johnson bound assumes that the distance of each oracle is within the Johnson bound (1 - √ρ). - /// We refer to this configuration as JB for short. - /// This assumes that RS have mutual correlated agreement for proximity parameter up to (1 - √ρ). - /// - /// # Proximity Gaps Improvement - /// - /// The error bound uses Theorem 1.5 from [BCSS25]: - /// - **Old [BCI+20]**: `a = O(n^2/η^7)` exceptional z's - /// - **New [BCSS25]**: `a = O(n/η^5)` exceptional z's - /// - /// This improvement of factor `n·η^2` translates to approximately `log_2(n)` additional bits of security, - /// making provable 128-bit security achievable with degree-5 extensions of small prime fields. - JohnsonBound, - - /// Capacity bound assumes that the distance of each oracle is within the capacity bound 1 - ρ. - /// We refer to this configuration as CB for short. - /// This requires conjecturing that RS codes are decodable up to capacity and have correlated agreement (mutual in WHIR) up to capacity. - CapacityBound, -} - -impl SecurityAssumption { - /// In both JB and CB theorems such as list-size only hold for proximity parameters slighly below the bound. - /// E.g. in JB proximity gaps holds for every δ ∈ (0, 1 - √ρ). - /// η is the distance between the chosen proximity parameter and the bound. - /// I.e. in JB δ = 1 - √ρ - η and in CB δ = 1 - ρ - η. - // TODO: Maybe it makes more sense to be multiplicative. I think this can be set in a better way. - #[must_use] - pub const fn log_eta(&self, log_inv_rate: usize) -> f64 { - match self { - // We don't use η in UD - Self::UniqueDecoding => 0., // TODO: Maybe just panic and avoid calling it in UD? - // Set as √ρ/20 - Self::JohnsonBound => -(0.5 * log_inv_rate as f64 + LOG2_10 + 1.), - // Set as ρ/20 - Self::CapacityBound => -(log_inv_rate as f64 + LOG2_10 + 1.), - } - } - - /// Given a RS code (specified by the log of the degree and log inv of the rate), compute the list size at the specified distance δ. - #[must_use] - pub const fn list_size_bits(&self, log_degree: usize, log_inv_rate: usize) -> f64 { - let log_eta = self.log_eta(log_inv_rate); - match self { - // In UD the list size is 1 - Self::UniqueDecoding => 0., - - // By the JB, RS codes are (1 - √ρ - η, (2*η*√ρ)^-1)-list decodable. - Self::JohnsonBound => { - let log_inv_sqrt_rate: f64 = log_inv_rate as f64 / 2.; - log_inv_sqrt_rate - (1. + log_eta) - } - - // In CB we assume that RS codes are (1 - ρ - η, d/ρ*η)-list decodable (see Conjecture 5.6 in STIR). - Self::CapacityBound => (log_degree + log_inv_rate) as f64 - log_eta, - } - } - - /// Given a RS code (specified by the log of the degree and log inv of the rate) a field_size - /// and an arity, compute the proximity gaps error (in bits) at the specified distance. - /// - /// # Johnson Bound Improvement - /// - /// For the Johnson bound case, this uses the improved Theorem 1.5 from [BCSS25]: - /// - /// > "On Proximity Gaps for Reed–Solomon Codes" (eprint 2025/2055) - /// > Ben-Sasson, Carmon, Haböck, Kopparty, Saraf - /// - /// The theorem states that for proximity parameter `γ < J(δ) - η` (below Johnson bound by η), - /// the number of exceptional z's satisfies: - /// - /// ```text - /// a > (2(m + 1/2)^5 + 3(m + 1/2)·γ·ρ) / (3·ρ^(3/2)) · n + (m + 1/2) / √ρ - /// ``` - /// - /// where `m = max(⌈√ρ / (2η)⌉, 3)` and the asymptotic behavior is `O(n/η^5)`. - /// - /// ## Comparison with prior work - /// - /// | Reference | Bound on exceptional z's | Proximity loss | - /// |-----------|--------------------------|----------------| - /// | [BCI+20] | `O(n^2/η^7)` | 0 | - /// | [BCSS25] | `O(n/η^5)` | 0 | - /// - /// The improvement factor of `n·η^2` translates to approximately `log_2(n)` additional bits - /// of provable security, enabling 128-bit security with degree-5 extensions of KoalaBear. - #[must_use] - pub fn prox_gaps_error( - &self, - log_degree: usize, - log_inv_rate: usize, - field_size_bits: usize, - num_functions: usize, - ) -> f64 { - assert!( - num_functions >= 2, - "num_functions must be >= 2 to compute proximity gaps error", - ); - - let log_eta = self.log_eta(log_inv_rate); - - // Note that this does not include the field_size - let error = match self { - // In UD the error is |L|/|F| = d/(ρ·|F|) - Self::UniqueDecoding => (log_degree + log_inv_rate) as f64, - - // From Theorem 1.5 in [BCSS25] "On Proximity Gaps for Reed–Solomon Codes": - // - // For γ < J(δ) - η, the number of exceptional z's is bounded by: - // a > (2(m + 1/2)^5 + 3(m + 1/2)·γ·ρ) / (3·ρ^(3/2)) · n + (m + 1/2) / √ρ - // - // With η = √ρ/20 (safe gap), m = max(⌈√ρ/(2η)⌉, 3) = max(10, 3) = 10. - // - // The dominant term for large n: - // a ≈ (2 · 10.5^5) / (3 · ρ^(3/2)) · n - // - // In log form: - // log_2(a) = log_2(n) + log_2(2 · 10.5^5 / 3) + 1.5 · log_2(1/ρ) - // = (log_degree + log_inv_rate) + 16.38 + 1.5 · log_inv_rate - // = log_degree + 2.5 · log_inv_rate + 16.38 - // - // This improves over [BCI+20] which had: - // log_2(a) = 2·log_degree + 3.5·log_inv_rate + 23.24 - Self::JohnsonBound => { - // n = 2^(log_degree + log_inv_rate) - let log_n = (log_degree + log_inv_rate) as f64; - - // Constant from (2 · 10.5^5 / 3) - let constant = libm::log2(2. * libm::pow(10.5, 5.) / 3.); - - // ρ^(-3/2) contributes 1.5 · log_inv_rate - let log_rho_neg_3_2 = 1.5 * log_inv_rate as f64; - - log_n + constant + log_rho_neg_3_2 - } - - // In CB we assume the error is degree/(η·ρ^2) - Self::CapacityBound => (log_degree + 2 * log_inv_rate) as f64 - log_eta, - }; - - // Error is (num_functions - 1) * error/|F|; - let num_functions_1_log = libm::log2(num_functions as f64 - 1.); - field_size_bits as f64 - (error + num_functions_1_log) - } - - /// The query error is (1 - δ)^t where t is the number of queries. - /// This computes log(1 - δ). - /// - In UD, δ is (1 - ρ)/2 - /// - In JB, δ is (1 - √ρ - η) - /// - In CB, δ is (1 - ρ - η) - #[must_use] - pub fn log_1_delta(&self, log_inv_rate: usize) -> f64 { - let log_eta = self.log_eta(log_inv_rate); - let eta = libm::pow(2., log_eta); - let rate = 1. / f64::from(1 << log_inv_rate); - - let delta = match self { - Self::UniqueDecoding => 0.5 * (1. - rate), - Self::JohnsonBound => 1. - libm::sqrt(rate) - eta, - Self::CapacityBound => 1. - rate - eta, - }; - - libm::log2(1. - delta) - } - - /// Compute the number of queries to match the security level - /// The error to drive down is (1-δ)^t < 2^-λ. - /// Where δ is set as in the `log_1_delta` function. - #[must_use] - pub fn queries(&self, protocol_security_level: usize, log_inv_rate: usize) -> usize { - let num_queries_f = -(protocol_security_level as f64) / self.log_1_delta(log_inv_rate); - - libm::ceil(num_queries_f) as usize - } - - /// Compute the error for the given number of queries - /// The error to drive down is (1-δ)^t < 2^-λ. - /// Where δ is set as in the `log_1_delta` function. - #[must_use] - pub fn queries_error(&self, log_inv_rate: usize, num_queries: usize) -> f64 { - let num_queries = num_queries as f64; - - -num_queries * self.log_1_delta(log_inv_rate) - } - - /// Compute the error for the OOD samples of the protocol - /// See Lemma 4.5 in STIR. - /// The error is list_size^2 * (degree/field_size_bits)^reps - /// NOTE: Here we are discounting the domain size as we assume it is negligible compared to the size of the field. - #[must_use] - pub const fn ood_error( - &self, - log_degree: usize, - log_inv_rate: usize, - field_size_bits: usize, - ood_samples: usize, - ) -> f64 { - if matches!(self, Self::UniqueDecoding) { - return 0.; - } - - let list_size_bits = self.list_size_bits(log_degree, log_inv_rate); - - let error = 2. * list_size_bits + (log_degree * ood_samples) as f64; - (ood_samples * field_size_bits) as f64 + 1. - error - } - - /// Computes the number of OOD samples required to achieve security_level bits of security - /// We note that in both STIR and WHIR there are various strategies to set OOD samples. - /// In this case, we are just sampling one element from the extension field - #[must_use] - pub fn determine_ood_samples( - &self, - security_level: usize, - log_degree: usize, - log_inv_rate: usize, - field_size_bits: usize, - ) -> usize { - if matches!(self, Self::UniqueDecoding) { - return 0; - } - - for ood_samples in 1..64 { - if self.ood_error(log_degree, log_inv_rate, field_size_bits, ood_samples) - >= security_level as f64 - { - return ood_samples; - } - } - - panic!("Could not find an appropriate number of OOD samples"); - } - - /// Compute the sumcheck soundness term of the folding step (in bits). - /// - /// During folding, the verifier samples a random challenge and checks - /// a degree-2 sumcheck identity. An adversary controlling a list of - /// L codewords can bias the check with probability at most `L / |F|`. - /// - /// In log form: - /// - /// ```text - /// bits_of_security = field_size_bits - (list_size_bits + 1) - /// ``` - /// - /// The `+1` accounts for the union bound over the list. - #[must_use] - pub const fn fold_sumcheck_error( - &self, - field_size_bits: usize, - num_variables: usize, - log_inv_rate: usize, - ) -> f64 { - // List size at the current proximity parameter and code rate. - let list_size = self.list_size_bits(num_variables, log_inv_rate); - - // Security = field size minus the adversary's advantage from list decoding. - field_size_bits as f64 - (list_size + 1.) - } - - /// Compute the soundness error (in bits) of the query-combination step. - /// - /// After STIR queries and OOD samples, the verifier takes a random - /// linear combination of all collected evaluations. An adversary must - /// fool this combination for every codeword in the list, giving error: - /// - /// ```text - /// error = (ood_samples + num_queries) * list_size / |F| - /// ``` - /// - /// In log form (all quantities in bits): - /// - /// ```text - /// bits_of_security = field_size_bits - (log_2(ood + queries) + list_size_bits + 1) - /// ``` - /// - /// The `+1` accounts for the union bound over list elements. - #[must_use] - pub fn queries_combination_error( - &self, - field_size_bits: usize, - num_variables: usize, - log_inv_rate: usize, - ood_samples: usize, - num_queries: usize, - ) -> f64 { - // List size at the current proximity parameter. - let list_size = self.list_size_bits(num_variables, log_inv_rate); - - // Total number of evaluation points available for the combination. - let log_combination = libm::log2((ood_samples + num_queries) as f64); - - // Security = field size minus the adversary's advantage. - field_size_bits as f64 - (log_combination + list_size + 1.) - } - - /// Compute the PoW difficulty needed for the folding step. - /// - /// The folding step has two independent error sources: - /// - Proximity gaps: the probability that a far-from-RS function - /// survives the fold (depends on the code rate and list size). - /// - Sumcheck: the probability that the sumcheck verifier accepts - /// a wrong claim (depends on the field size and list size). - /// - /// The overall folding error is limited by the weaker bound. - /// PoW must bridge the gap to the target security level: - /// - /// ```text - /// pow_bits = max(0, security_level - min(prox_gaps_error, sumcheck_error)) - /// ``` - /// - /// Returns 0 when the algebraic bounds alone meet the target. - #[must_use] - pub fn folding_pow_bits( - &self, - security_level: usize, - field_size_bits: usize, - num_variables: usize, - log_inv_rate: usize, - ) -> f64 { - // Proximity gaps bound: how many bits of security the - // fold-and-test step provides against far-from-RS functions. - // Uses arity 2 because each fold combines exactly 2 evaluations. - let prox_gaps_error = self.prox_gaps_error(num_variables, log_inv_rate, field_size_bits, 2); - - // Sumcheck bound: security from the random folding challenge. - // Bounded by (list_size + 1) / |F|. - let sumcheck_error = self.fold_sumcheck_error(field_size_bits, num_variables, log_inv_rate); - - // The folding step is only as strong as its weakest component. - let error = prox_gaps_error.min(sumcheck_error); - - // PoW covers the remaining gap; zero means no grinding needed. - 0_f64.max(security_level as f64 - error) - } -} - -impl Display for SecurityAssumption { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str(match self { - Self::JohnsonBound => "JohnsonBound", - Self::CapacityBound => "CapacityBound", - Self::UniqueDecoding => "UniqueDecoding", - }) - } -} - -impl FromStr for SecurityAssumption { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "JohnsonBound" => Ok(Self::JohnsonBound), - "CapacityBound" => Ok(Self::CapacityBound), - "UniqueDecoding" => Ok(Self::UniqueDecoding), - _ => Err(format!("Invalid soundness specification: {s}")), - } - } -} - -#[cfg(test)] -#[allow(clippy::cast_lossless)] -mod tests { - use alloc::string::ToString; - - use super::*; - - #[test] - fn test_soundness_type_display() { - assert_eq!(SecurityAssumption::JohnsonBound.to_string(), "JohnsonBound"); - assert_eq!( - SecurityAssumption::CapacityBound.to_string(), - "CapacityBound" - ); - assert_eq!( - SecurityAssumption::UniqueDecoding.to_string(), - "UniqueDecoding" - ); - } - - #[test] - fn test_soundness_type_from_str() { - assert_eq!( - SecurityAssumption::from_str("JohnsonBound"), - Ok(SecurityAssumption::JohnsonBound) - ); - assert_eq!( - SecurityAssumption::from_str("CapacityBound"), - Ok(SecurityAssumption::CapacityBound) - ); - assert_eq!( - SecurityAssumption::from_str("UniqueDecoding"), - Ok(SecurityAssumption::UniqueDecoding) - ); - - // Invalid cases - assert!(SecurityAssumption::from_str("InvalidType").is_err()); - assert!(SecurityAssumption::from_str("").is_err()); // Empty string - } - - #[test] - #[should_panic(expected = "num_functions must be >= 2")] - fn prox_gaps_error_panics_when_num_functions_is_one() { - let assumption = SecurityAssumption::UniqueDecoding; - let _ = assumption.prox_gaps_error(1, 1, 64, 1); - } - - #[test] - #[should_panic(expected = "num_functions must be >= 2")] - fn prox_gaps_error_panics_when_num_functions_is_zero() { - let assumption = SecurityAssumption::UniqueDecoding; - let _ = assumption.prox_gaps_error(1, 1, 64, 0); - } - - #[test] - fn test_ud_errors() { - let assumption = SecurityAssumption::UniqueDecoding; - - // Setting - let log_degree = 20; - let degree = (1 << log_degree) as f64; - let log_inv_rate = 2; - let rate = 1. / (1 << log_inv_rate) as f64; - - let field_size_bits = 128; - - // List size - assert!(assumption.list_size_bits(log_degree, log_inv_rate) - 0. < 0.01); - - // Prox gaps - let computed_error = - assumption.prox_gaps_error(log_degree, log_inv_rate, field_size_bits, 2); - let real_error_non_log = degree / rate; - let real_error = field_size_bits as f64 - real_error_non_log.log2(); - - assert!((computed_error - real_error).abs() < 0.01); - } - - #[test] - fn test_jb_errors() { - let assumption = SecurityAssumption::JohnsonBound; - - // Setting - let log_degree = 20; - let log_inv_rate = 2; - let rate = 1. / (1 << log_inv_rate) as f64; - - let eta = rate.sqrt() / 20.; - - let field_size_bits = 128; - - // List size - let real_list_size = 1. / (2. * eta * rate.sqrt()); - let computed_list_size = assumption.list_size_bits(log_degree, log_inv_rate); - assert!((real_list_size.log2() - computed_list_size).abs() < 0.01); - - // Prox gaps - Updated to use Theorem 1.5 from [BCSS25] - // - // From "On Proximity Gaps for Reed–Solomon Codes" (eprint 2025/2055): - // With η = √ρ/20, m = 10, the error bound is: - // a ≈ (2 · 10.5^5) / (3 · ρ^(3/2)) · n - // - // where n = 2^(log_degree + log_inv_rate) - let computed_error = - assumption.prox_gaps_error(log_degree, log_inv_rate, field_size_bits, 2); - - // n = 2^(log_degree + log_inv_rate) = 2^22 - let n = (1_u64 << (log_degree + log_inv_rate)) as f64; - // ρ = rate = 2^(-log_inv_rate) = 0.25 - let rho = rate; - // Constant from Theorem 1.5: (2 · 10.5^5) / 3 ≈ 85085.44 - let constant = 2. * 10.5_f64.powi(5) / 3.; - // a ≈ constant · n / ρ^(3/2) - let real_error_non_log = constant * n / rho.powf(1.5); - let real_error = field_size_bits as f64 - real_error_non_log.log2(); - - assert!( - (computed_error - real_error).abs() < 0.01, - "computed: {computed_error}, expected: {real_error}" - ); - } - - #[test] - fn test_cb_errors() { - let assumption = SecurityAssumption::CapacityBound; - - // Setting - let log_degree = 20; - let degree = (1 << log_degree) as f64; - let log_inv_rate = 2; - let rate = 1. / (1 << log_inv_rate) as f64; - - let eta = rate / 20.; - - let field_size_bits = 128; - - // List size - let real_list_size = degree / (rate * eta); - let computed_list_size = assumption.list_size_bits(log_degree, log_inv_rate); - assert!((real_list_size.log2() - computed_list_size).abs() < 0.01); - - // Prox gaps - let computed_error = - assumption.prox_gaps_error(log_degree, log_inv_rate, field_size_bits, 2); - let real_error_non_log = degree / (eta * rate.powi(2)); - let real_error = field_size_bits as f64 - real_error_non_log.log2(); - - assert!((computed_error - real_error).abs() < 0.01); - } - - #[test] - fn test_folding_pow_bits() { - let field_size_bits = 64; - let soundness = SecurityAssumption::CapacityBound; - - let pow_bits = soundness.folding_pow_bits( - 100, // Security level - field_size_bits, - 10, // Number of variables - 5, // Log inverse rate - ); - - // PoW bits should never be negative - assert!(pow_bits >= 0.); - } -} diff --git a/src/parameters/whir.rs b/src/parameters/whir.rs deleted file mode 100644 index 621306c5..00000000 --- a/src/parameters/whir.rs +++ /dev/null @@ -1,741 +0,0 @@ -//! Derived WHIR protocol configuration computed from user-facing parameters. - -use alloc::vec::Vec; -use core::marker::PhantomData; - -use p3_challenger::{FieldChallenger, GrindingChallenger}; -use p3_commit::Mmcs; -use p3_field::{ExtensionField, Field, TwoAdicField}; -use p3_multilinear_util::evals::EvaluationsList; - -use super::{FoldingFactor, ProtocolParameters, SecurityAssumption}; -use crate::constraints::statement::initial::InitialStatement; - -/// Selects which sumcheck algorithm variant to use during proving. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] -pub enum SumcheckStrategy { - /// Protocol with statement using classic sumcheck (no optimization). - /// - /// This is the standard baseline implementation where the prover proves - /// both polynomial commitment validity and evaluation statements. - Classic, - - /// Protocol with statement using Small Value Optimization (SVO). - /// - /// Uses SVO from Algorithm 6 of with - /// specialized accumulators for the first three rounds to reduce prover work. - #[default] - Svo, -} - -/// Derived configuration for a single intermediate WHIR round. -/// -/// All values are computed from the user-facing protocol parameters -/// and the accumulated state from prior rounds. -#[derive(Debug, Clone)] -pub struct RoundConfig { - /// Proof-of-work difficulty (in bits) for the STIR query phase. - pub pow_bits: usize, - /// Proof-of-work difficulty (in bits) for the folding sumcheck phase. - pub folding_pow_bits: usize, - /// Number of STIR proximity queries to make in this round. - pub num_queries: usize, - /// Number of out-of-domain evaluation samples. - pub ood_samples: usize, - /// Number of multilinear variables remaining after folding in this round. - pub num_variables: usize, - /// Number of variables folded in this round. - pub folding_factor: usize, - /// Size of the evaluation domain before folding in this round. - pub domain_size: usize, - /// Generator of the folded evaluation domain after this round's fold. - pub folded_domain_gen: F, -} - -/// Fully derived WHIR protocol configuration. -/// -/// Built from user-facing protocol parameters plus the polynomial size. -/// -/// Contains all precomputed values needed by the prover and verifier. -#[derive(Debug, Clone)] -pub struct WhirConfig -where - F: Field, - EF: ExtensionField, -{ - /// Number of variables in the original multilinear polynomial. - pub num_variables: usize, - /// Which proximity bound is assumed for soundness analysis. - pub soundness_type: SecurityAssumption, - /// Target security level in bits. - pub security_level: usize, - /// Maximum allowed proof-of-work difficulty in bits. - pub max_pow_bits: usize, - - /// Number of out-of-domain samples during the commitment phase. - pub commitment_ood_samples: usize, - /// Log_2 of the inverse rate of the initial Reed-Solomon code. - pub starting_log_inv_rate: usize, - /// PoW bits for the initial folding sumcheck (before any STIR rounds). - pub starting_folding_pow_bits: usize, - - /// Strategy for how many variables to fold per round. - pub folding_factor: FoldingFactor, - /// By how much the RS domain shrinks at the first round. - /// - /// Subsequent rounds always halve (factor = 1). - pub rs_domain_initial_reduction_factor: usize, - /// Per-round derived configuration for each intermediate STIR round. - pub round_parameters: Vec>, - - /// Number of STIR queries in the final proximity test. - pub final_queries: usize, - /// PoW bits for the final STIR query phase. - pub final_pow_bits: usize, - /// Number of sumcheck rounds in the final phase. - pub final_sumcheck_rounds: usize, - /// PoW bits for the final folding sumcheck. - pub final_folding_pow_bits: usize, - - /// Merkle tree commitment scheme. - pub mmcs: MT, - - /// Phantom marker for the extension field type. - pub _extension_field: PhantomData, - /// Phantom marker for the challenger type. - pub _challenger: PhantomData, -} - -impl WhirConfig -where - F: TwoAdicField, - EF: ExtensionField + TwoAdicField, - MT: Mmcs, - Challenger: FieldChallenger + GrindingChallenger, -{ - /// Derive a full protocol configuration from user-facing parameters. - #[allow(clippy::too_many_lines)] - pub fn new(num_variables: usize, whir_parameters: ProtocolParameters) -> Self { - // --------------------------------------------------------------- - // Phase 1: Validate inputs and set up global constants. - // --------------------------------------------------------------- - - // Preserve the original variable count; the mutable copy below - // tracks the remaining variables as we consume them round by round. - let initial_num_variables = num_variables; - - // Reject folding factors that are incompatible with the polynomial size. - whir_parameters - .folding_factor - .check_validity(num_variables) - .unwrap(); - - // The domain reduction at round 0 must not exceed the folding factor, - // otherwise the code rate would *increase*, weakening soundness. - assert!( - whir_parameters.rs_domain_initial_reduction_factor - <= whir_parameters.folding_factor.at_round(0), - "Increasing the code rate is not a good idea" - ); - - // PoW contributes an independent additive term to security, - // so the algebraic protocol only needs to cover the remainder. - let protocol_security_level = whir_parameters - .security_level - .saturating_sub(whir_parameters.pow_bits); - - // Number of bits in the extension field; upper-bounds all per-element errors. - let field_size_bits = EF::bits(); - - // Mutable state that evolves as we derive per-round parameters. - let mut log_inv_rate = whir_parameters.starting_log_inv_rate; - let mut num_variables = num_variables; - - // Initial evaluation domain: 2^(num_variables + log_inv_rate) points. - let log_domain_size = num_variables + log_inv_rate; - let mut domain_size: usize = 1 << log_domain_size; - - // --------------------------------------------------------------- - // Phase 2: Two-adicity guard. - // --------------------------------------------------------------- - // - // After the first fold the domain has size 2^log_folded_domain_size. - // We restrict this to F::TWO_ADICITY so that: - // - FFT twiddle factors stay in the base field (faster). - // - WHIR query equality polynomials stay in the base field. - // - // A larger folding_factor_0 pushes the folded domain below the limit. - // This does NOT restrict how much data can be committed. - let log_folded_domain_size = log_domain_size - whir_parameters.folding_factor.at_round(0); - assert!( - log_folded_domain_size <= F::TWO_ADICITY, - "Increase folding_factor_0" - ); - - // --------------------------------------------------------------- - // Phase 3: Determine round structure. - // --------------------------------------------------------------- - - // How many intermediate STIR rounds, and how many variables remain - // for the final direct-send sumcheck. - let (num_rounds, final_sumcheck_rounds) = whir_parameters - .folding_factor - .compute_number_of_rounds(num_variables); - - // OOD samples for the commitment phase (before any folding). - let commitment_ood_samples = whir_parameters.soundness_type.determine_ood_samples( - whir_parameters.security_level, - num_variables, - log_inv_rate, - field_size_bits, - ); - - // PoW difficulty for the very first folding sumcheck. - let starting_folding_pow_bits = whir_parameters.soundness_type.folding_pow_bits( - whir_parameters.security_level, - field_size_bits, - num_variables, - log_inv_rate, - ); - - // --------------------------------------------------------------- - // Phase 4: Per-round parameter derivation. - // --------------------------------------------------------------- - // - // After the initial fold, each round i: - // 1. Computes the new code rate after folding. - // 2. Determines query count from the old rate (queries test - // proximity to the code *before* this round's fold). - // 3. Determines OOD sample count from the new rate. - // 4. Derives PoW for both the query and folding sub-steps. - // 5. Records the domain generator for the folded evaluation domain. - - let mut round_parameters = Vec::with_capacity(num_rounds); - - // Subtract the first-round folding factor; the loop below - // handles subsequent rounds. - num_variables -= whir_parameters.folding_factor.at_round(0); - - for round in 0..num_rounds { - // Only round 0 applies the user-configured domain reduction; - // all later rounds halve the domain (reduction factor = 1). - let rs_reduction_factor = if round == 0 { - whir_parameters.rs_domain_initial_reduction_factor - } else { - 1 - }; - - // The code rate increases by (folding_factor - rs_reduction_factor) bits. - // Queries use the *old* rate; OOD and folding use the *new* rate. - let next_rate = log_inv_rate - + (whir_parameters.folding_factor.at_round(round) - rs_reduction_factor); - - // Number of STIR proximity queries at the current (old) rate. - let num_queries = whir_parameters - .soundness_type - .queries(protocol_security_level, log_inv_rate); - - // OOD samples needed at the post-fold (new) rate. - let ood_samples = whir_parameters.soundness_type.determine_ood_samples( - whir_parameters.security_level, - num_variables, - next_rate, - field_size_bits, - ); - - // Two independent error sources bound the STIR round: - // - query_error: (1 - delta)^num_queries proximity test. - // - combination_error: random linear combination of OOD + queries. - // The weaker bound determines how much PoW is needed. - let query_error = whir_parameters - .soundness_type - .queries_error(log_inv_rate, num_queries); - let combination_error = whir_parameters.soundness_type.queries_combination_error( - field_size_bits, - num_variables, - next_rate, - ood_samples, - num_queries, - ); - - // PoW bridges the gap between the target and the weaker bound. - let pow_bits = 0_f64 - .max(whir_parameters.security_level as f64 - (query_error.min(combination_error))); - - // PoW difficulty for the folding sumcheck at the new rate. - let folding_pow_bits = whir_parameters.soundness_type.folding_pow_bits( - whir_parameters.security_level, - field_size_bits, - num_variables, - next_rate, - ); - - let folding_factor = whir_parameters.folding_factor.at_round(round); - let next_folding_factor = whir_parameters.folding_factor.at_round(round + 1); - - // Generator of the two-adic subgroup for the folded domain. - let folded_domain_gen = - F::two_adic_generator(domain_size.ilog2() as usize - folding_factor); - - round_parameters.push(RoundConfig { - pow_bits: pow_bits as usize, - folding_pow_bits: folding_pow_bits as usize, - num_queries, - ood_samples, - num_variables, - folding_factor, - domain_size, - folded_domain_gen, - }); - - // Advance mutable state for the next iteration. - num_variables -= next_folding_factor; - log_inv_rate = next_rate; - domain_size >>= rs_reduction_factor; - } - - // --------------------------------------------------------------- - // Phase 5: Final round parameters. - // --------------------------------------------------------------- - - // Final proximity queries at the last accumulated rate. - let final_queries = whir_parameters - .soundness_type - .queries(protocol_security_level, log_inv_rate); - - // PoW for the final query phase: covers the gap between the - // target and the query error at the final rate. - let final_pow_bits = 0_f64.max( - whir_parameters.security_level as f64 - - whir_parameters - .soundness_type - .queries_error(log_inv_rate, final_queries), - ); - - // The final folding sumcheck error is bounded by 1/|F|, - // so PoW = max(0, security_level - (field_size - 1)). - let final_folding_pow_bits = - 0_f64.max(whir_parameters.security_level as f64 - (field_size_bits - 1) as f64); - - Self { - security_level: whir_parameters.security_level, - max_pow_bits: whir_parameters.pow_bits, - commitment_ood_samples, - num_variables: initial_num_variables, - soundness_type: whir_parameters.soundness_type, - starting_log_inv_rate: whir_parameters.starting_log_inv_rate, - starting_folding_pow_bits: starting_folding_pow_bits as usize, - folding_factor: whir_parameters.folding_factor, - rs_domain_initial_reduction_factor: whir_parameters.rs_domain_initial_reduction_factor, - round_parameters, - final_queries, - final_pow_bits: final_pow_bits as usize, - final_sumcheck_rounds, - final_folding_pow_bits: final_folding_pow_bits as usize, - mmcs: whir_parameters.mmcs, - _extension_field: PhantomData, - _challenger: PhantomData, - } - } - - /// Returns the size of the initial evaluation domain. - /// - /// This is the size of the domain used to evaluate the original multilinear polynomial - /// before any folding or reduction steps are applied in the WHIR protocol. - /// - /// It is computed as: - /// - /// \begin{equation} - /// 2^{\text{num\_variables} + \text{starting\_log\_inv\_rate}} - /// \end{equation} - /// - /// - `num_variables` is the number of variables in the original multivariate polynomial. - /// - `starting_log_inv_rate` is the initial inverse rate of the Reed–Solomon code, - /// controlling redundancy relative to the degree. - /// - /// # Returns - /// A power-of-two value representing the number of evaluation points in the starting domain. - pub const fn starting_domain_size(&self) -> usize { - 1 << (self.num_variables + self.starting_log_inv_rate) - } - - /// Returns the number of intermediate STIR rounds (excludes the final round). - pub const fn n_rounds(&self) -> usize { - self.round_parameters.len() - } - - /// Returns how many bits the RS domain shrinks by at the given round. - /// - /// The first round uses the user-configured initial reduction factor. - /// All subsequent rounds halve the domain (factor = 1). - pub const fn rs_reduction_factor(&self, round: usize) -> usize { - if round == 0 { - self.rs_domain_initial_reduction_factor - } else { - 1 - } - } - - /// Returns the log2 size of the largest FFT - /// (At commitment we perform 2^folding_factor FFT of size 2^max_fft_size) - pub const fn max_fft_size(&self) -> usize { - self.num_variables + self.starting_log_inv_rate - self.folding_factor.at_round(0) - } - - /// Returns whether all PoW difficulties are within the configured maximum. - /// - /// Checks the starting, final, and per-round PoW bits against the ceiling. - /// Returns false if any value exceeds the limit. - pub fn check_pow_bits(&self) -> bool { - let max_bits = self.max_pow_bits; - - // Check the main pow bits values - if self.starting_folding_pow_bits > max_bits - || self.final_pow_bits > max_bits - || self.final_folding_pow_bits > max_bits - { - return false; - } - - // Check all round parameters - self.round_parameters - .iter() - .all(|r| r.pow_bits <= max_bits && r.folding_pow_bits <= max_bits) - } - - /// Compute the synthetic or derived `RoundConfig` for the final phase. - /// - /// - If no folding rounds were configured, constructs a fallback config - /// based on the starting domain and folding factor. - /// - If rounds were configured, derives the final config by adapting - /// the last round’s values for the final folding phase. - /// - /// This is used by the verifier when verifying the final polynomial, - /// ensuring consistent challenge selection and STIR constraint handling. - pub fn final_round_config(&self) -> RoundConfig { - if self.round_parameters.is_empty() { - // No intermediate rounds: the polynomial was small enough that - // the initial fold leads directly to the final phase. - // Use the starting domain and initial folding factor. - RoundConfig { - num_variables: self.num_variables - self.folding_factor.at_round(0), - folding_factor: self.folding_factor.at_round(self.n_rounds()), - num_queries: self.final_queries, - pow_bits: self.final_pow_bits, - domain_size: self.starting_domain_size(), - folded_domain_gen: F::two_adic_generator( - self.starting_domain_size().ilog2() as usize - self.folding_factor.at_round(0), - ), - ood_samples: 0, - folding_pow_bits: self.final_folding_pow_bits, - } - } else { - // Apply the last round’s domain reduction to get the domain - // size entering the final phase. - let rs_reduction_factor = self.rs_reduction_factor(self.n_rounds() - 1); - let folding_factor = self.folding_factor.at_round(self.n_rounds()); - - let last = self.round_parameters.last().unwrap(); - - // The domain shrinks by the RS reduction factor from the last round. - let domain_size = last.domain_size >> rs_reduction_factor; - - // Generator for the final folded domain. - let folded_domain_gen = F::two_adic_generator( - domain_size.ilog2() as usize - self.folding_factor.at_round(self.n_rounds()), - ); - - RoundConfig { - // Variables remaining after this final fold. - num_variables: last.num_variables - folding_factor, - folding_factor, - num_queries: self.final_queries, - pow_bits: self.final_pow_bits, - domain_size, - folded_domain_gen, - // Inherit OOD count from the last intermediate round. - ood_samples: last.ood_samples, - folding_pow_bits: self.final_folding_pow_bits, - } - } - } - - /// Returns the inverse rate of the RS code at the given round. - /// - /// The inverse rate is `domain_size / degree`, where: - /// - `domain_size` is the evaluation domain after the round's reduction. - /// - `degree` is 2^(remaining variables after all folds up to this round). - /// - /// ```text - /// inv_rate = (round_domain_size >> rs_reduction) / 2^(num_vars - total_folded) - /// ``` - pub(crate) fn inv_rate(&self, round: usize) -> usize { - // Shrink the domain by this round's reduction factor. - let domain_reduction = 1 << self.rs_reduction_factor(round); - let new_domain_size = self.round_parameters[round].domain_size / domain_reduction; - - // Number of polynomial evaluations (= degree) after all folds so far. - let num_evals = 1 << (self.num_variables - self.folding_factor.total_number(round)); - - // Ratio gives the inverse rate. - new_domain_size / num_evals - } - - /// Create the initial statement for the WHIR protocol. - /// - /// Wraps the polynomial with the first-round folding factor and - /// the chosen sumcheck strategy. Evaluation constraints are added - /// by the caller before proving begins. - pub const fn initial_statement( - &self, - polynomial: EvaluationsList, - sumcheck_strategy: SumcheckStrategy, - ) -> InitialStatement { - InitialStatement::new( - polynomial, - self.folding_factor.at_round(0), - sumcheck_strategy, - ) - } -} - -#[cfg(test)] -mod tests { - use alloc::vec; - - use p3_baby_bear::{BabyBear, Poseidon2BabyBear}; - use p3_challenger::DuplexChallenger; - use p3_field::{Field, PrimeCharacteristicRing}; - use p3_merkle_tree::MerkleTreeMmcs; - use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; - use rand::SeedableRng; - - use super::*; - - type F = BabyBear; - type Perm = Poseidon2BabyBear<16>; - type MyHash = PaddingFreeSponge; - type MyCompress = TruncatedPermutation; - type PackedF = ::Packing; - type MyMmcs = MerkleTreeMmcs; - type MyChallenger = DuplexChallenger; - - /// Generates default WHIR parameters - fn default_whir_params() -> ProtocolParameters { - let mut rng = rand::rngs::SmallRng::seed_from_u64(1); - let perm = Perm::new_from_rng_128(&mut rng); - let mmcs = MyMmcs::new(MyHash::new(perm.clone()), MyCompress::new(perm), 0); - - ProtocolParameters { - security_level: 100, - pow_bits: 20, - rs_domain_initial_reduction_factor: 1, - folding_factor: FoldingFactor::ConstantFromSecondRound(4, 4), - mmcs, - soundness_type: SecurityAssumption::CapacityBound, - starting_log_inv_rate: 1, - } - } - - #[test] - fn test_whir_config_creation() { - let params = default_whir_params(); - - let config = WhirConfig::::new(10, params); - - assert_eq!(config.security_level, 100); - assert_eq!(config.max_pow_bits, 20); - assert_eq!(config.soundness_type, SecurityAssumption::CapacityBound); - } - - #[test] - fn test_n_rounds() { - let params = default_whir_params(); - let config = WhirConfig::::new(10, params); - - assert_eq!(config.n_rounds(), config.round_parameters.len()); - } - - #[test] - fn test_check_pow_bits_within_limits() { - let params = default_whir_params(); - let mut config = WhirConfig::::new(10, params); - - // Set all values within limits - config.max_pow_bits = 20; - config.starting_folding_pow_bits = 15; - config.final_pow_bits = 18; - config.final_folding_pow_bits = 19; - - // Ensure all rounds are within limits - config.round_parameters = vec![ - RoundConfig { - pow_bits: 17, - folding_pow_bits: 19, - num_queries: 5, - ood_samples: 2, - num_variables: 10, - folding_factor: 2, - domain_size: 10, - folded_domain_gen: F::from_u64(2), - }, - RoundConfig { - pow_bits: 18, - folding_pow_bits: 19, - num_queries: 6, - ood_samples: 2, - num_variables: 10, - folding_factor: 2, - domain_size: 10, - folded_domain_gen: F::from_u64(2), - }, - ]; - - assert!( - config.check_pow_bits(), - "All values are within limits, check_pow_bits should return true." - ); - } - - #[test] - fn test_check_pow_bits_starting_folding_exceeds() { - let params = default_whir_params(); - let mut config = WhirConfig::::new(10, params); - - config.max_pow_bits = 20; - config.starting_folding_pow_bits = 21; // Exceeds max_pow_bits - config.final_pow_bits = 18; - config.final_folding_pow_bits = 19; - - assert!( - !config.check_pow_bits(), - "Starting folding pow bits exceeds max_pow_bits, should return false." - ); - } - - #[test] - fn test_check_pow_bits_final_pow_exceeds() { - let params = default_whir_params(); - let mut config = WhirConfig::::new(10, params); - - config.max_pow_bits = 20; - config.starting_folding_pow_bits = 15; - config.final_pow_bits = 21; // Exceeds max_pow_bits - config.final_folding_pow_bits = 19; - - assert!( - !config.check_pow_bits(), - "Final pow bits exceeds max_pow_bits, should return false." - ); - } - - #[test] - fn test_check_pow_bits_round_pow_exceeds() { - let params = default_whir_params(); - let mut config = WhirConfig::::new(10, params); - - config.max_pow_bits = 20; - config.starting_folding_pow_bits = 15; - config.final_pow_bits = 18; - config.final_folding_pow_bits = 19; - - // One round's pow_bits exceeds limit - config.round_parameters = vec![RoundConfig { - pow_bits: 21, // Exceeds max_pow_bits - folding_pow_bits: 19, - num_queries: 5, - ood_samples: 2, - num_variables: 10, - folding_factor: 2, - domain_size: 10, - folded_domain_gen: F::from_u64(2), - }]; - - assert!( - !config.check_pow_bits(), - "A round has pow_bits exceeding max_pow_bits, should return false." - ); - } - - #[test] - fn test_check_pow_bits_round_folding_pow_exceeds() { - let params = default_whir_params(); - let mut config = WhirConfig::::new(10, params); - - config.max_pow_bits = 20; - config.starting_folding_pow_bits = 15; - config.final_pow_bits = 18; - config.final_folding_pow_bits = 19; - - // One round's folding_pow_bits exceeds limit - config.round_parameters = vec![RoundConfig { - pow_bits: 19, - folding_pow_bits: 21, // Exceeds max_pow_bits - num_queries: 5, - ood_samples: 2, - num_variables: 10, - folding_factor: 2, - domain_size: 10, - folded_domain_gen: F::from_u64(2), - }]; - - assert!( - !config.check_pow_bits(), - "A round has folding_pow_bits exceeding max_pow_bits, should return false." - ); - } - - #[test] - fn test_check_pow_bits_exactly_at_limit() { - let params = default_whir_params(); - let mut config = WhirConfig::::new(10, params); - - config.max_pow_bits = 20; - config.starting_folding_pow_bits = 20; - config.final_pow_bits = 20; - config.final_folding_pow_bits = 20; - - config.round_parameters = vec![RoundConfig { - pow_bits: 20, - folding_pow_bits: 20, - num_queries: 5, - ood_samples: 2, - num_variables: 10, - folding_factor: 2, - domain_size: 10, - folded_domain_gen: F::from_u64(2), - }]; - - assert!( - config.check_pow_bits(), - "All pow_bits are exactly at max_pow_bits, should return true." - ); - } - - #[test] - fn test_check_pow_bits_all_exceed() { - let params = default_whir_params(); - let mut config = WhirConfig::::new(10, params); - - config.max_pow_bits = 20; - config.starting_folding_pow_bits = 22; - config.final_pow_bits = 23; - config.final_folding_pow_bits = 24; - - config.round_parameters = vec![RoundConfig { - pow_bits: 25, - folding_pow_bits: 26, - num_queries: 5, - ood_samples: 2, - num_variables: 10, - folding_factor: 2, - domain_size: 10, - folded_domain_gen: F::from_u64(2), - }]; - - assert!( - !config.check_pow_bits(), - "All values exceed max_pow_bits, should return false." - ); - } -} diff --git a/src/sumcheck/data.rs b/src/sumcheck/data.rs deleted file mode 100644 index 58ff3709..00000000 --- a/src/sumcheck/data.rs +++ /dev/null @@ -1,634 +0,0 @@ -use alloc::vec::Vec; - -use p3_challenger::{FieldChallenger, GrindingChallenger}; -use p3_field::{ExtensionField, Field, TwoAdicField}; -use p3_multilinear_util::multilinear::MultilinearPoint; -use serde::{Deserialize, Serialize}; - -use super::{SumcheckError, extrapolate_012}; - -/// Sumcheck polynomial data -/// -/// Stores the polynomial evaluations for sumcheck rounds in a compact format. -/// Each round stores `[h(0), h(2)]` where `h(1)` is derived as `claimed_sum - h(0)`. -#[derive(Default, Serialize, Deserialize, Clone, Debug)] -pub struct SumcheckData { - /// Polynomial evaluations for each sumcheck round - /// - /// Each entry is `[h(0), h(2)]` - the evaluations at 0 and 2 - /// - /// `h(1)` is derived as `claimed_sum - h(0)` by the verifier - /// - /// Length: folding_factor - pub(crate) polynomial_evaluations: Vec<[EF; 2]>, - - /// PoW witnesses for each sumcheck round - /// Length: folding_factor - pub(crate) pow_witnesses: Vec, -} - -impl SumcheckData { - /// Returns the polynomial evaluations `[h(0), h(2)]` for each round. - #[must_use] - pub fn polynomial_evaluations(&self) -> &[[EF; 2]] { - &self.polynomial_evaluations - } - - /// Returns the number of rounds stored in this proof data. - #[must_use] - pub const fn num_rounds(&self) -> usize { - self.polynomial_evaluations.len() - } - - /// Commits polynomial coefficients to the transcript and returns a challenge. - /// - /// This helper function handles the Fiat-Shamir interaction for a sumcheck round. - /// - /// # Arguments - /// - /// * `challenger` - Fiat-Shamir transcript. - /// * `c0` - Constant coefficient `h(0)`. - /// * `c2` - Quadratic coefficient. - /// * `pow_bits` - PoW difficulty (0 to skip grinding). - /// - /// # Returns - /// - /// The sampled challenge `r \in EF`. - pub fn observe_and_sample( - &mut self, - challenger: &mut Challenger, - c0: EF, - c2: EF, - pow_bits: usize, - ) -> EF - where - BF: Field, - EF: ExtensionField, - F: Clone, - Challenger: FieldChallenger + GrindingChallenger, - { - // Record the polynomial coefficients in the proof. - self.polynomial_evaluations.push([c0, c2]); - - // Absorb coefficients into the transcript. - // - // Note: We only send (c_0, c_2). The verifier derives c_1 from the sum constraint. - challenger.observe_algebra_slice(&[c0, c2]); - - // Optional proof-of-work to increase prover cost. - // - // This makes it expensive for a malicious prover to "mine" favorable challenges. - if pow_bits > 0 { - self.pow_witnesses.push(challenger.grind(pow_bits)); - } - - // Sample the verifier's challenge for this round. - challenger.sample_algebra_element() - } - - /// Verifies standard sumcheck rounds and extracts folding randomness from the transcript. - /// - /// This method reads from the Fiat–Shamir transcript to simulate verifier interaction - /// in the sumcheck protocol. For each round, it recovers: - /// - One univariate polynomial (usually degree ≤ 2) sent by the prover. - /// - One challenge scalar chosen by the verifier (folding randomness). - /// - /// # Returns - /// - /// A `MultilinearPoint` of folding randomness values. - pub fn verify_rounds( - &self, - challenger: &mut Challenger, - claimed_sum: &mut EF, - pow_bits: usize, - ) -> Result, SumcheckError> - where - F: TwoAdicField, - EF: ExtensionField + TwoAdicField, - Challenger: FieldChallenger + GrindingChallenger, - { - let mut randomness = Vec::with_capacity(self.polynomial_evaluations.len()); - - for (i, &[c0, c2]) in self.polynomial_evaluations.iter().enumerate() { - // Observe only the sent polynomial evaluations (c0 and c2) - challenger.observe_algebra_slice(&[c0, c2]); - - // Verify PoW (only if pow_bits > 0) - if pow_bits > 0 && !challenger.check_witness(pow_bits, self.pow_witnesses[i]) { - return Err(SumcheckError::InvalidPowWitness); - } - - // Sample challenge - let r: EF = challenger.sample_algebra_element(); - // Evaluate sumcheck polynomial at r - *claimed_sum = extrapolate_012(c0, *claimed_sum - c0, c2, r); - randomness.push(r); - } - - Ok(MultilinearPoint::new(randomness)) - } -} - -/// Verify the final sumcheck rounds. -/// -/// This is a free function because the caller may not have a `SumcheckData` at all when `rounds == 0`. -/// -/// # Returns -/// -/// A `MultilinearPoint` of folding randomness values. -pub fn verify_final_sumcheck_rounds( - final_sumcheck: Option<&SumcheckData>, - challenger: &mut Challenger, - claimed_sum: &mut EF, - rounds: usize, - pow_bits: usize, -) -> Result, SumcheckError> -where - F: TwoAdicField, - EF: ExtensionField + TwoAdicField, - Challenger: FieldChallenger + GrindingChallenger, -{ - if rounds == 0 { - return Ok(MultilinearPoint::new(Vec::new())); - } - - let sumcheck = final_sumcheck.ok_or(SumcheckError::MissingSumcheckData { - expected_rounds: rounds, - })?; - - if sumcheck.polynomial_evaluations.len() != rounds { - return Err(SumcheckError::RoundCountMismatch { - expected: rounds, - actual: sumcheck.polynomial_evaluations.len(), - }); - } - sumcheck.verify_rounds(challenger, claimed_sum, pow_bits) -} - -#[cfg(test)] -mod tests { - use alloc::vec; - - use p3_baby_bear::{BabyBear, Poseidon2BabyBear}; - use p3_challenger::DuplexChallenger; - use p3_field::{Field, PrimeCharacteristicRing, extension::BinomialExtensionField}; - use p3_merkle_tree::MerkleTreeMmcs; - use p3_multilinear_util::evals::EvaluationsList; - use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; - use rand::{SeedableRng, rngs::SmallRng}; - - use super::*; - use crate::{ - constraints::statement::initial::InitialStatement, - fiat_shamir::domain_separator::{DomainSeparator, SumcheckParams}, - parameters::{FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy}, - sumcheck::prover::SumcheckProver, - whir::proof::WhirProof, - }; - - /// Type alias for the base field used in tests - type F = BabyBear; - - /// Type alias for the extension field used in tests - type EF = BinomialExtensionField; - - /// Type alias for the permutation used in tests - type Perm = Poseidon2BabyBear<16>; - - type MyHash = PaddingFreeSponge; - type MyCompress = TruncatedPermutation; - - /// Type alias for the challenger used in observe_and_sample tests. - type TestChallenger = DuplexChallenger; - - type PackedF = ::Packing; - type MyMmcs = MerkleTreeMmcs; - - const DIGEST_ELEMS: usize = 8; - - /// Creates a fresh challenger for testing. - /// - /// The challenger is seeded deterministically so tests are reproducible. - fn create_test_challenger() -> TestChallenger { - let perm = Perm::new_from_rng_128(&mut rand::rngs::SmallRng::seed_from_u64(42)); - DuplexChallenger::new(perm) - } - - #[test] - fn test_sumcheck_data_default() { - // Create a default SumcheckData - let sumcheck: SumcheckData = SumcheckData::default(); - - // Verify polynomial_evaluations is empty - assert_eq!(sumcheck.polynomial_evaluations.len(), 0); - - // Verify pow_witnesses is empty - assert!(sumcheck.pow_witnesses.is_empty()); - } - - #[test] - fn test_push_pow_witness() { - let mut sumcheck: SumcheckData = SumcheckData::default(); - - // First push - let witness1 = F::from_u64(42); - sumcheck.pow_witnesses.push(witness1); - - assert_eq!(sumcheck.pow_witnesses.len(), 1); - assert_eq!(sumcheck.pow_witnesses[0], witness1); - - // Second push should append to existing vector - let witness2 = F::from_u64(123); - sumcheck.pow_witnesses.push(witness2); - - assert_eq!(sumcheck.pow_witnesses.len(), 2); - assert_eq!(sumcheck.pow_witnesses[1], witness2); - } - - #[test] - fn test_observe_and_sample_records_coefficients() { - // The method should push [c0, c2] to polynomial_evaluations. - // - // polynomial_evaluations stores the sumcheck polynomial coefficients - // for each round: [h(0), h(2)] where h(1) is derived by the verifier. - let c0 = EF::from_u64(5); - let c2 = EF::from_u64(7); - - let mut sumcheck: SumcheckData = SumcheckData::default(); - let mut challenger = create_test_challenger(); - - // polynomial_evaluations should be empty initially - assert!(sumcheck.polynomial_evaluations.is_empty()); - - // Call observe_and_sample with pow_bits = 0 (no grinding) - let pow_bits = 0; - let _r = sumcheck.observe_and_sample::<_, F>(&mut challenger, c0, c2, pow_bits); - - // polynomial_evaluations should now have one entry: [c0, c2] - assert_eq!(sumcheck.polynomial_evaluations.len(), 1); - assert_eq!(sumcheck.polynomial_evaluations[0][0], c0); - assert_eq!(sumcheck.polynomial_evaluations[0][1], c2); - } - - #[test] - fn test_observe_and_sample_multiple_rounds() { - // Multiple calls should accumulate coefficients in order. - // - // Round 0: push [c0_0, c2_0] - // Round 1: push [c0_1, c2_1] - // Round 2: push [c0_2, c2_2] - let c0_0 = EF::from_u64(1); - let c2_0 = EF::from_u64(2); - let c0_1 = EF::from_u64(3); - let c2_1 = EF::from_u64(4); - let c0_2 = EF::from_u64(5); - let c2_2 = EF::from_u64(6); - - let mut sumcheck: SumcheckData = SumcheckData::default(); - let mut challenger = create_test_challenger(); - let pow_bits = 0; - - // Round 0 - let _r0 = sumcheck.observe_and_sample::<_, F>(&mut challenger, c0_0, c2_0, pow_bits); - assert_eq!(sumcheck.polynomial_evaluations.len(), 1); - - // Round 1 - let _r1 = sumcheck.observe_and_sample::<_, F>(&mut challenger, c0_1, c2_1, pow_bits); - assert_eq!(sumcheck.polynomial_evaluations.len(), 2); - - // Round 2 - let _r2 = sumcheck.observe_and_sample::<_, F>(&mut challenger, c0_2, c2_2, pow_bits); - assert_eq!(sumcheck.polynomial_evaluations.len(), 3); - - // Verify all stored coefficients match input order - assert_eq!(sumcheck.polynomial_evaluations[0], [c0_0, c2_0]); - assert_eq!(sumcheck.polynomial_evaluations[1], [c0_1, c2_1]); - assert_eq!(sumcheck.polynomial_evaluations[2], [c0_2, c2_2]); - } - - #[test] - fn test_observe_and_sample_without_pow() { - // When pow_bits = 0, no PoW witness should be recorded. - // - // The method skips the grinding step when pow_bits is zero, - // so pow_witnesses should remain empty. - let c0 = EF::from_u64(10); - let c2 = EF::from_u64(20); - - let mut sumcheck: SumcheckData = SumcheckData::default(); - let mut challenger = create_test_challenger(); - - // pow_witnesses should be empty initially - assert!(sumcheck.pow_witnesses.is_empty()); - - // Call with pow_bits = 0 - let pow_bits = 0; - let _r = sumcheck.observe_and_sample::<_, F>(&mut challenger, c0, c2, pow_bits); - - // pow_witnesses should still be empty (no grinding performed) - assert!(sumcheck.pow_witnesses.is_empty()); - } - - #[test] - fn test_observe_and_sample_with_pow() { - // When pow_bits > 0, a PoW witness should be recorded. - // - // The method calls challenger.grind(pow_bits) and pushes - // the resulting witness to pow_witnesses. - let c0 = EF::from_u64(10); - let c2 = EF::from_u64(20); - - let mut sumcheck: SumcheckData = SumcheckData::default(); - let mut challenger = create_test_challenger(); - - // pow_witnesses should be empty initially - assert!(sumcheck.pow_witnesses.is_empty()); - - // Call with pow_bits = 1 (minimal PoW) - let pow_bits = 1; - let _r = sumcheck.observe_and_sample::<_, F>(&mut challenger, c0, c2, pow_bits); - - // pow_witnesses should now have one entry - assert_eq!(sumcheck.pow_witnesses.len(), 1); - } - - #[test] - fn test_observe_and_sample_pow_accumulates() { - // Multiple rounds with PoW should accumulate witnesses. - // - // Each call with pow_bits > 0 should add one witness. - let c0 = EF::from_u64(1); - let c2 = EF::from_u64(2); - - let mut sumcheck: SumcheckData = SumcheckData::default(); - let mut challenger = create_test_challenger(); - let pow_bits = 1; - - // Three rounds with PoW - let _r0 = sumcheck.observe_and_sample::<_, F>(&mut challenger, c0, c2, pow_bits); - let _r1 = sumcheck.observe_and_sample::<_, F>(&mut challenger, c0, c2, pow_bits); - let _r2 = sumcheck.observe_and_sample::<_, F>(&mut challenger, c0, c2, pow_bits); - - // Should have 3 witnesses - assert_eq!(sumcheck.pow_witnesses.len(), 3); - // And 3 polynomial evaluations - assert_eq!(sumcheck.polynomial_evaluations.len(), 3); - } - - #[test] - fn test_observe_and_sample_deterministic_challenge() { - // Fiat-Shamir property: same inputs produce same challenge. - // - // Two challengers with the same initial state, observing the same - // coefficients, should sample the same challenge. - let c0 = EF::from_u64(42); - let c2 = EF::from_u64(99); - let pow_bits = 0; - - // First run - let mut sumcheck1: SumcheckData = SumcheckData::default(); - let mut challenger1 = create_test_challenger(); - let r1 = sumcheck1.observe_and_sample::<_, F>(&mut challenger1, c0, c2, pow_bits); - - // Second run with fresh but identically-seeded challenger - let mut sumcheck2: SumcheckData = SumcheckData::default(); - let mut challenger2 = create_test_challenger(); - let r2 = sumcheck2.observe_and_sample::<_, F>(&mut challenger2, c0, c2, pow_bits); - - // Challenges should be identical - assert_eq!(r1, r2); - } - - #[test] - fn test_observe_and_sample_challenge_depends_on_history() { - // The challenge at round i depends on all previous observations. - // - // Two sequences with different history should produce different - // challenges even if the final round has the same coefficients. - let c0 = EF::from_u64(100); - let c2 = EF::from_u64(200); - let pow_bits = 0; - - // Sequence A: observe once, then observe (c0, c2) - let mut sumcheck_a: SumcheckData = SumcheckData::default(); - let mut challenger_a = create_test_challenger(); - let _r0_a = - sumcheck_a.observe_and_sample::<_, F>(&mut challenger_a, EF::ONE, EF::TWO, pow_bits); - let r1_a = sumcheck_a.observe_and_sample::<_, F>(&mut challenger_a, c0, c2, pow_bits); - - // Sequence B: directly observe (c0, c2) without prior round - let mut sumcheck_b: SumcheckData = SumcheckData::default(); - let mut challenger_b = create_test_challenger(); - let r_b = sumcheck_b.observe_and_sample::<_, F>(&mut challenger_b, c0, c2, pow_bits); - - // Challenges should differ due to different transcript history - assert_ne!(r1_a, r_b); - } - - #[test] - fn test_observe_and_sample_returns_extension_field_element() { - // The returned challenge should be a valid extension field element. - // - // This is verified implicitly by the type system, but we can also - // check that it's not trivially zero (with high probability). - let c0 = EF::from_u64(7); - let c2 = EF::from_u64(11); - let pow_bits = 0; - - let mut sumcheck: SumcheckData = SumcheckData::default(); - let mut challenger = create_test_challenger(); - - let r: EF = sumcheck.observe_and_sample::<_, F>(&mut challenger, c0, c2, pow_bits); - - // The challenge should (with overwhelming probability) be non-zero - assert_ne!(r, EF::ZERO); - } - - /// Constructs a default WHIR configuration for testing - fn create_proof_from_test_protocol_params( - num_variables: usize, - folding_factor: FoldingFactor, - ) -> WhirProof { - let mut rng = SmallRng::seed_from_u64(1); - let perm = Perm::new_from_rng_128(&mut rng); - - let merkle_hash = MyHash::new(perm.clone()); - let merkle_compress = MyCompress::new(perm); - let mmcs = MyMmcs::new(merkle_hash, merkle_compress, 0); - - let whir_params = ProtocolParameters { - security_level: 32, - pow_bits: 0, - rs_domain_initial_reduction_factor: 1, - folding_factor, - mmcs, - soundness_type: SecurityAssumption::UniqueDecoding, - starting_log_inv_rate: 1, - }; - - WhirProof::from_protocol_parameters(&whir_params, num_variables) - } - - #[test] - #[allow(clippy::too_many_lines)] - fn test_verify_rounds() { - // Define a multilinear polynomial in 3 variables: - // f(X0, X1, X2) = 1 + 2*X2 + 3*X1 + 4*X1*X2 - // + 5*X0 + 6*X0*X2 + 7*X0*X1 + 8*X0*X1*X2 - let e1 = F::from_u64(1); - let e2 = F::from_u64(2); - let e3 = F::from_u64(3); - let e4 = F::from_u64(4); - let e5 = F::from_u64(5); - let e6 = F::from_u64(6); - let e7 = F::from_u64(7); - let e8 = F::from_u64(8); - - let evals = EvaluationsList::new(vec![ - e1, - e1 + e2, - e1 + e3, - e1 + e2 + e3 + e4, - e1 + e5, - e1 + e2 + e5 + e6, - e1 + e3 + e5 + e7, - e1 + e2 + e3 + e4 + e5 + e6 + e7 + e8, - ]); - - // Define the actual polynomial function over EF4 - let f = |x0: EF, x1: EF, x2: EF| { - x2 * e2 - + x1 * e3 - + x1 * x2 * e4 - + x0 * e5 - + x0 * x2 * e6 - + x0 * x1 * e7 - + x0 * x1 * x2 * e8 - + e1 - }; - - let n_vars = evals.num_variables(); - assert_eq!(n_vars, 3); - let folding_factor = 3; - let pow_bits = 0; - - // Create a constraint system with evaluations of f at various points - let mut statement = - InitialStatement::new(evals, folding_factor, SumcheckStrategy::default()); - - let x_000 = MultilinearPoint::new(vec![EF::ZERO, EF::ZERO, EF::ZERO]); - let x_100 = MultilinearPoint::new(vec![EF::ONE, EF::ZERO, EF::ZERO]); - let x_110 = MultilinearPoint::new(vec![EF::ONE, EF::ONE, EF::ZERO]); - let x_111 = MultilinearPoint::new(vec![EF::ONE, EF::ONE, EF::ONE]); - let x_011 = MultilinearPoint::new(vec![EF::ZERO, EF::ONE, EF::ONE]); - - let f_000 = f(EF::ZERO, EF::ZERO, EF::ZERO); - let f_100 = f(EF::ONE, EF::ZERO, EF::ZERO); - let f_110 = f(EF::ONE, EF::ONE, EF::ZERO); - let f_111 = f(EF::ONE, EF::ONE, EF::ONE); - let f_011 = f(EF::ZERO, EF::ONE, EF::ONE); - - assert_eq!(f_000, statement.evaluate(&x_000)); - assert_eq!(f_100, statement.evaluate(&x_100)); - assert_eq!(f_110, statement.evaluate(&x_110)); - assert_eq!(f_111, statement.evaluate(&x_111)); - assert_eq!(f_011, statement.evaluate(&x_011)); - - // Set up domain separator - let mut domsep: DomainSeparator = DomainSeparator::new(vec![]); - domsep.add_sumcheck(&SumcheckParams { - rounds: folding_factor, - pow_bits, - }); - - let mut rng = SmallRng::seed_from_u64(1); - let challenger = TestChallenger::new(Perm::new_from_rng_128(&mut rng)); - let mut prover_challenger = challenger.clone(); - - // Initialize proof and challenger - let mut proof = - create_proof_from_test_protocol_params(n_vars, FoldingFactor::Constant(folding_factor)); - domsep.observe_domain_separator(&mut prover_challenger); - - // Instantiate the prover with base field coefficients - let (_, _) = SumcheckProver::::from_base_evals( - &mut proof.initial_sumcheck, - &mut prover_challenger, - folding_factor, - pow_bits, - &statement, - ); - - // Reconstruct verifier state to simulate the rounds - let mut verifier_challenger = challenger; - domsep.observe_domain_separator(&mut verifier_challenger); - - // Save a fresh copy for verify_rounds - let mut verifier_challenger_for_verify = verifier_challenger.clone(); - - let mut t = EvaluationsList::zero(statement.num_variables()); - let mut expected_initial_sum = EF::ZERO; - statement.normalize().combine_hypercube::( - &mut t, - &mut expected_initial_sum, - EF::ONE, - ); - - // Start with the claimed sum before folding - let mut current_sum = expected_initial_sum; - - let mut expected = Vec::with_capacity(folding_factor); - - // First round: read c_0 = h(0) and c_2 (quadratic coefficient) - let [c_0, c_2] = proof.initial_sumcheck.polynomial_evaluations[0]; - let h_1 = current_sum - c_0; - - // Observe polynomial evaluations (must match what verify_rounds does) - verifier_challenger.observe_algebra_slice(&[c_0, c_2]); - - // Sample random challenge r_i ∈ EF4 and evaluate h_i(r_i) - let r: EF = verifier_challenger.sample_algebra_element(); - // h(r) = c_2 * r^2 + (h(1) - c_0 - c_2) * r + c_0 - current_sum = c_2 * r.square() + (h_1 - c_0 - c_2) * r + c_0; - expected.push(r); - - for i in 0..folding_factor - 1 { - // Read c_0 = h(0) and c_2 (quadratic coefficient), derive h(1) = claimed_sum - c_0 - let [c_0, c_2] = proof.initial_sumcheck.polynomial_evaluations[i + 1]; - let h_1 = current_sum - c_0; - - // Observe polynomial evaluations - verifier_challenger.observe_algebra_slice(&[c_0, c_2]); - - // Sample random challenge r - let r: EF = verifier_challenger.sample_algebra_element(); - // h(r) = c_2 * r^2 + (h(1) - c_0 - c_2) * r + c_0 - current_sum = c_2 * r.square() + (h_1 - c_0 - c_2) * r + c_0; - - if pow_bits > 0 { - // verifier_state.challenge_pow::(pow_bits).unwrap(); - } - - expected.push(r); - } - - let randomness = proof - .initial_sumcheck - .verify_rounds( - &mut verifier_challenger_for_verify, - &mut expected_initial_sum, - pow_bits, - ) - .unwrap(); - - // Check that number of parsed rounds is correct - assert_eq!(randomness.num_variables(), folding_factor); - - // Reconstruct the expected MultilinearPoint from expected randomness - let expected_randomness = MultilinearPoint::new(expected); - assert_eq!( - randomness, expected_randomness, - "Mismatch in full MultilinearPoint folding randomness" - ); - } -} diff --git a/src/sumcheck/error.rs b/src/sumcheck/error.rs deleted file mode 100644 index 24bddf91..00000000 --- a/src/sumcheck/error.rs +++ /dev/null @@ -1,17 +0,0 @@ -use thiserror::Error; - -/// Errors from sumcheck protocol verification. -#[derive(Error, Debug)] -pub enum SumcheckError { - /// The proof contains a different number of rounds than expected. - #[error("Sumcheck round count mismatch: expected {expected}, got {actual}")] - RoundCountMismatch { expected: usize, actual: usize }, - - /// The proof is missing sumcheck data when rounds > 0. - #[error("Missing sumcheck data for {expected_rounds} expected rounds")] - MissingSumcheckData { expected_rounds: usize }, - - /// Proof-of-work witness verification failed. - #[error("Invalid proof-of-work witness")] - InvalidPowWitness, -} diff --git a/src/sumcheck/lagrange.rs b/src/sumcheck/lagrange.rs deleted file mode 100644 index 5dbc1b6e..00000000 --- a/src/sumcheck/lagrange.rs +++ /dev/null @@ -1,354 +0,0 @@ -//! Lagrange interpolation utilities for sumcheck protocols. -//! -//! This module provides functions for evaluating quadratic polynomials using -//! Lagrange basis interpolation at the points `{0, 1, 2}`. -//! -//! # Mathematical Background -//! -//! Given evaluations of a quadratic polynomial `h(x)` at three points `{0, 1, 2}`, -//! we can reconstruct `h(r)` for any field element `r` using Lagrange interpolation: -//! -//! ```text -//! h(r) = h(0) * L_0(r) + h(1) * L_1(r) + h(2) * L_2(r) -//! ``` -//! -//! where `L_i` are the Lagrange basis polynomials satisfying `L_i(j) = δ_{i,j}`. - -use alloc::vec::Vec; - -use p3_field::Field; - -/// Computes the Lagrange basis weights for interpolation at points `{0, 1, 2}`. -/// -/// Given a field element `r`, returns the array `[L_0(r), L_1(r), L_2(r)]` where -/// `L_i` are the Lagrange basis polynomials for the interpolation set `{0, 1, 2}`. -/// -/// # Lagrange Basis Formulas -/// -/// The Lagrange basis polynomials for interpolation at `{0, 1, 2}` are: -/// -/// ```text -/// L_0(x) = (x - 1)(x - 2) / ((0 - 1)(0 - 2)) = (x - 1)(x - 2) / 2 -/// L_1(x) = (x - 0)(x - 2) / ((1 - 0)(1 - 2)) = -x(x - 2) = x(2 - x) -/// L_2(x) = (x - 0)(x - 1) / ((2 - 0)(2 - 1)) = x(x - 1) / 2 -/// ``` -/// -/// # Properties -/// -/// The basis polynomials satisfy: -/// - `L_i(j) = 1` if `i == j`, else `0` (Kronecker delta) -/// - `L_0(r) + L_1(r) + L_2(r) = 1` (partition of unity) -/// -/// # Arguments -/// -/// * `r` - The evaluation point -/// -/// # Returns -/// -/// Array of three field elements `[L_0(r), L_1(r), L_2(r)]`. -fn lagrange_weights_012(r: F) -> [F; 3] { - let r_minus_one = r - F::ONE; - - // L_0(r) = (r - 1)(r - 2) / 2 - let l0 = (r_minus_one * (r - F::TWO)).halve(); - - // L_1(r) = r(2 - r) - // - // Derived from: -r(r - 2) = r(2 - r) - let l1 = r * (F::TWO - r); - - // L_2(r) = r(r - 1) / 2 - let l2 = (r * r_minus_one).halve(); - - [l0, l1, l2] -} - -/// Computes tensor product Lagrange weights for multivariate interpolation. -/// -/// Given a vector of evaluation points `rs = [r_0, r_1, ..., r_{k-1}]`, computes -/// the `3^k` Lagrange weights for interpolating over the grid `{0, 1, 2}^k`. -/// -/// # Mathematical Background -/// -/// For multivariate interpolation, the basis functions are tensor products: -/// -/// ```text -/// L_{(i_0, i_1, ..., i_{k-1})}(r_0, r_1, ..., r_{k-1}) = ∏_{j=0}^{k-1} L_{i_j}(r_j) -/// ``` -/// -/// where each `i_j ∈ {0, 1, 2}` and `L_{i_j}` is the univariate Lagrange basis. -/// -/// # Output Ordering -/// -/// The weights are ordered lexicographically by the multi-index `(i_0, i_1, ..., i_{k-1})`: -/// - Index 0: weight for point `(0, 0, ..., 0)` -/// - Index 1: weight for point `(0, 0, ..., 1)` -/// - Index 2: weight for point `(0, 0, ..., 2)` -/// - ... -/// - Index `3^k - 1`: weight for point `(2, 2, ..., 2)` -/// -/// # Arguments -/// -/// * `rs` - Slice of evaluation points `[r_0, r_1, ..., r_{k-1}]` -/// -/// # Returns -/// -/// Vector of `3^k` field elements representing the tensor product Lagrange weights. -pub fn lagrange_weights_012_multi(rs: &[F]) -> Vec { - let total = 3usize.pow(rs.len() as u32); - let mut current = Vec::with_capacity(total); - let mut next = Vec::with_capacity(total); - current.push(F::ONE); - - // Iteratively compute tensor products. - // - // After processing r_j, we have 3^(j+1) weights for the grid {0,1,2}^(j+1). - for &r in rs { - // Compute univariate Lagrange weights for this coordinate. - let uni = lagrange_weights_012(r); - - // Tensor product: new_weights[3*i + j] = old_weights[i] * uni[j] - next.clear(); - for &li in &uni { - for &w in ¤t { - next.push(w * li); - } - } - core::mem::swap(&mut current, &mut next); - } - - current -} - -/// Evaluates a quadratic polynomial at point `r` given its evaluations at `{0, 1, 2}`. -/// -/// Uses Lagrange interpolation to compute `h(r)` from the values `h(0)`, `h(1)`, `h(2)`. -/// -/// # Mathematical Formula -/// -/// ```text -/// h(r) = h(0) * L_0(r) + h(1) * L_1(r) + h(2) * L_2(r) -/// ``` -/// -/// where `L_i` are the Lagrange basis polynomials computed by [`lagrange_weights_012`]. -/// -/// # Arguments -/// -/// * `e0` - The value `h(0)` (evaluation at 0) -/// * `e1` - The value `h(1)` (evaluation at 1) -/// * `e2` - The value `h(2)` (evaluation at 2) -/// * `r` - The point at which to evaluate the polynomial -/// -/// # Returns -/// -/// The value `h(r)`. -pub fn extrapolate_012(e0: F, e1: F, e2: F, r: F) -> F { - // Compute Lagrange basis weights at point r. - let [w0, w1, w2] = lagrange_weights_012(r); - - // Evaluate via linear combination. - e0 * w0 + e1 * w1 + e2 * w2 -} - -#[cfg(test)] -mod tests { - use p3_baby_bear::BabyBear; - use p3_field::PrimeCharacteristicRing; - use proptest::prelude::*; - - use super::*; - - type F = BabyBear; - - #[test] - fn test_lagrange_weights_at_interpolation_points() { - // At x = 0: L_0(0) = 1, L_1(0) = 0, L_2(0) = 0 - let [l0, l1, l2] = lagrange_weights_012(F::ZERO); - assert_eq!(l0, F::ONE); - assert_eq!(l1, F::ZERO); - assert_eq!(l2, F::ZERO); - - // At x = 1: L_0(1) = 0, L_1(1) = 1, L_2(1) = 0 - let [l0, l1, l2] = lagrange_weights_012(F::ONE); - assert_eq!(l0, F::ZERO); - assert_eq!(l1, F::ONE); - assert_eq!(l2, F::ZERO); - - // At x = 2: L_0(2) = 0, L_1(2) = 0, L_2(2) = 1 - let [l0, l1, l2] = lagrange_weights_012(F::TWO); - assert_eq!(l0, F::ZERO); - assert_eq!(l1, F::ZERO); - assert_eq!(l2, F::ONE); - } - - #[test] - fn test_lagrange_weights_partition_of_unity() { - // The Lagrange basis polynomials should sum to 1 at any point. - for i in 0..10 { - let r = F::from_u64(i); - let [l0, l1, l2] = lagrange_weights_012(r); - assert_eq!(l0 + l1 + l2, F::ONE, "Partition of unity failed at r = {i}"); - } - } - - #[test] - fn test_lagrange_weights_multi_k1() { - // For k=1, multi should match the single-variable version. - for i in 0..5 { - let r = F::from_u64(i); - let single = lagrange_weights_012(r); - let multi = lagrange_weights_012_multi(&[r]); - - assert_eq!(multi.len(), 3); - assert_eq!(multi[0], single[0]); - assert_eq!(multi[1], single[1]); - assert_eq!(multi[2], single[2]); - } - } - - #[test] - fn test_lagrange_weights_multi_k2() { - // For k=2, we get 9 weights. - let r0 = F::from_u64(5); - let r1 = F::from_u64(7); - - let weights = lagrange_weights_012_multi(&[r0, r1]); - assert_eq!(weights.len(), 9); - - // Verify tensor product structure. - // - // The implementation iterates: for each new coordinate's weight L_j(r1), - // multiply by all existing weights. This gives ordering: - // weights[3*j + i] = L_i(r0) * L_j(r1) - let w0 = lagrange_weights_012(r0); - let w1 = lagrange_weights_012(r1); - - for i in 0..3 { - for j in 0..3 { - assert_eq!(weights[3 * j + i], w0[i] * w1[j]); - } - } - } - - #[test] - fn test_lagrange_weights_multi_partition_of_unity() { - // Tensor product weights should sum to 1. - let r0 = F::from_u64(3); - let r1 = F::from_u64(7); - let r2 = F::from_u64(11); - - // k=1 - let w1: F = lagrange_weights_012_multi(&[r0]).into_iter().sum(); - assert_eq!(w1, F::ONE); - - // k=2 - let w2: F = lagrange_weights_012_multi(&[r0, r1]).into_iter().sum(); - assert_eq!(w2, F::ONE); - - // k=3 - let w3: F = lagrange_weights_012_multi(&[r0, r1, r2]).into_iter().sum(); - assert_eq!(w3, F::ONE); - } - - #[test] - fn test_extrapolate_at_interpolation_points() { - // Extrapolating at the interpolation points should return the original values. - let e0 = F::from_u64(7); - let e1 = F::from_u64(13); - let e2 = F::from_u64(23); - - assert_eq!(extrapolate_012(e0, e1, e2, F::ZERO), e0); - assert_eq!(extrapolate_012(e0, e1, e2, F::ONE), e1); - assert_eq!(extrapolate_012(e0, e1, e2, F::TWO), e2); - } - - #[test] - fn test_extrapolate_known_quadratic() { - // Test with h(x) = x^2 + 1 - // - // h(0) = 1, h(1) = 2, h(2) = 5 - let e0 = F::from_u64(1); - let e1 = F::from_u64(2); - let e2 = F::from_u64(5); - - // h(3) = 9 + 1 = 10 - assert_eq!(extrapolate_012(e0, e1, e2, F::from_u64(3)), F::from_u64(10)); - - // h(4) = 16 + 1 = 17 - assert_eq!(extrapolate_012(e0, e1, e2, F::from_u64(4)), F::from_u64(17)); - } - - proptest! { - /// Property: Extrapolating at interpolation points returns original values. - #[test] - fn prop_extrapolate_identity( - e0 in 0u32..1_000_000, - e1 in 0u32..1_000_000, - e2 in 0u32..1_000_000, - ) { - let e0 = F::from_u32(e0); - let e1 = F::from_u32(e1); - let e2 = F::from_u32(e2); - - prop_assert_eq!(extrapolate_012(e0, e1, e2, F::ZERO), e0); - prop_assert_eq!(extrapolate_012(e0, e1, e2, F::ONE), e1); - prop_assert_eq!(extrapolate_012(e0, e1, e2, F::TWO), e2); - } - - /// Property: Lagrange weights sum to 1 (partition of unity). - #[test] - fn prop_partition_of_unity(r in 0u32..1_000_000) { - let r = F::from_u32(r); - let [l0, l1, l2] = lagrange_weights_012(r); - prop_assert_eq!(l0 + l1 + l2, F::ONE); - } - - /// Property: Tensor product weights sum to 1. - #[test] - fn prop_multi_partition_of_unity( - r0 in 0u32..1_000_000, - r1 in 0u32..1_000_000, - ) { - let r0 = F::from_u32(r0); - let r1 = F::from_u32(r1); - - let sum: F = lagrange_weights_012_multi(&[r0, r1]).into_iter().sum(); - prop_assert_eq!(sum, F::ONE); - } - - /// Property: Extrapolation matches direct polynomial evaluation. - /// - /// Given evaluations at {0, 1, 2}, we can recover the unique quadratic h(x) - /// in monomial form: h(x) = c0 + c1*x + c2*x^2. - /// - /// The coefficients can be computed from evaluations: - /// - c0 = h(0) - /// - c2 = (h(0) - 2*h(1) + h(2)) / 2 - /// - c1 = h(1) - h(0) - c2 - #[test] - fn prop_extrapolate_matches_monomial( - e0 in 0u32..1_000_000, - e1 in 0u32..1_000_000, - e2 in 0u32..1_000_000, - r in 0u32..1_000_000, - ) { - let e0 = F::from_u32(e0); - let e1 = F::from_u32(e1); - let e2 = F::from_u32(e2); - let r = F::from_u32(r); - - // Compute monomial coefficients. - let c0 = e0; - let c2 = (e0 - e1.double() + e2) * F::TWO.inverse(); - let c1 = e1 - e0 - c2; - - // Evaluate using Horner's method: c0 + r*(c1 + r*c2) - let monomial_eval = c0 + r * (c1 + r * c2); - - // Evaluate using Lagrange extrapolation. - let lagrange_eval = extrapolate_012(e0, e1, e2, r); - - prop_assert_eq!(lagrange_eval, monomial_eval); - } - } -} diff --git a/src/sumcheck/mod.rs b/src/sumcheck/mod.rs deleted file mode 100644 index 46c0dca4..00000000 --- a/src/sumcheck/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Sumcheck protocol implementation for multilinear polynomial verification. -//! -//! # The Sumcheck Protocol -//! -//! The sumcheck protocol verifies a claimed sum of the form: -//! -//! ```text -//! sum_{x in {0,1}^l} g(x) = t -//! ``` -//! -//! Without it, the verifier would need to evaluate `g` at all `2^l` hypercube points. -//! The protocol reduces this to a single evaluation at a random point, using `l` rounds. -//! -//! In each round, the prover sends a low-degree univariate polynomial: -//! -//! ```text -//! h_i(X) = sum_{x_{i+1}, ..., x_l in {0,1}} g(r_1, ..., r_{i-1}, X, x_{i+1}, ..., x_l) -//! ``` -//! -//! The verifier checks `h_i(0) + h_i(1) == claimed_sum`. -//! Then samples a random challenge `r_i` and updates the claim to `h_i(r_i)`. -//! -//! After `l` rounds, the verifier holds `(r_1, ..., r_l)` and queries `g` directly. - -pub mod data; -pub mod error; -pub mod lagrange; -pub mod product_polynomial; -pub mod prover; -pub mod svo; - -#[cfg(test)] -mod tests; - -pub use data::{SumcheckData, verify_final_sumcheck_rounds}; -pub use error::SumcheckError; -pub(crate) use lagrange::extrapolate_012; diff --git a/src/sumcheck/product_polynomial.rs b/src/sumcheck/product_polynomial.rs deleted file mode 100644 index 49dc4de5..00000000 --- a/src/sumcheck/product_polynomial.rs +++ /dev/null @@ -1,1072 +0,0 @@ -//! SIMD-aware polynomial pair for quadratic sumcheck. -//! -//! This module implements a data structure that manages two multilinear polynomials. -//! -//! Evaluations and weights—whose pointwise product is required for the sumcheck protocol. -//! -//! # Mathematical Background -//! -//! In the sumcheck protocol, we prove knowledge of a claimed sum: -//! -//! ```text -//! S = \sum_{x \in \{0,1\}^n} f(x) \cdot w(x) -//! ``` -//! -//! where: -//! - `f(x)` is the multilinear polynomial being sumchecked (evaluations). -//! - `w(x)` is the weight polynomial, typically derived from equality constraints. -//! -//! At each round, we compute a univariate polynomial `h(X)` that represents the partial sum -//! over remaining variables. For quadratic sumcheck, `h(X)` is degree-2. - -use p3_challenger::{FieldChallenger, GrindingChallenger}; -use p3_field::{ExtensionField, Field, PackedFieldExtension, PackedValue, dot_product}; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; -use p3_util::log2_strict_usize; -use tracing::instrument; - -use crate::{ - constraints::Constraint, - sumcheck::{SumcheckData, extrapolate_012}, -}; - -/// A paired representation of evaluation and weight polynomials for quadratic sumcheck. -/// -/// This enum stores two multilinear polynomials: -/// - `evals` (the polynomial being sumchecked) -/// - `weights` (the constraint weights), -/// -/// in either SIMD-packed or scalar format, depending on the polynomial size. -/// -/// # Memory Layout -/// -/// In packed mode, evaluations are organized as: -/// -/// ```text -/// Logical view: [f(0,0,...,0), f(0,0,...,1), ..., f(1,1,...,1)] -/// \---------------- 2^n elements ---------------/ -/// -/// Packed view: [Pack_0, Pack_1, ..., Pack_{2^n/W - 1}] -/// \--2^{n - log_2(W)} packed elements--/ -/// -/// where W = SIMD_WIDTH and each Pack_i contains W consecutive field elements. -/// ``` -/// -/// # Variants -/// -/// - Packed: Uses SIMD-packed extension field elements for large polynomials. -/// Each packed element contains `F::Packing::WIDTH` consecutive evaluations, enabling -/// parallel arithmetic across SIMD lanes. -/// -/// - Small: Uses scalar extension field elements for small polynomials -/// where SIMD overhead would exceed the benefit. -/// -/// # Transition Logic -/// -/// The representation transitions from `Packed` to `Small` when: -/// -/// ```text -/// num_variables <= log_2(SIMD_WIDTH) -/// ``` -/// -/// This occurs after sufficient rounds of folding reduce the polynomial size below the -/// SIMD efficiency threshold. -#[derive(Debug, Clone)] -pub(crate) enum ProductPolynomial> { - /// SIMD-packed representation for large polynomials. - /// - /// Each element in `evals` and `weights` is an `EF::ExtensionPacking`, which holds - /// `F::Packing::WIDTH` extension field elements packed into SIMD lanes. - /// - /// # Memory Efficiency - /// - /// For a polynomial with `2^n` evaluations and SIMD width `W`: - /// - Stored elements: `2^{n - log_2(W)}` - /// - Memory per element: `sizeof(EF) * W` - /// - Total memory: `2^n * sizeof(EF)` (same as scalar, but with SIMD alignment) - Packed { - /// Packed evaluations of the polynomial `f(x)` being sumchecked. - /// - /// Layout: `evals[i]` contains logical evaluations at indices `[i*W, (i+1)*W)`. - evals: EvaluationsList, - - /// Packed evaluations of the weight polynomial `w(x)`. - /// - /// Derived from equality constraints and challenge batching. - weights: EvaluationsList, - }, - - /// Scalar representation for small polynomials. - /// - /// Each element in `evals` and `weights` is a single extension field element. - /// - /// Used when the polynomial is too small for SIMD packing to be beneficial. - Small { - /// Scalar evaluations of the polynomial `f(x)` being sumchecked. - evals: EvaluationsList, - - /// Scalar evaluations of the weight polynomial `w(x)`. - weights: EvaluationsList, - }, -} - -impl> ProductPolynomial { - /// Creates a packed variant and checks for immediate transition. - /// - /// This constructor is used when we know the data is already in packed format. - /// It performs a transition check in case the packed data has been folded down - /// to a single packed element. - /// - /// # Arguments - /// - /// * `evals` - Packed evaluations of `f(x)`. - /// * `weights` - Packed evaluations of `w(x)`. - pub(crate) fn new_packed( - evals: EvaluationsList, - weights: EvaluationsList, - ) -> Self { - assert_eq!(evals.num_variables(), weights.num_variables()); - let mut poly = Self::Packed { evals, weights }; - - // Check if we should immediately transition to scalar mode. - // This handles edge cases where the input is already small. - poly.transition(); - poly - } - - /// Creates a scalar variant. - /// - /// Used when the polynomial is too small for SIMD packing. - /// - /// # Arguments - /// - /// * `evals` - Scalar evaluations of `f(x)`. - /// * `weights` - Scalar evaluations of `w(x)`. - pub(crate) const fn new_small( - evals: EvaluationsList, - weights: EvaluationsList, - ) -> Self { - Self::Small { evals, weights } - } - - /// Returns the number of variables in the multilinear polynomials. - /// - /// This is the logical number of variables, accounting for SIMD packing. - /// - /// # Computation - /// - /// - **Packed**: `stored_variables + log_2(SIMD_WIDTH)` - /// - **Small**: `stored_variables` - pub(crate) fn num_variables(&self) -> usize { - match self { - Self::Packed { evals, weights } => { - // Get the number of variables in the packed representation. - let k = evals.num_variables(); - assert_eq!(k, weights.num_variables()); - - // Add back the variables absorbed by SIMD packing. - k + log2_strict_usize(F::Packing::WIDTH) - } - Self::Small { evals, weights } => { - let k = evals.num_variables(); - assert_eq!(k, weights.num_variables()); - k - } - } - } - - /// Evaluates the polynomial `f(x)` at a given multilinear point. - /// - /// This computes `f(point)` where `point \in EF^n`. - /// - /// # Arguments - /// - /// * `point` - The evaluation point as a [`MultilinearPoint`]. - pub(crate) fn eval(&self, point: &MultilinearPoint) -> EF { - match self { - Self::Packed { evals, .. } => evals.evaluate_hypercube_packed(point), - Self::Small { evals, .. } => evals.evaluate_hypercube_ext(point), - } - } - - /// Folds both polynomials by binding the first variable to a challenge. - /// - /// This is the core operation of each sumcheck round. After receiving a challenge `r`, - /// we reduce the polynomial from `n` variables to `n-1` variables by setting `X_1 = r`. - /// - /// # Mathematical Operation - /// - /// For a multilinear polynomial `p(X_1, X_2, ..., X_n)`: - /// - /// ```text - /// p'(X_2, ..., X_n) = p(r, X_2, ..., X_n) - /// = p(0, X_2, ..., X_n) + r * (p(1, X_2, ..., X_n) - p(0, X_2, ..., X_n)) - /// ``` - /// - /// This linear interpolation is applied independently to both `evals` and `weights`. - /// - /// # Arguments - /// - /// * `r` - The verifier's challenge for this round. - fn compress(&mut self, r: EF) { - match self { - // Apply folding to both packed polynomials. - // - // The compress operation handles SIMD lanes correctly. - Self::Packed { evals, weights } => { - evals.compress(r); - weights.compress(r); - } - // Apply folding to both scalar polynomials. - Self::Small { evals, weights } => { - evals.compress(r); - weights.compress(r); - } - } - } - - /// Transitions from packed to scalar mode if the polynomial is small enough. - /// - /// This is called after each fold operation to check if we should switch representations. - /// The transition occurs when the packed representation has only a single element - /// (i.e., `num_variables() == 0` in the packed view). - /// - /// # Transition Condition - /// - /// ```text - /// if packed_num_variables == 0: - /// -> Unpack to scalar and switch to Small variant - /// ``` - /// - /// # Why Transition? - /// - /// When only one packed element remains, SIMD operations become pure overhead: - /// - No parallelism benefit (only one "lane group" of work) - /// - Extra unpacking/repacking costs per operation - /// - /// Scalar mode eliminates this overhead for the final rounds. - fn transition(&mut self) { - if let Self::Packed { evals, weights } = self { - // Check if we've folded down to a single packed element. - let k = evals.num_variables(); - assert_eq!(k, weights.num_variables()); - - if k == 0 { - // Unpack the single packed element into SIMD_WIDTH scalar elements. - // - // Extract individual extension field elements from the packed representation. - let evals = - EF::ExtensionPacking::to_ext_iter(evals.as_slice().iter().copied()).collect(); - let weights = - EF::ExtensionPacking::to_ext_iter(weights.as_slice().iter().copied()).collect(); - - // Replace self with the scalar variant. - *self = Self::Small { - evals: EvaluationsList::new(evals), - weights: EvaluationsList::new(weights), - }; - } - } - } - - /// Executes one round of the quadratic sumcheck protocol. - /// - /// This is the main method that: - /// 1. Computes the sumcheck polynomial coefficients `(c_0, c_2)`. - /// 2. Commits them to the Fiat-Shamir transcript. - /// 3. Receives a challenge from the verifier. - /// 4. Folds both polynomials using the challenge. - /// 5. Updates the running sum. - /// - /// # Sumcheck Polynomial - /// - /// At each round, we send a univariate quadratic polynomial: - /// - /// ```text - /// h(X) = c_0 + c_1 * X + c_2 * X^2 - /// ``` - /// - /// where: - /// - `c_0 = h(0)` = sum of products where first variable is 0 - /// - `c_1` = derived from the constraint `h(0) + h(1) = claimed_sum` - /// - `c_2` = quadratic coefficient from cross-terms - /// - /// We only send `(c_0, c_2)` since `c_1` is derivable by the verifier. - /// - /// # Arguments - /// - /// * `sumcheck_data` - Storage for polynomial evaluations sent to verifier. - /// * `challenger` - Fiat-Shamir challenger for transcript operations. - /// * `sum` - Current claimed sum (updated after this round). - /// * `pow_bits` - Proof-of-work difficulty (0 to disable). - /// - /// # Returns - /// - /// The verifier's challenge `r \in EF` for this round. - #[instrument(skip_all)] - pub(crate) fn round( - &mut self, - sumcheck_data: &mut SumcheckData, - challenger: &mut Challenger, - sum: &mut EF, - pow_bits: usize, - ) -> EF - where - Challenger: FieldChallenger + GrindingChallenger, - { - // Step 1: Compute sumcheck polynomial coefficients. - // - // The strategy differs based on representation to maximize SIMD utilization. - let (c0, c2) = match self { - Self::Packed { evals, weights } => { - // Compute coefficients using packed arithmetic. - // Each operation processes SIMD_WIDTH elements in parallel. - let (c0, c2) = evals.sumcheck_coefficients(weights); - - // Horizontal reduction: sum across all SIMD lanes to get scalar result. - // - // The packed computation gives us one result per lane. - // We need the sum across all lanes as the final coefficient. - ( - EF::ExtensionPacking::to_ext_iter([c0]).sum(), - EF::ExtensionPacking::to_ext_iter([c2]).sum(), - ) - } - Self::Small { evals, weights } => { - // Compute coefficients directly on scalar elements. - evals.sumcheck_coefficients(weights) - } - }; - - // Step 2-4: Commit to transcript, do PoW, and receive challenge. - let r = sumcheck_data.observe_and_sample(challenger, c0, c2, pow_bits); - - // Step 5: Fold both polynomials using the challenge. - self.compress(r); - - // Step 6: Update the claimed sum using the quadratic formula. - // - // Recall: h(X) = c_0 + c_1 * X + c_2 * X^2 - // - // Update sum := h(r) - *sum = extrapolate_012(c0, *sum - c0, c2, r); - - // Sanity check: the updated sum should equal the inner product after folding. - debug_assert_eq!(*sum, self.dot_product()); - - // Step 7: Check if we should transition to scalar mode. - // - // After folding, the polynomial may be small enough that scalar operations - // are more efficient than packed operations. - self.transition(); - - r - } - - /// Extracts the evaluation polynomial as a scalar [`EvaluationsList`]. - /// - /// This unpacks the evaluations if in packed mode. - /// - /// # Returns - /// - /// A copy of the evaluations in scalar extension field format. - pub(crate) fn evals(&self) -> EvaluationsList { - match self { - Self::Packed { evals, .. } => EvaluationsList::new( - EF::ExtensionPacking::to_ext_iter(evals.as_slice().iter().copied()).collect(), - ), - Self::Small { evals, .. } => evals.clone(), - } - } - - /// Incorporates new constraints into the weight polynomial. - /// - /// This is used when additional constraints need to be folded into the sumcheck - /// after initial construction (e.g., from STIR challenges). - /// - /// # Arguments - /// - /// * `sum` - Running sum to update with new constraint contributions. - /// * `constraint` - The constraint to combine into weights. - pub(crate) fn combine(&mut self, sum: &mut EF, constraint: &Constraint) { - match self { - Self::Packed { weights, .. } => { - constraint.combine_packed(weights, sum); - } - Self::Small { weights, .. } => { - constraint.combine(weights, sum); - } - } - } - - /// Computes the dot product of evaluations and weights. - /// - /// This computes: - /// - /// ```text - /// \sum_{x \in \{0,1\}^n} evals(x) * weights(x) - /// ``` - /// - /// which should equal the current claimed sum at any point in the protocol. - /// - /// # Returns - /// - /// The dot product of `evals` and `weights`. - pub(crate) fn dot_product(&self) -> EF { - match self { - Self::Packed { evals, weights } => { - // Compute packed dot product (SIMD parallel multiply-accumulate). - let sum_packed = dot_product(evals.iter().copied(), weights.iter().copied()); - - // Horizontal sum to reduce packed result to scalar. - EF::ExtensionPacking::to_ext_iter([sum_packed]).sum() - } - Self::Small { evals, weights } => { - // Direct scalar dot product. - dot_product(evals.iter().copied(), weights.iter().copied()) - } - } - } -} - -#[cfg(test)] -mod tests { - use alloc::{vec, vec::Vec}; - - use p3_baby_bear::{BabyBear, Poseidon2BabyBear}; - use p3_challenger::DuplexChallenger; - use p3_field::{Field, PrimeCharacteristicRing, extension::BinomialExtensionField}; - use proptest::prelude::*; - use rand::{RngExt, SeedableRng, rngs::SmallRng}; - - use super::*; - - type F = BabyBear; - type EF = BinomialExtensionField; - type Perm = Poseidon2BabyBear<16>; - type TestChallenger = DuplexChallenger; - - /// Creates a test challenger with a deterministic seed. - fn make_challenger() -> TestChallenger { - let perm = Perm::new_from_rng_128(&mut SmallRng::seed_from_u64(42)); - DuplexChallenger::new(perm) - } - - #[test] - fn test_num_variables_small_variant() { - // Create a Small variant with 3 variables (8 evaluations). - let evals = EvaluationsList::new(vec![EF::ONE; 8]); - let weights = EvaluationsList::new(vec![EF::TWO; 8]); - - // Force Small variant by using new_small directly. - let poly = ProductPolynomial::::new_small(evals, weights); - - // The logical number of variables should be 3 (since 2^3 = 8). - assert_eq!(poly.num_variables(), 3); - } - - #[test] - fn test_dot_product_manual_calculation() { - // Create Small variant with known values and verify dot product. - // - // dot_product = Σ_i evals[i] * weights[i] - // = e0*w0 + e1*w1 + e2*w2 + e3*w3 - let e0 = EF::from_u64(1); - let e1 = EF::from_u64(2); - let e2 = EF::from_u64(3); - let e3 = EF::from_u64(4); - let w0 = EF::from_u64(5); - let w1 = EF::from_u64(6); - let w2 = EF::from_u64(7); - let w3 = EF::from_u64(8); - - let evals = EvaluationsList::new(vec![e0, e1, e2, e3]); - let weights = EvaluationsList::new(vec![w0, w1, w2, w3]); - - let poly = ProductPolynomial::::new_small(evals, weights); - - // dot_product = e0*w0 + e1*w1 + e2*w2 + e3*w3 - let expected = e0 * w0 + e1 * w1 + e2 * w2 + e3 * w3; - assert_eq!(poly.dot_product(), expected); - } - - #[test] - fn test_sumcheck_coefficients_manual_calculation() { - // Test the sumcheck coefficient computation with manual verification. - // - // For a 1-variable polynomial (2 evaluations): - // evals = [e0, e1] where f(0) = e0, f(1) = e1 - // weights = [w0, w1] where g(0) = w0, g(1) = w1 - // - // sumcheck_coefficients returns (h(0), h(2)) where: - // h(0) = f(0) * g(0) = e0 * w0 - // h(2) = f(2) * g(2) = (2*e1 - e0) * (2*w1 - w0) - let e0 = EF::from_u64(3); - let e1 = EF::from_u64(7); - let w0 = EF::from_u64(2); - let w1 = EF::from_u64(5); - - let evals = EvaluationsList::new(vec![e0, e1]); - let weights = EvaluationsList::new(vec![w0, w1]); - - let (h0, h2) = evals.sumcheck_coefficients(&weights); - - // h(0) = e0 * w0 - let expected_h0 = e0 * w0; - assert_eq!(h0, expected_h0); - - // h(2) = (2*e1 - e0) * (2*w1 - w0) - let expected_h2 = (e1.double() - e0) * (w1.double() - w0); - assert_eq!(h2, expected_h2); - - // Verify consistency: h(0) + h(1) should equal the claimed sum. - // h(0) = c0 - // h(1) = e1 * w1 - // sum = e0*w0 + e1*w1 - let h_0 = h0; - let h_1 = e1 * w1; - let sum = e0 * w0 + e1 * w1; - assert_eq!(h_0 + h_1, sum); - } - - #[test] - fn test_compress_manual_calculation() { - // Test the compress (folding) operation with manual verification. - // - // Initial state: 2-variable polynomial (4 evaluations) - // evals = [e0, e1, e2, e3] representing f(x0, x1) - // weights = [w0, w1, w2, w3] representing g(x0, x1) - // - // Memory layout: - // f(0,0) = e0, f(0,1) = e1 (lo half, x0 = 0) - // f(1,0) = e2, f(1,1) = e3 (hi half, x0 = 1) - // - // Folding binds x0 to challenge r: - // f'(x1) = f(0,x1) + r * (f(1,x1) - f(0,x1)) - // - // So: - // e'0 = e0 + r * (e2 - e0) - // e'1 = e1 + r * (e3 - e1) - // w'0 = w0 + r * (w2 - w0) - // w'1 = w1 + r * (w3 - w1) - let e0 = EF::from_u64(1); - let e1 = EF::from_u64(2); - let e2 = EF::from_u64(5); - let e3 = EF::from_u64(8); - let w0 = EF::from_u64(3); - let w1 = EF::from_u64(4); - let w2 = EF::from_u64(6); - let w3 = EF::from_u64(7); - - let evals = EvaluationsList::new(vec![e0, e1, e2, e3]); - let weights = EvaluationsList::new(vec![w0, w1, w2, w3]); - - let mut poly = ProductPolynomial::::new_small(evals, weights); - - // Initial dot product: sum = e0*w0 + e1*w1 + e2*w2 + e3*w3 - let initial_sum = e0 * w0 + e1 * w1 + e2 * w2 + e3 * w3; - assert_eq!(poly.dot_product(), initial_sum); - - // Fold with challenge r. - let r = EF::from_u64(2); - poly.compress(r); - - let folded_evals = poly.evals(); - - // e'0 = e0 + r * (e2 - e0) - // e'1 = e1 + r * (e3 - e1) - let expected_e0 = e0 + r * (e2 - e0); - let expected_e1 = e1 + r * (e3 - e1); - - assert_eq!(folded_evals.as_slice(), &[expected_e0, expected_e1]); - - // After folding, dot_product equals h(r) where h is the sumcheck polynomial: - // h(X) = c0 + c1*X + c2*X^2 - // c0 = e0*w0 + e1*w1 - // c2 = (e2 - e0)*(w2 - w0) + (e3 - e1)*(w3 - w1) - // h(1) = e2*w2 + e3*w3 - // c1 = h(1) - c0 - c2 - // h(r) = c0 + c1*r + c2*r^2 - let c0 = e0 * w0 + e1 * w1; - let c2 = (e2 - e0) * (w2 - w0) + (e3 - e1) * (w3 - w1); - let h_1 = e2 * w2 + e3 * w3; - let c1 = h_1 - c0 - c2; - let h_r = c0 + c1 * r + c2 * r.square(); - - assert_eq!(poly.dot_product(), h_r); - } - - #[test] - fn test_eval_multilinear_interpolation() { - // Test eval() with non-boolean points using multilinear interpolation. - // - // For a 2-variable polynomial f(x0, x1): - // f(x0, x1) = f(0,0)*(1-x0)*(1-x1) + f(0,1)*(1-x0)*x1 - // + f(1,0)*x0*(1-x1) + f(1,1)*x0*x1 - // - // With evals = [e0, e1, e2, e3]: - // f(0,0) = e0, f(0,1) = e1, f(1,0) = e2, f(1,1) = e3 - let e0 = EF::from_u64(2); - let e1 = EF::from_u64(5); - let e2 = EF::from_u64(3); - let e3 = EF::from_u64(11); - - let evals = EvaluationsList::new(vec![e0, e1, e2, e3]); - let weights = EvaluationsList::new(vec![EF::ONE; 4]); - - let poly = ProductPolynomial::::new_small(evals, weights); - - // Evaluate at (x0, x1): - // f(x0, x1) = e0*(1-x0)*(1-x1) + e1*(1-x0)*x1 + e2*x0*(1-x1) + e3*x0*x1 - let x0 = EF::from_u64(3); - let x1 = EF::from_u64(4); - let point = MultilinearPoint::new(vec![x0, x1]); - - let one = EF::ONE; - let expected = e0 * (one - x0) * (one - x1) - + e1 * (one - x0) * x1 - + e2 * x0 * (one - x1) - + e3 * x0 * x1; - - assert_eq!(poly.eval(&point), expected); - } - - #[test] - fn test_transition_from_packed_to_small() { - // Create a Packed variant that will transition to Small after sufficient folding. - // - // The SIMD threshold is log_2(F::Packing::WIDTH). - // We need a polynomial large enough to start in Packed mode. - type EP = >::ExtensionPacking; - - let simd_width = ::Packing::WIDTH; - let simd_log = log2_strict_usize(simd_width); - - // Start with simd_log + 2 variables (e.g., if simd_width=16, start with 6 vars = 64 evals). - // This gives us 4 packed elements initially (2 stored variables). - let num_vars = simd_log + 2; - let num_evals = 1 << num_vars; - - // Create scalar evaluations and pack them - let evals_scalar = vec![EF::ONE; num_evals]; - let weights_scalar = vec![EF::ONE; num_evals]; - - let packed_evals = EvaluationsList::new( - evals_scalar - .chunks(simd_width) - .map(EP::from_ext_slice) - .collect(), - ); - let packed_weights = EvaluationsList::new( - weights_scalar - .chunks(simd_width) - .map(EP::from_ext_slice) - .collect(), - ); - - let mut poly = ProductPolynomial::::new_packed(packed_evals, packed_weights); - - // Initially should be Packed with correct internal structure. - match &poly { - ProductPolynomial::Packed { - evals: packed_evals, - weights: packed_weights, - } => { - // Should have num_evals / simd_width = 4 packed elements. - let expected_packed_len = num_evals / simd_width; - assert_eq!(packed_evals.num_evals(), expected_packed_len); - assert_eq!(packed_weights.num_evals(), expected_packed_len); - } - ProductPolynomial::Small { .. } => { - panic!("Expected Packed variant initially"); - } - } - assert_eq!(poly.num_variables(), num_vars); - - // Fold twice to reduce to simd_log variables (threshold for transition). - for _ in 0..2 { - let challenge = EF::from_u64(7); - poly.compress(challenge); - poly.transition(); - } - - // After two folds: simd_log variables, which triggers transition to Small. - match &poly { - ProductPolynomial::Small { evals, weights } => { - // Should have 2^simd_log = simd_width scalar elements. - assert_eq!(evals.num_evals(), simd_width); - assert_eq!(weights.num_evals(), simd_width); - } - ProductPolynomial::Packed { .. } => { - panic!("Expected Small variant after transition"); - } - } - assert_eq!(poly.num_variables(), simd_log); - } - - #[test] - fn test_new_packed_with_single_element_transitions() { - // If we create a Packed variant with just 1 packed element (0 stored variables), - // it should immediately transition to Small. - // - // This happens when packed evals has exactly 1 element. - type EP = >::ExtensionPacking; - - // Get the actual SIMD width to create properly sized arrays. - let simd_width = ::Packing::WIDTH; - - // Create a single packed element containing simd_width extension field elements. - let evals_scalar: Vec = (0..simd_width).map(|i| EF::from_u64(i as u64)).collect(); - let weights_scalar: Vec = (0..simd_width) - .map(|i| EF::from_u64(100 + i as u64)) - .collect(); - - let evals = EvaluationsList::new(vec![EP::from_ext_slice(&evals_scalar)]); - let weights = EvaluationsList::new(vec![EP::from_ext_slice(&weights_scalar)]); - - let poly = ProductPolynomial::::new_packed(evals, weights); - - // Should have transitioned to Small with correct values. - match &poly { - ProductPolynomial::Small { - evals: small_evals, - weights: small_weights, - } => { - // Verify the unpacked values match the original scalars. - assert_eq!(small_evals.as_slice(), &evals_scalar); - assert_eq!(small_weights.as_slice(), &weights_scalar); - } - ProductPolynomial::Packed { .. } => { - panic!("Expected Small variant after transition from single packed element"); - } - } - - // Should have log_2(simd_width) variables. - assert_eq!(poly.num_variables(), log2_strict_usize(simd_width)); - } - - #[test] - fn test_round_updates_sum_correctly() { - // Test the round() function which is the core sumcheck protocol. - // - // The round function should: - // 1. Compute sumcheck coefficients (c0, c2) - // 2. Update the claimed sum to h(r) where r is the challenge - // 3. Fold both polynomials - // 4. Return the challenge r - // - // Verify: after round(), dot_product() == updated sum - let e0 = EF::from_u64(2); - let e1 = EF::from_u64(5); - let e2 = EF::from_u64(3); - let e3 = EF::from_u64(7); - let w0 = EF::from_u64(1); - let w1 = EF::from_u64(4); - let w2 = EF::from_u64(2); - let w3 = EF::from_u64(6); - - let evals = EvaluationsList::new(vec![e0, e1, e2, e3]); - let weights = EvaluationsList::new(vec![w0, w1, w2, w3]); - - let mut poly = ProductPolynomial::::new_small(evals, weights); - - // Initial sum = e0*w0 + e1*w1 + e2*w2 + e3*w3 - let mut sum = e0 * w0 + e1 * w1 + e2 * w2 + e3 * w3; - assert_eq!(poly.dot_product(), sum); - - // Perform one round of sumcheck. - let mut sumcheck_data = SumcheckData::default(); - let mut challenger = make_challenger(); - - let _r = poly.round(&mut sumcheck_data, &mut challenger, &mut sum, 0); - - // After round: - // 1. sum should be updated to h(r) - // 2. dot_product should equal the updated sum - assert_eq!(poly.dot_product(), sum); - - // Verify sumcheck_data was populated with polynomial evaluations. - assert!(!sumcheck_data.polynomial_evaluations.is_empty()); - } - - #[test] - fn test_round_multiple_rounds() { - // Test multiple rounds of sumcheck to verify protocol consistency. - // - // After each round: - // - Number of variables decreases by 1 - // - dot_product() == sum - let mut rng = SmallRng::seed_from_u64(123); - let num_vars = 4; - let num_evals = 1 << num_vars; - - let evals: Vec = (0..num_evals).map(|_| EF::from_u64(rng.random())).collect(); - let weights: Vec = (0..num_evals).map(|_| EF::from_u64(rng.random())).collect(); - - let mut poly = ProductPolynomial::::new_small( - EvaluationsList::new(evals), - EvaluationsList::new(weights), - ); - - let mut sum = poly.dot_product(); - let mut sumcheck_data = SumcheckData::default(); - let mut challenger = make_challenger(); - - // Perform all rounds except the last (need at least 1 evaluation left). - for expected_vars in (1..=num_vars).rev() { - assert_eq!(poly.num_variables(), expected_vars); - - let _ = poly.round(&mut sumcheck_data, &mut challenger, &mut sum, 0); - - // Invariant: dot_product == sum after each round. - assert_eq!(poly.dot_product(), sum); - } - - // After all rounds, should have 0 variables (1 evaluation). - assert_eq!(poly.num_variables(), 0); - } - - #[test] - fn test_dot_product_packed_matches_scalar() { - // Verify that Packed and Small variants compute the same dot product. - type EP = >::ExtensionPacking; - - let simd_width = ::Packing::WIDTH; - let num_vars = log2_strict_usize(simd_width) + 1; - let num_evals = 1 << num_vars; - - let mut rng = SmallRng::seed_from_u64(456); - let evals_scalar: Vec = (0..num_evals).map(|_| EF::from_u64(rng.random())).collect(); - let weights_scalar: Vec = (0..num_evals).map(|_| EF::from_u64(rng.random())).collect(); - - // Compute expected dot product manually. - let expected: EF = evals_scalar - .iter() - .zip(weights_scalar.iter()) - .map(|(&e, &w)| e * w) - .sum(); - - // Create Small variant and verify. - let small_poly = ProductPolynomial::::new_small( - EvaluationsList::new(evals_scalar.clone()), - EvaluationsList::new(weights_scalar.clone()), - ); - assert_eq!(small_poly.dot_product(), expected); - - // Create Packed variant and verify. - let packed_evals = EvaluationsList::new( - evals_scalar - .chunks(simd_width) - .map(EP::from_ext_slice) - .collect(), - ); - let packed_weights = EvaluationsList::new( - weights_scalar - .chunks(simd_width) - .map(EP::from_ext_slice) - .collect(), - ); - - let packed_poly = ProductPolynomial::::new_packed(packed_evals, packed_weights); - assert_eq!(packed_poly.dot_product(), expected); - } - - #[test] - fn test_evals_extraction() { - // Test that evals() returns correct values for both variants. - let e0 = EF::from_u64(10); - let e1 = EF::from_u64(20); - let e2 = EF::from_u64(30); - let e3 = EF::from_u64(40); - - let evals = EvaluationsList::new(vec![e0, e1, e2, e3]); - let weights = EvaluationsList::new(vec![EF::ONE; 4]); - - let poly = ProductPolynomial::::new_small(evals, weights); - - let extracted = poly.evals(); - assert_eq!(extracted.as_slice(), &[e0, e1, e2, e3]); - } - - #[test] - fn test_combine_updates_weights_and_sum() { - // Test that combine() correctly incorporates new constraints. - // - // The combine function should: - // 1. Update the weight polynomial with new constraint contributions - // 2. Update the running sum accordingly - use crate::constraints::{Constraint, statement::EqStatement}; - - let num_vars = 2; - let evals = EvaluationsList::new(vec![EF::ONE; 4]); - let weights = EvaluationsList::new(vec![EF::ONE; 4]); - - let mut poly = ProductPolynomial::::new_small(evals.clone(), weights); - - // Initial state: dot_product = 4 (all ones) - let initial_dot = poly.dot_product(); - assert_eq!(initial_dot, EF::from_u64(4)); - - // Create an EqStatement with one constraint. - let mut eq_statement = EqStatement::initialize(num_vars); - let point = MultilinearPoint::new(vec![EF::from_u64(2), EF::from_u64(3)]); - let eval = evals.evaluate_hypercube_ext::(&point); - eq_statement.add_evaluated_constraint(point, eval); - - // Create constraint with the eq_statement. - let challenge = EF::from_u64(7); - let constraint = Constraint::::new_eq_only(challenge, eq_statement); - - let mut sum = poly.dot_product(); - poly.combine(&mut sum, &constraint); - - // After combining, the weights may have changed. - // The exact behavior depends on the constraint implementation. - // We verify the invariant: dot_product reflects the combined state. - assert_eq!(poly.dot_product(), sum); - } - - #[test] - fn test_eval_at_boolean_points() { - // Test eval() at boolean points (0 and 1 coordinates). - // - // For multilinear polynomial over boolean hypercube, - // eval at boolean point should return the stored evaluation. - let e00 = EF::from_u64(1); - let e01 = EF::from_u64(2); - let e10 = EF::from_u64(3); - let e11 = EF::from_u64(4); - - let evals = EvaluationsList::new(vec![e00, e01, e10, e11]); - let weights = EvaluationsList::new(vec![EF::ONE; 4]); - - let poly = ProductPolynomial::::new_small(evals, weights); - - // Evaluate at (0, 0) -> should return e00 - let point_00 = MultilinearPoint::new(vec![EF::ZERO, EF::ZERO]); - assert_eq!(poly.eval(&point_00), e00); - - // Evaluate at (0, 1) -> should return e01 - let point_01 = MultilinearPoint::new(vec![EF::ZERO, EF::ONE]); - assert_eq!(poly.eval(&point_01), e01); - - // Evaluate at (1, 0) -> should return e10 - let point_10 = MultilinearPoint::new(vec![EF::ONE, EF::ZERO]); - assert_eq!(poly.eval(&point_10), e10); - - // Evaluate at (1, 1) -> should return e11 - let point_11 = MultilinearPoint::new(vec![EF::ONE, EF::ONE]); - assert_eq!(poly.eval(&point_11), e11); - } - - proptest! { - /// Verify that dot_product is consistent across random inputs. - #[test] - fn prop_dot_product_consistency(seed in 0u64..1000) { - let mut rng = SmallRng::seed_from_u64(seed); - let num_vars = 3; - let num_evals = 1 << num_vars; - - let evals: Vec = (0..num_evals) - .map(|_| EF::from_u64(u64::from(rng.random::()))) - .collect(); - let weights: Vec = (0..num_evals) - .map(|_| EF::from_u64(u64::from(rng.random::()))) - .collect(); - - let poly = ProductPolynomial::::new_small( - EvaluationsList::new(evals.clone()), - EvaluationsList::new(weights.clone()), - ); - - // Manual computation - let expected: EF = evals - .iter() - .zip(weights.iter()) - .map(|(&e, &w)| e * w) - .sum(); - - prop_assert_eq!(poly.dot_product(), expected); - } - - /// Verify that compress maintains the sumcheck invariant. - #[test] - fn prop_compress_maintains_invariant(seed in 0u64..1000, challenge_val in 1u64..100) { - let mut rng = SmallRng::seed_from_u64(seed); - let num_vars = 3; - let num_evals = 1 << num_vars; - - let evals: Vec = (0..num_evals) - .map(|_| EF::from_u64(u64::from(rng.random::()))) - .collect(); - let weights: Vec = (0..num_evals) - .map(|_| EF::from_u64(u64::from(rng.random::()))) - .collect(); - - let mut poly = ProductPolynomial::::new_small( - EvaluationsList::new(evals), - EvaluationsList::new(weights), - ); - - // Compute sumcheck coefficients before folding. - // sumcheck_coefficients returns (h(0), h(2)) where h is the univariate - // polynomial h(X) = sum_{b in {0,1}^{n-1}} f(X, b) * w(X, b). - let (c0, c2) = match &poly { - ProductPolynomial::Small { - evals: small_evals, - weights: small_weights, - } => small_evals.sumcheck_coefficients(small_weights), - ProductPolynomial::Packed { .. } => unreachable!(), - }; - - // The sumcheck relation: h(0) + h(1) = claimed_sum - // So h(1) = claimed_sum - h(0) = claimed_sum - c0 - let initial_sum = poly.dot_product(); - let h_1 = initial_sum - c0; - - // Fold with challenge r. - let r = EF::from_u64(challenge_val); - poly.compress(r); - - // Use Lagrange interpolation to compute h(r) from h(0), h(1), h(2). - // h(r) = extrapolate_012(c0, h_1, c2, r) - let h_r = extrapolate_012(c0, h_1, c2, r); - - // After folding, dot_product should equal h(r). - prop_assert_eq!(poly.dot_product(), h_r); - } - - /// Verify that round() maintains the sumcheck invariant. - #[test] - fn prop_round_maintains_invariant(seed in 0u64..1000) { - let mut rng = SmallRng::seed_from_u64(seed); - let num_vars = 4; - let num_evals = 1 << num_vars; - - let evals: Vec = (0..num_evals) - .map(|_| EF::from_u64(u64::from(rng.random::()))) - .collect(); - let weights: Vec = (0..num_evals) - .map(|_| EF::from_u64(u64::from(rng.random::()))) - .collect(); - - let mut poly = ProductPolynomial::::new_small( - EvaluationsList::new(evals), - EvaluationsList::new(weights), - ); - - let mut sum = poly.dot_product(); - let mut sumcheck_data = SumcheckData::default(); - - // Use seed to create challenger for reproducibility. - let perm = Perm::new_from_rng_128(&mut SmallRng::seed_from_u64(seed + 1000)); - let mut challenger: TestChallenger = DuplexChallenger::new(perm); - - // Perform one round. - let _ = poly.round(&mut sumcheck_data, &mut challenger, &mut sum, 0); - - // Invariant: dot_product == sum after round. - prop_assert_eq!(poly.dot_product(), sum); - } - } -} diff --git a/src/sumcheck/prover.rs b/src/sumcheck/prover.rs deleted file mode 100644 index 8312dfc5..00000000 --- a/src/sumcheck/prover.rs +++ /dev/null @@ -1,532 +0,0 @@ -//! Sumcheck prover: constructs and executes the sumcheck protocol for multilinear polynomials. - -use alloc::vec::Vec; - -use p3_challenger::{FieldChallenger, GrindingChallenger}; -use p3_field::{ExtensionField, Field, PackedFieldExtension, PackedValue, dot_product}; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; -use p3_util::log2_strict_usize; - -use crate::{ - constraints::{ - Constraint, - statement::{ - EqStatement, - initial::{InitialStatement, InitialStatementInner}, - }, - }, - sumcheck::{ - SumcheckData, extrapolate_012, lagrange::lagrange_weights_012_multi, - product_polynomial::ProductPolynomial, svo::SplitEq, - }, -}; - -/// Prover state for the sumcheck protocol over a multilinear polynomial. -/// -/// Holds the partially-folded polynomial pair `(f, w)` and the current claimed sum. -/// -/// # Invariant -/// -/// At every point during the protocol: -/// -/// ```text -/// sum == sum_{x in {0,1}^n} f(x) * w(x) -/// ``` -/// -/// where `n` is the number of remaining unbound variables. -/// It decreases by one per round as variables are bound to verifier challenges. -#[derive(Debug, Clone)] -pub struct SumcheckProver> { - /// Paired evaluation and weight polynomials for the quadratic sumcheck. - /// - /// Stores both `f(x)` and `w(x)` in either SIMD-packed or scalar format. - /// The format is chosen automatically based on polynomial size. - pub(crate) poly: ProductPolynomial, - - /// Current claimed sum for the remaining variables. - /// - /// Tracks `sum_{x in {0,1}^n} f(x) * w(x)`. - /// - /// After each round binding variable `X_i` to challenge `r_i`, updated via: - /// - /// ```text - /// sum := h(r_i) where h(X) = c_0 + c_1 * X + c_2 * X^2 - /// ``` - pub(crate) sum: EF, -} - -impl SumcheckProver -where - F: Field + Ord, - EF: ExtensionField, -{ - /// Constructs a sumcheck instance using the classic approach with scalar arithmetic. - /// - /// Fallback path for small polynomials where SIMD packing is not beneficial. - /// - /// # Algorithm - /// - /// 1. Sample a batching challenge `alpha` from the Fiat-Shamir transcript. - /// 2. Build `w(x)` from equality constraints via random linear combination. - /// 3. Compute `(c_0, c_2)` for the first round. - /// 4. Commit to the transcript, perform proof-of-work, receive challenge `r`. - /// 5. Fold both `f` and `w` by binding the first variable to `r`. - /// 6. Run the remaining rounds via the standard one-variable-per-round protocol. - /// - /// # Returns - /// - /// - The partially-folded prover state. - /// - The verifier challenges `(r_1, ..., r_{folding_factor})`. - #[tracing::instrument(skip_all)] - fn new_classic_small( - poly: &EvaluationsList, - sumcheck_data: &mut SumcheckData, - challenger: &mut Challenger, - folding_factor: usize, - pow_bits: usize, - statement: &EqStatement, - ) -> (Self, MultilinearPoint) - where - Challenger: FieldChallenger + GrindingChallenger, - { - // Sample a batching challenge for combining multiple equality constraints - // into a single weight polynomial via random linear combination. - let alpha: EF = challenger.sample_algebra_element(); - - let k = poly.num_variables(); - - // Initialize a zero weight polynomial with the same number of variables. - let mut weights = EvaluationsList::zero(k); - - // The claimed sum starts at zero. - // It will be accumulated from the equality constraints below. - let mut sum = EF::ZERO; - - // Populate the weight polynomial from equality constraints. - // INITIALIZED=false means the weights buffer is freshly zeroed, - // so we write directly rather than accumulate. - statement.combine_hypercube::(&mut weights, &mut sum, alpha); - - // Compute the constant and quadratic coefficients of the round polynomial: - // c_0 = h(0) = sum_{b in {0,1}^{k-1}} f(0, b) * w(0, b) - // c_2 = h(2) = sum_{b in {0,1}^{k-1}} f(2, b) * w(2, b) - // - // The linear coefficient c_1 is derived by the verifier from: - // h(0) + h(1) = claimed_sum - let (c0, c2) = poly.sumcheck_coefficients(&weights); - - // Commit (c_0, c_2) to the Fiat-Shamir transcript. - // Perform any required proof-of-work grinding. - // Receive the verifier's challenge `r`. - let r = sumcheck_data.observe_and_sample(challenger, c0, c2, pow_bits); - - // Fold the weight polynomial by binding its first variable to `r`: - // w'(x_2, ..., x_k) = w(0, x_2, ...) + r * (w(1, ...) - w(0, ...)) - weights.compress(r); - - // Fold the evaluation polynomial similarly. - // Promote base field evaluations into extension field elements during the fold. - let evals = poly.compress_ext(r); - - // Update the claimed sum to h(r) using quadratic extrapolation. - // h(1) = sum - c_0, since h(0) + h(1) = sum. - sum = extrapolate_012(c0, sum - c0, c2, r); - - // Wrap the folded polynomials into a paired polynomial (scalar variant). - let mut poly = ProductPolynomial::::new_small(evals, weights); - - // Verify the core sumcheck invariant. - debug_assert_eq!(poly.dot_product(), sum); - - // Collect all verifier challenges. - // The first challenge `r` was computed above. - // The remaining come from subsequent rounds. - let rs = core::iter::once(r) - .chain( - (1..folding_factor) - .map(|_| poly.round(sumcheck_data, challenger, &mut sum, pow_bits)), - ) - .collect(); - - (Self { poly, sum }, MultilinearPoint::new(rs)) - } - - /// Constructs a sumcheck instance using the classic approach with SIMD-packed arithmetic. - /// - /// Primary path for large polynomials. - /// Processes multiple evaluations in parallel using SIMD lanes. - /// - /// # Algorithm - /// - /// Same as the scalar classic approach, except: - /// - The weight polynomial uses packed entries (fewer elements, each holding SIMD_WIDTH values). - /// - The evaluation polynomial is packed into SIMD lanes before computing coefficients. - /// - A horizontal reduction (sum across SIMD lanes) produces scalar `(c_0, c_2)`. - /// - /// # Returns - /// - /// - The partially-folded prover state. - /// - The verifier challenges `(r_1, ..., r_{folding_factor})`. - #[tracing::instrument(skip_all)] - fn new_classic_packed( - poly: &EvaluationsList, - sumcheck_data: &mut SumcheckData, - challenger: &mut Challenger, - folding_factor: usize, - pow_bits: usize, - statement: &EqStatement, - ) -> (Self, MultilinearPoint) - where - Challenger: FieldChallenger + GrindingChallenger, - { - // Sample a batching challenge for combining multiple constraints. - let alpha: EF = challenger.sample_algebra_element(); - - let k = poly.num_variables(); - - // The packed representation absorbs the last k_pack variables into SIMD lanes. - // So the weight polynomial needs only 2^{k - k_pack} packed entries. - let k_pack = log2_strict_usize(F::Packing::WIDTH); - - // Initialize a zero weight polynomial in packed representation. - let mut weights = EvaluationsList::zero(k - k_pack); - let mut sum = EF::ZERO; - - // Populate packed weights from equality constraints. - statement.combine_hypercube_packed::(&mut weights, &mut sum, alpha); - - // Pack the base-field evaluations into SIMD lanes. - // Each packed element holds SIMD_WIDTH consecutive evaluations. - let poly_packed = EvaluationsList::new(F::Packing::pack_slice(poly.as_slice()).to_vec()); - - // Compute sumcheck coefficients in packed arithmetic. - // The result is still in packed form (one value per SIMD lane). - let (c0, c2) = poly_packed.sumcheck_coefficients(&weights); - - // Sum across all SIMD lanes to produce scalar coefficients. - // The sumcheck polynomial is a sum over ALL evaluation points, not per-lane. - let c0 = EF::ExtensionPacking::to_ext_iter([c0]).sum(); - let c2 = EF::ExtensionPacking::to_ext_iter([c2]).sum(); - - // Commit (c_0, c_2) to the transcript and receive the challenge. - let r = sumcheck_data.observe_and_sample(challenger, c0, c2, pow_bits); - - // Fold the packed weight polynomial by binding the first variable to `r`. - weights.compress(r); - - // Fold the base-field evaluations and promote into packed extension field form. - let evals = poly.compress_into_packed(r); - - // Update the claimed sum to h(r) via quadratic extrapolation. - sum = extrapolate_012(c0, sum - c0, c2, r); - - // Wrap into a paired polynomial (packed variant). - // The constructor checks whether the data is small enough for scalar mode. - let mut poly = ProductPolynomial::::new_packed(evals, weights); - - // Verify the sumcheck invariant. - debug_assert_eq!(poly.dot_product(), sum); - - // Collect all verifier challenges. - let rs = core::iter::once(r) - .chain( - (1..folding_factor) - .map(|_| poly.round(sumcheck_data, challenger, &mut sum, pow_bits)), - ) - .collect(); - - (Self { poly, sum }, MultilinearPoint::new(rs)) - } - - /// Constructs a sumcheck instance using the Split-Value Optimization (Algorithm 5). - /// - /// Used when the sumcheck polynomial has the form: - /// - /// ```text - /// g(X) = sum_j alpha^j * eq(w_j, X) * p(X) - /// ``` - /// - /// Exploits the factorization of the equality polynomial - /// to avoid building the full `2^l`-sized equality table. - /// - /// # Algorithm - /// - /// For each round `i`: - /// - /// 1. Compute Lagrange weights from challenges collected so far. - /// 2. For each constraint, compute its contribution to `c_0` and `c_2` - /// via dot products of accumulators with Lagrange weights, scaled by `alpha^j`. - /// 3. Commit `(c_0, c_2)` and receive challenge `r_i`. - /// 4. Update the claimed sum via quadratic extrapolation. - /// - /// After all rounds, the polynomial and weights are materialized in packed form. - /// - /// # Performance - /// - /// Avoids the `O(2^l)` cost of the full equality table. - /// Uses `O(2^{l/2})` pre-computed accumulators per constraint instead. - /// - /// # Panics - /// - /// - If the folding factor is 0 or exceeds the number of variables. - /// - If any constraint has a mismatched number of variables. - /// - If there are not enough variables for packed representation after folding. - #[tracing::instrument(skip_all)] - pub(super) fn new_svo( - poly: &EvaluationsList, - sumcheck_data: &mut SumcheckData, - challenger: &mut Challenger, - folding_factor: usize, - pow_bits: usize, - statements: &[SplitEq], - ) -> (Self, MultilinearPoint) - where - Challenger: FieldChallenger + GrindingChallenger, - { - assert_ne!(folding_factor, 0); - - // Sample the batching challenge for combining multiple split eq constraints. - let alpha: EF = challenger.sample_algebra_element(); - - let k = poly.num_variables(); - assert!( - folding_factor <= k, - "number of rounds must be less than or equal to instance size" - ); - - // All constraints must operate over the same number of variables. - for statement in statements { - assert_eq!(statement.num_variables(), k); - } - - let k_pack = log2_strict_usize(F::Packing::WIDTH); - - // After folding, the remaining polynomial must have enough variables - // for packed representation. - assert!(k >= 2 * k_pack + folding_factor); - - // Initialize the claimed sum as the random linear combination: - // sum = sum_j alpha^j * eval_j - let mut sum = statements - .iter() - .zip(alpha.powers()) - .map(|(statement, alpha)| statement.eval * alpha) - .sum::(); - - // Gather the pre-computed accumulator tables for all constraints. - // Each provides a 2D array indexed by [round][{0, 2}]: - // - [round][0] stores data for computing c_0 - // - [round][1] stores data for computing c_2 - let accumulators = statements - .iter() - .map(SplitEq::accumulators) - .collect::>(); - - // Collect verifier challenges across all SVO rounds. - let mut rs = Vec::with_capacity(folding_factor); - tracing::info_span!("svo rounds").in_scope(|| { - for round_idx in 0..folding_factor { - // Initialize round polynomial coefficients to zero. - let (mut c0, mut c2): (EF, EF) = Default::default(); - - // Compute Lagrange interpolation weights from previous challenges. - // These reconstruct round polynomial coefficients from the - // pre-computed accumulators without materializing the full equality table. - let weights = lagrange_weights_012_multi(rs.as_slice()); - - // Accumulate each constraint's contribution, scaled by alpha^j. - for (accumulators, alpha) in accumulators.iter().zip(alpha.powers()) { - // Accumulators for computing h(0) and h(2) at this round. - let acc0 = &accumulators[round_idx][0]; - let acc2 = &accumulators[round_idx][1]; - - // Dot product with Lagrange weights reconstructs the coefficient. - c0 += alpha - * dot_product::(acc0.iter().copied(), weights.iter().copied()); - c2 += alpha - * dot_product::(acc2.iter().copied(), weights.iter().copied()); - } - - // Commit (c_0, c_2) to the transcript and receive challenge r_i. - let r = sumcheck_data.observe_and_sample(challenger, c0, c2, pow_bits); - - // Update the claimed sum to h(r) using quadratic extrapolation. - sum = extrapolate_012(c0, sum - c0, c2, r); - - // Record this round's challenge for the next round's Lagrange weights. - rs.push(r); - } - }); - - // Materialize the evaluation polynomial by folding the original base-field - // evaluations through all collected challenges at once. - let poly = poly.compress_multi_into_packed(&rs); - - // Materialize the weight polynomial in packed form by combining all split eq - // constraints into a single packed weight array. - let mut weights = EvaluationsList::::zero(poly.num_variables()); - SplitEq::combine_into_packed(statements, weights.as_mut_slice(), alpha, &rs); - - // Wrap into a paired polynomial (packed) for subsequent standard rounds. - let poly = ProductPolynomial::::new_packed(poly, weights); - - // Verify the sumcheck invariant after materialization. - debug_assert_eq!(poly.dot_product(), sum); - (Self { poly, sum }, MultilinearPoint::new(rs)) - } - - /// Entry point: constructs a sumcheck prover from base-field evaluations. - /// - /// Dispatches to the appropriate proving strategy based on the statement: - /// - /// - **SVO**: For statements with pre-split equality polynomials. - /// - **Classic packed**: For large polynomials where SIMD is beneficial. - /// - **Classic scalar**: Fallback for very small instances. - /// - /// # Returns - /// - /// - The partially-folded prover state, ready for further rounds. - /// - The verifier challenges `(r_1, ..., r_{folding_factor})`. - /// - /// # Panics - /// - /// - If the folding factor is 0 or exceeds the number of variables. - /// - For SVO: if the folding factor does not match the pre-configured split point. - #[tracing::instrument(skip_all)] - pub fn from_base_evals( - sumcheck_data: &mut SumcheckData, - challenger: &mut Challenger, - folding_factor: usize, - pow_bits: usize, - statement: &InitialStatement, - ) -> (Self, MultilinearPoint) - where - Challenger: FieldChallenger + GrindingChallenger, - { - let k = statement.num_variables(); - assert_ne!(folding_factor, 0, "number of rounds must be non-zero"); - assert!( - folding_factor <= k, - "number of rounds must be less than or equal to instance size" - ); - - // Threshold for choosing packed vs. scalar. - let k_pack = log2_strict_usize(F::Packing::WIDTH); - let poly = &statement.poly; - match &statement.inner { - // SVO path: the statement has pre-split equality polynomials. - InitialStatementInner::Svo { split_eqs, l0 } => { - // The folding factor must match the pre-configured split point. - assert_eq!(*l0, folding_factor); - // Ensure enough variables remain after SVO rounds for packed mode. - assert!(k > 2 * k_pack + folding_factor); - Self::new_svo( - poly, - sumcheck_data, - challenger, - folding_factor, - pow_bits, - split_eqs, - ) - } - // Classic path: choose packed or scalar based on polynomial size. - InitialStatementInner::Classic(statement) => { - if k > k_pack { - // Large enough for SIMD packing to be beneficial. - Self::new_classic_packed( - poly, - sumcheck_data, - challenger, - folding_factor, - pow_bits, - statement, - ) - } else { - // Too small for SIMD; use scalar arithmetic. - Self::new_classic_small( - poly, - sumcheck_data, - challenger, - folding_factor, - pow_bits, - statement, - ) - } - } - } - } - - /// Returns the number of remaining (unbound) variables. - /// - /// Decreases by one per round. - /// Reaches zero when the polynomial is fully evaluated at a single point. - pub fn num_variables(&self) -> usize { - self.poly.num_variables() - } - - /// Extracts the current evaluation polynomial as scalar extension field elements. - /// - /// If the internal representation is SIMD-packed, unpacks all lanes first. - #[tracing::instrument(skip_all)] - pub fn evals(&self) -> EvaluationsList { - self.poly.evals() - } - - /// Evaluates `f` at a given multilinear point via interpolation. - /// - /// The weight polynomial is not involved in this evaluation. - pub fn eval(&self, point: &MultilinearPoint) -> EF { - self.poly.eval(point) - } - - /// Runs additional sumcheck rounds, optionally incorporating new constraints. - /// - /// Two phases: - /// - /// 1. **Constraint folding** (optional): - /// If a constraint is provided, fold it into the weight polynomial - /// and update the claimed sum before any rounds. - /// Used to incorporate STIR challenges between batches. - /// - /// 2. **Round execution**: - /// Performs `folding_factor` rounds of one-variable-per-round sumcheck. - /// Each round computes `(c_0, c_2)`, commits, receives a challenge, and folds. - /// - /// # Returns - /// - /// The verifier challenges `(r_1, ..., r_{folding_factor})` from this batch. - /// - /// # Panics - /// - /// If `folding_factor` exceeds the current number of remaining variables. - #[tracing::instrument(skip_all)] - pub fn compute_sumcheck_polynomials( - &mut self, - sumcheck_data: &mut SumcheckData, - challenger: &mut Challenger, - folding_factor: usize, - pow_bits: usize, - constraint: Option>, - ) -> MultilinearPoint - where - Challenger: FieldChallenger + GrindingChallenger, - { - // If a new constraint is provided, fold it into the weight polynomial - // and update the claimed sum before starting rounds. - if let Some(constraint) = constraint { - self.poly.combine(&mut self.sum, &constraint); - } - - // Execute rounds of the standard sumcheck protocol. - // Each call computes coefficients, commits, receives a challenge, and folds. - let res = (0..folding_factor) - .map(|_| { - self.poly - .round(sumcheck_data, challenger, &mut self.sum, pow_bits) - }) - .collect(); - - // Return the collected verifier challenges. - MultilinearPoint::new(res) - } -} diff --git a/src/sumcheck/svo.rs b/src/sumcheck/svo.rs deleted file mode 100644 index 0b524980..00000000 --- a/src/sumcheck/svo.rs +++ /dev/null @@ -1,675 +0,0 @@ -//! Split-Value Optimization (SVO) for the sumcheck protocol. -//! -//! Optimizes sumcheck proving when the polynomial includes an equality polynomial factor. -//! Implements Algorithm 5 from "Speeding Up Sum-Check Proving" (ePrint 2025/1117). -//! -//! # Key Insight -//! -//! For polynomials of the form `g(X) = eq(w, X) * p(X)`, -//! the equality polynomial can be decomposed as: -//! -//! ```text -//! eq(w, (r_{i}, x') -//! ``` -//! -//! This allows: -//! 1. Pre-computing smaller tables for the left and right components. -//! 2. Avoiding the full `2^l`-sized equality table. -//! 3. Reconstructing round polynomials from compact accumulators via Lagrange interpolation. - -use alloc::{vec, vec::Vec}; - -use itertools::Itertools; -use p3_field::{ExtensionField, Field, PackedFieldExtension, PackedValue, dot_product}; -use p3_maybe_rayon::prelude::*; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; -use p3_util::{log2_strict_usize, log3_strict_usize}; - -/// Generates grid points for SVO accumulator evaluation. -/// -/// Returns two arrays of `3^{l-1}` points each: -/// - First array: points in `{0,1,2}^{l-1} x {0}` (for computing `h(0)`) -/// - Second array: points in `{0,1,2}^{l-1} x {2}` (for computing `h(2)`) -/// -/// The round polynomial `h(X)` is quadratic (degree 2). -/// Three evaluations determine it uniquely. -/// We compute `h(0)` and `h(2)` from accumulators. -/// The verifier derives `h(1) = claimed_sum - h(0)`. -/// -/// # Panics -/// -/// Panics if `l == 0`. -/// -/// # Performance -/// -/// Time: O(3^l), Space: O(3^l) -pub(super) fn points_012(l: usize) -> [Vec>; 2] { - /// Expands a set of points by appending each value from `values` to each point. - /// - /// If `pts` has `n` points and `values` has `m` elements, the result has `m * n` points. - fn expand(pts: &[Vec], values: &[usize]) -> Vec> { - values - .iter() - .flat_map(|&v| { - // For each value, clone all existing points and append the value. - pts.iter().cloned().map(move |mut p| { - p.push(F::from_u32(v as u32)); - p - }) - }) - .collect() - } - - // We need at least one round. - assert!(l > 0, "points_012: l must be positive"); - - // Start with the empty point (representing 0 dimensions). - let mut pts = vec![vec![]]; - - // Build up points in {0,1,2}^{l-1} by iteratively expanding. - // After this loop, pts contains 3^{l-1} points. - for _ in 0..l - 1 { - pts = expand(&pts, &[0, 1, 2]); - } - - // Create final points by appending 0 or 2 as the last coordinate. - [expand(&pts, &[0]), expand(&pts, &[2])] -} - -/// Computes SVO accumulators for a set of grid points. -/// -/// For each grid point `u`, computes: -/// -/// ```text -/// A(u) = f(u) * eq(u, point) -/// ``` -/// -/// where `f(u)` is derived from partial evaluations -/// and `eq` is the equality polynomial. -/// -/// These values are later combined with Lagrange weights -/// to reconstruct the round polynomial. -/// -/// -/// # Algorithm -/// -/// 1. Split the challenge point into inner (`z0`) and outer (`z1`) components. -/// 2. Reduce partial evaluations over `z1` using the equality polynomial. -/// 3. For each grid point `u`, compute the accumulator via Lagrange interpolation. -/// -/// # Returns -/// -/// One accumulator value per grid point. -pub(super) fn calculate_accumulators>( - us: &[Vec], - partial_evals: &[EF], - point: &[EF], -) -> Vec { - // Determine the dimensions involved. - // - l0: log2 of partial_evals length (total variables in the partial evaluation domain) - // - offset: number of variables handled by the "outer" equality polynomial - let l0 = log2_strict_usize(partial_evals.len()); - let offset = l0 - log3_strict_usize(us.len()) - 1; - - // Split the challenge point into inner (z0) and outer (z1) components. - // - z0 corresponds to the variables covered by the grid points. - // - z1 corresponds to the remaining variables handled separately. - let (z0, z1) = point.split_at(point.len() - offset); - - // Build equality polynomial evaluation tables for both components. - // - eq0: evaluations of eq(z0, x) for x in {0,1}^{|z0|} - // - eq1: evaluations of eq(z1, x) for x in {0,1}^{|z1|} - let eq0 = EvaluationsList::new_from_point(z0, EF::ONE); - let eq1 = EvaluationsList::new_from_point(z1, EF::ONE); - - // Reduce partial evaluations over the outer variables using eq1. - // - // This computes: sum_{x1} eq(z1, x1) * partial_evals[chunk_for_x1] - let reduced_evals: Vec = partial_evals - .chunks(eq1.num_evals()) - .map(|chunk| dot_product::(eq1.iter().copied(), chunk.iter().copied())) - .collect(); - - // For each grid point u, compute the accumulator value. - // - // This uses parallel iteration for better performance when |us| is large. - // The computation for each u is independent, making it embarrassingly parallel. - us.par_iter() - .map(|u| { - // Build the Lagrange coefficient vector for this grid point. - // coeffs[x] = prod_{i} L_{u_i}(x_i) where L is the Lagrange basis. - let coeffs = EvaluationsList::new_from_point(u.as_slice(), F::ONE); - - // Compute: (sum_x eq(z0, x) * coeffs(x)) * (sum_x reduced_evals[x] * coeffs(x)) - // This gives f(u) * eq(u, point) via the Lagrange interpolation formula. - dot_product::(eq0.iter().copied(), coeffs.iter().copied()) - * dot_product::(reduced_evals.iter().copied(), coeffs.iter().copied()) - }) - .collect() -} - -/// Split equality polynomial with precomputed accumulators for optimized sumcheck proving. -/// -/// The equality polynomial for a point `w in F^k` is: -/// -/// ```text -/// eq(w, X) = prod_{i=1}^{k} (w_i * X_i + (1 - w_i) * (1 - X_i)) -/// ``` -/// -/// This struct exploits the product structure by splitting `w` into three parts: -/// -/// - The first `l0` coordinates, kept as a raw point for SVO rounds. -/// - The next `(k - l0) / 2` coordinates, stored as a scalar evaluation table. -/// - The remaining coordinates, stored as a packed evaluation table for SIMD. -/// -/// This allows computing `eq(w, x)` for many `x` values efficiently by: -/// 1. Pre-computing small tables for each half. -/// 2. Combining them as needed during the sumcheck protocol. -/// -/// # Memory -/// -/// Instead of storing a `2^{k-l0}`-sized table, we store: -/// - `2^{(k-l0)/2}` scalar extension field elements for the first half. -/// - `2^{(k-l0)/2}` packed extension field elements for the second half. -/// -/// This reduces memory from `O(2^{k-l0})` to `O(2^{(k-l0)/2})`. -/// Precomputes `O(3^l)` total accumulator values upfront. -#[derive(Debug, Clone)] -pub(crate) struct SplitEq> { - /// First part of the point where we apply the SVO optimization. - z_svo: MultilinearPoint, - - /// Evaluations of eq for the lower-index half of rest of the point. - /// - /// Contains 2^{(k-l0)/2} extension field elements. - eq0: EvaluationsList, - - /// Evaluations of eq for the higher-index half of rest of the point. - /// - /// Stored in packed form for SIMD operations. - /// Contains 2^{(k-l0)/2 - log2(packing_width)} packed elements. - eq1: EvaluationsList, - - /// Precomputed accumulators for all SVO rounds. - /// - /// `accumulators[i]` contains the accumulator values for round `i+1`: - /// - `accumulators[i][0]`: Values for grid points in `{0,1,2}^i x {0}` - /// - `accumulators[i][1]`: Values for grid points in `{0,1,2}^i x {2}` - accumulators: Vec<[Vec; 2]>, - - /// The evaluation `sum_x eq(w, x) * poly(x)` of the polynomial weighted by eq. - /// - /// This is the initial claimed sum for the sumcheck protocol. - pub(crate) eval: EF, - - /// The original challenge point `w` for the equality polynomial. - pub(crate) point: MultilinearPoint, -} - -impl> SplitEq { - /// Creates a new split equality polynomial with precomputed accumulators. - /// - /// 1. Splits the challenge point into SVO, scalar, and packed components. - /// 2. Computes partial evaluations of `poly` weighted by the equality polynomial. - /// 3. Evaluates `poly` at `point`, storing the result in `eval`. - /// 4. Precomputes accumulators for all `l` SVO rounds. - #[tracing::instrument(skip_all)] - pub(crate) fn new(point: &MultilinearPoint, l: usize, poly: &EvaluationsList) -> Self { - let k = point.num_variables(); - assert_eq!(k, poly.num_variables()); - assert!(k > l); - assert!(k >= 2 * log2_strict_usize(F::Packing::WIDTH)); - - // Split into SVO part and the rest. - let (z_svo, z_rest) = point.split_at(l); - - // Split the rest into two halves for the eq tables. - let (z0, z1) = z_rest.split_at((k - l) / 2); - - // Build evaluation tables: eq0 unpacked, eq1 packed for SIMD. - let eq0 = EvaluationsList::new_from_point(z0.as_slice(), EF::ONE); - let eq1 = EvaluationsList::new_packed_from_point(z1.as_slice(), EF::ONE); - - // Compute partial evaluations of the polynomial weighted by the split eq. - // - // For each chunk of the polynomial (one per SVO hypercube point), computes: - // sum_{x_inner, x_outer} eq_inner[x_inner] * eq_outer[x_outer] * poly[x_inner, x_outer] - // - // Also computes the full weighted evaluation by combining partial results - // with the SVO portion of the equality polynomial. - let (partial_evals, eval) = { - // Each chunk of size 2^{k-l} is processed independently. - let k_split = - eq1.num_variables() + eq0.num_variables() + log2_strict_usize(F::Packing::WIDTH); - let chunk_size = 1 << k_split; - let partial_evals = poly - .as_slice() - .chunks(chunk_size) - .map(|poly_chunk| { - // Pack the polynomial chunk for SIMD operations. - let poly_packed = F::Packing::pack_slice(poly_chunk); - - // Compute the double sum: sum_{x_L} sum_{x_R} left[x_L] * right[x_R] * poly[x_L, x_R] - // The outer sum is over right indices (parallelized). - // The inner sum is over left indices (vectorized via packing). - let sum = poly_packed - .par_chunks(eq1.num_evals()) - .zip_eq(eq0.as_slice().par_iter()) - .map(|(poly_packed, &eq0_val)| { - // Inner sum: sum_{x_1} eq1[x_1] * poly[x_0, x_1] - let inner_sum = poly_packed - .iter() - .zip_eq(eq1.iter()) - .map(|(&f, &eq1_val)| eq1_val * f) - .sum::(); - // Multiply by the eq0 weight. - inner_sum * eq0_val - }) - .sum::(); - - // Unpack the result to get a single extension field element. - EF::ExtensionPacking::to_ext_iter([sum]).sum() - }) - .collect::>(); - - // Combine partial evals with eq_svo to get the full evaluation. - let eq_svo = EvaluationsList::new_from_point(z_svo.as_slice(), EF::ONE); - let eval = - dot_product::(eq_svo.iter().copied(), partial_evals.iter().copied()); - (partial_evals, eval) - }; - - // Precompute accumulators for all SVO rounds. - let accumulators = (1..=z_svo.num_variables()) - .map(|i| { - let us = points_012::(i); - let acc0 = calculate_accumulators(&us[0], &partial_evals, z_svo.as_slice()); - let acc2 = calculate_accumulators(&us[1], &partial_evals, z_svo.as_slice()); - [acc0, acc2] - }) - .collect(); - - Self { - z_svo, - eq0, - eq1, - accumulators, - eval, - point: point.clone(), - } - } - - /// Returns the total number of variables `k` in the original point. - /// - /// Equal to the SVO depth plus the split table variables: - /// - /// ```text - /// k = k_svo + k_split - /// ``` - pub(crate) const fn num_variables(&self) -> usize { - self.k_svo() + self.k_split() - } - - /// Returns the precomputed accumulators for all SVO rounds. - /// - /// `accumulators()[i]` contains `[acc_0, acc_2]` for round `i+1`. - pub(crate) fn accumulators(&self) -> &[[Vec; 2]] { - &self.accumulators - } - - /// Returns the number of SVO variables (`l0`). - /// - /// This is the depth of the SVO optimization. - /// These coordinates are processed via the accumulator-based Lagrange - /// interpolation path rather than the standard fold-and-sum path. - const fn k_svo(&self) -> usize { - self.z_svo.num_variables() - } - - /// Returns the number of variables covered by the split eq tables. - /// - /// ```text - /// k_split = inner_half_vars + outer_half_vars + log_2(SIMD_WIDTH) - /// ``` - /// - /// The `log_2(SIMD_WIDTH)` term accounts for variables absorbed by SIMD packing. - /// Together with the SVO depth, this reconstructs the original `k`. - const fn k_split(&self) -> usize { - self.eq1.num_variables() + self.eq0.num_variables() + log2_strict_usize(F::Packing::WIDTH) - } - - /// Combines multiple split eq polynomials into a single packed output. - /// - /// Used after the SVO rounds to merge split representations - /// back into a single weight vector for subsequent sumcheck rounds. - /// - /// For each output position indexed by `(x_L, x_R)`: - /// - /// ```text - /// out[x_L, x_R] += sum_j alpha^j * eq(z_svo_j, rs) * eq0_j[x_L] * eq1_j[x_R] - /// ``` - /// - /// where: - /// - `j` ranges over the input split eqs - /// - `rs` are the random challenges from the completed SVO rounds - /// - `eq(z_svo_j, rs)` evaluates the SVO portion at the challenge point - /// - `eq0_j[x_L]` and `eq1_j[x_R]` are the precomputed split eq tables - /// - /// # Arguments - /// - /// * `selfs` - Slice of split eq representations to combine. - /// * `out` - Output buffer of packed extension field elements to accumulate into. - /// Must have size `2^{k_split - log_2(SIMD_WIDTH)}`. - /// * `alpha` - Batching challenge for merging multiple constraints. - /// * `rs` - The `k_svo` random challenges from the completed SVO rounds. - #[tracing::instrument(skip_all, fields(k = log2_strict_usize(out.len()), selfs = selfs.len()))] - pub(crate) fn combine_into_packed( - selfs: &[Self], - out: &mut [EF::ExtensionPacking], - alpha: EF, - rs: &[EF], - ) { - // Nothing to do if there are no split eqs. - if selfs.is_empty() { - return; - } - - // Verify all split eqs have the same k. - let k_split = selfs.iter().map(Self::k_split).all_equal_value().unwrap(); - - // Verify output buffer size. - assert_eq!( - out.len(), - 1 << (k_split - log2_strict_usize(F::Packing::WIDTH)), - "combine_into_packed: output buffer has wrong size" - ); - - // Verify all split eqs have the same SVO size. - let k_svo = selfs.iter().map(Self::k_svo).all_equal_value().unwrap(); - - assert_eq!( - rs.len(), - k_svo, - "combine_into_packed: wrong number of SVO challenges" - ); - - // Accumulate each split eq into the output, weighted by powers of alpha. - for (split_eq, alpha) in selfs.iter().zip(alpha.powers()) { - // Compute the scalar factor: alpha^j * eq(z_svo_j, rs). - // - // This evaluates the SVO portion of the equality polynomial at the - // sumcheck challenges `rs`, and folds in the batching challenge `alpha`. - // We pass `alpha` as the initial value to `new_from_point` so it gets - // multiplied into every term of the eq evaluation. - let scale = EvaluationsList::new_from_point(split_eq.z_svo.as_slice(), alpha) - .evaluate_hypercube_ext(&MultilinearPoint::new(rs.to_vec())); - - // Process output in chunks of size |eq1|, one chunk per eq0 entry. - // - // The output is indexed as out[x_L * |eq1| + x_R], and we iterate - // over x_L (outer loop via chunks) and x_R (inner loop within chunk). - // For each position: out[x_L, x_R] += eq1[x_R] * eq0[x_L] * scale. - out.par_chunks_mut(split_eq.eq1.num_evals()) - .zip(split_eq.eq0.as_slice().par_iter()) - .for_each(|(chunk, &right)| { - chunk - .iter_mut() - .zip(split_eq.eq1.iter()) - .for_each(|(out, &left)| *out += left * right * scale); - }); - } - } -} - -#[cfg(test)] -mod tests { - use alloc::vec::Vec; - - use p3_field::{PrimeCharacteristicRing, dot_product, extension::BinomialExtensionField}; - use p3_koala_bear::KoalaBear; - use proptest::prelude::*; - use rand::{RngExt, SeedableRng, rngs::SmallRng}; - - use super::*; - - type F = KoalaBear; - type EF = BinomialExtensionField; - - #[test] - fn test_points_012_l1() { - // For l=1: pts should have 3^0 = 1 point each. - // pts_0 = [(0,)] - // pts_2 = [(2,)] - let [pts_0, pts_2] = points_012::(1); - - assert_eq!(pts_0.len(), 1); - assert_eq!(pts_2.len(), 1); - - assert_eq!(pts_0[0], vec![F::ZERO]); - assert_eq!(pts_2[0], vec![F::TWO]); - } - - #[test] - fn test_points_012_l2() { - // For l=2: pts should have 3^1 = 3 points each. - // First l-1=1 coordinates in {0,1,2}, last coordinate fixed. - let [pts_0, pts_2] = points_012::(2); - - assert_eq!(pts_0.len(), 3); - assert_eq!(pts_2.len(), 3); - - // All pts_0 should end with 0. - for pt in &pts_0 { - assert_eq!(pt.len(), 2); - assert_eq!(*pt.last().unwrap(), F::ZERO); - } - - // All pts_2 should end with 2. - for pt in &pts_2 { - assert_eq!(pt.len(), 2); - assert_eq!(*pt.last().unwrap(), F::TWO); - } - } - - #[test] - fn test_points_012_sizes() { - // Verify output sizes: each array should have 3^{l-1} points. - for l in 1..=6 { - let [pts_0, pts_2] = points_012::(l); - let expected_size = 3usize.pow((l - 1) as u32); - - assert_eq!(pts_0.len(), expected_size, "pts_0 size mismatch for l={l}"); - assert_eq!(pts_2.len(), expected_size, "pts_2 size mismatch for l={l}"); - - // Each point should have l coordinates. - for pt in pts_0.iter().chain(pts_2.iter()) { - assert_eq!(pt.len(), l, "point dimension mismatch for l={l}"); - } - } - } - - #[test] - fn test_points_012_values_in_range() { - // All coordinates should be in {0, 1, 2}, since the grid points live - // in {0, 1, 2}^{l-1} x {0 or 2}. The first l-1 coordinates are free - // to take any value in {0, 1, 2}, while the last is fixed. - let [pts_0, pts_2] = points_012::(4); - - let valid_values = [F::ZERO, F::ONE, F::TWO]; - - for pt in pts_0.iter().chain(pts_2.iter()) { - for (i, &coord) in pt.iter().enumerate() { - // Last coordinate is fixed (0 for pts_0, 2 for pts_2). - // Other coordinates should be in {0, 1, 2}. - if i < pt.len() - 1 { - assert!( - valid_values.contains(&coord), - "invalid coordinate value at position {i}" - ); - } - } - } - } - - #[test] - fn test_points_012_unique() { - // All points within each array should be unique, since the expand - // function enumerates all combinations of {0,1,2}^{l-1} crossed - // with a fixed last coordinate. - let [pts_0, pts_2] = points_012::(4); - - let pts_0_set: alloc::collections::BTreeSet<_> = pts_0.iter().cloned().collect(); - let pts_2_set: alloc::collections::BTreeSet<_> = pts_2.iter().cloned().collect(); - - assert_eq!(pts_0_set.len(), pts_0.len(), "pts_0 contains duplicates"); - assert_eq!(pts_2_set.len(), pts_2.len(), "pts_2 contains duplicates"); - } - - #[test] - #[should_panic(expected = "l must be positive")] - fn test_points_012_panics_on_zero() { - let _: [Vec>; 2] = points_012(0); - } - - #[test] - fn test_accumulators_correctness() { - // Main correctness test: verify accumulators match naive computation. - // - // For each SVO depth l, verify that the precomputed accumulators match - // what we'd get by directly computing eq(z, u) * f(u) via compress_multi. - let k = 10; - let mut rng = SmallRng::seed_from_u64(1); - - // Generate a random polynomial f over the boolean hypercube {0,1}^k. - let f = EvaluationsList::new((0..1 << k).map(|_| rng.random()).collect()); - - // Generate a random challenge point z and build the full eq table. - let z = MultilinearPoint::::rand(&mut rng, f.num_variables()); - let eq = EvaluationsList::new_from_point(z.as_slice(), EF::ONE); - - // Test for each SVO depth l from 1 to k/2 - 1. - for l in 1..k / 2 { - let split_eq = SplitEq::::new(&z, l, &f); - - // There should be exactly l accumulator rounds (one per SVO variable). - let accumulators = split_eq.accumulators(); - assert_eq!(accumulators.len(), l); - - // For each round i, verify both the h(0) and h(2) accumulators. - for (i, accumulator) in accumulators.iter().enumerate() { - let us = points_012::(i + 1); - - // Verify h(0) accumulators: grid points ending in 0. - // For each grid point u, the accumulator should equal - // sum_x eq(z, (u, x)) * f(u, x) computed via compress_multi. - us[0] - .iter() - .zip(accumulator[0].iter()) - .for_each(|(u, &acc)| { - // Lift u from base field to extension field for compress_multi. - let u = u.iter().copied().map(EF::from).collect::>(); - // Partially evaluate f and eq by binding the first i+1 variables to u. - let f = f.compress_multi(&u); - let eq = eq.compress_multi(&u); - // The naive accumulator is the dot product of the residual eq and f. - let e1: EF = dot_product(eq.iter().copied(), f.iter().copied()); - assert_eq!(acc, e1); - }); - - // Verify h(2) accumulators: grid points ending in 2. - // Same logic as above, but with extrapolation point 2. - us[1] - .iter() - .zip(accumulator[1].iter()) - .for_each(|(u, &acc)| { - let u = u.iter().copied().map(EF::from).collect::>(); - let f = f.compress_multi(&u); - let eq = eq.compress_multi(&u); - let e1: EF = dot_product(eq.iter().copied(), f.iter().copied()); - assert_eq!(acc, e1); - }); - } - } - } - - #[test] - fn test_split_eq_k_calculation() { - // Verify that k() returns the original number of variables. - // The splitting into z_svo, eq0, and eq1 must preserve k = k_svo + k_split, - // regardless of how the coordinates are distributed. - let mut rng = SmallRng::seed_from_u64(42); - - for k in [8, 10, 12, 14] { - let point = MultilinearPoint::::rand(&mut rng, k); - let poly = EvaluationsList::new((0..1 << k).map(|_| rng.random()).collect()); - // Use l=0 SVO depth so all variables go into the split eq tables. - let split_eq = SplitEq::::new(&point, 0, &poly); - assert_eq!(split_eq.num_variables(), k, "k() mismatch for k={k}"); - } - } - - #[test] - fn test_split_eq_partial_evals_size() { - // Verify partial_evals returns the correct number of elements. - // - // When the polynomial has n variables and the split eq covers k variables, - // partial_evals processes the polynomial in chunks of 2^k, producing - // one partial evaluation per chunk. So the output length is 2^{n-k}. - let mut rng = SmallRng::seed_from_u64(42); - let k = 10; - - let point = MultilinearPoint::::rand(&mut rng, k); - let poly = EvaluationsList::new((0..1 << k).map(|_| rng.random()).collect()); - let split_eq = SplitEq::::new(&point, 0, &poly); - - // With l=0 SVO depth, all variables are in the split tables. - // partial_evals on a k-variable polynomial should give 2^0 = 1 partial eval. - // But let's test with a larger polynomial. - let n = 14; - let larger_poly = EvaluationsList::new((0..1 << n).map(|_| rng.random()).collect()); - - // We need to create a new split_eq for n variables to test partial_evals - let point_n = MultilinearPoint::::rand(&mut rng, n); - let split_eq_n = SplitEq::::new(&point_n, 0, &larger_poly); - // The number of partial evals is 2^{k_svo} = 2^0 = 1 for l=0 - // This is implicitly tested by the constructor succeeding. - assert_eq!(split_eq_n.num_variables(), n); - let _ = split_eq; - } - - proptest! { - /// Verify that points_012 generates the correct number of points. - #[test] - fn prop_points_012_sizes(l in 1usize..=8) { - let [pts_0, pts_2] = points_012::(l); - let expected = 3usize.pow((l - 1) as u32); - - prop_assert_eq!(pts_0.len(), expected); - prop_assert_eq!(pts_2.len(), expected); - } - - /// Verify that all pts_0 points end with 0 and all pts_2 points end with 2. - #[test] - fn prop_points_012_last_coordinate(l in 1usize..=6) { - let [pts_0, pts_2] = points_012::(l); - - for pt in &pts_0 { - prop_assert_eq!(*pt.last().unwrap(), F::ZERO); - } - for pt in &pts_2 { - prop_assert_eq!(*pt.last().unwrap(), F::TWO); - } - } - - /// Verify that the total variable count matches the original point dimension. - #[test] - fn prop_split_eq_k_consistency(k in 8usize..=14) { - let mut rng = SmallRng::seed_from_u64(k as u64); - let point = MultilinearPoint::::rand(&mut rng, k); - let poly = EvaluationsList::zero(k); - let split_eq = SplitEq::::new(&point, 0, &poly); - - prop_assert_eq!(split_eq.num_variables(), k); - } - } -} diff --git a/src/sumcheck/tests.rs b/src/sumcheck/tests.rs deleted file mode 100644 index f711d65b..00000000 --- a/src/sumcheck/tests.rs +++ /dev/null @@ -1,588 +0,0 @@ -use alloc::{vec, vec::Vec}; - -use p3_baby_bear::{BabyBear, Poseidon2BabyBear}; -use p3_challenger::{DuplexChallenger, FieldChallenger, GrindingChallenger}; -use p3_field::{Field, PrimeCharacteristicRing, TwoAdicField, extension::BinomialExtensionField}; -use p3_merkle_tree::MerkleTreeMmcs; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; -use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; -use rand::{RngExt, SeedableRng, rngs::SmallRng}; - -use crate::{ - constraints::{ - Constraint, - evaluator::ConstraintPolyEvaluator, - statement::{EqStatement, SelectStatement, initial::InitialStatement}, - }, - fiat_shamir::domain_separator::DomainSeparator, - parameters::{FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy}, - sumcheck::{SumcheckData, prover::SumcheckProver, verify_final_sumcheck_rounds}, - whir::proof::WhirProof, -}; - -// Base field: BabyBear (a 31-bit prime field suitable for fast arithmetic). -type F = BabyBear; -// Extension field: degree-4 binomial extension of BabyBear, used for challenge sampling. -type EF = BinomialExtensionField; -// Poseidon2 permutation over BabyBear with width 16, used as the core hash primitive. -type Perm = Poseidon2BabyBear<16>; - -// Sponge-based hash: absorbs 16 field elements, squeezes 8, with 8-element capacity. -type MyHash = PaddingFreeSponge; -// Merkle tree compression: truncates two 8-element digests into one via the permutation. -type MyCompress = TruncatedPermutation; -// Fiat-Shamir challenger: duplex sponge over BabyBear with width 16 and capacity 8. -type MyChallenger = DuplexChallenger; -// Packed field type for SIMD-friendly evaluation (used by the Merkle tree MMCS). -type PackedF = ::Packing; -// Merkle tree-based mixed-matrix commitment scheme, used for committing to polynomial evaluations. -type MyMmcs = MerkleTreeMmcs; - -/// Creates a fresh `DomainSeparator` and `DuplexChallenger` using a fixed RNG seed. -fn domainsep_and_challenger() -> (DomainSeparator, MyChallenger) { - // Initialize a small random number generator with a fixed seed. - let mut rng = SmallRng::seed_from_u64(1); - - // Construct a Poseidon2 permutation instance using 128 bits of security from the RNG - let perm = Perm::new_from_rng_128(&mut rng); - - // Create a new duplex challenger over the field `F` with this permutation - let challenger = MyChallenger::new(perm); - - // Return a fresh (empty) domain separator and the challenger - (DomainSeparator::new(vec![]), challenger) -} - -// Builds a minimal ProtocolParameters instance for testing. The security parameters -// are intentionally low (32-bit security, no proof-of-work grinding) to keep tests fast. -// The MMCS (mixed-matrix commitment scheme) is constructed from the same Poseidon2 -// permutation used elsewhere, ensuring a consistent cryptographic setup. -fn create_test_protocol_params(folding_factor: FoldingFactor) -> ProtocolParameters { - // Use the same fixed seed as domainsep_and_challenger for reproducibility. - let mut rng = SmallRng::seed_from_u64(1); - let perm = Perm::new_from_rng_128(&mut rng); - - // Build the Merkle tree MMCS from the hash and compression functions. - // The trailing `0` is the log of the packing ratio (no packing beyond the native width). - let mmcs = MyMmcs::new(MyHash::new(perm.clone()), MyCompress::new(perm), 0); - - ProtocolParameters { - security_level: 32, // Low security for fast testing. - pow_bits: 0, // No proof-of-work grinding. - rs_domain_initial_reduction_factor: 1, // No initial domain reduction. - folding_factor, - mmcs, - soundness_type: SecurityAssumption::UniqueDecoding, - starting_log_inv_rate: 1, // Rate = 1/2 (log inverse rate = 1). - } -} - -// Simulates the prover side of STIR constraint derivation for an intermediate round. -// -// In the WHIR protocol, between sumcheck folding rounds, the verifier asks for evaluations -// of the current polynomial at randomly chosen points. These evaluations become constraints -// that the next sumcheck round must satisfy. This function: -// 1. Samples `num_eqs` random multilinear points, evaluates the polynomial, and records -// them as "equality" constraints (poly(point) == eval). -// 2. Samples `num_sels` random indices in the multiplicative subgroup, evaluates the -// polynomial as a univariate (Horner's method), and records them as "select" constraints. -// 3. Draws a random combining coefficient alpha and bundles everything into a Constraint. -// -// The evaluations are pushed into `constraint_evals` so the verifier can read them later -// (simulating what would normally be transmitted in the proof). -fn make_constraint_ext( - challenger: &mut Challenger, - constraint_evals: &mut Vec, - num_vars: usize, - num_eqs: usize, - num_sels: usize, - poly: &EvaluationsList, -) -> Constraint -where - Challenger: FieldChallenger + GrindingChallenger, -{ - // omega is the 2^num_vars-th root of unity; powers of omega form the evaluation domain. - let omega = F::two_adic_generator(num_vars); - - // Initialize empty eq and select statements for this round's variable count. - let mut eq_statement = EqStatement::initialize(num_vars); - let mut sel_statement = SelectStatement::initialize(num_vars); - - // - Sample `num_eqs` univariate challenge points. - // - Evaluate the sumcheck polynomial on them. - // - Collect (point, eval) pairs for use in the statement and constraint aggregation. - (0..num_eqs).for_each(|_| { - // Sample a univariate field element from the prover's challenger. - let point: EF = challenger.sample_algebra_element(); - - // Expand it into a `num_vars`-dimensional multilinear point. - let point = MultilinearPoint::expand_from_univariate(point, num_vars); - - // Evaluate the current sumcheck polynomial at the sampled point. - let eval = poly.evaluate_hypercube_ext::(&point); - - // Store evaluation for verifier to read later. - constraint_evals.push(eval); - - // Add the evaluation result to the transcript for Fiat-Shamir soundness. - challenger.observe_algebra_element(eval); - - // Add the evaluation constraint: poly(point) == eval. - eq_statement.add_evaluated_constraint(point, eval); - }); - - // Build "select" constraints: sample indices in the multiplicative subgroup H = {omega^i}, - // evaluate the polynomial as a univariate at omega^index using Horner's method, - // and record (omega^index, eval) pairs. - (0..num_sels).for_each(|_| { - // Sample a random index in [0, 2^num_vars) from the challenger. - let index: usize = challenger.sample_bits(num_vars); - - // Compute the corresponding evaluation point in the multiplicative subgroup. - let var = omega.exp_u64(index as u64); - - // Evaluate the polynomial as a univariate at `var` using Horner's method. - // This treats the evaluation vector as coefficients of a univariate polynomial - // and computes f(var) = c_0 + c_1*var + c_2*var^2 + ... by folding from the right. - let eval = poly - .iter() - .rfold(EF::ZERO, |result, &coeff| result * var + coeff); - - // Store evaluation for verifier to read later. - constraint_evals.push(eval); - - // Add the evaluation result to the transcript for Fiat-Shamir soundness. - challenger.observe_algebra_element(eval); - - // Add the evaluation constraint: poly(point) == eval. - sel_statement.add_constraint(var, eval); - }); - - // Sample a random combining coefficient alpha from the Fiat-Shamir transcript. - // This alpha is used to take a random linear combination of all eq and sel constraints - // into a single aggregated constraint for the next sumcheck round. - let alpha: EF = challenger.sample_algebra_element(); - - Constraint::new(alpha, eq_statement, sel_statement) -} - -// Verifier-side counterpart of `make_constraint_ext`. Reconstructs the same Constraint -// that the prover built, but without access to the polynomial -- only from the -// evaluations that the prover committed to (passed in via `constraint_evals`). -// -// The key invariant is that the verifier's challenger must stay perfectly synchronized -// with the prover's challenger: both sample the same random points and observe the same -// evaluations, so their Fiat-Shamir transcripts remain identical. -fn read_constraint( - challenger: &mut Challenger, - constraint_evals: &[EF], - num_vars: usize, - num_eqs: usize, - num_sels: usize, -) -> Constraint -where - Challenger: FieldChallenger + GrindingChallenger, -{ - // Initialize an empty eq statement for this round. - let mut eq_statement = EqStatement::initialize(num_vars); - - // Reconstruct each eq constraint: sample the same random point as the prover, - // then read the evaluation from the proof data (instead of computing it). - for &eval in constraint_evals.iter().take(num_eqs) { - // Sample a univariate challenge and expand to a multilinear point. - let point = - MultilinearPoint::expand_from_univariate(challenger.sample_algebra_element(), num_vars); - - // Observe the evaluation to keep the challenger synchronized (must match prover) - challenger.observe_algebra_element(eval); - - // Add the constraint: poly(point) == eval. - eq_statement.add_evaluated_constraint(point, eval); - } - - // Initialize an empty select statement for this round. - let mut sel_statement = SelectStatement::::initialize(num_vars); - - // Same domain generator as the prover used. - let omega = F::two_adic_generator(num_vars); - - // Reconstruct each select constraint: sample the same random index as the prover, - // compute the same evaluation point omega^index, then read the evaluation from the proof. - for i in 0..num_sels { - // Sample the same random index as the prover did. - let index: usize = challenger.sample_bits(num_vars); - let var = omega.exp_u64(index as u64); - - // Read the committed evaluation corresponding to this point from constraint_evals. - // Sel evaluations are stored after eq evaluations. - let eval = constraint_evals[num_eqs + i]; - - // Observe the evaluation to keep the challenger synchronized (must match prover) - challenger.observe_algebra_element(eval); - - // Add the constraint: poly(point) == eval. - sel_statement.add_constraint(var, eval); - } - - // Sample the same combining coefficient alpha as the prover and assemble the constraint. - Constraint::new( - challenger.sample_algebra_element(), - eq_statement, - sel_statement, - ) -} - -/// Runs an end-to-end prover-verifier test for the `SumcheckSingle` protocol with nested folding. -/// This test: -/// - Initializes a random multilinear polynomial over `F`. -/// - Runs the prover through several rounds of sumcheck folding. -/// - Verifies the transcript using the verifier. -/// - Checks that all reconstructed constraints match the original ones. -/// - Verifies that the final sum satisfies: -/// \begin{equation} -/// \text{sum} = f(r) \cdot \text{eq}(z, r) -/// \end{equation} -/// -/// # Panics -/// Panics if: -/// - Any intermediate or final round does not produce expected sizes. -/// - Any constraint mismatches. -/// - The verifier-side evaluation differs from the expected one. -/// -/// # Arguments -/// - `num_vars`: Number of variables of the initial polynomial. -/// - `folding_factors`: List of how many variables to fold per round. -/// - `num_eqs`: Number of equality statements to apply at each stage. -/// - `num_sels`: Number of select statements to apply at each stage. -#[allow(clippy::too_many_lines)] -fn run_sumcheck_test( - num_vars: usize, - folding_factor: FoldingFactor, - num_eqs: &[usize], - num_sels: &[usize], - strategy: SumcheckStrategy, -) -> MultilinearPoint { - // Compute how many intermediate folding rounds there are, plus the size of the final round. - // For example, with num_vars=6 and folding_factor=2: num_rounds=2, final_rounds=2. - let (num_rounds, final_rounds) = folding_factor.compute_number_of_rounds(num_vars); - - // We need num_rounds+1 eq constraint counts (one for the initial round, one per intermediate), - // and num_rounds sel constraint counts (one per intermediate round; the initial has no sels). - assert_eq!(num_eqs.len(), num_rounds + 1); - assert_eq!(num_sels.len(), num_rounds); - folding_factor.check_validity(num_vars).unwrap(); - - // Initialize a random multilinear polynomial with 2^num_vars evaluations. - let mut rng = SmallRng::seed_from_u64(1); - let poly = EvaluationsList::new((0..1 << num_vars).map(|_| rng.random()).collect()); - - // ==================== PROVER PHASE ==================== - // The prover has access to the full polynomial and produces a proof transcript. - // The challenger is cloned so that prover and verifier start from the same state - // but evolve independently (they must stay synchronized via Fiat-Shamir). - let (domsep, challenger) = domainsep_and_challenger(); - let mut prover_challenger = challenger.clone(); - - // Create protocol parameters and allocate the proof structure. - let params = create_test_protocol_params(folding_factor); - let mut proof = WhirProof::::from_protocol_parameters(¶ms, num_vars); - - // Absorb the domain separator into the prover's transcript. - domsep.observe_domain_separator(&mut prover_challenger); - - // Store constraint evaluations for each round (prover writes, verifier reads) - // TODO: read from proof in verifier side - let mut all_constraint_evals: Vec> = Vec::new(); - - // Build the initial statement from the polynomial. The initial statement wraps - // the raw evaluations and supports constraint evaluation before the first fold. - let folding0 = folding_factor.at_round(0); - let mut initial_statement = InitialStatement::new(poly.clone(), folding0, strategy); - - // Sample eq constraints for the initial round: for each constraint, draw a random - // univariate challenge, expand it to a multilinear point, evaluate the polynomial - // there, and commit the evaluation to the transcript. - let constaint_evals = (0..num_eqs[0]) - .map(|_| { - let point: EF = prover_challenger.sample_algebra_element(); - let point = MultilinearPoint::expand_from_univariate(point, num_vars); - let eval = initial_statement.evaluate(&point); - prover_challenger.observe_algebra_element(eval); - eval - }) - .collect(); - all_constraint_evals.push(constaint_evals); - - // ROUND 0: Initialize the sumcheck prover from the base evaluations. - // This performs the first folding round: it computes the sumcheck polynomials - // h(X) = c0 + c1*X + c2*X^2 for each variable being folded, writes them into - // the proof, and returns the partially folded state along with the verifier's - // random challenges (prover_randomness) accumulated so far. - let (mut sumcheck, mut prover_randomness) = SumcheckProver::from_base_evals( - &mut proof.initial_sumcheck, - &mut prover_challenger, - folding0, - 0, - &initial_statement, - ); - - // Track how many variables remain to fold - let mut num_vars_inter = num_vars - folding0; - - // INTERMEDIATE ROUNDS (rounds 1..num_rounds): - // Each round simulates the STIR interaction: the verifier asks for evaluations at - // random points (eq + select constraints), the prover responds, and then a new - // sumcheck folding step reduces the number of variables by `folding`. - for (round, (&num_eq_points, &num_sel_points)) in - num_eqs.iter().skip(1).zip(num_sels.iter()).enumerate() - { - // Adjust index since enumerate starts at 0 but we skip the initial round. - let round = round + 1; - let folding = folding_factor.at_round(round); - - // Build STIR constraints for this round: sample random evaluation points, - // compute polynomial evaluations, and assemble them into a Constraint. - let mut constraint_evals: Vec = Vec::new(); - let constraint = make_constraint_ext( - &mut prover_challenger, - &mut constraint_evals, - num_vars_inter, - num_eq_points, - num_sel_points, - &sumcheck.evals(), - ); - all_constraint_evals.push(constraint_evals); - - // Compute the sumcheck polynomials for this folding round. - // Each step produces a univariate polynomial h(X) whose values at 0 and 1 - // sum to the claimed round sum. The verifier's random challenge r is drawn - // from the transcript and used to fold the polynomial: the new claimed sum - // becomes h(r). The constraint is folded into the sumcheck via `Some(constraint)`. - let mut sumcheck_data: SumcheckData = SumcheckData::default(); - prover_randomness.extend(&sumcheck.compute_sumcheck_polynomials( - &mut sumcheck_data, - &mut prover_challenger, - folding, - 0, - Some(constraint), - )); - proof.rounds[round - 1].sumcheck = sumcheck_data; - - // After folding, the polynomial has `folding` fewer variables. - num_vars_inter -= folding; - - // Sanity check: the sumcheck state should track the correct variable count. - assert_eq!(sumcheck.num_variables(), num_vars_inter); - } - - // After all intermediate rounds, the remaining variables should equal final_rounds. - assert_eq!(num_vars_inter, final_rounds); - - // FINAL ROUND: fold the remaining `final_rounds` variables down to a constant (0 variables). - // No constraint is passed (None) because there are no more STIR queries after this point. - let mut sumcheck_data: SumcheckData = SumcheckData::default(); - prover_randomness.extend(&sumcheck.compute_sumcheck_polynomials( - &mut sumcheck_data, - &mut prover_challenger, - final_rounds, - 0, - None, - )); - proof.set_final_sumcheck_data(sumcheck_data); - - // After folding all variables, the polynomial is a constant: f(r) for the full - // random point r = (r_0, r_1, ..., r_{n-1}) accumulated across all rounds. - let final_folded_value = sumcheck.evals().as_constant().unwrap(); - - // All variables have been folded away. - assert_eq!(sumcheck.num_variables(), 0); - - // Core correctness check: the final folded value must equal the polynomial - // evaluated at the full random point r that was built from all verifier challenges. - assert_eq!( - poly.evaluate_hypercube_base(&prover_randomness), - final_folded_value - ); - - // Commit the final evaluation to the Fiat-Shamir transcript. - prover_challenger.observe_algebra_element(final_folded_value); - - // ==================== VERIFIER PHASE ==================== - // The verifier processes the proof transcript, reconstructing each round’s constraints - // from the committed evaluations, verifying the sumcheck polynomials h(X), and - // accumulating the random evaluation point r. At the end it checks: - // sum == f(r) * weights(z, r) - // where weights(z, r) encodes the aggregated constraint polynomial. - let mut verifier_challenger = challenger; - - // Running accumulated sum: starts at 0, gets updated each round by combine_evals - // (which adds the weighted constraint evaluations) and verify_sumcheck_rounds - // (which checks h(0)+h(1)==sum and updates sum := h(r) for the round’s challenge r). - let mut sum = EF::ZERO; - - // The verifier builds up the same random point r as the prover, one chunk per round. - let mut verifier_randomness = MultilinearPoint::new(vec![]); - - // Collect all constraints for the final weights check. - let mut constraints = vec![]; - - // Track remaining variables, mirroring the prover’s count. - let mut num_vars_inter = num_vars; - - // Absorb the same domain separator as the prover to synchronize transcripts. - domsep.observe_domain_separator(&mut verifier_challenger); - - // VERIFY INITIAL ROUND (round 0): - // The initial round only has eq constraints (no select constraints, hence 0 for num_sels). - { - // Reconstruct the initial round's constraint from the proof evaluations. - let constraint = read_constraint( - &mut verifier_challenger, - &all_constraint_evals[0], - num_vars_inter, - num_eqs[0], - 0, - ); - // Accumulate the weighted sum of constraint values - constraint.combine_evals(&mut sum); - // Save constraints for later equality check - constraints.push(constraint); - - // Verify the initial sumcheck polynomials: for each folded variable, check that - // h(0) + h(1) == claimed_sum, then update sum := h(r) with the challenge r. - // The returned challenges are appended to the verifier's random point. - verifier_randomness.extend( - &proof - .initial_sumcheck - .verify_rounds(&mut verifier_challenger, &mut sum, 0) - .unwrap(), - ); - - num_vars_inter -= folding_factor.at_round(0); - } - - // VERIFY INTERMEDIATE ROUNDS (rounds 1..num_rounds): - // For each round, reconstruct constraints from the proof, add their weighted sum, - // then verify the sumcheck polynomials and collect the random challenges. - for (round, (&num_eq_points, &num_sel_points)) in - num_eqs.iter().skip(1).zip(num_sels.iter()).enumerate() - { - let round = round + 1; - // Reconstruct round constraint from transcript - let constraint = read_constraint( - &mut verifier_challenger, - &all_constraint_evals[round], - num_vars_inter, - num_eq_points, - num_sel_points, - ); - // Accumulate the weighted sum of constraint values - constraint.combine_evals(&mut sum); - // Save constraints for later equality check - constraints.push(constraint); - - // Extend r with verifier's folding challenges - // Note: proof.rounds[round - 1] because rounds are 0-indexed but we start at round 1 - let folding = folding_factor.at_round(round); - verifier_randomness.extend( - &proof.rounds[round - 1] - .sumcheck - .verify_rounds(&mut verifier_challenger, &mut sum, 0) - .unwrap(), - ); - - num_vars_inter -= folding; - } - - // VERIFY FINAL ROUND: fold the last remaining variables and finalize the sum. - verifier_randomness.extend( - &verify_final_sumcheck_rounds( - proof.final_sumcheck.as_ref(), - &mut verifier_challenger, - &mut sum, - final_rounds, - 0, - ) - .unwrap(), - ); - - // ==================== FINAL CHECKS ==================== - - // The prover and verifier must have derived the exact same random point r. - // If they differ, the Fiat-Shamir transcripts diverged (a bug in the protocol). - assert_eq!(prover_randomness, verifier_randomness); - - // Evaluate the aggregated constraint weight polynomial at the full random point. - // This polynomial encodes the linear combination of all eq and select constraints - // across all rounds. The reversed point accounts for the bit-reversal convention - // used by the constraint evaluator. - let evaluator = ConstraintPolyEvaluator::new(folding_factor); - let weights = evaluator.eval_constraints_poly(&constraints, &verifier_randomness.reversed()); - - // The fundamental sumcheck identity: the accumulated sum from all rounds must equal - // the final folded polynomial value f(r) multiplied by the constraint weights. - // This confirms that the sumcheck protocol correctly reduced the multilinear claim - // down to a single-point evaluation. - assert_eq!(sum, final_folded_value * weights); - - verifier_randomness -} - -#[test] -fn test_sumcheck_prover() { - // Brute-force test over all valid (num_vars, folding_factor) combinations. - // For each configuration, 100 random constraint setups are tested to cover - // a wide variety of eq/sel constraint counts (0, 1, or 2 each). - // - // Two sumcheck strategies are tested -- Classic and SVO (Shifted Virtual Oracle) -- - // and the test asserts they produce identical verifier randomness, confirming - // that both strategies implement the same protocol semantics. - let mut rng = SmallRng::seed_from_u64(0); - - // Iterate over polynomial sizes from 0 to 10 variables (1 to 1024 evaluations). - for num_vars in 0..=10 { - // Try every valid folding factor: fold 1..num_vars variables per round. - for folding_factor in 1..=num_vars { - // Run 100 random trials per configuration for statistical coverage. - for _ in 0..100 { - let folding_factor = FoldingFactor::Constant(folding_factor); - - // Compute total rounds (+1 because num_eqs includes the initial round). - let num_rounds = folding_factor.compute_number_of_rounds(num_vars).0 + 1; - - // Randomly choose 0, 1, or 2 eq constraints per round. - let num_eq_points = (0..num_rounds) - .map(|_| rng.random_range(0..=2)) - .collect::>(); - - // Randomly choose 0, 1, or 2 select constraints per intermediate round. - // (num_rounds - 1 because the initial round has no select constraints.) - let num_sel_points = (0..num_rounds - 1) - .map(|_| rng.random_range(0..=2)) - .collect::>(); - - // Run the full prover-verifier test with the Classic sumcheck strategy. - let randomness_classic = run_sumcheck_test( - num_vars, - folding_factor, - &num_eq_points, - &num_sel_points, - SumcheckStrategy::Classic, - ); - - // Run the same test with the SVO (Shifted Virtual Oracle) strategy. - let randomness_svo = run_sumcheck_test( - num_vars, - folding_factor, - &num_eq_points, - &num_sel_points, - SumcheckStrategy::Svo, - ); - - // Both strategies must produce the exact same random evaluation point. - // This confirms they are functionally equivalent implementations. - assert_eq!(randomness_classic, randomness_svo); - } - } - } -} diff --git a/src/whir/committer/reader.rs b/src/whir/committer/reader.rs index 164a4a33..d9bdad29 100644 --- a/src/whir/committer/reader.rs +++ b/src/whir/committer/reader.rs @@ -3,7 +3,7 @@ use core::{fmt::Debug, ops::Deref}; use p3_challenger::{CanObserve, FieldChallenger, GrindingChallenger}; use p3_commit::Mmcs; use p3_field::{ExtensionField, Field, PackedValue, TwoAdicField}; -use p3_multilinear_util::multilinear::MultilinearPoint; +use p3_multilinear_util::multilinear::Point; use crate::{constraints::statement::EqStatement, parameters::WhirConfig, whir::proof::WhirProof}; @@ -104,7 +104,7 @@ where let mut ood_statement = EqStatement::initialize(num_variables); (0..ood_samples).for_each(|i| { let point = challenger.sample_algebra_element(); - let point = MultilinearPoint::expand_from_univariate(point, num_variables); + let point = Point::expand_from_univariate(point, num_variables); let eval = ood_answers[i]; challenger.observe_algebra_element(eval); ood_statement.add_evaluated_constraint(point, eval); @@ -192,7 +192,7 @@ mod tests { use p3_dft::Radix2DFTSmallBatch; use p3_field::{Field, extension::BinomialExtensionField}; use p3_merkle_tree::MerkleTreeMmcs; - use p3_multilinear_util::evals::EvaluationsList; + use p3_multilinear_util::evals::Poly; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; @@ -266,7 +266,7 @@ mod tests { let (params, mut rng, mut proof) = make_test_params(5, 3); // Create a random degree-5 multilinear polynomial (32 coefficients). - let polynomial = EvaluationsList::new((0..32).map(|_| rng.random()).collect()); + let polynomial = Poly::new((0..32).map(|_| rng.random()).collect()); // Instantiate the committer using the test config. let committer = CommitmentWriter::new(¶ms); @@ -311,7 +311,7 @@ mod tests { let (params, mut rng, mut proof) = make_test_params(4, 0); // Generate a polynomial with 16 random coefficients. - let polynomial = EvaluationsList::new((0..16).map(|_| rng.random()).collect()); + let polynomial = Poly::new((0..16).map(|_| rng.random()).collect()); // Set up the committer and DFT engine. let committer = CommitmentWriter::new(¶ms); @@ -354,7 +354,7 @@ mod tests { let (params, mut rng, mut proof) = make_test_params(10, 5); // Generate a large polynomial with 1024 random coefficients. - let polynomial = EvaluationsList::new((0..1024).map(|_| rng.random()).collect()); + let polynomial = Poly::new((0..1024).map(|_| rng.random()).collect()); // Initialize the committer and DFT engine. let committer = CommitmentWriter::new(¶ms); @@ -395,7 +395,7 @@ mod tests { let (params, mut rng, mut proof) = make_test_params(4, 2); // Generate a multilinear polynomial with 16 coefficients. - let polynomial = EvaluationsList::new((0..16).map(|_| rng.random()).collect()); + let polynomial = Poly::new((0..16).map(|_| rng.random()).collect()); // Instantiate a committer and DFT backend. let committer = CommitmentWriter::new(¶ms); diff --git a/src/whir/committer/writer.rs b/src/whir/committer/writer.rs index bbe7aea1..a930ce4b 100644 --- a/src/whir/committer/writer.rs +++ b/src/whir/committer/writer.rs @@ -5,7 +5,7 @@ use p3_commit::Mmcs; use p3_dft::TwoAdicSubgroupDft; use p3_field::{ExtensionField, Field, TwoAdicField}; use p3_matrix::{Matrix, dense::RowMajorMatrixView}; -use p3_multilinear_util::multilinear::MultilinearPoint; +use p3_multilinear_util::multilinear::Point; use tracing::{info_span, instrument}; use crate::{ @@ -94,7 +94,7 @@ where // TODO: consider moving ood sampling to whir::Prover::prove (0..self.0.commitment_ood_samples).for_each(|_| { // Generate OOD points from ProverState randomness - let point = MultilinearPoint::expand_from_univariate( + let point = Point::expand_from_univariate( challenger.sample_algebra_element(), self.num_variables, ); @@ -128,7 +128,7 @@ mod tests { use p3_challenger::DuplexChallenger; use p3_dft::Radix2DFTSmallBatch; use p3_merkle_tree::MerkleTreeMmcs; - use p3_multilinear_util::evals::EvaluationsList; + use p3_multilinear_util::evals::Poly; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; @@ -182,7 +182,7 @@ mod tests { // Generate a random polynomial with 32 coefficients. let mut rng = SmallRng::seed_from_u64(1); - let polynomial = EvaluationsList::::new(vec![rng.random(); 32]); + let polynomial = Poly::::new(vec![rng.random(); 32]); let mut proof = WhirProof::::from_protocol_parameters(&whir_params, num_variables); @@ -218,7 +218,7 @@ mod tests { let poly = &statement.poly; let statement = statement.normalize(); for (i, (ood_point, ood_eval)) in statement.iter().enumerate() { - let expected_eval = poly.evaluate_hypercube_base(ood_point); + let expected_eval = poly.eval_base(ood_point); assert_eq!( *ood_eval, expected_eval, "OOD answer at index {i} should match expected evaluation" @@ -259,7 +259,7 @@ mod tests { WhirConfig::::new(num_variables, whir_params.clone()); let mut rng = SmallRng::seed_from_u64(1); - let polynomial = EvaluationsList::::new(vec![rng.random(); 1024]); + let polynomial = Poly::::new(vec![rng.random(); 1024]); let mut proof = WhirProof::::from_protocol_parameters(&whir_params, num_variables); @@ -315,7 +315,7 @@ mod tests { params.commitment_ood_samples = 0; let mut rng = SmallRng::seed_from_u64(1); - let polynomial = EvaluationsList::::new(vec![rng.random(); 32]); + let polynomial = Poly::::new(vec![rng.random(); 32]); let mut proof = WhirProof::::from_protocol_parameters(&whir_params, num_variables); diff --git a/src/whir/mod.rs b/src/whir/mod.rs index ef2b899a..dd00d4a5 100644 --- a/src/whir/mod.rs +++ b/src/whir/mod.rs @@ -14,7 +14,7 @@ mod test { use p3_dft::Radix2DFTSmallBatch; use p3_field::{Field, extension::BinomialExtensionField}; use p3_merkle_tree::MerkleTreeMmcs; - use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; + use p3_multilinear_util::{evals::Poly, multilinear::Point}; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; @@ -84,13 +84,13 @@ mod test { WhirConfig::::new(num_variables, whir_params.clone()); // Define test polynomial with random evaluations - let polynomial = EvaluationsList::new((0..num_evaluations).map(|_| rng.random()).collect()); + let polynomial = Poly::new((0..num_evaluations).map(|_| rng.random()).collect()); // New initial statement let mut statement = params.initial_statement(polynomial, sumcheck_strategy); // And equality constraints: polynomial(point) = expected_value for each point for _ in 0..num_points { - let point = MultilinearPoint::expand_from_univariate(rng.random(), num_variables); + let point = Point::expand_from_univariate(rng.random(), num_variables); let _ = statement.evaluate(&point); } // Normalize to classic eq statement for verifier @@ -296,14 +296,13 @@ mod test { // Create random polynomial let mut rng = SmallRng::seed_from_u64(1); - let polynomial = - EvaluationsList::new((0..num_evaluations).map(|_| rng.random()).collect()); + let polynomial = Poly::new((0..num_evaluations).map(|_| rng.random()).collect()); // New initial statement let mut statement = params.initial_statement(polynomial, sumcheck_strategy); // And equality constraints: polynomial(point) = expected_value for each point for _ in 0..num_points { - let point = MultilinearPoint::expand_from_univariate(rng.random(), num_variables); + let point = Point::expand_from_univariate(rng.random(), num_variables); let _ = statement.evaluate(&point); } // Normalize to classic eq statement for verifier diff --git a/src/whir/proof.rs b/src/whir/proof.rs index 3c5224ea..89130546 100644 --- a/src/whir/proof.rs +++ b/src/whir/proof.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use p3_commit::Mmcs; -use p3_multilinear_util::evals::EvaluationsList; +use p3_multilinear_util::evals::Poly; use serde::{Deserialize, Serialize}; use crate::parameters::ProtocolParameters; @@ -28,7 +28,7 @@ pub struct WhirProof> { pub rounds: Vec>, /// Final polynomial evaluations - pub final_poly: Option>, + pub final_poly: Option>, /// Final round PoW witness pub final_pow_witness: F, diff --git a/src/whir/prover/mod.rs b/src/whir/prover/mod.rs index 4ce68bf1..4248cc30 100644 --- a/src/whir/prover/mod.rs +++ b/src/whir/prover/mod.rs @@ -10,7 +10,7 @@ use p3_matrix::{ dense::{DenseMatrix, RowMajorMatrixView}, extension::FlatMatrixView, }; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; +use p3_multilinear_util::{evals::Poly, multilinear::Point}; use round_state::RoundState; use tracing::{info_span, instrument}; @@ -148,7 +148,7 @@ where { let folded_evaluations = &round_state.sumcheck_prover.evals(); let num_variables = self.num_variables - self.folding_factor.total_number(round_index); - assert_eq!(num_variables, folded_evaluations.num_variables()); + assert_eq!(num_variables, folded_evaluations.num_vars()); // Base case: final round reached if round_index == self.n_rounds() { @@ -164,7 +164,7 @@ where // Transpose for reverse variable order // And then pad with zeros let padded = info_span!("transpose & pad").in_scope(|| { - let num_vars = folded_evaluations.num_variables(); + let num_vars = folded_evaluations.num_vars(); let mut mat = RowMajorMatrixView::new( folded_evaluations.as_slice(), 1 << (num_vars - folding_factor_next), @@ -193,10 +193,8 @@ where let mut ood_statement = EqStatement::initialize(num_variables); let mut ood_answers = Vec::with_capacity(round_params.ood_samples); (0..round_params.ood_samples).for_each(|_| { - let point = MultilinearPoint::expand_from_univariate( - challenger.sample_algebra_element(), - num_variables, - ); + let point = + Point::expand_from_univariate(challenger.sample_algebra_element(), num_variables); let eval = round_state.sumcheck_prover.eval(&point); challenger.observe_algebra_element(eval); @@ -263,7 +261,7 @@ where // Process each set of evaluations retrieved from the Merkle tree openings. for (answer, var) in answers.iter().zip(stir_vars.into_iter()) { - let evals = EvaluationsList::new(answer.clone()); + let evals = Poly::new(answer.clone()); // Fold the polynomial represented by the `answer` evaluations using the verifier's challenge. // The evaluation method depends on whether this is a "skip round" or a "standard round". @@ -272,7 +270,7 @@ where // The `answer` represents a standard multilinear polynomial. // Perform a standard multilinear evaluation at the full challenge point `r`. - let eval = evals.evaluate_hypercube_base(&round_state.folding_randomness); + let eval = evals.eval_base(&round_state.folding_randomness); stir_statement.add_constraint(var, eval); } } @@ -291,9 +289,9 @@ where // Process each set of evaluations retrieved from the Merkle tree openings. for (answer, var) in answers.iter().zip(stir_vars.into_iter()) { // Wrap the evaluations to represent the polynomial. - let evals = EvaluationsList::new(answer.clone()); + let evals = Poly::new(answer.clone()); // Perform a standard multilinear evaluation at the full challenge point `r`. - let eval = evals.evaluate_hypercube_ext::(&round_state.folding_randomness); + let eval = evals.eval_ext::(&round_state.folding_randomness); stir_statement.add_constraint(var, eval); } } diff --git a/src/whir/prover/round_state/state.rs b/src/whir/prover/round_state/state.rs index 26409831..84cf7b1c 100644 --- a/src/whir/prover/round_state/state.rs +++ b/src/whir/prover/round_state/state.rs @@ -4,7 +4,7 @@ use p3_challenger::{FieldChallenger, GrindingChallenger}; use p3_field::{ExtensionField, TwoAdicField}; -use p3_multilinear_util::multilinear::MultilinearPoint; +use p3_multilinear_util::multilinear::Point; use tracing::instrument; use crate::{ @@ -45,7 +45,7 @@ where /// The randomness is sampled via Fiat-Shamir from the current transcript state, /// ensuring both parties derive identical challenge values while maintaining /// cryptographic soundness. The length equals the folding factor k for this round. - pub folding_randomness: MultilinearPoint, + pub folding_randomness: Point, /// Merkle tree commitment for the base field polynomial f: F^n → F. /// diff --git a/src/whir/prover/round_state/tests.rs b/src/whir/prover/round_state/tests.rs index b2528c36..2b67e61b 100644 --- a/src/whir/prover/round_state/tests.rs +++ b/src/whir/prover/round_state/tests.rs @@ -6,7 +6,7 @@ use p3_dft::Radix2DFTSmallBatch; use p3_field::{Field, PrimeCharacteristicRing, extension::BinomialExtensionField}; use p3_matrix::dense::DenseMatrix; use p3_merkle_tree::{MerkleTree, MerkleTreeMmcs}; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; +use p3_multilinear_util::{evals::Poly, multilinear::Point}; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{SeedableRng, rngs::SmallRng}; @@ -87,7 +87,7 @@ fn make_test_config( #[allow(clippy::type_complexity)] fn setup_domain_and_commitment( params: &WhirConfig, - poly: EvaluationsList, + poly: Poly, ) -> ( WhirProof, MyChallenger, @@ -105,7 +105,7 @@ fn setup_domain_and_commitment( }; // Create WhirProof structure from protocol parameters - let whir_proof = WhirProof::from_protocol_parameters(&protocol_params, poly.num_variables()); + let whir_proof = WhirProof::from_protocol_parameters(&protocol_params, poly.num_vars()); // Create a new Fiat-Shamir domain separator. let mut domsep = DomainSeparator::new(vec![]); @@ -155,7 +155,7 @@ fn test_no_initial_statement_no_sumcheck() { let folding0 = config.folding_factor.at_round(0); // Define a polynomial - let poly = EvaluationsList::new(vec![F::from_u64(3); 1 << num_variables]); + let poly = Poly::new(vec![F::from_u64(3); 1 << num_variables]); // Initialize: // - domain separator for Fiat-Shamir transcript, @@ -184,7 +184,7 @@ fn test_no_initial_statement_no_sumcheck() { .unwrap(); // Folding factor was 2, so we expect 2 sampled folding randomness values - assert_eq!(state.folding_randomness.num_variables(), 2); + assert_eq!(state.folding_randomness.num_vars(), 2); // Since this is the first round, no Merkle data for folded rounds should exist assert!(state.merkle_prover_data.is_none()); @@ -214,7 +214,7 @@ fn test_initial_statement_with_folding_factor_3() { let e7 = F::from_u64(7); let e8 = F::from_u64(8); - let poly = EvaluationsList::new(vec![ + let poly = Poly::new(vec![ e1, e1 + e2, e1 + e3, @@ -245,7 +245,7 @@ fn test_initial_statement_with_folding_factor_3() { // Create an empty public statement (no constraints) let mut statement = InitialStatement::new(poly, folding0, SumcheckStrategy::default()); // Evaluate at point (1, 1, 1) - let _ = statement.evaluate(&MultilinearPoint::new(vec![EF::ONE, EF::ONE, EF::ONE])); + let _ = statement.evaluate(&Point::new(vec![EF::ONE, EF::ONE, EF::ONE])); // Run the first round state initialization (this will trigger sumcheck) let state = RoundState::< @@ -307,7 +307,7 @@ fn test_zero_poly_multiple_constraints() { let folding0 = config.folding_factor.at_round(0); // Define a zero polynomial: f(X) = 0 for all X - let poly = EvaluationsList::new(vec![F::ZERO; 1 << num_variables]); + let poly = Poly::new(vec![F::ZERO; 1 << num_variables]); // Generate domain separator, prover state, and Merkle commitment witness for the poly let (mut proof, mut challenger_rf, prover_data) = @@ -321,7 +321,7 @@ fn test_zero_poly_multiple_constraints() { let point = (0..num_variables) .map(|b| EF::from_u64(((i >> b) & 1) as u64)) .collect(); - let eval = statement.evaluate(&MultilinearPoint::new(point)); + let eval = statement.evaluate(&Point::new(point)); assert_eq!(eval, EF::ZERO); } @@ -353,12 +353,12 @@ fn test_zero_poly_multiple_constraints() { assert_eq!(sumcheck.sum, EF::ZERO); // Folding randomness should have length equal to the folding factor (1) - assert_eq!(sumcheck_randomness.num_variables(), 1); + assert_eq!(sumcheck_randomness.num_vars(), 1); // Confirm that folding randomness matches exactly assert_eq!( state.folding_randomness, - MultilinearPoint::new(vec![sumcheck_randomness[0]]) + Point::new(vec![sumcheck_randomness[0]]) ); // No Merkle commitment data for folded rounds yet @@ -391,7 +391,7 @@ fn test_initialize_round_state_with_initial_statement() { let e7 = F::from_u64(7); let e8 = F::from_u64(8); - let poly = EvaluationsList::new(vec![ + let poly = Poly::new(vec![ e1, e1 + e2, e1 + e3, @@ -421,7 +421,7 @@ fn test_initialize_round_state_with_initial_statement() { // Construct a statement with one evaluation constraint at the point (1, 0, 1) let mut statement = InitialStatement::new(poly, folding0, SumcheckStrategy::default()); - let _ = statement.evaluate(&MultilinearPoint::new(vec![EF::ONE, EF::ZERO, EF::ONE])); + let _ = statement.evaluate(&Point::new(vec![EF::ONE, EF::ZERO, EF::ONE])); // Run the first round initialization let state = RoundState::< @@ -446,10 +446,7 @@ fn test_initialize_round_state_with_initial_statement() { // Evaluate f at (32636, 9876, r0) and match it with the sumcheck's recovered evaluation let evals_f = &sumcheck.evals(); assert_eq!( - evals_f.evaluate_hypercube_ext::(&MultilinearPoint::new(vec![ - EF::from_u64(32636), - EF::from_u64(9876) - ])), + evals_f.eval_ext::(&Point::new(vec![EF::from_u64(32636), EF::from_u64(9876)])), f( sumcheck_randomness[0], EF::from_u64(32636), @@ -466,6 +463,6 @@ fn test_initialize_round_state_with_initial_statement() { // The folding randomness must match what was sampled by the sumcheck assert_eq!( state.folding_randomness, - MultilinearPoint::new(vec![sumcheck_randomness[0]]) + Point::new(vec![sumcheck_randomness[0]]) ); } diff --git a/src/whir/verifier/mod.rs b/src/whir/verifier/mod.rs index 30056032..f8b9f776 100644 --- a/src/whir/verifier/mod.rs +++ b/src/whir/verifier/mod.rs @@ -6,7 +6,7 @@ use p3_challenger::{CanObserve, FieldChallenger, GrindingChallenger}; use p3_commit::{BatchOpeningRef, ExtensionMmcs, Mmcs}; use p3_field::{ExtensionField, Field, TwoAdicField}; use p3_matrix::Dimensions; -use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; +use p3_multilinear_util::{evals::Poly, multilinear::Point}; use tracing::instrument; use super::{committer::reader::ParsedCommitment, utils::get_challenge_stir_queries}; @@ -56,7 +56,7 @@ where challenger: &mut Challenger, parsed_commitment: &ParsedCommitment, mut statement: EqStatement, - ) -> Result, VerifierError> + ) -> Result, VerifierError> where Challenger: CanObserve, { @@ -167,7 +167,7 @@ where round_folding_randomness.push(final_sumcheck_randomness.clone()); // Compute folding randomness across all rounds. - let folding_randomness = MultilinearPoint::new( + let folding_randomness = Point::new( round_folding_randomness .into_iter() .flat_map(IntoIterator::into_iter) @@ -180,7 +180,7 @@ where .eval_constraints_poly(&constraints, &point_for_eval); // Check the final sumcheck evaluation - let final_value = final_evaluations.evaluate_hypercube_ext::(&final_sumcheck_randomness); + let final_value = final_evaluations.eval_ext::(&final_sumcheck_randomness); if claimed_eval != evaluation_of_weights * final_value { return Err(VerifierError::SumcheckFailed { round: self.final_sumcheck_rounds, @@ -223,7 +223,7 @@ where challenger: &mut Challenger, params: &RoundConfig, commitment: &ParsedCommitment, - folding_randomness: &MultilinearPoint, + folding_randomness: &Point, round_index: usize, ) -> Result, VerifierError> { // CRITICAL: Verify the prover's proof-of-work before generating challenges. @@ -279,9 +279,7 @@ where // Compute STIR Constraints let folds: Vec<_> = answers .into_iter() - .map(|answer| { - EvaluationsList::new(answer).evaluate_hypercube_ext::(folding_randomness) - }) + .map(|answer| Poly::new(answer).eval_ext::(folding_randomness)) .collect(); let stir_constraints = stir_challenges_indexes From 056eb7cdfe6c9154d0f81b5d1d711cf2f26900bd Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Wed, 25 Mar 2026 01:25:30 +0100 Subject: [PATCH 2/5] cleanup --- Cargo.toml | 32 ++++++++++++++-------------- benches/eval_multilinear.rs | 2 +- benches/evaluate.rs | 2 +- benches/sumcheck.rs | 2 +- benches/whir.rs | 2 +- src/bin/main.rs | 2 +- src/whir/committer/reader.rs | 4 ++-- src/whir/committer/writer.rs | 4 ++-- src/whir/mod.rs | 2 +- src/whir/proof.rs | 2 +- src/whir/prover/mod.rs | 2 +- src/whir/prover/round_state/state.rs | 2 +- src/whir/prover/round_state/tests.rs | 2 +- src/whir/verifier/mod.rs | 2 +- 14 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0cf2709e..e4000034 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,22 +41,22 @@ serde = { version = "1.0", default-features = false, features = [ "alloc", ] } rayon = { version = "1.0", optional = true } -p3-field = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-baby-bear = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-koala-bear = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-merkle-tree = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-symmetric = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-matrix = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-commit = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-goldilocks = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-dft = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-util = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-maybe-rayon = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-interpolation = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-challenger = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-multilinear-util = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-keccak = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-whir = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-field = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/field" } +p3-baby-bear = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/baby-bear" } +p3-koala-bear = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/koala-bear" } +p3-merkle-tree = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/merkle-tree" } +p3-symmetric = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/symmetric" } +p3-matrix = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/matrix" } +p3-commit = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/commit" } +p3-goldilocks = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/goldilocks" } +p3-dft = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/dft" } +p3-util = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/util" } +p3-maybe-rayon = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/maybe-rayon" } +p3-interpolation = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/interpolation" } +p3-challenger = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/challenger" } +p3-multilinear-util = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/multilinear-util" } +p3-keccak = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/keccak" } +p3-whir = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/whir" } hashbrown = "0.16" itertools = { version = "0.14.0", default-features = false, features = [ diff --git a/benches/eval_multilinear.rs b/benches/eval_multilinear.rs index 2fb52c7c..614761d1 100644 --- a/benches/eval_multilinear.rs +++ b/benches/eval_multilinear.rs @@ -1,7 +1,7 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use p3_baby_bear::BabyBear; use p3_field::{ExtensionField, Field, extension::BinomialExtensionField}; -use p3_multilinear_util::{evals::Poly, multilinear::Point}; +use p3_multilinear_util::{point::Point, poly::Poly}; use rand::{ RngExt, SeedableRng, distr::{Distribution, StandardUniform}, diff --git a/benches/evaluate.rs b/benches/evaluate.rs index 29f7c73d..41cdb834 100644 --- a/benches/evaluate.rs +++ b/benches/evaluate.rs @@ -1,7 +1,7 @@ use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; use p3_field::extension::BinomialExtensionField; use p3_koala_bear::KoalaBear; -use p3_multilinear_util::{evals::Poly, multilinear::Point}; +use p3_multilinear_util::{point::Point, poly::Poly}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; type F = KoalaBear; diff --git a/benches/sumcheck.rs b/benches/sumcheck.rs index d72cd9f5..1e8ee1d0 100644 --- a/benches/sumcheck.rs +++ b/benches/sumcheck.rs @@ -3,7 +3,7 @@ use p3_baby_bear::{BabyBear, Poseidon2BabyBear}; use p3_challenger::{DuplexChallenger, FieldChallenger}; use p3_field::{Field, extension::BinomialExtensionField}; use p3_merkle_tree::MerkleTreeMmcs; -use p3_multilinear_util::{evals::Poly, multilinear::Point}; +use p3_multilinear_util::{point::Point, poly::Poly}; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; use whir_p3::{ diff --git a/benches/whir.rs b/benches/whir.rs index 799cd6b1..23f075c5 100644 --- a/benches/whir.rs +++ b/benches/whir.rs @@ -4,7 +4,7 @@ use p3_dft::Radix2DFTSmallBatch; use p3_field::{Field, extension::BinomialExtensionField}; use p3_koala_bear::{KoalaBear, Poseidon2KoalaBear}; use p3_merkle_tree::MerkleTreeMmcs; -use p3_multilinear_util::{evals::Poly, multilinear::Point}; +use p3_multilinear_util::{point::Point, poly::Poly}; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; use whir_p3::{ diff --git a/src/bin/main.rs b/src/bin/main.rs index 15284561..ca710d62 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -6,7 +6,7 @@ use p3_dft::Radix2DFTSmallBatch; use p3_field::{Field, extension::BinomialExtensionField}; use p3_koala_bear::{KoalaBear, Poseidon2KoalaBear}; use p3_merkle_tree::MerkleTreeMmcs; -use p3_multilinear_util::{evals::Poly, multilinear::Point}; +use p3_multilinear_util::{point::Point, poly::Poly}; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{ RngExt, SeedableRng, diff --git a/src/whir/committer/reader.rs b/src/whir/committer/reader.rs index d9bdad29..10e49f6b 100644 --- a/src/whir/committer/reader.rs +++ b/src/whir/committer/reader.rs @@ -3,7 +3,7 @@ use core::{fmt::Debug, ops::Deref}; use p3_challenger::{CanObserve, FieldChallenger, GrindingChallenger}; use p3_commit::Mmcs; use p3_field::{ExtensionField, Field, PackedValue, TwoAdicField}; -use p3_multilinear_util::multilinear::Point; +use p3_multilinear_util::point::Point; use crate::{constraints::statement::EqStatement, parameters::WhirConfig, whir::proof::WhirProof}; @@ -192,7 +192,7 @@ mod tests { use p3_dft::Radix2DFTSmallBatch; use p3_field::{Field, extension::BinomialExtensionField}; use p3_merkle_tree::MerkleTreeMmcs; - use p3_multilinear_util::evals::Poly; + use p3_multilinear_util::poly::Poly; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; diff --git a/src/whir/committer/writer.rs b/src/whir/committer/writer.rs index a930ce4b..f91efed4 100644 --- a/src/whir/committer/writer.rs +++ b/src/whir/committer/writer.rs @@ -5,7 +5,7 @@ use p3_commit::Mmcs; use p3_dft::TwoAdicSubgroupDft; use p3_field::{ExtensionField, Field, TwoAdicField}; use p3_matrix::{Matrix, dense::RowMajorMatrixView}; -use p3_multilinear_util::multilinear::Point; +use p3_multilinear_util::point::Point; use tracing::{info_span, instrument}; use crate::{ @@ -128,7 +128,7 @@ mod tests { use p3_challenger::DuplexChallenger; use p3_dft::Radix2DFTSmallBatch; use p3_merkle_tree::MerkleTreeMmcs; - use p3_multilinear_util::evals::Poly; + use p3_multilinear_util::poly::Poly; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; diff --git a/src/whir/mod.rs b/src/whir/mod.rs index dd00d4a5..c14af0db 100644 --- a/src/whir/mod.rs +++ b/src/whir/mod.rs @@ -14,7 +14,7 @@ mod test { use p3_dft::Radix2DFTSmallBatch; use p3_field::{Field, extension::BinomialExtensionField}; use p3_merkle_tree::MerkleTreeMmcs; - use p3_multilinear_util::{evals::Poly, multilinear::Point}; + use p3_multilinear_util::{point::Point, poly::Poly}; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; diff --git a/src/whir/proof.rs b/src/whir/proof.rs index 89130546..fe2e12f7 100644 --- a/src/whir/proof.rs +++ b/src/whir/proof.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use p3_commit::Mmcs; -use p3_multilinear_util::evals::Poly; +use p3_multilinear_util::poly::Poly; use serde::{Deserialize, Serialize}; use crate::parameters::ProtocolParameters; diff --git a/src/whir/prover/mod.rs b/src/whir/prover/mod.rs index 4248cc30..2c6fd57b 100644 --- a/src/whir/prover/mod.rs +++ b/src/whir/prover/mod.rs @@ -10,7 +10,7 @@ use p3_matrix::{ dense::{DenseMatrix, RowMajorMatrixView}, extension::FlatMatrixView, }; -use p3_multilinear_util::{evals::Poly, multilinear::Point}; +use p3_multilinear_util::{point::Point, poly::Poly}; use round_state::RoundState; use tracing::{info_span, instrument}; diff --git a/src/whir/prover/round_state/state.rs b/src/whir/prover/round_state/state.rs index 84cf7b1c..c323f53a 100644 --- a/src/whir/prover/round_state/state.rs +++ b/src/whir/prover/round_state/state.rs @@ -4,7 +4,7 @@ use p3_challenger::{FieldChallenger, GrindingChallenger}; use p3_field::{ExtensionField, TwoAdicField}; -use p3_multilinear_util::multilinear::Point; +use p3_multilinear_util::point::Point; use tracing::instrument; use crate::{ diff --git a/src/whir/prover/round_state/tests.rs b/src/whir/prover/round_state/tests.rs index 2b67e61b..0db05f47 100644 --- a/src/whir/prover/round_state/tests.rs +++ b/src/whir/prover/round_state/tests.rs @@ -6,7 +6,7 @@ use p3_dft::Radix2DFTSmallBatch; use p3_field::{Field, PrimeCharacteristicRing, extension::BinomialExtensionField}; use p3_matrix::dense::DenseMatrix; use p3_merkle_tree::{MerkleTree, MerkleTreeMmcs}; -use p3_multilinear_util::{evals::Poly, multilinear::Point}; +use p3_multilinear_util::{point::Point, poly::Poly}; use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{SeedableRng, rngs::SmallRng}; diff --git a/src/whir/verifier/mod.rs b/src/whir/verifier/mod.rs index f8b9f776..3abcc409 100644 --- a/src/whir/verifier/mod.rs +++ b/src/whir/verifier/mod.rs @@ -6,7 +6,7 @@ use p3_challenger::{CanObserve, FieldChallenger, GrindingChallenger}; use p3_commit::{BatchOpeningRef, ExtensionMmcs, Mmcs}; use p3_field::{ExtensionField, Field, TwoAdicField}; use p3_matrix::Dimensions; -use p3_multilinear_util::{evals::Poly, multilinear::Point}; +use p3_multilinear_util::{point::Point, poly::Poly}; use tracing::instrument; use super::{committer::reader::ParsedCommitment, utils::get_challenge_stir_queries}; From aed2299269b97e7620119cb0e03ba55bbcb43ea5 Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Wed, 25 Mar 2026 01:29:07 +0100 Subject: [PATCH 3/5] fix --- Cargo.toml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e4000034..0cf2709e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,22 +41,22 @@ serde = { version = "1.0", default-features = false, features = [ "alloc", ] } rayon = { version = "1.0", optional = true } -p3-field = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/field" } -p3-baby-bear = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/baby-bear" } -p3-koala-bear = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/koala-bear" } -p3-merkle-tree = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/merkle-tree" } -p3-symmetric = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/symmetric" } -p3-matrix = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/matrix" } -p3-commit = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/commit" } -p3-goldilocks = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/goldilocks" } -p3-dft = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/dft" } -p3-util = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/util" } -p3-maybe-rayon = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/maybe-rayon" } -p3-interpolation = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/interpolation" } -p3-challenger = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/challenger" } -p3-multilinear-util = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/multilinear-util" } -p3-keccak = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/keccak" } -p3-whir = { path = "/Users/tcoratger/Documents/plonky3/Plonky3/whir" } +p3-field = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-baby-bear = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-koala-bear = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-merkle-tree = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-symmetric = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-matrix = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-commit = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-goldilocks = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-dft = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-util = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-maybe-rayon = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-interpolation = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-challenger = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-multilinear-util = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-keccak = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-whir = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } hashbrown = "0.16" itertools = { version = "0.14.0", default-features = false, features = [ From d870c994a5d6c0616884f34535ec7201eebf8ea8 Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Wed, 25 Mar 2026 16:00:19 +0100 Subject: [PATCH 4/5] update plonky3 --- Cargo.toml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0cf2709e..4cbe79bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,22 +41,22 @@ serde = { version = "1.0", default-features = false, features = [ "alloc", ] } rayon = { version = "1.0", optional = true } -p3-field = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-baby-bear = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-koala-bear = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-merkle-tree = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-symmetric = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-matrix = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-commit = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-goldilocks = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-dft = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-util = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-maybe-rayon = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-interpolation = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-challenger = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-multilinear-util = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-keccak = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } -p3-whir = { git = "https://github.com/tcoratger/Plonky3", branch = "ml-refactor1" } +p3-field = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-baby-bear = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-koala-bear = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-merkle-tree = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-symmetric = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-matrix = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-commit = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-goldilocks = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-dft = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-util = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-maybe-rayon = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-interpolation = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-challenger = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-multilinear-util = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-keccak = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-whir = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } hashbrown = "0.16" itertools = { version = "0.14.0", default-features = false, features = [ From 75be0cb75beeaa989e3796f34e6bb8b9990d277d Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Tue, 31 Mar 2026 23:29:57 +0200 Subject: [PATCH 5/5] bump p3 --- Cargo.toml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4cbe79bc..7193c7f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,22 +41,22 @@ serde = { version = "1.0", default-features = false, features = [ "alloc", ] } rayon = { version = "1.0", optional = true } -p3-field = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-baby-bear = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-koala-bear = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-merkle-tree = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-symmetric = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-matrix = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-commit = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-goldilocks = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-dft = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-util = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-maybe-rayon = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-interpolation = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-challenger = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-multilinear-util = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-keccak = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } -p3-whir = { git = "https://github.com/kilic/Plonky3", branch = "ml-refactor" } +p3-field = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-baby-bear = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-koala-bear = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-merkle-tree = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-symmetric = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-matrix = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-commit = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-goldilocks = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-dft = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-util = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-maybe-rayon = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-interpolation = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-challenger = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-multilinear-util = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-keccak = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } +p3-whir = { git = "https://github.com/Plonky3/Plonky3", branch = "main" } hashbrown = "0.16" itertools = { version = "0.14.0", default-features = false, features = [