From 02f0cc1b68c7072e1e69a6b00778f2cfd6fab39c Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Tue, 17 Mar 2026 16:33:28 +0100 Subject: [PATCH 1/2] core: unify parameters --- benches/sumcheck.rs | 3 +- benches/whir.rs | 10 +- src/bin/main.rs | 6 +- src/fiat_shamir/domain_separator.rs | 3 +- src/parameters/folding.rs | 264 +++++++++++++++++ src/parameters/mod.rs | 272 +----------------- src/parameters/{errors.rs => soundness.rs} | 0 .../parameters.rs => parameters/whir.rs} | 6 +- src/sumcheck/data.rs | 7 +- src/sumcheck/tests.rs | 3 +- src/whir/committer/reader.rs | 11 +- src/whir/committer/writer.rs | 7 +- src/whir/constraints/statement/initial.rs | 3 +- src/whir/mod.rs | 8 +- src/whir/proof.rs | 2 +- src/whir/prover/mod.rs | 3 +- src/whir/prover/round_state/tests.rs | 5 +- src/whir/verifier/mod.rs | 6 +- 18 files changed, 307 insertions(+), 312 deletions(-) create mode 100644 src/parameters/folding.rs rename src/parameters/{errors.rs => soundness.rs} (100%) rename src/{whir/parameters.rs => parameters/whir.rs} (99%) diff --git a/benches/sumcheck.rs b/benches/sumcheck.rs index ac94e443..e5db0923 100644 --- a/benches/sumcheck.rs +++ b/benches/sumcheck.rs @@ -8,11 +8,10 @@ use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; use whir_p3::{ fiat_shamir::domain_separator::DomainSeparator, - parameters::{FoldingFactor, ProtocolParameters, errors::SecurityAssumption}, + parameters::{FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy}, sumcheck::prover::SumcheckProver, whir::{ constraints::statement::initial::InitialStatement, - parameters::SumcheckStrategy, proof::{SumcheckData, WhirProof}, }, }; diff --git a/benches/whir.rs b/benches/whir.rs index 2cb8ff4c..ffdc9edb 100644 --- a/benches/whir.rs +++ b/benches/whir.rs @@ -9,13 +9,11 @@ use p3_symmetric::{PaddingFreeSponge, TruncatedPermutation}; use rand::{RngExt, SeedableRng, rngs::SmallRng}; use whir_p3::{ fiat_shamir::domain_separator::DomainSeparator, - parameters::{DEFAULT_MAX_POW, FoldingFactor, ProtocolParameters, errors::SecurityAssumption}, - whir::{ - committer::writer::CommitmentWriter, - parameters::{SumcheckStrategy, WhirConfig}, - proof::WhirProof, - prover::Prover, + parameters::{ + DEFAULT_MAX_POW, FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy, + WhirConfig, }, + whir::{committer::writer::CommitmentWriter, proof::WhirProof, prover::Prover}, }; type F = KoalaBear; diff --git a/src/bin/main.rs b/src/bin/main.rs index 74beac40..998fd3e4 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -16,10 +16,12 @@ use tracing_forest::{ForestLayer, util::LevelFilter}; use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt, util::SubscriberInitExt}; use whir_p3::{ fiat_shamir::domain_separator::DomainSeparator, - parameters::{DEFAULT_MAX_POW, FoldingFactor, ProtocolParameters, errors::SecurityAssumption}, + parameters::{ + DEFAULT_MAX_POW, FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy, + WhirConfig, + }, whir::{ committer::{reader::CommitmentReader, writer::CommitmentWriter}, - parameters::{SumcheckStrategy, WhirConfig}, proof::WhirProof, prover::Prover, verifier::Verifier, diff --git a/src/fiat_shamir/domain_separator.rs b/src/fiat_shamir/domain_separator.rs index bddfd925..24df40b8 100644 --- a/src/fiat_shamir/domain_separator.rs +++ b/src/fiat_shamir/domain_separator.rs @@ -9,8 +9,7 @@ use p3_field::{ExtensionField, Field, TwoAdicField}; use crate::{ fiat_shamir::pattern::{Hint, Observe, Pattern, Sample}, - parameters::FoldingFactor, - whir::parameters::WhirConfig, + parameters::{FoldingFactor, WhirConfig}, }; /// Configuration for a sumcheck phase in the protocol. diff --git a/src/parameters/folding.rs b/src/parameters/folding.rs new file mode 100644 index 00000000..426166be --- /dev/null +++ b/src/parameters/folding.rs @@ -0,0 +1,264 @@ +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 index 49dd01ca..6d440477 100644 --- a/src/parameters/mod.rs +++ b/src/parameters/mod.rs @@ -1,147 +1,15 @@ use core::fmt::Display; -use errors::SecurityAssumption; -use thiserror::Error; +mod folding; +pub mod soundness; +pub mod whir; -pub mod errors; - -/// Each WHIR steps folds the polymomial, which reduces the number of variables. -/// As soon as the number of variables is less than or equal to `MAX_NUM_VARIABLES_TO_SEND_COEFFS`, -/// the prover sends directly the coefficients of the polynomial. -const MAX_NUM_VARIABLES_TO_SEND_COEFFS: usize = 6; +pub use folding::{FoldingFactor, FoldingFactorError}; +pub use soundness::SecurityAssumption; +pub use whir::{RoundConfig, SumcheckStrategy, WhirConfig}; pub const DEFAULT_MAX_POW: usize = 16; -/// 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 - } - } - } -} - /// Configuration parameters for WHIR proofs. #[derive(Clone, Debug)] pub struct ProtocolParameters { @@ -179,131 +47,3 @@ impl Display for ProtocolParameters { ) } } - -#[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/errors.rs b/src/parameters/soundness.rs similarity index 100% rename from src/parameters/errors.rs rename to src/parameters/soundness.rs diff --git a/src/whir/parameters.rs b/src/parameters/whir.rs similarity index 99% rename from src/whir/parameters.rs rename to src/parameters/whir.rs index 7c24337f..c3383970 100644 --- a/src/whir/parameters.rs +++ b/src/parameters/whir.rs @@ -6,10 +6,8 @@ use p3_commit::Mmcs; use p3_field::{ExtensionField, Field, TwoAdicField}; use p3_multilinear_util::evals::EvaluationsList; -use crate::{ - parameters::{FoldingFactor, ProtocolParameters, errors::SecurityAssumption}, - whir::constraints::statement::initial::InitialStatement, -}; +use super::{FoldingFactor, ProtocolParameters, SecurityAssumption}; +use crate::whir::constraints::statement::initial::InitialStatement; /// Configuration for the initial phase of the WHIR protocol. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/sumcheck/data.rs b/src/sumcheck/data.rs index 749a6100..80ed1430 100644 --- a/src/sumcheck/data.rs +++ b/src/sumcheck/data.rs @@ -180,12 +180,9 @@ mod tests { use super::*; use crate::{ fiat_shamir::domain_separator::{DomainSeparator, SumcheckParams}, - parameters::{FoldingFactor, ProtocolParameters, errors::SecurityAssumption}, + parameters::{FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy}, sumcheck::prover::SumcheckProver, - whir::{ - constraints::statement::initial::InitialStatement, parameters::SumcheckStrategy, - proof::WhirProof, - }, + whir::{constraints::statement::initial::InitialStatement, proof::WhirProof}, }; /// Type alias for the base field used in tests diff --git a/src/sumcheck/tests.rs b/src/sumcheck/tests.rs index 1b0db548..92a60a63 100644 --- a/src/sumcheck/tests.rs +++ b/src/sumcheck/tests.rs @@ -10,7 +10,7 @@ use rand::{RngExt, SeedableRng, rngs::SmallRng}; use crate::{ fiat_shamir::domain_separator::DomainSeparator, - parameters::{FoldingFactor, ProtocolParameters, errors::SecurityAssumption}, + parameters::{FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy}, sumcheck::{SumcheckData, prover::SumcheckProver, verify_final_sumcheck_rounds}, whir::{ constraints::{ @@ -18,7 +18,6 @@ use crate::{ evaluator::ConstraintPolyEvaluator, statement::{EqStatement, SelectStatement, initial::InitialStatement}, }, - parameters::SumcheckStrategy, proof::WhirProof, }, }; diff --git a/src/whir/committer/reader.rs b/src/whir/committer/reader.rs index acd42808..69635cc5 100644 --- a/src/whir/committer/reader.rs +++ b/src/whir/committer/reader.rs @@ -5,7 +5,10 @@ use p3_commit::Mmcs; use p3_field::{ExtensionField, Field, PackedValue, TwoAdicField}; use p3_multilinear_util::multilinear::MultilinearPoint; -use crate::whir::{constraints::statement::EqStatement, parameters::WhirConfig, proof::WhirProof}; +use crate::{ + parameters::WhirConfig, + whir::{constraints::statement::EqStatement, proof::WhirProof}, +}; /// Represents a parsed commitment from the prover in the WHIR protocol. /// @@ -199,10 +202,8 @@ mod tests { use super::*; use crate::{ fiat_shamir::domain_separator::DomainSeparator, - parameters::{FoldingFactor, ProtocolParameters, errors::SecurityAssumption}, - whir::{ - committer::writer::CommitmentWriter, parameters::SumcheckStrategy, proof::WhirProof, - }, + parameters::{FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy}, + whir::{committer::writer::CommitmentWriter, proof::WhirProof}, }; type F = BabyBear; diff --git a/src/whir/committer/writer.rs b/src/whir/committer/writer.rs index 701857a3..799d727a 100644 --- a/src/whir/committer/writer.rs +++ b/src/whir/committer/writer.rs @@ -10,9 +10,9 @@ use tracing::{info_span, instrument}; use crate::{ fiat_shamir::errors::FiatShamirError, + parameters::WhirConfig, whir::{ - committer::DenseMatrix, constraints::statement::initial::InitialStatement, - parameters::WhirConfig, proof::WhirProof, + committer::DenseMatrix, constraints::statement::initial::InitialStatement, proof::WhirProof, }, }; @@ -136,8 +136,7 @@ mod tests { use super::*; use crate::{ fiat_shamir::domain_separator::DomainSeparator, - parameters::{FoldingFactor, ProtocolParameters, errors::SecurityAssumption}, - whir::parameters::SumcheckStrategy, + parameters::{FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy}, }; type F = BabyBear; diff --git a/src/whir/constraints/statement/initial.rs b/src/whir/constraints/statement/initial.rs index 0e14369e..1c3b7786 100644 --- a/src/whir/constraints/statement/initial.rs +++ b/src/whir/constraints/statement/initial.rs @@ -23,8 +23,7 @@ use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint} use p3_util::log2_strict_usize; use crate::{ - sumcheck::svo::SplitEq, - whir::{constraints::statement::EqStatement, parameters::SumcheckStrategy}, + parameters::SumcheckStrategy, sumcheck::svo::SplitEq, whir::constraints::statement::EqStatement, }; /// Manages the polynomial and constraints during the initial phase of WHIR. diff --git a/src/whir/mod.rs b/src/whir/mod.rs index 984f0c8b..9d54fe56 100644 --- a/src/whir/mod.rs +++ b/src/whir/mod.rs @@ -1,6 +1,5 @@ pub mod committer; pub mod constraints; -pub mod parameters; pub mod proof; pub mod prover; pub mod utils; @@ -22,10 +21,11 @@ mod test { use crate::{ fiat_shamir::domain_separator::DomainSeparator, - parameters::{FoldingFactor, ProtocolParameters, errors::SecurityAssumption}, + parameters::{ + FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy, WhirConfig, + }, whir::{ committer::{reader::CommitmentReader, writer::CommitmentWriter}, - parameters::{SumcheckStrategy, WhirConfig}, proof::WhirProof, prover::Prover, verifier::Verifier, @@ -245,7 +245,7 @@ mod test { use rand::{RngExt, SeedableRng, rngs::SmallRng}; use super::*; - use crate::whir::parameters::WhirConfig; + use crate::parameters::WhirConfig; // Field types for Keccak tests type F = KoalaBear; diff --git a/src/whir/proof.rs b/src/whir/proof.rs index e9a087c4..3c5224ea 100644 --- a/src/whir/proof.rs +++ b/src/whir/proof.rs @@ -205,7 +205,7 @@ mod tests { use rand::SeedableRng; use super::*; - use crate::parameters::{FoldingFactor, errors::SecurityAssumption}; + use crate::parameters::{FoldingFactor, SecurityAssumption}; /// Type alias for the base field used in tests type F = BabyBear; diff --git a/src/whir/prover/mod.rs b/src/whir/prover/mod.rs index 552319d5..45b62064 100644 --- a/src/whir/prover/mod.rs +++ b/src/whir/prover/mod.rs @@ -14,9 +14,10 @@ use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint} use round_state::RoundState; use tracing::{info_span, instrument}; -use super::{constraints::statement::EqStatement, parameters::WhirConfig}; +use super::constraints::statement::EqStatement; use crate::{ fiat_shamir::errors::FiatShamirError, + parameters::WhirConfig, whir::{ constraints::{ Constraint, diff --git a/src/whir/prover/round_state/tests.rs b/src/whir/prover/round_state/tests.rs index 62e369d9..0de2c7fe 100644 --- a/src/whir/prover/round_state/tests.rs +++ b/src/whir/prover/round_state/tests.rs @@ -12,11 +12,12 @@ use rand::{SeedableRng, rngs::SmallRng}; use crate::{ fiat_shamir::domain_separator::DomainSeparator, - parameters::{FoldingFactor, ProtocolParameters, errors::SecurityAssumption}, + parameters::{ + FoldingFactor, ProtocolParameters, SecurityAssumption, SumcheckStrategy, WhirConfig, + }, whir::{ committer::{ProverData, ProverDataExt, writer::CommitmentWriter}, constraints::statement::initial::InitialStatement, - parameters::{SumcheckStrategy, WhirConfig}, proof::WhirProof, prover::round_state::RoundState, }, diff --git a/src/whir/verifier/mod.rs b/src/whir/verifier/mod.rs index d53f9f61..3c963eff 100644 --- a/src/whir/verifier/mod.rs +++ b/src/whir/verifier/mod.rs @@ -9,11 +9,10 @@ use p3_matrix::Dimensions; use p3_multilinear_util::{evals::EvaluationsList, multilinear::MultilinearPoint}; use tracing::instrument; -use super::{ - committer::reader::ParsedCommitment, parameters::RoundConfig, utils::get_challenge_stir_queries, -}; +use super::{committer::reader::ParsedCommitment, utils::get_challenge_stir_queries}; use crate::{ alloc::string::ToString, + parameters::{RoundConfig, WhirConfig}, sumcheck::verify_final_sumcheck_rounds, whir::{ constraints::{ @@ -21,7 +20,6 @@ use crate::{ evaluator::ConstraintPolyEvaluator, statement::{EqStatement, SelectStatement}, }, - parameters::WhirConfig, proof::{QueryOpening, WhirProof}, }, }; From af0b07ce9e11bb60396e4589ce1dd0b083f48fab Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Tue, 17 Mar 2026 17:25:04 +0100 Subject: [PATCH 2/2] documentation --- src/parameters/folding.rs | 2 + src/parameters/mod.rs | 7 + src/parameters/soundness.rs | 121 +++++++++++++++ src/parameters/whir.rs | 297 +++++++++++++++++++----------------- 4 files changed, 290 insertions(+), 137 deletions(-) diff --git a/src/parameters/folding.rs b/src/parameters/folding.rs index 426166be..a932e65f 100644 --- a/src/parameters/folding.rs +++ b/src/parameters/folding.rs @@ -1,3 +1,5 @@ +//! Folding factor strategies for variable reduction across WHIR rounds. + use thiserror::Error; /// Each WHIR step folds the polynomial, reducing the number of variables. diff --git a/src/parameters/mod.rs b/src/parameters/mod.rs index 6d440477..58ba46ee 100644 --- a/src/parameters/mod.rs +++ b/src/parameters/mod.rs @@ -1,3 +1,5 @@ +//! Protocol configuration for WHIR polynomial commitments. + use core::fmt::Display; mod folding; @@ -8,6 +10,11 @@ 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. diff --git a/src/parameters/soundness.rs b/src/parameters/soundness.rs index 333b2f9f..d52a52bb 100644 --- a/src/parameters/soundness.rs +++ b/src/parameters/soundness.rs @@ -1,3 +1,5 @@ +//! 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}; @@ -253,6 +255,109 @@ impl SecurityAssumption { 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 { @@ -427,4 +532,20 @@ mod tests { 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 index c3383970..ad9aac56 100644 --- a/src/parameters/whir.rs +++ b/src/parameters/whir.rs @@ -1,3 +1,5 @@ +//! Derived WHIR protocol configuration computed from user-facing parameters. + use alloc::vec::Vec; use core::marker::PhantomData; @@ -9,7 +11,7 @@ use p3_multilinear_util::evals::EvaluationsList; use super::{FoldingFactor, ProtocolParameters, SecurityAssumption}; use crate::whir::constraints::statement::initial::InitialStatement; -/// Configuration for the initial phase of the WHIR protocol. +/// 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). @@ -26,45 +28,81 @@ pub enum SumcheckStrategy { 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, } @@ -75,46 +113,76 @@ where 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 { - // We need to store the initial number of variables for the final composition. + // --------------------------------------------------------------- + // 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; - // We could theorically tolerate a bigger `log_folded_domain_size` (up to EF::TWO_ADICITY), but this would reduce performance: - // 1) Because the FFT twiddle factors would be in the Extension Field - // 2) Because all the equality polynomials from WHIR queries would be in the Extension Field + // --------------------------------------------------------------- + // Phase 2: Two-adicity guard. + // --------------------------------------------------------------- // - // Note that this does not restrict the amount of data committed, as long as folding_factor_0 > EF::TWO_ADICITY - F::TWO_ADICITY + // 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, @@ -122,30 +190,52 @@ where field_size_bits, ); - let starting_folding_pow_bits = Self::folding_pow_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, - whir_parameters.soundness_type, 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 { - // Queries are set w.r.t. to old rate, while the rest to the new rate + // 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, @@ -153,11 +243,14 @@ where 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 = Self::rbr_soundness_queries_combination( - whir_parameters.soundness_type, + let combination_error = whir_parameters.soundness_type.queries_combination_error( field_size_bits, num_variables, next_rate, @@ -165,18 +258,22 @@ where 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))); - let folding_pow_bits = Self::folding_pow_bits( + // 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, - whir_parameters.soundness_type, 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); @@ -191,15 +288,23 @@ where 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 @@ -207,6 +312,8 @@ where .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); @@ -252,10 +359,15 @@ where 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 @@ -270,6 +382,10 @@ where 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; @@ -287,109 +403,6 @@ where .all(|r| r.pow_bits <= max_bits && r.folding_pow_bits <= max_bits) } - /// Compute the proximity gaps term of the fold. - /// - /// # 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 provides tighter bounds on the number of exceptional z's: - /// - **Old [BCI+20]**: `a = O(n^2/η^7)` - /// - **New [BCSS25]**: `a = O(n/η^5)` - /// - /// With `η = √ρ/20` and `m = 10`, the error becomes: - /// ```text - /// log_2(a) = log_2(n) + 1.5·log_inv_rate + 16.38 - /// = num_variables + 2.5·log_inv_rate + 16.38 - /// ``` - #[must_use] - pub fn rbr_soundness_fold_prox_gaps( - soundness_type: SecurityAssumption, - field_size_bits: usize, - num_variables: usize, - log_inv_rate: usize, - log_eta: f64, - ) -> f64 { - // Recall, at each round we are only folding by two at a time - let error = match soundness_type { - SecurityAssumption::CapacityBound => (num_variables + log_inv_rate) as f64 - log_eta, - - // From Theorem 1.5 in [BCSS25] "On Proximity Gaps for Reed–Solomon Codes": - // - // With η = √ρ/20, m = 10, the dominant term is: - // a ≈ (2 · 10.5^5) / (3 · ρ^(3/2)) · n - // - // In log form (n = 2^(num_variables + log_inv_rate)): - // log_2(a) = num_variables + log_inv_rate + 16.38 + 1.5·log_inv_rate - // = num_variables + 2.5·log_inv_rate + 16.38 - // - // This improves over the old formula: - // log_2(a) = 2·num_variables + 3.5·log_inv_rate + LOG2_10 - SecurityAssumption::JohnsonBound => { - // Constant from (2 · 10.5^5 / 3) - let constant = libm::log2(2. * libm::pow(10.5, 5.) / 3.); - num_variables as f64 + 2.5 * log_inv_rate as f64 + constant - } - - SecurityAssumption::UniqueDecoding => (num_variables + log_inv_rate) as f64, - }; - - field_size_bits as f64 - error - } - - #[must_use] - pub const fn rbr_soundness_fold_sumcheck( - soundness_type: SecurityAssumption, - field_size_bits: usize, - num_variables: usize, - log_inv_rate: usize, - ) -> f64 { - let list_size = soundness_type.list_size_bits(num_variables, log_inv_rate); - - field_size_bits as f64 - (list_size + 1.) - } - - #[must_use] - pub fn folding_pow_bits( - security_level: usize, - soundness_type: SecurityAssumption, - field_size_bits: usize, - num_variables: usize, - log_inv_rate: usize, - ) -> f64 { - let prox_gaps_error = - soundness_type.prox_gaps_error(num_variables, log_inv_rate, field_size_bits, 2); - let sumcheck_error = Self::rbr_soundness_fold_sumcheck( - soundness_type, - field_size_bits, - num_variables, - log_inv_rate, - ); - - let error = prox_gaps_error.min(sumcheck_error); - - 0_f64.max(security_level as f64 - error) - } - - #[must_use] - pub fn rbr_soundness_queries_combination( - soundness_type: SecurityAssumption, - field_size_bits: usize, - num_variables: usize, - log_inv_rate: usize, - ood_samples: usize, - num_queries: usize, - ) -> f64 { - let list_size = soundness_type.list_size_bits(num_variables, log_inv_rate); - - let log_combination = libm::log2((ood_samples + num_queries) as f64); - - field_size_bits as f64 - (log_combination + list_size + 1.) - } - /// Compute the synthetic or derived `RoundConfig` for the final phase. /// /// - If no folding rounds were configured, constructs a fallback config @@ -401,7 +414,9 @@ where /// ensuring consistent challenge selection and STIR constraint handling. pub fn final_round_config(&self) -> RoundConfig { if self.round_parameters.is_empty() { - // Fallback: no folding rounds, use initial domain setup + // 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()), @@ -411,41 +426,66 @@ where folded_domain_gen: F::two_adic_generator( self.starting_domain_size().ilog2() as usize - self.folding_factor.at_round(0), ), - ood_samples: 0, // no OOD in synthetic final phase + 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()); - // Derive final round config from last round, adjusted for next fold 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 } - // Creates the empty initial statement for the WHIR protocol + /// 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, @@ -516,23 +556,6 @@ mod tests { assert_eq!(config.n_rounds(), config.round_parameters.len()); } - #[test] - fn test_folding_pow_bits() { - let field_size_bits = 64; - let soundness = SecurityAssumption::CapacityBound; - - let pow_bits = WhirConfig::::folding_pow_bits( - 100, // Security level - soundness, - field_size_bits, - 10, // Number of variables - 5, // Log inverse rate - ); - - // PoW bits should never be negative - assert!(pow_bits >= 0.); - } - #[test] fn test_check_pow_bits_within_limits() { let params = default_whir_params();