From 2be72b2af2804bef2bb7f910fb55293bd717bfff Mon Sep 17 00:00:00 2001 From: Ashutosh Varma Date: Tue, 17 Jan 2023 23:10:30 +0530 Subject: [PATCH] Improve Support for WASM (#60) * feat: use `XofReader` instead of `io::Read` for no_std compatibility * feat: remove `thiserror` dependency thiserror is not no_std safe * feat: move deps std features behind new `std` feature - add new `std` feature - add `simd_backend` - move `colored` dep behind profile feature * feat: use old rand(v0.7) for cubic example. since curve25519-dalek (v3) uses old rand(v0.7) we need this. should upgrade curve25519-dalek to v4 once it out of pre release * feat: only build bench & profile if std in enabled * feat: remove rand_core as dependency * feat(ci): add job to test wasm build * fix: rollback rand to v7 and update debug test * fix(ci): Cargo.toml patching * feat: make clippy happy * feat: add wasm doc in readme * feat: readme formatting * feat: derive `Default` for `ProofVerifyError` --- .github/workflows/rust.yml | 36 +++++++++++ Cargo.toml | 54 +++++++++++----- README.md | 127 +++++++++++++++++++++++-------------- benches/nizk.rs | 4 +- benches/snark.rs | 6 +- src/commitments.rs | 4 +- src/errors.rs | 25 +++++--- src/r1csinstance.rs | 12 ++-- src/scalar/ristretto255.rs | 4 +- 9 files changed, 186 insertions(+), 86 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index df6ba15..abda4fd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -28,3 +28,39 @@ jobs: - name: Check clippy warnings run: cargo clippy --all-targets --all-features -- -D warnings + build_nightly_wasm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install + run: rustup default nightly + + - name: Build without std + run: cargo build --no-default-features --verbose + + - name: Run tests without std + run: cargo test --no-default-features --verbose + + - name: Build examples without std + run: cargo build --examples --no-default-features --verbose + + - name: Install wasm32-wasi target + run: rustup target add wasm32-wasi + + - name: Install wasm32-unknown-unknown target + run: rustup target add wasm32-unknown-unknown + + - name: Build for target wasm-wasi + run: RUSTFLAGS="" cargo build --target=wasm32-wasi --no-default-features --verbose + + - name: Patch Cargo.toml for wasm-bindgen + run: | + echo "[dependencies.getrandom]" >> Cargo.toml + echo "version = \"0.1\"" >> Cargo.toml + echo "default-features = false" >> Cargo.toml + echo "features = [\"wasm-bindgen\"]" >> Cargo.toml + + - name: Build for target wasm32-unknown-unknown + run: RUSTFLAGS="" cargo build --target=wasm32-unknown-unknown --no-default-features --verbose + diff --git a/Cargo.toml b/Cargo.toml index 8af29cf..4cb09cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,22 +11,24 @@ license-file = "LICENSE" keywords = ["zkSNARKs", "cryptography", "proofs"] [dependencies] -curve25519-dalek = {version = "3.2.0", features = ["serde"]} -merlin = "3.0.0" -rand = "0.7.3" -digest = "0.8.1" -sha3 = "0.8.2" -byteorder = "1.3.4" +curve25519-dalek = { version = "3.2.0", features = [ + "serde", + "u64_backend", + "alloc", +], default-features = false } +merlin = { version = "3.0.0", default-features = false } +rand = { version = "0.7.3", features = ["getrandom"], default-features = false } +digest = { version = "0.8.1", default-features = false } +sha3 = { version = "0.8.2", default-features = false } +byteorder = { version = "1.3.4", default-features = false } rayon = { version = "1.3.0", optional = true } -serde = { version = "1.0.106", features = ["derive"] } -bincode = "1.2.1" -subtle = { version = "2.4", default-features = false } -rand_core = { version = "0.5", default-features = false } -zeroize = { version = "1", default-features = false } -itertools = "0.10.0" -colored = "2.0.0" -flate2 = "1.0.14" -thiserror = "1.0" +serde = { version = "1.0.106", features = ["derive"], default-features = false } +bincode = { version = "1.3.3", default-features = false } +subtle = { version = "2.4", features = ["i128"], default-features = false } +zeroize = { version = "1.5", default-features = false } +itertools = { version = "0.10.0", default-features = false } +colored = { version = "2.0.0", default-features = false, optional = true } +flate2 = { version = "1.0.14" } [dev-dependencies] criterion = "0.3.1" @@ -38,20 +40,38 @@ path = "src/lib.rs" [[bin]] name = "snark" path = "profiler/snark.rs" +required-features = ["std"] [[bin]] name = "nizk" path = "profiler/nizk.rs" +required-features = ["std"] [[bench]] name = "snark" harness = false +required-features = ["std"] [[bench]] name = "nizk" harness = false +required-features = ["std"] [features] -default = ["curve25519-dalek/simd_backend"] +default = ["std", "simd_backend"] +std = [ + "curve25519-dalek/std", + "digest/std", + "merlin/std", + "rand/std", + "sha3/std", + "byteorder/std", + "serde/std", + "subtle/std", + "zeroize/std", + "itertools/use_std", + "flate2/rust_backend", +] +simd_backend = ["curve25519-dalek/simd_backend"] multicore = ["rayon"] -profile = [] +profile = ["colored"] diff --git a/README.md b/README.md index 8b255a0..29d05bb 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,37 @@ # Spartan: High-speed zkSNARKs without trusted setup ![Rust](https://github.com/microsoft/Spartan/workflows/Rust/badge.svg) -[![](https://img.shields.io/crates/v/spartan.svg)]((https://crates.io/crates/spartan)) +[![](https://img.shields.io/crates/v/spartan.svg)](<(https://crates.io/crates/spartan)>) Spartan is a high-speed zero-knowledge proof system, a cryptographic primitive that enables a prover to prove a mathematical statement to a verifier without revealing anything besides the validity of the statement. This repository provides `libspartan,` a Rust library that implements a zero-knowledge succinct non-interactive argument of knowledge (zkSNARK), which is a type of zero-knowledge proof system with short proofs and fast verification times. The details of the Spartan proof system are described in our [paper](https://eprint.iacr.org/2019/550) published at [CRYPTO 2020](https://crypto.iacr.org/2020/). The security of the Spartan variant implemented in this library is based on the discrete logarithm problem in the random oracle model. A simple example application is proving the knowledge of a secret s such that H(s) == d for a public d, where H is a cryptographic hash function (e.g., SHA-256, Keccak). A more complex application is a database-backed cloud service that produces proofs of correct state machine transitions for auditability. See this [paper](https://eprint.iacr.org/2020/758.pdf) for an overview and this [paper](https://eprint.iacr.org/2018/907.pdf) for details. -Note that this library has *not* received a security review or audit. +Note that this library has _not_ received a security review or audit. ## Highlights + We now highlight Spartan's distinctive features. -* **No "toxic" waste:** Spartan is a *transparent* zkSNARK and does not require a trusted setup. So, it does not involve any trapdoors that must be kept secret or require a multi-party ceremony to produce public parameters. +- **No "toxic" waste:** Spartan is a _transparent_ zkSNARK and does not require a trusted setup. So, it does not involve any trapdoors that must be kept secret or require a multi-party ceremony to produce public parameters. -* **General-purpose:** Spartan produces proofs for arbitrary NP statements. `libspartan` supports NP statements expressed as rank-1 constraint satisfiability (R1CS) instances, a popular language for which there exists efficient transformations and compiler toolchains from high-level programs of interest. +- **General-purpose:** Spartan produces proofs for arbitrary NP statements. `libspartan` supports NP statements expressed as rank-1 constraint satisfiability (R1CS) instances, a popular language for which there exists efficient transformations and compiler toolchains from high-level programs of interest. -* **Sub-linear verification costs:** Spartan is the first transparent proof system with sub-linear verification costs for arbitrary NP statements (e.g., R1CS). +- **Sub-linear verification costs:** Spartan is the first transparent proof system with sub-linear verification costs for arbitrary NP statements (e.g., R1CS). -* **Standardized security:** Spartan's security relies on the hardness of computing discrete logarithms (a standard cryptographic assumption) in the random oracle model. `libspartan` uses `ristretto255`, a prime-order group abstraction atop `curve25519` (a high-speed elliptic curve). We use [`curve25519-dalek`](https://docs.rs/curve25519-dalek) for arithmetic over `ristretto255`. +- **Standardized security:** Spartan's security relies on the hardness of computing discrete logarithms (a standard cryptographic assumption) in the random oracle model. `libspartan` uses `ristretto255`, a prime-order group abstraction atop `curve25519` (a high-speed elliptic curve). We use [`curve25519-dalek`](https://docs.rs/curve25519-dalek) for arithmetic over `ristretto255`. -* **State-of-the-art performance:** -Among transparent SNARKs, Spartan offers the fastest prover with speedups of 36–152× depending on the baseline, produces proofs that are shorter by 1.2–416×, and incurs the lowest verification times with speedups of 3.6–1326×. The only exception is proof sizes under Bulletproofs, but Bulletproofs incurs slower verification both asymptotically and concretely. When compared to the state-of-the-art zkSNARK with trusted setup, Spartan’s prover is 2× faster for arbitrary R1CS instances and 16× faster for data-parallel workloads. +- **State-of-the-art performance:** + Among transparent SNARKs, Spartan offers the fastest prover with speedups of 36–152× depending on the baseline, produces proofs that are shorter by 1.2–416×, and incurs the lowest verification times with speedups of 3.6–1326×. The only exception is proof sizes under Bulletproofs, but Bulletproofs incurs slower verification both asymptotically and concretely. When compared to the state-of-the-art zkSNARK with trusted setup, Spartan’s prover is 2× faster for arbitrary R1CS instances and 16× faster for data-parallel workloads. ### Implementation details -`libspartan` uses [`merlin`](https://docs.rs/merlin/) to automate the Fiat-Shamir transform. We also introduce a new type called `RandomTape` that extends a `Transcript` in `merlin` to allow the prover's internal methods to produce private randomness using its private transcript without having to create `OsRng` objects throughout the code. An object of type `RandomTape` is initialized with a new random seed from `OsRng` for each proof produced by the library. + +`libspartan` uses [`merlin`](https://docs.rs/merlin/) to automate the Fiat-Shamir transform. We also introduce a new type called `RandomTape` that extends a `Transcript` in `merlin` to allow the prover's internal methods to produce private randomness using its private transcript without having to create `OsRng` objects throughout the code. An object of type `RandomTape` is initialized with a new random seed from `OsRng` for each proof produced by the library. ## Examples + To import `libspartan` into your Rust project, add the following dependency to `Cargo.toml`: + ```text spartan = "0.7.1" ``` @@ -36,11 +40,11 @@ The following example shows how to use `libspartan` to create and verify a SNARK Some of our public APIs' style is inspired by the underlying crates we use. ```rust -# extern crate libspartan; -# extern crate merlin; -# use libspartan::{Instance, SNARKGens, SNARK}; -# use merlin::Transcript; -# fn main() { +extern crate libspartan; +extern crate merlin; +use libspartan::{Instance, SNARKGens, SNARK}; +use merlin::Transcript; +fn main() { // specify the size of an R1CS instance let num_vars = 1024; let num_cons = 1024; @@ -66,16 +70,17 @@ Some of our public APIs' style is inspired by the underlying crates we use. .verify(&comm, &inputs, &mut verifier_transcript, &gens) .is_ok()); println!("proof verification successful!"); -# } +} ``` Here is another example to use the NIZK variant of the Spartan proof system: + ```rust -# extern crate libspartan; -# extern crate merlin; -# use libspartan::{Instance, NIZKGens, NIZK}; -# use merlin::Transcript; -# fn main() { +extern crate libspartan; +extern crate merlin; +use libspartan::{Instance, NIZKGens, NIZK}; +use merlin::Transcript; +fn main() { // specify the size of an R1CS instance let num_vars = 1024; let num_cons = 1024; @@ -97,20 +102,22 @@ Here is another example to use the NIZK variant of the Spartan proof system: .verify(&inst, &inputs, &mut verifier_transcript, &gens) .is_ok()); println!("proof verification successful!"); -# } +} ``` Finally, we provide an example that specifies a custom R1CS instance instead of using a synthetic instance + ```rust #![allow(non_snake_case)] -# extern crate curve25519_dalek; -# extern crate libspartan; -# extern crate merlin; -# use curve25519_dalek::scalar::Scalar; -# use libspartan::{InputsAssignment, Instance, SNARKGens, VarsAssignment, SNARK}; -# use merlin::Transcript; -# use rand::rngs::OsRng; -# fn main() { +extern crate curve25519_dalek; +extern crate libspartan; +extern crate merlin; +use curve25519_dalek::scalar::Scalar; +use libspartan::{InputsAssignment, Instance, SNARKGens, VarsAssignment, SNARK}; +use merlin::Transcript; +use rand::rngs::OsRng; + +fn main() { // produce a tiny instance let ( num_cons, @@ -146,17 +153,17 @@ Finally, we provide an example that specifies a custom R1CS instance instead of .verify(&comm, &assignment_inputs, &mut verifier_transcript, &gens) .is_ok()); println!("proof verification successful!"); -# } +} -# fn produce_tiny_r1cs() -> ( -# usize, -# usize, -# usize, -# usize, -# Instance, -# VarsAssignment, -# InputsAssignment, -# ) { +fn produce_tiny_r1cs() -> ( + usize, + usize, + usize, + usize, + Instance, + VarsAssignment, + InputsAssignment, +) { // We will use the following example, but one could construct any R1CS instance. // Our R1CS instance is three constraints over five variables and two public inputs // (Z0 + Z1) * I0 - Z2 = 0 @@ -182,7 +189,7 @@ Finally, we provide an example that specifies a custom R1CS instance instead of // a variable that holds a byte representation of 1 let one = Scalar::one().to_bytes(); - // R1CS is a set of three sparse matrices A B C, where is a row for every + // R1CS is a set of three sparse matrices A B C, where is a row for every // constraint and a column for every entry in z = (vars, 1, inputs) // An R1CS instance is satisfiable iff: // Az \circ Bz = Cz, where z = (vars, 1, inputs) @@ -233,7 +240,7 @@ Finally, we provide an example that specifies a custom R1CS instance instead of inputs[0] = i0.to_bytes(); inputs[1] = i1.to_bytes(); let assignment_inputs = InputsAssignment::new(&inputs).unwrap(); - + // check if the instance we created is satisfiable let res = inst.is_sat(&assignment_vars, &assignment_inputs); assert_eq!(res.unwrap(), true); @@ -247,36 +254,42 @@ Finally, we provide an example that specifies a custom R1CS instance instead of assignment_vars, assignment_inputs, ) -# } + } ``` For more examples, see [`examples/`](examples) directory in this repo. ## Building `libspartan` + Install [`rustup`](https://rustup.rs/) Switch to nightly Rust using `rustup`: + ```text rustup default nightly ``` Clone the repository: + ```text git clone https://github.com/Microsoft/Spartan cd Spartan ``` To build docs for public APIs of `libspartan`: + ```text cargo doc ``` To run tests: + ```text RUSTFLAGS="-C target_cpu=native" cargo test ``` To build `libspartan`: + ```text RUSTFLAGS="-C target_cpu=native" cargo build --release ``` @@ -284,19 +297,41 @@ RUSTFLAGS="-C target_cpu=native" cargo build --release > NOTE: We enable SIMD instructions in `curve25519-dalek` by default, so if it fails to build remove the "simd_backend" feature argument in `Cargo.toml`. ### Supported features -* `profile`: enables fine-grained profiling information (see below for its use) + +- `std`: enables std features (enabled by default) +- `simd_backend`: enables `curve25519-dalek`'s simd feature (enabled by default) +- `profile`: enables fine-grained profiling information (see below for its use) + +### WASM Support + +`libspartan` depends upon `rand::OsRng` (internally uses `getrandom` crate), it has out of box support for `wasm32-wasi`. + +For the target `wasm32-unknown-unknown` disable default features for spartan +and add direct dependency on `getrandom` with `wasm-bindgen` feature enabled. + +```toml +[dependencies] +spartan = { version = "0.7", default-features = false } +# since spartan uses getrandom(rand's OsRng), we need to enable 'wasm-bindgen' +# feature to make it feed rand seed from js/nodejs env +# https://docs.rs/getrandom/0.1.16/getrandom/index.html#support-for-webassembly-and-asmjs +getrandom = { version = "0.1", features = ["wasm-bindgen"] } +``` ## Performance ### End-to-end benchmarks + `libspartan` includes two benches: `benches/nizk.rs` and `benches/snark.rs`. If you report the performance of Spartan in a research paper, we recommend using these benches for higher accuracy instead of fine-grained profiling (listed below). To run end-to-end benchmarks: -```text + +```text RUSTFLAGS="-C target_cpu=native" cargo bench ``` ### Fine-grained profiling + Build `libspartan` with `profile` feature enabled. It creates two profilers: `./target/release/snark` and `./target/release/nizk`. These profilers report performance as depicted below (for varying R1CS instance sizes). The reported @@ -304,7 +339,7 @@ performance is from running the profilers on a Microsoft Surface Laptop 3 on a s See Section 9 in our [paper](https://eprint.iacr.org/2019/550) to see how this compares with other zkSNARKs in the literature. ```text -$ ./target/release/snark +$ ./target/release/snark Profiler:: SNARK * number_of_constraints 1048576 * number_of_variables 1048576 @@ -355,7 +390,7 @@ Profiler:: SNARK ``` ```text -$ ./target/release/nizk +$ ./target/release/nizk Profiler:: NIZK * number_of_constraints 1048576 * number_of_variables 1048576 diff --git a/benches/nizk.rs b/benches/nizk.rs index 77846cc..982c467 100644 --- a/benches/nizk.rs +++ b/benches/nizk.rs @@ -27,7 +27,7 @@ fn nizk_prove_benchmark(c: &mut Criterion) { let gens = NIZKGens::new(num_cons, num_vars, num_inputs); - let name = format!("NIZK_prove_{}", num_vars); + let name = format!("NIZK_prove_{num_vars}"); group.bench_function(&name, move |b| { b.iter(|| { let mut prover_transcript = Transcript::new(b"example"); @@ -61,7 +61,7 @@ fn nizk_verify_benchmark(c: &mut Criterion) { let mut prover_transcript = Transcript::new(b"example"); let proof = NIZK::prove(&inst, vars, &inputs, &gens, &mut prover_transcript); - let name = format!("NIZK_verify_{}", num_cons); + let name = format!("NIZK_verify_{num_cons}"); group.bench_function(&name, move |b| { b.iter(|| { let mut verifier_transcript = Transcript::new(b"example"); diff --git a/benches/snark.rs b/benches/snark.rs index 9b6c67e..5b4e3d3 100644 --- a/benches/snark.rs +++ b/benches/snark.rs @@ -22,7 +22,7 @@ fn snark_encode_benchmark(c: &mut Criterion) { let gens = SNARKGens::new(num_cons, num_vars, num_inputs, num_cons); // produce a commitment to R1CS instance - let name = format!("SNARK_encode_{}", num_cons); + let name = format!("SNARK_encode_{num_cons}"); group.bench_function(&name, move |b| { b.iter(|| { SNARK::encode(black_box(&inst), black_box(&gens)); @@ -51,7 +51,7 @@ fn snark_prove_benchmark(c: &mut Criterion) { let (comm, decomm) = SNARK::encode(&inst, &gens); // produce a proof - let name = format!("SNARK_prove_{}", num_cons); + let name = format!("SNARK_prove_{num_cons}"); group.bench_function(&name, move |b| { b.iter(|| { let mut prover_transcript = Transcript::new(b"example"); @@ -100,7 +100,7 @@ fn snark_verify_benchmark(c: &mut Criterion) { ); // verify the proof - let name = format!("SNARK_verify_{}", num_cons); + let name = format!("SNARK_verify_{num_cons}"); group.bench_function(&name, move |b| { b.iter(|| { let mut verifier_transcript = Transcript::new(b"example"); diff --git a/src/commitments.rs b/src/commitments.rs index d3caf7f..fc41840 100644 --- a/src/commitments.rs +++ b/src/commitments.rs @@ -1,8 +1,8 @@ use super::group::{GroupElement, VartimeMultiscalarMul, GROUP_BASEPOINT_COMPRESSED}; use super::scalar::Scalar; +use digest::XofReader; use digest::{ExtendableOutput, Input}; use sha3::Shake256; -use std::io::Read; #[derive(Debug)] pub struct MultiCommitGens { @@ -21,7 +21,7 @@ impl MultiCommitGens { let mut gens: Vec = Vec::new(); let mut uniform_bytes = [0u8; 64]; for _ in 0..n + 1 { - reader.read_exact(&mut uniform_bytes).unwrap(); + reader.read(&mut uniform_bytes); gens.push(GroupElement::from_uniform_bytes(&uniform_bytes)); } diff --git a/src/errors.rs b/src/errors.rs index 97eeb44..ade919d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,17 +1,26 @@ -use core::fmt::Debug; -use thiserror::Error; +use core::{ + fmt::Display, + fmt::{self, Debug}, +}; -#[derive(Error, Debug)] +#[derive(Debug, Default)] pub enum ProofVerifyError { - #[error("Proof verification failed")] + #[default] InternalError, - #[error("Compressed group element failed to decompress: {0:?}")] DecompressionError([u8; 32]), } -impl Default for ProofVerifyError { - fn default() -> Self { - ProofVerifyError::InternalError +impl Display for ProofVerifyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + ProofVerifyError::DecompressionError(bytes) => write!( + f, + "Compressed group element failed to decompress: {bytes:?}", + ), + ProofVerifyError::InternalError => { + write!(f, "Proof verification failed",) + } + } } } diff --git a/src/r1csinstance.rs b/src/r1csinstance.rs index 579779d..e4bb992 100644 --- a/src/r1csinstance.rs +++ b/src/r1csinstance.rs @@ -90,9 +90,9 @@ impl R1CSInstance { B: &[(usize, usize, Scalar)], C: &[(usize, usize, Scalar)], ) -> R1CSInstance { - Timer::print(&format!("number_of_constraints {}", num_cons)); - Timer::print(&format!("number_of_variables {}", num_vars)); - Timer::print(&format!("number_of_inputs {}", num_inputs)); + Timer::print(&format!("number_of_constraints {num_cons}")); + Timer::print(&format!("number_of_variables {num_vars}")); + Timer::print(&format!("number_of_inputs {num_inputs}")); Timer::print(&format!("number_non-zero_entries_A {}", A.len())); Timer::print(&format!("number_non-zero_entries_B {}", B.len())); Timer::print(&format!("number_non-zero_entries_C {}", C.len())); @@ -157,9 +157,9 @@ impl R1CSInstance { num_vars: usize, num_inputs: usize, ) -> (R1CSInstance, Vec, Vec) { - Timer::print(&format!("number_of_constraints {}", num_cons)); - Timer::print(&format!("number_of_variables {}", num_vars)); - Timer::print(&format!("number_of_inputs {}", num_inputs)); + Timer::print(&format!("number_of_constraints {num_cons}")); + Timer::print(&format!("number_of_variables {num_vars}")); + Timer::print(&format!("number_of_inputs {num_inputs}")); let mut csprng: OsRng = OsRng; diff --git a/src/scalar/ristretto255.rs b/src/scalar/ristretto255.rs index e8e33c8..94ce945 100755 --- a/src/scalar/ristretto255.rs +++ b/src/scalar/ristretto255.rs @@ -10,7 +10,7 @@ use core::convert::TryFrom; use core::fmt; use core::iter::{Product, Sum}; use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use rand_core::{CryptoRng, RngCore}; +use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; use zeroize::Zeroize; @@ -813,7 +813,7 @@ mod tests { ); assert_eq!( format!("{:?}", R2), - "0x1824b159acc5056f998c4fefecbc4ff55884b7fa0003480200000001fffffffe" + "0x0ffffffffffffffffffffffffffffffec6ef5bf4737dcf70d6ec31748d98951d" ); }