Transfer Circuit
A private transfer spends one or two notes and creates two new ones, entirely inside the shielded pool. The proof shows the spend is valid without revealing the sender, the recipient, the amounts or which notes were spent.
What the circuit proves
Transfer is circuit id 1; version 3 is active. Its proof attests that:
- Each real input note sits in a tree of the Merkle forest, proven against that input's own root.
- The prover knows the spending key of each input note: the key derives the owner key inside the circuit, and it is canonical (see Canonical spending key).
- Each real input reveals its correct nullifier,
Poseidon2(commitment, spending_key). - Both output commitments are computed correctly.
- Value is conserved:
input_values[0] + input_values[1] = output_values[0] + output_values[1] + fee. - Every input value, output value and the fee fit in 128 bits, so no sum can wrap around the field.
- All four notes have the same asset, and it equals the public
asset_id. - A dummy input reveals a zero nullifier, and two real inputs are distinct notes.
- The proof is bound to the exact output memos submitted (see Memo binding).
Public inputs
In circuit order, which is also the order the verifier encodes them in:
| # | Signal | Description |
|---|---|---|
| 0 | merkle_roots[0] | Root input 0 is proven against |
| 1 | merkle_roots[1] | Root input 1 is proven against; a dummy input repeats the real root |
| 2 | nullifiers[0] | Nullifier of input 0; zero for a dummy |
| 3 | nullifiers[1] | Nullifier of input 1; zero for a dummy |
| 4 | commitments[0] | Commitment of output 0 |
| 5 | commitments[1] | Commitment of output 1 |
| 6 | asset_id | Asset of every note (u32) |
| 7 | fee | Spend fee deducted from the inputs (u128) |
| 8 | memo_hash | Digest of the output memos, recomputed by the pallet |
Every value is a 32-byte little-endian field element. The pallet builds fee, asset_id and
memo_hash from the extrinsic's own arguments, so a caller never passes them as field elements.
The asset and the fee are public; amounts, owners and the link between inputs and outputs are not. The fee is credited to the validator that authored the block, see Who receives the fee.
One root per input
Each input is proven against its own root, so the two notes may sit in different trees of the forest.
The private_transfer extrinsic takes merkle_roots: [Hash; 2] in input order.
| Spend | merkle_roots |
|---|---|
| Two notes of one tree | [r, r] |
| One note and a dummy | [r, r]: the dummy repeats the real root |
| Two notes of different trees | [r0, r1] |
Every root must be one the chain knows: the active tree's current root, a recent one still within its retention window, or a sealed tree's final root. See Spending notes from two trees.
Dummy inputs
The circuit always has two input slots and two output slots. A one-note spend fills the second input
with a dummy: a slot with value = 0.
is_dummy[i] = IsZero(input_values[i])
IsZero is deterministic in R1CS, so a prover cannot mark a slot with a non-zero value as a dummy.
| For a dummy slot | |
|---|---|
| Not enforced by the circuit | Merkle membership against merkle_roots[i]; the nullifier derived from the note |
| Still enforced by the circuit | nullifiers[i] = 0; the canonical-key bound; the one-asset rule; the 128-bit range check |
| Enforced by the pallet | The dummy's root repeats the real input's root; at least one input is real |
The zero nullifier stops a prover from naming a real note in a slot that skips membership. The pallet's root rule matters because the circuit leaves a dummy's root free: without it, that slot would carry a value of the submitter's choosing. A transfer whose inputs are both dummies would insert notes backed by nothing, so the pallet refuses it.
Ownership: Baby JubJub key derivation
Ownership is proven by key derivation, not a signature. The circuit derives the owner public key from the spending key inside the R1CS system:
owner_pk = BabyPbk(spending_key).Ax
The prover must know the scalar whose product with the Baby JubJub base point Base8 gives the Ax
inside the input note's commitment. That is a discrete-logarithm relation on the curve: it cannot be
faked, and the prover cannot substitute another owner key, because the derived Ax feeds the
commitment that must be in the tree. See Keys for where the spending key comes
from.
Canonical spending key
BabyPbk accepts any scalar below 2^253, but Base8 generates a subgroup of prime order
l ≈ 2^250.6 (BABYJUB_SUBORDER). The keys k, k + l, … therefore derive the same owner key, and so
the same note, while each gives a different nullifier Poseidon2(commitment, key). Without a bound,
one note could be spent several times.
The circuit requires every spending key, dummy slots included, to be below l:
spending_key < BABYJUB_SUBORDER
so each note has exactly one valid nullifier. The bound is checked after BabyPbk's own 253-bit
decomposition, which keeps the comparison in range. Wallets derive keys in [1, l), so every honest
key satisfies it. The unshield circuit applies the same check.
Memo binding
The encrypted memos carry the output notes' secrets but are not otherwise part of the statement. The last public input binds them to the proof:
memo_hash = blake2_256(SCALE(memos)) read little-endian, mod r
memos is the list of output memos in output order, each exactly 180 bytes. The prover computes the
digest over the memos it will submit. The circuit takes memo_hash only as a public input (a square
constraint keeps the signal from being optimised away); the binding comes from the pallet, which
recomputes the digest from the memos in the extrinsic and passes it to the verifier. A proof built
for other memos fails.
This stops a copy of a pending transfer from being resubmitted with substituted memos. The memos are how the recipient finds and decrypts the note, so swapping them would spend the sender's notes while leaving the outputs undiscoverable: the funds would sit in the tree with nobody able to open them. The unshield circuit binds its change memo the same way.
Private inputs
Input notes
| Signal | Description |
|---|---|
input_values[2] | Note values; 0 marks a dummy slot |
input_asset_ids[2] | Asset of each input note |
input_blindings[2] | Blinding factor of each input commitment |
spending_keys[2] | Keys that derive each owner key and nullifier |
input_path_elements[2][20] | Sibling hashes of each Merkle path |
input_path_indices[2][20] | Direction at each level (0 left, 1 right), constrained to be binary |
Output notes
| Signal | Description |
|---|---|
output_values[2] | Values of the new notes |
output_asset_ids[2] | Asset of each output note |
output_owner_pubkeys[2] | Baby JubJub Ax of each output owner |
output_blindings[2] | Blinding factor of each output commitment |
Checks outside the circuit
The pallet runs its own checks before and after verification: the call is unsigned, the circuit version is supported, the fee meets the minimum, the roots are known, the nullifiers are unused and canonical, the outputs are new, non-zero and distinct, and each memo is exactly 180 bytes. The full list, in order, with every error and pool-rejection code, is in Spend Validity.
Circuit parameters
| Parameter | Value |
|---|---|
| Circuit id | 1 |
| Active version | 3 |
| Constraints | 34,200 |
| Tree depth | 20 (1,048,576 notes per tree) |
| Public inputs | 9 |
| Private inputs | 96 field elements (16 scalars, 2 × 20 path elements, 2 × 20 path indices) |
| Proving scheme | Groth16 over BN254 |
| Verification | A few milliseconds on-chain, independent of the constraint count; see Prepared keys |
The transfer keys come from a single-contributor trusted setup; a multi-party ceremony is planned before mainnet. See Trusted Setup.
Known limitations
- The circuit spends exactly two input slots and creates exactly two outputs; a one-note spend uses a dummy slot.
- A spend across two trees reveals which two trees its notes came from and links them in one transaction. Wallets prefer a pair from one tree.
- The circuit binds the memos so they cannot be swapped, but their confidentiality rests on the memo encryption; see Note Discovery.
Related
- Private Transfer (user guide)
- Spend Validity: every check a transfer must pass
- Unshield Circuit and Shield Circuit
- Notes: commitments and nullifiers
- Merkle Forest: trees, roots and paths
- Spend Fees: who the fee is credited to
- Trusted Setup