> For the complete documentation index, see [llms.txt](https://docs.tonadocash.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tonadocash.com/protocol-reference/circuit-spec.md).

# Circuit spec

The protocol has three Groth16 circuits over BLS12-381:

| Circuit       | File                                          | Purpose                                                                   | Verifier output                                                                               |
| ------------- | --------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Withdraw      | `apps/contracts/circuits/withdraw.circom`     | Prove ownership of a deposit at withdrawal time                           | `contracts/verifier.tolk` (`groth16VerifyPayload`)                                            |
| Merkle-update | `apps/contracts/circuits/merkleUpdate.circom` | Prove `newRoot = insert(prevRoot, commitment, idx)` on deposit            | `contracts/verifier_merkle_update.tolk` (`mugroth16VerifyPayload`)                            |
| **AM Claim**  | `apps/contracts/circuits/amClaim.circom`      | Prove a commitment is in the tree, derive a domain-separated AM nullifier | `contracts/verifier_am.tolk` (`amgroth16VerifyPayload`) - currently a **STUB** until ceremony |

This page details the withdraw circuit. For the AM circuit, see [anonymity-mining.md](/protocol-reference/anonymity-mining.md). For the merkle-update circuit, see [contracts-spec.md](/protocol-reference/contracts-spec.md).

## File

`apps/contracts/circuits/withdraw.circom`

## Statement

> The prover knows `(nullifier, secret)` such that:
>
> * `commitment = Poseidon(nullifier, secret)` is a leaf of the Merkle tree with the claimed `root`, witnessed by `(pathElements, pathIndices)`.
> * `nullifierHash = Poseidon(nullifier)` matches the claimed `nullifierHash`. AND additionally binds:
> * `recipient`, `relayer`, `fee`, `refund` to the proof as public inputs, so the relayer cannot substitute alternative values for them.

## Public inputs (in order)

| # | Name            | Type   | Notes                                               |
| - | --------------- | ------ | --------------------------------------------------- |
| 0 | `root`          | bigint | A recent root from the pool's root-history ring.    |
| 1 | `nullifierHash` | bigint | Poseidon(nullifier).                                |
| 2 | `recipient`     | bigint | Address-as-field-element (`addressToFieldElement`). |
| 3 | `relayer`       | bigint | Address-as-field-element.                           |
| 4 | `fee`           | bigint | nanoTON or jetton-base-units. `≤ denomination`.     |
| 5 | `refund`        | bigint | nanoTON refund to recipient. `= 0` for native pool. |

## Private inputs

| Name               | Type        | Notes                                             |
| ------------------ | ----------- | ------------------------------------------------- |
| `nullifier`        | bigint      | 31-byte random.                                   |
| `secret`           | bigint      | 31-byte random.                                   |
| `pathElements[20]` | bigint\[20] | Sibling hashes at each tree level.                |
| `pathIndices[20]`  | int\[20]    | Bit per level: 0 = leaf is left child, 1 = right. |

## Constraints

Approx. 30,000 R1CS constraints (final count depends on Poseidon parameters):

* 1 Poseidon(2) for commitment
* 1 Poseidon(1) for nullifierHash
* 20 Poseidon(2) for Merkle path verification (`HashLeftRight` per level)
* 20 bit-decomposition checks (`s * (1 - s) = 0`)
* 4 squarings (`recipient^2`, `relayer^2`, `fee^2`, `refund^2`), used purely to bind these signals into the proof; the squarings have no semantic meaning but make the values un-droppable by the prover.

The squaring trick is borrowed from the original Tornado circuit. See `apps/contracts/circuits/withdraw.circom` for the exact source.

## Hash function: Poseidon on BLS12-381

We import `circomlib/circuits/poseidon.circom`. circomlib's Poseidon round constants and MDS matrix were originally derived for the BN254 scalar field. The Poseidon spec is field-parameterised; the same round counts and MDS *structure* are valid over BLS12-381's scalar field, with constants reduced modulo the new field's prime.

The Poseidon parameters circomlib chose (R\_F = 8 full rounds, R\_P partial rounds depending on arity) were security-analysed against generic algebraic attacks (Grobner basis, interpolation, statistical) on the *permutation*, which is mostly field-independent. The reduction mod BLS12-381's scalar field doesn't introduce known weaknesses.

**Implementation cross-check:** the pool no longer hashes on-chain. Poseidon lives entirely in the off-chain prover and the circuit. What still needs to agree byte-for-byte is:

1. The off-chain Poseidon in `packages/core/src/poseidon.ts` (used to compute commitments and Merkle paths off-chain).
2. The circuit's Poseidon (used inside the proof).

(1) is in fact the circom-compiled poseidon wasm, loaded at runtime, so (1) and (2) are the same artifact and parity is by construction. The `check-poseidon-parity.ts` script in `apps/contracts/scripts/` drives the wasm directly to catch artifact-staleness regressions; CI runs it as a separate job.

## Why no compliance / "secret message" extension

The original Tornado Cash circuit eventually added an optional encrypted "compliance note" to support voluntary disclosure to auditors. We omit this in v1; it can be added later without breaking existing notes by introducing a v2 pool with the extended circuit.
