Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
491fa88
Adding support for WHIR batching
yswami-tfh Mar 27, 2025
2892882
Merge branch 'batching'
yswami-tfh Apr 3, 2025
c4e3f7b
Changes after merge
yswami-tfh Apr 3, 2025
91af4da
Merge branch 'main' of github.com:yswami-tfh/whir
yswami-tfh Apr 6, 2025
146f101
Merge branch 'main' of github.com:yswami-tfh/whir
yswami-tfh Apr 7, 2025
8c6c996
Refactord code based on code review
yswami-tfh Apr 9, 2025
7af00da
Merge branch 'main' of github.com:yswami-tfh/whir
yswami-tfh Apr 9, 2025
babccf0
Merge branch 'main' into batching
yswami-tfh Apr 9, 2025
8ad4b48
Fix batching test case
yswami-tfh Apr 9, 2025
119c576
Added OOD for batching and removed index based array access
yswami-tfh Apr 13, 2025
f592525
Merge branch 'main' of github.com:yswami-tfh/whir
yswami-tfh Apr 23, 2025
b472dfd
Merge branch 'main' into batching
yswami-tfh Apr 23, 2025
b49625a
Remove code from batching file to Prover
yswami-tfh Apr 23, 2025
97218f5
fix build
veljkovranic Jun 30, 2025
8b88dfe
expose batching_randomness to public
veljkovranic Jul 16, 2025
6bb9157
Merge branch 'main' into remco/batching
veljkovranic Jul 17, 2025
8979c68
merge main, fix tests
veljkovranic Jul 17, 2025
c6de34c
remove dead code
veljkovranic Jul 17, 2025
12b2fd7
merge latest main
veljkovranic Jul 30, 2025
87e6cf1
Merge branch 'main' into veljko/batching
veljkovranic Jul 30, 2025
9200476
first try at combining merkle trees into one
veljkovranic Aug 22, 2025
f41a6cc
cleanup
veljkovranic Aug 22, 2025
04d549b
wrong type
veljkovranic Aug 22, 2025
38e07be
Merge pull request #192 from WizardOfMenlo/combine_merkle_trees
veljkovranic Aug 25, 2025
56ed6ef
code review
veljkovranic Aug 25, 2025
68e60e4
fixes to match code review
veljkovranic Aug 25, 2025
a19259d
CI
veljkovranic Aug 25, 2025
e76b6d9
fix CI
veljkovranic Aug 25, 2025
f03303a
ci?
veljkovranic Aug 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benches/expand_from_coeff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn interleaved_rs_encode(bencher: Bencher, case: &(u32, usize, usize)) {
(coeffs, expansion, coset_sz)
})
.bench_values(|(coeffs, expansion, coset_sz)| {
black_box(ntt::interleaved_rs_encode(&coeffs, expansion, coset_sz))
black_box(ntt::interleaved_rs_encode(&[coeffs], expansion, coset_sz))
});
}

Expand Down
1 change: 1 addition & 0 deletions src/bin/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ fn run_whir<F, MerkleConfig>(
soundness_type,
_pow_parameters: Default::default(),
starting_log_inv_rate: starting_rate,
batch_size: 1,
};

let polynomial = CoefficientList::new(
Expand Down
2 changes: 2 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ fn run_whir_as_ldt<F, MerkleConfig>(
soundness_type,
_pow_parameters: Default::default(),
starting_log_inv_rate: starting_rate,
batch_size: 1,
};

let params = WhirConfig::<F, MerkleConfig, PowStrategy>::new(mv_params, whir_params);
Expand Down Expand Up @@ -372,6 +373,7 @@ fn run_whir_pcs<F, MerkleConfig>(
soundness_type,
_pow_parameters: Default::default(),
starting_log_inv_rate: starting_rate,
batch_size: 1,
};

let params = WhirConfig::<F, MerkleConfig, PowStrategy>::new(mv_params, whir_params);
Expand Down
20 changes: 11 additions & 9 deletions src/fs_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,23 @@ pub trait OODDomainSeparator<F: Field> {
/// - A corresponding response labeled `"ood_ans"` with `num_samples` elements.
/// - If `num_samples == 0`, the DomainSeparator remains unchanged.
#[must_use]
fn add_ood(self, num_samples: usize) -> Self;
fn add_ood(self, num_samples: usize, batch_size: usize) -> Self;
}

impl<F, DomainSeparator> OODDomainSeparator<F> for DomainSeparator
where
F: Field,
DomainSeparator: FieldDomainSeparator<F>,
{
fn add_ood(self, num_samples: usize) -> Self {
if num_samples > 0 {
self.challenge_scalars(num_samples, "ood_query")
.add_scalars(num_samples, "ood_ans")
} else {
self
fn add_ood(mut self, num_samples: usize, batch_size: usize) -> Self {
if num_samples > 0 && batch_size > 0 {
self = self.challenge_scalars(num_samples, "ood_query");

for i in 0..batch_size {
self = self.add_scalars(num_samples, &format!("ood_ans_{i}"));
}
}
self
}
}

Expand Down Expand Up @@ -69,7 +71,7 @@ mod tests {

// Apply OOD query addition
let updated_domainsep =
<DomainSeparator as OODDomainSeparator<Field64>>::add_ood(domainsep.clone(), 3);
<DomainSeparator as OODDomainSeparator<Field64>>::add_ood(domainsep.clone(), 3, 1);

// Convert to a string for inspection
let pattern_str = String::from_utf8(updated_domainsep.as_bytes().to_vec()).unwrap();
Expand All @@ -80,7 +82,7 @@ mod tests {

// Test case where num_samples = 0 (should not modify anything)
let unchanged_domainsep =
<DomainSeparator as OODDomainSeparator<Field64>>::add_ood(domainsep, 0);
<DomainSeparator as OODDomainSeparator<Field64>>::add_ood(domainsep, 0, 1);
let unchanged_str = String::from_utf8(unchanged_domainsep.as_bytes().to_vec()).unwrap();
assert_eq!(unchanged_str, "test_protocol"); // Should remain the same
}
Expand Down
17 changes: 10 additions & 7 deletions src/ntt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ pub use self::{
transpose::transpose,
wavelet::{inverse_wavelet_transform, wavelet_transform},
};

///
/// RS encode interleaved data `interleaved_coeffs` at the rate
/// 1/`expansion`, where 2^`fold_factor` elements are interleaved
Expand All @@ -31,22 +30,26 @@ pub use self::{
///
#[cfg_attr(feature = "tracing", instrument(skip(interleaved_coeffs), fields(size = interleaved_coeffs.len())))]
pub fn interleaved_rs_encode<F: FftField>(
interleaved_coeffs: &[F],
interleaved_coeffs: &[Vec<F>],
expansion: usize,
fold_factor: usize,
) -> Vec<F> {
let num_coeffs = interleaved_coeffs.first().unwrap().len();
let fold_factor = u32::try_from(fold_factor).unwrap();
debug_assert!(expansion > 0);
debug_assert!(interleaved_coeffs.len().is_power_of_two());
debug_assert!(num_coeffs.is_power_of_two());

let fold_factor_exp = 2usize.pow(fold_factor);
let expanded_size = interleaved_coeffs.len() * expansion;
let expanded_size = num_coeffs * expansion;

debug_assert_eq!(expanded_size % fold_factor_exp, 0);

// 1. Create zero-padded message of appropriate size
let mut result = vec![F::zero(); expanded_size];
result[..interleaved_coeffs.len()].copy_from_slice(interleaved_coeffs);
let mut result: Vec<_> = vec![F::zero(); interleaved_coeffs.len() * expanded_size];
for (i, poly) in interleaved_coeffs.iter().enumerate() {
let offset = i * expanded_size;
result[offset..offset + num_coeffs].copy_from_slice(poly);
}

let rows = expanded_size / fold_factor_exp;
let columns = fold_factor_exp;
Expand Down Expand Up @@ -269,7 +272,7 @@ mod tests {
);

// Compute things the new way
let interleaved_ntt = interleaved_rs_encode(&poly, expansion, folding_factor);
let interleaved_ntt = interleaved_rs_encode(&[poly], expansion, folding_factor);
assert_eq!(expected, interleaved_ntt);
}
}
3 changes: 3 additions & 0 deletions src/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ where
/// These define the hashing function used when combining two child nodes into a parent node.
#[serde(with = "crate::ark_serde")]
pub two_to_one_params: TwoToOneParam<MerkleConfig>,

/// Number of polynomials committed in the batch.
pub batch_size: usize,
}

impl<MerkleConfig, PowStrategy> Debug for ProtocolParameters<MerkleConfig, PowStrategy>
Expand Down
237 changes: 237 additions & 0 deletions src/whir/batching.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
///
/// High level idea of batching multiple polynomials is as follows:
///
/// - Prover commits to multiple Merkle roots of evaluations of each polynomial
///
/// - Verifier samples a batching_randomness field element. For soundness
/// reasons, batching_randomness must be sampled after the Merkle roots have
/// been committed.
///
/// - Prover computes the weighted sum of each individual polynomial based on
/// batching_randomness to compute a single polynomial to proceed during
/// Sumcheck rounds and any other future round.
///
/// - In the first round of the proof, Prover includes the STIR queries from
/// each individual oracle. (The STIR query indexes should be the same for all
/// oracles.)
///
/// - During proof verification, for the first round, the Verifier validates
/// each individual STIR query in the proof against individual Merkle root.
/// Once these Merkle paths are validated, the Verifier re-derives
/// batching_randomness and combines the STIR responses using powers of
/// batching_randomness.
///
/// - After the first round, rest of the protocol proceeds as usual.
///
#[cfg(test)]
mod batching_tests {
use ark_std::UniformRand;
use spongefish::DomainSeparator;
use spongefish_pow::blake3::Blake3PoW;

use crate::{
crypto::{
fields::Field64,
merkle_tree::{
blake3::{Blake3Compress, Blake3LeafHash, Blake3MerkleTreeParams},
parameters::default_config,
},
},
parameters::{FoldingFactor, MultivariateParameters, ProtocolParameters, SoundnessType},
poly_utils::{
coeffs::CoefficientList, evals::EvaluationsList, multilinear::MultilinearPoint,
},
whir::{
committer::{reader::CommitmentReader, CommitmentWriter},
domainsep::WhirDomainSeparator,
parameters::WhirConfig,
prover::Prover,
statement::{Statement, Weights},
verifier::Verifier,
},
};

/// Merkle tree configuration type for commitment layers.
type MerkleConfig = Blake3MerkleTreeParams<F>;
/// PoW strategy used for grinding challenges in Fiat-Shamir transcript.
type PowStrategy = Blake3PoW;
/// Field type used in the tests.
type F = Field64;

fn random_poly(num_coefficients: usize) -> CoefficientList<F> {
let mut store = Vec::<F>::with_capacity(num_coefficients);
let mut rng = ark_std::rand::thread_rng();
(0..num_coefficients).for_each(|_| store.push(F::rand(&mut rng)));

CoefficientList::new(store)
}

/// Run a complete WHIR STARK proof lifecycle: commit, prove, and verify.
///
/// This function:
/// - builds a multilinear polynomial with a specified number of variables,
/// - constructs a STARK statement with constraints based on evaluations and linear relations,
/// - commits to the polynomial using a Merkle-based commitment scheme,
/// - generates a proof using the WHIR prover,
/// - verifies the proof using the WHIR verifier.
fn make_batched_whir_things(
batch_size: usize,
num_variables: usize,
folding_factor: FoldingFactor,
num_points: usize,
soundness_type: SoundnessType,
pow_bits: usize,
) {
println!("Test parameters: ");
println!(" num_polynomials: {batch_size}");
println!(" num_variables : {num_variables}");
println!(" folding_factor : {:?}", &folding_factor);
println!(" num_points : {num_points:?}");
println!(" soundness_type : {soundness_type:?}");
println!(" pow_bits : {pow_bits}");

// Number of coefficients in the multilinear polynomial (2^num_variables)
let num_coeffs = 1 << num_variables;

// Randomness source
let mut rng = ark_std::test_rng();
// Generate Merkle parameters: hash function and compression function
let (leaf_hash_params, two_to_one_params) =
default_config::<F, Blake3LeafHash<F>, Blake3Compress>(&mut rng);

// Configure multivariate polynomial parameters
let mv_params = MultivariateParameters::new(num_variables);

// Configure the WHIR protocol parameters
let whir_params = ProtocolParameters::<MerkleConfig, PowStrategy> {
initial_statement: true,
security_level: 32,
pow_bits,
folding_factor,
leaf_hash_params,
two_to_one_params,
soundness_type,
_pow_parameters: Default::default(),
starting_log_inv_rate: 1,
batch_size,
};

// Build global configuration from multivariate + protocol parameters
let params = WhirConfig::new(mv_params, whir_params);

let mut poly_list = Vec::<CoefficientList<F>>::with_capacity(batch_size);

(0..batch_size).for_each(|_| poly_list.push(random_poly(num_coeffs)));

// Construct a coefficient vector for linear sumcheck constraint
let weight_poly = CoefficientList::new((0..1 << num_variables).map(F::from).collect());

// Generate `num_points` random points in the multilinear domain
let points: Vec<_> = (0..num_points)
.map(|_| MultilinearPoint::rand(&mut rng, num_variables))
.collect();

// Define the Fiat-Shamir IOPattern for committing and proving
let io = DomainSeparator::new("🌪️")
.commit_statement(&params)
.add_whir_proof(&params);

// println!("IO Domain Separator: {:?}", str::from_utf8(io.as_bytes()));
// Initialize the Merlin transcript from the IOPattern
let mut prover_state = io.to_prover_state();

// Create a commitment to the polynomial and generate auxiliary witness data
let committer = CommitmentWriter::new(params.clone());
let batched_witness = committer
.commit_batch(&mut prover_state, &poly_list)
.unwrap();

// Get the batched polynomial
let batched_poly = batched_witness.batched_poly();

// Initialize a statement with no constraints yet
let mut statement = Statement::new(num_variables);

// For each random point, evaluate the polynomial and create a constraint
for point in &points {
let eval = batched_poly.evaluate(point);
let weights = Weights::evaluation(point.clone());
statement.add_constraint(weights, eval);
}

// Define weights for linear combination
let linear_claim_weight = Weights::linear(weight_poly.into());

// Convert polynomial to extension field representation
let poly = EvaluationsList::from(batched_poly.clone().to_extension());

// Compute the weighted sum of the polynomial (for sumcheck)
let sum = linear_claim_weight.weighted_sum(&poly);

// Add linear constraint to the statement
statement.add_constraint(linear_claim_weight, sum);

// Instantiate the prover with the given parameters
let prover = Prover(params.clone());

// Extract verifier-side version of the statement (only public data)

// Generate a STARK proof for the given statement and witness
prover
.prove(&mut prover_state, statement.clone(), batched_witness)
.unwrap();

// Create a verifier with matching parameters
let verifier = Verifier::new(&params);

// Reconstruct verifier's view of the transcript using the IOPattern and prover's data
let mut verifier_state = io.to_verifier_state(prover_state.narg_string());

// Create a commitment reader
let commitment_reader = CommitmentReader::new(&params);

let parsed_commitment = commitment_reader
.parse_commitment(&mut verifier_state)
.unwrap();

// Verify that the generated proof satisfies the statement
assert!(verifier
.verify(&mut verifier_state, &parsed_commitment, &statement,)
.is_ok());
}

#[test]
fn test_whir() {
let folding_factors = [1, 4];
let soundness_type = [
SoundnessType::ConjectureList,
SoundnessType::ProvableList,
SoundnessType::UniqueDecoding,
];
let num_points = [0, 2];
let pow_bits = [0, 10];

for folding_factor in folding_factors {
let num_variables = (2 * folding_factor)..=3 * folding_factor;
for num_variable in num_variables {
for num_points in num_points {
for soundness_type in soundness_type {
for pow_bits in pow_bits {
for batch_size in 1..=4 {
make_batched_whir_things(
batch_size,
num_variable,
FoldingFactor::Constant(folding_factor),
num_points,
soundness_type,
pow_bits,
// FoldType::Naive,
);
}
}
}
}
}
}
}
}
Loading
Loading