diff --git a/Cargo.toml b/Cargo.toml index c37eb04..fd73470 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ proptest = { version = "1.0", optional = true } rayon = "1.5.1" serde = "1.0" sha2 = "0.10.1" +tempfile = "3.3.0" thiserror = "1.0.0" tiny-keccak = { version = "2.0.2", optional = true } zkp-u256 = { version = "0.2", optional = true } diff --git a/README.md b/README.md index eab3d35..285011b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🦀 semaphore-rs +# 🦀 semaphore-rs Rust support library for using [semaphore](https://github.com/appliedzkp/semaphore). It's mostly a Rust rewrite of [zk-kit](https://github.com/appliedzkp/zk-kit), but just focuses on semaphore (for now) and still covers a much smaller scope. It's using [ark-circom](https://github.com/gakonst/ark-circom) under the hood for generating the groth16 proofs. @@ -33,8 +33,7 @@ let id = Identity::new(b"secret"); const LEAF: Hash = Hash::from_bytes_be([0u8; 32]); let mut tree = PoseidonTree::new(21, LEAF); -let (_, leaf) = id.commitment().to_bytes_be(); -tree.set(0, leaf.into()); +tree.set(0, id.commitment().into()); let merkle_proof = tree.proof(0).expect("proof should exist"); let root = tree.root(); @@ -44,15 +43,10 @@ let signal = b"xxx"; let external_nullifier = b"appId"; let external_nullifier_hash = hash_external_nullifier(external_nullifier); -let nullifier_hash = generate_nullifier_hash(&id, &external_nullifier_hash); +let nullifier_hash = generate_nullifier_hash(&id, external_nullifier_hash); -let config = SnarkFileConfig { - zkey: "./semaphore/build/snark/semaphore_final.zkey".to_string(), - wasm: "./semaphore/build/snark/semaphore.wasm".to_string(), -}; - -let proof = generate_proof(&config, &id, &merkle_proof, &external_nullifier_hash, signal).unwrap(); -let success = verify_proof(&config, &root.into(), &nullifier_hash, signal, &external_nullifier_hash, &proof).unwrap(); +let proof = generate_proof(&id, &merkle_proof, external_nullifier, signal).unwrap(); +let success = verify_proof(root.into(), nullifier_hash, signal, external_nullifier, &proof).unwrap(); assert!(success); ``` diff --git a/cspell.json b/cspell.json index aca1d33..df65f80 100644 --- a/cspell.json +++ b/cspell.json @@ -5,6 +5,7 @@ "dictionaries": [], "words": [ "biguint", + "chacha", "circom", "groth", "hasher", @@ -14,7 +15,9 @@ "mmaped", "modpow", "Repr", + "Seedable", "snarkfiles", + "thiserror", "zkey" ], "ignoreWords": [], diff --git a/src/circuit.rs b/src/circuit.rs new file mode 100644 index 0000000..175c1bf --- /dev/null +++ b/src/circuit.rs @@ -0,0 +1,27 @@ +use ark_bn254::{Bn254, Fr}; +use ark_circom::{read_zkey, WitnessCalculator}; +use ark_groth16::ProvingKey; +use ark_relations::r1cs::ConstraintMatrices; +use core::include_bytes; +use once_cell::sync::Lazy; +use std::io::{Cursor, Write}; +use tempfile::NamedTempFile; + +const ZKEY_BYTES: &[u8] = include_bytes!("../semaphore/build/snark/semaphore_final.zkey"); +const WASM: &[u8] = include_bytes!("../semaphore/build/snark/semaphore.wasm"); + +pub static ZKEY: Lazy<(ProvingKey, ConstraintMatrices)> = Lazy::new(|| { + let mut reader = Cursor::new(ZKEY_BYTES); + read_zkey(&mut reader).expect("zkey should be valid") +}); + +pub static WITNESS_CALCULATOR: Lazy = Lazy::new(|| { + // HACK: ark-circom requires a file, so we make one! + let mut tmpfile = NamedTempFile::new().expect("Failed to create temp file"); + let written = tmpfile.write(WASM).expect("Failed to write to temp file"); + assert_eq!(written, WASM.len()); + let path = tmpfile.into_temp_path(); + let result = WitnessCalculator::new(&path).expect("Failed to create witness calculator"); + path.close().expect("Could not remove tempfile"); + result +}); diff --git a/src/identity.rs b/src/identity.rs index e781558..59827ac 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -1,4 +1,4 @@ -use crate::{posseidon_hash, Field}; +use crate::{poseidon_hash, Field}; use ark_ff::PrimeField; use sha2::{Digest, Sha256}; @@ -42,11 +42,11 @@ impl Identity { #[must_use] pub fn secret_hash(&self) -> Field { - posseidon_hash(&[self.nullifier, self.trapdoor]) + poseidon_hash(&[self.nullifier, self.trapdoor]) } #[must_use] pub fn commitment(&self) -> Field { - posseidon_hash(&[self.secret_hash()]) + poseidon_hash(&[self.secret_hash()]) } } diff --git a/src/lib.rs b/src/lib.rs index 32a3b86..23f1f7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,14 @@ #![doc = include_str!("../Readme.md")] #![warn(clippy::all, clippy::pedantic, clippy::cargo, clippy::nursery)] -// TODO: ark-circom and ethers-core pull in a lot of deps, some duplicate. +// TODO: ark-circom and ethers-core pull in a lot of dependencies, some duplicate. #![allow(clippy::multiple_crate_versions)] +mod circuit; pub mod hash; pub mod identity; pub mod merkle_tree; +mod poseidon_hash; pub mod poseidon_tree; -mod posseidon_hash; pub mod protocol; pub mod util; @@ -16,10 +17,10 @@ pub mod mimc_hash; #[cfg(feature = "mimc")] pub mod mimc_tree; -use ark_bn254::{Fr, FrParameters, Parameters}; +use ark_bn254::{Fr, Parameters}; use ark_ec::bn::Bn; -pub use crate::posseidon_hash::posseidon_hash; +pub use crate::poseidon_hash::poseidon_hash; pub type Field = Fr; pub type Groth16Proof = ark_groth16::Proof>; @@ -33,7 +34,6 @@ mod test { poseidon_tree::PoseidonTree, protocol::{ generate_nullifier_hash, generate_proof, hash_external_nullifier, verify_proof, - SnarkFileConfig, }, }; use hex_literal::hex; @@ -61,23 +61,10 @@ mod test { let external_nullifier_hash = hash_external_nullifier(external_nullifier); let nullifier_hash = generate_nullifier_hash(&id, external_nullifier_hash); - let config = SnarkFileConfig { - zkey: "./semaphore/build/snark/semaphore_final.zkey".to_string(), - wasm: "./semaphore/build/snark/semaphore.wasm".to_string(), - }; + let proof = generate_proof(&id, &merkle_proof, external_nullifier, signal).unwrap(); - let proof = - generate_proof(&config, &id, &merkle_proof, external_nullifier, signal).unwrap(); - - let success = verify_proof( - &config, - root, - nullifier_hash, - signal, - external_nullifier, - &proof, - ) - .unwrap(); + let success = + verify_proof(root, nullifier_hash, signal, external_nullifier, &proof).unwrap(); assert!(success); } @@ -86,10 +73,7 @@ mod test { #[cfg(feature = "bench")] pub mod bench { use crate::{ - hash::Hash, - identity::Identity, - poseidon_tree::PoseidonTree, - protocol::{generate_proof, SnarkFileConfig}, + hash::Hash, identity::Identity, poseidon_tree::PoseidonTree, protocol::generate_proof, }; use criterion::Criterion; use hex_literal::hex; @@ -117,14 +101,9 @@ pub mod bench { let signal = b"xxx"; let external_nullifier = b"appId"; - let config = SnarkFileConfig { - zkey: "./semaphore/build/snark/semaphore_final.zkey".to_string(), - wasm: "./semaphore/build/snark/semaphore.wasm".to_string(), - }; - criterion.bench_function("proof", move |b| { b.iter(|| { - generate_proof(&config, &id, &merkle_proof, external_nullifier, signal).unwrap(); + generate_proof(&id, &merkle_proof, external_nullifier, signal).unwrap(); }); }); } diff --git a/src/posseidon_hash.rs b/src/poseidon_hash.rs similarity index 62% rename from src/posseidon_hash.rs rename to src/poseidon_hash.rs index da57f4a..1ac053a 100644 --- a/src/posseidon_hash.rs +++ b/src/poseidon_hash.rs @@ -1,43 +1,41 @@ -use crate::{ - hash::Hash, - merkle_tree::{self, Hasher, MerkleTree}, - Field, -}; +use crate::Field; use ark_ff::{BigInteger256, PrimeField as _}; -use ff::{PrimeField as _, PrimeFieldRepr as _}; +use ff::PrimeField as _; use once_cell::sync::Lazy; use poseidon_rs::{Fr, FrRepr, Poseidon}; -use serde::{Deserialize, Serialize}; static POSEIDON: Lazy = Lazy::new(Poseidon::new); -fn ark_to_posseidon(n: Field) -> Fr { +#[must_use] +fn ark_to_poseidon(n: Field) -> Fr { Fr::from_repr(FrRepr(n.into_repr().0)).expect("n is a valid field element") } -fn posseidon_to_ark(n: Fr) -> Field { +#[must_use] +fn poseidon_to_ark(n: Fr) -> Field { Field::from_repr(BigInteger256(n.into_repr().0)).expect("n is a valid field element") } -pub fn posseidon_hash(input: &[Field]) -> Field { +#[must_use] +pub fn poseidon_hash(input: &[Field]) -> Field { let input = input .iter() .copied() - .map(ark_to_posseidon) + .map(ark_to_poseidon) .collect::>(); POSEIDON .hash(input) - .map(posseidon_to_ark) + .map(poseidon_to_ark) .expect("hash with fixed input size can't fail") } #[cfg(test)] mod test { - use super::{ark_to_posseidon, posseidon_to_ark}; + use super::{ark_to_poseidon, poseidon_to_ark}; use crate::Field; use ark_ff::{Field as _, UniformRand}; - use ff::{Field as _, PrimeField, PrimeFieldRepr}; + use ff::PrimeField; use poseidon_rs::Fr; use rand_chacha::ChaChaRng; use rand_core::SeedableRng; @@ -50,10 +48,10 @@ mod test { #[test] fn test_ark_pos_ark_roundtrip() { let mut rng = ChaChaRng::seed_from_u64(123); - for i in 0..1000 { + for _ in 0..1000 { let n = Field::rand(&mut rng); - let m = posseidon_to_ark(ark_to_posseidon(n)); - assert_eq!(n, m) + let m = poseidon_to_ark(ark_to_poseidon(n)); + assert_eq!(n, m); } } } diff --git a/src/poseidon_tree.rs b/src/poseidon_tree.rs index 9ce20d1..59732b0 100644 --- a/src/poseidon_tree.rs +++ b/src/poseidon_tree.rs @@ -1,7 +1,7 @@ use crate::{ hash::Hash, merkle_tree::{self, Hasher, MerkleTree}, - posseidon_hash, Field, + poseidon_hash, Field, }; use ark_ff::{PrimeField, ToBytes}; use serde::{Deserialize, Serialize}; @@ -19,14 +19,14 @@ pub struct PoseidonHash; #[allow(clippy::fallible_impl_from)] // TODO impl From<&Hash> for Field { fn from(hash: &Hash) -> Self { - Field::from_be_bytes_mod_order(&hash.0) + Self::from_be_bytes_mod_order(&hash.0) } } #[allow(clippy::fallible_impl_from)] // TODO impl From for Field { fn from(hash: Hash) -> Self { - Field::from_be_bytes_mod_order(&hash.0) + Self::from_be_bytes_mod_order(&hash.0) } } @@ -46,7 +46,7 @@ impl Hasher for PoseidonHash { type Hash = Hash; fn hash_node(left: &Self::Hash, right: &Self::Hash) -> Self::Hash { - posseidon_hash(&[left.into(), right.into()]).into() + poseidon_hash(&[left.into(), right.into()]).into() } } @@ -60,11 +60,9 @@ pub mod test { #[test] fn test_ark_hash_ark_roundtrip() { - use ark_ff::One; let mut rng = ChaChaRng::seed_from_u64(123); - for i in 0..1000 { + for _ in 0..1000 { let n = Field::rand(&mut rng); - let n = Field::one(); let m = Hash::from(n).into(); assert_eq!(n, m); } diff --git a/src/protocol.rs b/src/protocol.rs index 1c525b5..bd76e47 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,11 +1,13 @@ use crate::{ + circuit::{WITNESS_CALCULATOR, ZKEY}, identity::Identity, merkle_tree::{self, Branch}, + poseidon_hash, poseidon_tree::PoseidonHash, - posseidon_hash, Field, + Field, }; use ark_bn254::{Bn254, Parameters}; -use ark_circom::{read_zkey, CircomReduction, WitnessCalculator}; +use ark_circom::CircomReduction; use ark_ec::bn::Bn; use ark_ff::{Fp256, PrimeField}; use ark_groth16::{create_proof_with_reduction_and_matrices, prepare_verifying_key, Proof}; @@ -14,14 +16,9 @@ use ark_std::{rand::thread_rng, UniformRand}; use color_eyre::Result; use ethers_core::utils::keccak256; use num_bigint::{BigInt, BigUint, ToBigInt}; -use std::{collections::HashMap, fs::File, ops::Shr, time::Instant}; +use std::time::Instant; use thiserror::Error; -pub struct SnarkFileConfig { - pub zkey: String, - pub wasm: String, -} - /// Helper to merkle proof into a bigint vector /// TODO: we should create a From trait for this fn merkle_proof_to_vec(proof: &merkle_tree::Proof) -> Vec { @@ -59,7 +56,7 @@ pub fn hash_external_nullifier(nullifier: &[u8]) -> Field { /// Generates the nullifier hash #[must_use] pub fn generate_nullifier_hash(identity: &Identity, external_nullifier: Field) -> Field { - posseidon_hash(&[external_nullifier, identity.nullifier]) + poseidon_hash(&[external_nullifier, identity.nullifier]) } #[derive(Error, Debug)] @@ -83,17 +80,11 @@ fn ark_to_bigint(n: Field) -> BigInt { /// /// Returns a [`ProofError`] if proving fails. pub fn generate_proof( - config: &SnarkFileConfig, identity: &Identity, merkle_proof: &merkle_tree::Proof, external_nullifier: &[u8], signal: &[u8], ) -> Result>, ProofError> { - let mut file = File::open(&config.zkey)?; - let (params, matrices) = read_zkey(&mut file)?; - let num_inputs = matrices.num_instance_variables; - let num_constraints = matrices.num_constraints; - let external_nullifier = hash_external_nullifier(external_nullifier); let signal = hash_signal(signal); let inputs = [ @@ -104,7 +95,7 @@ pub fn generate_proof( ("externalNullifier", vec![external_nullifier]), ("signalHash", vec![signal]), ]; - let inputs = inputs.iter().map(|(name, values)| { + let inputs = inputs.into_iter().map(|(name, values)| { ( name.to_string(), values @@ -117,9 +108,8 @@ pub fn generate_proof( let now = Instant::now(); - let mut witness = WitnessCalculator::new(&config.wasm).map_err(ProofError::WitnessError)?; - - let full_assignment = witness + let full_assignment = WITNESS_CALCULATOR + .clone() .calculate_witness_element::(inputs, false) .map_err(ProofError::WitnessError)?; @@ -134,12 +124,12 @@ pub fn generate_proof( let now = Instant::now(); let proof = create_proof_with_reduction_and_matrices::<_, CircomReduction>( - ¶ms, + &ZKEY.0, r, s, - &matrices, - num_inputs, - num_constraints, + &ZKEY.1, + ZKEY.1.num_instance_variables, + ZKEY.1.num_constraints, full_assignment.as_slice(), )?; @@ -155,17 +145,13 @@ pub fn generate_proof( /// Returns a [`ProofError`] if verifying fails. Verification failure does not /// necessarily mean the proof is incorrect. pub fn verify_proof( - config: &SnarkFileConfig, root: Field, nullifier_hash: Field, signal: &[u8], external_nullifier: &[u8], proof: &Proof>, ) -> Result { - let mut file = File::open(&config.zkey)?; - let (params, _) = read_zkey(&mut file)?; - - let pvk = prepare_verifying_key(¶ms.vk); + let pvk = prepare_verifying_key(&ZKEY.0.vk); let public_inputs = vec![ root,