Skip to main content

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

Fields:
  • height: Tree depth (26 for Privacy Cash)
  • next_index: Next available leaf position
  • root: Current Merkle root
  • root_index: Position in circular root history buffer
  • root_history_size: Size of root history buffer
  • root_history: Circular buffer of recent roots
  • subtrees: Cache of rightmost subtree hashes at each level

Initialization

Zero bytes: The tree uses pre-computed hashes of empty subtrees:
This allows efficient handling of empty branches without computing hashes.

Appending Leaves

The append function adds a new commitment to the tree:

Algorithm Walkthrough

Example: Appending the 5th leaf (index 4) to a height-3 tree
Level 0: (rightmost bit = 0)
Level 1: (middle bit = 0)
Level 2: (leftmost bit = 1)
Result:
  • New root computed and stored
  • Subtree cache updated for future appends
  • Merkle proof returned for immediate use

Key Optimizations

1. Subtree Caching The subtrees 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
If 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:
  1. User generates a proof using root R at time T
  2. Other transactions modify the tree between T and submission
  3. By time user submits, the current root is R’
The root history allows the user’s proof (using old root R) to still be valid if R is in the history.

Root Lookup

Algorithm:
  1. Reject zero roots (invalid)
  2. Start at current root index
  3. Walk backwards through circular buffer
  4. Return true if root found
  5. Return false if we wrap around to start
Complexity: O(root_history_size)

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
For Privacy Cash:
  • 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:
Where 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:
Solana rent: ~0.028 SOL for 4,100 bytes

Proof Verification

While proof generation happens off-chain, verification occurs in the circuit:
The circuit recomputes the root from the leaf and path elements, verifying membership.

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
Root lookup:
  • Compute units: ~100-500 CU
  • Time: O(root_history_size)
  • Storage reads: No writes
Proof generation (off-chain):
  • Time: O(height) tree traversal
  • Data: 26 × 32 = 832 bytes (path elements)

Security Considerations

Collision Resistance

Poseidon provides ~128 bits of collision resistance, sufficient for cryptographic applications.

Preimage Resistance

Given a root R, it is computationally infeasible to find a leaf L and path that produces R (without knowing a valid leaf).

Second Preimage Resistance

Given a leaf L with path P producing root R, it is computationally infeasible to find a different L’ and P’ producing the same R.

Tree Full Protection

The tree rejects appends once full, preventing overflow.

Root History Expiration

Old roots naturally expire from the history. Users must submit proofs before their root is evicted, or regenerate with a newer root.

Implementation Source

The Merkle tree implementation is adapted from Light Protocol’s sparse merkle tree, which has been audited and battle-tested in production.