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

# Program Logic

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`, `multi_payment_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 (Jupiter / Raydium / etc.) using the prestaged `InstructionsAccount`.
  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` and `instructions/multi_payment_deposit.rs` (both call `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 a `StealthAddressStructure` (no ZK proof). Below the per-token threshold (`TokenLimitStorageAccount`), no access token is required; otherwise a signature from the access-token owner is mandatory. `multi_payment_deposit` additionally 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 }` — `multi_payment_deposit` only.
* `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`, `instructions/multi_payment_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`                                                                                                                                                   |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://hinkal-team.gitbook.io/hinkal/technical-description/solana-program/program-logic.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
