Shield Circuit
Shield moves public tokens into the shielded pool and inserts one note commitment into the active tree of the Merkle forest. The deposit itself is public; the proof ties the new commitment to exactly the amount and asset that were paid in.
Why a deposit needs a proof
A commitment is an opaque hash, Poseidon4(value, asset_id, owner_pk, blinding) (see
Notes). Without a proof, nothing on-chain connects the value inside it to the
amount actually deposited: a deposit of 1 could insert a note worth 1,000, and spending that note
would drain other depositors' funds.
The shield circuit closes that gap. The pallet supplies value and asset_id from the call itself,
and the proof shows that the commitment opens to exactly those two values.
What the circuit proves
Shield is circuit id 3; version 1 is active. It proves one statement:
I know
owner_pubkeyandblindingsuch thatcommitment = Poseidon4(value, asset_id, owner_pubkey, blinding).
The note commitment recomputed from the inputs must equal the public commitment. Nothing else is
constrained.
Public inputs
In circuit order, which is also the order the verifier encodes them in:
| # | Signal | Source | Encoding |
|---|---|---|---|
| 0 | commitment | the call's commitment | 32-byte little-endian field element |
| 1 | value | the call's amount (u128) | little-endian in the low 16 bytes |
| 2 | asset_id | the call's asset_id (u32) | little-endian in the low 4 bytes |
The caller never passes these as field elements: the verifier builds them from the extrinsic's own arguments. A key with any other number of inputs fails verification.
Private inputs
| Signal | Description |
|---|---|
owner_pubkey | Baby JubJub Ax of the note owner |
blinding | Blinding factor of the commitment |
The wallet already holds both when it builds the note, so proving needs no Merkle path and no spending key.
What it does not bind
owner_pubkey is private and unconstrained. A commitment to a key the depositor does not hold only
locks the depositor's own funds.
Shield has no memo_hash input. A shield is a signed transaction: nobody can copy it and swap the
memo without paying the deposit themselves.
There is no range check on value: the pallet sets it from the call's u128 amount. Transfer and
unshield range-check it again whenever the note is spent.
The depositor, amount, asset and commitment of a shield are already visible on-chain. The proof adds a binding, not a disclosure.
How the pallet checks a shield
shield(asset_id, amount, commitment, encrypted_memo, proof, circuit_version) is a signed call. It
runs every check that needs no proof first, so a malformed request costs no verification:
| Step | Check | Error |
|---|---|---|
| 1 | The asset exists, is verified and is the native asset (0, ORB) | InvalidAssetId, AssetNotVerified, AssetNotSupported |
| 2 | amount is non-zero | InvalidAmount |
| 3 | The memo is exactly 180 bytes | InvalidMemoSize |
| 4 | The commitment is a canonical field element and non-zero | InvalidPublicSignals |
| 5 | The commitment is not already in the forest | CommitmentAlreadyExists |
| 6 | The proof verifies under shield circuit_version | ProofVerificationFailed; the verifier's UnsupportedCircuitVersion for an unknown or retired version |
| 7 | amount moves from the depositor to the pool account with KeepAlive | balance error |
| 8 | The commitment is inserted and the memo stored | MerkleTreeFull |
KeepAlive means a shield cannot take the depositor's free balance below the existential deposit.
Only the native asset moves today because the pool holds the native currency for every operation;
a note of any other asset would be backed by ORB. On success the pallet emits Shielded with the
depositor, amount, commitment, memo and leaf index.
shield_batch takes 1 to 20 entries, each with its own proof and circuit_version, and runs them
in order exactly as shield would. The first failing entry reverts the whole batch; an empty batch
fails with EmptyBatch. The app submits single shields.
Through the EVM, the ShieldedPool precompile takes the same
proof and version, with the amount in msg.value.
Circuit parameters
| Parameter | Value |
|---|---|
| Circuit id | 3 |
| Active version | 1 |
| Constraints | 736 |
| Public inputs | 3 (commitment, value, asset_id) |
| Private inputs | 2 (owner_pubkey, blinding) |
| Tree depth | None: the circuit checks the commitment, not its position |
| Proving scheme | Groth16 over BN254 |
The shield keys come from a single-contributor trusted setup; a multi-party ceremony is planned before mainnet. See Trusted Setup.
Related
- Shield (user guide): depositing from the app
- Transfer Circuit and Unshield Circuit: the spends that consume the note
- Notes: what the commitment encodes
- Merkle Forest: where the commitment is inserted
- ZK Verifier Reference: version resolution and input layouts