Summary
Plain notes never touch the chain. Void stores commitments and encrypted metadata; commitments become leaves in an append-only Merkle tree, while nullifiers mark notes as spent.
Notes & commitments
A note represents a claim to lamports. Its commitment binds the amount and a random nonce.
- amount. 64-bit lamport value encoded into the field and range-checked inside the circuit.
- noteNonce. Fresh randomness; changing it alters the commitment even if the amount stays the same.
Nullifiers
Nullifiers act as one-way spend tags derived from user secrets and a unique spend nonce.
- receiverViewPriv. Proves ownership; never leaves the client.
- spendNonce. Unique per spend attempt, ensuring the nullifier cannot collide.
Output commitments
New notes created as outputs reuse the same commitment scheme with their own randomness.
Merkle tree
Commitments are appended to a 26-level Merkle tree (67,108,864 leaves). The program stores the latest root plus a history buffer to validate proofs.
- Depth. 26 levels.
- Hash function. Internal nodes use VoidHash(left, right).
- Insertion optimization. Filled-subtrees cache enables append operations in time proportional to depth.
- Root history. A circular buffer stores the most recent ROOT_HISTORY roots.
merkleRoot
/ h01 h23
/ \ / h0 h1 h2 h3
/ \ / \ / \ / \
c0 c1 c2 c3 c4 c5 c6 c7 ...
- cX are note commitments
- internal nodes hX = VoidHash(left, right)
- leaf index + sibling hashes = Merkle pathWorking with Merkle paths
Clients fetch paths from indexers or RPC. Each path contains sibling hashes plus left/right indicators, all of which the circuit validates against the stored root.