Skip to main content

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:

  1. Each real input note sits in a tree of the Merkle forest, proven against that input's own root.
  2. 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).
  3. Each real input reveals its correct nullifier, Poseidon2(commitment, spending_key).
  4. Both output commitments are computed correctly.
  5. Value is conserved: input_values[0] + input_values[1] = output_values[0] + output_values[1] + fee.
  6. Every input value, output value and the fee fit in 128 bits, so no sum can wrap around the field.
  7. All four notes have the same asset, and it equals the public asset_id.
  8. A dummy input reveals a zero nullifier, and two real inputs are distinct notes.
  9. 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:

#SignalDescription
0merkle_roots[0]Root input 0 is proven against
1merkle_roots[1]Root input 1 is proven against; a dummy input repeats the real root
2nullifiers[0]Nullifier of input 0; zero for a dummy
3nullifiers[1]Nullifier of input 1; zero for a dummy
4commitments[0]Commitment of output 0
5commitments[1]Commitment of output 1
6asset_idAsset of every note (u32)
7feeSpend fee deducted from the inputs (u128)
8memo_hashDigest 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.

What is public

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.

Spendmerkle_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 circuitMerkle membership against merkle_roots[i]; the nullifier derived from the note
Still enforced by the circuitnullifiers[i] = 0; the canonical-key bound; the one-asset rule; the 128-bit range check
Enforced by the palletThe 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​

SignalDescription
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​

SignalDescription
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​

ParameterValue
Circuit id1
Active version3
Constraints34,200
Tree depth20 (1,048,576 notes per tree)
Public inputs9
Private inputs96 field elements (16 scalars, 2 × 20 path elements, 2 × 20 path indices)
Proving schemeGroth16 over BN254
VerificationA few milliseconds on-chain, independent of the constraint count; see Prepared keys
Single-contributor setup

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.