diff --git a/src/gates/gate_testing.rs b/src/gates/gate_testing.rs new file mode 100644 index 00000000..2b0cb84b --- /dev/null +++ b/src/gates/gate_testing.rs @@ -0,0 +1,67 @@ +use crate::field::field::Field; +use crate::gates::gate::Gate; +use crate::polynomial::polynomial::{PolynomialCoeffs, PolynomialValues}; +use crate::util::{log2_ceil, transpose}; +use crate::vars::EvaluationVars; + +const WITNESS_SIZE: usize = 1 << 5; +const WITNESS_DEGREE: usize = WITNESS_SIZE - 1; + +/// Tests that the constraints imposed by the given gate are low-degree by applying them to random +/// low-degree witness polynomials. +pub(crate) fn test_low_degree>(gate: G) { + let rate_bits = log2_ceil(gate.degree() + 1); + + let wire_ldes = random_low_degree_matrix(gate.num_wires(), rate_bits); + let constant_ldes = random_low_degree_matrix::(gate.num_constants(), rate_bits); + assert_eq!(wire_ldes.len(), constant_ldes.len()); + + let constraint_evals = wire_ldes + .iter() + .zip(constant_ldes.iter()) + .map(|(local_wires, local_constants)| EvaluationVars { + local_constants, + local_wires, + }) + .map(|vars| gate.eval_unfiltered(vars)) + .collect::>(); + + let constraint_eval_degrees = transpose(&constraint_evals) + .into_iter() + .map(PolynomialValues::new) + .map(|p| p.degree()) + .collect::>(); + + let expected_eval_degree = WITNESS_DEGREE * gate.degree(); + + assert!( + constraint_eval_degrees + .iter() + .all(|°| deg <= expected_eval_degree), + "Expected degrees at most {} * {} = {}, actual {:?}", + WITNESS_SIZE, + gate.degree(), + expected_eval_degree, + constraint_eval_degrees + ); +} + +fn random_low_degree_matrix(num_polys: usize, rate_bits: usize) -> Vec> { + let polys = (0..num_polys) + .map(|_| random_low_degree_values(rate_bits)) + .collect::>(); + + if polys.is_empty() { + // We want a Vec of many empty Vecs, whereas transpose would just give an empty Vec. + vec![Vec::new(); WITNESS_SIZE << rate_bits] + } else { + transpose(&polys) + } +} + +fn random_low_degree_values(rate_bits: usize) -> Vec { + PolynomialCoeffs::new(F::rand_vec(WITNESS_SIZE)) + .lde(rate_bits) + .fft() + .values +} diff --git a/src/gates/interpolation_quartic.rs b/src/gates/interpolation_quartic.rs index 6b37a48f..68ad0fd2 100644 --- a/src/gates/interpolation_quartic.rs +++ b/src/gates/interpolation_quartic.rs @@ -106,10 +106,18 @@ impl, const D: usize> Gate for QuarticInterpolationG .map(|i| vars.get_local_ext(self.wires_coeff(i))) .collect(); let interpolant = PolynomialCoeffs::new(coeffs); - let x_eval = vars.get_local_ext(self.wires_evaluation_point()); - let x_eval_powers = x_eval.powers().take(self.num_points); - // TODO + for i in 0..self.num_points { + let point = F::Extension::from_basefield(vars.local_wires[self.wire_point(i)]); + let value = vars.get_local_ext(self.wires_value(i)); + let computed_value = interpolant.eval(point); + constraints.extend(&(value - computed_value).to_basefield_array()); + } + + let evaluation_point = vars.get_local_ext(self.wires_evaluation_point()); + let evaluation_value = vars.get_local_ext(self.wires_evaluation_value()); + let computed_evaluation_value = interpolant.eval(evaluation_point); + constraints.extend(&(evaluation_value - computed_evaluation_value).to_basefield_array()); constraints } @@ -144,11 +152,15 @@ impl, const D: usize> Gate for QuarticInterpolationG } fn degree(&self) -> usize { - self.num_points - 1 + // The highest power of x is `num_points - 1`, and then multiplication by the coefficient + // adds 1. + self.num_points } fn num_constraints(&self) -> usize { - todo!() + // num_points * D constraints to check for consistency between the coefficients and the + // point-value pairs, plus D constraints for the evaluation value. + self.num_points * D + D } } @@ -231,14 +243,16 @@ mod tests { use crate::field::crandall_field::CrandallField; use crate::gates::gate::Gate; + use crate::gates::gate_testing::test_low_degree; use crate::gates::interpolation_quartic::QuarticInterpolationGate; #[test] - fn wire_indices_2_points() { + fn wire_indices() { let gate = QuarticInterpolationGate:: { num_points: 2, _phantom: PhantomData, }; + // The exact indices aren't really important, but we want to make sure we don't have any // overlaps or gaps. assert_eq!(gate.wire_point(0), 0); @@ -253,11 +267,12 @@ mod tests { } #[test] - fn wire_indices_4_points() { - let gate = QuarticInterpolationGate:: { + fn low_degree() { + type F = CrandallField; + let gate = QuarticInterpolationGate:: { num_points: 4, _phantom: PhantomData, }; - assert_eq!(gate.num_wires(), 44); + test_low_degree(gate); } } diff --git a/src/gates/mod.rs b/src/gates/mod.rs index e35a542d..e0d990b9 100644 --- a/src/gates/mod.rs +++ b/src/gates/mod.rs @@ -6,3 +6,6 @@ pub mod gmimc; pub(crate) mod gmimc_eval; mod interpolation_quartic; pub(crate) mod noop; + +#[cfg(test)] +mod gate_testing; diff --git a/src/polynomial/polynomial.rs b/src/polynomial/polynomial.rs index 1d47dea4..5e3dd9f0 100644 --- a/src/polynomial/polynomial.rs +++ b/src/polynomial/polynomial.rs @@ -32,6 +32,7 @@ impl PolynomialValues { pub fn ifft(self) -> PolynomialCoeffs { ifft(self) } + pub fn lde_multiple(polys: Vec, rate_bits: usize) -> Vec { polys.into_iter().map(|p| p.lde(rate_bits)).collect() } @@ -40,6 +41,16 @@ impl PolynomialValues { let coeffs = ifft(self).lde(rate_bits); fft(coeffs) } + + pub fn degree(&self) -> usize { + self.degree_plus_one() + .checked_sub(1) + .expect("deg(0) is undefined") + } + + pub fn degree_plus_one(&self) -> usize { + self.clone().ifft().degree_plus_one() + } } impl From> for PolynomialValues { diff --git a/src/util/mod.rs b/src/util/mod.rs index 2b459c92..7ab9bc5b 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -32,6 +32,10 @@ pub(crate) fn transpose_poly_values(polys: Vec>) - } pub(crate) fn transpose(matrix: &[Vec]) -> Vec> { + if matrix.is_empty() { + return Vec::new(); + } + let old_rows = matrix.len(); let old_cols = matrix[0].len(); let mut transposed = vec![Vec::with_capacity(old_rows); old_cols];