mirror of
https://github.com/arnaucube/ark-r1cs-std.git
synced 2026-01-11 16:31:30 +01:00
MNT4/6 curves and recursive SNARKs (#150)
* Add mnt6_753 curve Generalize mnt6 curve model * Add mnt4 curves * Use resampled generators * Calculate correct G2 cofactors * Add fields to r1cs-std * Add pairings * Improve reusing of Fq/Fr among MNT curves * Add instantiations of curves Fix Fp6_2over3 Rebase code to current master * Add test for recursive NIZK proof verification * Address comments in PR * Improve test case and port to GM17 Also fix a minor bug in to_field_vec
This commit is contained in:
@@ -532,3 +532,266 @@ mod test {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_recursive {
|
||||
use gm17::*;
|
||||
use r1cs_core::{ConstraintSynthesizer, ConstraintSystem, SynthesisError};
|
||||
|
||||
use super::*;
|
||||
use algebra::{
|
||||
fields::FpParameters,
|
||||
mnt4_298::{Fq as MNT4Fq, FqParameters as MNT4FqParameters, Fr as MNT4Fr, MNT4_298},
|
||||
mnt6_298::{Fq as MNT6Fq, FqParameters as MNT6FqParameters, Fr as MNT6Fr, MNT6_298},
|
||||
test_rng, BigInteger, PrimeField,
|
||||
};
|
||||
use r1cs_std::{
|
||||
fields::fp::FpGadget, mnt4_298::PairingGadget as MNT4_298PairingGadget,
|
||||
mnt6_298::PairingGadget as MNT6_298PairingGadget,
|
||||
test_constraint_system::TestConstraintSystem, uint8::UInt8,
|
||||
};
|
||||
use rand::Rng;
|
||||
|
||||
type TestProofSystem1 = Gm17<MNT6_298, Bench<MNT4Fq>, MNT6Fr>;
|
||||
type TestVerifierGadget1 = Gm17VerifierGadget<MNT6_298, MNT6Fq, MNT6_298PairingGadget>;
|
||||
type TestProofGadget1 = ProofGadget<MNT6_298, MNT6Fq, MNT6_298PairingGadget>;
|
||||
type TestVkGadget1 = VerifyingKeyGadget<MNT6_298, MNT6Fq, MNT6_298PairingGadget>;
|
||||
|
||||
type TestProofSystem2 = Gm17<MNT4_298, Wrapper, MNT4Fr>;
|
||||
type TestVerifierGadget2 = Gm17VerifierGadget<MNT4_298, MNT4Fq, MNT4_298PairingGadget>;
|
||||
type TestProofGadget2 = ProofGadget<MNT4_298, MNT4Fq, MNT4_298PairingGadget>;
|
||||
type TestVkGadget2 = VerifyingKeyGadget<MNT4_298, MNT4Fq, MNT4_298PairingGadget>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Bench<F: Field> {
|
||||
inputs: Vec<Option<F>>,
|
||||
num_constraints: usize,
|
||||
}
|
||||
|
||||
impl<F: Field> ConstraintSynthesizer<F> for Bench<F> {
|
||||
fn generate_constraints<CS: ConstraintSystem<F>>(
|
||||
self,
|
||||
cs: &mut CS,
|
||||
) -> Result<(), SynthesisError> {
|
||||
assert!(self.inputs.len() >= 2);
|
||||
assert!(self.num_constraints >= self.inputs.len());
|
||||
|
||||
let mut variables: Vec<_> = Vec::with_capacity(self.inputs.len());
|
||||
for (i, input) in self.inputs.into_iter().enumerate() {
|
||||
let input_var = cs.alloc_input(
|
||||
|| format!("Input {}", i),
|
||||
|| input.ok_or(SynthesisError::AssignmentMissing),
|
||||
)?;
|
||||
variables.push((input, input_var));
|
||||
}
|
||||
|
||||
for i in 0..self.num_constraints {
|
||||
let new_entry = {
|
||||
let (input_1_val, input_1_var) = variables[i];
|
||||
let (input_2_val, input_2_var) = variables[i + 1];
|
||||
let result_val = input_1_val
|
||||
.and_then(|input_1| input_2_val.map(|input_2| input_1 * &input_2));
|
||||
let result_var = cs.alloc(
|
||||
|| format!("Result {}", i),
|
||||
|| result_val.ok_or(SynthesisError::AssignmentMissing),
|
||||
)?;
|
||||
cs.enforce(
|
||||
|| format!("Enforce constraint {}", i),
|
||||
|lc| lc + input_1_var,
|
||||
|lc| lc + input_2_var,
|
||||
|lc| lc + result_var,
|
||||
);
|
||||
(result_val, result_var)
|
||||
};
|
||||
variables.push(new_entry);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Wrapper {
|
||||
inputs: Vec<Option<MNT4Fq>>,
|
||||
params: Parameters<MNT6_298>,
|
||||
proof: Proof<MNT6_298>,
|
||||
}
|
||||
|
||||
impl ConstraintSynthesizer<MNT6Fq> for Wrapper {
|
||||
fn generate_constraints<CS: ConstraintSystem<MNT6Fq>>(
|
||||
self,
|
||||
cs: &mut CS,
|
||||
) -> Result<(), SynthesisError> {
|
||||
let params = self.params;
|
||||
let proof = self.proof;
|
||||
let inputs: Vec<_> = self
|
||||
.inputs
|
||||
.into_iter()
|
||||
.map(|input| input.unwrap())
|
||||
.collect();
|
||||
let input_gadgets;
|
||||
|
||||
{
|
||||
let mut cs = cs.ns(|| "Allocate Input");
|
||||
// Chain all input values in one large byte array.
|
||||
let input_bytes = inputs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.flat_map(|input| {
|
||||
input
|
||||
.into_repr()
|
||||
.as_ref()
|
||||
.iter()
|
||||
.flat_map(|l| l.to_le_bytes().to_vec())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Allocate this byte array as input packed into field elements.
|
||||
let input_bytes = UInt8::alloc_input_vec(cs.ns(|| "Input"), &input_bytes[..])?;
|
||||
// 40 byte
|
||||
let element_size = <MNT4FqParameters as FpParameters>::BigInt::NUM_LIMBS * 8;
|
||||
input_gadgets = input_bytes
|
||||
.chunks(element_size)
|
||||
.map(|chunk| {
|
||||
chunk
|
||||
.iter()
|
||||
.flat_map(|byte| byte.into_bits_le())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
let vk_gadget = TestVkGadget1::alloc(cs.ns(|| "Vk"), || Ok(¶ms.vk))?;
|
||||
let proof_gadget =
|
||||
TestProofGadget1::alloc(cs.ns(|| "Proof"), || Ok(proof.clone())).unwrap();
|
||||
<TestVerifierGadget1 as NIZKVerifierGadget<TestProofSystem1, MNT6Fq>>::check_verify(
|
||||
cs.ns(|| "Verify"),
|
||||
&vk_gadget,
|
||||
input_gadgets.iter(),
|
||||
&proof_gadget,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gm17_recursive_verifier_test() {
|
||||
let num_inputs = 100;
|
||||
let num_constraints = num_inputs;
|
||||
let rng = &mut test_rng();
|
||||
let mut inputs: Vec<Option<MNT4Fq>> = Vec::with_capacity(num_inputs);
|
||||
for _ in 0..num_inputs {
|
||||
inputs.push(Some(rng.gen()));
|
||||
}
|
||||
|
||||
// Generate inner params and proof.
|
||||
let inner_params = {
|
||||
let c = Bench::<MNT4Fq> {
|
||||
inputs: vec![None; num_inputs],
|
||||
num_constraints,
|
||||
};
|
||||
|
||||
generate_random_parameters(c, rng).unwrap()
|
||||
};
|
||||
|
||||
let inner_proof = {
|
||||
// Create an instance of our circuit (with the
|
||||
// witness)
|
||||
let c = Bench {
|
||||
inputs: inputs.clone(),
|
||||
num_constraints,
|
||||
};
|
||||
// Create a groth16 proof with our parameters.
|
||||
create_random_proof(c, &inner_params, rng).unwrap()
|
||||
};
|
||||
|
||||
// Generate outer params and proof.
|
||||
let params = {
|
||||
let c = Wrapper {
|
||||
inputs: inputs.clone(),
|
||||
params: inner_params.clone(),
|
||||
proof: inner_proof.clone(),
|
||||
};
|
||||
|
||||
generate_random_parameters(c, rng).unwrap()
|
||||
};
|
||||
|
||||
{
|
||||
let proof = {
|
||||
// Create an instance of our circuit (with the
|
||||
// witness)
|
||||
let c = Wrapper {
|
||||
inputs: inputs.clone(),
|
||||
params: inner_params.clone(),
|
||||
proof: inner_proof.clone(),
|
||||
};
|
||||
// Create a groth16 proof with our parameters.
|
||||
create_random_proof(c, ¶ms, rng).unwrap()
|
||||
};
|
||||
|
||||
let mut cs = TestConstraintSystem::<MNT4Fq>::new();
|
||||
|
||||
let inputs: Vec<_> = inputs.into_iter().map(|input| input.unwrap()).collect();
|
||||
let mut input_gadgets = Vec::new();
|
||||
|
||||
{
|
||||
let bigint_size = <MNT4FqParameters as FpParameters>::BigInt::NUM_LIMBS * 64;
|
||||
let mut input_bits = Vec::new();
|
||||
let mut cs = cs.ns(|| "Allocate Input");
|
||||
for (i, input) in inputs.into_iter().enumerate() {
|
||||
let input_gadget =
|
||||
FpGadget::alloc_input(cs.ns(|| format!("Input {}", i)), || Ok(input))
|
||||
.unwrap();
|
||||
let mut fp_bits = input_gadget
|
||||
.to_bits(cs.ns(|| format!("To bits {}", i)))
|
||||
.unwrap();
|
||||
|
||||
// FpGadget::to_bits outputs a big-endian binary representation of
|
||||
// fe_gadget's value, so we have to reverse it to get the little-endian
|
||||
// form.
|
||||
fp_bits.reverse();
|
||||
|
||||
// Use 320 bits per element.
|
||||
for _ in fp_bits.len()..bigint_size {
|
||||
fp_bits.push(Boolean::constant(false));
|
||||
}
|
||||
input_bits.extend_from_slice(&fp_bits);
|
||||
}
|
||||
|
||||
// Pack input bits into field elements of the underlying circuit.
|
||||
let max_size = 8 * (<MNT6FqParameters as FpParameters>::CAPACITY / 8) as usize;
|
||||
let max_size = max_size as usize;
|
||||
let bigint_size = <MNT6FqParameters as FpParameters>::BigInt::NUM_LIMBS * 64;
|
||||
for chunk in input_bits.chunks(max_size) {
|
||||
let mut chunk = chunk.to_vec();
|
||||
let len = chunk.len();
|
||||
for _ in len..bigint_size {
|
||||
chunk.push(Boolean::constant(false));
|
||||
}
|
||||
input_gadgets.push(chunk);
|
||||
}
|
||||
// assert!(!verify_proof(&pvk, &proof, &[a]).unwrap());
|
||||
}
|
||||
|
||||
let vk_gadget = TestVkGadget2::alloc_input(cs.ns(|| "Vk"), || Ok(¶ms.vk)).unwrap();
|
||||
let proof_gadget =
|
||||
TestProofGadget2::alloc(cs.ns(|| "Proof"), || Ok(proof.clone())).unwrap();
|
||||
println!("Time to verify!\n\n\n\n");
|
||||
<TestVerifierGadget2 as NIZKVerifierGadget<TestProofSystem2, MNT4Fq>>::check_verify(
|
||||
cs.ns(|| "Verify"),
|
||||
&vk_gadget,
|
||||
input_gadgets.iter(),
|
||||
&proof_gadget,
|
||||
)
|
||||
.unwrap();
|
||||
if !cs.is_satisfied() {
|
||||
println!("=========================================================");
|
||||
println!("Unsatisfied constraints:");
|
||||
println!("{:?}", cs.which_is_unsatisfied().unwrap());
|
||||
println!("=========================================================");
|
||||
}
|
||||
|
||||
// cs.print_named_objects();
|
||||
assert!(cs.is_satisfied());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,3 +479,266 @@ mod test {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_recursive {
|
||||
use groth16::*;
|
||||
use r1cs_core::{ConstraintSynthesizer, ConstraintSystem, SynthesisError};
|
||||
|
||||
use super::*;
|
||||
use algebra::{
|
||||
fields::FpParameters,
|
||||
mnt4_298::{Fq as MNT4Fq, FqParameters as MNT4FqParameters, Fr as MNT4Fr, MNT4_298},
|
||||
mnt6_298::{Fq as MNT6Fq, FqParameters as MNT6FqParameters, Fr as MNT6Fr, MNT6_298},
|
||||
test_rng, BigInteger, PrimeField,
|
||||
};
|
||||
use r1cs_std::{
|
||||
fields::fp::FpGadget, mnt4_298::PairingGadget as MNT4_298PairingGadget,
|
||||
mnt6_298::PairingGadget as MNT6_298PairingGadget,
|
||||
test_constraint_system::TestConstraintSystem, uint8::UInt8,
|
||||
};
|
||||
use rand::Rng;
|
||||
|
||||
type TestProofSystem1 = Groth16<MNT6_298, Bench<MNT4Fq>, MNT6Fr>;
|
||||
type TestVerifierGadget1 = Groth16VerifierGadget<MNT6_298, MNT6Fq, MNT6_298PairingGadget>;
|
||||
type TestProofGadget1 = ProofGadget<MNT6_298, MNT6Fq, MNT6_298PairingGadget>;
|
||||
type TestVkGadget1 = VerifyingKeyGadget<MNT6_298, MNT6Fq, MNT6_298PairingGadget>;
|
||||
|
||||
type TestProofSystem2 = Groth16<MNT4_298, Wrapper, MNT4Fr>;
|
||||
type TestVerifierGadget2 = Groth16VerifierGadget<MNT4_298, MNT4Fq, MNT4_298PairingGadget>;
|
||||
type TestProofGadget2 = ProofGadget<MNT4_298, MNT4Fq, MNT4_298PairingGadget>;
|
||||
type TestVkGadget2 = VerifyingKeyGadget<MNT4_298, MNT4Fq, MNT4_298PairingGadget>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Bench<F: Field> {
|
||||
inputs: Vec<Option<F>>,
|
||||
num_constraints: usize,
|
||||
}
|
||||
|
||||
impl<F: Field> ConstraintSynthesizer<F> for Bench<F> {
|
||||
fn generate_constraints<CS: ConstraintSystem<F>>(
|
||||
self,
|
||||
cs: &mut CS,
|
||||
) -> Result<(), SynthesisError> {
|
||||
assert!(self.inputs.len() >= 2);
|
||||
assert!(self.num_constraints >= self.inputs.len());
|
||||
|
||||
let mut variables: Vec<_> = Vec::with_capacity(self.inputs.len());
|
||||
for (i, input) in self.inputs.into_iter().enumerate() {
|
||||
let input_var = cs.alloc_input(
|
||||
|| format!("Input {}", i),
|
||||
|| input.ok_or(SynthesisError::AssignmentMissing),
|
||||
)?;
|
||||
variables.push((input, input_var));
|
||||
}
|
||||
|
||||
for i in 0..self.num_constraints {
|
||||
let new_entry = {
|
||||
let (input_1_val, input_1_var) = variables[i];
|
||||
let (input_2_val, input_2_var) = variables[i + 1];
|
||||
let result_val = input_1_val
|
||||
.and_then(|input_1| input_2_val.map(|input_2| input_1 * &input_2));
|
||||
let result_var = cs.alloc(
|
||||
|| format!("Result {}", i),
|
||||
|| result_val.ok_or(SynthesisError::AssignmentMissing),
|
||||
)?;
|
||||
cs.enforce(
|
||||
|| format!("Enforce constraint {}", i),
|
||||
|lc| lc + input_1_var,
|
||||
|lc| lc + input_2_var,
|
||||
|lc| lc + result_var,
|
||||
);
|
||||
(result_val, result_var)
|
||||
};
|
||||
variables.push(new_entry);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Wrapper {
|
||||
inputs: Vec<Option<MNT4Fq>>,
|
||||
params: Parameters<MNT6_298>,
|
||||
proof: Proof<MNT6_298>,
|
||||
}
|
||||
|
||||
impl ConstraintSynthesizer<MNT6Fq> for Wrapper {
|
||||
fn generate_constraints<CS: ConstraintSystem<MNT6Fq>>(
|
||||
self,
|
||||
cs: &mut CS,
|
||||
) -> Result<(), SynthesisError> {
|
||||
let params = self.params;
|
||||
let proof = self.proof;
|
||||
let inputs: Vec<_> = self
|
||||
.inputs
|
||||
.into_iter()
|
||||
.map(|input| input.unwrap())
|
||||
.collect();
|
||||
let input_gadgets;
|
||||
|
||||
{
|
||||
let mut cs = cs.ns(|| "Allocate Input");
|
||||
// Chain all input values in one large byte array.
|
||||
let input_bytes = inputs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.flat_map(|input| {
|
||||
input
|
||||
.into_repr()
|
||||
.as_ref()
|
||||
.iter()
|
||||
.flat_map(|l| l.to_le_bytes().to_vec())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Allocate this byte array as input packed into field elements.
|
||||
let input_bytes = UInt8::alloc_input_vec(cs.ns(|| "Input"), &input_bytes[..])?;
|
||||
// 40 byte
|
||||
let element_size = <MNT4FqParameters as FpParameters>::BigInt::NUM_LIMBS * 8;
|
||||
input_gadgets = input_bytes
|
||||
.chunks(element_size)
|
||||
.map(|chunk| {
|
||||
chunk
|
||||
.iter()
|
||||
.flat_map(|byte| byte.into_bits_le())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
let vk_gadget = TestVkGadget1::alloc(cs.ns(|| "Vk"), || Ok(¶ms.vk))?;
|
||||
let proof_gadget =
|
||||
TestProofGadget1::alloc(cs.ns(|| "Proof"), || Ok(proof.clone())).unwrap();
|
||||
<TestVerifierGadget1 as NIZKVerifierGadget<TestProofSystem1, MNT6Fq>>::check_verify(
|
||||
cs.ns(|| "Verify"),
|
||||
&vk_gadget,
|
||||
input_gadgets.iter(),
|
||||
&proof_gadget,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groth16_recursive_verifier_test() {
|
||||
let num_inputs = 100;
|
||||
let num_constraints = num_inputs;
|
||||
let rng = &mut test_rng();
|
||||
let mut inputs: Vec<Option<MNT4Fq>> = Vec::with_capacity(num_inputs);
|
||||
for _ in 0..num_inputs {
|
||||
inputs.push(Some(rng.gen()));
|
||||
}
|
||||
|
||||
// Generate inner params and proof.
|
||||
let inner_params = {
|
||||
let c = Bench::<MNT4Fq> {
|
||||
inputs: vec![None; num_inputs],
|
||||
num_constraints,
|
||||
};
|
||||
|
||||
generate_random_parameters(c, rng).unwrap()
|
||||
};
|
||||
|
||||
let inner_proof = {
|
||||
// Create an instance of our circuit (with the
|
||||
// witness)
|
||||
let c = Bench {
|
||||
inputs: inputs.clone(),
|
||||
num_constraints,
|
||||
};
|
||||
// Create a groth16 proof with our parameters.
|
||||
create_random_proof(c, &inner_params, rng).unwrap()
|
||||
};
|
||||
|
||||
// Generate outer params and proof.
|
||||
let params = {
|
||||
let c = Wrapper {
|
||||
inputs: inputs.clone(),
|
||||
params: inner_params.clone(),
|
||||
proof: inner_proof.clone(),
|
||||
};
|
||||
|
||||
generate_random_parameters(c, rng).unwrap()
|
||||
};
|
||||
|
||||
{
|
||||
let proof = {
|
||||
// Create an instance of our circuit (with the
|
||||
// witness)
|
||||
let c = Wrapper {
|
||||
inputs: inputs.clone(),
|
||||
params: inner_params.clone(),
|
||||
proof: inner_proof.clone(),
|
||||
};
|
||||
// Create a groth16 proof with our parameters.
|
||||
create_random_proof(c, ¶ms, rng).unwrap()
|
||||
};
|
||||
|
||||
let mut cs = TestConstraintSystem::<MNT4Fq>::new();
|
||||
|
||||
let inputs: Vec<_> = inputs.into_iter().map(|input| input.unwrap()).collect();
|
||||
let mut input_gadgets = Vec::new();
|
||||
|
||||
{
|
||||
let bigint_size = <MNT4FqParameters as FpParameters>::BigInt::NUM_LIMBS * 64;
|
||||
let mut input_bits = Vec::new();
|
||||
let mut cs = cs.ns(|| "Allocate Input");
|
||||
for (i, input) in inputs.into_iter().enumerate() {
|
||||
let input_gadget =
|
||||
FpGadget::alloc_input(cs.ns(|| format!("Input {}", i)), || Ok(input))
|
||||
.unwrap();
|
||||
let mut fp_bits = input_gadget
|
||||
.to_bits(cs.ns(|| format!("To bits {}", i)))
|
||||
.unwrap();
|
||||
|
||||
// FpGadget::to_bits outputs a big-endian binary representation of
|
||||
// fe_gadget's value, so we have to reverse it to get the little-endian
|
||||
// form.
|
||||
fp_bits.reverse();
|
||||
|
||||
// Use 320 bits per element.
|
||||
for _ in fp_bits.len()..bigint_size {
|
||||
fp_bits.push(Boolean::constant(false));
|
||||
}
|
||||
input_bits.extend_from_slice(&fp_bits);
|
||||
}
|
||||
|
||||
// Pack input bits into field elements of the underlying circuit.
|
||||
let max_size = 8 * (<MNT6FqParameters as FpParameters>::CAPACITY / 8) as usize;
|
||||
let max_size = max_size as usize;
|
||||
let bigint_size = <MNT6FqParameters as FpParameters>::BigInt::NUM_LIMBS * 64;
|
||||
for chunk in input_bits.chunks(max_size) {
|
||||
let mut chunk = chunk.to_vec();
|
||||
let len = chunk.len();
|
||||
for _ in len..bigint_size {
|
||||
chunk.push(Boolean::constant(false));
|
||||
}
|
||||
input_gadgets.push(chunk);
|
||||
}
|
||||
// assert!(!verify_proof(&pvk, &proof, &[a]).unwrap());
|
||||
}
|
||||
|
||||
let vk_gadget = TestVkGadget2::alloc_input(cs.ns(|| "Vk"), || Ok(¶ms.vk)).unwrap();
|
||||
let proof_gadget =
|
||||
TestProofGadget2::alloc(cs.ns(|| "Proof"), || Ok(proof.clone())).unwrap();
|
||||
println!("Time to verify!\n\n\n\n");
|
||||
<TestVerifierGadget2 as NIZKVerifierGadget<TestProofSystem2, MNT4Fq>>::check_verify(
|
||||
cs.ns(|| "Verify"),
|
||||
&vk_gadget,
|
||||
input_gadgets.iter(),
|
||||
&proof_gadget,
|
||||
)
|
||||
.unwrap();
|
||||
if !cs.is_satisfied() {
|
||||
println!("=========================================================");
|
||||
println!("Unsatisfied constraints:");
|
||||
println!("{:?}", cs.which_is_unsatisfied().unwrap());
|
||||
println!("=========================================================");
|
||||
}
|
||||
|
||||
// cs.print_named_objects();
|
||||
assert!(cs.is_satisfied());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user