These docs serve as a simple explainer as to how LeafPay works under the hood.
- On-chain Merkle Mountain Range architecture
- Leaf parsing via Base64 memos
- Transfering & withdrawing funds
- Client-side SNARK‐proof offloading
- Roadmap: nullifier storage, multi-asset support, RPC endpoints
We maintain a Merkle Mountain Range (MMR) on‐chain, storing only peaks plus a 16‐leaf in‐flight buffer. Each batch is a 16‐leaf subtree (2⁴), whose root merges immediately into the MMR. By keeping only the current 16‐slot buffer plus peak array ([depth, root] pairs) on‐chain, all prove/deepen/roll‐up ops run in O(log N) time without storing millions of leaves.
A leaf is a hash of 3 values:
hash(amount|nullifier|assetId)This allows for ZK proofs that leaves being deposited or funds being transfered correspond to actual amounts that are locked in the pool.
Transfers work by proving that you know the preimage to one (or two) of the leaves of the pool, that the amount written on the leaf you're adding to the tree is equal to the amount on the leaves you're using and nullifying and that the asset match. Transfering funds to another user implies giving him the amount, nullifier and asset on your leaf.
For withdrawal, given the correct computed proof, a third party relayer can withdraw securely towards a wallet of you're choosing to avoid having to fund an empty wallet. This wallet can then be used to interact with the program, allowing for complete unlinkeability between two users exchangings funds.
At each sub‐batch (8 leaves) and full‐batch (16 leaves), we emit a Base64 memo via the Solana Memo program. Payload format:
batchNumber (8 bytes BE) ‖ leaf0 (32 bytes) ‖ … ‖ leafN (32 bytes)
Off‐chain indexers can call
getSignaturesForAddress(LeavesIndexerPDA, …)and replay memos to reconstruct up to 8 000 leaves in a single 1 000-signature fetch. On top of that a small tree indexer is used to check avoid parsing the whole tree and toget the siblings path. It is used every 10^16 transaction. These two methods combined make a finding the path for a leaf in a 1 billion leaves tree achievable with a maximum of 10 RPC calls, well under the 40 request/10sec of public endpoints.
Current nullifier storage mechanism is by progressively expanding a shard. Passed a certain point this shard is plit into two, where depending on the values of the nullifiers being stored it derives it's prefix. For example, all nullifiers with their first byte > 127 are stored in the shard with prefix 1, others in the shard with prefix 0. Once a threshold is hit for the first shard, we split depending on the value of the second byte. This way a user can deduce the shard he must add to his transaction by looking at the bytes in is nullifier. This allows for storing a nullifier at the low cost of 0.00022 SOL (about $0.04 with SOL @$150).
All SNARK‐proof generation, public‐input packing, Merkle‐root recomputation, and memo parsing are generated by the client. The on‐chain program only verifies proofs and enforces correct memo‐PDA inclusion—everything else runs in the browser. This allows for further scaling without the need of central server to store the gigabytes of data, or a third party indexing service like Light Protocol.
-
Multi‐asset support: Open deposits to SPL tokens, LSTs and NFTs in the same pool. The leaf format allows this but current anchor compatibility issues have halted the development of this feature.
-
Inbox system Allow a user depositing funds to add an encrypted message only decryptable by the recipient using chacha symetric encryption. This feature requires a wallet capable of trying multiple Chacha key generations which isn't supported currently by most Solana wallets.
-
Make a DAO As a anonymity tool, the end goal is to make this community-owned and allow for a community of passionates to contribute to the future of encrypted DeFi.