Overview
The Shielded Pool contract (identipay::shielded_pool) provides a privacy layer for breaking address clustering. Users deposit coins with a cryptographic commitment, then withdraw to fresh stealth addresses via ZK proofs.
This prevents observers from linking multiple stealth addresses belonging to the same user, as described in the whitepaper section 4.8.
Source Code
Location:contracts/sources/shielded_pool.move:11
How It Works
Deposit
User deposits tokens with a Poseidon commitment
C = Poseidon(amount, owner_pubkey, salt). The commitment is inserted into an incremental Merkle tree.Withdraw
User generates a ZK proof that they own a note in the Merkle tree without revealing which one. The proof also derives a nullifier to prevent double-spending.
Data Structures
ShieldedPool
The main pool object, generic over token type<T>.
Sui object identifier
Pool’s token balance
Current Merkle root (BN254 field element)
Spent nullifiers for double-spend prevention
Next available leaf slot in the Merkle tree
Incremental Merkle tree state (one hash per level)
Merkle tree depth (default 20, supports 2^20 = ~1M notes)
DepositEvent
Emitted when tokens are deposited into the pool.Poseidon commitment C = Poseidon(amount, owner_pubkey, salt)
Position in the Merkle tree (needed for generating withdrawal proofs)
Updated Merkle root after insertion
WithdrawEvent
Emitted when tokens are withdrawn from the pool.Unique nullifier N = Poseidon(commitment, owner_key)
Fresh stealth address receiving the withdrawal
Amount withdrawn
Updated Merkle root (if change commitment was added)
Entry Functions
create_pool
Create a new shielded pool for a token type. Called once during deployment.shielded_pool.move:77-101
deposit
Deposit tokens into the shielded pool with a note commitment. Function Signature:Mutable reference to the shielded pool
Coin to deposit (consumed entirely)
Poseidon commitment C = Poseidon(amount, owner_pubkey, salt)
EZeroCommitment(3): Note commitment is zeroEPoolFull(4): Merkle tree is at capacityEInvalidAmount(5): Coin value is zero
shielded_pool.move:113-139
Example:
withdraw
Withdraw tokens from the shielded pool using a ZK proof of note ownership. Function Signature:Mutable reference to the shielded pool
ZK verification key for the pool spend circuit
Groth16 proof bytes
Public inputs: [merkle_root, nullifier, recipient, amount, change_commitment]
Unique nullifier N = Poseidon(commitment, owner_key)
Fresh stealth address for withdrawal
Amount to withdraw
If withdrawing partial amount, commitment for change note. Pass 0 for full withdrawal.
EInvalidAmount(5): Amount is zeroEZeroNullifier(6): Nullifier is zeroEInsufficientPoolBalance(2): Pool doesn’t have enough tokensENullifierAlreadySpent(0): This nullifier was already usedEProofVerificationFailed(1): ZK proof is invalid
shielded_pool.move:150-193
Example:
Public Functions
pool_balance
merkle_root
next_leaf_index
is_nullifier_spent
shielded_pool.move:200-202
Merkle Tree Implementation
The pool uses an incremental Merkle tree with Poseidon BN254 hashing for ZK circuit compatibility.Tree Structure
- Depth: 20 levels (supports 2^20 = 1,048,576 notes)
- Hash function: Poseidon BN254 (2 inputs)
- Zero hash:
0u256at leaf level - Storage: O(depth) using filled subtrees approach
Insertion Algorithm
- If current index is even (left child): sibling is zero hash
- If current index is odd (right child): sibling is filled_subtrees[level]
- Compute parent as
Poseidon(left, right) - Move up one level
shielded_pool.move:226-258
Privacy Guarantees
Anonymity Set
Anonymity Set
The privacy level equals the number of deposits in the pool. With 1,000 deposits, withdrawals have 1-in-1,000 anonymity.
Unlinkability
Unlinkability
Zero-knowledge proofs ensure observers cannot link deposits to withdrawals. Even the contract cannot determine which note is being spent.
Double-Spend Prevention
Double-Spend Prevention
Nullifiers are cryptographically derived from note commitments. Each note can only be spent once, even though its commitment remains in the tree forever.
Amount Privacy
Amount Privacy
Note amounts are hidden in Poseidon commitments. Observers only see withdrawal amounts, not which deposits they came from.
Security Considerations
Merkle Root Updates: The Merkle root changes with every deposit and change-note insertion. Wallet UIs should display the current root and leaf count.
Gas Optimization: Incremental Merkle tree insertion costs O(depth) = O(20) Poseidon hashes per deposit, which is efficient for on-chain execution.
Related Modules
ZK Verifier
Groth16 proof verification
Settlement
Main commerce execution
