Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 provekit/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub use {
acir::FieldElement as NoirElement,
ark_bn254::Fr as FieldElement,
noir_proof_scheme::{NoirProof, NoirProofScheme},
prefix_covector::{OffsetCovector, PrefixCovector},
prefix_covector::{OffsetCovector, PrefixCovector, SparseCovector},
prover::Prover,
r1cs::R1CS,
verifier::Verifier,
Expand Down
159 changes: 159 additions & 0 deletions provekit/common/src/prefix_covector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,100 @@ pub fn compute_public_eval(
eval
}

/// Covector with non-zero weights at arbitrary (possibly non-contiguous)
/// positions. Used for challenge binding where Fiat-Shamir challenge
/// witnesses may be scattered throughout the w2 polynomial.
pub struct SparseCovector {
entries: Vec<(usize, FieldElement)>,
domain_size: usize,
}

impl SparseCovector {
/// Create a new `SparseCovector` from `(position, weight)` pairs and a
/// domain size.
pub fn new(entries: Vec<(usize, FieldElement)>, domain_size: usize) -> Self {
assert!(domain_size.is_power_of_two());
assert!(
entries.iter().all(|&(pos, _)| pos < domain_size),
"SparseCovector: all entry positions must be < domain_size ({domain_size})"
);
Self {
entries,
Comment thread
ashpect marked this conversation as resolved.
domain_size,
}
}
}

impl LinearForm<FieldElement> for SparseCovector {
fn size(&self) -> usize {
self.domain_size
}

fn mle_evaluate(&self, point: &[FieldElement]) -> FieldElement {
let n = point.len();
let mut result = FieldElement::zero();
for &(idx, w) in &self.entries {
if w.is_zero() {
continue;
}
let mut basis = FieldElement::one();
for (k, pk) in point.iter().enumerate() {
if (idx >> (n - 1 - k)) & 1 == 1 {
basis *= pk;
} else {
basis *= FieldElement::one() - pk;
}
}
result += w * basis;
}
result
}

fn accumulate(&self, accumulator: &mut [FieldElement], scalar: FieldElement) {
for &(pos, w) in &self.entries {
accumulator[pos] += scalar * w;
}
}
}

/// Build a [`SparseCovector`] that extracts the challenge positions from a w2
/// polynomial, weighted by successive powers of `x`.
#[must_use]
pub fn make_challenge_weight(
x: FieldElement,
challenge_offsets: &[usize],
m: usize,
) -> SparseCovector {
let domain_size = 1usize << m;
let mut x_pow = FieldElement::one();
let entries: Vec<(usize, FieldElement)> = challenge_offsets
.iter()
.map(|&pos| {
let entry = (pos, x_pow);
x_pow *= x;
entry
})
.collect();
SparseCovector::new(entries, domain_size)
}

/// Evaluate `Σ xⁱ · polynomial[challenge_offsets[i]]` — the challenge binding
/// value that the prover sends as a transcript-bound message.
#[must_use]
pub fn compute_challenge_eval(
x: FieldElement,
challenge_offsets: &[usize],
polynomial: &[FieldElement],
) -> FieldElement {
let mut result = FieldElement::zero();
let mut x_pow = FieldElement::one();
for &pos in challenge_offsets {
result += x_pow * polynomial[pos];
x_pow *= x;
}
result
}

#[cfg(test)]
mod tests {
use {super::*, whir::algebra::multilinear_extend};
Expand Down Expand Up @@ -430,4 +524,69 @@ mod tests {
// offset + weights.len() = 7 + 2 = 9 > 8
let _ = OffsetCovector::new(vec![fe(1), fe(2)], 7, 8);
}

#[test]
fn sparse_covector_mle_matches_dense() {
let entries = vec![(0, fe(3)), (3, fe(7))];
let sc = SparseCovector::new(entries, 4);
let mut dense = vec![FieldElement::zero(); 4];
dense[0] = fe(3);
dense[3] = fe(7);
let pc = PrefixCovector::new(dense, 4);
let point = vec![fe(2), fe(5)];
assert_eq!(sc.mle_evaluate(&point), pc.mle_evaluate(&point));
}

#[test]
fn sparse_covector_accumulate() {
let entries = vec![(1, fe(4)), (3, fe(2))];
let sc = SparseCovector::new(entries, 4);
let mut acc = vec![FieldElement::zero(); 4];
sc.accumulate(&mut acc, fe(3));
assert_eq!(acc[0], FieldElement::zero());
assert_eq!(acc[1], fe(12));
assert_eq!(acc[2], FieldElement::zero());
assert_eq!(acc[3], fe(6));
}

#[test]
fn make_challenge_weight_consistency() {
let x = fe(11);
let offsets = vec![2, 5, 9];
let cw = make_challenge_weight(x, &offsets, 4);
assert_eq!(cw.size(), 16);
let mut poly = vec![FieldElement::zero(); 16];
poly[2] = fe(100);
poly[5] = fe(200);
poly[9] = fe(300);
let eval = compute_challenge_eval(x, &offsets, &poly);
let mut acc = vec![FieldElement::zero(); 16];
cw.accumulate(&mut acc, FieldElement::one());
let dot: FieldElement = acc.iter().zip(poly.iter()).map(|(a, b)| *a * *b).sum();
assert_eq!(eval, dot);
}

#[test]
fn compute_challenge_eval_basic() {
let x = fe(3);
let offsets = vec![0, 2];
let poly = vec![fe(10), fe(20), fe(30)];
let eval = compute_challenge_eval(x, &offsets, &poly);
assert_eq!(eval, fe(10) + fe(3) * fe(30));
}

#[test]
fn sparse_covector_empty_entries() {
let sc = SparseCovector::new(vec![], 8);
let point = vec![fe(1), fe(2), fe(3)];
assert_eq!(sc.mle_evaluate(&point), FieldElement::zero());
}

#[test]
fn sparse_covector_single_entry_matches_prefix() {
let sc = SparseCovector::new(vec![(0, fe(5))], 4);
let pc = PrefixCovector::new(vec![fe(5), FieldElement::zero()], 4);
let point = vec![fe(7), fe(11)];
assert_eq!(sc.mle_evaluate(&point), pc.mle_evaluate(&point));
}
}
1 change: 1 addition & 0 deletions provekit/common/src/whir_r1cs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub struct WhirR1CSScheme {
pub m_0: usize,
pub a_num_terms: usize,
pub num_challenges: usize,
pub challenge_offsets: Vec<usize>,
pub has_public_inputs: bool,
pub whir_witness: WhirZkConfig,
}
Expand Down
23 changes: 17 additions & 6 deletions provekit/common/src/witness/witness_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,15 @@ impl WitnessBuilder {
r1cs: R1CS,
witness_map: Vec<Option<NonZeroU32>>,
acir_public_inputs_indices_set: HashSet<u32>,
) -> Result<(SplitWitnessBuilders, R1CS, Vec<Option<NonZeroU32>>, usize), SplitError> {
) -> Result<
(
SplitWitnessBuilders,
R1CS,
Vec<Option<NonZeroU32>>,
Vec<usize>,
),
SplitError,
> {
if witness_builders.is_empty() {
return Ok((
SplitWitnessBuilders {
Expand All @@ -305,7 +313,7 @@ impl WitnessBuilder {
},
r1cs,
witness_map,
0,
Vec::new(),
));
}

Expand Down Expand Up @@ -358,12 +366,15 @@ impl WitnessBuilder {
scheduler.build_layers()
};

let num_challenges = w2_layers
let challenge_offsets: Vec<usize> = w2_layers
.layers
.iter()
.flat_map(|layer| &layer.witness_builders)
.filter(|b| matches!(b, WitnessBuilder::Challenge(_)))
.count();
.filter_map(|b| match b {
WitnessBuilder::Challenge(idx) => Some(*idx - w1_size),
_ => None,
})
.collect();

Ok((
SplitWitnessBuilders {
Expand All @@ -373,7 +384,7 @@ impl WitnessBuilder {
},
remapped_r1cs,
remapped_witness_map,
num_challenges,
challenge_offsets,
))
}
}
29 changes: 25 additions & 4 deletions provekit/prover/src/whir_r1cs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use {
ark_std::{One, Zero},
provekit_common::{
prefix_covector::{
build_prefix_covectors, compute_alpha_evals, compute_public_eval, expand_powers,
make_public_weight, OffsetCovector,
build_prefix_covectors, compute_alpha_evals, compute_challenge_eval,
compute_public_eval, expand_powers, make_challenge_weight, make_public_weight,
OffsetCovector,
},
utils::{
pad_to_power_of_two,
Expand Down Expand Up @@ -223,6 +224,18 @@ impl WhirR1CSProver for WhirR1CSScheme {
None
};

// Challenge binding: compute eval of challenge positions in w2 and
// send as transcript-bound message so the verifier can check that
// the committed w2 polynomial contains the correct Fiat-Shamir
// challenges.
let challenge_eval = if !self.challenge_offsets.is_empty() {
let ce = compute_challenge_eval(x, &self.challenge_offsets, &c2.polynomial);
merlin.prover_message(&ce);
Some(ce)
} else {
None
};

let WhirR1CSCommitment {
witness: w1,
polynomial: p1,
Expand Down Expand Up @@ -264,12 +277,20 @@ impl WhirR1CSProver for WhirR1CSScheme {
} = c2;
{
let weights = build_prefix_covectors(self.m, alphas_2);
let evaluations: Vec<FieldElement> = evals_2;
let mut evaluations: Vec<FieldElement> = evals_2;

let boxed_weights: Vec<Box<dyn LinearForm<FieldElement>>> = weights
let mut boxed_weights: Vec<Box<dyn LinearForm<FieldElement>>> = weights
.into_iter()
.map(|w| Box::new(w) as Box<dyn LinearForm<FieldElement>>)
.collect();

if let Some(ce) = challenge_eval {
let challenge_weight =
make_challenge_weight(x, &self.challenge_offsets, self.m);
boxed_weights.push(Box::new(challenge_weight));
evaluations.push(ce);
}

let _ = self.whir_witness.prove(
&mut merlin,
vec![Cow::Borrowed(p2.as_slice())],
Expand Down
4 changes: 3 additions & 1 deletion provekit/r1cs-compiler/src/noir_proof_scheme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@ impl NoirProofSchemeBuilder for NoirProofScheme {

let has_public_inputs = !acir_public_inputs_indices_set.is_empty();
// Split witness builders and remap indices for sound challenge generation
let (split_witness_builders, remapped_r1cs, remapped_witness_map, num_challenges) =
let (split_witness_builders, remapped_r1cs, remapped_witness_map, challenge_offsets) =
WitnessBuilder::split_and_prepare_layers(
&witness_builders,
r1cs,
witness_map,
acir_public_inputs_indices_set,
)?;
let num_challenges = challenge_offsets.len();
info!(
"Witness split: w1 size = {}, w2 size = {}",
split_witness_builders.w1_size,
Expand All @@ -92,6 +93,7 @@ impl NoirProofSchemeBuilder for NoirProofScheme {
&remapped_r1cs,
split_witness_builders.w1_size,
num_challenges,
challenge_offsets,
has_public_inputs,
);

Expand Down
9 changes: 9 additions & 0 deletions provekit/r1cs-compiler/src/whir_r1cs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub trait WhirR1CSSchemeBuilder {
r1cs: &R1CS,
w1_size: usize,
num_challenges: usize,
challenge_offsets: Vec<usize>,
has_public_inputs: bool,
) -> Self;

Expand All @@ -22,8 +23,15 @@ impl WhirR1CSSchemeBuilder for WhirR1CSScheme {
r1cs: &R1CS,
w1_size: usize,
num_challenges: usize,
challenge_offsets: Vec<usize>,
has_public_inputs: bool,
) -> Self {
assert_eq!(
num_challenges,
challenge_offsets.len(),
"num_challenges ({num_challenges}) must equal challenge_offsets.len() ({})",
challenge_offsets.len()
);
let total_witnesses = r1cs.num_witnesses();
assert!(
w1_size <= total_witnesses,
Expand All @@ -49,6 +57,7 @@ impl WhirR1CSSchemeBuilder for WhirR1CSScheme {
m_0,
a_num_terms: next_power_of_two(r1cs.a().iter().count()),
num_challenges,
challenge_offsets,
whir_witness: Self::new_whir_zk_config_for_size(m_raw, 1),
has_public_inputs,
}
Expand Down
Loading
Loading