diff --git a/src/constraint_polynomial.rs b/src/constraint_polynomial.rs index 45bf7003..847b5f22 100644 --- a/src/constraint_polynomial.rs +++ b/src/constraint_polynomial.rs @@ -16,8 +16,8 @@ use crate::wire::Wire; pub(crate) struct EvaluationVars<'a, F: Field> { pub(crate) local_constants: &'a [F], pub(crate) next_constants: &'a [F], - pub(crate) local_wire_values: &'a [F], - pub(crate) next_wire_values: &'a [F], + pub(crate) local_wires: &'a [F], + pub(crate) next_wires: &'a [F], } /// A polynomial over all the variables that are subject to constraints (local constants, next @@ -39,6 +39,7 @@ impl ConstraintPolynomial { Self::constant(F::from_canonical_usize(c)) } + // TODO: const? pub fn zero() -> Self { Self::constant(F::ZERO) } @@ -55,18 +56,18 @@ impl ConstraintPolynomial { Self::from_inner(ConstraintPolynomialInner::NextConstant(index)) } - pub fn local_wire_value(index: usize) -> Self { + pub fn local_wire(index: usize) -> Self { Self::from_inner(ConstraintPolynomialInner::LocalWireValue(index)) } - pub fn next_wire_value(index: usize) -> Self { + pub fn next_wire(index: usize) -> Self { Self::from_inner(ConstraintPolynomialInner::NextWireValue(index)) } pub fn from_gate_output(gate_output: GateOutputLocation) -> Self { match gate_output { - GateOutputLocation::LocalWire(i) => Self::local_wire_value(i), - GateOutputLocation::NextWire(i) => Self::next_wire_value(i), + GateOutputLocation::LocalWire(i) => Self::local_wire(i), + GateOutputLocation::NextWire(i) => Self::next_wire(i), } } @@ -522,8 +523,8 @@ impl ConstraintPolynomialInner { ConstraintPolynomialInner::Constant(c) => *c, ConstraintPolynomialInner::LocalConstant(i) => vars.local_constants[*i], ConstraintPolynomialInner::NextConstant(i) => vars.next_constants[*i], - ConstraintPolynomialInner::LocalWireValue(i) => vars.local_wire_values[*i], - ConstraintPolynomialInner::NextWireValue(i) => vars.next_wire_values[*i], + ConstraintPolynomialInner::LocalWireValue(i) => vars.local_wires[*i], + ConstraintPolynomialInner::NextWireValue(i) => vars.next_wires[*i], ConstraintPolynomialInner::Sum { lhs, rhs } => { let lhs = lhs.evaluate_memoized(vars, mem); let rhs = rhs.evaluate_memoized(vars, mem); @@ -564,7 +565,7 @@ mod tests { #[test] fn equality() { type F = CrandallField; - let wire0 = ConstraintPolynomial::::local_wire_value(0); + let wire0 = ConstraintPolynomial::::local_wire(0); // == should compare the pointers, and the clone should point to the same underlying // ConstraintPolynomialInner. assert_eq!(wire0.clone(), wire0); diff --git a/src/field/fft.rs b/src/field/fft.rs index a75f1de5..bc0353b0 100644 --- a/src/field/fft.rs +++ b/src/field/fft.rs @@ -121,6 +121,22 @@ pub fn fft_with_precomputation_power_of_2( reverse_index_bits(evaluations) } +pub fn coset_fft(coefficients: Vec, shift: F) -> Vec { + fft(coefficients) + .into_iter() + .map(|x| x * shift) + .collect() +} + +pub fn coset_ifft(points: Vec, shift: F) -> Vec { + let shift_inv = shift.inverse(); + let precomputation = fft_precompute(points.len()); + ifft_with_precomputation_power_of_2(points, &precomputation) + .into_iter() + .map(|x| x * shift_inv) + .collect() +} + // #[cfg(test)] // mod tests { // use crate::{Bls12377Scalar, fft_precompute, fft_with_precomputation, CrandallField, ifft_with_precomputation_power_of_2}; diff --git a/src/fri.rs b/src/fri.rs index d7ebfee5..3c166779 100644 --- a/src/fri.rs +++ b/src/fri.rs @@ -2,20 +2,35 @@ /// while increasing L, potentially requiring more challenge points. const EPSILON: f64 = 0.01; +struct FriConfig { + proof_of_work_bits: usize, + + /// The arity of each FRI reduction step, expressed (i.e. the log2 of the actual arity). + /// For example, `[3, 2, 1]` would describe a FRI reduction tree with 8-to-1 reduction, then + /// a 4-to-1 reduction, then a 2-to-1 reduction. After these reductions, the reduced polynomial + /// is sent directly. + reduction_arity_bits: Vec, +} + fn fri_delta(rate_log: usize, conjecture: bool) -> f64 { let rate = (1 << rate_log) as f64; if conjecture { - todo!() + // See Conjecture 2.3 in DEEP-FRI. + 1.0 - rate - EPSILON } else { - return 1.0 - rate.sqrt() - EPSILON; + // See the Johnson radius. + 1.0 - rate.sqrt() - EPSILON } } -fn fri_l(rate_log: usize, conjecture: bool) -> f64 { +fn fri_l(codeword_len: usize, rate_log: usize, conjecture: bool) -> f64 { let rate = (1 << rate_log) as f64; if conjecture { - todo!() + // See Conjecture 2.3 in DEEP-FRI. + // We assume the conjecture holds with a constant of 1 (as do other STARK implementations). + (codeword_len as f64) / EPSILON } else { - return 1.0 / (2.0 * EPSILON * rate.sqrt()); + // See the Johnson bound. + 1.0 / (2.0 * EPSILON * rate.sqrt()) } } diff --git a/src/gadgets/conditionals.rs b/src/gadgets/conditionals.rs new file mode 100644 index 00000000..fd8683be --- /dev/null +++ b/src/gadgets/conditionals.rs @@ -0,0 +1,13 @@ +use crate::constraint_polynomial::ConstraintPolynomial; +use crate::field::field::Field; + +/// Evaluates to `x` if `c == 0`, or `x * y` if `c == 1`. +pub fn conditional_multiply_poly( + x: &ConstraintPolynomial, + y: &ConstraintPolynomial, + c: &ConstraintPolynomial, +) -> ConstraintPolynomial { + let product = x * y; + let delta = product - x; + x + c * delta +} diff --git a/src/gadgets/mod.rs b/src/gadgets/mod.rs new file mode 100644 index 00000000..f2de26aa --- /dev/null +++ b/src/gadgets/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod split_join; +pub(crate) mod conditionals; diff --git a/src/gadgets/split_join.rs b/src/gadgets/split_join.rs new file mode 100644 index 00000000..751f82f4 --- /dev/null +++ b/src/gadgets/split_join.rs @@ -0,0 +1,71 @@ +use std::iter; + +use crate::constraint_polynomial::ConstraintPolynomial; +use crate::field::field::Field; +use crate::generator::{SimpleGenerator, WitnessGenerator}; +use crate::target::Target; +use crate::witness::PartialWitness; +use crate::wire::Wire; + +/// Constraints for a little-endian split. +pub fn split_le_constraints( + integer: ConstraintPolynomial, + bits: &[ConstraintPolynomial], +) -> Vec> { + let weighted_sum = bits.iter() + .fold(ConstraintPolynomial::zero(), |acc, b| acc.double() + b); + bits.iter() + .rev() + .map(|b| b * (b - 1)) + .chain(iter::once(weighted_sum - integer)) + .collect() +} + +/// Generator for a little-endian split. +pub fn split_le_generator( + integer: Target, + bits: Vec, +) -> Box> { + Box::new(SplitGenerator { integer, bits }) +} + +/// Generator for a little-endian split. +pub fn split_le_generator_local_wires( + gate: usize, + integer_input_index: usize, + bit_input_indices: &[usize], +) -> Box> { + let integer = Target::Wire( + Wire { gate, input: integer_input_index }); + let bits = bit_input_indices.iter() + .map(|&input| Target::Wire(Wire { gate, input })) + .collect(); + Box::new(SplitGenerator { integer, bits }) +} + +struct SplitGenerator { + integer: Target, + bits: Vec, +} + +impl SimpleGenerator for SplitGenerator { + fn dependencies(&self) -> Vec { + vec![self.integer] + } + + fn run_once(&mut self, witness: &PartialWitness) -> PartialWitness { + let mut integer_value = witness.get_target(self.integer).to_canonical_u64(); + + let mut result = PartialWitness::new(); + for &b in &self.bits { + let b_value = integer_value & 1; + result.set_target(b, F::from_canonical_u64(b_value)); + integer_value >>= 1; + } + + debug_assert_eq!(integer_value, 0, + "Integer too large to fit in given number of bits"); + + result + } +} diff --git a/src/gates/deterministic_gate.rs b/src/gates/deterministic_gate.rs index 49b60049..d66c5e03 100644 --- a/src/gates/deterministic_gate.rs +++ b/src/gates/deterministic_gate.rs @@ -35,6 +35,8 @@ pub trait DeterministicGate: 'static { &self, _config: CircuitConfig, _gate_index: usize, + local_constants: Vec, + next_constants: Vec, ) -> Vec>> { Vec::new() } @@ -90,7 +92,8 @@ impl> Gate for DeterministicGateAdapter> = Box::new(og); b }) - .chain(self.gate.additional_generators(config, gate_index)) + .chain(self.gate.additional_generators( + config, gate_index, local_constants.clone(), next_constants.clone())) .collect() } } @@ -136,8 +139,8 @@ impl SimpleGenerator for OutputGenerator { let vars = EvaluationVars { local_constants: &self.local_constants, next_constants: &self.next_constants, - local_wire_values: &local_wire_values, - next_wire_values: &next_wire_values, + local_wires: &local_wire_values, + next_wires: &next_wire_values, }; let result_wire = match self.location { diff --git a/src/gates/fri_consistency_gate.rs b/src/gates/fri_consistency_gate.rs new file mode 100644 index 00000000..ee8e1099 --- /dev/null +++ b/src/gates/fri_consistency_gate.rs @@ -0,0 +1,370 @@ +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)] +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); + } + + // 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] + } +} + +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(&mut 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/gmimc.rs b/src/gates/gmimc.rs index 86058e9f..2aa90284 100644 --- a/src/gates/gmimc.rs +++ b/src/gates/gmimc.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use num::{BigUint, FromPrimitive, One}; +use num::{BigUint, One}; use crate::circuit_data::CircuitConfig; use crate::constraint_polynomial::ConstraintPolynomial; @@ -29,7 +29,6 @@ impl GMiMCGate { /// If this is set to 1, the first four inputs will be swapped with the next four inputs. This /// is useful for ordering hashes in Merkle proofs. Otherwise, this should be set to 0. - // TODO: Assert binary. pub const WIRE_SWITCH: usize = W; /// The wire index for the i'th input to the permutation. @@ -48,13 +47,14 @@ impl GMiMCGate { /// /// This may seem like it belongs in `OutputGraph`, but it is not that general, since it uses /// a notion of "next available" local wire indices specific to this gate. + // TODO: Switch to `ExpandableOutputGraph`. fn add_local( outputs: &mut OutputGraph, poly: ConstraintPolynomial, ) -> ConstraintPolynomial { let index = outputs.max_local_output_index().map_or(W + 1, |i| i + 1); outputs.add(GateOutputLocation::LocalWire(index), poly); - ConstraintPolynomial::local_wire_value(index) + ConstraintPolynomial::local_wire(index) } } @@ -64,15 +64,15 @@ impl DeterministicGate for GMiMCGat format!("{:?}", self) } - fn outputs(&self, config: CircuitConfig) -> OutputGraph { + fn outputs(&self, _config: CircuitConfig) -> OutputGraph { let original_inputs = (0..W) - .map(|i| ConstraintPolynomial::local_wire_value(Self::wire_input(i))) + .map(|i| ConstraintPolynomial::local_wire(Self::wire_input(i))) .collect::>(); let mut outputs = OutputGraph::new(); // Conditionally switch inputs based on the (boolean) switch wire. - let switch = ConstraintPolynomial::local_wire_value(Self::WIRE_SWITCH); + let switch = ConstraintPolynomial::local_wire(Self::WIRE_SWITCH); let mut state = Vec::new(); for i in 0..4 { let a = &original_inputs[i]; @@ -112,7 +112,7 @@ impl DeterministicGate for GMiMCGat } fn additional_constraints(&self, _config: CircuitConfig) -> Vec> { - let switch = ConstraintPolynomial::local_wire_value(Self::WIRE_SWITCH); + let switch = ConstraintPolynomial::local_wire(Self::WIRE_SWITCH); let switch_bool_constraint = &switch * (&switch - 1); vec![switch_bool_constraint] } @@ -123,8 +123,6 @@ mod tests { use std::convert::TryInto; use std::sync::Arc; - use num::ToPrimitive; - use crate::circuit_data::CircuitConfig; use crate::field::crandall_field::CrandallField; use crate::field::field::Field; diff --git a/src/gates/mod.rs b/src/gates/mod.rs index a476af32..c730d8f1 100644 --- a/src/gates/mod.rs +++ b/src/gates/mod.rs @@ -3,3 +3,4 @@ pub(crate) mod deterministic_gate; pub(crate) mod gate; pub(crate) mod gmimc; pub(crate) mod output_graph; +pub(crate) mod fri_consistency_gate; diff --git a/src/gates/output_graph.rs b/src/gates/output_graph.rs index a07f6b95..75cdcd35 100644 --- a/src/gates/output_graph.rs +++ b/src/gates/output_graph.rs @@ -1,10 +1,9 @@ -use std::{fmt, iter}; -use std::collections::HashMap; +use std::fmt; use std::fmt::{Display, Formatter}; -use num::{BigUint, FromPrimitive, One, ToPrimitive}; +use num::ToPrimitive; -use crate::constraint_polynomial::{ConstraintPolynomial, EvaluationVars}; +use crate::constraint_polynomial::ConstraintPolynomial; use crate::field::field::Field; /// Represents a set of deterministic gate outputs, expressed as polynomials over witness @@ -32,7 +31,7 @@ impl OutputGraph { /// The largest polynomial degree among all polynomials in this graph. pub fn degree(&self) -> usize { self.outputs.iter() - .map(|(loc, out)| out.degree().to_usize().unwrap()) + .map(|(_loc, out)| out.degree().to_usize().unwrap()) .max() .unwrap_or(0) } @@ -40,7 +39,7 @@ impl OutputGraph { /// The largest local wire index among this graph's output locations. pub fn max_local_output_index(&self) -> Option { self.outputs.iter() - .filter_map(|(loc, out)| match loc { + .filter_map(|(loc, _out)| match loc { GateOutputLocation::LocalWire(i) => Some(*i), GateOutputLocation::NextWire(_) => None }) @@ -74,3 +73,41 @@ impl Display for GateOutputLocation { } } } + +/// Like `OutputGraph`, but tracks the index of the next available local wire. +/// +/// Many gates have special wires, such as inputs and outputs, which must be placed at specific, +/// known indices. Many gates also use some wires for "intermediate values" which are internal to +/// the gate, and so can be placed at any available index. +/// +/// This object helps to track which wire indices are available to be used for intermediate values. +/// It starts placing intermediate values at a given initial index, and repeatedly increments the +/// index as more intermediate values are added. +/// +/// This assumes that there are no "reserved" indices greater than the given initial value. It also +/// assumes that there is no better place to store these intermediate values (such as wires of the +/// next gate). So this model may not make sense for all gates, which is why this is provided as an +/// optional utility. +pub struct ExpandableOutputGraph { + pub(crate) output_graph: OutputGraph, + next_unused_index: usize, +} + +impl ExpandableOutputGraph { + pub(crate) fn new(next_unused_index: usize) -> Self { + ExpandableOutputGraph { + output_graph: OutputGraph::new(), + next_unused_index, + } + } + + /// Adds an intermediate value at the next available wire index, and returns a + /// `ConstraintPolynomial` pointing to the newly created wire. + pub(crate) fn add(&mut self, poly: ConstraintPolynomial) -> ConstraintPolynomial { + let index = self.next_unused_index; + self.next_unused_index += 1; + + self.output_graph.add(GateOutputLocation::LocalWire(index), poly); + ConstraintPolynomial::local_wire(index) + } +} diff --git a/src/generator.rs b/src/generator.rs index 9d5c4919..ebeced47 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,4 +1,3 @@ -use std::borrow::Borrow; use std::collections::{HashMap, HashSet}; use crate::field::field::Field; diff --git a/src/main.rs b/src/main.rs index 753301f5..125ed1d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ mod circuit_data; mod constraint_polynomial; mod field; mod fri; +mod gadgets; mod gates; mod generator; mod gmimc; diff --git a/src/target.rs b/src/target.rs index 032de2fc..dfe428c2 100644 --- a/src/target.rs +++ b/src/target.rs @@ -1,8 +1,4 @@ -use std::convert::Infallible; -use std::marker::PhantomData; - use crate::circuit_data::CircuitConfig; -use crate::field::field::Field; use crate::wire::Wire; /// A location in the witness. diff --git a/src/witness.rs b/src/witness.rs index ae5abd70..982d98d3 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -47,7 +47,11 @@ impl PartialWitness { } pub fn set_target(&mut self, target: Target, value: F) { - self.target_values.insert(target, value); + let opt_old_value = self.target_values.insert(target, value); + if let Some(old_value) = opt_old_value { + assert_eq!(old_value, value, + "Target was set twice with different values: {:?}", target); + } } pub fn set_wire(&mut self, wire: Wire, value: F) {