Transaction Flow
End-to-end lifecycle of a private transaction in Senddy.
Overview#
A private transaction in Senddy goes through six phases: note selection, proof generation, attestation, relay, on-chain execution, and sync. The diagram below shows the full flow.
Base deposit and shield flow#
Before private spending begins, a Base deposit enters through a one-time EOA:
The random depositId gives the client a constant-time on-chain lookup. It does not scan historical blocks: active deposit records are reconciled through a single multicall, and an existing unconsumed deposit can resume shielding after a reload or relay timeout.
Phase Breakdown#
1. Note Selection#
When the user initiates a send, the app fetches all unspent notes from local storage (IndexedDB). Notes are sorted by value and selected greedily — largest first — to cover the target amount. If the selected notes exceed the amount, a change note is created for the remainder.
For each selected note, the app fetches the current commitment tree from the subgraph and builds a Merkle path proving membership.
2. Proof Generation#
The app generates a zero-knowledge proof using a Noir circuit compiled with UltraHonk. The proof attests to:
- Ownership — the sender knows the secret key for each input note
- Existence — each input note's commitment exists in the on-chain Merkle tree
- Conservation — the sum of inputs equals the sum of outputs (no value created or destroyed)
- Correctness — nullifiers and output commitments are correctly derived
The circuit supports up to 3 inputs (spend3) or 9 inputs (spend9) for note consolidation.
3. Attestation#
The proof is sent to the attestor — a trusted enclave that verifies compliance requirements and signs the transaction. The attestor's signature is required by the pool contract before accepting any spend.
4. On-chain Execution#
The signed transaction is submitted through Gelato Relay, so the user never needs to hold ETH for gas. The relay calls the pool contract, which:
- Verifies the ZK proof
- Checks the attestor signature
- Records each nullifier (preventing double-spend)
- Appends new output commitments to the Merkle tree
5. Sync#
After the transaction is confirmed, the sync engine detects the new on-chain events via the subgraph. Both the sender and recipient's clients:
- Fetch new commitments and nullifiers
- Attempt to decrypt attached memos using their view keys
- Store any decrypted notes locally and update balances