> 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/program-logic.md).

# Program Logic

Hinkal Protocol — Solana Program Overview

This page summarizes the on-chain Anchor program that powers Hinkal on Solana. The program is a single Anchor program (`hinkal`) responsible for Groth16 proof verification, commitment Merkle tree maintenance, nullifier tracking, access-token gating, and execution of arbitrary external instructions for private swaps.

State and logic live together in one program. Persistent data is split across **PDAs** (program-derived accounts), and the instructions in `lib.rs` operate on those PDAs, deriving them from a fixed `original_deployer` key and per-domain seeds.

***

#### Core Orchestration

* **`hinkal` program** (`lib.rs`)
  * Entry point for all user, relayer, and admin actions. Declares the program ID and registers every instruction handler (`initialize`, `proofless_deposit`, `transact2`/`transact6`, `transfer2`/`transfer6`, `swap2`/`swap6`, plus admin/access-token/proof-staging instructions).
  * Each transaction-style instruction (`transact*`, `transfer*`, `swap*`) verifies a Groth16 proof, asserts Merkle root membership, validates nullifiers, inserts new commitments, and emits indexer events.
* **`process_proof_and_state`** (`proof_processing.rs`)
  * The shared core for every proof-bearing instruction. Performs a 10-step pipeline:
    1. `verify_proof` (Groth16 over BN254 via `groth16-solana`).
    2. Parse public signals into a `ParsedPublicSignals` view.
    3. Recompute `expected_hashed_calldata` (recipient, signer, encrypted outputs, fees, instructions, remaining accounts) and compare with the in-proof `calldata_hash`.
    4. Confirm the Merkle root referenced by the proof exists in the on-chain root window.
    5. Validate amount-change ranges and `on_chain_creation` array length.
    6. Insert nullifiers (or reject if reused).
    7. Insert new commitments into the Merkle tree and emit `NewCommitment` events.
    8. Enforce that withdrawals only run with a whitelisted relayer (or zero fee).
    9. Verify the stealth-address structure has non-zero randomization.
    10. Verify each supplied mint matches the mint encoded in the proof.

#### State Layer (PDAs)

State is held in PDAs declared in `hinkal_accounts.rs` and `types.rs` and derived using the seeds in `seed_constants.rs`. All PDAs are seeded by `original_deployer.key()` so multiple deployments can co-exist.

* **`StorageAccount`** - `seeds = ["hinkal", original_deployer]`. Holds the relayer whitelist (`Vec<Pubkey>`, max 10), the `owner`, and `pending_owner` (two-step ownership).
* **`Merkle`** (zero-copy, `merkle.rs`) - `seeds = ["hinkal_merkle", original_deployer]`. Append-only commitment tree with up to 200 levels and a sliding window of the last 100 root hashes (`MAX_ROOT_NUMBER`). Provides `insert`, `insert_many`, `get_root_hash`, `root_hash_exists`, all using Poseidon.
* **`Merkle` (access-token tree)** - `seeds = ["hinkal_merkle_access_token", original_deployer]`. A second tree dedicated to access keys; access-token ZK proofs are checked against it during gated flows.
* **Storage vault** - `seeds = ["hinkal_vault", original_deployer]`. A `SystemAccount` PDA that custodies all shielded SOL and is the authority for shielded SPL token ATAs.
* **`AccessTokenStorageAccount`** - `seeds = ["hinkal_access_token", original_deployer]`. Stores the `access_token_owner_key` (the only signer that can mint new access tokens).
* **`AccesskeyAccount`** - `seeds = ["hinkal_access_keys", original_deployer, access_key]`. One PDA per access key; `is_added` prevents duplicate insertion.
* **`TokenLimitStorageAccount`** - `seeds = ["hinkal_token_limit", original_deployer, mint]` (mint = 32 zero bytes for SOL). Per-token deposit/withdrawal threshold above which an access token is required.
* **`NullifierAccount`** - `seeds = ["hinkal_nullifier", original_deployer, nullifier]`. One PDA per nullifier with `is_used: bool`. Pre-initialized via `init_nullifier`; flipped to `true` during the spending instruction; closed via `close_nullifier` (relayers only) to reclaim rent if the proof was never consumed.
* **Proof staging accounts** - `ProofAccountPart1`, `ProofAccountPart2`, `EncryptedOutputsAccount`, `InstructionsAccount`. Each is a per-`(deployer, signer, id)` PDA used to stage proof bytes, public signals, encrypted UTXO outputs, and CPI instruction blobs across multiple transactions before the final `transact*` / `transfer*` / `swap*` call (see `store_proof_part1`, `store_proof_part2`, `store_encrypted_outputs`, `store_instructions`). This split is required because Solana transactions cannot fit a Groth16 proof + signals + outputs + CPI calldata in a single message.
* **Swapper PDA** - `seeds = ["hinkal_swapper", original_deployer, swapper_account_additional_seed]`. A SystemAccount PDA used as the CPI signer for routed external swaps; `swapper_account_additional_seed` is bound into the proof to prevent cross-swap reuse.

All account types include a `version: u8` field and `is_latest_version()` constraint, enforced via the `declare_versioned!` macro in `versions.rs` so old layouts are rejected after upgrades.

#### Logic & Execution Layer

* **Transact (deposit/withdraw)** - `instructions/transact2.rs` and `instructions/transact6.rs` plus shared `transact_common.rs`, `perform_transact_funds.rs` (SOL), and `perform_transact_tokens.rs` (SPL). Routes between SOL and SPL leg based on whether `mint` is `Some`, runs `process_proof_and_state`, then either deposits funds into the storage vault (positive amount change) or withdraws to the recipient + pays the relayer fee to the signer (negative amount change). The `forbid_deposit` flag rejects any non-negative net change. Dimensions are fixed: `transact2 = 1×2×1`, `transact6 = 1×6×1`.
* **Transfer (shielded → shielded)** - `instructions/transfer2.rs` and `instructions/transfer6.rs` plus `transfer_common.rs`. Same proof pipeline as transact, but `forbid_deposit = true`, recipient pinned to the system program, and the only on-chain fund movement is paying `relayer_fee` from the storage vault to the signer. Dimensions: `transfer2 = 1×2×2`, `transfer6 = 1×6×2`.
* **Swap (shielded → external DEX → shielded)** - `instructions/swap2.rs`, `instructions/swap6.rs`, `swap_common.rs`, `swap_utils.rs`, `perform_swap.rs`, and `determine_swap_mode.rs`. After the proof pipeline:
  1. `validate_swap_atas` checks that the supplied source/destination ATAs match the configured mints.
  2. `determine_swap_mode` classifies the leg pair as `TokenToToken`, `TokenToSol`, or `SolToToken`.
  3. Funds move from the storage vault to the swapper PDA, which then signs CPI calls to the configured router using the prestaged `InstructionsAccount`. The instructions come from the off-chain quote (OKX for shielded swaps, which itself routes through Jupiter / Raydium / etc.).
  4. `perform_swap` measures `balance_after - balance_before` on the destination ATA (or swapper SOL balance) to compute the realized output.
  5. `calculate_total_fee` = `relayer_fee + output * variable_rate / 10000`. The fee is paid to the signer; the remainder forms a single output UTXO whose commitment is inserted on-chain (`process_onchain_commitments_swap*`). Dimensions: `swap2 = 2×2×1`, `swap6 = 2×6×1`.
* **Proofless deposit** - `instructions/proofless_deposit.rs` (calls `proofless_deposit_common.rs`). Lets a user deposit SOL or an SPL token into the vault and have the program build the resulting commitments on-chain from 1..N `StealthAddressStructure`s (no ZK proof). Below the per-token threshold (`TokenLimitStorageAccount`), no access token is required; otherwise a signature from the access-token owner is mandatory. Emits `BlockedUtxosCreated` when `create_blocked_utxos = true`.

#### Policy / Validation

* **Relayer policy** (`process_proof_and_state` step 8 + `transfer_common`/`swap_common`) - for any flow that pays a fee, the signer must be a member of `StorageAccount.whitelisted_relayers`, otherwise the instruction reverts with `RelayerNotWhitelisted`. `update_relayer_status` (owner-only) maintains the list.
* **Access tokens** (`validate_access_token.rs`, `is_access_token_required.rs`) - for amounts above the per-token limit, the proof must reference an access-key Merkle root from the access-token tree. New access keys are added through `add_access_token`, which requires the `access_token_owner` signer; the owner key itself can be rotated via `change_access_token_owner_key`.
* **Token limits** (`init_token_limit`, `update_token_limit`) - owner-only, sets the threshold above which access tokens become mandatory for the (mint, deployer) pair.
* **Calldata hash** (`get_hashed_calldata.rs`) - every proof commits to the recipient, signer, encrypted outputs, fees, CPI instructions, and remaining accounts via a Poseidon hash. The instruction handler recomputes this and rejects on mismatch (`CalldataHashMismatch`).
* **Balance equation** (`validate_balance_diff.rs`) - after every fund movement, the actual `balance_after - balance_before` on the storage vault (or its ATA) must equal `amount_change + utxo_amount` (or just `utxo_amount` when the commitment was created on-chain). Mismatches abort the instruction with `BalanceDiffMismatch`.
* **Nullifier validation** (`validate_nullifiers.rs`, `validate_nullifier_pdas.rs`, `process_tokens::process_nullifiers`) - instruction-supplied nullifiers must match the public signals; their PDAs must be derived from those exact bytes; each PDA must not already be `is_used = true`.
* **Self-CPI guard** (`perform_swap.rs`) - every CPI target must be a different program; calling back into Hinkal is rejected with `SelfCPIForbidden`.
* **Versioning** (`versions.rs` + `declare_versioned!`) - every PDA carries a `version` byte. Old versions are rejected by an `is_latest_version()` constraint, allowing forward migrations.
* **Two-step ownership** (`transfer_ownership` + `accept_ownership`) - only the current owner can nominate a `pending_owner`; the pending owner must accept before the transfer takes effect.

#### Events (for indexers)

Defined in `hinkal_events.rs`:

* `NewCommitment { commitment, index, encrypted_output, on_chain_data: [Bytes32; 8] }` - emitted on every commitment insert. `on_chain_data` is non-zero for proofless / on-chain UTXO creation and lets indexers reconstruct the UTXO without an off-chain encrypted output. Swap instructions emit this via `emit_cpi!` (event-CPI) instead of `emit!` to fit Anchor's `#[event_cpi]` accounts.
* `Nullified { nullifier }` - every successful nullifier spend.
* `NewAccessKeyAdded { access_key, index, sender_address }`.
* `BlockedUtxosCreated { sender, mint, amounts }` - `proofless_deposit` only (when `create_blocked_utxos = true`).
* `RelayerStatusUpdated`, `TokenLimitUpdated`, `AccessTokenOwnerChanged`, `OwnershipTransferred`, `OwnershipAccepted`.

***

#### Typical Flow (private swap)

1. **Off-chain**: client builds `CircomData`, generates a Groth16 proof, encrypts new UTXO outputs, and prepares the CPI instruction list for the chosen router.
2. **Stage**: `store_proof_part1` + `store_proof_part2` + `store_encrypted_outputs` + `store_instructions` populate the per-`id` PDAs (`init_nullifier` is also called for any nullifier whose PDA does not yet exist).
3. **Submit**: `swap2` / `swap6` is called with `dimensions`, `in_nullifiers`, `relayer_fee`, `variable_rate`, and the swapper-additional-seed.
4. **Verify**: `process_proof_and_state` runs the 10-step pipeline (proof, calldata hash, root membership, nullifiers, commitments, relayer/access-token rules, mint binding).
5. **Move funds**: the vault transfers input liquidity to the swapper PDA; `perform_swap` invokes the staged CPI batch against `remaining_accounts`; output is measured via balance delta.
6. **Settle**: relayer + variable fee are paid to the signer; the net output forms a UTXO whose commitment is inserted into the Merkle tree; `validate_*_balance_diff` enforces the closing balance equation.
7. **Emit**: `NewCommitment` (and any swap-specific event) is published for the indexer.

`transact*` and `transfer*` follow the same shape, minus the CPI step.

***

#### Source Map

| Concern                                  | File                                                                                                                                                                             |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Program entry / instruction registration | `lib.rs`                                                                                                                                                                         |
| Account / PDA layouts                    | `hinkal_accounts.rs`, `types.rs`                                                                                                                                                 |
| Seeds                                    | `seed_constants.rs`                                                                                                                                                              |
| Merkle tree                              | `merkle.rs`                                                                                                                                                                      |
| Groth16 verification                     | `verify_proof.rs`, `verifying_key.rs`, `basic_solana_*_verification_key.rs`                                                                                                      |
| Proof pipeline                           | `proof_processing.rs`, `process_tokens.rs`, `public_signals.rs`, `get_hashed_calldata.rs`                                                                                        |
| Transact (SOL/SPL)                       | `instructions/transact{2,6}.rs`, `transact_common.rs`, `perform_transact_funds.rs`, `perform_transact_tokens.rs`                                                                 |
| Transfer                                 | `instructions/transfer{2,6}.rs`, `transfer_common.rs`                                                                                                                            |
| Swap                                     | `instructions/swap{2,6}.rs`, `swap_common.rs`, `swap_utils.rs`, `perform_swap.rs`, `determine_swap_mode.rs`, `validate_swap_atas.rs`                                             |
| Proofless deposits                       | `instructions/proofless_deposit.rs`, `proofless_deposit_common.rs`, `build_on_chain_utxo.rs`                                                                                     |
| Proof staging                            | `instructions/store_proof_part{1,2}.rs`, `store_encrypted_outputs.rs`, `store_instructions.rs`, `merge_proof_data.rs`                                                            |
| Nullifiers                               | `instructions/init_nullifier.rs`, `instructions/close_nullifier.rs`, `validate_nullifiers.rs`, `validate_nullifier_pdas.rs`                                                      |
| Access tokens                            | `instructions/add_access_token.rs`, `instructions/change_access_token_owner_key.rs`, `validate_access_token.rs`, `validate_access_token_owner.rs`, `is_access_token_required.rs` |
| Admin                                    | `instructions/initialize.rs`, `transfer_ownership.rs`, `accept_ownership.rs`, `update_relayer_status.rs`, `init_token_limit.rs`, `update_token_limit.rs`                         |
| Balance & ATA validation                 | `validate_balance_diff.rs`, `validate_ata.rs`, `get_sol_balance.rs`, `get_token_balance.rs`                                                                                      |
| Errors / events                          | `error.rs`, `hinkal_events.rs`                                                                                                                                                   |
