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:

  1. Verifies the ZK proof
  2. Checks the attestor signature
  3. Records each nullifier (preventing double-spend)
  4. 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