Skip to main content

Transact circuit (NxM)

The transact circuit is the NxM family of spend proofs. A proof states that the prover spent N input notes whose nullifiers are now in the public set, produced M output notes whose commitments are now in the public set, and conserved value both globally (input value must equal output value plus the public amountOut) and per deposit label (the outputs of a label never exceed its inputs).

Variants

The protocol ships 25 variants, transact_NxM for every N and M from 1 to 5. The SDK picks the smallest variant that fits the call's input and output counts.

Tree depths

stateMaxDepth 22. Bounds the state-tree membership proofs for input commitments.
keystoreMaxDepth 18. Bounds the keystore-tree membership proof for the spender's auth policy.
associationSetMaxDepth 18. Bounds the ASP root membership proofs for input labels.

If the SDK's witness depth doesn't match the circuit's compiled depth, proving fails with "Too many values for input signal" errors.

Output composition rule

The circuit itself does not fix an output count per label. It enforces only global conservation, per-label conservation (the outputs of a label never exceed its inputs), and that every output label is inherited from some input.

The familiar one-recipient-plus-one-change shape is an SDK convention layered on top: each distinct deposit label spent emits one recipient note and one change note, no matter how many input notes carry that label, because multiple inputs with the same label aggregate into a single recipient-and-change pair. Under that convention, spending one label produces 2 outputs, which fits the 1x2 or 2x2 variants. Spending two labels produces 4 outputs, which fits 2x4, 3x4, and the larger variants. Spending three labels would need 6 outputs, and no variant goes that high, so such a cycle must be split across transactions.

Public inputs

  • The N nullifiers, which become the spent markers.
  • The M output commitments.
  • The state root, keystore root, and ASP root used in the witness.
  • amountOut (the public withdrawal amount, 0 for a pure transfer).
  • tokenIdOut (the token being withdrawn).
  • context (a commitment to the withdrawal details).

Private inputs

The spender supplies one set of account inputs per proof: ownerAddress, privateNullifyingKey, privateRevocableKey, the keystore membership proof (keystoreLeafIndex, keystoreSiblings, keystoreTreeDepth), and a single tokenId shared by all notes in the call.

Each input note then contributes its value, label, noteSecret, timestamp, a state-tree membership proof, and an ASP-tree membership proof. The timestamp is the block timestamp recorded when the commitment was inserted, and it matters because the state-tree leaf is Poseidon(COMMITMENT_LEAF_TAG, commitment, timestamp), a three-input hash with a domain-separation tag. The input commitment itself is not a signal, because the circuit recomputes it from these fields.

Finally, each output contributes a noteAddressHash (the precomputed Poseidon hash of ownerAddress and noteSecret), an outputValue, and an outputLabel.

Verifier routing

A TransactVerifierRouter contract holds one verifier address per (N, M) pair. Submitting to PoolVault.transact routes to the appropriate verifier.

Source: v2-monorepo/packages/circuits/circuits/templates/Transact.circom and circuits/main/transact/transact_NxM.circom