diff --git a/Cargo.lock b/Cargo.lock index bc9b96a5..3e7226d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,6 +413,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -453,6 +462,7 @@ dependencies = [ "serde", "serde_json", "tinytemplate", + "tokio", "walkdir", ] @@ -655,6 +665,95 @@ dependencies = [ "winapi", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "fxhash" version = "0.2.1" @@ -735,6 +834,52 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" +[[package]] +name = "icicle-bn254" +version = "4.0.0" +source = "git+https://github.com/ingonyama-zk/icicle.git#625532a624e5aaa6e9d31a1c92587f1fcc30dc76" +dependencies = [ + "cmake", + "icicle-core", + "icicle-hash", + "icicle-runtime", + "serde", +] + +[[package]] +name = "icicle-core" +version = "4.0.0" +source = "git+https://github.com/ingonyama-zk/icicle.git#625532a624e5aaa6e9d31a1c92587f1fcc30dc76" +dependencies = [ + "cmake", + "hex", + "icicle-runtime", + "once_cell", + "rand 0.8.5", + "rayon", + "serde", +] + +[[package]] +name = "icicle-hash" +version = "4.0.0" +source = "git+https://github.com/ingonyama-zk/icicle.git#625532a624e5aaa6e9d31a1c92587f1fcc30dc76" +dependencies = [ + "cmake", + "icicle-core", + "icicle-runtime", + "rand 0.8.5", +] + +[[package]] +name = "icicle-runtime" +version = "4.0.0" +source = "git+https://github.com/ingonyama-zk/icicle.git#625532a624e5aaa6e9d31a1c92587f1fcc30dc76" +dependencies = [ + "cmake", + "once_cell", +] + [[package]] name = "indexmap" version = "2.12.1" @@ -977,6 +1122,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "plotters" version = "0.3.7" @@ -1232,6 +1383,10 @@ dependencies = [ "cfg-if", "criterion", "document-features", + "futures", + "icicle-bn254", + "icicle-core", + "icicle-runtime", "num-bigint", "num-traits", "once_cell", @@ -1246,6 +1401,7 @@ dependencies = [ "tempfile", "thiserror", "tiny-keccak", + "tokio", "zeroize", "zerokit_utils", ] @@ -1425,6 +1581,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "sled" version = "0.34.7" @@ -1563,6 +1725,27 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "toml_datetime" version = "0.7.3" diff --git a/rln/Cargo.toml b/rln/Cargo.toml index 5bc828be..1497d582 100644 --- a/rln/Cargo.toml +++ b/rln/Cargo.toml @@ -59,8 +59,15 @@ serde = { version = "1.0.228", features = ["derive"] } # Documentation document-features = { version = "0.2.12", optional = true } +# GPU Acceleration (optional) +icicle-runtime = { git = "https://github.com/ingonyama-zk/icicle.git", optional = true } +icicle-core = { git = "https://github.com/ingonyama-zk/icicle.git", optional = true } +icicle-bn254 = { git = "https://github.com/ingonyama-zk/icicle.git", optional = true } + [dev-dependencies] -criterion = { version = "0.8.0", features = ["html_reports"] } +criterion = { version = "0.8.0", features = ["html_reports", "async_tokio"] } +tokio = { version = "1.44", features = ["rt-multi-thread", "macros", "sync"] } +futures = "0.3" [features] default = ["parallel", "pmtree-ft"] @@ -79,6 +86,7 @@ fullmerkletree = [] # Pre-allocated tree, fastest access optimalmerkletree = [] # Sparse storage, memory efficient pmtree-ft = ["zerokit_utils/pmtree-ft"] # Persistent storage, disk-based headers = ["safer-ffi/headers"] # Generate C header file with safer-ffi +icicle = ["icicle-runtime", "icicle-core", "icicle-bn254"] # GPU acceleration [[bench]] name = "pmtree_benchmark" @@ -89,6 +97,16 @@ required-features = ["pmtree-ft"] name = "poseidon_tree_benchmark" harness = false +[[bench]] +name = "proof_benchmark" +harness = false +required-features = ["pmtree-ft", "icicle"] + +[[bench]] +name = "async_proof_benchmark" +harness = false +required-features = ["pmtree-ft", "icicle"] + [package.metadata.docs.rs] all-features = true diff --git a/rln/Makefile.toml b/rln/Makefile.toml index 4af3d6d0..602058d5 100644 --- a/rln/Makefile.toml +++ b/rln/Makefile.toml @@ -21,3 +21,14 @@ args = [ [tasks.bench] command = "cargo" args = ["bench"] + +[tasks.bench_icicle] +command = "cargo" +args = [ + "bench", + "--bench", + "proof_benchmark", + "--no-default-features", + "--features", + "pmtree-ft,icicle", +] diff --git a/rln/benches/async_proof_benchmark.rs b/rln/benches/async_proof_benchmark.rs new file mode 100644 index 00000000..10f5d767 --- /dev/null +++ b/rln/benches/async_proof_benchmark.rs @@ -0,0 +1,151 @@ +use std::{ + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + thread::available_parallelism, + time::Duration, +}; + +use criterion::{criterion_group, criterion_main, Criterion, Throughput}; +use rln::{prelude::*, protocol::generate_zk_proof_icicle}; +use tokio::{runtime::Builder, task::JoinSet}; +use zerokit_utils::merkle_tree::{ZerokitMerkleProof, ZerokitMerkleTree}; + +type ConfigOf = ::Config; + +fn get_test_witness() -> RLNWitnessInput { + let leaf_index = 3; + let (identity_secret, id_commitment) = keygen().unwrap(); + let user_message_limit = Fr::from(100); + let rate_commitment = poseidon_hash(&[id_commitment, user_message_limit]).unwrap(); + + let default_leaf = Fr::from(0); + let mut tree = PoseidonTree::new( + DEFAULT_TREE_DEPTH, + default_leaf, + ConfigOf::::default(), + ) + .unwrap(); + tree.set(leaf_index, rate_commitment).unwrap(); + + let merkle_proof = tree.proof(leaf_index).unwrap(); + + let signal = b"hey hey"; + let x = hash_to_field_le(signal).unwrap(); + + let epoch = hash_to_field_le(b"test-epoch").unwrap(); + let rln_identifier = hash_to_field_le(b"test-rln-identifier").unwrap(); + let external_nullifier = poseidon_hash(&[epoch, rln_identifier]).unwrap(); + + let message_id = Fr::from(1); + + RLNWitnessInput::new( + identity_secret, + user_message_limit, + message_id, + merkle_proof.get_path_elements(), + merkle_proof.get_path_index(), + x, + external_nullifier, + ) + .unwrap() +} + +fn async_proof_generation_benchmark(c: &mut Criterion) { + let rt = Builder::new_multi_thread().enable_all().build().unwrap(); + + let witness = get_test_witness(); + let proving_key = zkey_from_folder(); + let graph_data = graph_from_folder(); + let proof_count = available_parallelism().map(|p| p.get() * 2).unwrap_or(8); + + let mut group = c.benchmark_group("async_proof_generation"); + group.sample_size(10); + group.measurement_time(Duration::from_secs(30)); + group.throughput(Throughput::Elements(proof_count as u64)); + + group.bench_function("standard", |b| { + b.to_async(&rt).iter(|| async { + let mut set = JoinSet::new(); + for _ in 0..proof_count { + let witness = witness.clone(); + set.spawn_blocking(move || { + let _ = generate_zk_proof(&proving_key, &witness, &graph_data).unwrap(); + }); + } + set.join_all().await + }); + }); + + group.finish(); +} + +fn async_proof_generation_icicle_benchmark(c: &mut Criterion) { + // let _ = init_icicle_backend(); + + let rt = Builder::new_multi_thread().enable_all().build().unwrap(); + + let witness = get_test_witness(); + let proving_key = zkey_from_folder(); + let graph_data = graph_from_folder(); + let proof_count = available_parallelism().map(|p| p.get() * 2).unwrap_or(8); + + let mut group = c.benchmark_group("async_proof_generation_icicle"); + group.sample_size(10); + group.measurement_time(Duration::from_secs(30)); + group.throughput(Throughput::Elements(proof_count as u64)); + + group.bench_function("icicle", |b| { + let witness = witness.clone(); + + b.to_async(&rt).iter(|| { + let witness = witness.clone(); + + async move { + let remaining = Arc::new(AtomicUsize::new(proof_count)); + let mut set = JoinSet::new(); + + { + let remaining = remaining.clone(); + let witness = witness.clone(); + set.spawn_blocking(move || loop { + let prev = remaining.fetch_sub(1, Ordering::Relaxed); + if prev == 0 || prev > proof_count { + break; + } + let _ = + generate_zk_proof_icicle(&proving_key, &witness, &graph_data).unwrap(); + }) + }; + + let mut cpu_handles = Vec::new(); + for _ in 0..proof_count { + let remaining = remaining.clone(); + let witness = witness.clone(); + let handle = set.spawn_blocking(move || loop { + let prev = remaining.fetch_sub(1, Ordering::Relaxed); + if prev == 0 || prev > proof_count { + break; + } + let _ = generate_zk_proof(&proving_key, &witness, &graph_data).unwrap(); + }); + cpu_handles.push(handle); + } + + set.join_all().await; + } + }); + }); + + group.finish(); +} + +criterion_group!( + benches, + async_proof_generation_benchmark, + async_proof_generation_icicle_benchmark +); + +criterion_main!(benches); + diff --git a/rln/benches/proof_benchmark.rs b/rln/benches/proof_benchmark.rs new file mode 100644 index 00000000..5df8d809 --- /dev/null +++ b/rln/benches/proof_benchmark.rs @@ -0,0 +1,91 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use rln::{prelude::*, protocol::generate_zk_proof_icicle}; +use zerokit_utils::merkle_tree::{ZerokitMerkleProof, ZerokitMerkleTree}; + +type ConfigOf = ::Config; + +fn get_test_witness() -> RLNWitnessInput { + let leaf_index = 3; + let (identity_secret, id_commitment) = keygen().unwrap(); + let user_message_limit = Fr::from(100); + let rate_commitment = poseidon_hash(&[id_commitment, user_message_limit]).unwrap(); + + let default_leaf = Fr::from(0); + let mut tree = PoseidonTree::new( + DEFAULT_TREE_DEPTH, + default_leaf, + ConfigOf::::default(), + ) + .unwrap(); + tree.set(leaf_index, rate_commitment).unwrap(); + + let merkle_proof = tree.proof(leaf_index).unwrap(); + + let signal = b"hey hey"; + let x = hash_to_field_le(signal).unwrap(); + + let epoch = hash_to_field_le(b"test-epoch").unwrap(); + let rln_identifier = hash_to_field_le(b"test-rln-identifier").unwrap(); + let external_nullifier = poseidon_hash(&[epoch, rln_identifier]).unwrap(); + + let message_id = Fr::from(1); + + RLNWitnessInput::new( + identity_secret, + user_message_limit, + message_id, + merkle_proof.get_path_elements(), + merkle_proof.get_path_index(), + x, + external_nullifier, + ) + .unwrap() +} + +fn proof_generation_benchmark(c: &mut Criterion) { + let witness = get_test_witness(); + let proving_key = zkey_from_folder(); + let graph_data = graph_from_folder(); + + let mut group = c.benchmark_group("proof_generation"); + + group.sample_size(10); + group.measurement_time(std::time::Duration::from_secs(10)); + + group.bench_function("standard", |b| { + b.iter(|| { + let _ = generate_zk_proof(&proving_key, &witness, &graph_data).unwrap(); + }) + }); + + group.finish(); +} + +fn proof_generation_icicle_benchmark(c: &mut Criterion) { + // let _ = init_icicle_backend(); + + let witness = get_test_witness(); + let proving_key = zkey_from_folder(); + let graph_data = graph_from_folder(); + + let mut group = c.benchmark_group("proof_generation_icicle"); + + group.sample_size(10); + group.measurement_time(std::time::Duration::from_secs(10)); + + group.bench_function("icicle", |b| { + b.iter(|| { + let _ = generate_zk_proof_icicle(&proving_key, &witness, &graph_data).unwrap(); + }) + }); + + group.finish(); +} + +criterion_group!( + benches, + proof_generation_benchmark, + proof_generation_icicle_benchmark +); + +criterion_main!(benches); diff --git a/rln/src/circuit/icicle.rs b/rln/src/circuit/icicle.rs new file mode 100644 index 00000000..67ad0117 --- /dev/null +++ b/rln/src/circuit/icicle.rs @@ -0,0 +1,39 @@ +// This crate is based on the code by Ingonyama. Its preimage can be found here: +// https://github.com/ingonyama-zk/icicle + +mod convert; +mod msm; +pub(crate) mod proof; + +use icicle_runtime::Device; + +use crate::error::ProtocolError; + +/// Initialize the ICICLE backend for GPU acceleration. +/// +/// This function attempts to load the Metal backend (for macOS) or CUDA backend (for Linux/Windows) +/// and set it as the active device. If no GPU backend is available, it falls back to the CPU backend. +pub fn init_icicle_backend() -> Result { + let mut device_name = String::from("CPU"); + + icicle_runtime::runtime::load_backend_from_env_or_default()?; + + let metal_device = Device::new("METAL", 0); + if icicle_runtime::is_device_available(&metal_device) { + icicle_runtime::set_device(&metal_device)?; + device_name = String::from("METAL"); + return Ok(device_name); + } + + let cuda_device = Device::new("CUDA", 0); + if icicle_runtime::is_device_available(&cuda_device) { + icicle_runtime::set_device(&cuda_device)?; + device_name = String::from("CUDA"); + return Ok(device_name); + } + + let cpu_device = Device::new("CPU", 0); + icicle_runtime::set_device(&cpu_device)?; + + Ok(device_name) +} diff --git a/rln/src/circuit/icicle/convert.rs b/rln/src/circuit/icicle/convert.rs new file mode 100644 index 00000000..c31bd459 --- /dev/null +++ b/rln/src/circuit/icicle/convert.rs @@ -0,0 +1,192 @@ +// This crate is based on the code by Ingonyama. Its preimage can be found here: +// https://github.com/ingonyama-zk/icicle/blob/main/examples/rust/arkworks-icicle-conversions + +use ark_ec::AffineRepr; +use ark_ff::{BigInteger, Field, PrimeField}; +use icicle_bn254::curve::{ + BaseField as IcicleBaseField, G1Affine as IcicleG1Affine, G1Projective as IcicleG1Projective, + G2Affine as IcicleG2Affine, G2BaseField as IcicleG2BaseField, + G2Projective as IcicleG2Projective, ScalarField as IcicleScalar, +}; +use icicle_core::{ + affine::Affine as IcicleAffineTrait, bignum::BigNum, projective::Projective as IcicleProjective, +}; + +use crate::circuit::{Fr, G1Affine, G2Affine}; + +#[inline] +fn from_ark(ark: &T) -> I +where + T: Field, + I: BigNum, +{ + let mut ark_bytes = vec![]; + for base_elem in ark.to_base_prime_field_elements() { + ark_bytes.extend_from_slice(&base_elem.into_bigint().to_bytes_le()); + } + I::from_bytes_le(&ark_bytes) +} + +/// Convert arkworks Fr scalar to ICICLE scalar +#[inline] +pub(crate) fn ark_fr_to_icicle(scalar: &Fr) -> IcicleScalar { + from_ark(scalar) +} + +/// Convert slice of arkworks Fr scalars to ICICLE scalars +pub(crate) fn ark_frs_to_icicle(scalars: &[Fr]) -> Vec { + scalars.iter().map(ark_fr_to_icicle).collect() +} + +/// Convert ICICLE scalar to arkworks Fr +#[inline] +pub(crate) fn icicle_to_ark_fr(scalar: &IcicleScalar) -> Fr { + let bytes = scalar.to_bytes_le(); + Fr::from_le_bytes_mod_order(&bytes) +} + +/// Convert slice of ICICLE scalars to arkworks Fr +#[allow(dead_code)] +pub(crate) fn icicle_to_ark_frs(scalars: &[IcicleScalar]) -> Vec { + scalars.iter().map(icicle_to_ark_fr).collect() +} + +/// Convert arkworks G1Affine point to ICICLE G1Affine +#[inline] +pub(crate) fn ark_g1_to_icicle(point: &G1Affine) -> IcicleG1Affine { + if point.is_zero() { + return IcicleG1Affine::zero(); + } + + IcicleG1Affine { + x: from_ark::<_, IcicleBaseField>(&point.x().unwrap()), + y: from_ark::<_, IcicleBaseField>(&point.y().unwrap()), + } +} + +/// Convert slice of arkworks G1Affine points to ICICLE G1Affine +pub(crate) fn ark_g1s_to_icicle(points: &[G1Affine]) -> Vec { + points.iter().map(ark_g1_to_icicle).collect() +} + +/// Convert ICICLE G1Projective to arkworks G1Affine +#[inline] +pub(crate) fn icicle_g1_proj_to_ark(point: &IcicleG1Projective) -> ark_bn254::G1Affine { + use ark_bn254::{Fq, G1Affine}; + + // Convert to affine in ICICLE + let affine: IcicleG1Affine = point.to_affine(); + + // Check for zero point + if affine == IcicleG1Affine::zero() { + return G1Affine::zero(); + } + + let x_bytes = affine.x.to_bytes_le(); + let y_bytes = affine.y.to_bytes_le(); + + let x = Fq::from_le_bytes_mod_order(&x_bytes); + let y = Fq::from_le_bytes_mod_order(&y_bytes); + + G1Affine::new_unchecked(x, y) +} + +/// Convert arkworks G2Affine point to ICICLE G2Affine +#[inline] +pub(crate) fn ark_g2_to_icicle(point: &G2Affine) -> IcicleG2Affine { + if point.is_zero() { + return IcicleG2Affine::zero(); + } + + // G2 coordinates are in Fq2 (extension field) + let x = point.x().unwrap(); + let y = point.y().unwrap(); + + // Fq2 has c0 and c1 components - combine into extension field format + let x_c0_bytes = x.c0.into_bigint().to_bytes_le(); + let x_c1_bytes = x.c1.into_bigint().to_bytes_le(); + let y_c0_bytes = y.c0.into_bigint().to_bytes_le(); + let y_c1_bytes = y.c1.into_bigint().to_bytes_le(); + + // Combine into extension field format for ICICLE + let mut x_bytes = [0u8; 64]; + let mut y_bytes = [0u8; 64]; + + x_bytes[..32].copy_from_slice(&x_c0_bytes); + x_bytes[32..].copy_from_slice(&x_c1_bytes); + y_bytes[..32].copy_from_slice(&y_c0_bytes); + y_bytes[32..].copy_from_slice(&y_c1_bytes); + + let x_field = IcicleG2BaseField::from_bytes_le(&x_bytes); + let y_field = IcicleG2BaseField::from_bytes_le(&y_bytes); + + IcicleG2Affine { + x: x_field, + y: y_field, + } +} + +/// Convert slice of arkworks G2Affine points to ICICLE G2Affine +pub(crate) fn ark_g2s_to_icicle(points: &[G2Affine]) -> Vec { + points.iter().map(ark_g2_to_icicle).collect() +} + +/// Convert ICICLE G2Projective to arkworks G2Affine +#[inline] +pub(crate) fn icicle_g2_proj_to_ark(point: &IcicleG2Projective) -> G2Affine { + use ark_bn254::{Fq, Fq2, G2Affine}; + + let affine: IcicleG2Affine = point.to_affine(); + + if affine == IcicleG2Affine::zero() { + return G2Affine::zero(); + } + + let x_bytes = affine.x.to_bytes_le(); + let y_bytes = affine.y.to_bytes_le(); + + // Split back into c0, c1 components + let x_c0 = Fq::from_le_bytes_mod_order(&x_bytes[..32]); + let x_c1 = Fq::from_le_bytes_mod_order(&x_bytes[32..64]); + let y_c0 = Fq::from_le_bytes_mod_order(&y_bytes[..32]); + let y_c1 = Fq::from_le_bytes_mod_order(&y_bytes[32..64]); + + let x = Fq2::new(x_c0, x_c1); + let y = Fq2::new(y_c0, y_c1); + + G2Affine::new_unchecked(x, y) +} + +#[cfg(test)] +mod tests { + use ark_std::UniformRand; + + use super::*; + + #[test] + fn test_scalar_roundtrip() { + let mut rng = ark_std::test_rng(); + for _ in 0..100 { + let ark_scalar = Fr::rand(&mut rng); + let icicle_scalar = ark_fr_to_icicle(&ark_scalar); + let back = icicle_to_ark_fr(&icicle_scalar); + assert_eq!(ark_scalar, back); + } + } + + #[test] + fn test_g1_conversion() { + use ark_bn254::G1Projective; + use ark_ec::CurveGroup; + + let mut rng = ark_std::test_rng(); + for _ in 0..10 { + let ark_point = G1Projective::rand(&mut rng).into_affine(); + let icicle_point = ark_g1_to_icicle(&ark_point); + // Basic sanity check - point should not be zero unless input was zero + if !ark_point.is_zero() { + assert!(icicle_point != IcicleG1Affine::zero()); + } + } + } +} diff --git a/rln/src/circuit/icicle/msm.rs b/rln/src/circuit/icicle/msm.rs new file mode 100644 index 00000000..8b70da8d --- /dev/null +++ b/rln/src/circuit/icicle/msm.rs @@ -0,0 +1,77 @@ +use ark_ec::AffineRepr; +use ark_ff::Zero; +use icicle_bn254::curve::{G1Projective as IcicleG1Projective, G2Projective as IcicleG2Projective}; +use icicle_core::{ + msm::{msm, MSMConfig}, + projective::Projective as IcicleProjective, +}; +use icicle_runtime::memory::HostSlice; + +use super::convert::{ + ark_frs_to_icicle, ark_g1s_to_icicle, ark_g2s_to_icicle, icicle_g1_proj_to_ark, + icicle_g2_proj_to_ark, +}; +use crate::{ + circuit::{Fr, G1Affine, G2Affine}, + error::ProtocolError, +}; + +/// Perform MSM on G1 using ICICLE +pub fn icicle_msm_g1( + scalars: &[Fr], + points: &[G1Affine], +) -> Result { + if scalars.is_empty() || points.is_empty() { + return Ok(ark_bn254::G1Projective::zero()); + } + + // Convert to ICICLE types + let icicle_scalars = ark_frs_to_icicle(scalars); + let icicle_points = ark_g1s_to_icicle(points); + + // Prepare result + let mut result = vec![IcicleG1Projective::zero()]; + + // Perform MSM + let cfg = MSMConfig::default(); + msm( + HostSlice::from_slice(&icicle_scalars), + HostSlice::from_slice(&icicle_points), + &cfg, + HostSlice::from_mut_slice(&mut result), + )?; + + // Convert back to arkworks + let affine = icicle_g1_proj_to_ark(&result[0]); + Ok(affine.into_group()) +} + +/// Perform MSM on G2 using ICICLE +pub fn icicle_msm_g2( + scalars: &[Fr], + points: &[G2Affine], +) -> Result { + if scalars.is_empty() || points.is_empty() { + return Ok(ark_bn254::G2Projective::zero()); + } + + // Convert to ICICLE types + let icicle_scalars = ark_frs_to_icicle(scalars); + let icicle_points = ark_g2s_to_icicle(points); + + // Prepare result + let mut result = vec![IcicleG2Projective::zero()]; + + // Perform MSM + let cfg = MSMConfig::default(); + msm( + HostSlice::from_slice(&icicle_scalars), + HostSlice::from_slice(&icicle_points), + &cfg, + HostSlice::from_mut_slice(&mut result), + )?; + + // Convert back to arkworks + let affine = icicle_g2_proj_to_ark(&result[0]); + Ok(affine.into_group()) +} diff --git a/rln/src/circuit/icicle/proof.rs b/rln/src/circuit/icicle/proof.rs new file mode 100644 index 00000000..5c1c2275 --- /dev/null +++ b/rln/src/circuit/icicle/proof.rs @@ -0,0 +1,118 @@ +use std::ops::Mul; + +use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::Zero; + +use crate::{ + circuit::{ + icicle::msm::{icicle_msm_g1, icicle_msm_g2}, + Fr, G1Affine, G1Projective, G2Affine, G2Projective, Proof, ProvingKey, + }, + error::ProtocolError, +}; + +/// Calculate coefficient for proof element using ICICLE MSM +fn calculate_coeff_g1_icicle( + initial: G1Projective, + query: &[G1Affine], + vk_param: G1Affine, + assignment: &[Fr], +) -> Result { + let el = query[0]; + + // MSM for query[1..] with assignment + let msm_result = if query.len() > 1 && !assignment.is_empty() { + icicle_msm_g1(assignment, &query[1..])? + } else { + G1Projective::zero() + }; + + let mut res = initial; + res += el.into_group(); + res += msm_result; + res += vk_param.into_group(); + + Ok(res) +} + +/// Calculate coefficient for G2 proof element using ICICLE MSM +fn calculate_coeff_g2_icicle( + initial: G2Projective, + query: &[G2Affine], + vk_param: G2Affine, + assignment: &[Fr], +) -> Result { + let el = query[0]; + + // MSM for query[1..] with assignment + let msm_result = if query.len() > 1 && !assignment.is_empty() { + icicle_msm_g2(assignment, &query[1..])? + } else { + G2Projective::zero() + }; + + let mut res = initial; + res += el.into_group(); + res += msm_result; + res += vk_param.into_group(); + + Ok(res) +} + +/// Generates a zkSNARK proof using ICICLE GPU acceleration. +pub fn create_proof_with_icicle_msm( + pk: &ProvingKey, + r: Fr, + s: Fr, + h: &[Fr], + input_assignment: &[Fr], + aux_assignment: &[Fr], +) -> Result { + // h_acc = MSM(pk.h_query, h) + let h_acc = icicle_msm_g1(h, &pk.h_query)?; + + // l_aux_acc = MSM(pk.l_query, aux_assignment) + let l_aux_acc = icicle_msm_g1(aux_assignment, &pk.l_query)?; + + // r_s_delta_g1 = pk.delta_g1 * (r * s) + let r_s_delta_g1 = pk.delta_g1.mul(r * s); + + // Combine assignments + let assignment: Vec = input_assignment + .iter() + .chain(aux_assignment.iter()) + .copied() + .collect(); + + // Compute A + // g_a = calculate_coeff(r * delta_g1, pk.a_query, pk.vk.alpha_g1, assignment) + let r_g1 = pk.delta_g1.mul(r); + let g_a = calculate_coeff_g1_icicle(r_g1, &pk.a_query, pk.vk.alpha_g1, &assignment)?; + let s_g_a = g_a.mul(s); + + // Compute B in G1 (only if r != 0) + let g1_b = if !r.is_zero() { + let s_g1 = pk.delta_g1.mul(s); + calculate_coeff_g1_icicle(s_g1, &pk.b_g1_query, pk.beta_g1, &assignment)? + } else { + ark_bn254::G1Projective::zero() + }; + + // Compute B in G2 + let s_g2 = pk.vk.delta_g2.mul(s); + let g2_b = calculate_coeff_g2_icicle(s_g2, &pk.b_g2_query, pk.vk.beta_g2, &assignment)?; + let r_g1_b = g1_b.mul(r); + + // Compute C + let mut g_c = s_g_a; + g_c += r_g1_b; + g_c -= r_s_delta_g1; + g_c += l_aux_acc; + g_c += h_acc; + + Ok(Proof { + a: g_a.into_affine(), + b: g2_b.into_affine(), + c: g_c.into_affine(), + }) +} diff --git a/rln/src/circuit/mod.rs b/rln/src/circuit/mod.rs index c4733678..bb4a32b8 100644 --- a/rln/src/circuit/mod.rs +++ b/rln/src/circuit/mod.rs @@ -4,6 +4,9 @@ pub(crate) mod error; pub(crate) mod iden3calc; pub(crate) mod qap; +#[cfg(feature = "icicle")] +pub mod icicle; + #[cfg(not(target_arch = "wasm32"))] use std::sync::LazyLock; diff --git a/rln/src/error.rs b/rln/src/error.rs index a99b09d7..e44d7439 100644 --- a/rln/src/error.rs +++ b/rln/src/error.rs @@ -1,6 +1,8 @@ use std::{array::TryFromSliceError, num::TryFromIntError}; use ark_relations::r1cs::SynthesisError; +#[cfg(feature = "icicle")] +use icicle_runtime::IcicleError; use num_bigint::{BigInt, ParseBigIntError}; use zerokit_utils::error::{FromConfigError, HashError, ZerokitMerkleTreeError}; @@ -27,6 +29,9 @@ pub enum UtilsError { /// Errors that can occur during RLN protocol operations (proof generation, verification, etc.) #[derive(Debug, thiserror::Error)] pub enum ProtocolError { + #[cfg(feature = "icicle")] + #[error("Icicle error: {0}")] + Icicle(#[from] IcicleError), #[error("Error producing proof: {0}")] Synthesis(#[from] SynthesisError), #[error("RLN utility error: {0}")] diff --git a/rln/src/prelude.rs b/rln/src/prelude.rs index 8a5c00af..396e2d65 100644 --- a/rln/src/prelude.rs +++ b/rln/src/prelude.rs @@ -1,5 +1,7 @@ // This module re-exports the most commonly used types and functions from the RLN library +#[cfg(feature = "icicle")] +pub use crate::circuit::icicle::init_icicle_backend; #[cfg(not(target_arch = "wasm32"))] pub use crate::circuit::{graph_from_folder, zkey_from_folder}; #[cfg(feature = "pmtree-ft")] diff --git a/rln/src/protocol/mod.rs b/rln/src/protocol/mod.rs index b35dec1f..b5ac8c16 100644 --- a/rln/src/protocol/mod.rs +++ b/rln/src/protocol/mod.rs @@ -12,6 +12,8 @@ pub use proof::{ rln_proof_to_bytes_be, rln_proof_to_bytes_le, rln_proof_values_to_bytes_be, rln_proof_values_to_bytes_le, verify_zk_proof, RLNProof, RLNProofValues, }; +#[cfg(feature = "icicle")] +pub use proof::{generate_zk_proof_icicle, generate_zk_proof_with_witness_icicle}; pub use slashing::recover_id_secret; pub use witness::{ bytes_be_to_rln_witness, bytes_le_to_rln_witness, compute_tree_root, proof_values_from_witness, diff --git a/rln/src/protocol/proof.rs b/rln/src/protocol/proof.rs index 0a966e09..0910756b 100644 --- a/rln/src/protocol/proof.rs +++ b/rln/src/protocol/proof.rs @@ -4,6 +4,11 @@ use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::{rand::thread_rng, UniformRand}; use num_bigint::BigInt; use num_traits::Signed; +#[cfg(feature = "icicle")] +use { + crate::circuit::icicle::proof::create_proof_with_icicle_msm, + ark_groth16::r1cs_to_qap::R1CSToQAP, ark_poly::GeneralEvaluationDomain, +}; use super::witness::{inputs_for_witness_calculation, RLNWitnessInput}; use crate::{ @@ -343,3 +348,75 @@ pub fn verify_zk_proof( Ok(verified) } + +/// Generates a zkSNARK proof from pre-calculated witness using ICICLE GPU acceleration. +/// +/// This is a drop-in replacement for `generate_zk_proof_with_witness` that uses +/// GPU-accelerated MSM operations via the ICICLE library. +/// Falls back to CPU if GPU is not available. +#[cfg(feature = "icicle")] +pub fn generate_zk_proof_with_witness_icicle( + calculated_witness: Vec, + zkey: &Zkey, +) -> Result { + let full_assignment = calculated_witness_to_field_elements::(calculated_witness)?; + + // Compute witness polynomial h using CircomReduction (same as before) + let h = CircomReduction::witness_map_from_matrices::>( + &zkey.1, + zkey.1.num_instance_variables, + zkey.1.num_constraints, + &full_assignment, + )?; + + // Random Values + let mut rng = thread_rng(); + let r = Fr::rand(&mut rng); + let s = Fr::rand(&mut rng); + + // Split assignment into input and auxiliary + let input_assignment = &full_assignment[1..zkey.1.num_instance_variables]; + let aux_assignment = &full_assignment[zkey.1.num_instance_variables..]; + + // Generate proof using ICICLE + create_proof_with_icicle_msm(&zkey.0, r, s, &h, input_assignment, aux_assignment) +} + +/// Generates a zkSNARK proof using ICICLE GPU acceleration. +/// +/// This is a drop-in replacement for `generate_zk_proof` that uses +/// GPU-accelerated MSM operations via the ICICLE library. +/// Falls back to CPU if GPU is not available. +#[cfg(feature = "icicle")] +pub fn generate_zk_proof_icicle( + zkey: &Zkey, + witness: &RLNWitnessInput, + graph: &Graph, +) -> Result { + // Calculate witness using the standard method + let inputs = inputs_for_witness_calculation(witness)? + .into_iter() + .map(|(name, values)| (name.to_string(), values)); + + let full_assignment = calc_witness(inputs, graph)?; + + // Compute witness polynomial h using CircomReduction (same as before) + let h = CircomReduction::witness_map_from_matrices::>( + &zkey.1, + zkey.1.num_instance_variables, + zkey.1.num_constraints, + &full_assignment, + )?; + + // Random Values + let mut rng = thread_rng(); + let r = Fr::rand(&mut rng); + let s = Fr::rand(&mut rng); + + // Split assignment into input and auxiliary + let input_assignment = &full_assignment[1..zkey.1.num_instance_variables]; + let aux_assignment = &full_assignment[zkey.1.num_instance_variables..]; + + // Generate proof using ICICLE + create_proof_with_icicle_msm(&zkey.0, r, s, &h, input_assignment, aux_assignment) +} diff --git a/rln/tests/protocol.rs b/rln/tests/protocol.rs index 10d62182..0c97d02f 100644 --- a/rln/tests/protocol.rs +++ b/rln/tests/protocol.rs @@ -4,6 +4,8 @@ mod test { use ark_ff::BigInt; use rln::prelude::*; + #[cfg(feature = "icicle")] + use rln::protocol::generate_zk_proof_icicle; use zerokit_utils::merkle_tree::{ZerokitMerkleProof, ZerokitMerkleTree}; type ConfigOf = ::Config; @@ -290,4 +292,28 @@ mod test { assert_eq!(identity_secret, expected_identity_secret_seed_phrase); assert_eq!(id_commitment, expected_id_commitment_seed_phrase); } + + #[test] + #[cfg(feature = "icicle")] + // We test that normal and ICICLE proofs both verify correctly + fn test_end_to_end_icicle() { + let witness = get_test_witness(); + + let proving_key = zkey_from_folder(); + let graph_data = graph_from_folder(); + + let normal_proof = generate_zk_proof(proving_key, &witness, graph_data).unwrap(); + + let icicle_proof = generate_zk_proof_icicle(proving_key, &witness, graph_data).unwrap(); + + let proof_values = proof_values_from_witness(&witness).unwrap(); + + let normal_success = + verify_zk_proof(&proving_key.0.vk, &normal_proof, &proof_values).unwrap(); + let icicle_success = + verify_zk_proof(&proving_key.0.vk, &icicle_proof, &proof_values).unwrap(); + + assert!(normal_success, "Normal proof should verify"); + assert!(icicle_success, "ICICLE proof should verify"); + } }