A private transfer spends up to six notes and creates three: one for the recipient, one for the sender’s change and one for the sender’s fee change.

Inputs and outputs

Circuit statement

Given public inputs (VIRTUAL_CHAIN_ID, noteNullifiers[6], txNullifier, root, rkRoot, three commitments, three ciphertexts, gasFee, nullifiersHash, fee, feeTokenAddress), the prover knows:
  • a signed EIP-1559 transaction to the recipient for (token, amount), signed by pk for VIRTUAL_CHAIN_ID
  • input notes owned by the sender, each with a Merkle path to root
  • the sender’s nk, matching the nullifiers
  • the recipient’s inbox key, with a Merkle path to rkRoot
  • trapdoors and ephemeral keys that make each commitment and ciphertext correct
such that the inputs equal amount + change + fee, per token.

Why the key registry root

The recipient’s inbox key must be registered. Proving membership under rkRoot means a sender can only encrypt to a real, owner-registered key. The contract checks that rkRoot is a known registry root (InvalidKeyRegistryRoot).

Fee change

The fee can be paid in a different token from the one sent. The separate fee change note returns the rest of the fee token to the sender.

Contract execution

  1. _checkSpend: relayer allowlist (if fee > 0), tx nullifier unused, root known
  2. Check the key registry root
  3. Verify the proof with 46 public inputs
  4. Record nullifiers
  5. Insert three commitments in one batch
  6. Pay fee of feeTokenAddress to msg.sender
  7. Emit ShieldedTransfer
See Private Transfer contract reference.