diff --git a/contracts/privacy_pool/src/contract.rs b/contracts/privacy_pool/src/contract.rs index a12b3ce..887fa2d 100644 --- a/contracts/privacy_pool/src/contract.rs +++ b/contracts/privacy_pool/src/contract.rs @@ -10,8 +10,8 @@ use soroban_sdk::{contract, contractimpl, Address, BytesN, Env}; use crate::core::{admin, deposit, initialize, view, withdraw}; use crate::types::errors::Error; use crate::types::state::{ - AnalyticsSnapshot, Denomination, PerformanceMetricKind, PoolConfig, PoolId, Proof, PublicInputs, - VerifyingKey, + AnalyticsSnapshot, Denomination, PerformanceMetricKind, PoolConfig, PoolId, Proof, + PublicInputsWithSchema, VerifyingKey, }; #[contract] @@ -58,7 +58,7 @@ impl PrivacyPool { env: Env, pool_id: PoolId, proof: Proof, - pub_inputs: PublicInputs, + pub_inputs: PublicInputsWithSchema, ) -> Result { withdraw::execute(env, pool_id, proof, pub_inputs) } diff --git a/contracts/privacy_pool/src/core/withdraw.rs b/contracts/privacy_pool/src/core/withdraw.rs index b452979..6669c56 100644 --- a/contracts/privacy_pool/src/core/withdraw.rs +++ b/contracts/privacy_pool/src/core/withdraw.rs @@ -8,29 +8,40 @@ use crate::crypto::verifier; use crate::storage::{analytics, config, nullifier}; use crate::types::errors::Error; use crate::types::events::emit_withdraw; -use crate::types::state::{PoolId, Proof, PublicInputs}; +use crate::types::state::{PoolId, Proof, PublicInputsWithSchema, EXPECTED_WITHDRAW_SCHEMA_VERSION}; use crate::utils::{address_decoder, validation}; +const EXPECTED_CIRCUIT_VERSION: &str = "1.0.0"; +const EXPECTED_MANIFEST_ID: &[u8; 32] = &[0; 32]; + + /// Execute a withdrawal from a specific shielded pool using a ZK proof. pub fn execute( env: Env, pool_id: PoolId, proof: Proof, - pub_inputs: PublicInputs, + pub_inputs_with_schema: PublicInputsWithSchema, ) -> Result { // Load and validate pool configuration let pool_config = config::load_pool_config(&env, &pool_id)?; validation::require_not_paused(&pool_config)?; let denomination_amount = pool_config.denomination.amount(); + let pub_inputs = pub_inputs_with_schema.inputs; + + // Step 1: Validate schema version + verifier::validate_schema_version( + &pub_inputs_with_schema.schema_version, + EXPECTED_WITHDRAW_SCHEMA_VERSION, + )?; - // Step 1: Validate root is in pool history + // Step 2: Validate root is in pool history validation::require_known_root(&env, &pool_id, &pub_inputs.root)?; - // Step 2: Check nullifier not already spent in this pool + // Step 3: Check nullifier not already spent in this pool validation::require_nullifier_unspent(&env, &pool_id, &pub_inputs.nullifier_hash)?; - // Step 2.5: Validate pool-id and denomination binding + // Step 3.5: Validate pool-id and denomination binding if pub_inputs.pool_id != pool_id.0 { return Err(Error::InvalidPoolId); } @@ -38,12 +49,14 @@ pub fn execute( return Err(Error::InvalidDenomination); } - // Step 3: Validate and decode fee + // Step 4: Validate and decode fee let fee = validation::decode_and_validate_fee(&pub_inputs.fee, denomination_amount)?; - // Step 4: Verify Groth16 proof for this pool + // Step 5: Verify Groth16 proof for this pool let vk = config::load_verifying_key(&env, &pool_id)?; - let proof_valid = verifier::verify_proof(&env, &vk, &proof, &pub_inputs)?; + let manifest_id_bytes = BytesN::from_array(&env, EXPECTED_MANIFEST_ID); + let proof_valid = + verifier::verify_proof(&env, &vk, &proof, &pub_inputs, EXPECTED_CIRCUIT_VERSION, &manifest_id_bytes)?; if !proof_valid { return Err(Error::InvalidProof); } diff --git a/contracts/privacy_pool/src/crypto/verifier.rs b/contracts/privacy_pool/src/crypto/verifier.rs index e58c7c4..f5d6778 100644 --- a/contracts/privacy_pool/src/crypto/verifier.rs +++ b/contracts/privacy_pool/src/crypto/verifier.rs @@ -91,7 +91,11 @@ pub fn verify_proof( vk: &VerifyingKey, proof: &Proof, pub_inputs: &PublicInputs, + expected_circuit_version: &str, + expected_manifest_id: &BytesN<32>, ) -> Result { + // Step 0: Validate VK identity + validate_vk_identity(vk, expected_circuit_version, expected_manifest_id)?; let bn254 = env.crypto().bn254(); // Step 1: Compute vk_x (linear combination of public inputs) @@ -175,3 +179,33 @@ pub fn validate_schema_version( Ok(()) } +// ────────────────────────────────────────────────────────────── +// Verifying Key Identity Validation +// ────────────────────────────────────────────────────────────── + +/// Validates the circuit version and manifest ID of the verifying key. +/// +/// # Arguments +/// * `vk` - The verifying key to validate +/// * `expected_circuit_version` - The expected circuit version string +/// * `expected_manifest_id` - The expected manifest ID +/// +/// # Returns +/// * `Ok(())` if the identity matches +/// * `Err(Error::CircuitVersionMismatch)` if the circuit version does not match +/// * `Err(Error::ManifestIdMismatch)` if the manifest ID does not match +pub fn validate_vk_identity( + vk: &VerifyingKey, + expected_circuit_version: &str, + expected_manifest_id: &BytesN<32>, +) -> Result<(), Error> { + let vk_circuit_version = vk.circuit_version.to_string(); + if vk_circuit_version != expected_circuit_version { + return Err(Error::CircuitVersionMismatch); + } + if vk.manifest_id != *expected_manifest_id { + return Err(Error::ManifestIdMismatch); + } + Ok(()) +} + diff --git a/contracts/privacy_pool/src/types/errors.rs b/contracts/privacy_pool/src/types/errors.rs index 5d8c359..02ac13e 100644 --- a/contracts/privacy_pool/src/types/errors.rs +++ b/contracts/privacy_pool/src/types/errors.rs @@ -77,4 +77,8 @@ pub enum Error { InvalidSchemaVersion = 80, /// Proof schema version does not match expected version SchemaVersionMismatch = 81, + /// Circuit version in the VK does not match the expected version + CircuitVersionMismatch = 82, + /// Manifest ID in the VK does not match the expected version + ManifestIdMismatch = 83, } diff --git a/contracts/privacy_pool/src/types/state.rs b/contracts/privacy_pool/src/types/state.rs index 1197364..32f8464 100644 --- a/contracts/privacy_pool/src/types/state.rs +++ b/contracts/privacy_pool/src/types/state.rs @@ -181,6 +181,10 @@ pub struct VerifyingKey { /// G1 points for public input combination: [IC_0, IC_1, ..., IC_8] /// One per public input (pool_id, root, nullifier_hash, recipient, amount, relayer, fee, denomination) + IC_0 pub gamma_abc_g1: soroban_sdk::Vec>, + /// Circuit version identifier (e.g., "1.0.0") + pub circuit_version: String, + /// Manifest identifier (sha256 of the manifest) + pub manifest_id: BytesN<32>, } // ────────────────────────────────────────────────────────────── diff --git a/sdk/src/types.ts b/sdk/src/types.ts index c69d3b6..f3ddc36 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -42,6 +42,7 @@ export interface ZkArtifactManifestCircuit { checksum?: string; // Legacy field for compatibility name: string; backend: string; + circuit_version?: string; root_depth?: number; public_input_schema?: string[]; } @@ -53,6 +54,7 @@ export interface ZkArtifactManifestBackend { } export interface ZkArtifactManifest { + manifest_id?: string; version: number | string; backend: string | ZkArtifactManifestBackend; circuits: Record;