Privacy Pools

Privacy Pools are the core shielding mechanism in Obelysk. You deposit tokens into a pool, which adds a cryptographic commitment to a Merkle tree. Later, you withdraw by proving you know the secret behind one of the commitments — without revealing which one.

5
Token Pools
20
Merkle Depth
1M+
Max Deposits
30bps
Fee

How It Works

1
Deposit
You send tokens to the pool contract. The contract creates a Pedersen commitment C = amount·G + blinding·H and inserts it into the LeanIMT Merkle tree. You receive a note (secret + nullifier).
2
Tokens are shielded
Your commitment sits in the Merkle tree alongside everyone else's deposits. The link between your address and your deposit is now broken.
3
Withdraw
Present your note to generate a zero-knowledge proof: (1) Merkle inclusion proof proving your commitment is in the tree, (2) nullifier to prevent double-spending, (3) range proof for the amount. Nobody learns which deposit is yours.
Save Your Note!

The note contains the secret and nullifier needed to withdraw. If you lose it, your funds are permanently locked. There is no recovery mechanism — not even the protocol owner can retrieve them.

Supported Pools

PoolTokenContract AddressDecimals
ETH PoolETH0x06d0b41c...1e82b518
STRK PoolSTRK0x02c348e...3c9cf118
USDC PoolUSDC0x05d36d7...4d4d59b6
wBTC PoolwBTC0x030fcfd4...d5e5e28
SAGE PoolSAGE0x022497...ac724f18

Cryptographic Primitives

Pedersen Commitments

Each deposit creates a Pedersen commitment that hides the amount while being binding (you can't change the amount later):

C = amount · G + blinding · H

where:
  G = STARK curve generator (known)
  H = Pedersen generator (nothing-up-my-sleeve point, unknown discrete log)
  amount = deposit value
  blinding = random scalar (part of your note)
Why Two Generators?

Using two generators G and H with unknown relative discrete log makes the commitment perfectly hiding — there are infinitely many (amount, blinding) pairs that produce the same commitment. But it's computationally binding — you can't find two different openings unless you can solve the discrete log problem.

LeanIMT Merkle Tree

The pool uses a Lean Indexed Merkle Tree (LeanIMT) with Poseidon hashing:

  • Depth: 20 levels (supports 2^20 = 1,048,576 deposits)
  • Hash: Poseidon-252 (STARK native)
  • Leaf: Pedersen commitment hash
  • Insert: O(20) hashes per deposit (update path to root)
  • Proof: 20 sibling hashes from leaf to root

Nullifiers

Nullifiers prevent double-spending without revealing which deposit is being withdrawn:

nullifier = Poseidon(nullifier_secret || commitment_hash)

The contract maintains a set of used nullifiers. When you withdraw, the contract checks that your nullifier hasn't been used before and adds it to the set. Since the nullifier is derived from your secret (which only you know), nobody can link it to a specific deposit.

Range Proofs

Range proofs verify that the withdrawal amount is valid (0 ≤ amount < 2^64) without revealing the actual value. This prevents overflow attacks where someone could withdraw more than they deposited.

ASP Compliance Layer

Privacy Pools include an optional Association Set Provider (ASP) compliance layer:

Inclusion Sets
Prove your deposit belongs to a curated set of 'clean' deposits. Useful for DeFi integrations requiring compliance.
Exclusion Sets
Prove your deposit is NOT in a blacklisted set. Negative compliance without revealing identity.

ASPs are registered on-chain with their own Merkle trees. They can add/remove deposits from their sets, and users can prove membership or non-membership during withdrawal.

SDK Usage

// Deposit
const note = await obelysk.privacyPool.deposit({
  token: 'eth',
  amount: '1.0',
});
// SAVE THIS NOTE SECURELY!

// Check pool stats
const stats = await obelysk.privacyPool.getPoolStats('eth');
console.log('Total deposits:', stats.totalDeposits);
console.log('Merkle root:', await obelysk.privacyPool.getMerkleRoot('eth'));

// Withdraw
const result = await obelysk.privacyPool.withdraw({
  token: 'eth',
  amount: '1.0',
  secret: note.secret,
  nullifier: note.nullifier,
  leafIndex: note.leafIndex,
  recipient: '0xYOUR_ADDRESS',
});

// Verify nullifier was consumed
const spent = await obelysk.privacyPool.isNullifierUsed('eth', note.nullifier);
console.log('Nullifier spent:', spent); // true

Fee Structure

A 30bps (0.30%) fee is deducted from withdrawals before the token transfer. Fees accumulate per-pool and are collected by the protocol owner via collect_fees().

Next Steps