> For the complete documentation index, see [llms.txt](https://hinkal-team.gitbook.io/hinkal/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hinkal-team.gitbook.io/hinkal/technical-description/circuits/solana-circuit.md).

# Solana Circuit

The Solana flow is gated by a Groth16 proof produced from a single Circom 2 template, `basic`, defined in `basicSolana.circom`. Six dimension-specialized variants instantiate it for the dimensions the on-chain program supports:

| File                      | Parameters `(tokenCount, inputCount, outputCount, treeDepth)` |
| ------------------------- | ------------------------------------------------------------- |
| `basicSolana1x2x1.circom` | `(1, 2, 1, 25)`                                               |
| `basicSolana1x2x2.circom` | `(1, 2, 2, 25)`                                               |
| `basicSolana1x6x1.circom` | `(1, 6, 1, 25)`                                               |
| `basicSolana1x6x2.circom` | `(1, 6, 2, 25)`                                               |
| `basicSolana2x2x1.circom` | `(2, 2, 1, 25)`                                               |
| `basicSolana2x6x1.circom` | `(2, 6, 1, 25)`                                               |

`treeDepth = 25` is the depth of the Merkle path the circuit verifies for each input UTXO and for the access-token tree.

The on-chain program dispatches to the verifying key matching `Dimensions(tokenCount, inputCount, outputCount)` — `1×2×1` for `transact2`, `1×6×1` for `transact6`, `1×2×2` for `transfer2`, `1×6×2` for `transfer6`, `2×2×1` for `swap2`, `2×6×1` for `swap6`.

All hashing inside the circuit is **Poseidon** over BN254. Stealth-address derivation uses **BabyJubJub** via `circomlib`.

***

#### Public signals

The `component main` declarations expose the same set of public inputs across every variant:

```
rootHashHinkal
rootHashAccessToken
mintAccountPart1[tokenCount]
mintAccountPart2[tokenCount]
inNullifiers[tokenCount][inputCount]
outTimeStamp
extraRandomization
amountChanges[tokenCount]
outCommitments[tokenCount][outputCount]
calldataHash
```

Plus the circuit's outputs (also public by virtue of being `signal output`):

```
swapperAccountAdditionalSeed
extraRandomizedStealthAddress
extraRandomizedStealthAddressSigns
extraRandomizedStealthAddressH0Ay
extraRandomizedStealthAddressH1Ay
```

Every public signal above is read by the on-chain program through `ParsedPublicSignals` and used to gate state transitions:

* `rootHashHinkal` is checked against the rolling root buffer via `merkle.root_hash_exists`.
* `rootHashAccessToken` is the access-token Merkle root the prover claims membership in.
* `mintAccountPart1` / `mintAccountPart2` together encode the SPL mint as a 256-bit value split into two 128-bit limbs (BN254 `F_r` is shorter than 32 bytes). The handler reassembles them and checks against the provided `Mint` account.
* `inNullifiers` are matched against the `NullifierAccount` PDAs the caller supplies; non-zero ones are flipped to `is_used = true`.
* `outCommitments` are inserted into the commitment Merkle tree.
* `extraRandomization` is required to be non-zero — see "Stealth address" below.
* `amountChanges` drives deposit / withdrawal accounting. Negative ⇒ withdrawal, positive ⇒ deposit, zero ⇒ pure shielded-to-shielded.
* `calldataHash` is the Poseidon binding to recipient, signer, encrypted outputs, fees, CPI instructions, and remaining accounts; recomputed by the program in `get_hashed_calldata`.
* `swapperAccountAdditionalSeed` (output) — bound into the swap PDA derivation, prevents proof reuse against a different swapper PDA.

#### Private inputs

```
shieldedPrivateKey
inAmounts[tokenCount][inputCount]
inTimeStamps[tokenCount][inputCount]
inRandomizations[tokenCount][inputCount]
inCommitmentSiblings[tokenCount][inputCount][treeDepth]
inCommitmentSiblingSides[tokenCount][inputCount][treeDepth]
outAmounts[tokenCount][outputCount]
outPublicKeys[tokenCount][outputCount]
accessTokenSiblings[treeDepth]
accessTokenSiblingSides[treeDepth]
swapperAccountSalt
```

These never leave the prover.

***

#### What the circuit proves

Per token leg `i ∈ [0, tokenCount)` and per input slot `j ∈ [0, inputCount)`:

1. **Mint range** — `MintAccountValidator` enforces `mintAccountPart1[i], mintAccountPart2[i] < 2^128`. Each part is a 128-bit limb; together they encode a full 256-bit Solana mint key.
2. **Mint hashing** — `mintAccountHashed[i] = Poseidon(mintAccountPart1[i], mintAccountPart2[i])`. Used as the token tag inside every commitment so commitments stay BN254-sized regardless of mint length.
3. **Mint distinctness** — for every pair `i ≠ j`, `mintAccountHashed[i] ≠ mintAccountHashed[j]`. A 2-token swap cannot collapse to a single token leg.
4. **Owner public key** — `publicKey = Poseidon(shieldedPrivateKey)`. Bound to the access-token check below.
5. **Per-input stealth public key** — `inPubKey[i][j] = StealthAddressCalculator(shieldedPrivateKey, inRandomizations[i][j])`. Each input UTXO can carry an independent randomization so different deposits do not share an on-chain identifier.
6. **Input commitment recomputation** — `OriginalCommitmentCalculator` rebuilds `commitment = Poseidon(amount, mintAccountHashed[i], inPubKey[i][j], timeStamp)`, returning `0` if `amount == 0` so empty input slots act as padding.
7. **Input amount range** — `OverflowPreventer(inputCount)` bounds `inAmounts[i][j]` so `inputCount` of them cannot overflow `F_r`.
8. **Signature** — `signature[i][j] = Poseidon(shieldedPrivateKey, commitment[i][j])`. Acts as the spending signature for the UTXO.
9. **Nullifier** — `nullifier[i][j] = Poseidon(commitment, signature)` (zero when commitment is zero). Forced to equal the public `inNullifiers[i][j]`.
10. **Membership proof** — `MerkleRootCalculator(treeDepth)` recomputes the commitment Merkle root from `inCommitmentSiblings[i][j]` / `inCommitmentSiblingSides[i][j]` and forces it to equal `rootHashHinkal` whenever `inAmount[i][j] != 0` (`ForceEqualIfEnabled` skips empty slots).
11. **Output commitments** — for each output slot `(i, k)`, `OriginalCommitmentCalculator` rebuilds `Poseidon(outAmount, mintAccountHashed[i], outPublicKey, outTimeStamp)` and forces equality with the public `outCommitments[i][k]`.
12. **Output amount range** — `OverflowPreventer(outputCount)` bounds each `outAmount[i][k]`. Negative outputs cannot be encoded (the bit-decomposition rejects them).
13. **Conservation per token** — `Σ inAmounts[i] + amountChanges[i] = Σ outAmounts[i]`. `amountChanges[i]` is the only non-private-balance term; it is what the on-chain program reads as deposit (positive) or withdrawal (negative).
14. **Access-token gate** — `AccessTokenChecker(treeDepth)`:
    * re-derives `publicKey = Poseidon(shieldedPrivateKey)` and forces it to equal the local `publicKey` signal,
    * computes `accessToken = Poseidon(shieldedPrivateKey, publicKey)`,
    * rebuilds the access-token Merkle root from `accessTokenSiblings` and forces equality with `rootHashAccessToken`. The root is checked against the access-token tree on-chain only when the per-mint limit requires it.
15. **Stealth address output** — `extraRandomization` is fed through `StealthAddressCalculator` again to produce the **extra-randomized** key the new commitments are addressed to:
    * `extraRandomizedStealthAddress = Poseidon(signs, H0Ay, H1Ay)`
    * `extraRandomizedStealthAddressSigns`, `H0Ay`, `H1Ay` are the compressed BabyJubJub coordinates that let an external observer reconstruct the keys of new UTXOs from on-chain events. The on-chain program rejects `extraRandomization == 0` (`ZeroRandomizationForbidden`), so every commitment is unlinkable from prior deposits.
16. **Swapper seed binding** — `swapperAccountAdditionalSeed = Poseidon(swapperAccountSalt)`. The on-chain program checks this against the seed used to derive the swap PDA so a relayer cannot retarget a generated proof at a different swapper PDA.

***
