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
88 changes: 52 additions & 36 deletions crates/halo2_proofs/src/plonk/vanishing/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,22 @@ fn fold_quotient_pieces_pasta<
//
// Q_x(x_3) = (... + x_1 h(x_3)) + r(x_3).
//
// For r(X) = a + bX, the map from (a, b) to (r(x), r(x_3)) has determinant
// x_3 - x. Conditioned on the revealed r(x), r(x_3) is therefore uniform when
// the points are distinct. The verifier rejects x_3 equal to any queried
// point. The independently Pedersen-blinded group messages hide the
// coefficients before each challenge, while the commitment scheme's separate
// IPA mask hides its final folded scalar. This commitment participates in one
// multi-opening, and the verifier sees only r(x) and its single affine
// contribution r(x_3). Exposing r at another independent point would require
// revisiting the two-coefficient argument. Thus two random coefficients give
// the same HVZK masking role as the previous dense polynomial, up to the
// scheme's existing negligible challenge-collision and transcript-abort
// events. Soundness does not depend on an honest prover sampling r from either
// For r(X) = a + bX + cX^2, the first two columns of the map from (a, b, c) to
// (r(x), r(x_3)) have determinant x_3 - x. The map therefore has rank two
// when the points are distinct, and a one-dimensional kernel. Conditioned on
// the revealed r(x), r(x_3) is uniform, with one random coefficient of excess
// entropy after both evaluations are fixed. The verifier rejects x_3 equal to
// any queried point. The independently Pedersen-blinded group messages hide
// the coefficients before each challenge, while the commitment scheme's
// separate IPA mask hides its final folded scalar. This commitment
// participates in one multi-opening, and the verifier sees only r(x) and its
// single affine contribution r(x_3). Thus three random coefficients give the
// same HVZK masking role as the previous dense polynomial, with one excess
// coefficient beyond the two needed for these evaluations, up to the scheme's
// existing negligible challenge-collision and transcript-abort events.
// Soundness does not depend on an honest prover sampling r from either
// distribution.
const QUOTIENT_EVALUATION_MASK_COEFFICIENTS: usize = 2;
const QUOTIENT_EVALUATION_MASK_COEFFICIENTS: usize = 3;

fn sample_quotient_evaluation_mask<F: WithSmallOrderMulGroup<3>, R: Rng>(
domain: &EvaluationDomain<F>,
Expand Down Expand Up @@ -182,10 +184,16 @@ fn commit_quotient_evaluation_mask<C: CurveAffine>(
.all(|coefficient| *coefficient == C::Scalar::ZERO)
);

let scalars = [polynomial[0], polynomial[1], blind.0];
let bases = [params.g[0], params.g[1], params.w];
let scalars = [polynomial[0], polynomial[1], polynomial[2]];
let bases = [params.g[0], params.g[1], params.g[2]];

best_multiexp(&scalars, &bases)
params
.try_commit_sparse_with_prepared_blind(&scalars, &bases, blind)
.unwrap_or_else(|| {
let scalars = [polynomial[0], polynomial[1], polynomial[2], blind.0];
let bases = [params.g[0], params.g[1], params.g[2], params.w];
best_multiexp(&scalars, &bases)
})
}

fn evaluate_quotient_evaluation_mask<F: Field>(polynomial: &Polynomial<F, Coeff>, point: F) -> F {
Expand All @@ -196,7 +204,7 @@ fn evaluate_quotient_evaluation_mask<F: Field>(polynomial: &Polynomial<F, Coeff>
.all(|coefficient| *coefficient == F::ZERO)
);

polynomial[0] + polynomial[1] * point
polynomial[0] + point * (polynomial[1] + polynomial[2] * point)
}

pub(in crate::plonk) struct CommittedRandomPolynomial<C: CurveAffine> {
Expand Down Expand Up @@ -229,12 +237,12 @@ impl<C: CurveAffine> Argument<C> {
mut rng: R,
transcript: &mut T,
) -> Result<CommittedRandomPolynomial<C>, Error> {
// Sample a random linear polynomial. If the PLONK and multi-opening
// Sample a random quadratic polynomial. If the PLONK and multi-opening
// evaluation points are distinct, its values at those two points are
// independent and uniform: the evaluation matrix has determinant
// equal to the difference of the points. The multi-opening verifier
// rejects the exceptional point collision, which is the same
// negligible honest-abort event under the previous dense mask.
// independent and uniform, with one coefficient of excess entropy.
// The multi-opening verifier rejects the exceptional point collision,
// which is the same negligible honest-abort event under the previous
// dense mask.
let random_poly = sample_quotient_evaluation_mask(domain, &mut rng);
// Sample a random blinding factor
let random_blind = Blind(C::Scalar::random(&mut rng));
Expand Down Expand Up @@ -476,23 +484,31 @@ mod tests {
);
}

fn two_evaluations_have_full_masking_rank<F>()
fn quadratic_mask_has_excess_entropy_after_two_evaluations<F>()
where
F: WithSmallOrderMulGroup<3> + From<u64> + core::fmt::Debug,
{
let first_point = F::from(5);
let later_point = F::from(9);
let first_evaluation = F::from(17);
let point_difference_inverse = (later_point - first_point).invert().unwrap();

// For every desired later evaluation there is exactly one linear
// polynomial with the fixed first evaluation. Thus a uniform random
// linear polynomial leaves the later evaluation uniform.
let point_difference = later_point - first_point;
let point_difference_inverse = point_difference.invert().unwrap();
let squared_difference = later_point.square() - first_point.square();

// For every desired later evaluation and every quadratic coefficient,
// there is exactly one choice of the remaining two coefficients. Thus
// the later evaluation is uniform, and fixing both evaluations leaves
// one coefficient of entropy.
for later_evaluation in [F::ZERO, F::ONE, F::from(29)] {
let linear = (later_evaluation - first_evaluation) * point_difference_inverse;
let constant = first_evaluation - linear * first_point;
assert_eq!(constant + linear * first_point, first_evaluation);
assert_eq!(constant + linear * later_point, later_evaluation);
for quadratic in [F::ZERO, F::ONE, F::from(31)] {
let linear = (later_evaluation - first_evaluation - quadratic * squared_difference)
* point_difference_inverse;
let constant =
first_evaluation - linear * first_point - quadratic * first_point.square();
let evaluate = |point: F| constant + point * (linear + quadratic * point);
assert_eq!(evaluate(first_point), first_evaluation);
assert_eq!(evaluate(later_point), later_evaluation);
}
}
}

Expand Down Expand Up @@ -543,12 +559,12 @@ mod tests {
}

#[test]
fn two_evaluations_have_full_masking_rank_fp() {
two_evaluations_have_full_masking_rank::<pallas::Base>();
fn quadratic_mask_has_excess_entropy_after_two_evaluations_fp() {
quadratic_mask_has_excess_entropy_after_two_evaluations::<pallas::Base>();
}

#[test]
fn two_evaluations_have_full_masking_rank_fq() {
two_evaluations_have_full_masking_rank::<vesta::Base>();
fn quadratic_mask_has_excess_entropy_after_two_evaluations_fq() {
quadratic_mask_has_excess_entropy_after_two_evaluations::<vesta::Base>();
}
}
156 changes: 156 additions & 0 deletions crates/halo2_proofs/src/poly/commitment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ const BLIND_WINDOW_ENTRIES: usize = (1 << BLIND_WINDOW_BITS) - 1;
const SCALAR_BYTE_ORDER_PROBE: u64 = 0x0102_0304_0506_0708;
#[cfg(feature = "orbits")]
const PREPARED_COMMITMENT_EXTRA_BASES: usize = 2;
#[cfg(all(feature = "multicore", not(feature = "orbits")))]
const MIN_PARALLEL_SPARSE_COMMITMENT_TERMS: usize = 3;

/// The `k = 11` SRS shape, whose current Pasta α7 tables remain ahead
/// through all ten cores on the benchmarked Apple M4 systems.
Expand Down Expand Up @@ -778,6 +780,40 @@ impl<C: CurveAffine> Params<C> {
best_multiexp::<C>(&tmp_scalars, &tmp_bases)
}

/// Tries to commit to sparse coefficient terms using the prepared blind.
///
/// When the no-orbits commitment preparation is armed, the independent
/// blinding term reuses its fixed-window table and runs alongside the
/// remaining MSM. Returns [`None`] when the caller should retain its
/// ordinary combined MSM.
pub(crate) fn try_commit_sparse_with_prepared_blind(
&self,
scalars: &[C::Scalar],
bases: &[C],
blind: Blind<C::Scalar>,
) -> Option<C::Curve> {
assert_eq!(scalars.len(), bases.len());
assert!(!scalars.is_empty());

#[cfg(all(feature = "multicore", not(feature = "orbits")))]
// A two-term body shares doublings with the blind more cheaply than a
// second Rayon task can recover. One worker avoids that scheduling
// cost, while larger sparse bodies have enough MSM work to overlap.
if (crate::multicore::current_num_threads() == 1
|| scalars.len() >= MIN_PARALLEL_SPARSE_COMMITMENT_TERMS)
&& let Some(blind_table) = self.blind_table()
{
let (commitment, blind) = crate::multicore::join(
|| best_multiexp(scalars, bases),
|| blind_table.multiply(blind.0),
);
return Some(commitment + blind);
}

let _ = blind;
None
}

/// This commits to a polynomial using its evaluations over the $2^k$ size
/// evaluation domain. The commitment will be blinded by the blinding factor
/// `r`.
Expand Down Expand Up @@ -1634,6 +1670,126 @@ fn blind_table_cache_is_shared_by_clones_and_not_serialized() {
assert!(deserialized.blind_table().is_none());
}

#[cfg(all(feature = "multicore", not(feature = "orbits")))]
#[test]
fn prepared_sparse_commitment_matches_combined_msm() {
use rand::{SeedableRng, rngs::StdRng};

use crate::pasta::{EqAffine, Fp};

let params = Params::<EqAffine>::new(6);
assert!(params.prepare_commitments());
let mut rng = StdRng::seed_from_u64(0x7370_6172_7365_6d73);

for indices in [&[0, 1][..], &[0, 1, 2][..], &[0, 1, 2, 4, 8, 16, 32][..]] {
let scalars = indices
.iter()
.map(|_| Fp::random(&mut rng))
.collect::<Vec<_>>();
let bases = indices
.iter()
.map(|index| params.g[*index])
.collect::<Vec<_>>();
let blind = Blind(Fp::random(&mut rng));
let mut combined_scalars = scalars.clone();
let mut combined_bases = bases.clone();
combined_scalars.push(blind.0);
combined_bases.push(params.w);
let expected = best_multiexp(&combined_scalars, &combined_bases);

for workers in [1, 2] {
let pool = maybe_rayon::ThreadPoolBuilder::new()
.num_threads(workers)
.build()
.unwrap();
let actual = pool
.install(|| params.try_commit_sparse_with_prepared_blind(&scalars, &bases, blind));
if workers > 1 && scalars.len() < MIN_PARALLEL_SPARSE_COMMITMENT_TERMS {
assert!(actual.is_none());
} else {
assert_eq!(actual.unwrap(), expected);
}
}
}
}

#[cfg(all(feature = "multicore", not(feature = "orbits")))]
#[test]
#[ignore]
/// Compares the combined MSM with the prepared-blind split used by the two
/// sparse masking commitments in one- and six-worker pools.
fn benchmark_prepared_sparse_commitment() {
use std::{hint::black_box, time::Instant};

use rand::{SeedableRng, rngs::StdRng};

use crate::pasta::{EqAffine, Fp};

const ITERATIONS: u32 = 1_000;
const REPETITIONS: usize = 5;

let params = Params::<EqAffine>::new(11);
assert!(params.prepare_commitments());
let mut rng = StdRng::seed_from_u64(0x7370_6172_7365_626d);

for (name, indices) in [
("quotient", &[0, 1, 2][..]),
("ipa", &[0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024][..]),
] {
let scalars = indices
.iter()
.map(|_| Fp::random(&mut rng))
.collect::<Vec<_>>();
let bases = indices
.iter()
.map(|index| params.g[*index])
.collect::<Vec<_>>();
let blind = Blind(Fp::random(&mut rng));
let mut combined_scalars = scalars.clone();
let mut combined_bases = bases.clone();
combined_scalars.push(blind.0);
combined_bases.push(params.w);

for workers in [1, 6] {
let pool = maybe_rayon::ThreadPoolBuilder::new()
.num_threads(workers)
.build()
.unwrap();
let measure = |prepared: bool| {
let start = Instant::now();
pool.install(|| {
for _ in 0..ITERATIONS {
let commitment = if prepared {
params
.try_commit_sparse_with_prepared_blind(&scalars, &bases, blind)
.unwrap_or_else(|| {
best_multiexp(&combined_scalars, &combined_bases)
})
} else {
best_multiexp(&combined_scalars, &combined_bases)
};
black_box(commitment.to_affine());
}
});
start.elapsed().as_nanos() as f64 / f64::from(ITERATIONS)
};

for repetition in 0..REPETITIONS {
let (baseline, prepared) = if repetition % 2 == 0 {
(measure(false), measure(true))
} else {
let prepared = measure(true);
(measure(false), prepared)
};
eprintln!(
"sparse-commitment name={name} workers={workers} repetition={} baseline_ns={baseline:.3} prepared_ns={prepared:.3}",
repetition + 1,
);
}
}
}
}

#[test]
fn test_commit_lagrange_epaffine() {
const K: u32 = 6;
Expand Down
Loading
Loading