Shield, transfer, unshield
Three instructions move value through the pool. Each is an ordinary ZUL transaction targeting the shielded pool builtin; what differs is which side of the public/private boundary each crosses.
Shield — public → private
in : depositor signature, amount, asset (ZUL or wSOL)
do : move value into the pool vault
append note_commitment to the tree
out : encrypted note (only the depositor's wallet can open it)
proof: none — public inputs bind the commitmentAfter one block, the wallet's scanner finds the encrypted note and the value exists privately. The deposit itself is public — see Privacy limits for why you should not unshield a matching amount five minutes later.
Transfer — private → private
in : 2 nullifiers, 2 new commitments,
Groth16 proof, encrypted outputs, recent root
do : verify proof (native), reject seen nullifiers,
append commitments
public: counts only — nothing else
typical shape: [pay note → recipient] + [change note → self]This is the fully private operation: amounts, parties, and the asset are all inside the proof. Don't-need-2-inputs? A dummy input with zero value fills the slot — the observer cannot tell.
Unshield — private → public
in : nullifier(s), Groth16 proof,
public recipient + public amount + asset
do : verify, mark nullifiers, pay recipient from vault,
append a private change note if value remains
public: the exit — recipient, amount, assetThe proof reveals exactly one thing: the value leaving. Where it had been since it entered the pool stays sealed.
Fees
Pool transactions are still ZUL transactions, so a fee payer signs publicly in v1. The transfer circuit already supports an optional public fee output, which is the hook for the planned relayer (someone else pays the fee, compensated from inside the pool). Until then, fee-payer linkage is the pool's loudest side channel — documented in Privacy limits.
Pool activity — kinds, note counts, sealed amounts — is live on /explorer/shielded.