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.
- Alice's client selects her $100 note as input
- Creates two output notes:
- $30 note owned by Bob
- $70 note owned by Alice (change)
- 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
- Submits the proof on-chain
- The $100 note's nullifier is recorded (can't be spent again)
- Two new commitments ($30 for Bob, $70 for Alice) are added to the tree