diff --git a/README.md b/README.md index 5548ec5..bfed32c 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ This property enables efficient proof aggregation and batch verification. See `e ## Usage ```rust -use dory_pcs::{setup, prove, verify, Transparent}; +use dory_pcs::{setup, prove, verify_transparent, Transparent}; use dory_pcs::backends::arkworks::{ BN254, G1Routines, G2Routines, ArkworksPolynomial, ArkFr, Blake2bTranscript }; @@ -139,7 +139,7 @@ fn main() -> Result<(), Box> { // 6. Verify: check that the proof is valid let evaluation = polynomial.evaluate(&point); let mut verifier_transcript = Blake2bTranscript::new(b"dory-example"); - verify::<_, BN254, G1Routines, G2Routines, _>( + verify_transparent::<_, BN254, G1Routines, G2Routines, _>( tier_2, evaluation, &point, @@ -153,6 +153,12 @@ fn main() -> Result<(), Box> { } ``` +`verify_transparent` and `verify_zk` reject proofs for the wrong mode. The older +`verify` function remains as a compatibility entry point that autodetects the +mode from the proof shape. In ZK mode, the evaluation hiding commitment is owned +by the proof; protocols that need to bind it should read `proof.y_com()` after +proving and bind that value, rather than carrying a separate commitment. + ## Examples The repository includes six comprehensive examples demonstrating different aspects of Dory: diff --git a/examples/zk_e2e.rs b/examples/zk_e2e.rs index 8e40759..bacb1d3 100644 --- a/examples/zk_e2e.rs +++ b/examples/zk_e2e.rs @@ -11,7 +11,7 @@ use dory_pcs::backends::arkworks::{ }; use dory_pcs::primitives::arithmetic::Field; use dory_pcs::primitives::poly::Polynomial; -use dory_pcs::{prove, setup, verify, ZK}; +use dory_pcs::{prove, setup, verify_zk, ZK}; fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); @@ -43,9 +43,12 @@ fn main() -> Result<(), Box> { &prover_setup, &mut prover_transcript, )?; + let _evaluation_hiding_commitment = proof + .y_com() + .expect("ZK proofs contain an evaluation hiding commitment"); let mut verifier_transcript = Blake2bTranscript::new(b"dory-zk-example"); - verify::<_, BN254, G1Routines, G2Routines, _>( + verify_zk::<_, BN254, G1Routines, G2Routines, _>( tier_2, evaluation, &point, diff --git a/src/backends/arkworks/ark_cache.rs b/src/backends/arkworks/ark_cache.rs index 9a541d3..aa0e21b 100644 --- a/src/backends/arkworks/ark_cache.rs +++ b/src/backends/arkworks/ark_cache.rs @@ -21,7 +21,22 @@ pub struct PreparedCache { pub g2_prepared: Vec<::G2Prepared>, } -static CACHE: RwLock>> = RwLock::new(None); +#[derive(Debug, Clone)] +struct CachedPrepared { + prepared: Arc, + g1_vec: Vec, + g2_vec: Vec, +} + +static CACHE: RwLock>> = RwLock::new(None); + +fn slice_starts_with(cached: &[T], requested: &[T]) -> bool { + cached.len() >= requested.len() && &cached[..requested.len()] == requested +} + +fn cache_covers(cached: &CachedPrepared, g1_vec: &[ArkG1], g2_vec: &[ArkG2]) -> bool { + slice_starts_with(&cached.g1_vec, g1_vec) && slice_starts_with(&cached.g2_vec, g2_vec) +} /// Initialize the global cache with G1 and G2 vectors. /// @@ -52,7 +67,7 @@ pub fn init_cache(g1_vec: &[ArkG1], g2_vec: &[ArkG2]) { { let read_guard = CACHE.read().unwrap(); if let Some(ref cache) = *read_guard { - if cache.g1_prepared.len() >= g1_vec.len() && cache.g2_prepared.len() >= g2_vec.len() { + if cache_covers(cache, g1_vec, g2_vec) { return; // Existing cache is large enough } } @@ -63,7 +78,7 @@ pub fn init_cache(g1_vec: &[ArkG1], g2_vec: &[ArkG2]) { // Double-check after acquiring write lock (another thread may have initialized) if let Some(ref cache) = *write_guard { - if cache.g1_prepared.len() >= g1_vec.len() && cache.g2_prepared.len() >= g2_vec.len() { + if cache_covers(cache, g1_vec, g2_vec) { return; // Another thread initialized a sufficient cache } } @@ -85,9 +100,13 @@ pub fn init_cache(g1_vec: &[ArkG1], g2_vec: &[ArkG2]) { }) .collect(); - *write_guard = Some(Arc::new(PreparedCache { - g1_prepared, - g2_prepared, + *write_guard = Some(Arc::new(CachedPrepared { + prepared: Arc::new(PreparedCache { + g1_prepared, + g2_prepared, + }), + g1_vec: g1_vec.to_vec(), + g2_vec: g2_vec.to_vec(), })); } @@ -110,7 +129,23 @@ pub fn invalidate_cache() { /// # Returns /// Arc-wrapped cache, or `None` if uninitialized. pub fn get_prepared_cache() -> Option> { - CACHE.read().unwrap().clone() + CACHE + .read() + .unwrap() + .as_ref() + .map(|cached| cached.prepared.clone()) +} + +pub(crate) fn get_prepared_cache_for_g1(g1_vec: &[ArkG1]) -> Option> { + CACHE.read().unwrap().as_ref().and_then(|cached| { + slice_starts_with(&cached.g1_vec, g1_vec).then(|| cached.prepared.clone()) + }) +} + +pub(crate) fn get_prepared_cache_for_g2(g2_vec: &[ArkG2]) -> Option> { + CACHE.read().unwrap().as_ref().and_then(|cached| { + slice_starts_with(&cached.g2_vec, g2_vec).then(|| cached.prepared.clone()) + }) } /// Check if cache is initialized. diff --git a/src/backends/arkworks/ark_group.rs b/src/backends/arkworks/ark_group.rs index ccc6a45..ec211bb 100644 --- a/src/backends/arkworks/ark_group.rs +++ b/src/backends/arkworks/ark_group.rs @@ -20,7 +20,11 @@ pub struct ArkG1(pub G1Projective); #[repr(transparent)] pub struct ArkG2(pub G2Projective); -#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, CanonicalSerialize, CanonicalDeserialize)] +/// BN254 target-group element. +/// +/// Use checked deserialization for untrusted inputs. Unchecked deserialization +/// skips zero and r-torsion validation. +#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, CanonicalSerialize)] #[repr(transparent)] pub struct ArkGT(pub Fq12); diff --git a/src/backends/arkworks/ark_pairing.rs b/src/backends/arkworks/ark_pairing.rs index c8278e5..907fb3a 100644 --- a/src/backends/arkworks/ark_pairing.rs +++ b/src/backends/arkworks/ark_pairing.rs @@ -75,7 +75,8 @@ mod pairing_helpers { #[cfg(feature = "cache")] { - if let Some(cache) = crate::backends::arkworks::ark_cache::get_prepared_cache() { + if let Some(cache) = crate::backends::arkworks::ark_cache::get_prepared_cache_for_g2(qs) + { return multi_pair_with_prepared(ps_prep, &cache.g2_prepared[..qs.len()]); } } @@ -107,7 +108,8 @@ mod pairing_helpers { #[cfg(feature = "cache")] { - if let Some(cache) = crate::backends::arkworks::ark_cache::get_prepared_cache() { + if let Some(cache) = crate::backends::arkworks::ark_cache::get_prepared_cache_for_g1(ps) + { let ps_prep: Vec<_> = ps .iter() .enumerate() @@ -189,7 +191,7 @@ mod pairing_helpers { let chunk_size = determine_chunk_size(ps.len()); #[cfg(feature = "cache")] - let cache = crate::backends::arkworks::ark_cache::get_prepared_cache(); + let cache = crate::backends::arkworks::ark_cache::get_prepared_cache_for_g2(qs); let combined = ps .par_chunks(chunk_size) @@ -253,7 +255,7 @@ mod pairing_helpers { let chunk_size = determine_chunk_size(ps.len()); #[cfg(feature = "cache")] - let cache = crate::backends::arkworks::ark_cache::get_prepared_cache(); + let cache = crate::backends::arkworks::ark_cache::get_prepared_cache_for_g1(ps); let combined = qs .par_chunks(chunk_size) diff --git a/src/backends/arkworks/ark_proof.rs b/src/backends/arkworks/ark_proof.rs index d0d8b0d..67e1631 100644 --- a/src/backends/arkworks/ark_proof.rs +++ b/src/backends/arkworks/ark_proof.rs @@ -6,6 +6,13 @@ use super::{ArkG1, ArkG2, ArkGT}; use crate::proof::DoryProof; +/// Maximum number of reduce-and-fold rounds accepted by default proof deserialization. +/// +/// This bounds allocation before a verifier setup is available. It is intentionally +/// conservative for current deployments; callers needing larger proofs should add +/// an explicitly bounded deserialization entry point tied to their setup. +pub const MAX_SERIALIZED_PROOF_ROUNDS: usize = 64; + /// Arkworks-specific Dory proof type /// /// This is a type alias for `DoryProof` specialized to arkworks group types. diff --git a/src/backends/arkworks/ark_serde.rs b/src/backends/arkworks/ark_serde.rs index 6d43998..cd184b5 100644 --- a/src/backends/arkworks/ark_serde.rs +++ b/src/backends/arkworks/ark_serde.rs @@ -2,6 +2,7 @@ use crate::backends::arkworks::{ArkFr, ArkG1, ArkG2, ArkGT}; use crate::primitives::serialization::{Compress, SerializationError, Valid, Validate}; use crate::primitives::{DoryDeserialize, DorySerialize}; +use ark_ff::{Field as ArkField, PrimeField, Zero}; use ark_serialize::{ CanonicalDeserialize, CanonicalSerialize, Compress as ArkCompress, SerializationError as ArkSerializationError, Valid as ArkValid, Validate as ArkValidate, @@ -183,11 +184,37 @@ impl DoryDeserialize for ArkG2 { } } +fn ark_gt_in_target_group(inner: &ark_bn254::Fq12) -> bool { + !inner.is_zero() && inner.pow(ark_bn254::Fr::MODULUS) == ark_bn254::Fq12::ONE +} + +fn validate_dory_gt(inner: &ark_bn254::Fq12) -> Result<(), SerializationError> { + inner + .check() + .map_err(|e| SerializationError::InvalidData(format!("{e:?}")))?; + + if ark_gt_in_target_group(inner) { + Ok(()) + } else { + Err(SerializationError::InvalidData( + "invalid BN254 target-group element".to_string(), + )) + } +} + +fn validate_ark_gt(inner: &ark_bn254::Fq12) -> Result<(), ArkSerializationError> { + inner.check()?; + + if ark_gt_in_target_group(inner) { + Ok(()) + } else { + Err(ArkSerializationError::InvalidData) + } +} + impl Valid for ArkGT { fn check(&self) -> Result<(), SerializationError> { - self.0 - .check() - .map_err(|e| SerializationError::InvalidData(format!("{e:?}"))) + validate_dory_gt(&self.0) } } @@ -231,9 +258,32 @@ impl DoryDeserialize for ArkGT { }; if matches!(validate, Validate::Yes) { - inner - .check() - .map_err(|e| SerializationError::InvalidData(format!("{e:?}")))?; + validate_dory_gt(&inner)?; + } + + Ok(ArkGT(inner)) + } +} + +impl ArkValid for ArkGT { + fn check(&self) -> Result<(), ArkSerializationError> { + validate_ark_gt(&self.0) + } +} + +impl CanonicalDeserialize for ArkGT { + fn deserialize_with_mode( + reader: R, + compress: ArkCompress, + validate: ArkValidate, + ) -> Result { + let inner = match compress { + ArkCompress::Yes => ark_bn254::Fq12::deserialize_compressed(reader)?, + ArkCompress::No => ark_bn254::Fq12::deserialize_uncompressed(reader)?, + }; + + if matches!(validate, ArkValidate::Yes) { + validate_ark_gt(&inner)?; } Ok(ArkGT(inner)) @@ -241,7 +291,28 @@ impl DoryDeserialize for ArkGT { } // Arkworks-specific Dory proof type -use super::ArkDoryProof; +use super::{ArkDoryProof, MAX_SERIALIZED_PROOF_ROUNDS}; + +fn validate_serialized_proof_shape( + num_rounds: usize, + nu: usize, + sigma: usize, +) -> Result<(), ArkSerializationError> { + let total_dimension = nu + .checked_add(sigma) + .ok_or(ArkSerializationError::InvalidData)?; + + if num_rounds > MAX_SERIALIZED_PROOF_ROUNDS + || nu > sigma + || sigma != num_rounds + || sigma >= usize::BITS as usize + || total_dimension >= usize::BITS as usize + { + return Err(ArkSerializationError::InvalidData); + } + + Ok(()) +} #[cfg(feature = "zk")] mod zk_serde { @@ -437,6 +508,9 @@ impl CanonicalDeserialize for ArkDoryProof { let num_rounds = ::deserialize_with_mode(&mut reader, compress, validate)? as usize; + if num_rounds > MAX_SERIALIZED_PROOF_ROUNDS { + return Err(ArkSerializationError::InvalidData); + } // Deserialize first messages let mut first_messages = Vec::with_capacity(num_rounds); @@ -501,6 +575,8 @@ impl CanonicalDeserialize for ArkDoryProof { ::deserialize_with_mode(&mut reader, compress, validate)? as usize; + validate_serialized_proof_shape(num_rounds, nu, sigma)?; + Ok(ArkDoryProof { vmv_message, first_messages, diff --git a/src/backends/arkworks/mod.rs b/src/backends/arkworks/mod.rs index 7664ec6..693ff67 100644 --- a/src/backends/arkworks/mod.rs +++ b/src/backends/arkworks/mod.rs @@ -16,7 +16,7 @@ pub use ark_field::ArkFr; pub use ark_group::{ArkG1, ArkG2, ArkGT, G1Routines, G2Routines}; pub use ark_pairing::BN254; pub use ark_poly::ArkworksPolynomial; -pub use ark_proof::ArkDoryProof; +pub use ark_proof::{ArkDoryProof, MAX_SERIALIZED_PROOF_ROUNDS}; pub use ark_setup::{ArkworksProverSetup, ArkworksVerifierSetup}; pub use blake2b_transcript::Blake2bTranscript; diff --git a/src/evaluation_proof.rs b/src/evaluation_proof.rs index f68366a..cc6068b 100644 --- a/src/evaluation_proof.rs +++ b/src/evaluation_proof.rs @@ -293,8 +293,8 @@ where /// # Errors /// Returns `DoryError::InvalidProof` if verification fails, or other variants /// if the input parameters are incorrect (e.g., point dimension mismatch). -#[tracing::instrument(skip_all, name = "verify_evaluation_proof")] -pub fn verify_evaluation_proof( +#[tracing::instrument(skip_all, name = "verify_evaluation_proof_with_mode")] +pub fn verify_evaluation_proof_with_mode( commitment: E::GT, evaluation: F, point: &[F], @@ -311,49 +311,23 @@ where M1: DoryRoutines, M2: DoryRoutines, T: Transcript, + Mo: Mode, { let nu = proof.nu; let sigma = proof.sigma; - if point.len() != nu + sigma { + if nu > sigma { + return Err(DoryError::InvalidProof); + } + + let point_dimension = nu.checked_add(sigma).ok_or(DoryError::InvalidProof)?; + if point.len() != point_dimension { return Err(DoryError::InvalidPointDimension { - expected: nu + sigma, + expected: point_dimension, actual: point.len(), }); } - let vmv_message = &proof.vmv_message; - transcript.append_serde(b"vmv_c", &vmv_message.c); - transcript.append_serde(b"vmv_d2", &vmv_message.d2); - transcript.append_serde(b"vmv_e1", &vmv_message.e1); - - #[cfg(feature = "zk")] - let (e2, is_zk) = match (&proof.e2, &proof.y_com) { - (Some(pe2), Some(yc)) => { - use crate::reduce_and_fold::{verify_sigma1_proof, verify_sigma2_proof}; - transcript.append_serde(b"vmv_e2", pe2); - transcript.append_serde(b"vmv_y_com", yc); - match (&proof.sigma1_proof, &proof.sigma2_proof) { - (Some(s1), Some(s2)) => { - verify_sigma1_proof::(pe2, yc, s1, &setup, transcript)?; - verify_sigma2_proof::( - &vmv_message.e1, - &vmv_message.d2, - s2, - &setup, - transcript, - )?; - } - _ => return Err(DoryError::InvalidProof), - } - (*pe2, true) - } - (None, None) => (setup.g2_0.scale(&evaluation), false), - _ => return Err(DoryError::InvalidProof), - }; - #[cfg(not(feature = "zk"))] - let (e2, _is_zk) = (setup.g2_0.scale(&evaluation), false); - // Folded-scalar accumulation with per-round coordinates. // num_rounds = sigma (we fold column dimensions). let num_rounds = sigma; @@ -367,6 +341,41 @@ where return Err(DoryError::InvalidProof); } + #[cfg(feature = "zk")] + { + if Mo::BLINDING { + if !proof.zk_fields_present() { + return Err(DoryError::InvalidProof); + } + } else if !proof.zk_fields_absent() { + return Err(DoryError::InvalidProof); + } + } + + let vmv_message = &proof.vmv_message; + transcript.append_serde(b"vmv_c", &vmv_message.c); + transcript.append_serde(b"vmv_d2", &vmv_message.d2); + transcript.append_serde(b"vmv_e1", &vmv_message.e1); + + #[cfg(feature = "zk")] + let e2 = if Mo::BLINDING { + use crate::reduce_and_fold::{verify_sigma1_proof, verify_sigma2_proof}; + let pe2 = proof.e2.as_ref().ok_or(DoryError::InvalidProof)?; + let yc = proof.y_com.as_ref().ok_or(DoryError::InvalidProof)?; + let s1 = proof.sigma1_proof.as_ref().ok_or(DoryError::InvalidProof)?; + let s2 = proof.sigma2_proof.as_ref().ok_or(DoryError::InvalidProof)?; + + transcript.append_serde(b"vmv_e2", pe2); + transcript.append_serde(b"vmv_y_com", yc); + verify_sigma1_proof::(pe2, yc, s1, &setup, transcript)?; + verify_sigma2_proof::(&vmv_message.e1, &vmv_message.d2, s2, &setup, transcript)?; + *pe2 + } else { + setup.g2_0.scale(&evaluation) + }; + #[cfg(not(feature = "zk"))] + let e2 = setup.g2_0.scale(&evaluation); + // s1 (right/prover): the σ column coordinates in natural order (LSB→MSB). // No padding here: the verifier folds across the σ column dimensions. // With MSB-first folding, these coordinates are only consumed after the first σ−ν rounds, @@ -417,21 +426,21 @@ where // In ZK mode: absorb scalar product proof into transcript before deriving d. #[cfg(feature = "zk")] - let zk_data = if is_zk { - if let Some(ref sp) = proof.scalar_product_proof { - for (l, v) in [ - (b"sigma_p1" as &[u8], &sp.p1), - (b"sigma_p2", &sp.p2), - (b"sigma_q", &sp.q), - (b"sigma_r", &sp.r), - ] { - transcript.append_serde(l, v); - } - let c = transcript.challenge_scalar(b"sigma_c"); - Some((sp, c)) - } else { - return Err(DoryError::InvalidProof); + let zk_data = if Mo::BLINDING { + let sp = proof + .scalar_product_proof + .as_ref() + .ok_or(DoryError::InvalidProof)?; + for (l, v) in [ + (b"sigma_p1" as &[u8], &sp.p1), + (b"sigma_p2", &sp.p2), + (b"sigma_q", &sp.q), + (b"sigma_r", &sp.r), + ] { + transcript.append_serde(l, v); } + let c = transcript.challenge_scalar(b"sigma_c"); + Some((sp, c)) } else { None }; @@ -448,3 +457,58 @@ where verifier_state.verify_final(&proof.final_message, &gamma, &d, zk) } + +/// Verify an evaluation proof with compatibility/autodetect proof-mode handling. +/// +/// New callers should prefer [`verify_evaluation_proof_with_mode`] so the +/// expected proof mode is explicit. This function keeps the historical +/// behavior: proofs with all ZK fields present are verified in ZK mode, proofs +/// with all ZK fields absent are verified in transparent mode, and partial ZK +/// field combinations are rejected. +/// +/// # Errors +/// Returns `DoryError::InvalidProof` if the proof shape or verification checks +/// fail, or another `DoryError` if the inputs are malformed. +#[tracing::instrument(skip_all, name = "verify_evaluation_proof")] +pub fn verify_evaluation_proof( + commitment: E::GT, + evaluation: F, + point: &[F], + proof: &DoryProof, + setup: VerifierSetup, + transcript: &mut T, +) -> Result<(), DoryError> +where + F: Field, + E: PairingCurve, + E::G1: Group, + E::G2: Group, + E::GT: Group, + M1: DoryRoutines, + M2: DoryRoutines, + T: Transcript, +{ + #[cfg(feature = "zk")] + { + use crate::mode::ZK; + + if proof.zk_fields_present() { + return verify_evaluation_proof_with_mode::( + commitment, evaluation, point, proof, setup, transcript, + ); + } + if proof.zk_fields_absent() { + return verify_evaluation_proof_with_mode::( + commitment, evaluation, point, proof, setup, transcript, + ); + } + Err(DoryError::InvalidProof) + } + + #[cfg(not(feature = "zk"))] + { + verify_evaluation_proof_with_mode::( + commitment, evaluation, point, proof, setup, transcript, + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7f2e8f5..ed37ada 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,7 @@ //! ### Basic Example //! //! ```ignore -//! use dory_pcs::{setup, prove, verify, Transparent}; +//! use dory_pcs::{setup, prove, verify_transparent, Transparent}; //! use dory_pcs::backends::arkworks::{BN254, G1Routines, G2Routines, Blake2bTranscript}; //! //! // 1. Generate setup (automatically loads from/saves to disk) @@ -59,7 +59,7 @@ //! //! // 4. Verify //! let mut verifier_transcript = Blake2bTranscript::new(b"domain-separation"); -//! verify::<_, BN254, G1Routines, G2Routines, _>( +//! verify_transparent::<_, BN254, G1Routines, G2Routines, _>( //! tier_2_commitment, evaluation, &point, &proof, //! verifier_setup, &mut verifier_transcript //! )?; @@ -107,7 +107,7 @@ pub mod setup; pub mod backends; pub use error::DoryError; -pub use evaluation_proof::create_evaluation_proof; +pub use evaluation_proof::{create_evaluation_proof, verify_evaluation_proof_with_mode}; pub use messages::{ FirstReduceMessage, ScalarProductMessage, ScalarProductProof, SecondReduceMessage, VMVMessage, }; @@ -302,10 +302,102 @@ where ) } -/// Verify an evaluation proof +/// Verify an evaluation proof with an explicit expected proof mode. +/// +/// Transparent verification rejects proofs containing ZK-only fields. ZK +/// verification rejects proofs missing any required ZK fields. +/// +/// # Errors +/// Returns `DoryError::InvalidProof` if the proof mode, dimensions, transcript, +/// or proof equations are invalid. +#[tracing::instrument(skip_all, name = "verify_with_mode")] +pub fn verify_with_mode( + commitment: E::GT, + evaluation: F, + point: &[F], + proof: &DoryProof, + setup: VerifierSetup, + transcript: &mut T, +) -> Result<(), DoryError> +where + F: Field, + E: PairingCurve + Clone, + E::G1: Group, + E::G2: Group, + E::GT: Group, + M1: DoryRoutines, + M2: DoryRoutines, + T: primitives::transcript::Transcript, + Mo: Mode, +{ + evaluation_proof::verify_evaluation_proof_with_mode::( + commitment, evaluation, point, proof, setup, transcript, + ) +} + +/// Verify a transparent evaluation proof. +/// +/// # Errors +/// Returns `DoryError::InvalidProof` if the proof contains ZK-only fields or +/// fails transparent verification. +#[tracing::instrument(skip_all, name = "verify_transparent")] +pub fn verify_transparent( + commitment: E::GT, + evaluation: F, + point: &[F], + proof: &DoryProof, + setup: VerifierSetup, + transcript: &mut T, +) -> Result<(), DoryError> +where + F: Field, + E: PairingCurve + Clone, + E::G1: Group, + E::G2: Group, + E::GT: Group, + M1: DoryRoutines, + M2: DoryRoutines, + T: primitives::transcript::Transcript, +{ + verify_with_mode::( + commitment, evaluation, point, proof, setup, transcript, + ) +} + +/// Verify a zero-knowledge evaluation proof. +/// +/// # Errors +/// Returns `DoryError::InvalidProof` if any required ZK field is missing or the +/// ZK verification checks fail. +#[cfg(feature = "zk")] +#[tracing::instrument(skip_all, name = "verify_zk")] +pub fn verify_zk( + commitment: E::GT, + evaluation: F, + point: &[F], + proof: &DoryProof, + setup: VerifierSetup, + transcript: &mut T, +) -> Result<(), DoryError> +where + F: Field, + E: PairingCurve + Clone, + E::G1: Group, + E::G2: Group, + E::GT: Group, + M1: DoryRoutines, + M2: DoryRoutines, + T: primitives::transcript::Transcript, +{ + verify_with_mode::(commitment, evaluation, point, proof, setup, transcript) +} + +/// Verify an evaluation proof using compatibility/autodetect proof-mode handling. /// /// Verifies that a committed polynomial evaluates to the claimed value at the given point. /// The matrix dimensions (nu, sigma) are extracted from the proof. +/// New callers should prefer [`verify_with_mode`], [`verify_transparent`], or +/// `verify_zk` so the expected proof mode is explicit. /// /// Works with both square and non-square matrix layouts (nu ≤ sigma), and can verify /// proofs for homomorphically combined polynomials. diff --git a/src/proof.rs b/src/proof.rs index 62b339f..07ca705 100644 --- a/src/proof.rs +++ b/src/proof.rs @@ -53,3 +53,52 @@ pub struct DoryProof { #[cfg(feature = "zk")] pub scalar_product_proof: Option>, } + +impl DoryProof { + /// Returns `true` when no zero-knowledge-only proof fields are present. + pub fn is_transparent(&self) -> bool { + #[cfg(feature = "zk")] + { + self.zk_fields_absent() + } + #[cfg(not(feature = "zk"))] + { + true + } + } + + /// Returns `true` when all zero-knowledge-required proof fields are present. + #[cfg(feature = "zk")] + pub fn is_zk(&self) -> bool { + self.zk_fields_present() + } + + /// Returns the proof-owned hiding commitment to the claimed evaluation. + /// + /// Higher-level protocols that bind an evaluation hiding commitment should + /// bind this value from the proof instead of carrying an independent copy. + #[cfg(feature = "zk")] + pub fn y_com(&self) -> Option<&G1> { + self.y_com.as_ref() + } + + /// Returns `true` when every zero-knowledge-required field is present. + #[cfg(feature = "zk")] + pub fn zk_fields_present(&self) -> bool { + self.e2.is_some() + && self.y_com.is_some() + && self.sigma1_proof.is_some() + && self.sigma2_proof.is_some() + && self.scalar_product_proof.is_some() + } + + /// Returns `true` when every zero-knowledge-only field is absent. + #[cfg(feature = "zk")] + pub fn zk_fields_absent(&self) -> bool { + self.e2.is_none() + && self.y_com.is_none() + && self.sigma1_proof.is_none() + && self.sigma2_proof.is_none() + && self.scalar_product_proof.is_none() + } +} diff --git a/tests/arkworks/serialization.rs b/tests/arkworks/serialization.rs index adcae96..35029e6 100644 --- a/tests/arkworks/serialization.rs +++ b/tests/arkworks/serialization.rs @@ -1,9 +1,16 @@ //! Proof serialization round-trip tests use super::*; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use dory_pcs::backends::arkworks::ArkDoryProof; +use ark_bn254::{Fq12, Fr}; +use ark_ff::{Field as ArkField, PrimeField, Zero}; +use ark_serialize::{ + CanonicalDeserialize, CanonicalSerialize, Compress as ArkCompress, Validate as ArkValidate, +}; +use dory_pcs::backends::arkworks::{ArkDoryProof, ArkGT, MAX_SERIALIZED_PROOF_ROUNDS}; use dory_pcs::primitives::poly::Polynomial; +use dory_pcs::primitives::serialization::{ + Compress as DoryCompress, DoryDeserialize, Validate as DoryValidate, +}; use dory_pcs::{prove, verify, Transparent}; fn make_transparent_proof() -> ( @@ -47,6 +54,45 @@ fn make_transparent_proof() -> ( (proof, tier_2, point) } +fn serialized_rounds_offset(proof: &ArkDoryProof, compress: ArkCompress) -> usize { + CanonicalSerialize::serialized_size(&proof.vmv_message.c, compress) + + CanonicalSerialize::serialized_size(&proof.vmv_message.d2, compress) + + CanonicalSerialize::serialized_size(&proof.vmv_message.e1, compress) +} + +fn serialized_sigma_offset(proof: &ArkDoryProof, compress: ArkCompress) -> usize { + let u32_size = CanonicalSerialize::serialized_size(&0u32, compress); + let mut offset = serialized_rounds_offset(proof, compress) + u32_size; + + for msg in &proof.first_messages { + offset += CanonicalSerialize::serialized_size(&msg.d1_left, compress); + offset += CanonicalSerialize::serialized_size(&msg.d1_right, compress); + offset += CanonicalSerialize::serialized_size(&msg.d2_left, compress); + offset += CanonicalSerialize::serialized_size(&msg.d2_right, compress); + offset += CanonicalSerialize::serialized_size(&msg.e1_beta, compress); + offset += CanonicalSerialize::serialized_size(&msg.e2_beta, compress); + } + + for msg in &proof.second_messages { + offset += CanonicalSerialize::serialized_size(&msg.c_plus, compress); + offset += CanonicalSerialize::serialized_size(&msg.c_minus, compress); + offset += CanonicalSerialize::serialized_size(&msg.e1_plus, compress); + offset += CanonicalSerialize::serialized_size(&msg.e1_minus, compress); + offset += CanonicalSerialize::serialized_size(&msg.e2_plus, compress); + offset += CanonicalSerialize::serialized_size(&msg.e2_minus, compress); + } + + offset += CanonicalSerialize::serialized_size(&proof.final_message.e1, compress); + offset += CanonicalSerialize::serialized_size(&proof.final_message.e2, compress); + offset + u32_size +} + +fn overwrite_serialized_u32(bytes: &mut [u8], offset: usize, value: u32, compress: ArkCompress) { + let mut encoded = Vec::new(); + CanonicalSerialize::serialize_with_mode(&value, &mut encoded, compress).unwrap(); + bytes[offset..offset + encoded.len()].copy_from_slice(&encoded); +} + #[test] fn test_transparent_proof_roundtrip_compressed() { let (proof, _, _) = make_transparent_proof(); @@ -113,6 +159,117 @@ fn test_transparent_proof_roundtrip_verifies() { .unwrap(); } +#[test] +fn test_arkgt_deserialization_rejects_zero() { + let mut bytes = Vec::new(); + let zero = Fq12::zero(); + zero.serialize_compressed(&mut bytes).unwrap(); + + let dory_result = ::deserialize_with_mode( + &bytes[..], + DoryCompress::Yes, + DoryValidate::Yes, + ); + assert!( + dory_result.is_err(), + "Dory ArkGT validation must reject zero" + ); + + let ark_result = ::deserialize_with_mode( + &bytes[..], + ArkCompress::Yes, + ArkValidate::Yes, + ); + assert!( + ark_result.is_err(), + "arkworks ArkGT validation must reject zero" + ); + + let unchecked = ::deserialize_with_mode( + &bytes[..], + DoryCompress::Yes, + DoryValidate::No, + ) + .unwrap(); + assert_eq!(unchecked.0, zero); +} + +#[test] +fn test_arkgt_deserialization_rejects_non_r_torsion() { + let non_torsion = Fq12::ONE + Fq12::ONE; + assert_ne!(non_torsion.pow(Fr::MODULUS), Fq12::ONE); + + let mut bytes = Vec::new(); + non_torsion.serialize_compressed(&mut bytes).unwrap(); + + let dory_result = ::deserialize_with_mode( + &bytes[..], + DoryCompress::Yes, + DoryValidate::Yes, + ); + assert!( + dory_result.is_err(), + "Dory ArkGT validation must reject non-r-torsion elements" + ); + + let ark_result = ::deserialize_with_mode( + &bytes[..], + ArkCompress::Yes, + ArkValidate::Yes, + ); + assert!( + ark_result.is_err(), + "arkworks ArkGT validation must reject non-r-torsion elements" + ); +} + +#[test] +fn test_proof_deserialization_rejects_u32_max_rounds() { + let (proof, _, _) = make_transparent_proof(); + let compress = ArkCompress::Yes; + let mut bytes = Vec::new(); + proof.serialize_with_mode(&mut bytes, compress).unwrap(); + + let offset = serialized_rounds_offset(&proof, compress); + overwrite_serialized_u32(&mut bytes, offset, u32::MAX, compress); + + let result = ArkDoryProof::deserialize_with_mode(&bytes[..], compress, ArkValidate::Yes); + assert!(result.is_err()); +} + +#[test] +fn test_proof_deserialization_rejects_rounds_over_bound() { + let (proof, _, _) = make_transparent_proof(); + let compress = ArkCompress::Yes; + let mut bytes = Vec::new(); + proof.serialize_with_mode(&mut bytes, compress).unwrap(); + + let offset = serialized_rounds_offset(&proof, compress); + overwrite_serialized_u32( + &mut bytes, + offset, + (MAX_SERIALIZED_PROOF_ROUNDS as u32) + 1, + compress, + ); + + let result = ArkDoryProof::deserialize_with_mode(&bytes[..], compress, ArkValidate::Yes); + assert!(result.is_err()); +} + +#[test] +fn test_proof_deserialization_rejects_sigma_round_mismatch() { + let (proof, _, _) = make_transparent_proof(); + let compress = ArkCompress::Yes; + let mut bytes = Vec::new(); + proof.serialize_with_mode(&mut bytes, compress).unwrap(); + + let offset = serialized_sigma_offset(&proof, compress); + overwrite_serialized_u32(&mut bytes, offset, (proof.sigma as u32) + 1, compress); + + let result = ArkDoryProof::deserialize_with_mode(&bytes[..], compress, ArkValidate::Yes); + assert!(result.is_err()); +} + #[cfg(feature = "zk")] mod zk_roundtrip { use super::*; diff --git a/tests/arkworks/zk.rs b/tests/arkworks/zk.rs index 9915a6d..1a29614 100644 --- a/tests/arkworks/zk.rs +++ b/tests/arkworks/zk.rs @@ -5,7 +5,7 @@ use ark_bn254::{Fq12, Fr, G1Projective, G2Projective}; use ark_ff::UniformRand; use dory_pcs::backends::arkworks::{ArkFr, ArkG1, ArkG2, ArkGT}; use dory_pcs::primitives::poly::Polynomial; -use dory_pcs::{create_evaluation_proof, prove, setup, verify, ZK}; +use dory_pcs::{create_evaluation_proof, prove, setup, verify, verify_with_mode, Transparent, ZK}; #[test] fn test_zk_full_workflow() { @@ -218,6 +218,7 @@ fn test_zk_hidden_evaluation() { .unwrap(); assert!(proof.y_com.is_some(), "ZK proof should contain y_com"); + assert!(proof.y_com().is_some(), "ZK y_com accessor should be set"); assert!(proof.e2.is_some(), "ZK proof should contain e2"); let mut verifier_transcript = fresh_transcript(); @@ -237,6 +238,123 @@ fn test_zk_hidden_evaluation() { ); } +#[test] +fn test_explicit_verify_modes_accept_and_reject_cross_mode() { + let (prover_setup, verifier_setup) = test_setup_pair(6); + let poly = random_polynomial(16); + let nu = 2; + let sigma = 2; + let point = random_point(4); + + let (transparent_commitment, transparent_tier_1, transparent_blind) = poly + .commit::(nu, sigma, &prover_setup) + .unwrap(); + let mut transparent_prover_transcript = fresh_transcript(); + let (transparent_proof, _) = + prove::<_, BN254, TestG1Routines, TestG2Routines, _, _, Transparent>( + &poly, + &point, + transparent_tier_1, + transparent_blind, + nu, + sigma, + &prover_setup, + &mut transparent_prover_transcript, + ) + .unwrap(); + let evaluation = poly.evaluate(&point); + + let mut transparent_verifier_transcript = fresh_transcript(); + assert!( + verify_with_mode::<_, BN254, TestG1Routines, TestG2Routines, _, Transparent>( + transparent_commitment, + evaluation, + &point, + &transparent_proof, + verifier_setup.clone(), + &mut transparent_verifier_transcript, + ) + .is_ok() + ); + + let mut wrong_zk_transcript = fresh_transcript(); + assert!( + verify_with_mode::<_, BN254, TestG1Routines, TestG2Routines, _, ZK>( + transparent_commitment, + evaluation, + &point, + &transparent_proof, + verifier_setup.clone(), + &mut wrong_zk_transcript, + ) + .is_err(), + "ZK verification must reject transparent proofs" + ); + + let (zk_commitment, zk_tier_1, zk_blind) = poly + .commit::(nu, sigma, &prover_setup) + .unwrap(); + let mut zk_prover_transcript = fresh_transcript(); + let (zk_proof, _) = prove::<_, BN254, TestG1Routines, TestG2Routines, _, _, ZK>( + &poly, + &point, + zk_tier_1, + zk_blind, + nu, + sigma, + &prover_setup, + &mut zk_prover_transcript, + ) + .unwrap(); + + let mut zk_verifier_transcript = fresh_transcript(); + assert!( + verify_with_mode::<_, BN254, TestG1Routines, TestG2Routines, _, ZK>( + zk_commitment, + evaluation, + &point, + &zk_proof, + verifier_setup.clone(), + &mut zk_verifier_transcript, + ) + .is_ok() + ); + + let mut wrong_transparent_transcript = fresh_transcript(); + assert!( + verify_with_mode::<_, BN254, TestG1Routines, TestG2Routines, _, Transparent>( + zk_commitment, + evaluation, + &point, + &zk_proof, + verifier_setup.clone(), + &mut wrong_transparent_transcript, + ) + .is_err(), + "transparent verification must reject ZK proofs" + ); +} + +#[test] +fn test_explicit_zk_verification_rejects_partial_zk_proof() { + let (verifier_setup, point, commitment, evaluation, mut proof) = + create_valid_zk_proof_components(256, 4, 4); + + proof.scalar_product_proof = None; + + let mut verifier_transcript = fresh_transcript(); + let result = verify_with_mode::<_, BN254, TestG1Routines, TestG2Routines, _, ZK>( + commitment, + evaluation, + &point, + &proof, + verifier_setup, + &mut verifier_transcript, + ); + + assert!(result.is_err(), "ZK mode must reject partial ZK proofs"); +} + /// Test that tampered e2 in proof is rejected #[test] fn test_zk_tampered_e2_rejected() {