use crate::{
constants::{NUM_FE_WITHOUT_IO_FOR_CRHF, NUM_HASH_BITS},
gadgets::{
ecc::AllocatedPoint,
r1cs::{AllocatedR1CSInstance, AllocatedRelaxedR1CSInstance},
utils::{
alloc_num_equals, alloc_scalar_as_base, alloc_zero, conditionally_select_vec, le_bits_to_num,
},
},
r1cs::{R1CSInstance, RelaxedR1CSInstance},
traits::{
circuit::StepCircuit, commitment::CommitmentTrait, Group, ROCircuitTrait, ROConstantsCircuit,
},
Commitment,
};
use bellpepper::gadgets::Assignment;
use bellpepper_core::{
boolean::{AllocatedBit, Boolean},
num::AllocatedNum,
ConstraintSystem, SynthesisError,
};
use ff::Field;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NovaAugmentedCircuitParams {
limb_width: usize,
n_limbs: usize,
is_primary_circuit: bool, }
impl NovaAugmentedCircuitParams {
pub const fn new(limb_width: usize, n_limbs: usize, is_primary_circuit: bool) -> Self {
Self {
limb_width,
n_limbs,
is_primary_circuit,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(bound = "")]
pub struct NovaAugmentedCircuitInputs<G: Group> {
params: G::Scalar,
i: G::Base,
z0: Vec<G::Base>,
zi: Option<Vec<G::Base>>,
U: Option<RelaxedR1CSInstance<G>>,
u: Option<R1CSInstance<G>>,
T: Option<Commitment<G>>,
}
impl<G: Group> NovaAugmentedCircuitInputs<G> {
pub fn new(
params: G::Scalar,
i: G::Base,
z0: Vec<G::Base>,
zi: Option<Vec<G::Base>>,
U: Option<RelaxedR1CSInstance<G>>,
u: Option<R1CSInstance<G>>,
T: Option<Commitment<G>>,
) -> Self {
Self {
params,
i,
z0,
zi,
U,
u,
T,
}
}
}
pub struct NovaAugmentedCircuit<'a, G: Group, SC: StepCircuit<G::Base>> {
params: &'a NovaAugmentedCircuitParams,
ro_consts: ROConstantsCircuit<G>,
inputs: Option<NovaAugmentedCircuitInputs<G>>,
step_circuit: &'a SC, }
impl<'a, G: Group, SC: StepCircuit<G::Base>> NovaAugmentedCircuit<'a, G, SC> {
pub const fn new(
params: &'a NovaAugmentedCircuitParams,
inputs: Option<NovaAugmentedCircuitInputs<G>>,
step_circuit: &'a SC,
ro_consts: ROConstantsCircuit<G>,
) -> Self {
Self {
params,
inputs,
step_circuit,
ro_consts,
}
}
fn alloc_witness<CS: ConstraintSystem<<G as Group>::Base>>(
&self,
mut cs: CS,
arity: usize,
) -> Result<
(
AllocatedNum<G::Base>,
AllocatedNum<G::Base>,
Vec<AllocatedNum<G::Base>>,
Vec<AllocatedNum<G::Base>>,
AllocatedRelaxedR1CSInstance<G>,
AllocatedR1CSInstance<G>,
AllocatedPoint<G>,
),
SynthesisError,
> {
let params = alloc_scalar_as_base::<G, _>(
cs.namespace(|| "params"),
self.inputs.as_ref().map(|inputs| inputs.params),
)?;
let i = AllocatedNum::alloc(cs.namespace(|| "i"), || Ok(self.inputs.get()?.i))?;
let z_0 = (0..arity)
.map(|i| {
AllocatedNum::alloc(cs.namespace(|| format!("z0_{i}")), || {
Ok(self.inputs.get()?.z0[i])
})
})
.collect::<Result<Vec<AllocatedNum<G::Base>>, _>>()?;
let zero = vec![G::Base::ZERO; arity];
let z_i = (0..arity)
.map(|i| {
AllocatedNum::alloc(cs.namespace(|| format!("zi_{i}")), || {
Ok(self.inputs.get()?.zi.as_ref().unwrap_or(&zero)[i])
})
})
.collect::<Result<Vec<AllocatedNum<G::Base>>, _>>()?;
let U: AllocatedRelaxedR1CSInstance<G> = AllocatedRelaxedR1CSInstance::alloc(
cs.namespace(|| "Allocate U"),
self.inputs.as_ref().and_then(|inputs| inputs.U.as_ref()),
self.params.limb_width,
self.params.n_limbs,
)?;
let u = AllocatedR1CSInstance::alloc(
cs.namespace(|| "allocate instance u to fold"),
self.inputs.as_ref().and_then(|inputs| inputs.u.as_ref()),
)?;
let T = AllocatedPoint::alloc(
cs.namespace(|| "allocate T"),
self
.inputs
.as_ref()
.and_then(|inputs| inputs.T.map(|T| T.to_coordinates())),
)?;
Ok((params, i, z_0, z_i, U, u, T))
}
fn synthesize_base_case<CS: ConstraintSystem<<G as Group>::Base>>(
&self,
mut cs: CS,
u: AllocatedR1CSInstance<G>,
) -> Result<AllocatedRelaxedR1CSInstance<G>, SynthesisError> {
let U_default: AllocatedRelaxedR1CSInstance<G> = if self.params.is_primary_circuit {
AllocatedRelaxedR1CSInstance::default(
cs.namespace(|| "Allocate U_default"),
self.params.limb_width,
self.params.n_limbs,
)?
} else {
AllocatedRelaxedR1CSInstance::from_r1cs_instance(
cs.namespace(|| "Allocate U_default"),
u,
self.params.limb_width,
self.params.n_limbs,
)?
};
Ok(U_default)
}
fn synthesize_non_base_case<CS: ConstraintSystem<<G as Group>::Base>>(
&self,
mut cs: CS,
params: &AllocatedNum<G::Base>,
i: &AllocatedNum<G::Base>,
z_0: &[AllocatedNum<G::Base>],
z_i: &[AllocatedNum<G::Base>],
U: &AllocatedRelaxedR1CSInstance<G>,
u: &AllocatedR1CSInstance<G>,
T: &AllocatedPoint<G>,
arity: usize,
) -> Result<(AllocatedRelaxedR1CSInstance<G>, AllocatedBit), SynthesisError> {
let mut ro = G::ROCircuit::new(
self.ro_consts.clone(),
NUM_FE_WITHOUT_IO_FOR_CRHF + 2 * arity,
);
ro.absorb(params);
ro.absorb(i);
for e in z_0 {
ro.absorb(e);
}
for e in z_i {
ro.absorb(e);
}
U.absorb_in_ro(cs.namespace(|| "absorb U"), &mut ro)?;
let hash_bits = ro.squeeze(cs.namespace(|| "Input hash"), NUM_HASH_BITS)?;
let hash = le_bits_to_num(cs.namespace(|| "bits to hash"), &hash_bits)?;
let check_pass = alloc_num_equals(
cs.namespace(|| "check consistency of u.X[0] with H(params, U, i, z0, zi)"),
&u.X0,
&hash,
)?;
let U_fold = U.fold_with_r1cs(
cs.namespace(|| "compute fold of U and u"),
params,
u,
T,
self.ro_consts.clone(),
self.params.limb_width,
self.params.n_limbs,
)?;
Ok((U_fold, check_pass))
}
}
impl<'a, G: Group, SC: StepCircuit<G::Base>> NovaAugmentedCircuit<'a, G, SC> {
pub fn synthesize<CS: ConstraintSystem<<G as Group>::Base>>(
self,
cs: &mut CS,
) -> Result<Vec<AllocatedNum<G::Base>>, SynthesisError> {
let arity = self.step_circuit.arity();
let (params, i, z_0, z_i, U, u, T) =
self.alloc_witness(cs.namespace(|| "allocate the circuit witness"), arity)?;
let zero = alloc_zero(cs.namespace(|| "zero"));
let is_base_case = alloc_num_equals(cs.namespace(|| "Check if base case"), &i.clone(), &zero)?;
let Unew_base = self.synthesize_base_case(cs.namespace(|| "base case"), u.clone())?;
let (Unew_non_base, check_non_base_pass) = self.synthesize_non_base_case(
cs.namespace(|| "synthesize non base case"),
¶ms,
&i,
&z_0,
&z_i,
&U,
&u,
&T,
arity,
)?;
let should_be_false = AllocatedBit::nor(
cs.namespace(|| "check_non_base_pass nor base_case"),
&check_non_base_pass,
&is_base_case,
)?;
cs.enforce(
|| "check_non_base_pass nor base_case = false",
|lc| lc + should_be_false.get_variable(),
|lc| lc + CS::one(),
|lc| lc,
);
let Unew = Unew_base.conditionally_select(
cs.namespace(|| "compute U_new"),
&Unew_non_base,
&Boolean::from(is_base_case.clone()),
)?;
let i_new = AllocatedNum::alloc(cs.namespace(|| "i + 1"), || {
Ok(*i.get_value().get()? + G::Base::ONE)
})?;
cs.enforce(
|| "check i + 1",
|lc| lc,
|lc| lc,
|lc| lc + i_new.get_variable() - CS::one() - i.get_variable(),
);
let z_input = conditionally_select_vec(
cs.namespace(|| "select input to F"),
&z_0,
&z_i,
&Boolean::from(is_base_case),
)?;
let z_next = self
.step_circuit
.synthesize(&mut cs.namespace(|| "F"), &z_input)?;
if z_next.len() != arity {
return Err(SynthesisError::IncompatibleLengthVector(
"z_next".to_string(),
));
}
let mut ro = G::ROCircuit::new(self.ro_consts, NUM_FE_WITHOUT_IO_FOR_CRHF + 2 * arity);
ro.absorb(¶ms);
ro.absorb(&i_new);
for e in &z_0 {
ro.absorb(e);
}
for e in &z_next {
ro.absorb(e);
}
Unew.absorb_in_ro(cs.namespace(|| "absorb U_new"), &mut ro)?;
let hash_bits = ro.squeeze(cs.namespace(|| "output hash bits"), NUM_HASH_BITS)?;
let hash = le_bits_to_num(cs.namespace(|| "convert hash to num"), &hash_bits)?;
u.X1
.inputize(cs.namespace(|| "Output unmodified hash of the other circuit"))?;
hash.inputize(cs.namespace(|| "output new hash of this circuit"))?;
Ok(z_next)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bellpepper::{solver::SatisfyingAssignment, test_shape_cs::TestShapeCS};
type PastaG1 = pasta_curves::pallas::Point;
type PastaG2 = pasta_curves::vesta::Point;
use crate::constants::{BN_LIMB_WIDTH, BN_N_LIMBS};
use crate::provider;
use crate::{
bellpepper::r1cs::{NovaShape, NovaWitness},
gadgets::utils::scalar_as_base,
provider::poseidon::PoseidonConstantsCircuit,
traits::circuit::TrivialCircuit,
};
fn test_recursive_circuit_with<G1, G2>(
primary_params: &NovaAugmentedCircuitParams,
secondary_params: &NovaAugmentedCircuitParams,
ro_consts1: ROConstantsCircuit<G2>,
ro_consts2: ROConstantsCircuit<G1>,
num_constraints_primary: usize,
num_constraints_secondary: usize,
) where
G1: Group<Base = <G2 as Group>::Scalar>,
G2: Group<Base = <G1 as Group>::Scalar>,
{
let tc1 = TrivialCircuit::default();
let circuit1: NovaAugmentedCircuit<'_, G2, TrivialCircuit<<G2 as Group>::Base>> =
NovaAugmentedCircuit::new(primary_params, None, &tc1, ro_consts1.clone());
let mut cs: TestShapeCS<G1> = TestShapeCS::new();
let _ = circuit1.synthesize(&mut cs);
let (shape1, ck1) = cs.r1cs_shape();
assert_eq!(cs.num_constraints(), num_constraints_primary);
let tc2 = TrivialCircuit::default();
let circuit2: NovaAugmentedCircuit<'_, G1, TrivialCircuit<<G1 as Group>::Base>> =
NovaAugmentedCircuit::new(secondary_params, None, &tc2, ro_consts2.clone());
let mut cs: TestShapeCS<G2> = TestShapeCS::new();
let _ = circuit2.synthesize(&mut cs);
let (shape2, ck2) = cs.r1cs_shape();
assert_eq!(cs.num_constraints(), num_constraints_secondary);
let zero1 = <<G2 as Group>::Base as Field>::ZERO;
let mut cs1: SatisfyingAssignment<G1> = SatisfyingAssignment::new();
let inputs1: NovaAugmentedCircuitInputs<G2> = NovaAugmentedCircuitInputs::new(
scalar_as_base::<G1>(zero1), zero1,
vec![zero1],
None,
None,
None,
None,
);
let circuit1: NovaAugmentedCircuit<'_, G2, TrivialCircuit<<G2 as Group>::Base>> =
NovaAugmentedCircuit::new(primary_params, Some(inputs1), &tc1, ro_consts1);
let _ = circuit1.synthesize(&mut cs1);
let (inst1, witness1) = cs1.r1cs_instance_and_witness(&shape1, &ck1).unwrap();
assert!(shape1.is_sat(&ck1, &inst1, &witness1).is_ok());
let zero2 = <<G1 as Group>::Base as Field>::ZERO;
let mut cs2: SatisfyingAssignment<G2> = SatisfyingAssignment::new();
let inputs2: NovaAugmentedCircuitInputs<G1> = NovaAugmentedCircuitInputs::new(
scalar_as_base::<G2>(zero2), zero2,
vec![zero2],
None,
None,
Some(inst1),
None,
);
let circuit2: NovaAugmentedCircuit<'_, G1, TrivialCircuit<<G1 as Group>::Base>> =
NovaAugmentedCircuit::new(secondary_params, Some(inputs2), &tc2, ro_consts2);
let _ = circuit2.synthesize(&mut cs2);
let (inst2, witness2) = cs2.r1cs_instance_and_witness(&shape2, &ck2).unwrap();
assert!(shape2.is_sat(&ck2, &inst2, &witness2).is_ok());
}
#[test]
fn test_recursive_circuit_pasta() {
let params1 = NovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, true);
let params2 = NovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, false);
let ro_consts1: ROConstantsCircuit<PastaG2> = PoseidonConstantsCircuit::default();
let ro_consts2: ROConstantsCircuit<PastaG1> = PoseidonConstantsCircuit::default();
test_recursive_circuit_with::<PastaG1, PastaG2>(
¶ms1, ¶ms2, ro_consts1, ro_consts2, 9815, 10347,
);
}
#[test]
fn test_recursive_circuit_grumpkin() {
let params1 = NovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, true);
let params2 = NovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, false);
let ro_consts1: ROConstantsCircuit<provider::bn256_grumpkin::grumpkin::Point> =
PoseidonConstantsCircuit::default();
let ro_consts2: ROConstantsCircuit<provider::bn256_grumpkin::bn256::Point> =
PoseidonConstantsCircuit::default();
test_recursive_circuit_with::<
provider::bn256_grumpkin::bn256::Point,
provider::bn256_grumpkin::grumpkin::Point,
>(¶ms1, ¶ms2, ro_consts1, ro_consts2, 9983, 10536);
}
#[test]
fn test_recursive_circuit_secp() {
let params1 = NovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, true);
let params2 = NovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, false);
let ro_consts1: ROConstantsCircuit<provider::secp_secq::secq256k1::Point> =
PoseidonConstantsCircuit::default();
let ro_consts2: ROConstantsCircuit<provider::secp_secq::secp256k1::Point> =
PoseidonConstantsCircuit::default();
test_recursive_circuit_with::<
provider::secp_secq::secp256k1::Point,
provider::secp_secq::secq256k1::Point,
>(¶ms1, ¶ms2, ro_consts1, ro_consts2, 10262, 10959);
}
}