diff --git a/src/gates/arithmetic.rs b/src/gates/arithmetic.rs index 4a2f63b7..f24ea7c1 100644 --- a/src/gates/arithmetic.rs +++ b/src/gates/arithmetic.rs @@ -142,13 +142,14 @@ impl SimpleGenerator for ArithmeticGenerator { } } -// #[cfg(test)] -// mod tests { -// use crate::{test_gate_low_degree, ArithmeticGate, Tweedledum}; -// -// test_gate_low_degree!( -// low_degree_ArithmeticGate, -// Tweedledum, -// ArithmeticGate -// ); -// } +#[cfg(test)] +mod tests { + use crate::field::crandall_field::CrandallField; + use crate::gates::arithmetic::ArithmeticGate; + use crate::gates::gate_testing::test_low_degree; + + #[test] + fn low_degree() { + test_low_degree(ArithmeticGate::new::()) + } +} diff --git a/src/gates/constant.rs b/src/gates/constant.rs index 8482a6de..a0a7685e 100644 --- a/src/gates/constant.rs +++ b/src/gates/constant.rs @@ -89,3 +89,15 @@ impl SimpleGenerator for ConstantGenerator { PartialWitness::singleton_target(Target::Wire(wire), self.constant) } } + +#[cfg(test)] +mod tests { + use crate::field::crandall_field::CrandallField; + use crate::gates::constant::ConstantGate; + use crate::gates::gate_testing::test_low_degree; + + #[test] + fn low_degree() { + test_low_degree(ConstantGate::get::()) + } +} diff --git a/src/gates/fri_consistency_gate.rs b/src/gates/fri_consistency_gate.rs deleted file mode 100644 index 6c9c8aaf..00000000 --- a/src/gates/fri_consistency_gate.rs +++ /dev/null @@ -1,372 +0,0 @@ -// use crate::circuit_data::CircuitConfig; -// use crate::constraint_polynomial::ConstraintPolynomial; -// use crate::field::fft::coset_ifft; -// use crate::field::field::Field; -// use crate::gadgets::split_join::{split_le_constraints, split_le_generator_local_wires}; -// use crate::gates::deterministic_gate::{DeterministicGate, DeterministicGateAdapter}; -// use crate::gates::gate::GateRef; -// use crate::gates::output_graph::{OutputGraph, GateOutputLocation, ExpandableOutputGraph}; -// use crate::generator::{SimpleGenerator, WitnessGenerator}; -// use crate::target::Target; -// use crate::wire::Wire; -// use crate::witness::PartialWitness; -// use crate::gadgets::conditionals::conditional_multiply_poly; -// -// /// Performs a FRI consistency check. The goal is to check consistency between polynomials `f^(i)` -// /// and `f^(i + 1)`, where `f^(i + 1)` is supposed to be an `arity`-to-one reduction of `f^(i)`. See -// /// the FRI paper for details. -// /// -// /// This check involves `arity` openings of `f^(i)` and a single opening of `f^(i + 1)`. Let's call -// /// the set of `f^(i)` opening locations `{s_j}`, and the `f^(i + 1)` opening location `y`. Note -// /// that all of these points can be derived from the query path. So, we take as input an integer -// /// whose binary decomposition represents the left and right turns in the query path. -// /// -// /// Since our protocol uses a variant of FRI with `k` commit phases, this gate takes `k` opening -// /// sets for `f^(i)`, and `k` openings for `f^(i + 1)`. -// /// -// /// Putting it together, this gate does the following: -// /// - Computes the binary decomposition of the input representing the query path. -// /// - Computes `{s_j}` and `y` from the query path. -// /// - For each commit phase: -// /// - Non-deterministically interpolates a polynomial through each `(s_j, f^(i))`. -// /// - Evaluates the purported interpolant at each `s_j`, and checks that the result matches the -// /// associated opening of `f^(i)(s_j)`. -// /// - Evaluates the purported interpolant at `y`, and checks that the result matches the opening -// /// of `f^(i + 1)(y)`. -// #[derive(Debug, Copy, Clone)] -// pub(crate) struct FriConsistencyGate { -// /// The arity of this reduction step. -// arity_bits: usize, -// -// /// The number of commit phases. -// num_commits: usize, -// -// /// The maximum number of bits in any query path. -// max_path_bits: usize, -// } -// -// impl FriConsistencyGate { -// pub fn new( -// arity_bits: usize, -// num_commits: usize, -// max_path_bits: usize, -// ) -> GateRef { -// let gate = Self { arity_bits, num_commits, max_path_bits }; -// GateRef::new(DeterministicGateAdapter::new(gate)) -// } -// -// fn arity(&self) -> usize { -// 1 << self.arity_bits -// } -// -// /// Generator for the `i`'th layer of FRI. -// pub const CONST_GENERATOR_I: usize = 0; -// -// // Note: These methods relating to wire indices are ordered by wire index. The index -// // calculations are a little hairy since there are quite a few different "sections" of wires, -// // and each section has to calculate where the previous sections left off. To make it more -// // manageable, we have separate functions to calculate the start of each section. There is also -// // a test to make sure that they behave as expected, with no overlapping indices or what not. -// -// /// An integer representing the location of the `f^(i + 1)` node in the reduction tree. Its -// /// `i`th bit in little-endian form represents the direction of the `i`th turn in the query -// /// path, starting from the root. -// pub const WIRE_PATH: usize = 0; -// -// fn start_wire_f_i(&self) -> usize { -// 1 -// } -// -// pub fn wire_f_i(&self, commit_idx: usize, j: usize) -> usize { -// debug_assert!(commit_idx < self.num_commits); -// debug_assert!(j < self.arity()); -// self.start_wire_f_i() + self.arity() * commit_idx + j -// } -// -// fn start_wire_f_i_plus_1(&self) -> usize { -// self.start_wire_f_i() + self.arity() * self.num_commits -// } -// -// pub fn wire_f_i_plus_1(&self, commit_idx: usize) -> usize { -// debug_assert!(commit_idx < self.num_commits); -// self.start_wire_f_i_plus_1() + commit_idx -// } -// -// fn start_wire_path_bits(&self) -> usize { -// self.start_wire_f_i_plus_1() + self.num_commits -// } -// -// /// The `i`th bit of the path. -// fn wire_path_bit_i(&self, i: usize) -> usize { -// self.start_wire_path_bits() + i -// } -// -// fn start_wire_s_j(&self) -> usize { -// self.start_wire_path_bits() + self.max_path_bits -// } -// -// /// The input index of `s_j` (see the FRI paper). -// fn wire_s_j(&self, j: usize) -> usize { -// debug_assert!(j < self.arity()); -// self.start_wire_s_j() + j -// } -// -// fn start_wire_y(&self) -> usize { -// self.start_wire_s_j() + self.arity() -// } -// -// /// The input index of `y` (see the FRI paper). -// fn wire_y(&self) -> usize { -// self.start_wire_y() -// } -// -// fn start_wire_coefficient(&self) -> usize { -// self.start_wire_y() + 1 -// } -// -// /// The wire input index of the j'th coefficient of the interpolant. -// fn wire_coefficient(&self, commit_idx: usize, j: usize) -> usize { -// debug_assert!(commit_idx < self.num_commits); -// debug_assert!(j < self.arity()); -// self.start_wire_coefficient() + commit_idx * self.arity() + j -// } -// -// fn start_unnamed_wires(&self) -> usize { -// self.start_wire_coefficient() + self.num_commits * self.arity() -// } -// -// fn add_s_j_outputs(&self, output_graph: &mut ExpandableOutputGraph) { -// // Each s_j = g^path, where g is the generator for s_j's layer (see CONST_GENERATOR), and -// // path is an integer representing the location of s_j in the reduction tree. This assumes -// // that path is encoded such that its less significant bits are closer to the root of the -// // tree. -// -// // Note about bit ordering: in a FRI reduction tree, the `j`th node in a layer can be -// // written as `g^rev(j)`, where `rev` reverses the bits of its inputs. One way to think of -// // this is that squaring left-shifts the exponent, so for adjacent nodes to have the same -// // square, they must differ only in the left-most bit (which will "overflow" after the -// // left-shift). FFT trees have the same property. -// -// // We start by computing g^0, g^10, g^100, g^1000, ... -// let mut squares = vec![ConstraintPolynomial::local_constant(0)]; -// for _ in 1..self.max_path_bits { -// let prev_square = squares.last().unwrap(); -// let next_square = output_graph.add(prev_square.square()); -// squares.push(next_square) -// } -// -// // We can think of path as having two parts: a less significant part that is common to all -// // {s_j}, and a more significant part that depends on j. We start by computing -// // g^path_common: -// let mut g_exp_path_common = ConstraintPolynomial::zero(); -// let shared_path_bits = self.max_path_bits - self.arity_bits; -// for i in 0..shared_path_bits { -// let bit = ConstraintPolynomial::local_wire(self.wire_path_bit_i(i)); -// g_exp_path_common = conditional_multiply_poly(&g_exp_path_common, &squares[i], &bit); -// g_exp_path_common = output_graph.add(g_exp_path_common); -// } -// -// // Then, we factor in the "extra" powers of g specific to each child. -// for j in 0..self.arity() { -// let mut s_j = g_exp_path_common.clone(); -// for bit_index in 0..self.arity_bits { -// let bit = (j >> bit_index & 1) != 0; -// if bit { -// // See the comment near the top about bit ordering. -// s_j *= &squares[shared_path_bits + self.arity_bits - 1 - bit_index]; -// } -// } -// let s_j_loc = GateOutputLocation::LocalWire(self.wire_s_j(j)); -// output_graph.output_graph.add(s_j_loc, s_j); -// } -// } -// -// fn add_y_output(&self, output_graph: &mut ExpandableOutputGraph) { -// let loc = GateOutputLocation::LocalWire(self.wire_y()); -// // We can start with any s_j and repeatedly square it. We arbitrary pick s_0. -// let mut out = ConstraintPolynomial::local_wire(self.wire_s_j(0)); -// for _ in 0..self.arity_bits { -// out = out.square(); -// } -// output_graph.output_graph.add(loc, out); -// } -// -// fn evaluate_each_poly(&self, commit_idx: usize) -> Vec> { -// let coefficients = (0..self.arity()) -// .map(|i| ConstraintPolynomial::local_wire(self.wire_coefficient(commit_idx, i))) -// .collect::>>(); -// let mut constraints = Vec::new(); -// -// for j in 0..self.arity() { -// // Check the evaluation of f^(i) at s_j. -// let expected = ConstraintPolynomial::local_wire(self.wire_f_i(commit_idx, j)); -// let actual = self.evaluate_poly(&coefficients, -// ConstraintPolynomial::local_wire(self.wire_s_j(j))); -// constraints.push(actual - expected); -// } -// -// // Check the evaluation of f^(i + 1) at y. -// let expected = ConstraintPolynomial::local_wire(self.wire_f_i_plus_1(commit_idx)); -// let actual = self.evaluate_poly(&coefficients, -// ConstraintPolynomial::local_wire(self.wire_y())); -// constraints.push(actual - expected); -// -// constraints -// } -// -// /// Given a polynomial's coefficients, naively evaluate it at a point. -// fn evaluate_poly( -// &self, -// coefficients: &[ConstraintPolynomial], -// point: ConstraintPolynomial, -// ) -> ConstraintPolynomial { -// coefficients.iter() -// .enumerate() -// .map(|(i, coeff)| coeff * point.exp(i)) -// .sum() -// } -// } -// -// impl DeterministicGate for FriConsistencyGate { -// fn id(&self) -> String { -// format!("{:?}", self) -// } -// -// fn outputs(&self, _config: CircuitConfig) -> OutputGraph { -// let mut output_graph = ExpandableOutputGraph::new(self.start_unnamed_wires()); -// self.add_s_j_outputs(&mut output_graph); -// self.add_y_output(&mut output_graph); -// output_graph.output_graph -// } -// -// fn additional_constraints(&self, _config: CircuitConfig) -> Vec> { -// let mut constraints = Vec::new(); -// -// // Add constraints for splitting the path into its binary representation. -// let bits = (0..self.max_path_bits) -// .map(|i| ConstraintPolynomial::local_wire(self.wire_path_bit_i(i))) -// .collect::>(); -// let split_constraints = split_le_constraints( -// ConstraintPolynomial::local_wire(Self::WIRE_PATH), -// &bits); -// constraints.extend(split_constraints); -// -// // Add constraints for checking each polynomial evaluation. -// for commit_idx in 0..self.num_commits { -// constraints.extend(self.evaluate_each_poly(commit_idx)); -// } -// -// constraints -// } -// -// fn additional_generators( -// &self, -// _config: CircuitConfig, -// gate_index: usize, -// local_constants: Vec, -// _next_constants: Vec, -// ) -> Vec>> { -// let interpolant_generator = Box::new( -// InterpolantGenerator { -// gate: *self, -// gate_index, -// generator_i: local_constants[Self::CONST_GENERATOR_I], -// } -// ); -// -// let bit_input_indices = (0..self.max_path_bits) -// .map(|i| self.wire_path_bit_i(i)) -// .collect::>(); -// let split_generator = split_le_generator_local_wires( -// gate_index, Self::WIRE_PATH, &bit_input_indices); -// -// vec![interpolant_generator, split_generator] -// } -// } -// -// #[derive(Debug)] -// struct InterpolantGenerator { -// gate: FriConsistencyGate, -// gate_index: usize, -// generator_i: F, -// } -// -// impl InterpolantGenerator { -// /// Convenience method for converting a wire input index into a Target with our gate index. -// fn local_wire(&self, input: usize) -> Target { -// Target::Wire(Wire { gate: self.gate_index, input }) -// } -// } -// -// impl SimpleGenerator for InterpolantGenerator { -// fn dependencies(&self) -> Vec { -// let mut deps = vec![self.local_wire(FriConsistencyGate::WIRE_PATH)]; -// for i in 0..self.gate.arity() { -// deps.push(self.local_wire(self.gate.wire_s_j(i))); -// for commit_idx in 0..self.gate.num_commits { -// deps.push(self.local_wire(self.gate.wire_f_i(commit_idx, i))); -// } -// } -// deps -// } -// -// fn run_once(&self, witness: &PartialWitness) -> PartialWitness { -// let mut result = PartialWitness::new(); -// -// for commit_idx in 0..self.gate.num_commits { -// let values = (0..self.gate.arity()) -// .map(|j| witness.get_target(self.local_wire(self.gate.wire_f_i(commit_idx, j)))) -// .collect(); -// -// let path = witness.get_target(self.local_wire(FriConsistencyGate::WIRE_PATH)); -// let shift = self.generator_i.exp(path); -// let coeffs = coset_ifft(values, shift); -// -// for (i, coeff) in coeffs.into_iter().enumerate() { -// result.set_target( -// self.local_wire(self.gate.wire_coefficient(commit_idx, i)), -// coeff); -// } -// } -// -// result -// } -// } -// -// #[cfg(test)] -// mod tests { -// use crate::gates::fri_consistency_gate::FriConsistencyGate; -// -// #[test] -// fn wire_indices() { -// let gate = FriConsistencyGate { -// arity_bits: 1, -// num_commits: 2, -// max_path_bits: 4, -// }; -// -// // The actual indices aren't really important, but we want to make sure that -// // - there are no overlaps -// // - there are no gaps -// // - the routed inputs come first -// assert_eq!(0, FriConsistencyGate::WIRE_PATH); -// assert_eq!(1, gate.wire_f_i(0, 0)); -// assert_eq!(2, gate.wire_f_i(0, 1)); -// assert_eq!(3, gate.wire_f_i(1, 0)); -// assert_eq!(4, gate.wire_f_i(1, 1)); -// assert_eq!(5, gate.wire_f_i_plus_1(0)); -// assert_eq!(6, gate.wire_f_i_plus_1(1)); -// assert_eq!(7, gate.wire_path_bit_i(0)); -// assert_eq!(8, gate.wire_path_bit_i(1)); -// assert_eq!(9, gate.wire_path_bit_i(2)); -// assert_eq!(10, gate.wire_path_bit_i(3)); -// assert_eq!(11, gate.wire_s_j(0)); -// assert_eq!(12, gate.wire_s_j(1)); -// assert_eq!(13, gate.wire_y()); -// assert_eq!(14, gate.wire_coefficient(0, 0)); -// assert_eq!(15, gate.wire_coefficient(0, 1)); -// assert_eq!(16, gate.wire_coefficient(1, 0)); -// assert_eq!(17, gate.wire_coefficient(1, 1)); -// assert_eq!(18, gate.start_unnamed_wires()); -// } -// } diff --git a/src/gates/gate_testing.rs b/src/gates/gate_testing.rs new file mode 100644 index 00000000..b7a62c7e --- /dev/null +++ b/src/gates/gate_testing.rs @@ -0,0 +1,68 @@ +use crate::field::field::Field; +use crate::gates::gate::{Gate, GateRef}; +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: GateRef) { + let gate = gate.0; + 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/gmimc.rs b/src/gates/gmimc.rs index de957deb..da8ce670 100644 --- a/src/gates/gmimc.rs +++ b/src/gates/gmimc.rs @@ -59,6 +59,11 @@ impl GMiMCGate { fn wire_cubing_input(i: usize) -> usize { 2 * W + 3 + i } + + /// End of wire indices, exclusive. + fn end() -> usize { + 2 * W + 3 + R + } } impl Gate for GMiMCGate { @@ -223,7 +228,7 @@ impl Gate for GMiMCGate { } fn num_wires(&self) -> usize { - W + 1 + R + Self::end() } fn num_constants(&self) -> usize { @@ -349,6 +354,7 @@ mod tests { use crate::gmimc::gmimc_permute_naive; use crate::wire::Wire; use crate::witness::PartialWitness; + use crate::gates::gate_testing::test_low_degree; #[test] fn generated_output() { @@ -411,4 +417,14 @@ mod tests { }); assert_eq!(acc_new, F::from_canonical_usize(7 * 2)); } + + #[test] + fn low_degree() { + type F = CrandallField; + const R: usize = 101; + let constants = Arc::new([F::TWO; R]); + type Gate = GMiMCGate; + let gate = Gate::with_constants(constants); + test_low_degree(gate) + } } diff --git a/src/gates/gmimc_eval.rs b/src/gates/gmimc_eval.rs index 413ccd70..57d9206c 100644 --- a/src/gates/gmimc_eval.rs +++ b/src/gates/gmimc_eval.rs @@ -1,3 +1,5 @@ +use std::marker::PhantomData; + use crate::circuit_builder::CircuitBuilder; use crate::field::field::Field; use crate::gates::gate::{Gate, GateRef}; @@ -6,7 +8,6 @@ use crate::target::Target; use crate::vars::{EvaluationTargets, EvaluationVars}; use crate::wire::Wire; use crate::witness::PartialWitness; -use std::marker::PhantomData; /// Performs some arithmetic involved in the evaluation of GMiMC's constraint polynomials for one /// round. In particular, this performs the following computations: @@ -224,3 +225,15 @@ impl SimpleGenerator for GMiMCEvalGenerator { witness } } + +#[cfg(test)] +mod tests { + use crate::field::crandall_field::CrandallField; + use crate::gates::gate_testing::test_low_degree; + use crate::gates::gmimc_eval::GMiMCEvalGate; + + #[test] + fn low_degree() { + test_low_degree(GMiMCEvalGate::::get()) + } +} diff --git a/src/gates/interpolation.rs b/src/gates/interpolation.rs new file mode 100644 index 00000000..732f28ba --- /dev/null +++ b/src/gates/interpolation.rs @@ -0,0 +1,273 @@ +use std::convert::TryInto; +use std::marker::PhantomData; +use std::ops::Range; + +use crate::circuit_builder::CircuitBuilder; +use crate::field::extension_field::{Extendable, FieldExtension}; +use crate::field::field::Field; +use crate::field::lagrange::interpolant; +use crate::gates::gate::{Gate, GateRef}; +use crate::generator::{SimpleGenerator, WitnessGenerator}; +use crate::polynomial::polynomial::PolynomialCoeffs; +use crate::target::Target; +use crate::vars::{EvaluationTargets, EvaluationVars}; +use crate::wire::Wire; +use crate::witness::PartialWitness; + +/// Evaluates the interpolant of some given elements from a field extension. +/// +/// In particular, this gate takes as inputs `num_points` points, `num_points` values, and the point +/// to evaluate the interpolant at. It computes the interpolant and outputs its evaluation at the +/// given point. +#[derive(Clone, Debug)] +pub(crate) struct InterpolationGate, const D: usize> { + num_points: usize, + _phantom: PhantomData, +} + +impl, const D: usize> InterpolationGate { + pub fn new(num_points: usize) -> GateRef { + let gate = Self { + num_points, + _phantom: PhantomData, + }; + GateRef::new(gate) + } + + fn start_points(&self) -> usize { + 0 + } + + /// Wire indices of the `i`th interpolant point. + pub fn wire_point(&self, i: usize) -> usize { + debug_assert!(i < self.num_points); + self.start_points() + i + } + + fn start_values(&self) -> usize { + self.start_points() + self.num_points + } + + /// Wire indices of the `i`th interpolant value. + pub fn wires_value(&self, i: usize) -> Range { + debug_assert!(i < self.num_points); + let start = self.start_values() + i * D; + start..start + D + } + + fn start_evaluation_point(&self) -> usize { + self.start_values() + self.num_points * D + } + + /// Wire indices of the point to evaluate the interpolant at. + pub fn wires_evaluation_point(&self) -> Range { + let start = self.start_evaluation_point(); + start..start + D + } + + fn start_evaluation_value(&self) -> usize { + self.start_evaluation_point() + D + } + + /// Wire indices of the interpolated value. + pub fn wires_evaluation_value(&self) -> Range { + let start = self.start_evaluation_value(); + start..start + D + } + + fn start_coeffs(&self) -> usize { + self.start_evaluation_value() + D + } + + /// Wire indices of the interpolant's `i`th coefficient. + pub fn wires_coeff(&self, i: usize) -> Range { + debug_assert!(i < self.num_points); + let start = self.start_coeffs() + i * D; + start..start + D + } + + /// End of wire indices, exclusive. + fn end(&self) -> usize { + self.start_coeffs() + self.num_points * D + } +} + +impl, const D: usize> Gate for InterpolationGate { + fn id(&self) -> String { + format!("{:?}", self, D) + } + + fn eval_unfiltered(&self, vars: EvaluationVars) -> Vec { + let mut constraints = Vec::with_capacity(self.num_constraints()); + + let coeffs = (0..self.num_points) + .map(|i| vars.get_local_ext(self.wires_coeff(i))) + .collect(); + let interpolant = PolynomialCoeffs::new(coeffs); + + 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 + } + + fn eval_unfiltered_recursively( + &self, + builder: &mut CircuitBuilder, + vars: EvaluationTargets, + ) -> Vec { + todo!() + } + + fn generators( + &self, + gate_index: usize, + _local_constants: &[F], + ) -> Vec>> { + let gen = InterpolationGenerator:: { + gate_index, + gate: self.clone(), + _phantom: PhantomData, + }; + vec![Box::new(gen)] + } + + fn num_wires(&self) -> usize { + self.end() + } + + fn num_constants(&self) -> usize { + 0 + } + + fn degree(&self) -> usize { + // 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 { + // 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 + } +} + +struct InterpolationGenerator, const D: usize> { + gate_index: usize, + gate: InterpolationGate, + _phantom: PhantomData, +} + +impl, const D: usize> SimpleGenerator + for InterpolationGenerator +{ + fn dependencies(&self) -> Vec { + let local_target = |input| { + Target::Wire(Wire { + gate: self.gate_index, + input, + }) + }; + + let local_targets = |inputs: Range| inputs.map(local_target); + + let mut deps = Vec::new(); + deps.extend(local_targets(self.gate.wires_evaluation_point())); + deps.extend(local_targets(self.gate.wires_evaluation_value())); + for i in 0..self.gate.num_points { + deps.push(local_target(self.gate.wire_point(i))); + deps.extend(local_targets(self.gate.wires_value(i))); + deps.extend(local_targets(self.gate.wires_coeff(i))); + } + deps + } + + fn run_once(&self, witness: &PartialWitness) -> PartialWitness { + let n = self.gate.num_points; + + let local_wire = |input| Wire { + gate: self.gate_index, + input, + }; + + let get_local_wire = |input| witness.get_wire(local_wire(input)); + + let get_local_ext = |wire_range: Range| { + debug_assert_eq!(wire_range.len(), D); + let values = wire_range.map(get_local_wire).collect::>(); + let arr = values.try_into().unwrap(); + F::Extension::from_basefield_array(arr) + }; + + // Compute the interpolant. + let points = (0..n) + .map(|i| { + ( + F::Extension::from_basefield(get_local_wire(self.gate.wire_point(i))), + get_local_ext(self.gate.wires_value(i)), + ) + }) + .collect::>(); + let interpolant = interpolant(&points); + + let mut result = PartialWitness::::new(); + for (i, &coeff) in interpolant.coeffs.iter().enumerate() { + let wires = self.gate.wires_coeff(i).map(local_wire); + result.set_ext_wires(wires, coeff); + } + + let evaluation_point = get_local_ext(self.gate.wires_evaluation_point()); + let evaluation_value = interpolant.eval(evaluation_point); + let evaluation_value_wires = self.gate.wires_evaluation_value().map(local_wire); + result.set_ext_wires(evaluation_value_wires, evaluation_value); + + result + } +} + +#[cfg(test)] +mod tests { + use std::marker::PhantomData; + + use crate::field::crandall_field::CrandallField; + use crate::gates::gate::Gate; + use crate::gates::gate_testing::test_low_degree; + use crate::gates::interpolation::InterpolationGate; + + #[test] + fn wire_indices() { + let gate = InterpolationGate:: { + 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); + assert_eq!(gate.wire_point(1), 1); + assert_eq!(gate.wires_value(0), 2..6); + assert_eq!(gate.wires_value(1), 6..10); + assert_eq!(gate.wires_evaluation_point(), 10..14); + assert_eq!(gate.wires_evaluation_value(), 14..18); + assert_eq!(gate.wires_coeff(0), 18..22); + assert_eq!(gate.wires_coeff(1), 22..26); + assert_eq!(gate.num_wires(), 26); + } + + #[test] + fn low_degree() { + type F = CrandallField; + test_low_degree(InterpolationGate::::new(4)); + test_low_degree(InterpolationGate::::new(4)); + } +} diff --git a/src/gates/mod.rs b/src/gates/mod.rs index f222549d..afc0b904 100644 --- a/src/gates/mod.rs +++ b/src/gates/mod.rs @@ -1,7 +1,10 @@ pub(crate) mod arithmetic; pub mod constant; -pub(crate) mod fri_consistency_gate; pub(crate) mod gate; pub mod gmimc; pub(crate) mod gmimc_eval; +mod interpolation; pub(crate) mod noop; + +#[cfg(test)] +mod gate_testing; diff --git a/src/gates/noop.rs b/src/gates/noop.rs index edd4e5dd..fdde6ec6 100644 --- a/src/gates/noop.rs +++ b/src/gates/noop.rs @@ -55,3 +55,15 @@ impl Gate for NoopGate { 0 } } + +#[cfg(test)] +mod tests { + use crate::field::crandall_field::CrandallField; + use crate::gates::gate_testing::test_low_degree; + use crate::gates::noop::NoopGate; + + #[test] + fn low_degree() { + test_low_degree(NoopGate::get::()) + } +} diff --git a/src/plonk_challenger.rs b/src/plonk_challenger.rs index 67957e13..6a1a8888 100644 --- a/src/plonk_challenger.rs +++ b/src/plonk_challenger.rs @@ -304,7 +304,7 @@ mod tests { } let config = CircuitConfig { - num_wires: 114, + num_wires: 12 + 12 + 3 + 101, num_routed_wires: 27, ..CircuitConfig::default() }; 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]; diff --git a/src/vars.rs b/src/vars.rs index f2744e8f..7c0afab9 100644 --- a/src/vars.rs +++ b/src/vars.rs @@ -1,3 +1,7 @@ +use std::convert::TryInto; +use std::ops::Range; + +use crate::field::extension_field::{Extendable, FieldExtension}; use crate::field::field::Field; use crate::target::Target; @@ -7,6 +11,17 @@ pub struct EvaluationVars<'a, F: Field> { pub(crate) local_wires: &'a [F], } +impl<'a, F: Field> EvaluationVars<'a, F> { + pub fn get_local_ext(&self, wire_range: Range) -> F::Extension + where + F: Extendable, + { + debug_assert_eq!(wire_range.len(), D); + let arr = self.local_wires[wire_range].try_into().unwrap(); + F::Extension::from_basefield_array(arr) + } +} + #[derive(Copy, Clone)] pub struct EvaluationTargets<'a> { pub(crate) local_constants: &'a [Target], diff --git a/src/witness.rs b/src/witness.rs index 42b7150c..a0b4b2a4 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use crate::field::extension_field::{Extendable, FieldExtension}; use crate::field::field::Field; use crate::target::Target; use crate::wire::Wire; @@ -73,6 +74,24 @@ impl PartialWitness { self.set_target(Target::Wire(wire), value) } + pub fn set_wires(&mut self, wires: W, values: &[F]) + where + W: IntoIterator, + { + // If we used itertools, we could use zip_eq for extra safety. + for (wire, &value) in wires.into_iter().zip(values) { + self.set_wire(wire, value); + } + } + + pub fn set_ext_wires(&mut self, wires: W, value: F::Extension) + where + F: Extendable, + W: IntoIterator, + { + self.set_wires(wires, &value.to_basefield_array()); + } + pub fn extend(&mut self, other: PartialWitness) { for (target, value) in other.target_values { self.set_target(target, value);