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:
Deposit and shield are one transaction. There is no escrow record to poll, nothing to resume after a reload, and nothing to cancel: the call either mints the note or reverts before any funds move. Because the authorization's nonce is derived from the note commitments and the proof hash, it can only ever be spent on this one shield.
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