diff --git a/contracts/privacy_pool/src/core/withdraw.rs b/contracts/privacy_pool/src/core/withdraw.rs index b452979..aad6141 100644 --- a/contracts/privacy_pool/src/core/withdraw.rs +++ b/contracts/privacy_pool/src/core/withdraw.rs @@ -18,6 +18,7 @@ pub fn execute( proof: Proof, pub_inputs: PublicInputs, ) -> Result { + let invoker_addr = env.invoker(); // Load and validate pool configuration let pool_config = config::load_pool_config(&env, &pool_id)?; validation::require_not_paused(&pool_config)?; @@ -55,6 +56,14 @@ pub fn execute( let recipient = address_decoder::decode_address(&env, &pub_inputs.recipient); let relayer_opt = address_decoder::decode_optional_relayer(&env, &pub_inputs.relayer); + // ZK-073: If no relayer is specified (zero-relayer semantics), + // enforce that the recipient must be the transaction invoker. + if relayer_opt.is_none() { + if recipient != invoker_addr { + return Err(Error::ZeroRelayerRecipientMismatch); + } + } + // Step 7: Transfer funds transfer_funds( &env, diff --git a/contracts/privacy_pool/src/crypto/batch_verifier.rs b/contracts/privacy_pool/src/crypto/batch_verifier.rs new file mode 100644 index 0000000..b91a5cf --- /dev/null +++ b/contracts/privacy_pool/src/crypto/batch_verifier.rs @@ -0,0 +1,160 @@ + +// ============================================================ +// PrivacyLayer — Batch Groth16 Verifier (BN254 via soroban-sdk v25) +// ============================================================ +// Verifies multiple Groth16 ZK proofs in a batch using multi-scalar +// multiplication (MSM) and aggregated pairing checks. +// +// This module implements a batch verification algorithm for Groth16 proofs +// over the BN254 curve, aiming for gas efficiency by reducing the number +// of expensive pairing operations. +// +// Reference: "Scalable Zero Knowledge Proofs for Distributed Ledgers" +// (Maller et al., 2019) or similar batching techniques. +// ============================================================ + +use soroban_sdk::{ + crypto::bn254::{Bn254G1Affine, Bn254G2Affine, Fr}, + BytesN, Env, Vec, +}; + +use crate::types::errors::Error; +use crate::types::state::{Proof, PublicInputs, VerifyingKey}; +use crate::crypto::verifier; + +// ────────────────────────────────────────────────────────────── +// Batch Proof Verification +// ────────────────────────────────────────────────────────────── + +/// Verifies a batch of Groth16 proofs using an aggregated pairing check. +/// +/// This function takes multiple proofs and their corresponding public inputs +/// and combines their verification into a single multi-pairing check +/// using random challenges. +/// +/// # Arguments +/// * `env` - The Soroban environment. +/// * `vk` - The common VerifyingKey for all proofs in the batch. +/// * `proofs` - A vector of Groth16 proofs. +/// * `pub_inputs_batch` - A vector of PublicInputs, one for each proof. +/// +/// # Returns +/// * `Ok(())` if all proofs in the batch are valid. +/// * `Err(Error::BatchVerificationFailed(index))` if any proof fails, +/// returning the 0-based index of the first failed proof. +/// * `Err(Error::MalformedVerifyingKey)` or other `Error` on malformed inputs. +pub fn verify_batch( + env: &Env, + vk: &VerifyingKey, + proofs: &Vec, + pub_inputs_batch: &Vec, +) -> Result<(), Error> { + if proofs.len() == 0 { + return Ok(()); // Nothing to verify + } + if proofs.len() != pub_inputs_batch.len() { + return Err(Error::BatchLengthMismatch); + } + + let bn254 = env.crypto().bn254(); + + let mut g1_points: Vec = Vec::new(env); + let mut g2_points: Vec = Vec::new(env); + + // Accumulators for the common VK terms scaled by sum of random scalars + let mut sum_r_alpha_g1 = Bn254G1Affine::identity(); + let mut sum_r_vk_x = Bn254G1Affine::identity(); + let mut sum_r_c = Bn254G1Affine::identity(); + + // Parse VK points once + let alpha_g1_vk = Bn254G1Affine::from_bytes(vk.alpha_g1.clone()); + let beta_g2_vk = Bn254G2Affine::from_bytes(vk.beta_g2.clone()); + let gamma_g2_vk = Bn254G2Affine::from_bytes(vk.gamma_g2.clone()); + let delta_g2_vk = Bn254G2Affine::from_bytes(vk.delta_g2.clone()); + + // Generate random scalars for each proof + // NOTE: In a production environment, this should use a cryptographically + // secure random number generator. For this exercise, we use a simple + // deterministic approach based on environment parameters for reproducibility + // and to avoid external crate dependencies. + let mut random_scalars: Vec = Vec::new(env); + for i in 0..proofs.len() { + // Simple pseudo-randomness based on block sequence and proof index + let seed_bytes: BytesN<32> = env.crypto().sha256( + &env.bytes().add(&env.block().sequence().to_be_bytes(), &i.to_be_bytes()) + ); + random_scalars.push(Fr::from_bytes(seed_bytes)); + } + + for i in 0..proofs.len() { + let proof = proofs.get(i).ok_or(Error::BatchVerificationFailed(i as u32))?; + let pub_inputs = pub_inputs_batch.get(i).ok_or(Error::BatchVerificationFailed(i as u32))?; + let r_i = random_scalars.get(i).ok_or(Error::BatchVerificationFailed(i as u32))?; + + // 1. Compute vk_x (linear combination of public inputs for this proof) + let vk_x_i = verifier::compute_vk_x(env, vk, &pub_inputs)?; + + // 2. Parse proof points + let proof_a_i = Bn254G1Affine::from_bytes(proof.a.clone()); + let proof_b_i = Bn254G2Affine::from_bytes(proof.b.clone()); + let proof_c_i = Bn254G1Affine::from_bytes(proof.c.clone()); + + // Apply random scalar r_i + let r_i_neg_a_i = bn254.g1_mul(&-proof_a_i, &r_i); + let r_i_c_i = bn254.g1_mul(&proof_c_i, &r_i); + let r_i_vk_x_i = bn254.g1_mul(&vk_x_i, &r_i); + + // Add to the G1 and G2 point vectors for pairing_check + // Terms for e(-A_i, B_i) + g1_points.push(r_i_neg_a_i); + g2_points.push(proof_b_i); + + // Accumulate terms that pair with constant G2 points + // sum_r_alpha_g1 += r_i * alpha_g1_vk + sum_r_alpha_g1 = bn254.g1_add(&sum_r_alpha_g1, &bn254.g1_mul(&alpha_g1_vk, &r_i)); + + // sum_r_vk_x += r_i * vk_x_i + sum_r_vk_x = bn254.g1_add(&sum_r_vk_x, &r_i_vk_x_i); + + // sum_r_c += r_i * C_i + sum_r_c = bn254.g1_add(&sum_r_c, &r_i_c_i); + } + + // Add the aggregated terms to the final pairing check + // e(sum_r_alpha_g1, beta_g2_vk) + g1_points.push(sum_r_alpha_g1); + g2_points.push(beta_g2_vk); + + // e(sum_r_vk_x, gamma_g2_vk) + g1_points.push(sum_r_vk_x); + g2_points.push(gamma_g2_vk); + + // e(sum_r_c, delta_g2_vk) + g1_points.push(sum_r_c); + g2_points.push(delta_g2_vk); + + // Perform the final multi-pairing check + let result = bn254.pairing_check(g1_points, g2_points); + + if result { + Ok(()) + } else { + // If the batch check fails, we need to find the specific failing proof. + // This requires re-verifying proofs individually. + for i in 0..proofs.len() { + let proof = proofs.get(i).ok_or(Error::BatchVerificationFailed(i as u32))?; + let pub_inputs = pub_inputs_batch.get(i).ok_or(Error::BatchVerificationFailed(i as u32))?; + + let is_valid = verifier::verify_proof(env, vk, &proof, &pub_inputs)?; + if !is_valid { + return Err(Error::BatchVerificationFailed(i as u32)); + } + } + // This should theoretically not be reached if the batch check failed + // but individual checks pass. This implies an issue with the batching logic. + Err(Error::BatchVerificationFailed(u32::MAX)) // Indicate an unexpected failure + } +} + +// NOTE: The `compute_vk_x` function is needed by `batch_verifier.rs` but it's +// currently private in `verifier.rs`. We need to make it public. diff --git a/contracts/privacy_pool/src/crypto/mod.rs b/contracts/privacy_pool/src/crypto/mod.rs index 37a3791..655c20e 100644 --- a/contracts/privacy_pool/src/crypto/mod.rs +++ b/contracts/privacy_pool/src/crypto/mod.rs @@ -8,6 +8,9 @@ pub mod merkle; pub mod verifier; +pub mod batch_verifier; // Add this line #[cfg(test)] mod verifier_test; +#[cfg(test)] +mod batch_verifier_test; // Add this line for future tests diff --git a/contracts/privacy_pool/src/crypto/verifier.rs b/contracts/privacy_pool/src/crypto/verifier.rs index e58c7c4..fbf0b97 100644 --- a/contracts/privacy_pool/src/crypto/verifier.rs +++ b/contracts/privacy_pool/src/crypto/verifier.rs @@ -30,7 +30,7 @@ use crate::types::state::{Proof, PublicInputs, SchemaVersion, VerifyingKey}; /// /// This is the linear combination of public inputs with the /// verifying key IC points (Groth16 "vk_x" calculation). -fn compute_vk_x( +pub fn compute_vk_x( env: &Env, vk: &VerifyingKey, pub_inputs: &PublicInputs, diff --git a/contracts/privacy_pool/src/types/errors.rs b/contracts/privacy_pool/src/types/errors.rs index 5d8c359..237a2a3 100644 --- a/contracts/privacy_pool/src/types/errors.rs +++ b/contracts/privacy_pool/src/types/errors.rs @@ -77,4 +77,10 @@ pub enum Error { InvalidSchemaVersion = 80, /// Proof schema version does not match expected version SchemaVersionMismatch = 81, + + // ── Batch Verification ───────────────────────────── + /// The number of proofs does not match the number of public inputs in a batch + BatchLengthMismatch = 90, + /// Batch verification failed for the proof at the given index + BatchVerificationFailed(u32) = 91, } diff --git a/sdk/src/test/harness/errors.ts b/sdk/src/test/harness/errors.ts index 6316207..d4a8a76 100644 --- a/sdk/src/test/harness/errors.ts +++ b/sdk/src/test/harness/errors.ts @@ -82,9 +82,10 @@ export enum ContractErrorCode { InvalidProof = 42, FeeExceedsAmount = 43, InvalidRelayerFee = 44, - InvalidRecipient = 45, - InvalidPoolId = 46, - InvalidDenomination = 47, + ZeroRelayerRecipientMismatch = 45, + InvalidRecipient = 46, + InvalidPoolId = 47, + InvalidDenomination = 48, // Verifying Key NoVerifyingKey = 50, @@ -419,6 +420,24 @@ function classifyByErrorCode( }; // Recipient errors + case ContractErrorCode.ZeroRelayerRecipientMismatch: + return { + category: ErrorCategory.RECIPIENT_ERROR, + originalMessage: error.message, + errorCode, + actionableMessage: 'Recipient address must be the transaction invoker for zero-relayer withdrawals.', + recommendations: [ + 'When not using a relayer, ensure the withdrawal recipient is your own address', + 'Verify the recipient address matches the invoker address of the transaction', + `Recipient: ${context?.recipient?.slice(0, 16)}...`, + ], + context: { + errorCode, + recipient: context?.recipient, + relayer: context?.relayer, + }, + }; + case ContractErrorCode.InvalidRecipient: return { category: ErrorCategory.RECIPIENT_ERROR,