Unshield Circuit
Unshield spends one private note and pays part or all of it to a public account. The recipient and the amount become public; which note was spent, and its history inside the pool, do not.
What the circuit proves
Unshield is circuit id 2; version 3 is active. Its proof attests that:
- The note sits in a tree of the Merkle forest behind
merkle_root. - The prover knows the note's spending key: it derives the owner key inside the circuit and is canonical, exactly as in the transfer circuit (see Canonical spending key).
nullifieris the note's nullifier,Poseidon2(commitment, spending_key).- Value is conserved:
note_value = amount + fee + change_value. note_value,amount,feeandchange_valueeach fit in 128 bits.- The note's asset equals the public
asset_id. change_commitmentis the change note's commitment when there is change, and0when there is none.- The proof is bound to the exact change memo submitted and to the recipient.
Public inputs
In circuit order, which is also the order the verifier encodes them in:
| # | Signal | Description |
|---|---|---|
| 0 | merkle_root | Root the note is proven against |
| 1 | nullifier | Nullifier of the spent note |
| 2 | amount | What the recipient receives (u128) |
| 3 | recipient | blake2_256(recipient) mod r, see Recipient encoding |
| 4 | asset_id | Asset being withdrawn (u32) |
| 5 | fee | Spend fee deducted from the note (u128) |
| 6 | change_commitment | Commitment of the change note; 0 for a total unshield |
| 7 | memo_hash | Digest of the change memo, recomputed by the pallet |
Every value is a 32-byte little-endian field element. The pallet builds amount, recipient,
asset_id, fee and memo_hash from the extrinsic's own arguments.
Recipient encoding
The recipient is an AccountId32, which is wider than the BN254 scalar field. Taken mod r directly,
an account R and the alias R ± r (a different account nobody holds a key for) would be the same
public input, and a copy of a pending withdrawal could redirect the funds to that alias. The verifier
therefore hashes it first:
recipient_input = blake2_256(recipient_account_bytes) read little-endian, mod r
Producing an alias now needs a blake2 collision. Inside the circuit, recipient carries a square
constraint of its own, so it is bound by the proof rather than left as an unused input. The pallet
also refuses the zero account and the pool's own account as recipients.
Value conservation and the fee
note_value = amount + fee + change_value
| Term | Meaning |
|---|---|
amount | Tokens paid out of the pool to the recipient |
fee | Spend fee, credited inside the pool to the validator that authored the block; see Spend Fees |
change_value | The remainder, returned to the pool as a new private note; 0 for a total unshield |
The four range checks keep every term below 2^128, so the sum cannot wrap around the field whatever
the prover passes. The recipient receives exactly amount; the fee comes out of the note on top of it.
An unshield is accepted only unsigned, so no transaction names the withdrawer: it reveals the recipient and amount, not who submitted it. See How it is sent.
Partial and total unshield
| Total unshield | Partial unshield | |
|---|---|---|
change_value | 0 | > 0 |
change_commitment | must be 0 | must equal Poseidon4(change_value, note_asset_id, change_owner_pubkey, change_blinding) |
| Change memo | empty | exactly 180 bytes |
| Leaf inserted | none | the change note |
The circuit computes the change commitment either way, since constraints always run, and binds it only
when there is change: (change - change_commitment) · (1 - has_no_change) = 0 and
change_commitment · has_no_change = 0. A tampered commitment, wrong blinding, wrong owner or wrong
asset fails the proof. The change note takes the spent note's asset.
The change memo is the only on-chain copy of the change note's secrets. It is bound through
memo_hash exactly as transfer binds its outputs, over a list holding that one memo (empty for a total
unshield); see Memo binding.
Private inputs
| Signal | Description |
|---|---|
note_value | Value of the spent note |
note_asset_id | Asset of the spent note; must equal asset_id |
note_blinding | Blinding factor of the spent note's commitment |
spending_key | Key that derives the owner key and the nullifier |
path_elements[20] | Sibling hashes of the Merkle path |
path_indices[20] | Direction at each level (0 left, 1 right), constrained to be binary |
change_value | Value of the change note; 0 for a total unshield |
change_blinding | Blinding factor of the change commitment |
change_owner_pubkey | Baby JubJub Ax of the change note's owner |
Checks outside the circuit
The pallet runs its own checks around the proof: the call is unsigned, the circuit version is
supported, the fee meets the minimum, the root is known, the nullifier is canonical and unused, the
amount is non-zero, the pool holds amount + fee, the memo has the right size, the recipient is
allowed, and the change commitment is canonical and new. The full list, in order, with every error and
pool-rejection code, is in Spend Validity.
Circuit parameters
| Parameter | Value |
|---|---|
| Circuit id | 2 |
| Active version | 3 |
| Constraints | 17,290 |
| Tree depth | 20 (1,048,576 notes per tree) |
| Public inputs | 8 |
| Private inputs | 47 field elements (7 scalars, 20 path elements, 20 path indices) |
| Proving scheme | Groth16 over BN254 |
| Verification | A few milliseconds on-chain, independent of the constraint count; see Prepared keys |
The unshield keys come from a single-contributor trusted setup; a multi-party ceremony is planned before mainnet. See Trusted Setup.
Known limitations
- The amount, recipient, asset and fee of a withdrawal are public.
- The root used to build the proof must still be known when the transaction executes. A sealed tree's root is kept permanently; a root of the active tree is kept for 300 blocks (about 30 minutes) after a later insert replaces it. Rebuild the proof if submission is delayed past that. See Merkle Forest.
- One unshield spends one note. Withdrawing more than one note holds takes several unshields, or a private transfer that merges notes first.
Related
- Unshield (user guide)
- Spend Validity: every check an unshield must pass
- Transfer Circuit: key derivation and memo binding
- Shield Circuit
- Spend Fees
- Trusted Setup