A Merkle tree is a cryptographic data structure that allows efficient and secure verification of large data sets. In Privacy Cash, Merkle trees store all deposit commitments in a verifiable structure.
Key Properties:
Efficient proofs: Prove a leaf exists in a tree with only O(log n) data
Tamper-evident: Any change to a leaf changes the root
Append-only: New leaves are added but never removed
Constant verification: Proof verification takes constant time
let mut current_index = tree_account.next_index as usize;let mut current_level_hash = leaf; // The commitmentfor i in 0..height { let subtree = &mut tree_account.subtrees[i]; let zero_byte = H::zero_bytes()[i]; if current_index % 2 == 0 { // Left child: pair with zero (right is empty) left = current_level_hash; right = zero_byte; *subtree = current_level_hash; } else { // Right child: pair with stored left sibling left = *subtree; right = current_level_hash; } current_level_hash = H::hashv(&[&left, &right]).unwrap(); current_index /= 2; // Move up one level}
This efficiently computes the new root using only stored subtrees.
pub fn is_known_root(tree_account: &MerkleTreeAccount, root: [u8; 32]) -> bool { if root == [0u8; 32] { return false; // Reject zero root } let root_history_size = tree_account.root_history_size as usize; let current_root_index = tree_account.root_index as usize; let mut i = current_root_index; // Check all roots in circular buffer loop { if root == tree_account.root_history[i] { return true; // Found matching root } if i == 0 { i = root_history_size - 1; } else { i -= 1; } if i == current_root_index { break; // Checked all roots } } false // Root not found}
Root history allows withdrawals using commitments from up to 100 transactions ago, providing flexibility in timing.
// pathIndices encodes the path as a single number// Bit i = 0 if leaf is left child at level i, 1 if right// Example: leafIndex = 42 (binary: 101010)const pathIndices = leafIndex; // Use index directly
The zero-knowledge circuit verifies the Merkle proof:
From merkleProof.circom:9
template MerkleProof(levels) { signal input leaf; signal input pathElements[levels]; signal input pathIndices; signal output root; component switcher[levels]; component hasher[levels]; // Convert pathIndices to bits (one per level) component indexBits = Num2Bits(levels); indexBits.in <== pathIndices; for (var i = 0; i < levels; i++) { // Determine if leaf is left or right child switcher[i] = Switcher(); switcher[i].L <== i == 0 ? leaf : hasher[i - 1].out; switcher[i].R <== pathElements[i]; switcher[i].sel <== indexBits.out[i]; // Hash left and right children hasher[i] = Poseidon(2); hasher[i].inputs[0] <== switcher[i].outL; hasher[i].inputs[1] <== switcher[i].outR; } // Output the computed root root <== hasher[levels - 1].out;}
Verification steps:
Extract path direction bits from pathIndices
For each level, place leaf/hash on correct side based on bit
Subscribe to commitment events and update tree in real-time:
const connection = new Connection(RPC_URL);// Listen for commitment eventsconnection.onLogs( PRIVACY_CASH_PROGRAM_ID, (logs) => { const events = parseCommitmentEvents(logs); for (const event of events) { merkleTree.append(event.commitment); } }, 'confirmed');
Periodically fetch all commitments and rebuild tree:
async function syncTree() { const treeAccount = await program.account.merkleTreeAccount.fetch(TREE_PDA); const nextIndex = treeAccount.nextIndex.toNumber(); // Fetch commitment events from program logs const commitments = await fetchCommitments(0, nextIndex); // Rebuild tree merkleTree = new MerkleTree(MERKLE_TREE_HEIGHT); for (const commitment of commitments) { merkleTree.append(commitment); } // Verify root matches on-chain assert(merkleTree.root === treeAccount.root);}
Download periodic tree snapshots for faster initial sync:
// Example: Download checkpoint from your own indexer serviceconst checkpoint = await fetch(`https://your-indexer.example.com/checkpoints/10000.json`);merkleTree = MerkleTree.fromCheckpoint(checkpoint);// Sync only new commitments since checkpointconst newCommitments = await fetchCommitments(10000, currentIndex);for (const commitment of newCommitments) { merkleTree.append(commitment);}
Checkpoint sync requires setting up your own indexer service to periodically save tree state.