diff --git a/Cargo.lock b/Cargo.lock index a105054669..ee03f01131 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1617,6 +1617,20 @@ name = "bytemuck" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] [[package]] name = "byteorder" @@ -6903,6 +6917,16 @@ dependencies = [ "serde", ] +[[package]] +name = "sp1-lib" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14deb700469a37ec075bcf88dac3815b026dd9c4b9cb175980826f1fbb2e4e80" +dependencies = [ + "bincode", + "serde", +] + [[package]] name = "sp1-perf" version = "3.0.0" @@ -7199,6 +7223,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "sp1-verifier" +version = "3.0.0" +dependencies = [ + "hex", + "lazy_static", + "num-bigint 0.4.6", + "num-traits", + "sha2 0.10.8", + "sp1-sdk", + "substrate-bn", + "thiserror-no-std", +] + [[package]] name = "sp1-zkvm" version = "3.0.0" @@ -7211,7 +7249,7 @@ dependencies = [ "p3-field", "rand 0.8.5", "sha2 0.10.8", - "sp1-lib", + "sp1-lib 3.0.0", "sp1-primitives", ] @@ -7354,6 +7392,22 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "substrate-bn" +version = "0.6.0" +source = "git+https://github.com/sp1-patches/bn?tag=substrate_bn-v0.6.0-patch-v2#8ef05d3969312eca34fa9f1f566a469022badda6" +dependencies = [ + "bytemuck", + "byteorder", + "cfg-if", + "crunchy", + "lazy_static", + "num-bigint 0.4.6", + "rand 0.8.5", + "rustc-hex", + "sp1-lib 3.1.0", +] + [[package]] name = "subtle" version = "2.6.1" @@ -7577,6 +7631,26 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "thiserror-impl-no-std" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e6318948b519ba6dc2b442a6d0b904ebfb8d411a3ad3e07843615a72249758" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "thiserror-no-std" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ad459d94dd517257cc96add8a43190ee620011bb6e6cdc82dafd97dfafafea" +dependencies = [ + "thiserror-impl-no-std", +] + [[package]] name = "thread_local" version = "1.1.8" diff --git a/Cargo.toml b/Cargo.toml index cad031a8a7..144da1beb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ members = [ "crates/sdk", "crates/cuda", "crates/stark", + "crates/verifier", "crates/zkvm/*", ] exclude = ["examples/target"] diff --git a/crates/verifier/Cargo.toml b/crates/verifier/Cargo.toml new file mode 100644 index 0000000000..0436c54f40 --- /dev/null +++ b/crates/verifier/Cargo.toml @@ -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"] diff --git a/crates/verifier/README.md b/crates/verifier/README.md new file mode 100644 index 0000000000..6a4042e04e --- /dev/null +++ b/crates/verifier/README.md @@ -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///_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. diff --git a/crates/verifier/bn254-vk/groth16_vk.bin b/crates/verifier/bn254-vk/groth16_vk.bin new file mode 100644 index 0000000000..348cc8a0a3 Binary files /dev/null and b/crates/verifier/bn254-vk/groth16_vk.bin differ diff --git a/crates/verifier/bn254-vk/plonk_vk.bin b/crates/verifier/bn254-vk/plonk_vk.bin new file mode 100644 index 0000000000..c8e98e6fdb Binary files /dev/null and b/crates/verifier/bn254-vk/plonk_vk.bin differ diff --git a/crates/verifier/src/constants.rs b/crates/verifier/src/constants.rs new file mode 100644 index 0000000000..7480c5a42f --- /dev/null +++ b/crates/verifier/src/constants.rs @@ -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 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 for u8 { + fn from(value: CompressedPointFlag) -> Self { + value as u8 + } +} diff --git a/crates/verifier/src/converter.rs b/crates/verifier/src/converter.rs new file mode 100644 index 0000000000..2c747ce5d4 --- /dev/null +++ b/crates/verifier/src/converter.rs @@ -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 { + 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 { + 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 { + 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 { + 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) +} diff --git a/crates/verifier/src/error.rs b/crates/verifier/src/error.rs new file mode 100644 index 0000000000..2d37bceac9 --- /dev/null +++ b/crates/verifier/src/error.rs @@ -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, +} diff --git a/crates/verifier/src/groth16/converter.rs b/crates/verifier/src/groth16/converter.rs new file mode 100644 index 0000000000..6c7a5e3b97 --- /dev/null +++ b/crates/verifier/src/groth16/converter.rs @@ -0,0 +1,53 @@ +use alloc::vec::Vec; + +use crate::{ + converter::{ + unchecked_compressed_x_to_g1_point, unchecked_compressed_x_to_g2_point, + uncompressed_bytes_to_g1_point, uncompressed_bytes_to_g2_point, + }, + groth16::{Groth16G1, Groth16G2, Groth16Proof, Groth16VerifyingKey}, +}; + +use super::error::Groth16Error; + +/// Load the Groth16 proof from the given byte slice. +/// +/// The byte slice is represented as 2 uncompressed g1 points, and one uncompressed g2 point, +/// as outputted from gnark. +pub(crate) fn load_groth16_proof_from_bytes(buffer: &[u8]) -> Result { + let ar = uncompressed_bytes_to_g1_point(&buffer[..64])?; + let bs = uncompressed_bytes_to_g2_point(&buffer[64..192])?; + let krs = uncompressed_bytes_to_g1_point(&buffer[192..256])?; + + Ok(Groth16Proof { ar, bs, krs }) +} + +/// Load the Groth16 verification key from the given byte slice. +/// +/// The gnark verification key includes a lot of extraneous information. We only extract the necessary +/// elements to verify a proof. +pub(crate) fn load_groth16_verifying_key_from_bytes( + buffer: &[u8], +) -> Result { + // We don't need to check each compressed point because the Groth16 vkey is a public constant + // that doesn't usually change. The party using the Groth16 vkey will usually clearly know + // how the vkey was generated. + let g1_alpha = unchecked_compressed_x_to_g1_point(&buffer[..32])?; + let g2_beta = unchecked_compressed_x_to_g2_point(&buffer[64..128])?; + let g2_gamma = unchecked_compressed_x_to_g2_point(&buffer[128..192])?; + let g2_delta = unchecked_compressed_x_to_g2_point(&buffer[224..288])?; + + let num_k = u32::from_be_bytes([buffer[288], buffer[289], buffer[290], buffer[291]]); + let mut k = Vec::new(); + let mut offset = 292; + for _ in 0..num_k { + let point = unchecked_compressed_x_to_g1_point(&buffer[offset..offset + 32])?; + k.push(point); + offset += 32; + } + + Ok(Groth16VerifyingKey { + g1: Groth16G1 { alpha: g1_alpha, k }, + g2: Groth16G2 { beta: -g2_beta, gamma: g2_gamma, delta: g2_delta }, + }) +} diff --git a/crates/verifier/src/groth16/error.rs b/crates/verifier/src/groth16/error.rs new file mode 100644 index 0000000000..36952cb749 --- /dev/null +++ b/crates/verifier/src/groth16/error.rs @@ -0,0 +1,15 @@ +use thiserror_no_std::Error; + +#[derive(Debug, Error)] +pub enum Groth16Error { + #[error("Proof verification failed")] + ProofVerificationFailed, + #[error("Process verifying key failed")] + ProcessVerifyingKeyFailed, + #[error("Prepare inputs failed")] + PrepareInputsFailed, + #[error("General error")] + GeneralError(#[from] crate::error::Error), + #[error("Groth16 vkey hash mismatch")] + Groth16VkeyHashMismatch, +} diff --git a/crates/verifier/src/groth16/mod.rs b/crates/verifier/src/groth16/mod.rs new file mode 100644 index 0000000000..c6cf98a23a --- /dev/null +++ b/crates/verifier/src/groth16/mod.rs @@ -0,0 +1,67 @@ +mod converter; +pub mod error; +mod verify; + +pub(crate) use converter::{load_groth16_proof_from_bytes, load_groth16_verifying_key_from_bytes}; +use sha2::{Digest, Sha256}; +pub(crate) use verify::*; + +use error::Groth16Error; + +use crate::{bn254_public_values, decode_sp1_vkey_hash, error::Error}; + +/// A verifier for Groth16 zero-knowledge proofs. +#[derive(Debug)] +pub struct Groth16Verifier; +impl Groth16Verifier { + /// Verifies a Groth16 proof. + /// + /// # Arguments + /// + /// * `proof` - The proof bytes. + /// * `public_inputs` - The SP1 public inputs. + /// * `sp1_vkey_hash` - The SP1 vkey hash. + /// This is generated in the following manner: + /// + /// ```ignore + /// use sp1_sdk::ProverClient; + /// let client = ProverClient::new(); + /// let (pk, vk) = client.setup(ELF); + /// let sp1_vkey_hash = vk.bytes32(); + /// ``` + /// * `groth16_vk` - The Groth16 verifying key bytes. + /// Usually this will be the [`static@crate::GROTH16_VK_BYTES`] constant, which is the Groth16 + /// verifying key for the current SP1 version. + /// + /// # Returns + /// + /// 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<(), Groth16Error> { + // Hash the vk and get the first 4 bytes. + 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. + // + // SP1 prepends the raw Groth16 proof with the first 4 bytes of the groth16 vkey to + // facilitate this check. + if groth16_vk_hash != proof[..4] { + return Err(Groth16Error::Groth16VkeyHashMismatch); + } + + 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..])?; + let groth16_vk = load_groth16_verifying_key_from_bytes(groth16_vk)?; + + verify_groth16_raw(&groth16_vk, &proof, &public_inputs) + } +} diff --git a/crates/verifier/src/groth16/verify.rs b/crates/verifier/src/groth16/verify.rs new file mode 100644 index 0000000000..686e62ff61 --- /dev/null +++ b/crates/verifier/src/groth16/verify.rs @@ -0,0 +1,71 @@ +use alloc::vec::Vec; +use bn::{pairing_batch, AffineG1, AffineG2, Fr, Gt, G1, G2}; + +use super::error::Groth16Error; + +/// G1 elements of the verification key. +#[derive(Clone, PartialEq)] +pub(crate) struct Groth16G1 { + pub(crate) alpha: AffineG1, + pub(crate) k: Vec, +} + +/// G2 elements of the verification key. +#[derive(Clone, PartialEq)] +pub(crate) struct Groth16G2 { + pub(crate) beta: AffineG2, + pub(crate) delta: AffineG2, + pub(crate) gamma: AffineG2, +} + +/// Verification key for the Groth16 proof. +#[derive(Clone, PartialEq)] +pub(crate) struct Groth16VerifyingKey { + pub(crate) g1: Groth16G1, + pub(crate) g2: Groth16G2, +} + +/// Proof for the Groth16 verification. +pub(crate) struct Groth16Proof { + pub(crate) ar: AffineG1, + pub(crate) krs: AffineG1, + pub(crate) bs: AffineG2, +} + +/// Prepare the inputs for the Groth16 verification by combining the public inputs with the +/// corresponding elements of the verification key. +fn prepare_inputs(vk: Groth16VerifyingKey, public_inputs: &[Fr]) -> Result { + if (public_inputs.len() + 1) != vk.g1.k.len() { + return Err(Groth16Error::PrepareInputsFailed); + } + + Ok(public_inputs + .iter() + .zip(vk.g1.k.iter().skip(1)) + .fold(vk.g1.k[0], |acc, (i, b)| acc + (*b * *i)) + .into()) +} + +/// Verify the Groth16 proof +/// +/// First, prepare the public inputs by folding them with the verification key. +/// Then, verify the proof by checking the pairing equation. +pub(crate) fn verify_groth16_raw( + vk: &Groth16VerifyingKey, + proof: &Groth16Proof, + public_inputs: &[Fr], +) -> Result<(), Groth16Error> { + let prepared_inputs = prepare_inputs(vk.clone(), public_inputs)?; + + if pairing_batch(&[ + (-Into::::into(proof.ar), proof.bs.into()), + (prepared_inputs, vk.g2.gamma.into()), + (proof.krs.into(), vk.g2.delta.into()), + (vk.g1.alpha.into(), -Into::::into(vk.g2.beta)), + ]) == Gt::one() + { + Ok(()) + } else { + Err(Groth16Error::ProofVerificationFailed) + } +} diff --git a/crates/verifier/src/lib.rs b/crates/verifier/src/lib.rs new file mode 100644 index 0000000000..228d60624f --- /dev/null +++ b/crates/verifier/src/lib.rs @@ -0,0 +1,35 @@ +//! This crate provides verifiers for SP1 Groth16 and Plonk BN254 proofs in a no-std environment. +//! It is patched for efficient verification within the SP1 ZKVM context. + +#![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + +use lazy_static::lazy_static; + +lazy_static! { + /// The PLONK verifying key for this SP1 version. + pub static ref PLONK_VK_BYTES: &'static [u8] = include_bytes!("../bn254-vk/plonk_vk.bin"); +} + +lazy_static! { + /// The Groth16 verifying key for this SP1 version. + pub static ref GROTH16_VK_BYTES: &'static [u8] = include_bytes!("../bn254-vk/groth16_vk.bin"); +} + +mod constants; +mod converter; +mod error; + +mod utils; +pub use utils::*; + +pub use groth16::error::Groth16Error; +pub use groth16::Groth16Verifier; +mod groth16; + +pub use plonk::error::PlonkError; +pub use plonk::PlonkVerifier; +mod plonk; + +#[cfg(test)] +mod tests; diff --git a/crates/verifier/src/plonk/converter.rs b/crates/verifier/src/plonk/converter.rs new file mode 100644 index 0000000000..df94303d77 --- /dev/null +++ b/crates/verifier/src/plonk/converter.rs @@ -0,0 +1,181 @@ +use crate::{ + converter::{ + unchecked_compressed_x_to_g1_point, unchecked_compressed_x_to_g2_point, + uncompressed_bytes_to_g1_point, + }, + error::Error, +}; +use alloc::vec::Vec; +use bn::{AffineG1, Fr, G2}; + +use super::{ + error::PlonkError, + kzg::{self, BatchOpeningProof, LineEvaluationAff, OpeningProof, E2}, + verify::PlonkVerifyingKey, + PlonkProof, +}; + +pub(crate) fn load_plonk_verifying_key_from_bytes( + buffer: &[u8], +) -> Result { + let size = u64::from_be_bytes([ + buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], buffer[7], + ]) as usize; + let size_inv = + Fr::from_slice(&buffer[8..40]).map_err(|e| PlonkError::GeneralError(Error::Field(e)))?; + let generator = + Fr::from_slice(&buffer[40..72]).map_err(|e| PlonkError::GeneralError(Error::Field(e)))?; + + let nb_public_variables = u64::from_be_bytes([ + buffer[72], buffer[73], buffer[74], buffer[75], buffer[76], buffer[77], buffer[78], + buffer[79], + ]) as usize; + + let coset_shift = + Fr::from_slice(&buffer[80..112]).map_err(|e| PlonkError::GeneralError(Error::Field(e)))?; + let s0 = unchecked_compressed_x_to_g1_point(&buffer[112..144])?; + let s1 = unchecked_compressed_x_to_g1_point(&buffer[144..176])?; + let s2 = unchecked_compressed_x_to_g1_point(&buffer[176..208])?; + let ql = unchecked_compressed_x_to_g1_point(&buffer[208..240])?; + let qr = unchecked_compressed_x_to_g1_point(&buffer[240..272])?; + let qm = unchecked_compressed_x_to_g1_point(&buffer[272..304])?; + let qo = unchecked_compressed_x_to_g1_point(&buffer[304..336])?; + let qk = unchecked_compressed_x_to_g1_point(&buffer[336..368])?; + let num_qcp = u32::from_be_bytes([buffer[368], buffer[369], buffer[370], buffer[371]]); + let mut qcp = Vec::new(); + let mut offset = 372; + + for _ in 0..num_qcp { + let point = unchecked_compressed_x_to_g1_point(&buffer[offset..offset + 32])?; + qcp.push(point); + offset += 32; + } + + let g1 = unchecked_compressed_x_to_g1_point(&buffer[offset..offset + 32])?; + let g2_0 = unchecked_compressed_x_to_g2_point(&buffer[offset + 32..offset + 96])?; + let g2_1 = unchecked_compressed_x_to_g2_point(&buffer[offset + 96..offset + 160])?; + + offset += 160 + 33788; + + let num_commitment_constraint_indexes = u64::from_be_bytes([ + buffer[offset], + buffer[offset + 1], + buffer[offset + 2], + buffer[offset + 3], + buffer[offset + 4], + buffer[offset + 5], + buffer[offset + 6], + buffer[offset + 7], + ]) as usize; + + let mut commitment_constraint_indexes = Vec::new(); + offset += 8; + for _ in 0..num_commitment_constraint_indexes { + let index = u64::from_be_bytes([ + buffer[offset], + buffer[offset + 1], + buffer[offset + 2], + buffer[offset + 3], + buffer[offset + 4], + buffer[offset + 5], + buffer[offset + 6], + buffer[offset + 7], + ]) as usize; + commitment_constraint_indexes.push(index); + offset += 8; + } + + let result = PlonkVerifyingKey { + size, + size_inv, + generator, + nb_public_variables, + kzg: kzg::KZGVerifyingKey { + g2: [G2::from(g2_0), G2::from(g2_1)], + g1: g1.into(), + lines: [[[LineEvaluationAff { + r0: E2 { a0: Fr::zero(), a1: Fr::zero() }, + r1: E2 { a0: Fr::zero(), a1: Fr::zero() }, + }; 66]; 2]; 2], + }, + coset_shift, + s: [s0, s1, s2], + ql, + qr, + qm, + qo, + qk, + qcp, + commitment_constraint_indexes, + }; + + Ok(result) +} + +/// 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 { + 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 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 = 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 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::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); + offset += 64; + } + + let result = PlonkProof { + lro: [lro0, lro1, lro2], + z, + h: [h0, h1, h2], + bsb22_commitments, + batched_proof: BatchOpeningProof { h: batched_proof_h, claimed_values }, + z_shifted_opening: OpeningProof { + h: z_shifted_opening_h, + claimed_value: z_shifted_opening_value, + }, + }; + + Ok(result) +} + +pub(crate) fn g1_to_bytes(g1: &AffineG1) -> Result, PlonkError> { + let mut bytes: [u8; 64] = unsafe { core::mem::transmute(*g1) }; + bytes[..32].reverse(); + bytes[32..].reverse(); + Ok(bytes.to_vec()) +} diff --git a/crates/verifier/src/plonk/error.rs b/crates/verifier/src/plonk/error.rs new file mode 100644 index 0000000000..1d33e503d6 --- /dev/null +++ b/crates/verifier/src/plonk/error.rs @@ -0,0 +1,33 @@ +use thiserror_no_std::Error; + +#[derive(Error, Debug)] +pub enum PlonkError { + #[error("Beyond the modulus")] + BeyondTheModulus, + #[error("BSB22 Commitment number mismatch")] + Bsb22CommitmentMismatch, + #[error("Challenge already computed")] + ChallengeAlreadyComputed, + #[error("Challenge not found")] + ChallengeNotFound, + #[error("DST too large")] + DSTTooLarge, + #[error("Ell too large")] + EllTooLarge, + #[error("Inverse not found")] + InverseNotFound, + #[error("Invalid number of digests")] + InvalidNumberOfDigests, + #[error("Invalid witness")] + InvalidWitness, + #[error("Pairing check failed")] + PairingCheckFailed, + #[error("Previous challenge not computed")] + PreviousChallengeNotComputed, + #[error("Transcript error")] + TranscriptError, + #[error("Plonk vkey hash mismatch")] + PlonkVkeyHashMismatch, + #[error("General error")] + GeneralError(#[from] crate::error::Error), +} diff --git a/crates/verifier/src/plonk/hash_to_field.rs b/crates/verifier/src/plonk/hash_to_field.rs new file mode 100644 index 0000000000..fb077019b1 --- /dev/null +++ b/crates/verifier/src/plonk/hash_to_field.rs @@ -0,0 +1,122 @@ +use alloc::vec; +use alloc::vec::Vec; +use core::hash::Hasher; +use sha2::Digest; + +use crate::PlonkError; + +pub(crate) struct WrappedHashToField { + domain: Vec, + to_hash: Vec, +} + +impl WrappedHashToField { + // Creates a new instance with a domain separator + pub(crate) fn new(domain_separator: &[u8]) -> Result { + 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, PlonkError> { + let res = Self::hash(self.to_hash.clone(), self.domain.clone(), 1)?; + + Ok(res[0].clone()) + } + + pub(crate) fn hash( + msg: Vec, + dst: Vec, + count: usize, + ) -> Result>, PlonkError> { + let bytes = 32; + let l = 16 + bytes; + + let len_in_bytes = count * l; + let pseudo_random_bytes = Self::expand_msg_xmd(msg, dst, len_in_bytes).unwrap(); + + let mut res = Vec::new(); + for i in 0..count { + res.push(pseudo_random_bytes[i * l..(i + 1) * l].to_vec()); + } + + Ok(res) + } + + fn expand_msg_xmd(msg: Vec, dst: Vec, len: usize) -> Result, PlonkError> { + let mut h = sha2::Sha256::new(); + + let ell = (len + 32 - 1) / 32; + + if ell > 255 { + Err(PlonkError::EllTooLarge)?; + } + if dst.len() > 255 { + Err(PlonkError::DSTTooLarge)?; + } + + let size_domain = dst.len(); + + h.reset(); + + // b_0 = H(msg_prime) + h.update([0u8; 64]); // Assuming the block size is 64 bytes for SHA-256 + h.update(&msg); + h.update([(len >> 8) as u8, len as u8, 0]); + h.update(&dst); + h.update([size_domain as u8]); + let b0 = h.finalize_reset(); + + // b_1 = H(b_0 || I2OSP(1, 1) || DST_prime) + h.update(b0); + h.update([1]); // I2OSP(1, 1) + h.update(&dst); + h.update([size_domain as u8]); + let mut b1 = h.finalize_reset(); + + let mut res = vec![0u8; len]; + res[..32].copy_from_slice(&b1); + + for i in 2..=ell { + h.reset(); + let mut strxor = vec![0u8; 32]; + for (j, (b0_byte, b1_byte)) in b0.iter().zip(b1.iter()).enumerate() { + strxor[j] = b0_byte ^ b1_byte; + } + h.update(&strxor); + h.update([i as u8]); + h.update(&dst); + h.update([size_domain as u8]); + b1 = h.finalize_reset(); + + let start = 32 * (i - 1); + let end = core::cmp::min(start + 32, res.len()); + res[start..end].copy_from_slice(&b1[..end - start]); + } + + Ok(res) + } +} + +impl Hasher for WrappedHashToField { + fn finish(&self) -> u64 { + // This method is not directly applicable to field elements, so it's a stub + unimplemented!(); + } + + fn write(&mut self, bytes: &[u8]) { + self.to_hash.extend_from_slice(bytes); + } +} + +impl Default for WrappedHashToField { + fn default() -> Self { + Self::new(&[]).unwrap() + } +} + +impl WrappedHashToField { + // Resets the state of the hasher + pub(crate) fn reset(&mut self) { + self.to_hash.clear(); + } +} diff --git a/crates/verifier/src/plonk/kzg.rs b/crates/verifier/src/plonk/kzg.rs new file mode 100644 index 0000000000..03098162ac --- /dev/null +++ b/crates/verifier/src/plonk/kzg.rs @@ -0,0 +1,193 @@ +use alloc::{string::ToString, vec, vec::Vec}; +use bn::{pairing_batch, AffineG1, Fr, G1, G2}; + +use crate::{error::Error, plonk::transcript::Transcript}; + +use super::{converter::g1_to_bytes, error::PlonkError, GAMMA, U}; + +pub(crate) type Digest = AffineG1; + +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] +pub(crate) struct E2 { + pub(crate) a0: Fr, + pub(crate) a1: Fr, +} + +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] +pub(crate) struct LineEvaluationAff { + pub(crate) r0: E2, + pub(crate) r1: E2, +} + +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] +pub(crate) struct KZGVerifyingKey { + pub(crate) g2: [G2; 2], // [G₂, [α]G₂] + pub(crate) g1: G1, + // Precomputed pairing lines corresponding to G₂, [α]G₂ + pub(crate) lines: [[[LineEvaluationAff; 66]; 2]; 2], +} + +#[derive(Clone, Debug)] +pub(crate) struct BatchOpeningProof { + pub(crate) h: AffineG1, + pub(crate) claimed_values: Vec, +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct OpeningProof { + pub(crate) h: AffineG1, + pub(crate) claimed_value: Fr, +} + +/// Derives the folding factor for the batched opening proof. +/// +/// Uses a separate transcript than the main transcript used for the other fiat shamir randomness. +fn derive_gamma( + point: &Fr, + digests: Vec, + claimed_values: Vec, + data_transcript: Option>, +) -> Result { + let mut transcript = Transcript::new(Some([GAMMA.to_string()].to_vec()))?; + transcript.bind(GAMMA, &point.into_u256().to_bytes_be())?; + + for digest in digests.iter() { + transcript.bind(GAMMA, &g1_to_bytes(digest)?)?; + } + + for claimed_value in claimed_values.iter() { + transcript.bind(GAMMA, &claimed_value.into_u256().to_bytes_be())?; + } + + if let Some(data_transcript) = data_transcript { + transcript.bind(GAMMA, &data_transcript)?; + } + + let gamma_byte = transcript.compute_challenge(GAMMA)?; + + let x = Fr::from_bytes_be_mod_order(gamma_byte.as_slice()) + .map_err(|e| PlonkError::GeneralError(Error::Field(e)))?; + + Ok(x) +} + +fn fold(di: Vec, fai: Vec, ci: Vec) -> Result<(AffineG1, Fr), PlonkError> { + let nb_digests = di.len(); + let mut folded_evaluations = Fr::zero(); + + for i in 0..nb_digests { + folded_evaluations += fai[i] * ci[i]; + } + + let folded_digests = AffineG1::msm(&di, &ci); + + Ok((folded_digests, folded_evaluations)) +} + +pub(crate) fn fold_proof( + digests: Vec, + batch_opening_proof: &BatchOpeningProof, + point: &Fr, + data_transcript: Option>, + global_transcript: &mut Transcript, +) -> Result<(OpeningProof, AffineG1), PlonkError> { + let nb_digests = digests.len(); + + if nb_digests != batch_opening_proof.claimed_values.len() { + return Err(PlonkError::InvalidNumberOfDigests); + } + + let gamma = derive_gamma( + point, + digests.clone(), + batch_opening_proof.claimed_values.clone(), + data_transcript, + )?; + + // Bind gamma to the transcript to challenge U. + global_transcript.bind(U, &gamma.into_u256().to_bytes_be())?; + + let mut gammai = vec![Fr::zero(); nb_digests]; + gammai[0] = Fr::one(); + + if nb_digests > 1 { + gammai[1] = gamma; + } + + for i in 2..nb_digests { + gammai[i] = gammai[i - 1] * gamma; + } + + let (folded_digests, folded_evaluations) = + fold(digests, batch_opening_proof.claimed_values.clone(), gammai)?; + + let open_proof = OpeningProof { h: batch_opening_proof.h, claimed_value: folded_evaluations }; + + Ok((open_proof, folded_digests)) +} + +pub(crate) fn batch_verify_multi_points( + digests: Vec, + proofs: Vec, + points: Vec, + u: Fr, + vk: &KZGVerifyingKey, +) -> Result<(), PlonkError> { + let nb_digests = digests.len(); + let nb_proofs = proofs.len(); + let nb_points = points.len(); + + if nb_digests != nb_proofs { + return Err(PlonkError::InvalidNumberOfDigests); + } + + if nb_digests != nb_points { + return Err(PlonkError::InvalidNumberOfDigests); + } + + if nb_digests == 1 { + unimplemented!(); + } + + let mut random_numbers = Vec::with_capacity(nb_digests); + random_numbers.push(Fr::one()); + for i in 1..nb_digests { + random_numbers.push(u * random_numbers[i - 1]); + } + + let mut quotients = Vec::with_capacity(nb_proofs); + for item in proofs.iter().take(nb_digests) { + quotients.push(item.h); + } + + let mut folded_quotients = AffineG1::msm("ients, &random_numbers); + let mut evals = Vec::with_capacity(nb_digests); + + for item in proofs.iter().take(nb_digests) { + evals.push(item.claimed_value); + } + + let (mut folded_digests, folded_evals) = fold(digests, evals, random_numbers.clone())?; + let folded_evals_commit = vk.g1 * folded_evals; + folded_digests = folded_digests - folded_evals_commit.into(); + + for i in 0..random_numbers.len() { + random_numbers[i] *= points[i]; + } + let folded_points_quotients = AffineG1::msm("ients, &random_numbers); + + folded_digests = folded_digests + folded_points_quotients; + folded_quotients = -folded_quotients; + + let pairing_result = + pairing_batch(&[(folded_digests.into(), vk.g2[0]), (folded_quotients.into(), vk.g2[1])]); + + if !pairing_result.is_one() { + return Err(PlonkError::PairingCheckFailed); + } + + Ok(()) +} diff --git a/crates/verifier/src/plonk/mod.rs b/crates/verifier/src/plonk/mod.rs new file mode 100644 index 0000000000..0456704f3e --- /dev/null +++ b/crates/verifier/src/plonk/mod.rs @@ -0,0 +1,76 @@ +pub(crate) const GAMMA: &str = "gamma"; +pub(crate) const BETA: &str = "beta"; +pub(crate) const ALPHA: &str = "alpha"; +pub(crate) const ZETA: &str = "zeta"; +pub(crate) const U: &str = "u"; + +mod converter; +mod hash_to_field; +mod kzg; +mod proof; +mod transcript; +mod verify; + +pub(crate) mod error; + +pub(crate) use converter::{load_plonk_proof_from_bytes, load_plonk_verifying_key_from_bytes}; +pub(crate) use proof::PlonkProof; +pub(crate) use verify::verify_plonk_raw; + +use error::PlonkError; +use sha2::{Digest, Sha256}; + +use crate::{bn254_public_values, decode_sp1_vkey_hash, error::Error}; +/// A verifier for Plonk zero-knowledge proofs. +#[derive(Debug)] +pub struct PlonkVerifier; + +impl PlonkVerifier { + /// # Arguments + /// + /// * `proof` - The proof bytes. + /// * `public_inputs` - The SP1 public inputs. + /// * `sp1_vkey_hash` - The SP1 vkey hash. + /// This is generated in the following manner: + /// + /// ```ignore + /// use sp1_sdk::ProverClient; + /// let client = ProverClient::new(); + /// let (pk, vk) = client.setup(ELF); + /// let sp1_vkey_hash = vk.bytes32(); + /// ``` + /// * `plonk_vk` - The Plonk verifying key bytes. + /// Usually this will be the [`static@crate::PLONK_VK_BYTES`] constant. + /// + /// # Returns + /// + /// A success [`Result`] if verification succeeds, or a [`PlonkError`] if verification fails. + pub fn verify( + proof: &[u8], + sp1_public_inputs: &[u8], + sp1_vkey_hash: &str, + plonk_vk: &[u8], + ) -> Result<(), PlonkError> { + // Hash the vk and get the first 4 bytes. + let plonk_vk_hash: [u8; 4] = Sha256::digest(plonk_vk)[..4] + .try_into() + .map_err(|_| PlonkError::GeneralError(Error::InvalidData))?; + + // Check to make sure that this proof was generated by the plonk proving key corresponding to + // the given plonk vk. + // + // SP1 prepends the raw Plonk proof with the first 4 bytes of the plonk vkey to + // facilitate this check. + if plonk_vk_hash != proof[..4] { + return Err(PlonkError::PlonkVkeyHashMismatch); + } + + let sp1_vkey_hash = decode_sp1_vkey_hash(sp1_vkey_hash)?; + let public_inputs = bn254_public_values(&sp1_vkey_hash, sp1_public_inputs); + + let plonk_vk = load_plonk_verifying_key_from_bytes(plonk_vk)?; + let proof = load_plonk_proof_from_bytes(&proof[4..], plonk_vk.qcp.len())?; + + verify_plonk_raw(&plonk_vk, &proof, &public_inputs) + } +} diff --git a/crates/verifier/src/plonk/proof.rs b/crates/verifier/src/plonk/proof.rs new file mode 100644 index 0000000000..850ecf00f4 --- /dev/null +++ b/crates/verifier/src/plonk/proof.rs @@ -0,0 +1,13 @@ +use alloc::vec::Vec; + +use super::kzg::{BatchOpeningProof, Digest, OpeningProof}; + +#[derive(Debug)] +pub(crate) struct PlonkProof { + pub(crate) lro: [Digest; 3], + pub(crate) z: Digest, + pub(crate) h: [Digest; 3], + pub(crate) bsb22_commitments: Vec, + pub(crate) batched_proof: BatchOpeningProof, + pub(crate) z_shifted_opening: OpeningProof, +} diff --git a/crates/verifier/src/plonk/transcript.rs b/crates/verifier/src/plonk/transcript.rs new file mode 100644 index 0000000000..55dbd99dcb --- /dev/null +++ b/crates/verifier/src/plonk/transcript.rs @@ -0,0 +1,104 @@ +use alloc::{collections::btree_map::BTreeMap, string::String, vec::Vec}; +use sha2::{Digest, Sha256}; + +use crate::PlonkError; + +/// A challenge in the transcript, derived with randomness from `bindings` and the previous +/// challenge. +#[derive(Clone, Debug)] +pub(crate) struct Challenge { + position: usize, + bindings: Vec>, + value: Vec, + is_computed: bool, +} + +/// A Fiat-Shamir transcript. +#[derive(Clone, Debug)] +pub(crate) struct Transcript { + pub(crate) h: Sha256, + + pub(crate) challenges: BTreeMap, + previous_challenge: Option, +} + +impl Transcript { + /// Creates a new transcript. + pub(crate) fn new(challenges_id: Option>) -> Result { + let h = Sha256::new(); + + if let Some(challenges_id) = challenges_id { + let mut challenges = BTreeMap::new(); + for (position, id) in challenges_id.iter().enumerate() { + challenges.insert( + id.clone(), + Challenge { + position, + bindings: Vec::new(), + value: Vec::new(), + is_computed: false, + }, + ); + } + + Ok(Transcript { h, challenges, previous_challenge: None }) + } else { + Ok(Transcript { h, challenges: BTreeMap::new(), previous_challenge: None }) + } + } + + /// Binds some data to a challenge. + pub(crate) fn bind(&mut self, id: &str, binding: &[u8]) -> Result<(), PlonkError> { + let current_challenge = self.challenges.get_mut(id).ok_or(PlonkError::ChallengeNotFound)?; + if current_challenge.is_computed { + return Err(PlonkError::ChallengeAlreadyComputed); + } + + current_challenge.bindings.push(binding.to_vec()); + + Ok(()) + } + + /// Computes a challenge and returns its value. + /// + /// Challenges must be computed in order. The previous challenge is automatically fed into the + /// challenge currently being computed. + pub(crate) fn compute_challenge(&mut self, challenge_id: &str) -> Result, PlonkError> { + let challenge = + self.challenges.get_mut(challenge_id).ok_or(PlonkError::ChallengeNotFound)?; + + if challenge.is_computed { + return Ok(challenge.value.clone()); + } + + // Reset the hash function before and after computing the challenge + self.h.reset(); + + self.h.update(challenge_id.as_bytes()); + + if challenge.position != 0 { + if let Some(previous_challenge) = &self.previous_challenge { + if previous_challenge.position != challenge.position - 1 { + return Err(PlonkError::PreviousChallengeNotComputed); + } + self.h.update(&previous_challenge.value) + } else { + return Err(PlonkError::PreviousChallengeNotComputed); + } + } + + for binding in challenge.bindings.iter() { + self.h.update(binding) + } + + let res = self.h.finalize_reset(); + + challenge.value = res.to_vec(); + challenge.is_computed = true; + + // Update the previous challenge reference + self.previous_challenge = Some(challenge.clone()); + + Ok(res.to_vec()) + } +} diff --git a/crates/verifier/src/plonk/verify.rs b/crates/verifier/src/plonk/verify.rs new file mode 100644 index 0000000000..4cfbcc884f --- /dev/null +++ b/crates/verifier/src/plonk/verify.rs @@ -0,0 +1,405 @@ +use alloc::{string::ToString, vec, vec::Vec}; +use bn::{arith::U256, AffineG1, Fr}; +use core::hash::Hasher; + +use crate::{ + error::Error, + plonk::{kzg::BatchOpeningProof, transcript::Transcript}, +}; + +use super::{ + converter::g1_to_bytes, error::PlonkError, kzg, PlonkProof, ALPHA, BETA, GAMMA, U, ZETA, +}; +#[derive(Debug)] +pub(crate) struct PlonkVerifyingKey { + pub(crate) size: usize, + pub(crate) size_inv: Fr, + pub(crate) generator: Fr, + pub(crate) nb_public_variables: usize, + + pub(crate) kzg: kzg::KZGVerifyingKey, + + pub(crate) coset_shift: Fr, + + pub(crate) s: [kzg::Digest; 3], + + pub(crate) ql: kzg::Digest, + pub(crate) qr: kzg::Digest, + pub(crate) qm: kzg::Digest, + pub(crate) qo: kzg::Digest, + pub(crate) qk: kzg::Digest, + pub(crate) qcp: Vec, + + pub(crate) commitment_constraint_indexes: Vec, +} + +/// Verifies a PLONK proof +/// +/// # Arguments +/// +/// * `vk` - The verifying key +/// * `proof` - The PLONK proof +/// * `public_inputs` - The public inputs to the circuit +/// +/// # Returns +/// +/// * `Result` - Returns true if the proof is valid, or an error if verification fails +pub(crate) fn verify_plonk_raw( + vk: &PlonkVerifyingKey, + proof: &PlonkProof, + public_inputs: &[Fr], +) -> Result<(), PlonkError> { + // Check if the number of BSB22 commitments matches the number of Qcp in the verifying key + if proof.bsb22_commitments.len() != vk.qcp.len() { + return Err(PlonkError::Bsb22CommitmentMismatch); + } + + // Check if the number of public inputs matches the number of public variables in the verifying key + if public_inputs.len() != vk.nb_public_variables { + return Err(PlonkError::InvalidWitness); + } + + // Initialize the Fiat-Shamir transcript + let mut fs = Transcript::new(Some( + [GAMMA.to_string(), BETA.to_string(), ALPHA.to_string(), ZETA.to_string(), U.to_string()] + .to_vec(), + ))?; + + // Bind public data to the transcript + bind_public_data(&mut fs, GAMMA, vk, public_inputs)?; + + // Derive gamma challenge: γ + let gamma = derive_randomness( + &mut fs, + GAMMA, + Some([proof.lro[0], proof.lro[1], proof.lro[2]].to_vec()), + )?; + + // Derive beta challenge: β + let beta = derive_randomness(&mut fs, BETA, None)?; + + // Derive alpha challenge: α + let mut alpha_deps: Vec = proof.bsb22_commitments.to_vec(); + alpha_deps.push(proof.z); + let alpha = derive_randomness(&mut fs, ALPHA, Some(alpha_deps))?; + + // Derive zeta challenge (point of evaluation): ζ + let zeta = + derive_randomness(&mut fs, ZETA, Some([proof.h[0], proof.h[1], proof.h[2]].to_vec()))?; + + // Compute zh_zeta = ζⁿ - 1 + let one = Fr::one(); + let n = U256::from(vk.size as u64); + let n = + Fr::from_slice(&n.to_bytes_be()).map_err(|e| PlonkError::GeneralError(Error::Field(e)))?; + let zeta_power_n = zeta.pow(n); + let zh_zeta = zeta_power_n - one; + + // Compute Lagrange polynomial at ζ: L₁(ζ) = (ζⁿ - 1) / (n * (ζ - 1)) + let mut lagrange_one = (zeta - one).inverse().ok_or(PlonkError::InverseNotFound)?; + lagrange_one *= zh_zeta; + lagrange_one *= vk.size_inv; + + // Compute PI = ∑_{i Result<(), PlonkError> { + transcript.bind(challenge, &g1_to_bytes(&vk.s[0])?)?; + transcript.bind(challenge, &g1_to_bytes(&vk.s[1])?)?; + transcript.bind(challenge, &g1_to_bytes(&vk.s[2])?)?; + + transcript.bind(challenge, &g1_to_bytes(&vk.ql)?)?; + transcript.bind(challenge, &g1_to_bytes(&vk.qr)?)?; + transcript.bind(challenge, &g1_to_bytes(&vk.qm)?)?; + transcript.bind(challenge, &g1_to_bytes(&vk.qo)?)?; + transcript.bind(challenge, &g1_to_bytes(&vk.qk)?)?; + + for qcp in vk.qcp.iter() { + transcript.bind(challenge, &g1_to_bytes(qcp)?)?; + } + + for public_input in public_inputs.iter() { + transcript.bind(challenge, &public_input.into_u256().to_bytes_be())?; + } + + Ok(()) +} + +/// Derives the randomness from the transcript. +/// +/// If you want to include some data for a challenge that isn't an affine g1 point, use +/// [`Transcript::bind`] to bind the data to the transcript before deriving the randomness. +fn derive_randomness( + transcript: &mut Transcript, + challenge: &str, + points: Option>, +) -> Result { + if let Some(points) = points { + for point in points { + let buf = g1_to_bytes(&point)?; + transcript.bind(challenge, &buf)?; + } + } + + let b = transcript.compute_challenge(challenge)?; + let x = Fr::from_bytes_be_mod_order(b.as_slice()) + .map_err(|e| PlonkError::GeneralError(Error::Field(e)))?; + Ok(x) +} + +/// Wrapper for [`batch_inversion`]. +fn batch_invert(elements: &[Fr]) -> Result, PlonkError> { + let mut elements = elements.to_vec(); + batch_inversion(&mut elements); + Ok(elements) +} + +/// Inverts a batch of Fr elements. +fn batch_inversion(v: &mut [Fr]) { + batch_inversion_and_mul(v, &Fr::one()); +} + +/// Inverts a batch of Fr elements and multiplies them by a given coefficient. +fn batch_inversion_and_mul(v: &mut [Fr], coeff: &Fr) { + let mut prod = Vec::with_capacity(v.len()); + let mut tmp = Fr::one(); + for f in v.iter().filter(|f| !f.is_zero()) { + tmp *= *f; + prod.push(tmp); + } + + tmp = tmp.inverse().unwrap(); + + tmp *= *coeff; + + for (f, s) in v + .iter_mut() + .rev() + .filter(|f| !f.is_zero()) + .zip(prod.into_iter().rev().skip(1).chain(Some(Fr::one()))) + { + let new_tmp = tmp * *f; + *f = tmp * s; + tmp = new_tmp; + } +} diff --git a/crates/verifier/src/tests.rs b/crates/verifier/src/tests.rs new file mode 100644 index 0000000000..e99d71ebd9 --- /dev/null +++ b/crates/verifier/src/tests.rs @@ -0,0 +1,52 @@ +use sp1_sdk::{install::try_install_circuit_artifacts, SP1ProofWithPublicValues}; + +extern crate std; + +#[test] +fn test_verify_groth16() { + // Location of the serialized SP1ProofWithPublicValues. See README.md for more information. + let proof_file = "test_binaries/fibonacci-groth16.bin"; + + // Load the saved proof and extract the proof and public inputs. + let sp1_proof_with_public_values = SP1ProofWithPublicValues::load(proof_file).unwrap(); + + let proof = sp1_proof_with_public_values.bytes(); + let public_inputs = sp1_proof_with_public_values.public_values.to_vec(); + + // This vkey hash was derived by calling `vk.bytes32()` on the verifying key. + let vkey_hash = "0x00e60860c07bfc6e4c480286c0ddbb879674eb47f84b4ef041cf858b17aa0ed1"; + + crate::Groth16Verifier::verify(&proof, &public_inputs, vkey_hash, &crate::GROTH16_VK_BYTES) + .expect("Groth16 proof is invalid"); +} + +#[test] +fn test_verify_plonk() { + // Location of the serialized SP1ProofWithPublicValues. See README.md for more information. + let proof_file = "test_binaries/fibonacci-plonk.bin"; + + // Load the saved proof and extract the proof and public inputs. + let sp1_proof_with_public_values = SP1ProofWithPublicValues::load(proof_file).unwrap(); + + let proof = sp1_proof_with_public_values.bytes(); + let public_inputs = sp1_proof_with_public_values.public_values.to_vec(); + + // This vkey hash was derived by calling `vk.bytes32()` on the verifying key. + let vkey_hash = "0x00e60860c07bfc6e4c480286c0ddbb879674eb47f84b4ef041cf858b17aa0ed1"; + + crate::PlonkVerifier::verify(&proof, &public_inputs, vkey_hash, &crate::PLONK_VK_BYTES) + .expect("Plonk proof is invalid"); +} + +#[test] +fn test_vkeys() { + let groth16_path = try_install_circuit_artifacts("groth16"); + let s3_vkey_path = groth16_path.join("groth16_vk.bin"); + let s3_vkey_bytes = std::fs::read(s3_vkey_path).unwrap(); + assert_eq!(s3_vkey_bytes, *crate::GROTH16_VK_BYTES); + + let plonk_path = try_install_circuit_artifacts("plonk"); + let s3_vkey_path = plonk_path.join("plonk_vk.bin"); + let s3_vkey_bytes = std::fs::read(s3_vkey_path).unwrap(); + assert_eq!(s3_vkey_bytes, *crate::PLONK_VK_BYTES); +} diff --git a/crates/verifier/src/utils.rs b/crates/verifier/src/utils.rs new file mode 100644 index 0000000000..7c2472374f --- /dev/null +++ b/crates/verifier/src/utils.rs @@ -0,0 +1,29 @@ +use bn::Fr; +use sha2::{Digest, Sha256}; + +use crate::error::Error; + +/// Hashes the public inputs in the same format as the Plonk and Groth16 verifiers. +pub fn hash_public_inputs(public_inputs: &[u8]) -> [u8; 32] { + let mut result = Sha256::digest(public_inputs); + + // The Plonk and Groth16 verifiers operate over a 254 bit field, so we need to zero + // out the first 3 bits. The same logic happens in the SP1 Ethereum verifier contract. + result[0] &= 0x1F; + + result.into() +} + +/// Formats the sp1 vkey hash and public inputs for use in either the Plonk or Groth16 verifier. +pub fn bn254_public_values(sp1_vkey_hash: &[u8; 32], sp1_public_inputs: &[u8]) -> [Fr; 2] { + let committed_values_digest = hash_public_inputs(sp1_public_inputs); + let vkey_hash = Fr::from_slice(&sp1_vkey_hash[1..]).unwrap(); + let committed_values_digest = Fr::from_slice(&committed_values_digest).unwrap(); + [vkey_hash, committed_values_digest] +} + +/// Decodes the sp1 vkey hash from the string from a call to `vk.bytes32`. +pub fn decode_sp1_vkey_hash(sp1_vkey_hash: &str) -> Result<[u8; 32], Error> { + let bytes = hex::decode(&sp1_vkey_hash[2..]).map_err(|_| Error::InvalidProgramVkeyHash)?; + bytes.try_into().map_err(|_| Error::InvalidProgramVkeyHash) +} diff --git a/crates/verifier/test_binaries/fibonacci-groth16.bin b/crates/verifier/test_binaries/fibonacci-groth16.bin new file mode 100644 index 0000000000..72c58644c3 Binary files /dev/null and b/crates/verifier/test_binaries/fibonacci-groth16.bin differ diff --git a/crates/verifier/test_binaries/fibonacci-plonk.bin b/crates/verifier/test_binaries/fibonacci-plonk.bin new file mode 100644 index 0000000000..303c9ff278 Binary files /dev/null and b/crates/verifier/test_binaries/fibonacci-plonk.bin differ