Protocol
Proofs and circuits

The transaction circuit

The fixed 3×3×2×2 circuit behind every deposit, private transfer and withdrawal, its 53 public values, the fifteen conditions it proves, and what it leaves to the program and the attesters.

One circuit proves every deposit, private transfer (Private Send) and withdrawal. It has a fixed shape, so every value transaction produces a proof of the same size over the same 53 public values, whatever it actually does.

Shape

The circuit is instantiated once, as Transaction(3, 3, 2, 2, 10, 8, 1, 4, 2, 8):

  • 3 input notes, spent from one note tree;
  • 3 output notes, appended to a note tree;
  • 2 inflow lanes, each a deposit of one token from one public account;
  • 2 outflow lanes, each a withdrawal of one token to one public account;
  • note and identity trees of 10 levels of 8 children and one level of 4, 2^32 leaves;
  • a zone signer-set tree of 2 levels of 8 children, 64 leaves.

Unused slots are filled with dummy notes (amount zero) and idle lanes (amount zero), so a deposit, a transfer and a withdrawal are indistinguishable by shape. The compiled circuit has 89,801 non-linear constraints (circom 2.2.3, optimisation level 2), under a pinned budget of 114,500, and fits a setup of 2^17.

Notes spent
input note
input note
input note
Deposit lanes
deposit
deposit
One transaction
same shape every time
Notes created
output note
output note
output note
Withdrawal lanes
withdrawal
withdrawal
Every value transaction has this shape. Unused slots are dummies.

How the circuit is organised

The circuit takes a large private witness (your keys, the notes being spent and created, their Merkle paths, the fee allocation and any zone signatures) and a single public input. The chart below shows how the checks are grouped.

flowchart LR
P["Private inputs"] --> C1["Identity and ownership"]
P --> C2["Notes and nullifiers"]
P --> C3["Fees and conservation"]
P --> C4["Zone approval"]
P --> C5["Records and key sharing"]
C1 --> F["Fold of 53 values"]
C2 --> F
C3 --> F
C4 --> F
C5 --> F

None of the groups produces a public value on its own terms. Each one ties private data to values that also appear in the fold: a tree root, a nullifier, a commitment, a lane, a rate. Because the shielded pool program rebuilds the fold from its own accounts and the signed payload, a proof only verifies if every group's private data agrees with the values the program supplied. The fifteen conditions under what the circuit proves are those groups, one check at a time.

The 53 public values

The circuit's single public input is a Poseidon fold of 53 values in a fixed order. The program computes the same fold itself and never accepts it from the transaction. The values fall into the groups below, in fold order. Addresses enter as two 128-bit halves because a 256-bit Solana address does not fit one field element. An idle lane contributes five zeros.

Roots and the clock (slots 0 to 2)

Slot 0 is the root of the note tree the inputs are spent from, and slot 1 the root of the identity tree. The payload names each by sequence number, and the program reads the root from the tree account's recent-root history, refusing one that has aged out. Slot 2 is the execution slot, the slot against which the circuit checks each spent note's validity window. The payload names it, and the program refuses it if it is in the future or older than the process policy's proof-age window, which governance can set between 300 and 216,000 slots.

Context digest (slots 3 and 4)

The program computes a Keccak-256 hash over the payload up to the attester signature and the addresses of all 39 accounts, the submitter's first, and folds its low and high 128 bits. The six attesters sign the same digest. This is what binds the proof to one transaction: the same proof submitted with a changed payload or a different account list produces a different digest, so the fold no longer matches.

Nullifiers and output commitments (slots 5 to 10)

Slots 5 to 7 are the three nullifiers and slots 8 to 10 the three output note commitments, all taken from the payload. The program stages the nullifiers in the staging buffer and appends the commitments to the note tree, so the values the proof covers are the values that change the pool's state.

Deposit lanes (slots 11 to 20)

Each of the two inflow lanes takes five slots: the token address in two halves, the amount, and the depositor's address in two halves. The program takes the token and the depositor from the accounts, and the amount from its own measurement: it moves the tokens into the pool token account (for SOL, the pool's lamport account), reads the balance before and after, and folds the difference. The amount the payload requested is not folded, so the proof must match what actually arrived, whatever the payload asked for.

Withdrawal lanes (slots 21 to 30)

Each outflow lane also takes five slots: the token address, the amount and the destination owner, with both addresses in halves. The token comes from the asset table, the amount from the payload, and the destination owner from the destination token account's owner field. For SOL, which is paid out as lamports, the destination address itself is folded. A proof made for one destination therefore cannot pay another.

Relayer tip and fee settings (slots 31 to 39)

Slot 31 is the relayer's tip and slots 32 and 33 its token address. The tip comes from the payload; its token comes from an entry in the asset table, or from a separate mint account when the tip's token is not in the asset table. Slot 34 is the root of the fee-rate tree and slots 35 to 38 the default rates for deposit, transfer, withdraw and pass-through, in basis points. Slot 39 packs four self-exemption flags, one per action. The program reads all of slots 34 to 39 from the process policy account, so the fee schedule the proof is checked against is governance's, not the prover's.

Fee commitments and voucher (slots 40 to 43)

Slots 40 to 42 are the fee commitments, one per token slot, and slot 43 the fee voucher commitment. All four come from the payload. The fee enclave sees the openings behind them and recomputes them before it signs, and the program stages the three fee commitments in the fee store, from which a later insert appends them to the fee tree for the treasury.

Records and key sharing (slots 44 to 47)

Slot 44 is the privacy artifacts root, which commits to the encrypted sender record, the viewing-key share leaves and the disclosure sections. Slot 45 is the public randomness that goes into the per-transaction viewing key. Both come from the payload. Slots 46 and 47 are the sharing threshold and node count, which the program reads from the guardian cluster's account.

Zone signer set (slot 48)

Slot 48 is the root of the zone signer-set tree, which lists every zone authority's key with its zone. The payload names it by sequence number, and the program reads it from the zone registry's signer-set account.

Program-owned spends (slots 49 to 51)

A note can be owned by a Solana address rather than an identity, so that a program can hold private balance. Slots 49 and 50 are the address of the spender authority that owns such a note, and slot 51 a mask with one bit per input marking which inputs are address-owned. The program takes the address from an account that must sign, or folds zero when the transaction spends no address-owned note. The mask comes from the payload, and the program refuses a payload whose mask is not zero unless it also flags a program-owned spend, which is what makes the authority's signature required.

Cross-pool flag (slot 52)

A transaction names two pool indexes: the one it spends from and the one it appends its outputs to. Slot 52 is 1 when they differ and 0 when they are the same. The program computes it from the two pool indexes in the payload, which are public already, so the flag discloses nothing new. The circuit uses it in condition 15.

What the circuit proves

The conditions below hold for every accepted proof. "Real" means a note whose amount is not zero.

1. The sender is a registered identity

The circuit rebuilds the sender's identity commitment (sometimes called the user commitment) from the spending key, the nullifier key and the outgoing viewing key with their blinders, plus the five other slot leaves as given. It then proves that identity commitment is a leaf of the identity tree at the root in slot 1.

The check is unconditional, so even a pure deposit that spends nothing has a registered sender. Registration is where an identity's outgoing viewing key was shared with the guardian nodes, and the identity commitment fixes that key, so this check ensures the per-transaction viewing key derived below comes from a key the guardian nodes hold shares of. Knowing the opening is the whole of spend authority; the spending key signs nothing.

The circuit also opens the platform wallet slot: it rebuilds that slot's leaf from the platform wallet's address and blinder. The address is used below to recognise transfers to and from yourself.

2. Every note has a valid kind

Each of the six notes has kind 0 (dummy), 1 (owned by an identity) or 2 (owned by a Solana address). A note is a dummy exactly when its amount is zero. Dummies are exempt from the tree-membership and validity checks below, so this rule is what stops a prover from labelling an invented note as a dummy to skip those checks while still counting its amount.

3. The sender owns every input

An identity-owned input's owner must be the sender's identity commitment. An address-owned input's owner must be the program owner commitment, a Poseidon hash of the spender authority address in slots 49 and 50. The program takes that address from an account that signed. Together this stops anyone from spending a note they do not own: an identity-owned note needs the owner's keys, and an address-owned note needs the owning address's signature on the transaction. When no input is address-owned, the spender address must be zero, so the mask and the address cannot disagree.

4. Every real input is in the note tree

For each real input, the circuit recomputes the note commitment from its thirteen fields, wraps it into a tree leaf with its insertion slot and insertion position, and proves the leaf is in the note tree at the root in slot 0. Dummy inputs are exempt. The chain from note to root is shown below.

  1. Thirteen note fields
  2. Note commitment
    Poseidon hash
  3. Tree leaf
    with insertion slot and position
  4. Note-tree root
    slot 0
A spent note is proven to exist by rebuilding its leaf and its path to a recent root.

This stops a prover from spending a note that was never created. Because the leaf includes the insertion position, it also fixes the position the nullifier is computed from, so one note cannot be presented at a second position to obtain a second nullifier.

5. Every real input is within its validity window

A real input's valid-after slot must not exceed the execution slot, and its valid-before slot must be zero (no expiry) or greater than the execution slot. The execution slot is the one the program checked against the clock. A note can therefore never be spent before its valid-after slot. Because the execution slot may lag the clock by up to the proof-age window, a note can still be spent for up to that many slots after its valid-before slot.

6. The nullifiers are correct and distinct

For every input, dummies included, the circuit derives the nullifier as a keyed Poseidon hash of the note's insertion position and nullifier blinding factor, keyed by the owner's nullifier key, and requires it to equal the published nullifier. For an address-owned note the key is the owner's program owner commitment, which anyone can compute, so such a spend is linkable.

A correct derivation means each real note has exactly one nullifier, which is what lets the nullifier tree and the staging buffer catch a second spend. The three nullifiers must also be pairwise distinct, so one transaction cannot list the same note twice and count its value twice. Because dummies publish nullifiers too, the nullifiers do not reveal how many notes were spent.

7. The outputs are well formed and correctly funded

Each output commitment is the Poseidon hash of the note's thirteen fields, with the version and kind range-checked to 16 bits, the zone to 20 bits, the amount to 64 bits and the validity slots to 48 bits. Both of every output's blinding factors, the one that hides the commitment and the one its nullifier is later derived from, must be non-zero, dummy outputs included. The escape-hatch circuit refuses a note with either blinding at zero, so a note created with one could never leave through the escape hatch; refusing it here, when the note is made, keeps every real note escapable. An identity-owned output's owner must be in the identity tree at slot 1's root, so nobody can pay an unregistered identity. An address-owned output's owner must be the program owner commitment of an address whose halves are range-checked to 128 bits.

Each output's two funding fields are forced. The user funding field is the sender's identity commitment when some identity-owned input carries the same token, and zero otherwise; the program funding field is the same rule for address-owned inputs. A sender cannot leave these out or put someone else's identity in them. This is how a recipient learns the sender's identity commitment when the sender spent a same-token note.

8. The lanes are consistent

A lane whose amount is zero is idle, and its token address and counterparty must be zero. A relayer tip of zero must name no token. This gives an unused lane exactly one encoding, five zeros, which is what the program folds for a lane the transaction did not activate. Inside the circuit a lane's token is identified by the Poseidon hash of its address's two halves, the same identifier every note carries in its token field, so lanes and notes can be compared token by token.

9. Every value is in range

Amounts in lanes, notes and the relayer tip are 64-bit; the execution slot is 48-bit; each default rate is at most 10,000 basis points; owner, depositor, destination and relayer-token address halves are 128-bit. Range checks matter because the circuit's arithmetic is modulo a 254-bit prime: without them, a huge "amount" could wrap around and make a false balance equation hold.

Two kinds of value are not range-checked in the circuit: the halves of the lane token addresses and the halves of the context digest. The program supplies both, from accounts it has checked and a hash it computed, so a prover cannot choose them.

10. The fees are at least what the rates require

The fee logic has five parts.

Token slots. The transaction declares up to three token slots, and every live lane and every real note must use exactly one of them. A transaction therefore moves at most three tokens.

Rates. For each slot the circuit looks up four rates, one per action, in the fee-rate tree (two levels of eight, one leaf per token), and each must be at most that action's default. A token without a leaf pays the defaults. A prover can say a token has no leaf, but that only applies the defaults, which are never lower.

Vouchers. A table of up to three entries adjusts the rates per token and action. The adjusted rate must stay between 0 and 10,000 basis points, and where the process policy's self-exemption flag for an action is set, the rate for moving value between the same owner is zero. The table is committed in the fee voucher commitment (slot 43) together with the transaction's nullifiers and a blinding. The fee enclave fills the table from its own schedule, recomputes the voucher commitment and the fee commitments, and signs a digest built with its own values in place of the payload's. A payload carrying a voucher the enclave did not grant therefore gets a signature that does not match it, so a prover cannot grant itself a discount.

Allocation. A five-by-five matrix assigns value from each source (two inflow lanes, three inputs) to each destination (three outputs, two outflow lanes). Each destination's amount must be fully covered by its column, no source may give out more than it has, and value only moves between the same token. Each cell's action follows from its position: lane to note is a deposit, note to note a transfer, note to lane a withdrawal, lane to lane a pass-through. A cell between the same owner is charged the self rate, any other the cross rate; the two are equal unless the action's self-exemption flag is set. Your platform wallet counts as you, so a deposit from it into your own note is a movement between the same owner.

Rounding. Each destination's fee must be at least the rounded-up total of its cells' charges. The circuit proves a lower bound; it does not pin the fee to exactly that number, so a prover may pay more but never less.

The fees for each token slot are summed into a fee commitment, a Poseidon hash of the token identifier, the total and two blindings, published in slots 40 to 42 and staged for the treasury's fee tree.

11. Value is conserved per token

For every token that appears in the transaction, including the relayer tip's token:

spent notes + deposits = new notes + protocol fees + withdrawals + relayer tip

The equation is checked in the field, and the range checks keep every sum far below the field modulus, so it holds as ordinary integer arithmetic. This is the check that stops value being created inside the pool: no combination of notes and lanes can pay out more of a token than came in. See value conservation.

12. Zone rules hold

All real inputs must share one zone, the source zone. When the transaction spends at least one real input and the source zone is not zero, the circuit verifies the source zone authority's EdDSA-Poseidon signature on BabyJubJub over a hash of the context digest, and proves that the authority's key and zone are a leaf of the zone signer-set tree at slot 48's root. When there is at least one real output and the destination zone is not zero, a second signature from the destination zone's authority is verified the same way under a different domain, so one signature cannot stand in for the other.

Every real output in a non-zero zone must be in the source zone (when there is a real input) or the destination zone. Together these stop notes leaving a zone, or arriving in one, without that zone's authority approving this exact transaction. Zone 0 opts out of all of this. The zones and the keys are private inputs, so the public values reveal only the signer-set root. See zones.

13. The sender record and disclosure sections are correct

The circuit encrypts a 19-lane sender record with a Poseidon keystream under the per-transaction viewing key. The lanes are the sender's identity commitment, the three input tokens and amounts, and for each output its owner (two lanes), token and amount. Each lane also gets a commitment to its keystream value under a separate blinding key.

It then builds two disclosure sections from per-field commitments to each note's owner, amount, token, creator and kind (and each input's insertion slot). The source section covers the inputs and the outputs that stay in the source zone; the destination section covers the outputs that land elsewhere. Because the circuit computes all of this from the same notes it checks above, whoever later decrypts the record or opens a section sees the transaction that actually happened, not one the sender described. See disclosure.

14. The per-transaction viewing key is shared correctly

The per-transaction viewing key is a Poseidon hash of the sender's outgoing viewing key, the three nullifiers and the public randomness in slot 45. The circuit splits it into three Shamir shares, any two of which rebuild it, and commits each share with a blinding share into a share leaf. The path from your key to the public root is shown below.

  1. Outgoing viewing key
    with the nullifiers and randomness
  2. Per-transaction viewing key
  3. Three Shamir shares
    any two rebuild the key
  4. Three share leaves
    each blinded
  5. Privacy artifacts root
    slot 44
The per-transaction viewing key is derived, split and committed inside the proof.

The slope of the sharing line is proven non-zero. With a zero slope every share would equal the key itself, and any single guardian node could read the transaction. The 19 ciphertexts, the 19 lane commitments, the three share leaves and the two disclosure sections fold into the privacy artifacts root in slot 44. The threshold 2 and node count 3 are constants of the circuit, and the program supplies the cluster's actual values in slots 46 and 47, so the proof fails for a cluster of any other size. See secret sharing.

15. A transaction that spends nothing stays in its own pool

When all three inputs are dummies, as in a deposit, the cross-pool flag in slot 52 must be 0, so the outputs are appended to the pool index the transaction names. The flag itself must be 0 or 1. A transaction that spends at least one real note may append to another pool index. Without this rule a transaction that spends nothing from a pool index could still take three places in that pool index's nullifier tree, whose capacity is fixed, while its notes went to another pool index.

What the circuit does not prove

A proof covers only what the circuit can see. The facts below are established somewhere else, and one is not established at all.

The nullifiers have not been spent before. The circuit proves each nullifier is correctly derived, not that it is new. The nullifier-freshness proof shows the nullifiers are absent from the nullifier tree, and the program's staging buffer refuses any nullifier it already holds.

The roots are recent and the slot is current. The circuit accepts whatever roots and slot it is given. The program supplies them from its tree accounts and checks the slot against the clock.

The deposited amount actually arrived. The circuit trusts the lane amount. The program measures the pool token account before and after the transfer and folds what it measured.

The screening subjects are acceptable. The circuit does not establish a screening verdict for the submitter, depositor or withdrawal destination. The screener checks the bound submitter of every transaction and its public deposit or withdrawal parties, and signs only a Clear verdict.

The fee commitments and voucher follow the fee schedule. The circuit proves the fee is at least what the rates and voucher imply, but the voucher's contents are private. The fee enclave recomputes the fee commitments from their openings and the voucher from its own schedule, and signs over its own values.

The encrypted notes match the memo digest, and the artifact leaves match the artifacts root. The encrypted notes for recipients are made outside the circuit. The data endorser recomputes the memo digest from the ciphertexts and the artifacts root from its leaves before it signs.

Each guardian node received the share its leaf commits to. The circuit commits to the shares; it cannot deliver them. Each guardian node checks its own share against its leaf.

The encrypted notes decrypt to the notes committed. Nobody checks this. The data endorser checks that the ciphertexts fold to the memo digest, not that they decrypt to the output notes, so a recipient's ciphertext is not proven to be readable.

The proof-authorisation key committed in slot 3 of your identity is not used by this circuit; its leaf is carried as given.

Where it is verified

process_3x3x2x2 verifies the proof after moving any deposits in, in one pairing with the nullifier freshness proof and the combined attester signature, under the transaction key compiled into the program. See how Umbra uses proofs for the order of checks.

The constraints are modelled by hand in the TransactionCircuit Lean library, which proves no minting, no unfunded withdrawal and funded fees under explicit range assumptions; see formal verification.

Reference

Last updated

On this page