Skip to content
Draft
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
4 changes: 2 additions & 2 deletions artifacts/zk/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
"fee",
"denomination"
],
"schema_version": "1.20680.19972"
"schema_version": "1.23292.854"
},
"commitment": {
"circuit_id": "commitment",
"path": "commitment.json",
"artifact_sha256": "0xc664b4c5e4bb06f7d54c4e86fd6a81d42a968899941e6f42d91656ddf9f9a661",
"artifact_sha256": "0x2456b7e6e0162b8b14a8619914f964734cc7c031727c1a52ac49e3f1e1a93472",
"bytecode_sha256": "0xdfbdca2108939b0184db56a381088d4698702988647507242598869290c5d5e0",
"abi_sha256": "0xe084f973bddc1be32518abc050b9f03bcb06ffbef43034b653e9319485a4bb9d",
"name": "commitment",
Expand Down
15 changes: 7 additions & 8 deletions circuits/lib/src/validation/fee.nr
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
// Rule set
// --------
// R1. fee <= amount (prevent fee-only griefing)
// R2. fee == 0 relayer == 0 (no phantom relayer)
// R3. fee > 0 relayer != 0 (relayer must be set when fee is non-zero)
// R2. fee == 0 -> relayer == 0 (no phantom relayer)
// R3. fee > 0 -> relayer != 0 (relayer must be set when fee is non-zero)
//
// All three rules are enforced both here (circuit side) and in
// the SDK (sdk/src/proof.ts see `validateRelayerFeeEncoding`).
// the SDK (sdk/src/proof.ts - see `validateRelayerFeeEncoding`).
// ============================================================

/// Validate that fee does not exceed the withdrawal amount.
Expand All @@ -26,8 +26,8 @@ pub fn validate_fee(fee: Field, amount: Field) {
/// Validate the canonical relayer/fee encoding rule.
///
/// # Encoding contract
/// - fee == 0 relayer MUST be 0 (no fee, no relayer)
/// - fee > 0 relayer MUST be != 0 (fee without a relayer is invalid)
/// - fee == 0 -> relayer MUST be 0 (no fee, no relayer)
/// - fee > 0 -> relayer MUST be != 0 (fee without a relayer is invalid)
///
/// # Panics
/// Panics when either encoding invariant is violated.
Expand All @@ -39,7 +39,7 @@ pub fn validate_relayer(relayer: Field, fee: Field) {
}
}

/// Combined convenience guard call once in the spend circuit.
/// Combined convenience guard - call once in the spend circuit.
pub fn validate_fee_and_relayer(fee: Field, amount: Field, relayer: Field) {
validate_fee(fee, amount);
validate_relayer(relayer, fee);
Expand All @@ -53,7 +53,6 @@ pub fn validate_fee_and_relayer(fee: Field, amount: Field, relayer: Field) {
// directly to a reproduction case.
// ============================================================

#[cfg(test)]
mod tests {
use super::{validate_fee, validate_relayer, validate_fee_and_relayer};

Expand Down Expand Up @@ -83,7 +82,7 @@ mod tests {

#[test(should_fail_with = "fee cannot exceed withdrawal amount")]
fn fee_equals_max_field_panics_when_amount_is_small() {
// Largest value that fits in u64 must fail when amount is 0
// Largest value that fits in u64 - must fail when amount is 0
validate_fee(18446744073709551615, 0);
}

Expand Down
5 changes: 4 additions & 1 deletion circuits/lib/src/validation/relayer.nr
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/// Validate relayer address consistency with fee.
/// If fee is zero, relayer must be zero address.
/// - If fee is zero, relayer must be zero address (no relayer).
/// - If fee is non-zero, relayer must be non-zero address.
pub fn validate_relayer(relayer: Field, fee: Field) {
if fee == 0 {
assert(relayer == 0, "relayer must be zero address if fee is zero");
} else {
assert(relayer != 0, "relayer must be non-zero address if fee is non-zero");
}
}
11 changes: 6 additions & 5 deletions circuits/lib/src/validation/test_helpers.nr
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub fn build_valid_fixture() -> (Field, Field, Field, Field, [Field; 20], Field,

let hash_path = hash::zero_sibling_path();
let root = merkle::compute_root(commitment, leaf_index, hash_path);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, root);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, pool_id);

(nullifier, secret, pool_id, leaf_index, hash_path, root, nullifier_hash)
}
Expand All @@ -81,7 +81,7 @@ pub fn build_fixture_at_index(
let commitment = hash::compute_commitment(nullifier, secret, pool_id);
let hash_path = hash::zero_sibling_path();
let root = merkle::compute_root(commitment, leaf_index, hash_path);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, root);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, pool_id);
(hash_path, root, nullifier_hash)
}

Expand Down Expand Up @@ -164,7 +164,7 @@ fn test_build_valid_fixture_is_consistent() {
// Re-derive all values and verify consistency
let commitment = hash::compute_commitment(nullifier, secret, pool_id);
let recomputed_root = merkle::compute_root(commitment, leaf_index, hash_path);
let recomputed_nh = hash::compute_nullifier_hash(nullifier, root);
let recomputed_nh = hash::compute_nullifier_hash(nullifier, pool_id);

assert(recomputed_root == root, "fixture root must match recomputed root");
assert(recomputed_nh == nullifier_hash, "fixture nullifier_hash must match recomputed value");
Expand Down Expand Up @@ -211,7 +211,7 @@ fn test_build_fixture_at_high_index() {

let commitment = hash::compute_commitment(nullifier, secret, pool_id);
let recomputed_root = merkle::compute_root(commitment, leaf_index, hash_path);
let recomputed_nh = hash::compute_nullifier_hash(nullifier, root);
let recomputed_nh = hash::compute_nullifier_hash(nullifier, pool_id);

assert(recomputed_root == root);
assert(recomputed_nh == nullifier_hash);
Expand Down Expand Up @@ -339,6 +339,7 @@ impl WithdrawalBuilder {

pub fn with_amount(mut self, amount: Field) -> Self {
self.amount = amount;
self.denomination = amount;
self
}

Expand Down Expand Up @@ -395,7 +396,7 @@ impl WithdrawalBuilder {
let nullifier_hash = if self.override_nullifier_hash {
self.nullifier_hash
} else {
hash::compute_nullifier_hash(self.nullifier, root)
hash::compute_nullifier_hash(self.nullifier, self.pool_id)
};

(
Expand Down
58 changes: 33 additions & 25 deletions circuits/withdraw/src/tests.nr
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ fn test_wrong_secret_fails() {
let real_commitment = hash::compute_commitment(nullifier, real_secret, pool_id);
let path = hash::zero_sibling_path();
let real_root = merkle::compute_root(real_commitment, 0, path);
let real_nh = hash::compute_nullifier_hash(nullifier, real_root);
let real_nh = hash::compute_nullifier_hash(nullifier, pool_id);

execute(WithdrawalBuilder::new()
.with_nullifier(nullifier)
Expand All @@ -89,7 +89,7 @@ fn test_wrong_leaf_index_fails() {
let commitment = hash::compute_commitment(nullifier, secret, pool_id);
let path = hash::zero_sibling_path();
let real_root = merkle::compute_root(commitment, 0, path); // Root for index 0
let real_nh = hash::compute_nullifier_hash(nullifier, real_root);
let real_nh = hash::compute_nullifier_hash(nullifier, pool_id);

execute(WithdrawalBuilder::new()
.with_nullifier(nullifier)
Expand Down Expand Up @@ -138,7 +138,7 @@ fn test_max_leaf_index() {
.with_recipient(0xFFFF));
}

#[test(should_fail_with = "nullifier_hash mismatch: invalid nullifier or wrong root")]
#[test(should_fail_with = "nullifier_hash mismatch: invalid nullifier or wrong pool_id")]
fn test_wrong_nullifier_hash_fails() {
execute(WithdrawalBuilder::new()
.with_nullifier(111)
Expand All @@ -147,7 +147,7 @@ fn test_wrong_nullifier_hash_fails() {
.with_nullifier_hash(54321));
}

#[test(should_fail_with = "nullifier_hash mismatch: invalid nullifier or wrong root")]
#[test(should_fail_with = "nullifier_hash mismatch: invalid nullifier or wrong pool_id")]
fn test_nullifier_hash_from_different_nullifier_fails() {
let nullifier = 111;
let secret = 222;
Expand All @@ -156,7 +156,7 @@ fn test_nullifier_hash_from_different_nullifier_fails() {
let path = hash::zero_sibling_path();
let real_root = merkle::compute_root(commitment, 0, path);

let wrong_nh = hash::compute_nullifier_hash(9999, real_root);
let wrong_nh = hash::compute_nullifier_hash(9999, pool_id);

execute(WithdrawalBuilder::new()
.with_nullifier(nullifier)
Expand All @@ -165,20 +165,20 @@ fn test_nullifier_hash_from_different_nullifier_fails() {
.with_nullifier_hash(wrong_nh));
}

#[test(should_fail_with = "nullifier_hash mismatch: invalid nullifier or wrong root")]
fn test_nullifier_hash_bound_to_root() {
#[test(should_fail_with = "nullifier_hash mismatch: invalid nullifier or wrong pool_id")]
fn test_nullifier_hash_bound_to_pool_id() {
let nullifier = 111;
let stale_root: Field = 0xdeadcafe;
let stale_nh = hash::compute_nullifier_hash(nullifier, stale_root);
let wrong_pool_id: Field = 999;
let wrong_nh = hash::compute_nullifier_hash(nullifier, wrong_pool_id);

execute(WithdrawalBuilder::new()
.with_nullifier(nullifier)
.with_secret(222)
.with_pool_id(1)
.with_nullifier_hash(stale_nh));
.with_nullifier_hash(wrong_nh));
}

#[test(should_fail_with = "nullifier_hash mismatch: invalid nullifier or wrong root")]
#[test(should_fail_with = "nullifier_hash mismatch: invalid nullifier or wrong pool_id")]
fn test_zero_nullifier_hash_fails() {
execute(WithdrawalBuilder::new()
.with_nullifier(111)
Expand Down Expand Up @@ -208,6 +208,16 @@ fn test_nonzero_relayer_with_zero_fee_fails() {
.with_fee(0));
}

#[test(should_fail_with = "relayer must be non-zero address if fee is non-zero")]
fn test_zero_relayer_with_nonzero_fee_fails() {
execute(WithdrawalBuilder::new()
.with_nullifier(777)
.with_secret(888)
.with_pool_id(1)
.with_relayer(0)
.with_fee(10));
}

#[test]
fn test_zero_fee_zero_relayer_valid() {
execute(WithdrawalBuilder::new()
Expand Down Expand Up @@ -275,16 +285,16 @@ fn test_two_notes_same_root_both_valid() {
}

#[test]
fn test_nullifier_hash_differs_across_roots() {
fn test_nullifier_hash_differs_across_pools() {
let nullifier: Field = 0x1234;

let root_a: Field = 1111;
let root_b: Field = 2222;
let pool_a: Field = 1111;
let pool_b: Field = 2222;

let nh_a = hash::compute_nullifier_hash(nullifier, root_a);
let nh_b = hash::compute_nullifier_hash(nullifier, root_b);
let nh_a = hash::compute_nullifier_hash(nullifier, pool_a);
let nh_b = hash::compute_nullifier_hash(nullifier, pool_b);

assert(nh_a != nh_b, "same nullifier in different roots must yield different nullifier_hashes");
assert(nh_a != nh_b, "same nullifier in different pools must yield different nullifier_hashes");
}

// ============================================================
Expand All @@ -300,7 +310,7 @@ fn test_leaf_index_exceeds_tree_capacity() {

// Build a valid path at index 0, then attempt with invalid index 2^20
let (hash_path, root) = build_sparse_path(commitment, 0);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, root);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, pool_id);

// 2^20 = 1,048,576 -- first invalid index
main(
Expand All @@ -317,7 +327,7 @@ fn test_leaf_index_large_out_of_range() {
let commitment = hash::compute_commitment(nullifier, secret, pool_id);

let (hash_path, root) = build_sparse_path(commitment, 0);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, root);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, pool_id);

// Far outside valid range
main(
Expand All @@ -336,7 +346,7 @@ fn test_leaf_index_zero_boundary() {
let leaf_index: Field = 0;

let (hash_path, root) = build_sparse_path(commitment, leaf_index);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, root);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, pool_id);

main(
nullifier, secret, leaf_index, hash_path,
Expand All @@ -354,7 +364,7 @@ fn test_leaf_index_max_boundary() {
let leaf_index: Field = 1_048_575;

let (hash_path, root) = build_sparse_path(commitment, leaf_index);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, root);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, pool_id);

main(
nullifier, secret, leaf_index, hash_path,
Expand All @@ -372,15 +382,14 @@ fn test_leaf_index_high_bit_only() {
let leaf_index: Field = 524_288;

let (hash_path, root) = build_sparse_path(commitment, leaf_index);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, root);
let nullifier_hash = hash::compute_nullifier_hash(nullifier, pool_id);

main(
nullifier, secret, leaf_index, hash_path,
pool_id, root, nullifier_hash, 0x9ABC, 100_0000000, 0, 0, 100_0000000,
);
}

#[test]
#[test]
fn test_withdrawal_realistic_fixtures() {
// TV-005: Realistic withdrawal with deep non-zero siblings and non-zero leaf index
Expand Down Expand Up @@ -413,10 +422,9 @@ fn test_withdrawal_realistic_fixtures() {
.with_pool_id(0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff)
.with_leaf_index(10)
.with_hash_path(path)
.with_root(0x07be1a8886dd8b97c93528f7e40b657e91f6891bf8a0e7347f7834e3459a5f27)
.with_nullifier_hash(0x2670bcddb5d53b38fcbe58ecf256e97e1026a2630f594d427034da07cb547627)
.with_recipient(0x0379e6ee06f99867f4a3394f241036f395c248ad93e27fd7f2674f62645c44ea)
.with_amount(1000000000)
.with_denomination(1000000000)
.with_relayer(0x0457e64b8de401c599dbe00dcace8a6b8976214c957bb78c958ee1f5d88ffc8e)
.with_fee(1000000));
}
5 changes: 2 additions & 3 deletions contracts/privacy_pool/src/core/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ pub fn record_performance(
/// Returns global aggregate analytics snapshot for public dashboards.
pub fn analytics_snapshot(env: Env) -> Result<AnalyticsSnapshot, Error> {
config::load_global_config(&env)?;
// Use aggregate withdrawals for now as a placeholder for global deposits
let withdrawals = analytics::withdrawal_count(&env);
Ok(analytics::snapshot(&env, withdrawals as u32))
let deposits = analytics::deposit_count(&env);
Ok(analytics::snapshot(&env, deposits as u32))
}
3 changes: 2 additions & 1 deletion contracts/privacy_pool/src/core/withdraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ pub fn execute(
return Err(Error::InvalidDenomination);
}

// Step 3: Validate and decode fee
// Step 3: Validate and decode fee and relayer binding
let fee = validation::decode_and_validate_fee(&pub_inputs.fee, denomination_amount)?;
validation::validate_relayer_and_fee(&pub_inputs.relayer, fee)?;

// Step 4: Verify Groth16 proof for this pool
let vk = config::load_verifying_key(&env, &pool_id)?;
Expand Down
Loading