Merkle Tree Implementation
Privacy Cash uses a sparse Merkle tree to store commitments on-chain. The tree enables efficient membership proofs while maintaining constant-size state.Overview
The Merkle tree implementation is adapted from Light Protocol and provides:- Efficient append-only operations
- Poseidon hashing for zk-SNARK compatibility
- Root history for asynchronous proof generation
- Optimized storage using subtree caching
Tree Structure
Height: 26 levels (configurable) Capacity: 2^26 = 67,108,864 leaves Hash function: Poseidon (2-to-1 compression) The tree stores commitment values as leaves and maintains:- Current root
- Root history (circular buffer)
- Subtree cache for efficient appends
- Next available leaf index
Account Structure
height: Tree depth (26 for Privacy Cash)next_index: Next available leaf positionroot: Current Merkle rootroot_index: Position in circular root history bufferroot_history_size: Size of root history bufferroot_history: Circular buffer of recent rootssubtrees: Cache of rightmost subtree hashes at each level
Initialization
Appending Leaves
Theappend function adds a new commitment to the tree:
Algorithm Walkthrough
Example: Appending the 5th leaf (index 4) to a height-3 tree- New root computed and stored
- Subtree cache updated for future appends
- Merkle proof returned for immediate use
Key Optimizations
1. Subtree Caching Thesubtrees array stores the rightmost filled subtree at each level. This allows O(height) append operations instead of O(n log n) for n leaves.
2. Index-Based Path Selection
The algorithm uses the binary representation of the leaf index to determine left/right positioning:
- Bit 0: Level 0 position
- Bit 1: Level 1 position
- Bit i: Level i position
current_index % 2 == 0, the new node goes on the left.
3. Zero Byte Precomputation
Empty subtrees use precomputed hashes instead of computing them on the fly.
4. Proof Generation
The append function returns the Merkle proof, allowing users to immediately generate zero-knowledge proofs without a separate query.
Root History
The tree maintains a circular buffer of recent roots to support asynchronous proof generation.Why Root History?
In a blockchain environment:- User generates a proof using root R at time T
- Other transactions modify the tree between T and submission
- By time user submits, the current root is R’
Root Lookup
- Reject zero roots (invalid)
- Start at current root index
- Walk backwards through circular buffer
- Return true if root found
- Return false if we wrap around to start
Root History Size
Typical configuration: 100-1000 roots Trade-offs:- Larger history: More flexibility for async proofs, more storage
- Smaller history: Less storage, but proofs must be submitted quickly
- Root history size: 100
- Allows ~10-15 minutes for proof submission (assuming ~10s per transaction)
Poseidon Hashing
The tree uses Poseidon hash for zk-SNARK compatibility:H: Hasher is typically Poseidon with 2 inputs.
Properties:
- Field arithmetic: Operates in BN254 scalar field
- Efficient circuits: ~200 constraints per hash
- Domain separation: Hashes at different levels use different parameters
Storage Costs
For a height-26 tree with 100-root history:Proof Verification
While proof generation happens off-chain, verification occurs in the circuit:Performance Characteristics
Append operation:- Compute units: ~2,000-3,000 CU
- Time: O(height) = O(26) hashes
- Storage writes: 1 root + 26 subtrees + 1 history entry
- Compute units: ~100-500 CU
- Time: O(root_history_size)
- Storage reads: No writes
- Time: O(height) tree traversal
- Data: 26 × 32 = 832 bytes (path elements)