use ff::{Field, PrimeField};
use midnight_curves::Fq as Fp;
use midnight_proofs::{
circuit::{Layouter, SimpleFloorPlanner, Value},
dev::MockProver,
plonk::{Circuit, ConstraintSystem, Error},
};
use num_bigint::{BigInt, RandBigInt};
use num_traits::Zero;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use super::{
chip::{P2RDecompositionChip, P2RDecompositionConfig},
instructions::CoreDecompositionInstructions,
};
use crate::CircuitField;
use crate::{
field::{
decomposition::{
cpu_utils::{decompose_in_variable_limbsizes, variable_limbsize_coefficients},
pow2range::Pow2RangeChip,
},
native::{NB_ARITH_COLS, NB_ARITH_FIXED_COLS},
NativeChip,
},
instructions::{ArithInstructions, AssertionInstructions, AssignmentInstructions},
types::AssignedNative,
utils::{util::bigint_to_fe, ComposableChip},
};
#[test]
fn test_decompose_variable_in_cpu() {
let mut rng = ChaCha8Rng::from_entropy();
let mut limb_sizes = (0..6).map(|_| rng.gen_range(0..16usize)).collect::<Vec<_>>();
limb_sizes.push(rng.gen_range(1..16usize));
let max_bound: u128 = 1 << limb_sizes.iter().sum::<usize>();
let x = Fp::from_u128(rng.gen_range(0..max_bound));
let limbs = decompose_in_variable_limbsizes::<Fp, Fp>(&x, limb_sizes.as_slice());
let coefficients = variable_limbsize_coefficients::<Fp>(limb_sizes.as_slice());
let reconstructed = limbs
.iter()
.zip(coefficients.iter())
.fold(Fp::ZERO, |acc, (limb, c)| acc + limb * c);
assert_eq!(x, reconstructed);
}
#[derive(Clone, Debug)]
enum LimbType {
Variable(Vec<usize>),
Fixed((usize, usize)),
}
#[derive(Clone, Debug)]
struct TestDecompositionCircuit<F: CircuitField, const NR_COLS: usize> {
input: F,
limb_sizes: LimbType,
expected: Vec<F>,
}
impl<F, const NR_COLS: usize> Circuit<F> for TestDecompositionCircuit<F, NR_COLS>
where
F: CircuitField,
{
type Config = P2RDecompositionConfig;
type FloorPlanner = SimpleFloorPlanner;
type Params = ();
fn without_witnesses(&self) -> Self {
unimplemented!()
}
fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
let advice_columns: [_; NB_ARITH_COLS] = core::array::from_fn(|_| meta.advice_column());
let fixed_columns: [_; NB_ARITH_FIXED_COLS] = core::array::from_fn(|_| meta.fixed_column());
let committed_instance_column = meta.instance_column();
let instance_column = meta.instance_column();
let native_config = NativeChip::configure(
meta,
&(
advice_columns,
fixed_columns,
[committed_instance_column, instance_column],
),
);
let pow2range_config = Pow2RangeChip::configure(meta, &advice_columns[1..=NR_COLS]);
P2RDecompositionConfig::new(&native_config, &pow2range_config)
}
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<F>,
) -> Result<(), Error> {
let max_bit_len = 8;
let native_chip = NativeChip::<F>::new(&config.native_config, &());
let decomposition_chip = P2RDecompositionChip::new(&config, &max_bit_len);
let assigned_input: AssignedNative<F> =
native_chip.assign(&mut layouter, Value::known(self.input))?;
let computed_limbs = match self.limb_sizes.clone() {
LimbType::Variable(limb_sizes) => {
let (_, limbs) = decomposition_chip.decompose_core(
&mut layouter,
assigned_input.value().copied(),
limb_sizes.as_slice(),
)?;
Ok(limbs)
}
LimbType::Fixed((bit_length, limb_size)) => {
decomposition_chip.decompose_fixed_limb_size(
&mut layouter,
&assigned_input,
bit_length,
limb_size,
)
}
}?;
let assigned_expected_result = self
.expected
.iter()
.map(|limb| native_chip.assign(&mut layouter, Value::known(*limb)))
.collect::<Result<Vec<_>, _>>()?;
computed_limbs
.iter()
.zip(assigned_expected_result.iter())
.try_for_each(|(x, y)| native_chip.assert_equal(&mut layouter, x, y))?;
let terms_owned = match self.limb_sizes.clone() {
LimbType::Variable(limb_sizes) => {
variable_limbsize_coefficients(limb_sizes.as_slice())
.iter()
.filter(|c: &&F| **c != F::ZERO)
.copied()
.zip(computed_limbs.iter().cloned())
.collect::<Vec<(F, AssignedNative<F>)>>()
}
LimbType::Fixed((bit_length, limb_size)) => {
let mut limb_sizes = vec![limb_size; bit_length / limb_size];
if bit_length % limb_size != 0 {
limb_sizes.push(bit_length % limb_size)
}
variable_limbsize_coefficients(limb_sizes.as_slice())
.iter()
.copied()
.zip(computed_limbs.iter().cloned())
.collect::<Vec<(F, AssignedNative<F>)>>()
}
};
let terms = terms_owned.iter().map(|(c, v)| (*c, v.clone())).collect::<Vec<_>>();
let lc_result = native_chip.linear_combination(&mut layouter, terms.as_slice(), F::ZERO)?;
native_chip.assert_equal(&mut layouter, &lc_result, &assigned_input)?;
decomposition_chip.load(&mut layouter)
}
}
fn run_decomposition_chip_variable_test<const NR_COLS: usize>() {
let mut rng = ChaCha8Rng::from_entropy();
let mut limb_sizes = Vec::new();
for i in [7, 4, 6] {
let limb_size_group = rng.gen_range(1..=8usize);
for _ in 0..i {
limb_sizes.push(limb_size_group);
}
while limb_sizes.len() % NR_COLS != 0 {
limb_sizes.push(0);
}
}
let max_bound: u128 = 1 << limb_sizes.iter().sum::<usize>();
let x = Fp::from_u128(rng.gen_range(0..max_bound));
let non_zero_limb_sizes =
limb_sizes.iter().filter(|x| !x.is_zero()).copied().collect::<Vec<_>>();
let expected = decompose_in_variable_limbsizes(&x, non_zero_limb_sizes.as_slice());
let circuit_variable = TestDecompositionCircuit::<Fp, NR_COLS> {
input: x,
limb_sizes: LimbType::Variable(limb_sizes),
expected,
};
let prover = MockProver::run(&circuit_variable, vec![vec![], vec![]])
.expect("Failed to run mock prover");
prover.assert_satisfied();
}
#[test]
fn test_decomposition_chip_variable() {
run_decomposition_chip_variable_test::<1>();
run_decomposition_chip_variable_test::<2>();
run_decomposition_chip_variable_test::<3>();
run_decomposition_chip_variable_test::<4>();
}
fn run_decomposition_chip_fixed_test<const NR_COLS: usize>() {
let mut rng = ChaCha8Rng::from_entropy();
let limb_size_small = rng.gen_range(1..=8);
let limb_size_big = rng.gen_range(9..=42usize);
let x = Fp::random(rng);
for limb_size in [limb_size_small, limb_size_big] {
let mut limb_sizes = vec![limb_size; Fp::NUM_BITS as usize / limb_size];
if !(Fp::NUM_BITS as usize).is_multiple_of(limb_size) {
limb_sizes.push(Fp::NUM_BITS as usize % limb_size)
}
let expected = decompose_in_variable_limbsizes(&x, limb_sizes.as_slice());
let circuit_fixed = TestDecompositionCircuit::<Fp, NR_COLS> {
input: x,
limb_sizes: LimbType::Fixed((Fp::NUM_BITS as usize, limb_size)),
expected,
};
let prover = MockProver::run(&circuit_fixed, vec![vec![], vec![]])
.expect("Failed to run mock prover");
prover.assert_satisfied();
}
}
#[test]
fn test_decomposition_chip_fixed() {
run_decomposition_chip_fixed_test::<1>();
run_decomposition_chip_fixed_test::<2>();
run_decomposition_chip_fixed_test::<3>();
run_decomposition_chip_fixed_test::<4>();
}
#[derive(Clone, Debug)]
struct TestLessThanPow2Circuit<F: CircuitField, const NR_COLS: usize> {
input: F,
bound: usize,
}
impl<F, const NR_COLS: usize> Circuit<F> for TestLessThanPow2Circuit<F, NR_COLS>
where
F: CircuitField,
{
type Config = P2RDecompositionConfig;
type FloorPlanner = SimpleFloorPlanner;
type Params = ();
fn without_witnesses(&self) -> Self {
unimplemented!()
}
fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
let advice_columns: [_; NB_ARITH_COLS] = core::array::from_fn(|_| meta.advice_column());
let fixed_columns: [_; NB_ARITH_FIXED_COLS] = core::array::from_fn(|_| meta.fixed_column());
let committed_instance_column = meta.instance_column();
let instance_column = meta.instance_column();
let native_config = NativeChip::configure(
meta,
&(
advice_columns,
fixed_columns,
[committed_instance_column, instance_column],
),
);
let pow2range_config = Pow2RangeChip::configure(meta, &advice_columns[1..=NR_COLS]);
P2RDecompositionConfig::new(&native_config, &pow2range_config)
}
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<F>,
) -> Result<(), Error> {
let max_bit_len = 8;
let native_chip = NativeChip::<F>::new(&config.native_config, &());
let decomposition_chip = P2RDecompositionChip::new(&config, &max_bit_len);
let assigned_input = native_chip.assign(&mut layouter, Value::known(self.input))?;
decomposition_chip.assert_less_than_pow2(&mut layouter, &assigned_input, self.bound)?;
decomposition_chip.load(&mut layouter)
}
}
fn run_decomposition_less_than_pow2_test<const NR_COLS: usize>() {
let mut rng = ChaCha8Rng::from_entropy();
let bound = rng.gen_range(1..255usize);
let max_value = BigInt::from(1) << bound;
let bign = rng.gen_bigint_range(&BigInt::zero(), &max_value);
let x: Fp = bigint_to_fe(&bign);
let circuit = TestLessThanPow2Circuit::<Fp, NR_COLS> { input: x, bound };
let prover =
MockProver::run(&circuit, vec![vec![], vec![]]).expect("Failed to run mock prover");
prover.assert_satisfied();
}
#[test]
fn test_decomposition_less_than_pow2() {
run_decomposition_less_than_pow2_test::<1>();
run_decomposition_less_than_pow2_test::<2>();
run_decomposition_less_than_pow2_test::<3>();
run_decomposition_less_than_pow2_test::<4>();
}