Browse Source

Add Grumpkin cycle implementation (#181)

* bn256+grumpkin from halo2curves

* chore: Integrate halo2curves more extensively

- Extend existing tests with additional test cases using the new curve types

* fix: Assign correct orders to bn256 and grumpkin scalar fields

- Swap scalar orders between grumpkin and bn256 in `impl_traits!` implementation

* test: Finish improving test integration with halo2curves

- Enhances test coverage for `pasta_curves` and `halo2curves`
- Cleans up commented code in `test_ivc_nontrivial` and `test_ivc_nontrivial_with_compression` tests
- Updates relevant test cases in `src/lib.rs` to include new curve tests

* chore: Remove commented-out/uneeded code in bn254_grumpkin.rs

* test: reproduce test_from_label for bn254_grumpkin

- Implement the `from_label_serial` function in bn254_grumpkin provider
- Add a test to compare parallel and serial implementations of `from_label` function

* refactor: Clean up to_coordinate & summarize changes

* refactor: rename bn254_grumpkin -> bn256_grumpkin

* test: Expand testing for public params digest using bn256 and grumpkin

* chore: Update halo2curves dependency in Cargo.toml

- Updated the `halo2curves` dependency in `Cargo.toml` to the latest version `0.1.0` from a specific git branch.

* refactor: Refactor multi-exponentiation methods across providers

- Updated bn256_grumpkin.rs to use the cpu_best_multiexp function from pasta provider instead of its native function.
- Modified visibility of cpu_best_multiexp function in pasta.rs from private to crate level.

* chore: set up dependencies to import the correct getrandom feature on Wasm

---------

Co-authored-by: Leo Alt <leo@ethereum.org>
main
François Garillot 1 year ago
committed by GitHub
parent
commit
4b077bcab1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 340 additions and 19 deletions
  1. +5
    -0
      Cargo.toml
  2. +2
    -2
      src/bellperson/mod.rs
  3. +17
    -1
      src/circuit.rs
  4. +13
    -1
      src/gadgets/ecc.rs
  5. +25
    -0
      src/lib.rs
  6. +3
    -0
      src/nifs.rs
  7. +255
    -0
      src/provider/bn256_grumpkin.rs
  8. +13
    -11
      src/provider/keccak.rs
  9. +1
    -0
      src/provider/mod.rs
  10. +1
    -1
      src/provider/pasta.rs
  11. +3
    -3
      src/provider/poseidon.rs
  12. +2
    -0
      src/spartan/pp.rs

+ 5
- 0
Cargo.toml

@ -32,10 +32,15 @@ flate2 = "1.0"
bitvec = "1.0"
byteorder = "1.4.3"
thiserror = "1.0"
halo2curves = { version="0.1.0", features = [ "derive_serde" ] }
[target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies]
pasta-msm = { version = "0.1.4" }
[target.wasm32-unknown-unknown.dependencies]
# see https://github.com/rust-random/rand/pull/948
getrandom = { version = "0.2.0", default-features = false, features = ["js"]}
[dev-dependencies]
criterion = { version = "0.4", features = ["html_reports"] }
rand = "0.8.4"

+ 2
- 2
src/bellperson/mod.rs

@ -62,7 +62,7 @@ mod tests {
#[test]
fn test_alloc_bit() {
type G = pasta_curves::pallas::Point;
test_alloc_bit_with::<G>();
test_alloc_bit_with::<pasta_curves::pallas::Point>();
test_alloc_bit_with::<crate::provider::bn256_grumpkin::bn256::Point>();
}
}

+ 17
- 1
src/circuit.rs

@ -376,6 +376,7 @@ mod tests {
type PastaG2 = pasta_curves::vesta::Point;
use crate::constants::{BN_LIMB_WIDTH, BN_N_LIMBS};
use crate::provider;
use crate::{
bellperson::r1cs::{NovaShape, NovaWitness},
gadgets::utils::scalar_as_base,
@ -471,7 +472,7 @@ mod tests {
}
#[test]
fn test_recursive_circuit() {
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::new();
@ -481,4 +482,19 @@ mod tests {
params1, params2, 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::new();
let ro_consts2: ROConstantsCircuit<provider::bn256_grumpkin::bn256::Point> =
PoseidonConstantsCircuit::new();
test_recursive_circuit_with::<
provider::bn256_grumpkin::bn256::Point,
provider::bn256_grumpkin::grumpkin::Point,
>(params1, params2, ro_consts1, ro_consts2, 9983, 10536);
}
}

+ 13
- 1
src/gadgets/ecc.rs

@ -754,6 +754,7 @@ mod tests {
r1cs::{NovaShape, NovaWitness},
{shape_cs::ShapeCS, solver::SatisfyingAssignment},
};
use crate::provider::bn256_grumpkin::{bn256, grumpkin};
use ff::{Field, PrimeFieldBits};
use pasta_curves::{arithmetic::CurveAffine, group::Curve, pallas, vesta};
use rand::rngs::OsRng;
@ -768,7 +769,6 @@ mod tests {
is_infinity: bool,
}
#[cfg(test)]
impl<G> Point<G>
where
G: Group,
@ -896,6 +896,9 @@ mod tests {
fn test_ecc_ops() {
test_ecc_ops_with::<pallas::Affine, pallas::Point>();
test_ecc_ops_with::<vesta::Affine, vesta::Point>();
test_ecc_ops_with::<bn256::Affine, bn256::Point>();
test_ecc_ops_with::<grumpkin::Affine, grumpkin::Point>();
}
fn test_ecc_ops_with<C, G>()
@ -977,6 +980,9 @@ mod tests {
fn test_ecc_circuit_ops() {
test_ecc_circuit_ops_with::<pallas::Point, vesta::Point>();
test_ecc_circuit_ops_with::<vesta::Point, pallas::Point>();
test_ecc_circuit_ops_with::<bn256::Point, grumpkin::Point>();
test_ecc_circuit_ops_with::<grumpkin::Point, bn256::Point>();
}
fn test_ecc_circuit_ops_with<G1, G2>()
@ -1027,6 +1033,9 @@ mod tests {
fn test_ecc_circuit_add_equal() {
test_ecc_circuit_add_equal_with::<pallas::Point, vesta::Point>();
test_ecc_circuit_add_equal_with::<vesta::Point, pallas::Point>();
test_ecc_circuit_add_equal_with::<bn256::Point, grumpkin::Point>();
test_ecc_circuit_add_equal_with::<grumpkin::Point, bn256::Point>();
}
fn test_ecc_circuit_add_equal_with<G1, G2>()
@ -1081,6 +1090,9 @@ mod tests {
fn test_ecc_circuit_add_negation() {
test_ecc_circuit_add_negation_with::<pallas::Point, vesta::Point>();
test_ecc_circuit_add_negation_with::<vesta::Point, pallas::Point>();
test_ecc_circuit_add_negation_with::<bn256::Point, grumpkin::Point>();
test_ecc_circuit_add_negation_with::<grumpkin::Point, bn256::Point>();
}
fn test_ecc_circuit_add_negation_with<G1, G2>()

+ 25
- 0
src/lib.rs

@ -793,6 +793,7 @@ fn compute_digest(o: &T) -> G::Scalar {
#[cfg(test)]
mod tests {
use crate::provider::bn256_grumpkin::{bn256, grumpkin};
use crate::provider::pedersen::CommitmentKeyExtTrait;
use super::*;
@ -895,6 +896,23 @@ mod tests {
trivial_circuit2,
"3f7b25f589f2da5ab26254beba98faa54f6442ebf5fa5860caf7b08b576cab00",
);
let trivial_circuit1_grumpkin =
TrivialTestCircuit::<<bn256::Point as Group>::Scalar>::default();
let trivial_circuit2_grumpkin =
TrivialTestCircuit::<<grumpkin::Point as Group>::Scalar>::default();
let cubic_circuit1_grumpkin = CubicCircuit::<<bn256::Point as Group>::Scalar>::default();
test_pp_digest_with::<bn256::Point, grumpkin::Point, _, _>(
trivial_circuit1_grumpkin,
trivial_circuit2_grumpkin.clone(),
"967acca1d6b4731cd65d4072c12bbaca9648f24d7bcc2877aee720e4265d4302",
);
test_pp_digest_with::<bn256::Point, grumpkin::Point, _, _>(
cubic_circuit1_grumpkin,
trivial_circuit2_grumpkin,
"44629f26a78bf6c4e3077f940232050d1793d304fdba5e221d0cf66f76a37903",
);
}
fn test_ivc_trivial_with<G1, G2>()
@ -949,6 +967,8 @@ mod tests {
type G1 = pasta_curves::pallas::Point;
type G2 = pasta_curves::vesta::Point;
test_ivc_trivial_with::<G1, G2>();
test_ivc_trivial_with::<bn256::Point, grumpkin::Point>();
}
fn test_ivc_nontrivial_with<G1, G2>()
@ -1030,6 +1050,7 @@ mod tests {
type G2 = pasta_curves::vesta::Point;
test_ivc_nontrivial_with::<G1, G2>();
test_ivc_nontrivial_with::<bn256::Point, grumpkin::Point>();
}
fn test_ivc_nontrivial_with_compression_with<G1, G2>()
@ -1124,6 +1145,7 @@ mod tests {
type G2 = pasta_curves::vesta::Point;
test_ivc_nontrivial_with_compression_with::<G1, G2>();
test_ivc_nontrivial_with_compression_with::<bn256::Point, grumpkin::Point>();
}
fn test_ivc_nontrivial_with_spark_compression_with<G1, G2>()
@ -1221,6 +1243,7 @@ mod tests {
type G2 = pasta_curves::vesta::Point;
test_ivc_nontrivial_with_spark_compression_with::<G1, G2>();
test_ivc_nontrivial_with_spark_compression_with::<bn256::Point, grumpkin::Point>();
}
fn test_ivc_nondet_with_compression_with<G1, G2>()
@ -1377,6 +1400,7 @@ mod tests {
type G2 = pasta_curves::vesta::Point;
test_ivc_nondet_with_compression_with::<G1, G2>();
test_ivc_nondet_with_compression_with::<bn256::Point, grumpkin::Point>();
}
fn test_ivc_base_with<G1, G2>()
@ -1443,5 +1467,6 @@ mod tests {
type G2 = pasta_curves::vesta::Point;
test_ivc_base_with::<G1, G2>();
test_ivc_base_with::<bn256::Point, grumpkin::Point>();
}
}

+ 3
- 0
src/nifs.rs

@ -210,6 +210,8 @@ mod tests {
#[test]
fn test_tiny_r1cs_bellperson() {
test_tiny_r1cs_bellperson_with::<G>();
test_tiny_r1cs_bellperson_with::<crate::provider::bn256_grumpkin::bn256::Point>();
}
#[allow(clippy::too_many_arguments)]
@ -384,5 +386,6 @@ mod tests {
#[test]
fn test_tiny_r1cs() {
test_tiny_r1cs_with::<pasta_curves::pallas::Point>();
test_tiny_r1cs_with::<crate::provider::bn256_grumpkin::bn256::Point>();
}
}

+ 255
- 0
src/provider/bn256_grumpkin.rs

@ -0,0 +1,255 @@
//! This module implements the Nova traits for bn256::Point, bn256::Scalar, grumpkin::Point, grumpkin::Scalar.
use crate::{
provider::{
keccak::Keccak256Transcript,
pedersen::CommitmentEngine,
poseidon::{PoseidonRO, PoseidonROCircuit},
},
traits::{CompressedGroup, Group, PrimeFieldExt, TranscriptReprTrait},
};
use digest::{ExtendableOutput, Input};
use ff::{FromUniformBytes, PrimeField};
use num_bigint::BigInt;
use num_traits::Num;
use pasta_curves::{
self,
arithmetic::{CurveAffine, CurveExt},
group::{cofactor::CofactorCurveAffine, Curve, Group as AnotherGroup, GroupEncoding},
};
use rayon::prelude::*;
use sha3::Shake256;
use std::io::Read;
use halo2curves::bn256::{
G1Affine as Bn256Affine, G1Compressed as Bn256Compressed, G1 as Bn256Point,
};
use halo2curves::grumpkin::{
G1Affine as GrumpkinAffine, G1Compressed as GrumpkinCompressed, G1 as GrumpkinPoint,
};
/// Re-exports that give access to the standard aliases used in the code base, for bn256
pub mod bn256 {
pub use halo2curves::bn256::{
Fq as Base, Fr as Scalar, G1Affine as Affine, G1Compressed as Compressed, G1 as Point,
};
}
/// Re-exports that give access to the standard aliases used in the code base, for grumpkin
pub mod grumpkin {
pub use halo2curves::grumpkin::{
Fq as Base, Fr as Scalar, G1Affine as Affine, G1Compressed as Compressed, G1 as Point,
};
}
// This implementation behaves in ways specific to the bn256/grumpkin curves in:
// - to_coordinates,
// - vartime_multiscalar_mul, where it does not call into accelerated implementations.
macro_rules! impl_traits {
(
$name:ident,
$name_compressed:ident,
$name_curve:ident,
$name_curve_affine:ident,
$order_str:literal
) => {
impl Group for $name::Point {
type Base = $name::Base;
type Scalar = $name::Scalar;
type CompressedGroupElement = $name_compressed;
type PreprocessedGroupElement = $name::Affine;
type RO = PoseidonRO<Self::Base, Self::Scalar>;
type ROCircuit = PoseidonROCircuit<Self::Base>;
type TE = Keccak256Transcript<Self>;
type CE = CommitmentEngine<Self>;
fn vartime_multiscalar_mul(
scalars: &[Self::Scalar],
bases: &[Self::PreprocessedGroupElement],
) -> Self {
cpu_best_multiexp(scalars, bases)
}
fn preprocessed(&self) -> Self::PreprocessedGroupElement {
self.to_affine()
}
fn compress(&self) -> Self::CompressedGroupElement {
self.to_bytes()
}
fn from_label(label: &'static [u8], n: usize) -> Vec<Self::PreprocessedGroupElement> {
let mut shake = Shake256::default();
shake.input(label);
let mut reader = shake.xof_result();
let mut uniform_bytes_vec = Vec::new();
for _ in 0..n {
let mut uniform_bytes = [0u8; 32];
reader.read_exact(&mut uniform_bytes).unwrap();
uniform_bytes_vec.push(uniform_bytes);
}
let gens_proj: Vec<$name_curve> = (0..n)
.collect::<Vec<usize>>()
.into_par_iter()
.map(|i| {
let hash = $name_curve::hash_to_curve("from_uniform_bytes");
hash(&uniform_bytes_vec[i])
})
.collect();
let num_threads = rayon::current_num_threads();
if gens_proj.len() > num_threads {
let chunk = (gens_proj.len() as f64 / num_threads as f64).ceil() as usize;
(0..num_threads)
.collect::<Vec<usize>>()
.into_par_iter()
.map(|i| {
let start = i * chunk;
let end = if i == num_threads - 1 {
gens_proj.len()
} else {
core::cmp::min((i + 1) * chunk, gens_proj.len())
};
if end > start {
let mut gens = vec![$name_curve_affine::identity(); end - start];
<Self as Curve>::batch_normalize(&gens_proj[start..end], &mut gens);
gens
} else {
vec![]
}
})
.collect::<Vec<Vec<$name_curve_affine>>>()
.into_par_iter()
.flatten()
.collect()
} else {
let mut gens = vec![$name_curve_affine::identity(); n];
<Self as Curve>::batch_normalize(&gens_proj, &mut gens);
gens
}
}
fn to_coordinates(&self) -> (Self::Base, Self::Base, bool) {
let coordinates = self.to_affine().coordinates();
if coordinates.is_some().unwrap_u8() == 1
// The bn256/grumpkin convention is to define and return the identity point's affine encoding (not None)
&& (Self::PreprocessedGroupElement::identity() != self.to_affine())
{
(*coordinates.unwrap().x(), *coordinates.unwrap().y(), false)
} else {
(Self::Base::zero(), Self::Base::zero(), true)
}
}
fn get_curve_params() -> (Self::Base, Self::Base, BigInt) {
let A = $name::Point::a();
let B = $name::Point::b();
let order = BigInt::from_str_radix($order_str, 16).unwrap();
(A, B, order)
}
fn zero() -> Self {
$name::Point::identity()
}
fn get_generator() -> Self {
$name::Point::generator()
}
}
impl PrimeFieldExt for $name::Scalar {
fn from_uniform(bytes: &[u8]) -> Self {
let bytes_arr: [u8; 64] = bytes.try_into().unwrap();
$name::Scalar::from_uniform_bytes(&bytes_arr)
}
}
impl<G: Group> TranscriptReprTrait<G> for $name_compressed {
fn to_transcript_bytes(&self) -> Vec<u8> {
self.as_ref().to_vec()
}
}
impl CompressedGroup for $name_compressed {
type GroupElement = $name::Point;
fn decompress(&self) -> Option<$name::Point> {
Some($name_curve::from_bytes(&self).unwrap())
}
}
};
}
impl<G: Group> TranscriptReprTrait<G> for grumpkin::Base {
fn to_transcript_bytes(&self) -> Vec<u8> {
self.to_repr().to_vec()
}
}
impl<G: Group> TranscriptReprTrait<G> for grumpkin::Scalar {
fn to_transcript_bytes(&self) -> Vec<u8> {
self.to_repr().to_vec()
}
}
impl_traits!(
bn256,
Bn256Compressed,
Bn256Point,
Bn256Affine,
"30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001"
);
impl_traits!(
grumpkin,
GrumpkinCompressed,
GrumpkinPoint,
GrumpkinAffine,
"30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47"
);
/// Performs a multi-exponentiation operation without GPU acceleration.
///
/// This function will panic if coeffs and bases have a different length.
///
/// This will use multithreading if beneficial.
/// Adapted from zcash/halo2
// TODO: update once https://github.com/privacy-scaling-explorations/halo2curves/pull/29
// (or a successor thereof) is merged
fn cpu_best_multiexp<C: CurveAffine>(coeffs: &[C::Scalar], bases: &[C]) -> C::Curve {
crate::provider::pasta::cpu_best_multiexp(coeffs, bases)
}
#[cfg(test)]
mod tests {
use super::*;
type G = bn256::Point;
fn from_label_serial(label: &'static [u8], n: usize) -> Vec<Bn256Affine> {
let mut shake = Shake256::default();
shake.input(label);
let mut reader = shake.xof_result();
let mut ck = Vec::new();
for _ in 0..n {
let mut uniform_bytes = [0u8; 32];
reader.read_exact(&mut uniform_bytes).unwrap();
let hash = bn256::Point::hash_to_curve("from_uniform_bytes");
ck.push(hash(&uniform_bytes).to_affine());
}
ck
}
#[test]
fn test_from_label() {
let label = b"test_from_label";
for n in [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 1021,
] {
let ck_par = <G as Group>::from_label(label, n);
let ck_ser = from_label_serial(label, n);
assert_eq!(ck_par.len(), n);
assert_eq!(ck_ser.len(), n);
assert_eq!(ck_par, ck_ser);
}
}
}

+ 13
- 11
src/provider/keccak.rs

@ -95,13 +95,14 @@ impl TranscriptEngineTrait for Keccak256Transcript {
#[cfg(test)]
mod tests {
use crate::{
provider::bn256_grumpkin::bn256,
provider::keccak::Keccak256Transcript,
traits::{Group, TranscriptEngineTrait},
};
use ff::PrimeField;
use sha3::{Digest, Keccak256};
fn test_keccak_transcript_with<G: Group>() {
fn test_keccak_transcript_with<G: Group>(expected_h1: &'static str, expected_h2: &'static str) {
let mut transcript: Keccak256Transcript<G> = Keccak256Transcript::new(b"test");
// two scalars
@ -114,10 +115,7 @@ mod tests {
// make a challenge
let c1: <G as Group>::Scalar = transcript.squeeze(b"c1").unwrap();
assert_eq!(
hex::encode(c1.to_repr().as_ref()),
"432d5811c8be3d44d47f52108a8749ae18482efd1a37b830f966456b5d75340c"
);
assert_eq!(hex::encode(c1.to_repr().as_ref()), expected_h1);
// a scalar
let s3 = <G as Group>::Scalar::from(128u64);
@ -127,16 +125,20 @@ mod tests {
// make a challenge
let c2: <G as Group>::Scalar = transcript.squeeze(b"c2").unwrap();
assert_eq!(
hex::encode(c2.to_repr().as_ref()),
"65f7908d53abcd18f3b1d767456ef9009b91c7566a635e9ca7be26e21d4d7a10"
);
assert_eq!(hex::encode(c2.to_repr().as_ref()), expected_h2);
}
#[test]
fn test_keccak_transcript() {
type G = pasta_curves::pallas::Point;
test_keccak_transcript_with::<G>()
test_keccak_transcript_with::<pasta_curves::pallas::Point>(
"432d5811c8be3d44d47f52108a8749ae18482efd1a37b830f966456b5d75340c",
"65f7908d53abcd18f3b1d767456ef9009b91c7566a635e9ca7be26e21d4d7a10",
);
test_keccak_transcript_with::<bn256::Point>(
"93f9160d5501865b399ee4ff0ffe17b697a4023e33e931e2597d36e6cc4ac602",
"bca8bdb96608a8277a7cb34bd493dfbc5baf2a080d1d6c9d32d7ab4f238eb803",
);
}
#[test]

+ 1
- 0
src/provider/mod.rs

@ -4,6 +4,7 @@
//! `RO` traits with Poseidon
//! `EvaluationEngine` with an IPA-based polynomial evaluation argument
pub mod bn256_grumpkin;
pub mod ipa_pc;
pub mod keccak;
pub mod pasta;

+ 1
- 1
src/provider/pasta.rs

@ -320,7 +320,7 @@ fn cpu_multiexp_serial(coeffs: &[C::Scalar], bases: &[C], acc: &
///
/// This will use multithreading if beneficial.
/// Adapted from zcash/halo2
fn cpu_best_multiexp<C: CurveAffine>(coeffs: &[C::Scalar], bases: &[C]) -> C::Curve {
pub(crate) fn cpu_best_multiexp<C: CurveAffine>(coeffs: &[C::Scalar], bases: &[C]) -> C::Curve {
assert_eq!(coeffs.len(), bases.len());
let num_threads = rayon::current_num_threads();

+ 3
- 3
src/provider/poseidon.rs

@ -201,6 +201,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::bn256_grumpkin::bn256;
use crate::{
bellperson::solver::SatisfyingAssignment, constants::NUM_CHALLENGE_BITS,
gadgets::utils::le_bits_to_num, traits::Group,
@ -242,8 +243,7 @@ mod tests {
#[test]
fn test_poseidon_ro() {
type G = pasta_curves::pallas::Point;
test_poseidon_ro_with::<G>()
test_poseidon_ro_with::<pasta_curves::pallas::Point>();
test_poseidon_ro_with::<bn256::Point>();
}
}

+ 2
- 0
src/spartan/pp.rs

@ -2185,6 +2185,7 @@ impl, C: StepCircuit
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::bn256_grumpkin::bn256;
use ::bellperson::{gadgets::num::AllocatedNum, ConstraintSystem, SynthesisError};
use core::marker::PhantomData;
use ff::PrimeField;
@ -2244,6 +2245,7 @@ mod tests {
type EE = crate::provider::ipa_pc::EvaluationEngine<G>;
test_spartan_snark_with::<G, EE>();
test_spartan_snark_with::<_, crate::provider::ipa_pc::EvaluationEngine<bn256::Point>>();
}
fn test_spartan_snark_with<G: Group, EE: EvaluationEngineTrait<G, CE = G::CE>>() {

Loading…
Cancel
Save