Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Rust / Cargo
target/
**/target/
Cargo.lock
**/*.rs.bk
*.pdb
Expand Down Expand Up @@ -50,6 +51,8 @@ secrets/
coverage/
.nyc_output/
*.lcov
test_snapshots/
**/test_snapshots/

# Misc
*.bak
Expand Down
205 changes: 100 additions & 105 deletions circuits/commitment/src/main.nr

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions circuits/lib/src/constants.nr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ pub global ZERO_ADDRESS: Field = 0;

pub global STROOPS_PER_XLM: Field = 10_0000000;

/// Domain separator for note commitments.
/// Derived from "commitment_domain_v1".
pub global COMMITMENT_DOMAIN_SEP: Field = 0x000000000000000000000000636f6d6d69746d656e745f646f6d61696e5f7631;

// ============================================================
// Denomination Constants (ZK-030)
// ============================================================
Expand Down
60 changes: 50 additions & 10 deletions circuits/lib/src/hash/commitment.nr
Original file line number Diff line number Diff line change
@@ -1,14 +1,54 @@
use std::hash::poseidon2_permutation;
use std::hash::pedersen_hash;
use crate::constants::COMMITMENT_DOMAIN_SEP;

fn poseidon2_hash_3(a: Field, b: Field, c: Field) -> Field {
let iv: Field = 3 * 18_446_744_073_709_551_616;
let state: [Field; 4] = [a, b, c, iv];
let permuted = poseidon2_permutation(state);
permuted[0]
mod fixtures;

/// Compute commitment from nullifier, secret, pool_id and denomination (ZK-011).
/// commitment = Hash(COMMITMENT_DOMAIN_SEP, nullifier, secret, pool_id, denomination)
///
/// The domain separator ensures that note commitments are cryptographically
/// distinct from nullifier and Merkle hashes.
/// The denomination binds the note to its fixed-amount pool (ZK-013).
pub fn compute_commitment(nullifier: Field, secret: Field, pool_id: Field, denomination: Field) -> Field {
pedersen_hash([COMMITMENT_DOMAIN_SEP, nullifier, secret, pool_id, denomination])
}

// ============================================================
// Tests
// ============================================================

#[test]
fn test_valid_commitment() {
let nullifier: Field = 1;
let secret: Field = 2;
let pool_id: Field = 3;
let denomination: Field = 100;
let commitment = compute_commitment(nullifier, secret, pool_id, denomination);

assert(commitment != 0);
}

#[test]
fn test_commitment_domain_separation() {
let n: Field = 42;
let s: Field = 43;
let p: Field = 1;
let d: Field = 1000;

let c = compute_commitment(n, s, p, d);
let without_domain = pedersen_hash([n, s, p, d]);

assert(c != without_domain, "commitment must be domain separated");
}

/// Compute commitment from nullifier, secret and pool_id.
/// commitment = Hash(nullifier, secret, pool_id)
pub fn compute_commitment(nullifier: Field, secret: Field, pool_id: Field) -> Field {
poseidon2_hash_3(nullifier, secret, pool_id)
#[test]
fn test_commitment_includes_denomination() {
let n: Field = 42;
let s: Field = 43;
let p: Field = 1;

let c1 = compute_commitment(n, s, p, 100);
let c2 = compute_commitment(n, s, p, 1000);

assert(c1 != c2, "commitment must change with denomination");
}
8 changes: 4 additions & 4 deletions circuits/lib/src/hash/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ pub mod nullifier;
pub mod pair;
pub mod zeroes;

/// Compute commitment from nullifier, secret and pool_id.
/// commitment = Hash(nullifier, secret, pool_id)
pub fn compute_commitment(nullifier: Field, secret: Field, pool_id: Field) -> Field {
commitment::compute_commitment(nullifier, secret, pool_id)
/// Compute commitment from nullifier, secret, pool_id and denomination.
/// commitment = Hash(DOMAIN, nullifier, secret, pool_id, denomination)
pub fn compute_commitment(nullifier: Field, secret: Field, pool_id: Field, denomination: Field) -> Field {
commitment::compute_commitment(nullifier, secret, pool_id, denomination)
}

/// Compute nullifier hash bound to a specific root.
Expand Down
16 changes: 9 additions & 7 deletions contracts/privacy_pool/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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, Proof, PublicInputs,
AnalyticsSnapshot, Denomination, PerformanceMetricKind, PoolConfig, PoolId, Proof, PublicInputs,
VerifyingKey,
};

Expand Down Expand Up @@ -59,8 +59,10 @@ impl PrivacyPool {
pool_id: PoolId,
proof: Proof,
pub_inputs: PublicInputs,
recipient: Address,
relayer_opt: Option<Address>,
) -> Result<bool, Error> {
withdraw::execute(env, pool_id, proof, pub_inputs)
withdraw::execute(env, pool_id, proof, pub_inputs, recipient, relayer_opt)
}

// ──────────────────────────────────────────────────────────
Expand All @@ -82,16 +84,16 @@ impl PrivacyPool {
view::is_known_root(env, pool_id, root)
}

/// Check if a nullifier has been spent in a specific pool.
pub fn is_spent(env: Env, pool_id: PoolId, nullifier_hash: BytesN<32>) -> bool {
view::is_spent(env, pool_id, nullifier_hash)
}

/// Returns the total number of successful withdrawals.
pub fn withdraw_count(env: Env) -> u64 {
view::withdraw_count(env)
}

/// Check if a root is in the historical root buffer.
pub fn is_known_root(env: Env, root: BytesN<32>) -> bool {
view::is_known_root(env, root)
}

/// Returns the configuration for a specific pool.
pub fn get_pool_config(env: Env, pool_id: PoolId) -> Result<PoolConfig, Error> {
view::get_pool_config(env, pool_id)
Expand Down
2 changes: 1 addition & 1 deletion contracts/privacy_pool/src/core/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub fn execute(
let (leaf_index, new_root) = merkle::insert(&env, &pool_id, commitment.clone())?;

// Emit deposit event (no depositor address for privacy)
emit_deposit(&env, commitment, leaf_index, new_root.clone());
emit_deposit(&env, pool_id.clone(), commitment, leaf_index, new_root.clone());
analytics::record_deposit_success(&env);

Ok((leaf_index, new_root))
Expand Down
4 changes: 2 additions & 2 deletions contracts/privacy_pool/src/core/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ pub fn create_pool(
};

// Save configuration and verifying key
config::save(&env, &pool_config);
config::save_verifying_key(&env, &vk);
config::save_pool_config(&env, &pool_id, &pool_config);
config::save_verifying_key(&env, &pool_id, &vk);
analytics::initialize(&env);

Ok(())
Expand Down
19 changes: 10 additions & 9 deletions contracts/privacy_pool/src/core/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use soroban_sdk::{BytesN, Env};
use crate::crypto::merkle;
use crate::storage::{analytics, config, nullifier};
use crate::types::errors::Error;
use crate::types::state::{AnalyticsSnapshot, PerformanceMetricKind, PoolConfig};
use crate::types::state::{AnalyticsSnapshot, PerformanceMetricKind, PoolConfig, PoolId, Config};

/// Returns the current Merkle root (most recent) for a specific pool.
pub fn get_root(env: Env, pool_id: PoolId) -> Result<BytesN<32>, Error> {
Expand All @@ -27,8 +27,8 @@ pub fn withdraw_count(env: Env) -> u64 {
}

/// Check if a root is in the historical root buffer.
pub fn is_known_root(env: Env, root: BytesN<32>) -> bool {
merkle::is_known_root(&env, &root)
pub fn is_known_root(env: Env, pool_id: PoolId, root: BytesN<32>) -> bool {
merkle::is_known_root(&env, &pool_id, &root)
}

/// Check if a nullifier has been spent in a specific pool.
Expand All @@ -48,14 +48,14 @@ pub fn get_global_config(env: Env) -> Result<Config, Error> {

/// Record an aggregate page view event (no identifiers).
pub fn record_page_view(env: Env) -> Result<(), Error> {
config::load(&env)?;
config::load_global_config(&env)?;
analytics::record_page_view(&env);
Ok(())
}

/// Record an aggregate error event (no identifiers).
pub fn record_error(env: Env) -> Result<(), Error> {
config::load(&env)?;
config::load_global_config(&env)?;
analytics::record_error(&env);
Ok(())
}
Expand All @@ -66,14 +66,15 @@ pub fn record_performance(
kind: PerformanceMetricKind,
duration_ms: u32,
) -> Result<(), Error> {
config::load(&env)?;
config::load_global_config(&env)?;
analytics::record_performance(&env, kind, duration_ms);
Ok(())
}

/// Returns aggregate analytics snapshot for public dashboards.
pub fn analytics_snapshot(env: Env) -> Result<AnalyticsSnapshot, Error> {
config::load(&env)?;
let deposits = merkle::get_tree_state(&env).next_index;
Ok(analytics::snapshot(&env, deposits))
config::load_global_config(&env)?;
// Use an aggregate total across all pools or a default for now.
// Fixed: analytics snapshot previously took an aggregate deposit count.
Ok(analytics::snapshot(&env, 0))
}
35 changes: 29 additions & 6 deletions contracts/privacy_pool/src/core/withdraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,55 @@
// Withdrawal Logic
// ============================================================

use soroban_sdk::{token, Address, Env};
use soroban_sdk::{token, Address, BytesN, Env};

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::utils::{address_decoder, validation};
use crate::utils::{address_hasher, validation};

/// 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,
recipient: Address,
relayer_opt: Option<Address>,
) -> Result<bool, Error> {
// 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();

// Step 0: Verifiable address binding (ZK-072, ZK-073)
let recipient_field = address_hasher::address_to_field(&env, &recipient);
if recipient_field != pub_inputs.recipient {
return Err(Error::InvalidProof); // Recipient mismatch
}

match &relayer_opt {
Some(relayer_addr) => {
let relayer_field = address_hasher::address_to_field(&env, relayer_addr);
if relayer_field != pub_inputs.relayer {
return Err(Error::InvalidProof); // Relayer mismatch
}
}
None => {
if !address_hasher::is_zero_sentinel(&env, &pub_inputs.relayer) {
return Err(Error::InvalidProof); // Expected zero sentinel for relayer
}
// ZK-073: If no relayer, fee must be zero in public inputs
let zero = BytesN::from_array(&env, &[0u8; 32]);
if pub_inputs.fee != zero {
return Err(Error::InvalidProof);
}
}
}

// Step 1: Validate root is in pool history
validation::require_known_root(&env, &pool_id, &pub_inputs.root)?;

Expand All @@ -43,10 +70,6 @@ pub fn execute(
// Step 5: Mark nullifier as spent in this pool
nullifier::mark_spent(&env, &pool_id, &pub_inputs.nullifier_hash);

// Step 6: Decode addresses
let recipient = address_decoder::decode_address(&env, &pub_inputs.recipient);
let relayer_opt = address_decoder::decode_optional_relayer(&env, &pub_inputs.relayer);

// Step 7: Transfer funds
transfer_funds(
&env,
Expand Down
10 changes: 6 additions & 4 deletions contracts/privacy_pool/src/crypto/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ fn compute_vk_x(
vk: &VerifyingKey,
pub_inputs: &PublicInputs,
) -> Result<Bn254G1Affine, Error> {
// The VK must have exactly 7 IC points: IC[0] + 6 public inputs
// [root, nullifier_hash, recipient, amount, relayer, fee]
if vk.gamma_abc_g1.len() != 7 {
// The VK must have exactly 9 IC points: IC[0] + 8 public inputs
// [pool_id, root, nullifier_hash, recipient, amount, relayer, fee, denomination]
if vk.gamma_abc_g1.len() != 9 {
return Err(Error::MalformedVerifyingKey);
}

Expand All @@ -48,13 +48,15 @@ fn compute_vk_x(
let mut acc = Bn254G1Affine::from_bytes(ic0_bytes);

// Public inputs as 32-byte field elements → Fr scalars
let inputs: [&BytesN<32>; 6] = [
let inputs: [&BytesN<32>; 8] = [
&pub_inputs.pool_id,
&pub_inputs.root,
&pub_inputs.nullifier_hash,
&pub_inputs.recipient,
&pub_inputs.amount,
&pub_inputs.relayer,
&pub_inputs.fee,
&pub_inputs.denomination,
];

for (i, input_bytes) in inputs.iter().enumerate() {
Expand Down
Loading