mirror of
https://github.com/logos-storage/plonky2.git
synced 2026-01-02 13:53:07 +00:00
FriConsistencyGate
This commit is contained in:
parent
ea33c5567f
commit
ca7f20bf45
@ -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<F: Field> ConstraintPolynomial<F> {
|
||||
Self::constant(F::from_canonical_usize(c))
|
||||
}
|
||||
|
||||
// TODO: const?
|
||||
pub fn zero() -> Self {
|
||||
Self::constant(F::ZERO)
|
||||
}
|
||||
@ -55,18 +56,18 @@ impl<F: Field> ConstraintPolynomial<F> {
|
||||
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<F: Field> ConstraintPolynomialInner<F> {
|
||||
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::<F>::local_wire_value(0);
|
||||
let wire0 = ConstraintPolynomial::<F>::local_wire(0);
|
||||
// == should compare the pointers, and the clone should point to the same underlying
|
||||
// ConstraintPolynomialInner.
|
||||
assert_eq!(wire0.clone(), wire0);
|
||||
|
||||
@ -121,6 +121,22 @@ pub fn fft_with_precomputation_power_of_2<F: Field>(
|
||||
reverse_index_bits(evaluations)
|
||||
}
|
||||
|
||||
pub fn coset_fft<F: Field>(coefficients: Vec<F>, shift: F) -> Vec<F> {
|
||||
fft(coefficients)
|
||||
.into_iter()
|
||||
.map(|x| x * shift)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn coset_ifft<F: Field>(points: Vec<F>, shift: F) -> Vec<F> {
|
||||
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};
|
||||
|
||||
25
src/fri.rs
25
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<usize>,
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
13
src/gadgets/conditionals.rs
Normal file
13
src/gadgets/conditionals.rs
Normal file
@ -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<F: Field>(
|
||||
x: &ConstraintPolynomial<F>,
|
||||
y: &ConstraintPolynomial<F>,
|
||||
c: &ConstraintPolynomial<F>,
|
||||
) -> ConstraintPolynomial<F> {
|
||||
let product = x * y;
|
||||
let delta = product - x;
|
||||
x + c * delta
|
||||
}
|
||||
2
src/gadgets/mod.rs
Normal file
2
src/gadgets/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub(crate) mod split_join;
|
||||
pub(crate) mod conditionals;
|
||||
71
src/gadgets/split_join.rs
Normal file
71
src/gadgets/split_join.rs
Normal file
@ -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<F: Field>(
|
||||
integer: ConstraintPolynomial<F>,
|
||||
bits: &[ConstraintPolynomial<F>],
|
||||
) -> Vec<ConstraintPolynomial<F>> {
|
||||
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<F: Field>(
|
||||
integer: Target,
|
||||
bits: Vec<Target>,
|
||||
) -> Box<dyn WitnessGenerator<F>> {
|
||||
Box::new(SplitGenerator { integer, bits })
|
||||
}
|
||||
|
||||
/// Generator for a little-endian split.
|
||||
pub fn split_le_generator_local_wires<F: Field>(
|
||||
gate: usize,
|
||||
integer_input_index: usize,
|
||||
bit_input_indices: &[usize],
|
||||
) -> Box<dyn WitnessGenerator<F>> {
|
||||
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<Target>,
|
||||
}
|
||||
|
||||
impl<F: Field> SimpleGenerator<F> for SplitGenerator {
|
||||
fn dependencies(&self) -> Vec<Target> {
|
||||
vec![self.integer]
|
||||
}
|
||||
|
||||
fn run_once(&mut self, witness: &PartialWitness<F>) -> PartialWitness<F> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -35,6 +35,8 @@ pub trait DeterministicGate<F: Field>: 'static {
|
||||
&self,
|
||||
_config: CircuitConfig,
|
||||
_gate_index: usize,
|
||||
local_constants: Vec<F>,
|
||||
next_constants: Vec<F>,
|
||||
) -> Vec<Box<dyn WitnessGenerator<F>>> {
|
||||
Vec::new()
|
||||
}
|
||||
@ -90,7 +92,8 @@ impl<F: Field, DG: DeterministicGate<F>> Gate<F> for DeterministicGateAdapter<F,
|
||||
let b: Box::<dyn WitnessGenerator<F>> = 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<F: Field> SimpleGenerator<F> for OutputGenerator<F> {
|
||||
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 {
|
||||
|
||||
370
src/gates/fri_consistency_gate.rs
Normal file
370
src/gates/fri_consistency_gate.rs
Normal file
@ -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<F: Field>(
|
||||
arity_bits: usize,
|
||||
num_commits: usize,
|
||||
max_path_bits: usize,
|
||||
) -> GateRef<F> {
|
||||
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<F: Field>(&self, output_graph: &mut ExpandableOutputGraph<F>) {
|
||||
// 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<F: Field>(&self, output_graph: &mut ExpandableOutputGraph<F>) {
|
||||
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<F: Field>(&self, commit_idx: usize) -> Vec<ConstraintPolynomial<F>> {
|
||||
let coefficients = (0..self.arity())
|
||||
.map(|i| ConstraintPolynomial::local_wire(self.wire_coefficient(commit_idx, i)))
|
||||
.collect::<Vec<ConstraintPolynomial<F>>>();
|
||||
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<F: Field>(
|
||||
&self,
|
||||
coefficients: &[ConstraintPolynomial<F>],
|
||||
point: ConstraintPolynomial<F>,
|
||||
) -> ConstraintPolynomial<F> {
|
||||
coefficients.iter()
|
||||
.enumerate()
|
||||
.map(|(i, coeff)| coeff * point.exp(i))
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Field> DeterministicGate<F> for FriConsistencyGate {
|
||||
fn id(&self) -> String {
|
||||
format!("{:?}", self)
|
||||
}
|
||||
|
||||
fn outputs(&self, config: CircuitConfig) -> OutputGraph<F> {
|
||||
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<ConstraintPolynomial<F>> {
|
||||
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::<Vec<_>>();
|
||||
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<F>,
|
||||
next_constants: Vec<F>,
|
||||
) -> Vec<Box<dyn WitnessGenerator<F>>> {
|
||||
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::<Vec<_>>();
|
||||
let split_generator = split_le_generator_local_wires(
|
||||
gate_index, Self::WIRE_PATH, &bit_input_indices);
|
||||
|
||||
vec![interpolant_generator, split_generator]
|
||||
}
|
||||
}
|
||||
|
||||
struct InterpolantGenerator<F: Field> {
|
||||
gate: FriConsistencyGate,
|
||||
gate_index: usize,
|
||||
generator_i: F,
|
||||
}
|
||||
|
||||
impl<F: Field> InterpolantGenerator<F> {
|
||||
/// 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<F: Field> SimpleGenerator<F> for InterpolantGenerator<F> {
|
||||
fn dependencies(&self) -> Vec<Target> {
|
||||
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<F>) -> PartialWitness<F> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@ -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<F: Field, const W: usize, const R: usize> GMiMCGate<F, W, R> {
|
||||
|
||||
/// 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<F: Field, const W: usize, const R: usize> GMiMCGate<F, W, R> {
|
||||
///
|
||||
/// 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<F>,
|
||||
poly: ConstraintPolynomial<F>,
|
||||
) -> ConstraintPolynomial<F> {
|
||||
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<F: Field, const W: usize, const R: usize> DeterministicGate<F> for GMiMCGat
|
||||
format!("{:?}", self)
|
||||
}
|
||||
|
||||
fn outputs(&self, config: CircuitConfig) -> OutputGraph<F> {
|
||||
fn outputs(&self, _config: CircuitConfig) -> OutputGraph<F> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<F: Field, const W: usize, const R: usize> DeterministicGate<F> for GMiMCGat
|
||||
}
|
||||
|
||||
fn additional_constraints(&self, _config: CircuitConfig) -> Vec<ConstraintPolynomial<F>> {
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<F: Field> OutputGraph<F> {
|
||||
/// 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<F: Field> OutputGraph<F> {
|
||||
/// The largest local wire index among this graph's output locations.
|
||||
pub fn max_local_output_index(&self) -> Option<usize> {
|
||||
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<F: Field> {
|
||||
pub(crate) output_graph: OutputGraph<F>,
|
||||
next_unused_index: usize,
|
||||
}
|
||||
|
||||
impl<F: Field> ExpandableOutputGraph<F> {
|
||||
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<F>) -> ConstraintPolynomial<F> {
|
||||
let index = self.next_unused_index;
|
||||
self.next_unused_index += 1;
|
||||
|
||||
self.output_graph.add(GateOutputLocation::LocalWire(index), poly);
|
||||
ConstraintPolynomial::local_wire(index)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::field::field::Field;
|
||||
|
||||
@ -17,6 +17,7 @@ mod circuit_data;
|
||||
mod constraint_polynomial;
|
||||
mod field;
|
||||
mod fri;
|
||||
mod gadgets;
|
||||
mod gates;
|
||||
mod generator;
|
||||
mod gmimc;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -47,7 +47,11 @@ impl<F: Field> PartialWitness<F> {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user