> 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/protocol/solana-program/proof-pipeline.md).

# Proof Pipeline

Every spending instruction (`transact2`, `transact6`, `transfer2`, `transfer6`, `swap2`, `swap6`) routes through `proof_processing::process_proof_and_state` in `proof_processing.rs`. The pipeline does four things: prove, validate, mutate state, emit events.

#### Inputs

* `MergedProofData` - assembled by `merge_proof_data` from the two `ProofAccount*` PDAs and the `EncryptedOutputsAccount`. Contains `proof_a/b/c`, the full `public_signals: Vec<Bytes32>`, and the encrypted UTXO blobs.
* `Dimensions` - `(token_number, nullifier_amount, output_amount)`. Each instruction pins this to a specific tuple:
  * `transact2 = 1×2×1`
  * `transact6 = 1×6×1`
  * `transfer2 = 1×2×2`
  * `transfer6 = 1×6×2`
  * `swap2 = 2×2×1`
  * `swap6 = 2×6×1`
* `nullifier_accounts` - slice of `Option<Box<Account<NullifierAccount>>>` indexed `[token_index * nullifier_amount + nullifier_index]`. Length must equal `token_number * nullifier_amount`.
* `nullifiers_to_validate` - the raw nullifier bytes the caller is committing to spend.
* `on_chain_creation` - booleans (one per token leg) indicating whether a leg's output commitment will be created on-chain (e.g. swap output) instead of from the proof's encrypted outputs.

#### Step-by-step

```
1. verify_proof(...)                            # Groth16 BN254
2. ParsedPublicSignals::parse(...)
3. expected = get_hashed_calldata(...)          # Poseidon over recipient,
                                                # signer, encrypted outputs,
                                                # fees, instructions, accounts
   require expected == parsed.calldata_hash()
4. require merkle.root_hash_exists(parsed.root_hash_hinkal())
5. process_amount_changes(...)                  # bounds check on each leg
   process_on_chain_creation(...)               # length match
6. process_nullifiers(...)                      # uniqueness + insertion
7. process_commitments(...)                     # Merkle insert + emit_insert_commitments
8. require signer ∈ whitelisted_relayers
        OR all amount_changes > 0
        OR (relayer_fee == 0 && variable_rate == 0)
9. require parsed.stealth_address_structure().extra_randomization != 0
10. for each (i, mint) in mint_accounts:
       require mint.key() == parsed.get_mint_address(i)
```

#### `verify_proof`

`verify_proof.rs` wraps `groth16-solana::Groth16Verifier`. It deserializes `proof_a` from BN254 G1 affine, negates it (Groth16 verifier expects `-A`), and dispatches to a `verify_with_size::<N>` for the matching public-signal count:

| Dimensions | Public-signal count | Verifying key                            |
| ---------- | ------------------- | ---------------------------------------- |
| `1×2×1`    | 16                  | `basic_solana_1x2x1_verification_key.rs` |
| `1×6×1`    | 20                  | `basic_solana_1x6x1_verification_key.rs` |
| `1×2×2`    | 17                  | `basic_solana_1x2x2_verification_key.rs` |
| `1×6×2`    | 21                  | `basic_solana_1x6x2_verification_key.rs` |
| `2×2×1`    | 22                  | `basic_solana_2x2x1_verification_key.rs` |
| `2×6×1`    | 30                  | `basic_solana_2x6x1_verification_key.rs` |

A mismatched signal count raises `InvalidPublicSignalsSize`; an unknown dimensions tuple raises `UnsupportedDimensions`.

#### `get_hashed_calldata`

The proof publicly commits to a Poseidon hash over: recipient, signer, encrypted outputs, `relayer_fee`, `variable_rate`, the staged `SolanaInstructionData` list, and the `remaining_accounts` keys passed to the instruction. The handler recomputes this and rejects with `CalldataHashMismatch` on any divergence — so a relayer cannot, for instance, swap the recipient or alter the CPI batch after the user signed the proof.

#### Nullifier processing

`process_tokens::process_nullifiers`:

* Length check: `nullifier_accounts.len() == token_number * nullifier_amount`, even.
* Version check on every supplied nullifier PDA.
* `validate_nullifiers` - the bytes the caller committed to spend must equal what the proof's public signals claim.
* For each non-zero nullifier (zero nullifiers act as padding for unused legs), the matching PDA must exist, must currently have `is_used == false`, and is flipped to `true` with a `Nullified` event.

`validate_nullifier_pdas` (called from `swap*` before the pipeline) additionally re-derives each PDA from `(seed, deployer, nullifier_bytes, program_id)` to confirm the supplied account is the canonical one.

#### Commitment insertion

`process_tokens::process_commitments` walks `(token_index, output_index)` over `token_number × output_amount`, skipping legs marked `on_chain_creation`. For each non-zero `out_commitment`:

1. `merkle.insert(out_commitment)` - Poseidon hash up the tree, with the new root written into the 100-root circular buffer.
2. `emit_insert_commitments` - emits `NewCommitment { commitment, index, encrypted_output, on_chain_data }`. `on_chain_data` is zero for proof-derived commitments.

For on-chain-created commitments (proofless deposit, swap output) `process_onchain_commitments*` is called separately and `on_chain_data` carries the full UTXO (`amount`, `mint_address_part1/2`, the stealth-address structure, `timestamp`).

Swap instructions emit via `emit_cpi!` instead of `emit!` because they declare `#[event_cpi]` accounts.

#### Relayer / withdrawal gate

After commitments are inserted, the pipeline checks:

```rust
require!(
    is_signer_relayer
    || token_results.iter().all(|x| *x > BigInt::ZERO)
    || (relayer_fee == 0 && variable_rate == 0),
    HinkalError::RelayerNotWhitelisted
);
```

Any flow that pays a fee must be initiated by a whitelisted relayer; pure deposits (positive amount changes for every leg) are exempt.

#### Stealth address & mint binding

* `extra_randomization` from the public signals must be non-zero (`ZeroRandomizationForbidden`) - this is what makes commitments unlinkable across deposits.
* For every supplied `Mint` account, its key must equal the mint address encoded in the proof's public signals (split into 128-bit parts because BN254 `F_r` is shorter than a 32-byte SPL mint).

#### Output

`process_proof_and_state` returns `ProcessedProof`:

```rust
struct ProcessedProof<'a> {
    parsed_public_signals: ParsedPublicSignals<'a>,
    token_results: Vec<BigInt>,           // amount_change per leg
    stealth_address_structure: StealthAddressStructure,
}
```

The caller (transact / transfer / swap common) then performs the actual SOL/SPL movement and asserts the closing balance equation through `validate_balance_diff`.
