Skip to content

Commit

Permalink
verifier crate
Browse files Browse the repository at this point in the history
  • Loading branch information
yuwen01 committed Nov 6, 2024
1 parent ff8f482 commit 7398c3b
Show file tree
Hide file tree
Showing 26 changed files with 1,774 additions and 1 deletion.
76 changes: 75 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ members = [
"crates/sdk",
"crates/cuda",
"crates/stark",
"crates/verifier",
"crates/zkvm/*",
]
exclude = ["examples/target"]
Expand Down
26 changes: 26 additions & 0 deletions crates/verifier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "sp1-verifier"
description = "Verifier for SP1 Groth16 and Plonk proofs."
readme = "README.md"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
keywords = { workspace = true }
categories = { workspace = true }

[dependencies]
bn = { git = "https://github.com/sp1-patches/bn", version = "0.6.0", tag = "substrate_bn-v0.6.0-patch-v2", package = "substrate-bn" }
sha2 = { version = "0.10.8", default-features = false }
thiserror-no-std = "2.0.2"
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
lazy_static = { version = "1.5.0", default-features = false }

[dev-dependencies]
sp1-sdk = { workspace = true }
num-bigint = "0.4.6"
num-traits = "0.2.19"

[features]
default = ["std"]
std = ["thiserror-no-std/std"]
37 changes: 37 additions & 0 deletions crates/verifier/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# SP1 Verifier

This crate provides verifiers for SP1 Groth16 and Plonk zero-knowledge proofs. These proofs are expected
to be generated using the [SP1 SDK](../sdk).

## Features

Groth16 and Plonk proof verification are supported in `no-std` environments. Verification in the
SP1 ZKVM context is patched, in order to make use of the
[bn254 precompiles](https://blog.succinct.xyz/succinctshipsprecompiles/).

### Pre-generated verification keys

Verification keys for Groth16 and Plonk are stored in the [`bn254-vk`](./bn254-vk/) directory. These
vkeys are used to verify all SP1 proofs.

These vkeys are the same as those found locally in
`~/.sp1/circuits/<circuit_name>/<version>/<circuit_name>_vk.bin`, and should be automatically
updated after every release.

## Tests

Run tests with the following command:

```sh
cargo test --package sp1-verifier
```

These tests verify the proofs in the [`test_binaries`](./test_binaries) directory. These test binaries
were generated from the fibonacci [groth16](../../examples/fibonacci/script/bin/groth16_bn254.rs) and
[plonk](../../examples/fibonacci/script/bin/plonk_bn254.rs) examples. You can reproduce these proofs
from the examples by running `cargo run --bin groth16_bn254` and `cargo run --bin plonk_bn254` from the
[`examples/fibonacci`](../../examples/fibonacci/) directory.

## Acknowledgements

Adapted from [@Bisht13's](https://github.com/Bisht13/gnark-bn254-verifier) `gnark-bn254-verifier` crate.
Binary file added crates/verifier/bn254-vk/groth16_vk.bin
Binary file not shown.
Binary file added crates/verifier/bn254-vk/plonk_vk.bin
Binary file not shown.
33 changes: 33 additions & 0 deletions crates/verifier/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/// Gnark (and arkworks) use the 2 most significant bits to encode the flag for a compressed
/// G1 point.
/// https://github.com/Consensys/gnark-crypto/blob/a7d721497f2a98b1f292886bb685fd3c5a90f930/ecc/bn254/marshal.go#L32-L42
pub(crate) const MASK: u8 = 0b11 << 6;

/// The flags for a positive, negative, or infinity compressed point.
pub(crate) const COMPRESSED_POSITIVE: u8 = 0b10 << 6;
pub(crate) const COMPRESSED_NEGATIVE: u8 = 0b11 << 6;
pub(crate) const COMPRESSED_INFINITY: u8 = 0b01 << 6;

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum CompressedPointFlag {
Positive = COMPRESSED_POSITIVE as isize,
Negative = COMPRESSED_NEGATIVE as isize,
Infinity = COMPRESSED_INFINITY as isize,
}

impl From<u8> for CompressedPointFlag {
fn from(val: u8) -> Self {
match val {
COMPRESSED_POSITIVE => CompressedPointFlag::Positive,
COMPRESSED_NEGATIVE => CompressedPointFlag::Negative,
COMPRESSED_INFINITY => CompressedPointFlag::Infinity,
_ => panic!("Invalid compressed point flag"),
}
}
}

impl From<CompressedPointFlag> for u8 {
fn from(value: CompressedPointFlag) -> Self {
value as u8
}
}
122 changes: 122 additions & 0 deletions crates/verifier/src/converter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use core::cmp::Ordering;

use bn::{AffineG1, AffineG2, Fq, Fq2};

use crate::{
constants::{CompressedPointFlag, MASK},
error::Error,
};

/// Deserializes an Fq element from a buffer.
///
/// If this Fq element is part of a compressed point, the flag that indicates the sign of the
/// y coordinate is also returned.
pub(crate) fn deserialize_with_flags(buf: &[u8]) -> Result<(Fq, CompressedPointFlag), Error> {
if buf.len() != 32 {
return Err(Error::InvalidXLength);
};

let m_data = buf[0] & MASK;
if m_data == u8::from(CompressedPointFlag::Infinity) {
// Checks if the first byte is zero after masking AND the rest of the bytes are zero.
if buf[0] & !MASK == 0 && buf[1..].iter().all(|&b| b == 0) {
return Err(Error::InvalidPoint);
}
Ok((Fq::zero(), CompressedPointFlag::Infinity))
} else {
let mut x_bytes: [u8; 32] = [0u8; 32];
x_bytes.copy_from_slice(buf);
x_bytes[0] &= !MASK;

let x = Fq::from_be_bytes_mod_order(&x_bytes).expect("Failed to convert x bytes to Fq");

Ok((x, m_data.into()))
}
}

/// Converts a compressed G1 point to an AffineG1 point.
///
/// Asserts that the compressed point is represented as a single fq element: the x coordinate
/// of the point. The y coordinate is then computed from the x coordinate. The final point
/// is not checked to be on the curve for efficiency.
pub(crate) fn unchecked_compressed_x_to_g1_point(buf: &[u8]) -> Result<AffineG1, Error> {
let (x, m_data) = deserialize_with_flags(buf)?;
let (y, neg_y) = AffineG1::get_ys_from_x_unchecked(x).ok_or(Error::InvalidPoint)?;

let mut final_y = y;
if y.cmp(&neg_y) == Ordering::Greater {
if m_data == CompressedPointFlag::Positive {
final_y = -y;
}
} else if m_data == CompressedPointFlag::Negative {
final_y = -y;
}

Ok(AffineG1::new_unchecked(x, final_y))
}

/// Converts an uncompressed G1 point to an AffineG1 point.
///
/// Asserts that the affine point is represented as two fq elements.
pub(crate) fn uncompressed_bytes_to_g1_point(buf: &[u8]) -> Result<AffineG1, Error> {
if buf.len() != 64 {
return Err(Error::InvalidXLength);
};

let (x_bytes, y_bytes) = buf.split_at(32);

let x = Fq::from_slice(x_bytes).map_err(Error::Field)?;
let y = Fq::from_slice(y_bytes).map_err(Error::Field)?;
AffineG1::new(x, y).map_err(Error::Group)
}

/// Converts a compressed G2 point to an AffineG2 point.
///
/// Asserts that the compressed point is represented as a single fq2 element: the x coordinate
/// of the point.
/// Then, gets the y coordinate from the x coordinate.
/// For efficiency, this function does not check that the final point is on the curve.
pub(crate) fn unchecked_compressed_x_to_g2_point(buf: &[u8]) -> Result<AffineG2, Error> {
if buf.len() != 64 {
return Err(Error::InvalidXLength);
};

let (x1, flag) = deserialize_with_flags(&buf[..32])?;
let x0 = Fq::from_be_bytes_mod_order(&buf[32..64]).map_err(Error::Field)?;
let x = Fq2::new(x0, x1);

if flag == CompressedPointFlag::Infinity {
return Ok(AffineG2::one());
}

let (y, neg_y) = AffineG2::get_ys_from_x_unchecked(x).ok_or(Error::InvalidPoint)?;

match flag {
CompressedPointFlag::Positive => Ok(AffineG2::new_unchecked(x, y)),
CompressedPointFlag::Negative => Ok(AffineG2::new_unchecked(x, neg_y)),
_ => Err(Error::InvalidPoint),
}
}

/// Converts an uncompressed G2 point to an AffineG2 point.
///
/// Asserts that the affine point is represented as two fq2 elements.
pub(crate) fn uncompressed_bytes_to_g2_point(buf: &[u8]) -> Result<AffineG2, Error> {
if buf.len() != 128 {
return Err(Error::InvalidXLength);
}

let (x_bytes, y_bytes) = buf.split_at(64);
let (x1_bytes, x0_bytes) = x_bytes.split_at(32);
let (y1_bytes, y0_bytes) = y_bytes.split_at(32);

let x1 = Fq::from_slice(x1_bytes).map_err(Error::Field)?;
let x0 = Fq::from_slice(x0_bytes).map_err(Error::Field)?;
let y1 = Fq::from_slice(y1_bytes).map_err(Error::Field)?;
let y0 = Fq::from_slice(y0_bytes).map_err(Error::Field)?;

let x = Fq2::new(x0, x1);
let y = Fq2::new(y0, y1);

AffineG2::new(x, y).map_err(Error::Group)
}
31 changes: 31 additions & 0 deletions crates/verifier/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use bn::{CurveError, FieldError, GroupError};
use thiserror_no_std::Error;

#[derive(Error, Debug)]
pub enum Error {
// Input Errors
#[error("Invalid witness")]
InvalidWitness,
#[error("Invalid x length")]
InvalidXLength,
#[error("Invalid data")]
InvalidData,
#[error("Invalid point in subgroup check")]
InvalidPoint,

// Conversion Errors
#[error("Failed to get Fr from random bytes")]
FailedToGetFrFromRandomBytes,

// External Library Errors
#[error("BN254 Field Error")]
Field(FieldError),
#[error("BN254 Group Error")]
Group(GroupError),
#[error("BN254 Curve Error")]
Curve(CurveError),

// SP1 Errors
#[error("Invalid program vkey hash")]
InvalidProgramVkeyHash,
}
Loading

0 comments on commit 7398c3b

Please sign in to comment.