No more polynomial programming abstraction

It was too expensive.
This commit is contained in:
Daniel Lubarov 2021-03-28 15:36:51 -07:00
parent ba96ab4e99
commit f42120482a
23 changed files with 1008 additions and 1420 deletions

View File

@ -6,17 +6,17 @@ use log::info;
use crate::circuit_data::{CircuitConfig, CircuitData, CommonCircuitData, ProverCircuitData, ProverOnlyCircuitData, VerifierCircuitData, VerifierOnlyCircuitData};
use crate::field::fft::lde_multiple;
use crate::field::field::Field;
use crate::gates::constant::ConstantGate2;
use crate::gates::gate::{GateInstance, GateRef};
use crate::gates::constant::ConstantGate;
use crate::gates::gate::{Gate, GateInstance, GateRef};
use crate::gates::noop::NoopGate;
use crate::generator::{CopyGenerator, WitnessGenerator};
use crate::hash::merkle_root_bit_rev_order;
use crate::target::Target;
use crate::util::{transpose, log2_strict};
use crate::wire::Wire;
use crate::gates::noop::NoopGate;
use crate::util::transpose;
pub struct CircuitBuilder<F: Field> {
config: CircuitConfig,
pub(crate) config: CircuitConfig,
/// The types of gates used in this circuit.
gates: HashSet<GateRef<F>>,
@ -55,7 +55,7 @@ impl<F: Field> CircuitBuilder<F> {
}
fn check_gate_compatibility(&self, gate: &GateRef<F>) {
assert!(gate.0.min_wires(self.config) <= self.config.num_wires);
assert!(gate.0.num_wires() <= self.config.num_wires);
}
/// Shorthand for `generate_copy` and `assert_equal`.
@ -103,8 +103,8 @@ impl<F: Field> CircuitBuilder<F> {
/// Returns a routable target with the given constant value.
pub fn constant(&mut self, c: F) -> Target {
let gate = self.add_gate(ConstantGate2::get(), vec![c]);
Target::Wire(Wire { gate, input: ConstantGate2::WIRE_OUTPUT })
let gate = self.add_gate(ConstantGate::get(), vec![c]);
Target::Wire(Wire { gate, input: ConstantGate::WIRE_OUTPUT })
}
fn blind_and_pad(&mut self) {
@ -119,10 +119,9 @@ impl<F: Field> CircuitBuilder<F> {
self.gate_instances.iter()
.enumerate()
.flat_map(|(gate_index, gate_inst)| gate_inst.gate_type.0.generators(
self.config,
gate_index,
gate_inst.constants.clone(),
vec![])) // TODO: Not supporting next_const for now.
&gate_inst.constants,
&[])) // TODO: Not supporting next_const for now.
.collect()
}
@ -157,20 +156,32 @@ impl<F: Field> CircuitBuilder<F> {
let constant_vecs = self.constant_vecs();
let constant_ldes = lde_multiple(constant_vecs, self.config.rate_bits);
let constants_root = merkle_root_bit_rev_order(constant_ldes);
let constant_ldes_t = transpose(&constant_ldes);
let constants_root = merkle_root_bit_rev_order(constant_ldes_t.clone());
let sigma_vecs = self.sigma_vecs();
let sigma_ldes = lde_multiple(sigma_vecs, self.config.rate_bits);
let sigmas_root = merkle_root_bit_rev_order(sigma_ldes);
let sigmas_root = merkle_root_bit_rev_order(transpose(&sigma_ldes));
let generators = self.get_generators();
let prover_only = ProverOnlyCircuitData { generators };
let prover_only = ProverOnlyCircuitData { generators, constant_ldes_t };
let verifier_only = VerifierOnlyCircuitData {};
// The HashSet of gates will have a non-deterministic order. When converting to a Vec, we
// sort by ID to make the ordering deterministic.
let mut gates = self.gates.iter().cloned().collect::<Vec<_>>();
gates.sort_unstable_by_key(|gate| gate.0.id());
let num_gate_constraints = gates.iter()
.map(|gate| gate.0.num_constraints())
.max()
.expect("No gates?");
let common = CommonCircuitData {
config: self.config,
degree,
gates: self.gates.iter().cloned().collect(),
degree_bits: log2_strict(degree),
gates,
num_gate_constraints,
constants_root,
sigmas_root,
};

View File

@ -5,6 +5,8 @@ use crate::prover::prove;
use crate::verifier::verify;
use crate::witness::PartialWitness;
use crate::gates::gate::{GateRef};
use crate::constraint_polynomial::{EvaluationVars, EvaluationTargets};
use crate::target::Target;
#[derive(Copy, Clone)]
pub struct CircuitConfig {
@ -12,6 +14,20 @@ pub struct CircuitConfig {
pub num_routed_wires: usize,
pub security_bits: usize,
pub rate_bits: usize,
/// The number of times to repeat checks that have soundness errors of (roughly) `degree / |F|`.
pub num_checks: usize,
}
impl Default for CircuitConfig {
fn default() -> Self {
CircuitConfig {
num_wires: 3,
num_routed_wires: 3,
security_bits: 128,
rate_bits: 3,
num_checks: 3,
}
}
}
impl CircuitConfig {
@ -64,6 +80,7 @@ impl<F: Field> VerifierCircuitData<F> {
/// Circuit data required by the prover, but not the verifier.
pub(crate) struct ProverOnlyCircuitData<F: Field> {
pub generators: Vec<Box<dyn WitnessGenerator<F>>>,
pub constant_ldes_t: Vec<Vec<F>>,
}
/// Circuit data required by the verifier, but not the prover.
@ -73,11 +90,13 @@ pub(crate) struct VerifierOnlyCircuitData {}
pub(crate) struct CommonCircuitData<F: Field> {
pub(crate) config: CircuitConfig,
pub(crate) degree: usize,
pub(crate) degree_bits: usize,
/// The types of gates used in this circuit.
pub(crate) gates: Vec<GateRef<F>>,
pub(crate) num_gate_constraints: usize,
/// A commitment to each constant polynomial.
pub(crate) constants_root: Hash<F>,
@ -86,10 +105,42 @@ pub(crate) struct CommonCircuitData<F: Field> {
}
impl<F: Field> CommonCircuitData<F> {
pub fn constraint_degree(&self, config: CircuitConfig) -> usize {
pub fn degree(&self) -> usize {
1 << self.degree_bits
}
pub fn lde_size(&self) -> usize {
1 << (self.degree_bits + self.config.rate_bits)
}
pub fn lde_generator(&self) -> F {
F::primitive_root_of_unity(self.degree_bits + self.config.rate_bits)
}
pub fn constraint_degree(&self) -> usize {
self.gates.iter()
.map(|g| g.0.degree(config))
.map(|g| g.0.degree())
.max()
.expect("No gates?")
}
pub fn total_constraints(&self) -> usize {
// 2 constraints for each Z check.
self.config.num_checks * 2 + self.num_gate_constraints
}
pub fn evaluate(&self, vars: EvaluationVars<F>) -> Vec<F> {
let mut constraints = vec![F::ZERO; self.num_gate_constraints];
for gate in &self.gates {
let gate_constraints = gate.0.eval_filtered(vars);
for (i, c) in gate_constraints.into_iter().enumerate() {
constraints[i] += c;
}
}
constraints
}
pub fn evaluate_recursive(&self, vars: EvaluationTargets) -> Vec<Target> {
todo!()
}
}

View File

@ -10,570 +10,20 @@ use std::rc::Rc;
use num::{BigUint, FromPrimitive, One, Zero};
use crate::field::field::Field;
use crate::gates::output_graph::GateOutputLocation;
use crate::target::Target;
use crate::wire::Wire;
pub(crate) struct EvaluationVars<'a, F: Field> {
#[derive(Copy, Clone)]
pub struct EvaluationVars<'a, F: Field> {
pub(crate) local_constants: &'a [F],
pub(crate) next_constants: &'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
/// constants, local wire values, and next wire values). This representation does not require any
/// particular form; it permits arbitrary forms such as `(x + 1)^3 + y z`.
///
/// This type implements `Hash` and `Eq` based on references rather
/// than content. This is useful when we want to use constraint polynomials as `HashMap` keys, but
/// we want address-based hashing for performance reasons.
#[derive(Clone)]
pub struct ConstraintPolynomial<F: Field>(pub(crate) Rc<ConstraintPolynomialInner<F>>);
impl<F: Field> ConstraintPolynomial<F> {
pub fn constant(c: F) -> Self {
Self::from_inner(ConstraintPolynomialInner::Constant(c))
}
pub fn constant_usize(c: usize) -> Self {
Self::constant(F::from_canonical_usize(c))
}
// TODO: const?
pub fn zero() -> Self {
Self::constant(F::ZERO)
}
pub fn one() -> Self {
Self::constant(F::ONE)
}
pub fn local_constant(index: usize) -> Self {
Self::from_inner(ConstraintPolynomialInner::LocalConstant(index))
}
pub fn next_constant(index: usize) -> Self {
Self::from_inner(ConstraintPolynomialInner::NextConstant(index))
}
pub fn local_wire(index: usize) -> Self {
Self::from_inner(ConstraintPolynomialInner::LocalWireValue(index))
}
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(i),
GateOutputLocation::NextWire(i) => Self::next_wire(i),
}
}
// TODO: Have these take references?
pub fn add(&self, rhs: &Self) -> Self {
// TODO: Special case for either operand being 0.
Self::from_inner(ConstraintPolynomialInner::Sum {
lhs: self.clone(),
rhs: rhs.clone(),
})
}
pub fn sub(&self, rhs: &Self) -> Self {
// TODO: Special case for either operand being 0.
// TODO: Faster to have a dedicated ConstraintPolynomialInner::Difference?
// TODO: `self + -rhs`?
self.add(&rhs.neg())
}
pub fn double(&self) -> Self {
self.clone().add(self)
}
pub fn triple(&self) -> Self {
self * 3
}
pub fn mul(&self, rhs: &Self) -> Self {
// TODO: Special case for either operand being 1.
Self::from_inner(ConstraintPolynomialInner::Product {
lhs: self.clone(),
rhs: rhs.clone(),
})
}
pub fn exp(&self, exponent: usize) -> Self {
Self::from_inner(ConstraintPolynomialInner::Exponentiation {
base: self.clone(),
exponent,
})
}
pub fn square(&self) -> Self {
self * self
}
pub fn cube(&self) -> Self {
self * self * self
}
pub fn degree(&self) -> BigUint {
self.0.degree()
}
/// Returns the set of wires that this constraint would depend on if it were applied at a
/// certain gate index.
pub(crate) fn dependencies(&self, gate: usize) -> Vec<Wire> {
let mut deps = HashSet::new();
self.0.add_dependencies(gate, &mut deps);
deps.into_iter().collect()
}
/// Find the largest input index among the wires this constraint depends on.
pub(crate) fn max_wire_input_index(&self) -> Option<usize> {
self.dependencies(0)
.into_iter()
.map(|wire| wire.input)
.max()
}
pub(crate) fn max_constant_index(&self) -> Option<usize> {
let mut indices = HashSet::new();
self.0.add_constant_indices(&mut indices);
indices.into_iter().max()
}
pub(crate) fn evaluate(&self, vars: EvaluationVars<F>) -> F {
let results = Self::evaluate_all(&[self.clone()], vars);
assert_eq!(results.len(), 1);
results[0]
}
/// Evaluate multiple constraint polynomials simultaneously. This can be more efficient than
/// evaluating them sequentially, since shared intermediate results will only be computed once.
pub(crate) fn evaluate_all(
polynomials: &[ConstraintPolynomial<F>],
vars: EvaluationVars<F>,
) -> Vec<F> {
let mut mem = HashMap::new();
polynomials.iter()
.map(|p| p.evaluate_memoized(&vars, &mut mem))
.collect()
}
fn evaluate_memoized(
&self,
vars: &EvaluationVars<F>,
mem: &mut HashMap<Self, F>,
) -> F {
if let Some(&result) = mem.get(self) {
result
} else {
let result = self.0.evaluate(vars, mem);
mem.insert(self.clone(), result);
result
}
}
/// Replace all occurrences of `from` with `to` in this polynomial graph.
pub(crate) fn replace_all(
&self,
from: Self,
to: Self,
) -> Self {
self.replace_all_helper(from, to, &mut HashMap::new())
}
/// Replace all occurrences of `from` with `to` in this polynomial graph. In order to preserve
/// the structure of the graph, we keep track of any `ConstraintPolynomial`s that have been
/// replaced already.
fn replace_all_helper(
&self, from: Self,
to: Self,
replacements: &mut HashMap<Self, Self>,
) -> Self {
if *self == from {
return to;
}
if let Some(replacement) = replacements.get(self) {
return replacement.clone();
}
match self.0.borrow() {
ConstraintPolynomialInner::Constant(_) => self.clone(),
ConstraintPolynomialInner::LocalConstant(_) => self.clone(),
ConstraintPolynomialInner::NextConstant(_) => self.clone(),
ConstraintPolynomialInner::LocalWireValue(_) => self.clone(),
ConstraintPolynomialInner::NextWireValue(_) => self.clone(),
ConstraintPolynomialInner::Sum { lhs, rhs } => {
let lhs = lhs.replace_all_helper(from.clone(), to.clone(), replacements);
let rhs = rhs.replace_all_helper(from, to, replacements);
let replacement = Self::from_inner(ConstraintPolynomialInner::Sum { lhs, rhs });
debug_assert!(!replacements.contains_key(self));
replacements.insert(self.clone(), replacement.clone());
replacement
}
ConstraintPolynomialInner::Product { lhs, rhs } => {
let lhs = lhs.replace_all_helper(from.clone(), to.clone(), replacements);
let rhs = rhs.replace_all_helper(from, to, replacements);
let replacement = Self::from_inner(ConstraintPolynomialInner::Product { lhs, rhs });
debug_assert!(!replacements.contains_key(self));
replacements.insert(self.clone(), replacement.clone());
replacement
}
ConstraintPolynomialInner::Exponentiation { base, exponent } => {
let base = base.replace_all_helper(from, to, replacements);
let replacement = Self::from_inner(
ConstraintPolynomialInner::Exponentiation { base, exponent: *exponent });
debug_assert!(!replacements.contains_key(self));
replacements.insert(self.clone(), replacement.clone());
replacement
}
}
}
fn from_inner(inner: ConstraintPolynomialInner<F>) -> Self {
Self(Rc::new(inner))
}
}
impl<F: Field> Debug for ConstraintPolynomial<F> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl<F: Field> Display for ConstraintPolynomial<F> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl<F: Field> PartialEq for ConstraintPolynomial<F> {
fn eq(&self, other: &Self) -> bool {
ptr::eq(&*self.0, &*other.0)
}
}
impl<F: Field> Eq for ConstraintPolynomial<F> {}
impl<F: Field> Hash for ConstraintPolynomial<F> {
fn hash<H: Hasher>(&self, state: &mut H) {
ptr::hash(&*self.0, state);
}
}
impl<F: Field> Neg for ConstraintPolynomial<F> {
type Output = Self;
fn neg(self) -> Self {
// TODO: Faster to have a dedicated ConstraintPolynomialInner::Negation?
self * ConstraintPolynomial::constant(F::NEG_ONE)
}
}
impl<F: Field> Neg for &ConstraintPolynomial<F> {
type Output = ConstraintPolynomial<F>;
fn neg(self) -> ConstraintPolynomial<F> {
self.clone().neg()
}
}
/// Generates the following variants of a binary operation:
/// - `Self . Self`
/// - `&Self . Self`
/// - `Self . &Self`
/// - `&Self . &Self`
/// - `Self . F`
/// - `&Self . F`
/// - `Self . usize`
/// - `&Self . usize`
/// where `Self` is `ConstraintPolynomial<F>`.
///
/// Takes the following arguments:
/// - `$trait`: the name of the binary operation trait to implement
/// - `$method`: the name of the method in the trait. It is assumed that `ConstraintPolynomial`
/// contains a method with the same name, implementing the `&Self . &Self` variant.
macro_rules! binop_variants {
($trait:ident, $method:ident) => {
impl<F: Field> $trait<Self> for ConstraintPolynomial<F> {
type Output = Self;
fn $method(self, rhs: Self) -> Self {
ConstraintPolynomial::$method(&self, &rhs)
}
}
impl<F: Field> $trait<&Self> for ConstraintPolynomial<F> {
type Output = Self;
fn $method(self, rhs: &Self) -> Self {
ConstraintPolynomial::$method(&self, rhs)
}
}
impl<F: Field> $trait<ConstraintPolynomial<F>> for &ConstraintPolynomial<F> {
type Output = ConstraintPolynomial<F>;
fn $method(self, rhs: ConstraintPolynomial<F>) -> Self::Output {
ConstraintPolynomial::$method(self, &rhs)
}
}
impl<F: Field> $trait for &ConstraintPolynomial<F> {
type Output = ConstraintPolynomial<F>;
fn $method(self, rhs: Self) -> Self::Output {
ConstraintPolynomial::$method(self, rhs)
}
}
impl<F: Field> $trait<F> for ConstraintPolynomial<F> {
type Output = Self;
fn $method(self, rhs: F) -> Self {
ConstraintPolynomial::$method(&self, &ConstraintPolynomial::constant(rhs))
}
}
impl<F: Field> $trait<F> for &ConstraintPolynomial<F> {
type Output = ConstraintPolynomial<F>;
fn $method(self, rhs: F) -> Self::Output {
ConstraintPolynomial::$method(self, &ConstraintPolynomial::constant(rhs))
}
}
impl<F: Field> $trait<usize> for ConstraintPolynomial<F> {
type Output = Self;
fn $method(self, rhs: usize) -> Self {
ConstraintPolynomial::$method(&self, &ConstraintPolynomial::constant_usize(rhs))
}
}
impl<F: Field> $trait<usize> for &ConstraintPolynomial<F> {
type Output = ConstraintPolynomial<F>;
fn $method(self, rhs: usize) -> Self::Output {
ConstraintPolynomial::$method(self, &ConstraintPolynomial::constant_usize(rhs))
}
}
};
}
/// Generates the following variants of a binary operation assignment:
/// - `.= Self`
/// - `.= &Self`
/// - `.= F`
/// - `.= usize`
/// where `Self` is `ConstraintPolynomial<F>`.
///
/// Takes the following arguments:
/// - `$trait`: the name of the binary operation trait to implement
/// - `$assign_method`: the name of the method in the trait
/// - `$binop_method`: the name of a method in `ConstraintPolynomial`
/// which implements the `&Self . &Self` operation.
macro_rules! binop_assign_variants {
($trait:ident, $assign_method:ident, $binop_method:ident) => {
impl<F: Field> $trait<Self> for ConstraintPolynomial<F> {
fn $assign_method(&mut self, rhs: Self) {
*self = ConstraintPolynomial::$binop_method(self, &rhs);
}
}
impl<F: Field> $trait<&Self> for ConstraintPolynomial<F> {
fn $assign_method(&mut self, rhs: &Self) {
*self = ConstraintPolynomial::$binop_method(self, rhs);
}
}
impl<F: Field> $trait<F> for ConstraintPolynomial<F> {
fn $assign_method(&mut self, rhs: F) {
*self = ConstraintPolynomial::$binop_method(self, &ConstraintPolynomial::constant(rhs));
}
}
impl<F: Field> $trait<usize> for ConstraintPolynomial<F> {
fn $assign_method(&mut self, rhs: usize) {
*self = ConstraintPolynomial::$binop_method(self, &ConstraintPolynomial::constant_usize(rhs));
}
}
}
}
binop_variants!(Add, add);
binop_variants!(Sub, sub);
binop_variants!(Mul, mul);
binop_assign_variants!(AddAssign, add_assign, add);
binop_assign_variants!(SubAssign, sub_assign, sub);
binop_assign_variants!(MulAssign, mul_assign, mul);
impl<F: Field> Sum for ConstraintPolynomial<F> {
fn sum<I: Iterator<Item=Self>>(iter: I) -> Self {
iter.fold(
ConstraintPolynomial::zero(),
|sum, x| sum + x)
}
}
impl<F: Field> Product for ConstraintPolynomial<F> {
fn product<I: Iterator<Item=Self>>(iter: I) -> Self {
iter.fold(
ConstraintPolynomial::one(),
|product, x| product * x)
}
}
#[derive(Clone, Debug)]
pub(crate) enum ConstraintPolynomialInner<F: Field> {
Constant(F),
LocalConstant(usize),
NextConstant(usize),
LocalWireValue(usize),
NextWireValue(usize),
Sum {
lhs: ConstraintPolynomial<F>,
rhs: ConstraintPolynomial<F>,
},
Product {
lhs: ConstraintPolynomial<F>,
rhs: ConstraintPolynomial<F>,
},
Exponentiation {
base: ConstraintPolynomial<F>,
exponent: usize,
},
}
impl<F: Field> Display for ConstraintPolynomialInner<F> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
ConstraintPolynomialInner::Constant(c) =>
write!(f, "{}", c),
ConstraintPolynomialInner::LocalConstant(i) =>
write!(f, "local_const_{}", i),
ConstraintPolynomialInner::NextConstant(i) =>
write!(f, "next_const_{}", i),
ConstraintPolynomialInner::LocalWireValue(i) =>
write!(f, "local_wire_{}", i),
ConstraintPolynomialInner::NextWireValue(i) =>
write!(f, "next_wire_{}", i),
ConstraintPolynomialInner::Sum { lhs, rhs } =>
write!(f, "({} + {})", lhs, rhs),
ConstraintPolynomialInner::Product { lhs, rhs } =>
write!(f, "({} * {})", lhs, rhs),
ConstraintPolynomialInner::Exponentiation { base, exponent } =>
write!(f, "({} ^ {})", base, exponent),
}
}
}
impl<F: Field> ConstraintPolynomialInner<F> {
fn add_dependencies(&self, gate: usize, deps: &mut HashSet<Wire>) {
match self {
ConstraintPolynomialInner::Constant(_) => (),
ConstraintPolynomialInner::LocalConstant(_) => (),
ConstraintPolynomialInner::NextConstant(_) => (),
ConstraintPolynomialInner::LocalWireValue(i) =>
{ deps.insert(Wire { gate, input: *i }); }
ConstraintPolynomialInner::NextWireValue(i) =>
{ deps.insert(Wire { gate: gate + 1, input: *i }); }
ConstraintPolynomialInner::Sum { lhs, rhs } => {
lhs.0.add_dependencies(gate, deps);
rhs.0.add_dependencies(gate, deps);
}
ConstraintPolynomialInner::Product { lhs, rhs } => {
lhs.0.add_dependencies(gate, deps);
rhs.0.add_dependencies(gate, deps);
}
ConstraintPolynomialInner::Exponentiation { base, exponent: _ } => {
base.0.add_dependencies(gate, deps);
}
}
}
fn add_constant_indices(&self, indices: &mut HashSet<usize>) {
match self {
ConstraintPolynomialInner::Constant(_) => (),
ConstraintPolynomialInner::LocalConstant(i) => { indices.insert(*i); }
ConstraintPolynomialInner::NextConstant(i) => { indices.insert(*i); }
ConstraintPolynomialInner::LocalWireValue(_) => (),
ConstraintPolynomialInner::NextWireValue(_) => (),
ConstraintPolynomialInner::Sum { lhs, rhs } => {
lhs.0.add_constant_indices(indices);
rhs.0.add_constant_indices(indices);
}
ConstraintPolynomialInner::Product { lhs, rhs } => {
lhs.0.add_constant_indices(indices);
rhs.0.add_constant_indices(indices);
}
ConstraintPolynomialInner::Exponentiation { base, exponent: _ } => {
base.0.add_constant_indices(indices);
}
}
}
fn evaluate(
&self,
vars: &EvaluationVars<F>,
mem: &mut HashMap<ConstraintPolynomial<F>, F>,
) -> F {
match self {
ConstraintPolynomialInner::Constant(c) => *c,
ConstraintPolynomialInner::LocalConstant(i) => vars.local_constants[*i],
ConstraintPolynomialInner::NextConstant(i) => vars.next_constants[*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);
lhs + rhs
}
ConstraintPolynomialInner::Product { lhs, rhs } => {
let lhs = lhs.evaluate_memoized(vars, mem);
let rhs = rhs.evaluate_memoized(vars, mem);
lhs * rhs
}
ConstraintPolynomialInner::Exponentiation { base, exponent } => {
let base = base.evaluate_memoized(vars, mem);
base.exp_usize(*exponent)
}
}
}
fn degree(&self) -> BigUint {
match self {
ConstraintPolynomialInner::Constant(_) => BigUint::zero(),
ConstraintPolynomialInner::LocalConstant(_) => BigUint::one(),
ConstraintPolynomialInner::NextConstant(_) => BigUint::one(),
ConstraintPolynomialInner::LocalWireValue(_) => BigUint::one(),
ConstraintPolynomialInner::NextWireValue(_) => BigUint::one(),
ConstraintPolynomialInner::Sum { lhs, rhs } => lhs.0.degree().max(rhs.0.degree()),
ConstraintPolynomialInner::Product { lhs, rhs } => lhs.0.degree() + rhs.0.degree(),
ConstraintPolynomialInner::Exponentiation { base, exponent } =>
base.0.degree() * BigUint::from_usize(*exponent).unwrap(),
}
}
}
#[cfg(test)]
mod tests {
use crate::constraint_polynomial::ConstraintPolynomial;
use crate::field::crandall_field::CrandallField;
#[test]
fn equality() {
type F = CrandallField;
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);
}
pub struct EvaluationTargets<'a> {
pub(crate) local_constants: &'a [Target],
pub(crate) next_constants: &'a [Target],
pub(crate) local_wires: &'a [Target],
pub(crate) next_wires: &'a [Target],
}

21
src/gadgets/arithmetic.rs Normal file
View File

@ -0,0 +1,21 @@
use crate::circuit_builder::CircuitBuilder;
use crate::field::field::Field;
use crate::target::Target;
impl<F: Field> CircuitBuilder<F> {
pub fn add(&mut self, x: Target, y: Target) -> Target {
todo!()
}
pub fn sub(&mut self, x: Target, y: Target) -> Target {
todo!()
}
pub fn mul(&mut self, x: Target, y: Target) -> Target {
todo!()
}
pub fn div(&mut self, x: Target, y: Target) -> Target {
todo!()
}
}

View File

@ -1,13 +0,0 @@
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
}

View File

@ -1,2 +1,2 @@
pub(crate) mod arithmetic;
pub(crate) mod split_join;
pub(crate) mod conditionals;

View File

@ -1,25 +1,24 @@
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()
}
// /// 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>(

View File

@ -1,16 +1,18 @@
use crate::circuit_data::CircuitConfig;
use crate::constraint_polynomial::ConstraintPolynomial;
use crate::circuit_builder::CircuitBuilder;
use crate::constraint_polynomial::{EvaluationTargets, EvaluationVars};
use crate::field::field::Field;
use crate::gates::deterministic_gate::{DeterministicGate, DeterministicGateAdapter};
use crate::gates::gate::GateRef;
use crate::gates::output_graph::{GateOutputLocation, OutputGraph};
use crate::gates::gate::{Gate, GateRef};
use crate::generator::{SimpleGenerator, WitnessGenerator};
use crate::target::Target;
use crate::witness::PartialWitness;
use crate::wire::Wire;
/// A gate which takes a single constant parameter and outputs that value.
pub struct ConstantGate2;
pub struct ConstantGate;
impl ConstantGate2 {
impl ConstantGate {
pub fn get<F: Field>() -> GateRef<F> {
GateRef::new(DeterministicGateAdapter::new(ConstantGate2))
GateRef::new(ConstantGate)
}
pub const CONST_INPUT: usize = 0;
@ -18,14 +20,70 @@ impl ConstantGate2 {
pub const WIRE_OUTPUT: usize = 0;
}
impl<F: Field> DeterministicGate<F> for ConstantGate2 {
impl<F: Field> Gate<F> for ConstantGate {
fn id(&self) -> String {
"ConstantGate".into()
}
fn outputs(&self, _config: CircuitConfig) -> OutputGraph<F> {
let loc = GateOutputLocation::LocalWire(Self::WIRE_OUTPUT);
let out = ConstraintPolynomial::local_constant(Self::CONST_INPUT);
OutputGraph::single_output(loc, out)
fn eval_unfiltered(&self, vars: EvaluationVars<F>) -> Vec<F> {
let input = vars.local_constants[Self::CONST_INPUT];
let output = vars.local_wires[Self::WIRE_OUTPUT];
vec![output - input]
}
fn eval_unfiltered_recursively(
&self,
builder: &mut CircuitBuilder<F>,
vars: EvaluationTargets,
) -> Vec<Target> {
let input = vars.local_constants[Self::CONST_INPUT];
let output = vars.local_wires[Self::WIRE_OUTPUT];
vec![builder.sub(output, input)]
}
fn generators(
&self,
gate_index: usize,
local_constants: &[F],
next_constants: &[F],
) -> Vec<Box<dyn WitnessGenerator<F>>> {
let gen = ConstantGenerator {
gate_index,
constant: local_constants[0],
};
vec![Box::new(gen)]
}
fn num_wires(&self) -> usize {
1
}
fn num_constants(&self) -> usize {
1
}
fn degree(&self) -> usize {
1
}
fn num_constraints(&self) -> usize {
1
}
}
#[derive(Debug)]
struct ConstantGenerator<F: Field> {
gate_index: usize,
constant: F,
}
impl<F: Field> SimpleGenerator<F> for ConstantGenerator<F> {
fn dependencies(&self) -> Vec<Target> {
Vec::new()
}
fn run_once(&self, witness: &PartialWitness<F>) -> PartialWitness<F> {
let wire = Wire { gate: self.gate_index, input: ConstantGate::WIRE_OUTPUT };
PartialWitness::singleton(Target::Wire(wire), self.constant)
}
}

View File

@ -1,157 +0,0 @@
use std::marker::PhantomData;
use crate::circuit_data::CircuitConfig;
use crate::constraint_polynomial::{ConstraintPolynomial, EvaluationVars};
use crate::field::field::Field;
use crate::gates::gate::Gate;
use crate::generator::{SimpleGenerator, WitnessGenerator};
use crate::target::Target;
use crate::wire::Wire;
use crate::witness::PartialWitness;
use crate::gates::output_graph::{OutputGraph, GateOutputLocation};
/// A deterministic gate. Each entry in `outputs()` describes how that output is evaluated; this is
/// used to create both the constraint set and the generator set.
///
/// `DeterministicGate`s do not automatically implement `Gate`; they should instead be wrapped in
/// `DeterministicGateAdapter`.
pub trait DeterministicGate<F: Field>: 'static {
/// A unique identifier for this gate.
fn id(&self) -> String;
/// A vector of `(loc, out)` pairs, where `loc` is the location of an output and `out` is a
/// polynomial defining how that output is evaluated.
fn outputs(&self, config: CircuitConfig) -> OutputGraph<F>;
/// Any additional constraints to be enforced, besides the (automatically provided) ones that
/// constraint output values.
fn additional_constraints(&self, _config: CircuitConfig) -> Vec<ConstraintPolynomial<F>> {
Vec::new()
}
/// Any additional generators, besides the (automatically provided) ones that generate output
/// values.
fn additional_generators(
&self,
_config: CircuitConfig,
_gate_index: usize,
local_constants: Vec<F>,
next_constants: Vec<F>,
) -> Vec<Box<dyn WitnessGenerator<F>>> {
Vec::new()
}
}
/// A wrapper around `DeterministicGate` which implements `Gate`. Note that a blanket implementation
/// is not possible in this context given Rust's coherence rules.
pub struct DeterministicGateAdapter<F: Field, DG: DeterministicGate<F> + ?Sized> {
gate: Box<DG>,
_phantom: PhantomData<F>,
}
impl<F: Field, DG: DeterministicGate<F>> DeterministicGateAdapter<F, DG> {
pub fn new(gate: DG) -> Self {
Self { gate: Box::new(gate), _phantom: PhantomData }
}
}
impl<F: Field, DG: DeterministicGate<F>> Gate<F> for DeterministicGateAdapter<F, DG> {
fn id(&self) -> String {
self.gate.id()
}
fn constraints(&self, config: CircuitConfig) -> Vec<ConstraintPolynomial<F>> {
// For each output, we add a constraint of the form `out - expression = 0`,
// then we append any additional constraints that the gate defines.
self.gate.outputs(config).outputs.into_iter()
.map(|(output_loc, out)| out - ConstraintPolynomial::from_gate_output(output_loc))
.chain(self.gate.additional_constraints(config).into_iter())
.collect()
}
fn generators(
&self,
config: CircuitConfig,
gate_index: usize,
local_constants: Vec<F>,
next_constants: Vec<F>,
) -> Vec<Box<dyn WitnessGenerator<F>>> {
self.gate.outputs(config).outputs
.into_iter()
.map(|(location, out)| {
let og = OutputGenerator {
gate_index,
location,
out,
local_constants: local_constants.clone(),
next_constants: next_constants.clone(),
};
// We need the type system to treat this as a boxed `WitnessGenerator2<F>`, rather
// than a boxed `OutputGenerator<F>`.
let b: Box::<dyn WitnessGenerator<F>> = Box::new(og);
b
})
.chain(self.gate.additional_generators(
config, gate_index, local_constants.clone(), next_constants.clone()))
.collect()
}
}
#[derive(Debug)]
struct OutputGenerator<F: Field> {
gate_index: usize,
location: GateOutputLocation,
out: ConstraintPolynomial<F>,
local_constants: Vec<F>,
next_constants: Vec<F>,
}
impl<F: Field> SimpleGenerator<F> for OutputGenerator<F> {
fn dependencies(&self) -> Vec<Target> {
self.out.dependencies(self.gate_index)
.into_iter()
.map(Target::Wire)
.collect()
}
fn run_once(&self, witness: &PartialWitness<F>) -> PartialWitness<F> {
let mut local_wire_values = Vec::new();
let mut next_wire_values = Vec::new();
// Get an exclusive upper bound on the largest input index in this constraint.
let input_limit_exclusive = self.out.max_wire_input_index()
.map_or(0, |i| i + 1);
for input in 0..input_limit_exclusive {
let local_wire = Wire { gate: self.gate_index, input };
let next_wire = Wire { gate: self.gate_index + 1, input };
// Lookup the values if they exist. If not, we can just insert a zero, knowing
// that it will not be used. (If it was used, it would have been included in our
// dependencies, and this generator would not have run yet.)
let local_value = witness.try_get_target(Target::Wire(local_wire)).unwrap_or(F::ZERO);
let next_value = witness.try_get_target(Target::Wire(next_wire)).unwrap_or(F::ZERO);
local_wire_values.push(local_value);
next_wire_values.push(next_value);
}
let vars = EvaluationVars {
local_constants: &self.local_constants,
next_constants: &self.next_constants,
local_wires: &local_wire_values,
next_wires: &next_wire_values,
};
let result_wire = match self.location {
GateOutputLocation::LocalWire(input) =>
Wire { gate: self.gate_index, input },
GateOutputLocation::NextWire(input) =>
Wire { gate: self.gate_index + 1, input },
};
let result_value = self.out.evaluate(vars);
PartialWitness::singleton(Target::Wire(result_wire), result_value)
}
}

View File

@ -1,372 +1,372 @@
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<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);
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<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]
}
}
#[derive(Debug)]
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(&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());
}
}
// 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<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);
// 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<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]
// }
// }
//
// #[derive(Debug)]
// 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(&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());
// }
// }

View File

@ -1,62 +1,48 @@
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use crate::circuit_data::CircuitConfig;
use crate::constraint_polynomial::{ConstraintPolynomial, EvaluationVars};
use crate::field::field::Field;
use crate::generator::WitnessGenerator;
use num::ToPrimitive;
use crate::circuit_builder::CircuitBuilder;
use crate::constraint_polynomial::{EvaluationTargets, EvaluationVars};
use crate::field::field::Field;
use crate::generator::WitnessGenerator;
use crate::target::Target;
/// A custom gate.
// TODO: Remove CircuitConfig params? Could just use fields within each struct.
pub trait Gate<F: Field>: 'static {
fn id(&self) -> String;
/// A set of expressions which must evaluate to zero.
fn constraints(&self, config: CircuitConfig) -> Vec<ConstraintPolynomial<F>>;
fn eval_unfiltered(&self, vars: EvaluationVars<F>) -> Vec<F>;
// fn eval_constraints(&self, config: CircuitConfig, vars: EvaluationVars<F>) -> Vec<F> {
// self.constraints(config)
// .into_iter()
// .map(|c| c.evaluate(vars))
// .collect()
// }
fn eval_unfiltered_recursively(
&self,
builder: &mut CircuitBuilder<F>,
vars: EvaluationTargets,
) -> Vec<Target>;
fn eval_filtered(&self, vars: EvaluationVars<F>) -> Vec<F> {
// TODO: Filter
self.eval_unfiltered(vars)
}
fn generators(
&self,
config: CircuitConfig,
gate_index: usize,
// TODO: Switch to slices?
local_constants: Vec<F>,
next_constants: Vec<F>,
local_constants: &[F],
next_constants: &[F],
) -> Vec<Box<dyn WitnessGenerator<F>>>;
/// The number of constants used by this gate.
fn num_constants(&self, config: CircuitConfig) -> usize {
self.constraints(config)
.into_iter()
.map(|c| c.max_constant_index().map_or(0, |i| i + 1))
.max()
.unwrap_or(0)
}
/// The number of wires used by this gate.
fn num_wires(&self) -> usize;
/// The minimum number of wires required to use this gate.
fn min_wires(&self, config: CircuitConfig) -> usize {
self.constraints(config)
.into_iter()
.map(|c| c.max_wire_input_index().map_or(0, |i| i + 1))
.max()
.unwrap_or(0)
}
/// The number of constants used by this gate.
fn num_constants(&self) -> usize;
/// The maximum degree among this gate's constraint polynomials.
fn degree(&self, config: CircuitConfig) -> usize {
self.constraints(config)
.into_iter()
.map(|c| c.degree().to_usize().expect("degree too large"))
.max()
.unwrap_or(0)
}
fn degree(&self) -> usize;
fn num_constraints(&self) -> usize;
}
/// A wrapper around an `Rc<Gate>` which implements `PartialEq`, `Eq` and `Hash` based on gate IDs.

View File

@ -2,25 +2,31 @@ use std::sync::Arc;
use num::{BigUint, One};
use crate::circuit_builder::CircuitBuilder;
use crate::circuit_data::CircuitConfig;
use crate::constraint_polynomial::ConstraintPolynomial;
use crate::constraint_polynomial::{EvaluationTargets, EvaluationVars};
use crate::field::field::Field;
use crate::gates::deterministic_gate::{DeterministicGate, DeterministicGateAdapter};
use crate::gates::gate::GateRef;
use crate::gates::output_graph::{GateOutputLocation, OutputGraph};
use crate::gates::gate::{Gate, GateRef};
use crate::generator::{SimpleGenerator, WitnessGenerator};
use crate::target::Target;
use crate::witness::PartialWitness;
use crate::wire::Wire;
use crate::gates::noop::NoopGate;
/// Evaluates a full GMiMC permutation, and writes the output to the next gate's first `width`
/// wires (which could be the input of another `GMiMCGate`).
/// The width of the permutation, in field elements.
const W: usize = 12;
/// Evaluates a full GMiMC permutation with 12 state elements, and writes the output to the next
/// gate's first `width` wires (which could be the input of another `GMiMCGate`).
#[derive(Debug)]
pub struct GMiMCGate<F: Field, const W: usize, const R: usize> {
pub struct GMiMCGate<F: Field, const R: usize> {
constants: Arc<[F; R]>,
}
impl<F: Field, const W: usize, const R: usize> GMiMCGate<F, W, R> {
impl<F: Field, const R: usize> GMiMCGate<F, R> {
pub fn with_constants(constants: Arc<[F; R]>) -> GateRef<F> {
let gate = GMiMCGate::<F, W, R> { constants };
let adapter = DeterministicGateAdapter::new(gate);
GateRef::new(adapter)
let gate = GMiMCGate::<F, R> { constants };
GateRef::new(gate)
}
pub fn with_automatic_constants() -> GateRef<F> {
@ -42,79 +48,163 @@ impl<F: Field, const W: usize, const R: usize> GMiMCGate<F, W, R> {
i
}
/// Adds a local wire output to this output graph. Returns a `ConstraintPolynomial` which
/// references the newly-created wire.
///
/// 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(index)
/// A wire which stores the input to the `i`th cubing.
fn wire_cubing_input(i: usize) -> usize {
W + 1 + i
}
}
impl<F: Field, const W: usize, const R: usize> DeterministicGate<F> for GMiMCGate<F, W, R> {
impl<F: Field, const R: usize> Gate<F> for GMiMCGate<F, R> {
fn id(&self) -> String {
// TODO: This won't include generic params?
format!("{:?}", self)
}
fn outputs(&self, _config: CircuitConfig) -> OutputGraph<F> {
let original_inputs = (0..W)
.map(|i| ConstraintPolynomial::local_wire(Self::wire_input(i)))
fn eval_unfiltered(&self, vars: EvaluationVars<F>) -> Vec<F> {
let mut constraints = Vec::with_capacity(W + R);
// Value that is implicitly added to each element.
// See https://affine.group/2020/02/starkware-challenge
let mut addition_buffer = F::ZERO;
let switch = vars.local_wires[Self::WIRE_SWITCH];
let mut state = Vec::with_capacity(12);
for i in 0..4 {
let a = vars.local_wires[i];
let b = vars.local_wires[i + 4];
state.push(a + switch * (b - a));
}
for i in 0..4 {
let a = vars.local_wires[i + 4];
let b = vars.local_wires[i];
state.push(a + switch * (b - a));
}
for i in 8..12 {
state.push(vars.local_wires[i]);
}
for r in 0..R {
let active = r % W;
let cubing_input = state[active] + addition_buffer + self.constants[r];
let cubing_input_wire = vars.local_wires[Self::wire_cubing_input(r)];
constraints.push(cubing_input - cubing_input_wire);
let f = cubing_input_wire.cube();
addition_buffer += f;
state[active] -= f;
}
for i in 0..W {
state[i] += addition_buffer;
constraints.push(state[i] - vars.next_wires[i]);
}
constraints
}
fn eval_unfiltered_recursively(
&self,
builder: &mut CircuitBuilder<F>,
vars: EvaluationTargets,
) -> Vec<Target> {
unimplemented!()
}
fn generators(
&self,
gate_index: usize,
_local_constants: &[F],
_next_constants: &[F],
) -> Vec<Box<dyn WitnessGenerator<F>>> {
let gen = GMiMCGenerator {
gate_index,
constants: self.constants.clone(),
};
vec![Box::new(gen)]
}
fn num_wires(&self) -> usize {
W + 1 + R
}
fn num_constants(&self) -> usize {
0
}
fn degree(&self) -> usize {
3
}
fn num_constraints(&self) -> usize {
R + W
}
}
#[derive(Debug)]
struct GMiMCGenerator<F: Field, const R: usize> {
gate_index: usize,
constants: Arc<[F; R]>,
}
impl<F: Field, const R: usize> SimpleGenerator<F> for GMiMCGenerator<F, R> {
fn dependencies(&self) -> Vec<Target> {
(0..W)
.map(|i| Target::Wire(Wire {
gate: self.gate_index,
input: GMiMCGate::<F, R>::wire_input(i),
}))
.collect()
}
fn run_once(&self, witness: &PartialWitness<F>) -> PartialWitness<F> {
let mut result = PartialWitness::new();
let mut state = (0..W)
.map(|i| witness.get_wire(Wire {
gate: self.gate_index,
input: GMiMCGate::<F, R>::wire_input(i),
}))
.collect::<Vec<_>>();
let mut outputs = OutputGraph::new();
// Conditionally switch inputs based on the (boolean) switch wire.
let switch = ConstraintPolynomial::local_wire(Self::WIRE_SWITCH);
let mut state = Vec::new();
for i in 0..4 {
let a = &original_inputs[i];
let b = &original_inputs[i + 4];
state.push(a + &switch * (b - a));
}
for i in 0..4 {
let a = &original_inputs[i + 4];
let b = &original_inputs[i];
state.push(a + &switch * (b - a));
}
for i in 8..W {
state.push(original_inputs[i].clone());
let switch_value = witness.get_wire(Wire {
gate: self.gate_index,
input: GMiMCGate::<F, R>::WIRE_SWITCH,
});
debug_assert!(switch_value == F::ZERO || switch_value == F::ONE);
if switch_value == F::ONE {
for i in 0..4 {
state.swap(i, 4 + i);
}
}
// Value that is implicitly added to each element.
// See https://affine.group/2020/02/starkware-challenge
let mut addition_buffer = ConstraintPolynomial::zero();
let mut addition_buffer = F::ZERO;
for r in 0..R {
let active = r % W;
let round_constant = ConstraintPolynomial::constant(self.constants[r]);
let mut f_input = &state[active] + &addition_buffer + round_constant;
if f_input.degree() > BigUint::one() {
f_input = Self::add_local(&mut outputs, f_input);
}
let f_output = f_input.cube();
addition_buffer += &f_output;
state[active] -= f_output;
let cubing_input = state[active] + addition_buffer + self.constants[r];
result.set_wire(
Wire {
gate: self.gate_index,
input: GMiMCGate::<F, R>::wire_cubing_input(r),
},
cubing_input);
let f = cubing_input.cube();
addition_buffer += f;
state[active] -= f;
}
for i in 0..W {
outputs.add(GateOutputLocation::NextWire(i), &state[i] + &addition_buffer);
state[i] += addition_buffer;
result.set_wire(
Wire {
gate: self.gate_index + 1,
input: GMiMCGate::<F, R>::wire_output(i),
},
state[i]);
}
outputs
}
fn additional_constraints(&self, _config: CircuitConfig) -> Vec<ConstraintPolynomial<F>> {
let switch = ConstraintPolynomial::local_wire(Self::WIRE_SWITCH);
let switch_bool_constraint = &switch * (&switch - 1);
vec![switch_bool_constraint]
result
}
}
@ -127,40 +217,24 @@ mod tests {
use crate::field::crandall_field::CrandallField;
use crate::field::field::Field;
use crate::gates::deterministic_gate::DeterministicGate;
use crate::gates::gmimc::GMiMCGate;
use crate::gates::gmimc::{GMiMCGate, W};
use crate::generator::generate_partial_witness;
use crate::gmimc::gmimc_permute_naive;
use crate::wire::Wire;
use crate::witness::PartialWitness;
#[test]
fn degree() {
type F = CrandallField;
const W: usize = 12;
const R: usize = 101;
let gate = GMiMCGate::<F, W, R> { constants: Arc::new([F::TWO; R]) };
let config = CircuitConfig {
num_wires: 200,
num_routed_wires: 200,
security_bits: 128,
};
let outs = gate.outputs(config);
assert_eq!(outs.degree(), 3);
}
#[test]
fn generated_output() {
type F = CrandallField;
const W: usize = 12;
const R: usize = 101;
let constants = Arc::new([F::TWO; R]);
type Gate = GMiMCGate::<F, W, R>;
type Gate = GMiMCGate::<F, R>;
let gate = Gate::with_constants(constants.clone());
let config = CircuitConfig {
num_wires: 200,
num_routed_wires: 200,
security_bits: 128,
..Default::default()
};
let permutation_inputs = (0..W)
@ -175,7 +249,7 @@ mod tests {
permutation_inputs[i]);
}
let generators = gate.0.generators(config, 0, vec![], vec![]);
let generators = gate.0.generators(0, &[], &[]);
generate_partial_witness(&mut witness, &generators);
let expected_outputs: [F; W] = gmimc_permute_naive(

81
src/gates/gmimc_eval.rs Normal file
View File

@ -0,0 +1,81 @@
use crate::circuit_builder::CircuitBuilder;
use crate::constraint_polynomial::{EvaluationTargets, EvaluationVars};
use crate::field::field::Field;
use crate::gates::gate::{Gate, GateRef};
use crate::generator::{SimpleGenerator, WitnessGenerator};
use crate::target::Target;
use crate::witness::PartialWitness;
/// Performs some arithmetic involved in the evaluation of GMiMC's constraint polynomials for one
/// round.
#[derive(Debug)]
pub struct GMiMCEvalGate;
impl GMiMCEvalGate {
pub fn get<F: Field>() -> GateRef<F> {
GateRef::new(GMiMCEvalGate)
}
}
impl<F: Field> Gate<F> for GMiMCEvalGate {
fn id(&self) -> String {
format!("{:?}", self)
}
fn eval_unfiltered(&self, vars: EvaluationVars<F>) -> Vec<F> {
todo!()
}
fn eval_unfiltered_recursively(
&self,
builder: &mut CircuitBuilder<F>,
vars: EvaluationTargets,
) -> Vec<Target> {
unimplemented!()
}
fn generators(
&self,
gate_index: usize,
local_constants: &[F],
_next_constants: &[F],
) -> Vec<Box<dyn WitnessGenerator<F>>> {
let gen = GMiMCEvalGenerator::<F> {
gate_index,
constant: local_constants[0],
};
vec![Box::new(gen)]
}
fn num_wires(&self) -> usize {
6
}
fn num_constants(&self) -> usize {
1
}
fn degree(&self) -> usize {
3
}
fn num_constraints(&self) -> usize {
unimplemented!()
}
}
#[derive(Debug)]
struct GMiMCEvalGenerator<F: Field> {
gate_index: usize,
constant: F,
}
impl<F: Field> SimpleGenerator<F> for GMiMCEvalGenerator<F> {
fn dependencies(&self) -> Vec<Target> {
todo!()
}
fn run_once(&self, witness: &PartialWitness<F>) -> PartialWitness<F> {
todo!()
}
}

View File

@ -1,7 +1,6 @@
pub(crate) mod constant;
pub(crate) mod deterministic_gate;
pub(crate) mod fri_consistency_gate;
pub(crate) mod gate;
pub(crate) mod gmimc;
pub(crate) mod gmimc_eval;
pub(crate) mod noop;
pub(crate) mod output_graph;

View File

@ -1,9 +1,10 @@
use crate::circuit_data::CircuitConfig;
use crate::constraint_polynomial::ConstraintPolynomial;
use crate::constraint_polynomial::{EvaluationVars, EvaluationTargets};
use crate::field::field::Field;
use crate::gates::deterministic_gate::{DeterministicGate, DeterministicGateAdapter};
use crate::gates::gate::{Gate, GateRef};
use crate::generator::WitnessGenerator;
use crate::target::Target;
use crate::circuit_builder::CircuitBuilder;
/// A gate which takes a single constant parameter and outputs that value.
pub struct NoopGate;
@ -19,17 +20,40 @@ impl<F: Field> Gate<F> for NoopGate {
"NoopGate".into()
}
fn constraints(&self, _config: CircuitConfig) -> Vec<ConstraintPolynomial<F>> {
fn eval_unfiltered(&self, vars: EvaluationVars<F>) -> Vec<F> {
Vec::new()
}
fn eval_unfiltered_recursively(
&self,
_builder: &mut CircuitBuilder<F>,
vars: EvaluationTargets,
) -> Vec<Target> {
Vec::new()
}
fn generators(
&self,
_config: CircuitConfig,
_gate_index: usize,
_local_constants: Vec<F>,
_next_constants: Vec<F>
gate_index: usize,
local_constants: &[F],
next_constants: &[F],
) -> Vec<Box<dyn WitnessGenerator<F>>> {
Vec::new()
}
fn num_wires(&self) -> usize {
0
}
fn num_constants(&self) -> usize {
0
}
fn degree(&self) -> usize {
0
}
fn num_constraints(&self) -> usize {
0
}
}

View File

@ -1,113 +0,0 @@
use std::fmt;
use std::fmt::{Display, Formatter};
use num::ToPrimitive;
use crate::constraint_polynomial::ConstraintPolynomial;
use crate::field::field::Field;
/// Represents a set of deterministic gate outputs, expressed as polynomials over witness
/// values.
#[derive(Clone, Debug)]
pub struct OutputGraph<F: Field> {
pub(crate) outputs: Vec<(GateOutputLocation, ConstraintPolynomial<F>)>
}
impl<F: Field> OutputGraph<F> {
/// Creates a new output graph with no outputs.
pub fn new() -> Self {
Self { outputs: Vec::new() }
}
/// Creates an output graph with a single output.
pub fn single_output(loc: GateOutputLocation, out: ConstraintPolynomial<F>) -> Self {
Self { outputs: vec![(loc, out)] }
}
pub fn add(&mut self, location: GateOutputLocation, poly: ConstraintPolynomial<F>) {
self.outputs.push((location, poly))
}
/// 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())
.max()
.unwrap_or(0)
}
/// 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 {
GateOutputLocation::LocalWire(i) => Some(*i),
GateOutputLocation::NextWire(_) => None
})
.max()
}
}
impl<F: Field> Display for OutputGraph<F> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
for (loc, out) in &self.outputs {
write!(f, "{} := {}, ", loc, out)?;
}
Ok(())
}
}
/// Represents an output location of a deterministic gate.
#[derive(Copy, Clone, Debug)]
pub enum GateOutputLocation {
/// A wire belonging to the gate itself.
LocalWire(usize),
/// A wire belonging to the following gate.
NextWire(usize),
}
impl Display for GateOutputLocation {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
GateOutputLocation::LocalWire(i) => write!(f, "local_wire_{}", i),
GateOutputLocation::NextWire(i) => write!(f, "next_wire_{}", i),
}
}
}
/// 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)
}
}

View File

@ -79,6 +79,7 @@ pub trait WitnessGenerator<F: Field>: 'static + Debug {
}
/// A generator which runs once after a list of dependencies is present in the witness.
// TODO: Remove Debug. Here temporarily to debug generator issues.
pub trait SimpleGenerator<F: Field>: 'static + Debug {
fn dependencies(&self) -> Vec<Target>;

View File

@ -8,7 +8,7 @@ use rayon::prelude::*;
use crate::field::field::Field;
use crate::gmimc::{gmimc_compress, gmimc_permute_array};
use crate::proof::Hash;
use crate::util::{reverse_index_bits, transpose};
use crate::util::{reverse_index_bits, transpose, reverse_index_bits_in_place};
const RATE: usize = 8;
const CAPACITY: usize = 4;
@ -80,11 +80,9 @@ pub fn hash_n_to_1<F: Field>(inputs: Vec<F>, pad: bool) -> F {
}
/// Like `merkle_root`, but first reorders each vector so that `new[i] = old[i.reverse_bits()]`.
pub(crate) fn merkle_root_bit_rev_order<F: Field>(vecs: Vec<Vec<F>>) -> Hash<F> {
let vecs_reordered = vecs.into_par_iter()
.map(reverse_index_bits)
.collect();
merkle_root(vecs_reordered)
pub(crate) fn merkle_root_bit_rev_order<F: Field>(mut vecs: Vec<Vec<F>>) -> Hash<F> {
reverse_index_bits_in_place(&mut vecs);
merkle_root(vecs)
}
/// Given `n` vectors, each of length `l`, constructs a Merkle tree with `l` leaves, where each leaf
@ -92,8 +90,7 @@ pub(crate) fn merkle_root_bit_rev_order<F: Field>(vecs: Vec<Vec<F>>) -> Hash<F>
/// is skipped, as there is no need to compress leaf data.
pub(crate) fn merkle_root<F: Field>(vecs: Vec<Vec<F>>) -> Hash<F> {
// TODO: Parallelize.
let mut vecs_t = transpose(&vecs);
let mut hashes = vecs_t.into_iter()
let mut hashes = vecs.into_iter()
.map(|leaf_set| hash_or_noop(leaf_set))
.collect::<Vec<_>>();
while hashes.len() > 1 {

View File

@ -14,11 +14,11 @@ use crate::util::log2_ceil;
use crate::circuit_builder::CircuitBuilder;
use crate::circuit_data::CircuitConfig;
use crate::witness::PartialWitness;
use crate::gates::fri_consistency_gate::FriConsistencyGate;
use env_logger::Env;
use crate::gates::gmimc::GMiMCGate;
use std::sync::Arc;
use std::convert::TryInto;
use crate::gates::constant::ConstantGate;
mod circuit_builder;
mod circuit_data;
@ -29,6 +29,7 @@ mod gadgets;
mod gates;
mod generator;
mod gmimc;
mod plonk_common;
mod proof;
mod prover;
mod recursive_verifier;
@ -69,7 +70,7 @@ fn bench_prove<F: Field>() {
for i in 0..GMIMC_ROUNDS {
gmimc_constants[i] = F::from_canonical_u64(GMIMC_CONSTANTS[i]);
}
let gmimc_gate = GMiMCGate::<F, 12, GMIMC_ROUNDS>::with_constants(
let gmimc_gate = GMiMCGate::<F, GMIMC_ROUNDS>::with_constants(
Arc::new(gmimc_constants));
let config = CircuitConfig {
@ -77,6 +78,7 @@ fn bench_prove<F: Field>() {
num_routed_wires: 12,
security_bits: 128,
rate_bits: 3,
num_checks: 3,
};
let mut builder = CircuitBuilder::<F>::new(config);
@ -85,11 +87,13 @@ fn bench_prove<F: Field>() {
builder.add_gate_no_constants(gmimc_gate.clone());
}
for _ in 0..(40 * 5) {
builder.add_gate(
FriConsistencyGate::new(2, 3, 13),
vec![F::primitive_root_of_unity(13)]);
}
builder.add_gate(ConstantGate::get(), vec![F::NEG_ONE]);
// for _ in 0..(40 * 5) {
// builder.add_gate(
// FriConsistencyGate::new(2, 3, 13),
// vec![F::primitive_root_of_unity(13)]);
// }
let prover = builder.build_prover();
let inputs = PartialWitness::new();

19
src/plonk_common.rs Normal file
View File

@ -0,0 +1,19 @@
use crate::field::field::Field;
use crate::target::Target;
use crate::circuit_builder::CircuitBuilder;
pub(crate) fn reduce_with_powers<F: Field>(terms: Vec<F>, alpha: F) -> F {
let mut sum = F::ZERO;
for &term in terms.iter().rev() {
sum = sum * alpha + term;
}
sum
}
pub(crate) fn reduce_with_powers_recursive<F: Field>(
builder: &mut CircuitBuilder<F>,
terms: Vec<Target>,
alpha: Target,
) -> Target {
todo!()
}

View File

@ -1,17 +1,20 @@
use std::env::var;
use std::time::Instant;
use log::info;
use rayon::prelude::*;
use crate::circuit_data::{CommonCircuitData, ProverOnlyCircuitData};
use crate::field::fft::{fft, ifft, lde};
use crate::circuit_data::{CommonCircuitData, ProverCircuitData, ProverOnlyCircuitData};
use crate::constraint_polynomial::EvaluationVars;
use crate::field::fft::{fft, ifft, lde, lde_multiple};
use crate::field::field::Field;
use crate::generator::generate_partial_witness;
use crate::hash::{compress, hash_n_to_hash, hash_n_to_m, hash_or_noop, merkle_root_bit_rev_order};
use crate::plonk_common::reduce_with_powers;
use crate::proof::{Hash, Proof2};
use crate::util::{log2_ceil, reverse_index_bits};
use crate::util::{log2_ceil, reverse_index_bits, transpose};
use crate::wire::Wire;
use crate::witness::PartialWitness;
use rayon::prelude::*;
pub(crate) fn prove<F: Field>(
prover_data: &ProverOnlyCircuitData<F>,
@ -31,21 +34,33 @@ pub(crate) fn prove<F: Field>(
// TODO: Simplify using lde_multiple.
// TODO: Parallelize.
let wire_ldes = (0..num_wires)
.map(|i| compute_wire_lde(i, &witness, common_data.degree, config.rate_bits))
.map(|i| compute_wire_lde(i, &witness, common_data.degree(), config.rate_bits))
.collect::<Vec<_>>();
info!("Computing wire LDEs took {}s", start_wire_ldes.elapsed().as_secs_f32());
let start_wires_root = Instant::now();
let wires_root = merkle_root_bit_rev_order(wire_ldes);
let wire_ldes_t = transpose(&wire_ldes);
// TODO: Could avoid cloning if it's significant?
let wires_root = merkle_root_bit_rev_order(wire_ldes_t.clone());
info!("Merklizing wire LDEs took {}s", start_wires_root.elapsed().as_secs_f32());
let plonk_z_vecs = todo!();
let plonk_z_ldes = todo!();
let plonk_z_root = merkle_root_bit_rev_order(plonk_z_ldes);
let plonk_z_vecs = compute_zs(&common_data);
let plonk_z_ldes = lde_multiple(plonk_z_vecs, config.rate_bits);
let plonk_z_ldes_t = transpose(&plonk_z_ldes);
let plonk_z_root = merkle_root_bit_rev_order(plonk_z_ldes_t.clone());
let plonk_t_vecs = todo!();
let plonk_t_ldes = todo!();
let plonk_t_root = merkle_root_bit_rev_order(plonk_t_ldes);
let alpha = F::ZERO; // TODO
let start_vanishing_poly = Instant::now();
let vanishing_poly = compute_vanishing_poly(
common_data, prover_data, wire_ldes_t, plonk_z_ldes_t, alpha);
info!("Computing vanishing poly took {}s", start_vanishing_poly.elapsed().as_secs_f32());
let plonk_t: Vec<F> = todo!(); // vanishing_poly / Z_H
// Need to convert to coeff form and back?
let plonk_t_parts = todo!();
let plonk_t_ldes = lde_multiple(plonk_t_parts, config.rate_bits);
let plonk_t_root = merkle_root_bit_rev_order(transpose(&plonk_t_ldes));
let openings = todo!();
@ -57,6 +72,70 @@ pub(crate) fn prove<F: Field>(
}
}
fn compute_zs<F: Field>(common_data: &CommonCircuitData<F>) -> Vec<Vec<F>> {
(0..common_data.config.num_checks)
.map(|i| compute_z(common_data, i))
.collect()
}
fn compute_z<F: Field>(common_data: &CommonCircuitData<F>, i: usize) -> Vec<F> {
vec![F::ZERO; common_data.degree()] // TODO
}
fn compute_vanishing_poly<F: Field>(
common_data: &CommonCircuitData<F>,
prover_data: &ProverOnlyCircuitData<F>,
wire_ldes_t: Vec<Vec<F>>,
plonk_z_lde_t: Vec<Vec<F>>,
alpha: F,
) -> Vec<F> {
let lde_size = common_data.lde_size();
let lde_gen = common_data.lde_generator();
let mut result = Vec::with_capacity(lde_size);
let mut point = F::ONE;
for i in 0..lde_size {
debug_assert!(point != F::ONE);
let i_next = (i + 1) % lde_size;
let local_wires = &wire_ldes_t[i];
let next_wires = &wire_ldes_t[i_next];
let local_constants = &prover_data.constant_ldes_t[i];
let next_constants = &prover_data.constant_ldes_t[i_next];
let local_plonk_zs = &plonk_z_lde_t[i];
let next_plonk_zs = &plonk_z_lde_t[i_next];
debug_assert_eq!(local_wires.len(), common_data.config.num_wires);
debug_assert_eq!(local_plonk_zs.len(), common_data.config.num_checks);
let vars = EvaluationVars {
local_constants,
next_constants,
local_wires,
next_wires,
};
result.push(compute_vanishing_poly_entry(
common_data, vars, local_plonk_zs, next_plonk_zs, alpha));
point *= lde_gen;
}
debug_assert_eq!(point, F::ONE);
result
}
fn compute_vanishing_poly_entry<F: Field>(
common_data: &CommonCircuitData<F>,
vars: EvaluationVars<F>,
local_plonk_zs: &[F],
next_plonk_zs: &[F],
alpha: F,
) -> F {
let mut constraints = Vec::with_capacity(common_data.total_constraints());
// TODO: Add Z constraints.
constraints.extend(common_data.evaluate(vars));
reduce_with_powers(constraints, alpha)
}
fn compute_wire_lde<F: Field>(
input: usize,
witness: &PartialWitness<F>,

View File

@ -1,5 +1,10 @@
use crate::circuit_builder::CircuitBuilder;
use crate::field::field::Field;
const MIN_WIRES: usize = 120; // TODO: Double check.
const MIN_ROUTED_WIRES: usize = 12; // TODO: Double check.
pub fn add_recursive_verifier<F: Field>(builder: &mut CircuitBuilder<F>) {
assert!(builder.config.num_wires >= MIN_WIRES);
assert!(builder.config.num_wires >= MIN_ROUTED_WIRES);
}

View File

@ -30,17 +30,29 @@ pub(crate) fn transpose<T: Clone>(matrix: &[Vec<T>]) -> Vec<Vec<T>> {
}
/// Permutes `arr` such that each index is mapped to its reverse in binary.
pub(crate) fn reverse_index_bits<T: Copy>(arr: Vec<T>) -> Vec<T> {
pub(crate) fn reverse_index_bits<T: Clone>(arr: Vec<T>) -> Vec<T> {
let n = arr.len();
let n_power = log2_strict(n);
let mut result = Vec::with_capacity(n);
for i in 0..n {
result.push(arr[reverse_bits(i, n_power)]);
result.push(arr[reverse_bits(i, n_power)].clone());
}
result
}
pub(crate) fn reverse_index_bits_in_place<T>(arr: &mut Vec<T>) {
let n = arr.len();
let n_power = log2_strict(n);
for src in 0..n {
let dst = reverse_bits(src, n_power);
if src < dst {
arr.swap(src, dst);
}
}
}
fn reverse_bits(n: usize, num_bits: usize) -> usize {
let mut result = 0;
for i in 0..num_bits {