mirror of
https://github.com/arnaucube/sonobe.git
synced 2026-02-06 19:26:45 +01:00
* Reduce the number of constraints in `AugmentedFCircuit` For the test `folding::nova::tests::test_ivc` Before: 138240 After: 86756 (1.6x improvement) Two notable optimization techniques: 1. Instead of allocating two witness variables `a, b` and enforce their equality by calling `a.conditional_enforce_equal(&b, &cond)`, we can avoid the allocation of `b` and directly set `b = a`. The former might be costly due to the checks in allocation and `conditional_enforce_equal`. See `nova/circuits.rs` for details. 2. Before this commit, `NonNativeFieldVar::to_constraint_field` was majorly called for generating the inputs (preimage) to hash functions. However, it turns out that the underlying conversion strategy (optimized for weight) is not optimal for reducing the length of hash preimage. We can go further by maximizing the number of bits per limb, thereby minimizing the preimage length. See `circuits/nonnative.rs` for details. * Format * Fix clippy warnings * Move the comments to the right position * Cleanup unnecessary code
307 lines
11 KiB
Rust
307 lines
11 KiB
Rust
use ark_ec::{AffineRepr, CurveGroup};
|
|
use ark_ff::{BigInteger, PrimeField};
|
|
use ark_r1cs_std::{
|
|
alloc::{AllocVar, AllocationMode},
|
|
boolean::Boolean,
|
|
fields::{
|
|
fp::FpVar,
|
|
nonnative::{
|
|
params::{get_params, OptimizationType},
|
|
AllocatedNonNativeFieldVar, NonNativeFieldVar,
|
|
},
|
|
FieldVar,
|
|
},
|
|
ToBitsGadget, ToConstraintFieldGadget,
|
|
};
|
|
use ark_relations::r1cs::{ConstraintSystemRef, Namespace, OptimizationGoal, SynthesisError};
|
|
use ark_std::Zero;
|
|
use core::borrow::Borrow;
|
|
use std::marker::PhantomData;
|
|
|
|
/// Compose a vector boolean into a `NonNativeFieldVar`
|
|
pub fn nonnative_field_var_from_le_bits<TargetField: PrimeField, BaseField: PrimeField>(
|
|
cs: ConstraintSystemRef<BaseField>,
|
|
bits: &[Boolean<BaseField>],
|
|
) -> Result<NonNativeFieldVar<TargetField, BaseField>, SynthesisError> {
|
|
let params = get_params(
|
|
TargetField::MODULUS_BIT_SIZE as usize,
|
|
BaseField::MODULUS_BIT_SIZE as usize,
|
|
match cs.optimization_goal() {
|
|
OptimizationGoal::None => OptimizationType::Constraints,
|
|
OptimizationGoal::Constraints => OptimizationType::Constraints,
|
|
OptimizationGoal::Weight => OptimizationType::Weight,
|
|
},
|
|
);
|
|
|
|
// push the lower limbs first
|
|
let mut limbs = bits
|
|
.chunks(params.bits_per_limb)
|
|
.map(Boolean::le_bits_to_fp_var)
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
limbs.resize(params.num_limbs, FpVar::zero());
|
|
limbs.reverse();
|
|
|
|
Ok(AllocatedNonNativeFieldVar {
|
|
cs,
|
|
limbs,
|
|
num_of_additions_over_normal_form: BaseField::one(),
|
|
is_in_the_normal_form: false,
|
|
target_phantom: PhantomData,
|
|
}
|
|
.into())
|
|
}
|
|
|
|
/// A more efficient version of `NonNativeFieldVar::to_constraint_field`
|
|
pub fn nonnative_field_var_to_constraint_field<TargetField: PrimeField, BaseField: PrimeField>(
|
|
f: &NonNativeFieldVar<TargetField, BaseField>,
|
|
) -> Result<Vec<FpVar<BaseField>>, SynthesisError> {
|
|
let bits = f.to_bits_le()?;
|
|
|
|
let bits_per_limb = BaseField::MODULUS_BIT_SIZE as usize - 1;
|
|
let num_limbs = (TargetField::MODULUS_BIT_SIZE as usize).div_ceil(bits_per_limb);
|
|
|
|
let mut limbs = bits
|
|
.chunks(bits_per_limb)
|
|
.map(|chunk| {
|
|
let mut limb = FpVar::<BaseField>::zero();
|
|
let mut w = BaseField::one();
|
|
for b in chunk.iter() {
|
|
limb += FpVar::from(b.clone()) * w;
|
|
w.double_in_place();
|
|
}
|
|
limb
|
|
})
|
|
.collect::<Vec<FpVar<BaseField>>>();
|
|
limbs.resize(num_limbs, FpVar::zero());
|
|
limbs.reverse();
|
|
|
|
Ok(limbs)
|
|
}
|
|
|
|
/// The out-circuit counterpart of `nonnative_field_var_to_constraint_field`
|
|
pub fn nonnative_field_to_field_elements<TargetField: PrimeField, BaseField: PrimeField>(
|
|
f: &TargetField,
|
|
) -> Vec<BaseField> {
|
|
let bits = f.into_bigint().to_bits_le();
|
|
|
|
let bits_per_limb = BaseField::MODULUS_BIT_SIZE as usize - 1;
|
|
let num_limbs = (TargetField::MODULUS_BIT_SIZE as usize).div_ceil(bits_per_limb);
|
|
|
|
let mut limbs = bits
|
|
.chunks(bits_per_limb)
|
|
.map(|chunk| {
|
|
let mut limb = BaseField::zero();
|
|
let mut w = BaseField::one();
|
|
for &b in chunk.iter() {
|
|
limb += BaseField::from(b) * w;
|
|
w.double_in_place();
|
|
}
|
|
limb
|
|
})
|
|
.collect::<Vec<BaseField>>();
|
|
limbs.resize(num_limbs, BaseField::zero());
|
|
limbs.reverse();
|
|
|
|
limbs
|
|
}
|
|
|
|
/// NonNativeAffineVar represents an elliptic curve point in Affine representation in the non-native
|
|
/// field, over the constraint field. It is not intended to perform operations, but just to contain
|
|
/// the affine coordinates in order to perform hash operations of the point.
|
|
#[derive(Debug, Clone)]
|
|
pub struct NonNativeAffineVar<C: CurveGroup>
|
|
where
|
|
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
|
|
{
|
|
pub x: NonNativeFieldVar<C::BaseField, C::ScalarField>,
|
|
pub y: NonNativeFieldVar<C::BaseField, C::ScalarField>,
|
|
}
|
|
|
|
impl<C> AllocVar<C, C::ScalarField> for NonNativeAffineVar<C>
|
|
where
|
|
C: CurveGroup,
|
|
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
|
|
{
|
|
fn new_variable<T: Borrow<C>>(
|
|
cs: impl Into<Namespace<C::ScalarField>>,
|
|
f: impl FnOnce() -> Result<T, SynthesisError>,
|
|
mode: AllocationMode,
|
|
) -> Result<Self, SynthesisError> {
|
|
f().and_then(|val| {
|
|
let cs = cs.into();
|
|
|
|
let affine = val.borrow().into_affine();
|
|
let zero_point = (&C::BaseField::zero(), &C::BaseField::zero());
|
|
let xy = affine.xy().unwrap_or(zero_point);
|
|
|
|
let x = NonNativeFieldVar::<C::BaseField, C::ScalarField>::new_variable(
|
|
cs.clone(),
|
|
|| Ok(xy.0),
|
|
mode,
|
|
)?;
|
|
let y = NonNativeFieldVar::<C::BaseField, C::ScalarField>::new_variable(
|
|
cs.clone(),
|
|
|| Ok(xy.1),
|
|
mode,
|
|
)?;
|
|
|
|
Ok(Self { x, y })
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<C: CurveGroup> ToConstraintFieldGadget<C::ScalarField> for NonNativeAffineVar<C>
|
|
where
|
|
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
|
|
{
|
|
// A more efficient version of `point_to_nonnative_limbs_custom_opt`.
|
|
// Used for converting `NonNativeAffineVar` to a vector of `FpVar` with minimum length in
|
|
// the circuit.
|
|
fn to_constraint_field(&self) -> Result<Vec<FpVar<C::ScalarField>>, SynthesisError> {
|
|
let x = nonnative_field_var_to_constraint_field(&self.x)?;
|
|
let y = nonnative_field_var_to_constraint_field(&self.y)?;
|
|
Ok([x, y].concat())
|
|
}
|
|
}
|
|
|
|
/// The out-circuit counterpart of `NonNativeAffineVar::to_constraint_field`
|
|
#[allow(clippy::type_complexity)]
|
|
pub fn nonnative_affine_to_field_elements<C: CurveGroup>(
|
|
p: C,
|
|
) -> Result<(Vec<C::ScalarField>, Vec<C::ScalarField>), SynthesisError>
|
|
where
|
|
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
|
|
{
|
|
let affine = p.into_affine();
|
|
if affine.is_zero() {
|
|
let x = nonnative_field_to_field_elements(&C::BaseField::zero());
|
|
let y = nonnative_field_to_field_elements(&C::BaseField::zero());
|
|
return Ok((x, y));
|
|
}
|
|
|
|
let (x, y) = affine.xy().unwrap();
|
|
let x = nonnative_field_to_field_elements(x);
|
|
let y = nonnative_field_to_field_elements(y);
|
|
Ok((x, y))
|
|
}
|
|
|
|
impl<C: CurveGroup> NonNativeAffineVar<C>
|
|
where
|
|
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
|
|
{
|
|
// A wrapper of `point_to_nonnative_limbs_custom_opt` with constraints-focused optimization
|
|
// type (which is the default optimization type for arkworks' Groth16).
|
|
// Used for extracting a list of field elements of type `C::ScalarField` from the public input
|
|
// `p`, in exactly the same way as how `NonNativeAffineVar` is represented as limbs of type
|
|
// `FpVar` in-circuit.
|
|
#[allow(clippy::type_complexity)]
|
|
pub fn inputize(p: C) -> Result<(Vec<C::ScalarField>, Vec<C::ScalarField>), SynthesisError> {
|
|
point_to_nonnative_limbs_custom_opt(p, OptimizationType::Constraints)
|
|
}
|
|
}
|
|
|
|
// Used to compute (outside the circuit) the limbs representation of a point.
|
|
// For `OptimizationType::Constraints`, the result matches the one used in-circuit.
|
|
// For `OptimizationType::Weight`, the result vector is more dense and is suitable for hashing.
|
|
// It is possible to further optimize the length of the result vector (see
|
|
// `nonnative_affine_to_field_elements`)
|
|
#[allow(clippy::type_complexity)]
|
|
fn point_to_nonnative_limbs_custom_opt<C: CurveGroup>(
|
|
p: C,
|
|
optimization_type: OptimizationType,
|
|
) -> Result<(Vec<C::ScalarField>, Vec<C::ScalarField>), SynthesisError>
|
|
where
|
|
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
|
|
{
|
|
let affine = p.into_affine();
|
|
if affine.is_zero() {
|
|
let x =
|
|
AllocatedNonNativeFieldVar::<C::BaseField, C::ScalarField>::get_limbs_representations(
|
|
&C::BaseField::zero(),
|
|
optimization_type,
|
|
)?;
|
|
let y =
|
|
AllocatedNonNativeFieldVar::<C::BaseField, C::ScalarField>::get_limbs_representations(
|
|
&C::BaseField::zero(),
|
|
optimization_type,
|
|
)?;
|
|
return Ok((x, y));
|
|
}
|
|
|
|
let (x, y) = affine.xy().unwrap();
|
|
let x = AllocatedNonNativeFieldVar::<C::BaseField, C::ScalarField>::get_limbs_representations(
|
|
x,
|
|
optimization_type,
|
|
)?;
|
|
let y = AllocatedNonNativeFieldVar::<C::BaseField, C::ScalarField>::get_limbs_representations(
|
|
y,
|
|
optimization_type,
|
|
)?;
|
|
Ok((x, y))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use ark_pallas::{Fr, Projective};
|
|
use ark_r1cs_std::R1CSVar;
|
|
use ark_relations::r1cs::ConstraintSystem;
|
|
use ark_std::UniformRand;
|
|
|
|
#[test]
|
|
fn test_alloc_zero() {
|
|
let cs = ConstraintSystem::<Fr>::new_ref();
|
|
|
|
// dealing with the 'zero' point should not panic when doing the unwrap
|
|
let p = Projective::zero();
|
|
assert!(NonNativeAffineVar::<Projective>::new_witness(cs.clone(), || Ok(p)).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_arkworks_to_constraint_field() {
|
|
let cs = ConstraintSystem::<Fr>::new_ref();
|
|
|
|
// check that point_to_nonnative_limbs returns the expected values
|
|
let mut rng = ark_std::test_rng();
|
|
let p = Projective::rand(&mut rng);
|
|
let pVar = NonNativeAffineVar::<Projective>::new_witness(cs.clone(), || Ok(p)).unwrap();
|
|
let (x, y) = point_to_nonnative_limbs_custom_opt(p, OptimizationType::Weight).unwrap();
|
|
assert_eq!(pVar.x.to_constraint_field().unwrap().value().unwrap(), x);
|
|
assert_eq!(pVar.y.to_constraint_field().unwrap().value().unwrap(), y);
|
|
}
|
|
|
|
#[test]
|
|
fn test_improved_to_constraint_field() {
|
|
let cs = ConstraintSystem::<Fr>::new_ref();
|
|
|
|
// check that point_to_nonnative_limbs returns the expected values
|
|
let mut rng = ark_std::test_rng();
|
|
let p = Projective::rand(&mut rng);
|
|
let pVar = NonNativeAffineVar::<Projective>::new_witness(cs.clone(), || Ok(p)).unwrap();
|
|
let (x, y) = nonnative_affine_to_field_elements(p).unwrap();
|
|
assert_eq!(
|
|
pVar.to_constraint_field().unwrap().value().unwrap(),
|
|
[x, y].concat()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inputize() {
|
|
let cs = ConstraintSystem::<Fr>::new_ref();
|
|
|
|
// check that point_to_nonnative_limbs returns the expected values
|
|
let mut rng = ark_std::test_rng();
|
|
let p = Projective::rand(&mut rng);
|
|
let pVar = NonNativeAffineVar::<Projective>::new_witness(cs.clone(), || Ok(p)).unwrap();
|
|
let (x, y) = NonNativeAffineVar::inputize(p).unwrap();
|
|
|
|
match (pVar.x, pVar.y) {
|
|
(NonNativeFieldVar::Var(p_x), NonNativeFieldVar::Var(p_y)) => {
|
|
assert_eq!(p_x.limbs.value().unwrap(), x);
|
|
assert_eq!(p_y.limbs.value().unwrap(), y);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|