Skip to content
Draft
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 @@ -27,7 +27,7 @@ pub use {
hash_config::HashConfig,
mavros::{MavrosProver, MavrosSchemeData},
noir_proof_scheme::{NoirProof, NoirProofScheme, NoirSchemeData},
prefix_covector::{OffsetCovector, PrefixCovector},
prefix_covector::{OffsetCovector, PrefixCovector, SparseCovector},
prover::{NoirProver, Prover},
r1cs::R1CS,
transcript_sponge::TranscriptSponge,
Expand Down
182 changes: 182 additions & 0 deletions provekit/common/src/prefix_covector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,109 @@ pub fn compute_public_eval(
eval
}

/// A covector with non-zero weights at arbitrary scattered positions within a
/// `domain_size`-length domain. Used for challenge binding where challenge
/// positions in w2 may not be contiguous.
pub struct SparseCovector {
/// (position, weight) pairs.
entries: Vec<(usize, FieldElement)>,
domain_size: usize,
}

impl SparseCovector {
/// Create a new `SparseCovector` from position-weight pairs.
///
/// # Panics
///
/// Asserts that `domain_size` is a power of two and all positions are
/// within bounds.
#[must_use]
pub fn new(entries: Vec<(usize, FieldElement)>, domain_size: usize) -> Self {
debug_assert!(domain_size.is_power_of_two());
for &(pos, _) in &entries {
assert!(
pos < domain_size,
"SparseCovector: position {pos} >= domain_size {domain_size}"
);
}
Self {
entries,
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;
}
}
}

/// Create a challenge-binding weight [`SparseCovector`] from Fiat-Shamir
/// randomness `x`.
///
/// Places `[1, x, x², …]` at the given `challenge_offsets` positions within a
/// `2^m`-length domain.
#[must_use]
pub fn make_challenge_weight(
x: FieldElement,
challenge_offsets: &[usize],
m: usize,
) -> SparseCovector {
let domain_size = 1 << m;
let mut entries = Vec::with_capacity(challenge_offsets.len());
let mut x_pow = FieldElement::one();
for &offset in challenge_offsets {
entries.push((offset, x_pow));
x_pow *= x;
}
SparseCovector::new(entries, domain_size)
}

/// Compute the challenge weight evaluation
/// `⟨[1, x, x², …], poly[offsets[0]], poly[offsets[1]], …⟩` without
/// allocating a [`SparseCovector`].
#[must_use]
pub fn compute_challenge_eval(
x: FieldElement,
challenge_offsets: &[usize],
polynomial: &[FieldElement],
) -> FieldElement {
let mut eval = FieldElement::zero();
let mut x_pow = FieldElement::one();
for &offset in challenge_offsets {
eval += x_pow * polynomial[offset];
x_pow *= x;
}
eval
}

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

fn sparse_full_vector(
entries: &[(usize, FieldElement)],
domain_size: usize,
) -> Vec<FieldElement> {
let mut v = vec![FieldElement::zero(); domain_size];
for &(pos, w) in entries {
v[pos] = w;
}
v
}

#[test]
fn sparse_mle_evaluate_matches_full_vector() {
let domain_size = 16;
let entries = vec![(2, fe(7)), (5, fe(3)), (11, fe(13))];
let point = vec![fe(2), fe(5), fe(13), fe(17)];

let covector = SparseCovector::new(entries.clone(), domain_size);
let full = sparse_full_vector(&entries, domain_size);

let expected = multilinear_extend(&full, &point);
let actual = covector.mle_evaluate(&point);
assert_eq!(actual, expected);
}

#[test]
fn sparse_accumulate_writes_correct_positions() {
let domain_size = 16;
let entries = vec![(2, fe(7)), (5, fe(3)), (11, fe(13))];
let scalar = fe(4);

let covector = SparseCovector::new(entries.clone(), domain_size);
let mut accumulator = vec![FieldElement::zero(); domain_size];
covector.accumulate(&mut accumulator, scalar);

let expected = sparse_full_vector(&entries, domain_size);
for i in 0..domain_size {
assert_eq!(accumulator[i], scalar * expected[i], "mismatch at {i}");
}
}

#[test]
fn sparse_mle_and_accumulate_are_consistent() {
let domain_size = 8;
let entries = vec![(1, fe(5)), (3, fe(11)), (6, fe(7))];

let covector = SparseCovector::new(entries.clone(), domain_size);

let mut full_weights = vec![FieldElement::zero(); domain_size];
covector.accumulate(&mut full_weights, FieldElement::one());
assert_eq!(full_weights, sparse_full_vector(&entries, domain_size));

let point = vec![fe(3), fe(7), fe(11)];
let mle_from_full = multilinear_extend(&full_weights, &point);
let mle_from_covector = covector.mle_evaluate(&point);
assert_eq!(mle_from_full, mle_from_covector);
}

#[test]
#[should_panic(expected = "position 8 >= domain_size 8")]
fn sparse_panics_on_out_of_bounds() {
let _ = SparseCovector::new(vec![(8, fe(1))], 8);
}

#[test]
fn compute_challenge_eval_matches_weight() {
let offsets = vec![1, 5, 11];
let x = fe(7);

let mut poly = vec![FieldElement::zero(); 16];
poly[1] = fe(42);
poly[5] = fe(99);
poly[11] = fe(17);

let eval = compute_challenge_eval(x, &offsets, &poly);
let expected = fe(42) + fe(7) * fe(99) + fe(49) * fe(17);
assert_eq!(eval, expected);
}
}
1 change: 1 addition & 0 deletions provekit/common/src/whir_r1cs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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 @@ -512,7 +512,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 @@ -522,7 +530,7 @@ impl WitnessBuilder {
},
r1cs,
witness_map,
0,
Vec::new(),
));
}

Expand Down Expand Up @@ -575,12 +583,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 @@ -590,7 +601,7 @@ impl WitnessBuilder {
},
remapped_r1cs,
remapped_witness_map,
num_challenges,
challenge_offsets,
))
}
}
26 changes: 22 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 @@ -334,6 +335,16 @@ fn prove_from_alphas(
None
};

// Challenge binding: prove that w2 contains the correct Fiat-Shamir
// challenge values at the expected positions.
let challenge_eval = if !scheme.challenge_offsets.is_empty() {
let ce = compute_challenge_eval(x, &scheme.challenge_offsets, &c2.polynomial);
merlin.prover_message(&ce);
Some(ce)
} else {
None
};

let WhirR1CSCommitment {
witness: w1,
polynomial: p1,
Expand Down Expand Up @@ -375,12 +386,19 @@ fn prove_from_alphas(
} = c2;
{
let weights = build_prefix_covectors(scheme.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 cw = make_challenge_weight(x, &scheme.challenge_offsets, scheme.m);
evaluations.push(ce);
boxed_weights.push(Box::new(cw));
}

let _ = scheme.whir_witness.prove(
&mut merlin,
vec![Cow::Borrowed(p2.as_slice())],
Expand Down
11 changes: 9 additions & 2 deletions provekit/r1cs-compiler/src/noir_proof_scheme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,14 @@ impl NoirCompiler {
main.public_inputs().indices().iter().cloned().collect();

let has_public_inputs = !acir_public_inputs_indices_set.is_empty();
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 @@ -96,6 +97,7 @@ impl NoirCompiler {
&remapped_r1cs,
split_witness_builders.w1_size,
num_challenges,
challenge_offsets,
has_public_inputs,
hash_config.engine_id(),
);
Expand Down Expand Up @@ -158,10 +160,15 @@ impl MavrosCompiler {
}
}

let challenges_size = mavros_r1cs.witness_layout.challenges_size;
// In Mavros, challenges occupy the first `challenges_size` positions of
// w2 (immediately after the pre-commitment boundary).
let challenge_offsets: Vec<usize> = (0..challenges_size).collect();
let whir_for_witness = WhirR1CSScheme::new_from_mavros_r1cs(
&mavros_r1cs,
mavros_r1cs.witness_layout.pre_commitment_size(),
mavros_r1cs.witness_layout.challenges_size,
challenges_size,
challenge_offsets,
num_public_inputs > 0,
hash_config.engine_id(),
);
Expand Down
Loading
Loading