Skip to content

Commit

Permalink
feat: plonk verification with .bytes() (#1753)
Browse files Browse the repository at this point in the history
  • Loading branch information
yuwen01 authored Nov 6, 2024
1 parent 2d73382 commit 2c78683
Show file tree
Hide file tree
Showing 16 changed files with 120 additions and 180 deletions.
20 changes: 1 addition & 19 deletions crates/sdk/src/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,9 @@ impl SP1ProofWithPublicValues {
}
}

/// Returns the *raw* proof as bytes, prepended with the first 4 bytes of the vkey hash.
///
/// This is the format expected by the `sp1-verifier` crate. The extra 4 bytes are used to
/// ensure that the proof will eventually be verified by the correct vkey.
pub fn raw_with_checksum(&self) -> Vec<u8> {
match &self.proof {
SP1Proof::Plonk(plonk) => {
let proof_bytes = hex::decode(&plonk.raw_proof).expect("Invalid Plonk proof");
[plonk.plonk_vkey_hash[..4].to_vec(), proof_bytes].concat()
}
SP1Proof::Groth16(groth16) => {
let proof_bytes = hex::decode(&groth16.raw_proof).expect("Invalid Groth16 proof");
[groth16.groth16_vkey_hash[..4].to_vec(), proof_bytes].concat()
}
_ => unimplemented!(),
}
}

/// For Plonk or Groth16 proofs, returns the proof in a byte encoding the onchain verifier
/// accepts. The bytes consist of the first four bytes of Plonk vkey hash followed by the
/// *encoded* proof.
/// encoded proof, in a form optimized for onchain verification.
pub fn bytes(&self) -> Vec<u8> {
match &self.proof {
SP1Proof::Plonk(plonk_proof) => {
Expand Down
6 changes: 3 additions & 3 deletions crates/verifier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "sp1-verifier"
description = "Verifier for SP1 Groth16 and Plonk proofs."
readme = "../../README.md"
readme = "README.md"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
Expand All @@ -10,7 +10,7 @@ keywords = { workspace = true }
categories = { workspace = true }

[dependencies]
bn = { git = "https://github.com/sp1-patches/bn", tag = "substrate_bn-v0.6.0-patch-v2", package = "substrate-bn" }
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"] }
Expand All @@ -23,4 +23,4 @@ num-traits = "0.2.19"

[features]
default = ["std"]
std = []
std = ["thiserror-no-std/std"]
36 changes: 2 additions & 34 deletions crates/verifier/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,19 @@ use thiserror_no_std::Error;

#[derive(Error, Debug)]
pub enum Error {
// Cryptographic Errors
#[error("BSB22 Commitment number mismatch")]
Bsb22CommitmentMismatch,
#[error("Challenge already computed")]
ChallengeAlreadyComputed,
#[error("Challenge not found")]
ChallengeNotFound,
#[error("Previous challenge not computed")]
PreviousChallengeNotComputed,
#[error("Pairing check failed")]
PairingCheckFailed,
#[error("Invalid point in subgroup check")]
InvalidPoint,

// Arithmetic Errors
#[error("Beyond the modulus")]
BeyondTheModulus,
#[error("Ell too large")]
EllTooLarge,
#[error("Inverse not found")]
InverseNotFound,
#[error("Opening linear polynomial mismatch")]
OpeningPolyMismatch,

// Input Errors
#[error("DST too large")]
DSTTooLarge,
#[error("Invalid number of digests")]
InvalidNumberOfDigests,
#[error("Invalid witness")]
InvalidWitness,
#[error("Invalid x length")]
InvalidXLength,
#[error("Unexpected flag")]
UnexpectedFlag,
#[error("Invalid data")]
InvalidData,
#[error("Invalid point in subgroup check")]
InvalidPoint,

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

// External Library Errors
#[error("BN254 Field Error")]
Expand Down
2 changes: 0 additions & 2 deletions crates/verifier/src/groth16/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ pub enum Groth16Error {
ProcessVerifyingKeyFailed,
#[error("Prepare inputs failed")]
PrepareInputsFailed,
#[error("Unexpected identity")]
UnexpectedIdentity,
#[error("General error")]
GeneralError(#[from] crate::error::Error),
#[error("Groth16 vkey hash mismatch")]
Expand Down
19 changes: 10 additions & 9 deletions crates/verifier/src/groth16/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod converter;
pub(crate) mod error;
pub mod error;
mod verify;

pub(crate) use converter::{load_groth16_proof_from_bytes, load_groth16_verifying_key_from_bytes};
Expand All @@ -8,7 +8,7 @@ pub(crate) use verify::*;

use error::Groth16Error;

use crate::{bn254_public_values, decode_sp1_vkey_hash};
use crate::{bn254_public_values, decode_sp1_vkey_hash, error::Error};

/// A verifier for Groth16 zero-knowledge proofs.
#[derive(Debug)]
Expand All @@ -30,21 +30,22 @@ impl Groth16Verifier {
/// let sp1_vkey_hash = vk.bytes32();
/// ```
/// * `groth16_vk` - The Groth16 verifying key bytes.
/// Usually this will be the [`crate::GROTH16_VK_BYTES`] constant, which is the Groth16
/// Usually this will be the [`static@crate::GROTH16_VK_BYTES`] constant, which is the Groth16
/// verifying key for the current SP1 version.
///
/// # Returns
///
/// A [`Result`] containing a boolean indicating whether the proof is valid,
/// or a [`Groth16Error`] if verification fails.
/// A success [`Result`] if verification succeeds, or a [`Groth16Error`] if verification fails.
pub fn verify(
proof: &[u8],
sp1_public_inputs: &[u8],
sp1_vkey_hash: &str,
groth16_vk: &[u8],
) -> Result<bool, Groth16Error> {
) -> Result<(), Groth16Error> {
// Hash the vk and get the first 4 bytes.
let groth16_vk_hash: [u8; 4] = Sha256::digest(groth16_vk)[..4].try_into().unwrap();
let groth16_vk_hash: [u8; 4] = Sha256::digest(groth16_vk)[..4]
.try_into()
.map_err(|_| Groth16Error::GeneralError(Error::InvalidData))?;

// Check to make sure that this proof was generated by the groth16 proving key corresponding to
// the given groth16_vk.
Expand All @@ -58,8 +59,8 @@ impl Groth16Verifier {
let sp1_vkey_hash = decode_sp1_vkey_hash(sp1_vkey_hash)?;
let public_inputs = bn254_public_values(&sp1_vkey_hash, sp1_public_inputs);

let proof = load_groth16_proof_from_bytes(&proof[4..]).unwrap();
let groth16_vk = load_groth16_verifying_key_from_bytes(groth16_vk).unwrap();
let proof = load_groth16_proof_from_bytes(&proof[4..])?;
let groth16_vk = load_groth16_verifying_key_from_bytes(groth16_vk)?;

verify_groth16_raw(&groth16_vk, &proof, &public_inputs)
}
Expand Down
11 changes: 8 additions & 3 deletions crates/verifier/src/groth16/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,18 @@ pub(crate) fn verify_groth16_raw(
vk: &Groth16VerifyingKey,
proof: &Groth16Proof,
public_inputs: &[Fr],
) -> Result<bool, Groth16Error> {
) -> Result<(), Groth16Error> {
let prepared_inputs = prepare_inputs(vk.clone(), public_inputs)?;

Ok(pairing_batch(&[
if pairing_batch(&[
(-Into::<G1>::into(proof.ar), proof.bs.into()),
(prepared_inputs, vk.g2.gamma.into()),
(proof.krs.into(), vk.g2.delta.into()),
(vk.g1.alpha.into(), -Into::<G2>::into(vk.g2.beta)),
]) == Gt::one())
]) == Gt::one()
{
Ok(())
} else {
Err(Groth16Error::ProofVerificationFailed)
}
}
9 changes: 6 additions & 3 deletions crates/verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ lazy_static! {
mod constants;
mod converter;
mod error;
mod groth16;

mod utils;
pub use utils::*;

pub use groth16::error::Groth16Error;
pub use groth16::Groth16Verifier;
pub use utils::*;
mod groth16;

mod plonk;
pub use plonk::error::PlonkError;
pub use plonk::PlonkVerifier;
mod plonk;

#[cfg(test)]
mod tests;
49 changes: 27 additions & 22 deletions crates/verifier/src/plonk/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,41 +112,46 @@ pub(crate) fn load_plonk_verifying_key_from_bytes(
Ok(result)
}

pub(crate) fn load_plonk_proof_from_bytes(buffer: &[u8]) -> Result<PlonkProof, PlonkError> {
/// See https://github.com/jtguibas/gnark/blob/26e3df73fc223292be8b7fc0b7451caa4059a649/backend/plonk/bn254/solidity.go
/// for how the proof is serialized.
pub(crate) fn load_plonk_proof_from_bytes(
buffer: &[u8],
num_bsb22_commitments: usize,
) -> Result<PlonkProof, PlonkError> {
let lro0 = uncompressed_bytes_to_g1_point(&buffer[..64])?;
let lro1 = uncompressed_bytes_to_g1_point(&buffer[64..128])?;
let lro2 = uncompressed_bytes_to_g1_point(&buffer[128..192])?;
let z = uncompressed_bytes_to_g1_point(&buffer[192..256])?;
let h0 = uncompressed_bytes_to_g1_point(&buffer[256..320])?;
let h1 = uncompressed_bytes_to_g1_point(&buffer[320..384])?;
let h2 = uncompressed_bytes_to_g1_point(&buffer[384..448])?;
let batched_proof_h = uncompressed_bytes_to_g1_point(&buffer[448..512])?;

let num_claimed_values =
u32::from_be_bytes([buffer[512], buffer[513], buffer[514], buffer[515]]) as usize;

let mut claimed_values = Vec::new();
let mut offset = 516;
for _ in 0..num_claimed_values {
let h0 = uncompressed_bytes_to_g1_point(&buffer[192..256])?;
let h1 = uncompressed_bytes_to_g1_point(&buffer[256..320])?;
let h2 = uncompressed_bytes_to_g1_point(&buffer[320..384])?;

// Stores l_at_zeta, r_at_zeta, o_at_zeta, s 1_at_zeta, s2_at_zeta, bsb22_commitments
let mut claimed_values = Vec::with_capacity(5 + num_bsb22_commitments);
let mut offset = 384;
for _ in 1..6 {
let value = Fr::from_slice(&buffer[offset..offset + 32])
.map_err(|e| PlonkError::GeneralError(Error::Field(e)))?;
claimed_values.push(value);
offset += 32;
}

let z_shifted_opening_h = uncompressed_bytes_to_g1_point(&buffer[offset..offset + 64])?;
let z = uncompressed_bytes_to_g1_point(&buffer[offset..offset + 64])?;
let z_shifted_opening_value = Fr::from_slice(&buffer[offset + 64..offset + 96])
.map_err(|e| PlonkError::GeneralError(Error::Field(e)))?;
offset += 96;

let num_bsb22_commitments = u32::from_be_bytes([
buffer[offset + 96],
buffer[offset + 97],
buffer[offset + 98],
buffer[offset + 99],
]) as usize;
let batched_proof_h = uncompressed_bytes_to_g1_point(&buffer[offset..offset + 64])?;
let z_shifted_opening_h = uncompressed_bytes_to_g1_point(&buffer[offset + 64..offset + 128])?;
offset += 128;

for _ in 0..num_bsb22_commitments {
let commitment = Fr::from_slice(&buffer[offset..offset + 32])
.map_err(|e| PlonkError::GeneralError(Error::Field(e)))?;
claimed_values.push(commitment);
offset += 32;
}

let mut bsb22_commitments = Vec::new();
offset += 100;
let mut bsb22_commitments = Vec::with_capacity(num_bsb22_commitments);
for _ in 0..num_bsb22_commitments {
let commitment = uncompressed_bytes_to_g1_point(&buffer[offset..offset + 64])?;
bsb22_commitments.push(commitment);
Expand Down
16 changes: 0 additions & 16 deletions crates/verifier/src/plonk/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,18 @@ pub enum PlonkError {
DSTTooLarge,
#[error("Ell too large")]
EllTooLarge,
#[error("Failed to get Fr from random bytes")]
FailedToGetFrFromRandomBytes,
#[error("Failed to get x")]
FailedToGetX,
#[error("Failed to get y")]
FailedToGetY,
#[error("Inverse not found")]
InverseNotFound,
#[error("Invalid number of digests")]
InvalidNumberOfDigests,
#[error("Invalid point in subgroup check")]
InvalidPoint,
#[error("Invalid witness")]
InvalidWitness,
#[error("Invalid x length")]
InvalidXLength,
#[error("Opening linear polynomial mismatch")]
OpeningPolyMismatch,
#[error("Pairing check failed")]
PairingCheckFailed,
#[error("Previous challenge not computed")]
PreviousChallengeNotComputed,
#[error("Unexpected flag")]
UnexpectedFlag,
#[error("Transcript error")]
TranscriptError,
#[error("Hash to field initialization failed")]
HashToFieldInitializationFailed,
#[error("Plonk vkey hash mismatch")]
PlonkVkeyHashMismatch,
#[error("General error")]
Expand Down
18 changes: 11 additions & 7 deletions crates/verifier/src/plonk/hash_to_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use alloc::vec::Vec;
use core::hash::Hasher;
use sha2::Digest;

use crate::error::Error;
use crate::PlonkError;

pub(crate) struct WrappedHashToField {
domain: Vec<u8>,
Expand All @@ -12,18 +12,22 @@ pub(crate) struct WrappedHashToField {

impl WrappedHashToField {
// Creates a new instance with a domain separator
pub(crate) fn new(domain_separator: &[u8]) -> Result<Self, Error> {
pub(crate) fn new(domain_separator: &[u8]) -> Result<Self, PlonkError> {
Ok(Self { domain: domain_separator.to_vec(), to_hash: Vec::new() })
}

// Hashes the bytes to a field element and returns the byte representation
pub(crate) fn sum(&self) -> Result<Vec<u8>, Error> {
pub(crate) fn sum(&self) -> Result<Vec<u8>, PlonkError> {
let res = Self::hash(self.to_hash.clone(), self.domain.clone(), 1)?;

Ok(res[0].clone())
}

pub(crate) fn hash(msg: Vec<u8>, dst: Vec<u8>, count: usize) -> Result<Vec<Vec<u8>>, Error> {
pub(crate) fn hash(
msg: Vec<u8>,
dst: Vec<u8>,
count: usize,
) -> Result<Vec<Vec<u8>>, PlonkError> {
let bytes = 32;
let l = 16 + bytes;

Expand All @@ -38,16 +42,16 @@ impl WrappedHashToField {
Ok(res)
}

fn expand_msg_xmd(msg: Vec<u8>, dst: Vec<u8>, len: usize) -> Result<Vec<u8>, Error> {
fn expand_msg_xmd(msg: Vec<u8>, dst: Vec<u8>, len: usize) -> Result<Vec<u8>, PlonkError> {
let mut h = sha2::Sha256::new();

let ell = (len + 32 - 1) / 32;

if ell > 255 {
Err(Error::EllTooLarge)?;
Err(PlonkError::EllTooLarge)?;
}
if dst.len() > 255 {
Err(Error::DSTTooLarge)?;
Err(PlonkError::DSTTooLarge)?;
}

let size_domain = dst.len();
Expand Down
Loading

0 comments on commit 2c78683

Please sign in to comment.