SenddySenddy Docs

Note System (UTXO)

How Senddy's private note system works — commitments, nullifiers, and Merkle trees.

Overview

Senddy uses a UTXO (Unspent Transaction Output) model similar to Bitcoin and Zcash. Instead of maintaining account balances, the system tracks individual "notes" that represent discrete amounts of value.

Notes

A note is a private unit of value. Each note contains:

  • Value — The amount of USDC the note represents
  • Owner — The public key of the note's owner
  • Randomness — A random value that makes each note unique

Notes are never stored in plaintext on-chain. Instead, only their commitments (cryptographic hashes) are recorded.

Commitments

A commitment is a Poseidon2 hash of a note's contents:

commitment = Poseidon2(value, ownerPubKey, randomness)

Commitments are stored in an append-only Merkle tree on-chain. When you deposit and shield, new commitments are added to the tree. When you spend, the commitments remain in the tree forever — they're never removed.

Nullifiers

To prevent double-spending, each note has a unique nullifier derived from the note and the owner's secret key:

nullifier = Poseidon2(commitment, secretKey)

When a note is spent, its nullifier is published on-chain. The system maintains a nullifier accumulator — if a nullifier has been seen before, the spend is rejected.

Because nullifiers are derived from the secret key, no one can link a nullifier to its commitment without knowing the key. This is what preserves privacy.

Merkle Tree

All commitments are stored in a Merkle tree. This data structure allows efficient membership proofs — you can prove a commitment exists in the tree without revealing which one it is.

The spend proof includes a Merkle path that proves "I know a commitment in this tree" without revealing the commitment's position or value.

Transaction Flow

Shield (Deposit to Private)

USDC deposit → Shield proof → New note commitments added to Merkle tree
  • Proves: value conservation, commitment integrity

Spend (Private Transfer)

Input notes → Spend proof → Nullifiers published + New output commitments added
  • Input notes are "consumed" by publishing their nullifiers
  • Change is returned as a new note to the sender
  • Supports optional withdrawal (one output is a public USDC transfer)

Example

Alice has a note worth $100. She wants to send $30 to Bob.

  1. Alice's client selects her $100 note as input
  2. Creates two output notes:
    • $30 note owned by Bob
    • $70 note owned by Alice (change)
  3. Generates a spend proof proving:
    • The $100 input note exists in the Merkle tree
    • Alice knows the secret key for the input note
    • $30 + $70 = $100 (conservation)
    • The nullifier for the $100 note is correctly derived
  4. Submits the proof on-chain
  5. The $100 note's nullifier is recorded (can't be spent again)
  6. Two new commitments ($30 for Bob, $70 for Alice) are added to the tree

On this page