Shield
Depositing funds into the anonymity pool
Overview#
The shield instruction deposits SOL into the anonymity pool vault. The deposit creates a commitment that is inserted into the Poseidon Merkle tree as a leaf. The commitment binds a note (nullifier + secret) to a specific leaf position.
Shield Flow
Wallet
User's SOL wallet
Note
nullifier + secret
Commitment
Poseidon(nullifier, secret)
Merkle Tree
Inserted as leaf
Deposit Event
Emitted on-chain
Note Structure#
Each deposit creates a note composed of:
nullifier = Poseidon(spendingKey, DOMAIN_SEPARATOR, counter)
nullifierHash = Poseidon(nullifier) ← public signal
leaf = Poseidon(nullifier, secret) ← Merkle leaf
commitment = leaf ← emitted in DepositEvent
| Component | Derivation | Visibility |
|---|---|---|
| spendingKey | Derived from wallet signature | Private |
| DOMAIN_SEPARATOR | Poseidon(["mixer_nullifier"]) = 1965203435... | Public constant |
| counter | Per-(key, denomination) counter from relayer | Private |
| secret | Random 254-bit field element | Private |
| nullifier | Poseidon(spendingKey, domain, counter) | Private |
| nullifierHash | Poseidon(nullifier) | Public (in proof) |
| commitment | Poseidon(nullifier, secret) | Public (in event) |
Commitment Collision
If two deposits produce the same commitment, the Merkle tree would have duplicate leaves, breaking the unlinkability guarantee. The counter ensures uniqueness per (key, denomination) pair. The random secret prevents cross-key collisions.
On-chain Instruction#
shield(denomination: u64, commitment: [u8; 32])
Accounts:
payer(Signer, mut) — the depositormixer_state(mut, PDA) — the pool state for this denominationvault(mut, PDA, system-owned) — the pool's SOL vaultsystem_program
Logic:
- Validates denomination against allowed values (0.1, 1, 10 SOL)
- Derives and verifies mixer and vault PDA addresses
- Transfers exactly
denominationlamports from payer to vault viasystem_program::transfer - Increments
mixer_state.total_shielded - Emits
DepositEvent { commitment, amount, denomination }
Relayer Processing#
After the shield transaction confirms:
- The relayer's
connection.onLogs(PROGRAM_ID)callback fires parseDepositEventsFromLogs()extracts theDepositEventdiscriminatorSHA-256("event:DepositEvent")[0:8]- The commitment is inserted into the relayer's in-memory Poseidon Merkle tree
- If
pendingLeaves >= threshold, a multisigupdate_roottransaction is submitted - The
/proofendpoint resolves for this commitment
Counter Management#
The counter is a nonce that ensures each note from a given spending key has a unique nullifier. It's managed by the relayer:
GET /next-counter?key=0x{keyHash}&denomination={value}increments and returns the counter- Persisted to
relayer-counters.jsonon disk - Fallback: frontend uses localStorage counter
mixer-counter-{pk}-{denom}
Token Shield Coming Soon#
For SPL tokens, shield_token follows the same pattern but uses CPI spl_token::transfer to move tokens from the payer's ATA to the vault's ATA. The MixerTokenState includes the token mint address. Token pool support is implemented in the Solana program but not yet available in the frontend.