Docs/Architecture/Overview

Architecture Overview

High-level architecture of the ZKMix protocol

Architecture Overview

ZKMix is a non-custodial, zero-knowledge mixer built on Solana that severs the on-chain link between deposit and withdrawal addresses. The protocol leverages ZK-SNARKs to allow users to prove membership in a set of depositors without revealing which deposit belongs to them, achieving transactional privacy on an otherwise fully transparent ledger.

System Diagram

The ZKMix protocol consists of five primary components that operate across three distinct layers:

+------------------------------------------------------------------+
|                        Client Layer                              |
|                                                                  |
|   +-------------------+         +-------------------+            |
|   |      Web UI       |         |    Client SDK     |            |
|   | (Browser Wallet)  |-------->| (TypeScript/Rust) |            |
|   +-------------------+         +--------+----------+            |
|                                          |                       |
+------------------------------------------|-----------------------+
                                           |
+------------------------------------------|-----------------------+
|                     Off-Chain Layer       |                       |
|                                          v                       |
|   +-------------------+         +--------+----------+            |
|   |   ZK-SNARK Circuit|<--------+  Proof Generator  |            |
|   |   (Circom/Groth16)|         |  (snarkjs/rapidsnark)         |
|   +-------------------+         +--------+----------+            |
|                                          |                       |
|   +-------------------+                  |                       |
|   |  Relayer Network  |<----------------+                       |
|   | (Fee-based submit)|                                          |
|   +--------+----------+                                          |
|            |                                                     |
+------------|-----------------------------------------------------+
             |
+------------|-----------------------------------------------------+
|            v              On-Chain Layer (Solana)                 |
|                                                                  |
|   +-------------------+   +-------------------+                  |
|   |   Mixer Program   |   | Verifier Program  |                  |
|   | (Anchor/Native)   |-->| (Groth16 on-chain)|                  |
|   +--------+----------+   +-------------------+                  |
|            |                                                     |
|   +--------v----------+   +-------------------+                  |
|   | Incremental Merkle|   |  Nullifier Hash   |                  |
|   | Tree (PDA Storage)|   |   Set (PDA Map)   |                  |
|   +-------------------+   +-------------------+                  |
|                                                                  |
+------------------------------------------------------------------+

Key Components

ZK-SNARK Circuit

The cryptographic core of ZKMix is a Groth16 ZK-SNARK circuit compiled from Circom. The circuit encodes the following statement: "I know a secret and nullifier such that their hash (the commitment) exists as a leaf in the Merkle tree with the given root, and the nullifier hashes to the given nullifier hash." This proof is generated entirely client-side, meaning neither the relayer nor the Solana program ever learns which deposit corresponds to a withdrawal.

The circuit operates over the BN128 elliptic curve and produces proofs consisting of three group elements (two G1 points and one G2 point), totaling 256 bytes. Verification requires a constant number of pairing operations, making on-chain verification cost predictable and fixed regardless of tree size.

Solana Programs

ZKMix deploys two on-chain programs:

Mixer Program: The primary program that manages the protocol state. It handles deposit instructions (accepting SOL or SPL tokens and inserting commitments into the Merkle tree), withdrawal instructions (verifying proofs and disbursing funds), and administrative functions (pool configuration, fee parameters). The program stores the incremental Merkle tree state, the nullifier hash set, and a rolling history of recent Merkle roots in Program Derived Accounts (PDAs).

Verifier Program: A dedicated program that performs Groth16 proof verification using Solana's alt_bn128 precompiled instructions. Separating the verifier into its own program allows independent upgrades to the verification logic and keeps the mixer program's instruction budget focused on state management. The verifier performs elliptic curve pairing checks and returns a boolean result to the mixer program via Cross-Program Invocation (CPI).

Relayer Network

Relayers are off-chain services that submit withdrawal transactions on behalf of users. Without a relayer, a user would need to pay transaction fees from a funded wallet, potentially linking their withdrawal address to existing on-chain activity. Relayers solve this by accepting the user's proof and submitting the transaction themselves, deducting a small fee from the withdrawn amount.

Relayers are permissionless: anyone can run one. They cannot steal funds because the withdrawal recipient is encoded as a public input to the ZK proof. The relayer address and fee are also public inputs, meaning the proof is bound to a specific relayer and fee amount. If a relayer attempts to change either value, the proof verification will fail on-chain.

The relayer network operates over HTTPS endpoints. Clients discover relayers through a registry (on-chain or off a well-known configuration endpoint), select one based on fee and availability, and submit the proof payload. The relayer validates the proof locally before submitting to avoid wasting transaction fees on invalid proofs.

Client SDK

The Client SDK is a TypeScript library (with optional Rust bindings via WASM) that abstracts the full deposit and withdrawal lifecycle. It provides:

  • Key generation: Derives the secret and nullifier from a cryptographically secure random source, computes the commitment as Poseidon(nullifier, secret), and returns a deposit note encoding all values.
  • Deposit construction: Builds and signs the Solana transaction that sends funds to the mixer pool and inserts the commitment into the Merkle tree.
  • Merkle tree reconstruction: Fetches on-chain commitment events (via getProgramAccounts or indexed event logs) and reconstructs the Merkle tree locally to determine the leaf index and compute the authentication path.
  • Proof generation: Invokes the Groth16 prover (snarkjs in browser, rapidsnark natively) with the circuit's WASM and proving key, producing the proof and public signals.
  • Withdrawal construction: Packages the proof, public inputs, and relayer metadata into a withdrawal request and submits it either directly to the Solana RPC or to a relayer endpoint.

Web UI

The web UI is a browser-based frontend built with a modern framework (React/Next.js) that connects to Solana wallets via the Wallet Adapter standard. It provides a streamlined interface for depositing funds, storing deposit notes (encrypted in local storage or exported as files), and initiating withdrawals. The UI runs proof generation in a Web Worker to avoid blocking the main thread, with proving times typically between 5 and 15 seconds depending on the device.

Data Flow

Deposit Flow

  1. The user connects their Solana wallet and selects a denomination (e.g., 1 SOL, 10 SOL, 100 SOL).
  2. The Client SDK generates a random 31-byte secret and a random 31-byte nullifier.
  3. The SDK computes the commitment: commitment = Poseidon(nullifier, secret).
  4. The SDK encodes secret, nullifier, and pool metadata into a deposit note string for the user to store securely.
  5. The SDK constructs a Solana transaction containing a deposit instruction with the commitment as an argument. This instruction transfers the denomination amount from the user's wallet to the mixer pool PDA and inserts the commitment into the next available leaf of the on-chain incremental Merkle tree.
  6. The on-chain program updates the Merkle tree root and emits a deposit event with the commitment and leaf index.
  7. The user stores their deposit note offline. This note is the only link between their deposit and the ability to withdraw.

Withdrawal Flow

  1. The user (or an automated process) inputs their deposit note into the client.
  2. The Client SDK parses the note to recover the secret, nullifier, and pool identifier.
  3. The SDK fetches all deposit events from the on-chain program to reconstruct the full Merkle tree locally.
  4. The SDK locates the user's commitment in the tree and computes the Merkle authentication path (sibling hashes from the leaf to the root).
  5. The SDK computes nullifierHash = Poseidon(nullifier).
  6. The SDK fetches the current Merkle root from the on-chain program (or uses a recent historical root).
  7. The SDK generates a Groth16 proof with the following inputs:
  • Public inputs: root, nullifierHash, recipient (withdrawal address), relayer (relayer address or zero), fee (relayer fee or zero), refund (gas refund or zero).
  • Private inputs: secret, nullifier, pathElements (sibling hashes), pathIndices (left/right path directions).
  1. The proof and public inputs are sent to a relayer (or submitted directly if the user has a funded wallet).
  2. The relayer submits a withdraw transaction to the Solana program.
  3. The on-chain program verifies that the root matches a stored recent root, that the nullifierHash has not been used before, and that the Groth16 proof is valid by calling the verifier program via CPI.
  4. If all checks pass, the program marks the nullifierHash as spent, transfers the denomination amount (minus the relayer fee) to the recipient, transfers the fee to the relayer, and emits a withdrawal event.

Component Interactions

The components interact through well-defined interfaces:

  • Client SDK to Solana Programs: Standard Solana RPC calls (sendTransaction, getProgramAccounts, getAccountInfo). The SDK serializes instruction data using Borsh encoding matching the on-chain program's expected format.
  • Client SDK to Relayer: HTTPS POST requests with a JSON payload containing the proof (serialized as hex strings for the three curve points), public inputs (as decimal string field elements), and metadata (recipient, fee).
  • Mixer Program to Verifier Program: Cross-Program Invocation (CPI). The mixer program passes the proof bytes and public input bytes to the verifier, which returns success or failure. This CPI consumes a significant portion of the transaction's compute budget, which is why ZKMix requests an increased compute unit limit (typically 400,000 CU) for withdrawal transactions.
  • Client SDK to ZK Circuit: The SDK loads the circuit's compiled WASM file and the Groth16 proving key (a ~40 MB binary file for a tree depth of 20). It invokes the prover through either snarkjs (JavaScript) or rapidsnark (native binary via WASM or subprocess).

Design Principles

Non-Custodial

At no point does any party other than the user control their funds. Deposits are held in a Program Derived Account (PDA) controlled by the mixer program's deterministic logic. Withdrawals are authorized exclusively by valid ZK proofs. There are no admin keys that can drain the pool, no multisig that can redirect funds, and no upgrade authority that can alter withdrawal logic without a transparent governance process. The program's upgrade authority can be permanently revoked after deployment to achieve full immutability.

Trustless Verification

All critical verification happens on-chain through deterministic computation. The Groth16 proof is verified by Solana's alt_bn128_pairing precompile, which performs elliptic curve pairing checks that are computationally infeasible to forge. The Merkle root check ensures the commitment existed in the tree at some recent point in time. The nullifier hash check ensures each deposit can only be withdrawn once. No off-chain oracle, relayer attestation, or trusted third party is required to validate a withdrawal.

The only trust assumption is the Groth16 trusted setup. ZKMix uses a ceremony-generated proving and verification key. The security of Groth16 requires that at least one participant in the ceremony honestly destroyed their toxic waste (the random values used during key generation). ZKMix mitigates this risk by using a large-scale, publicly auditable ceremony with hundreds of participants, following the model established by Zcash and Tornado Cash.

Minimal On-Chain Footprint

ZKMix minimizes on-chain storage and computation to reduce costs and maximize throughput:

  • Incremental Merkle tree: Only the current path of filled subtree hashes is stored on-chain (20 field elements for a depth-20 tree), not the entire tree. New leaves are inserted by updating only the path from the new leaf to the root.
  • Root history: A fixed-size circular buffer of the last 30 roots is stored, consuming a constant amount of space regardless of the number of deposits.
  • Nullifier hash set: Each spent nullifier hash is stored as a PDA with the hash as the seed. Existence checks use Solana's native account lookup, which is O(1).
  • Proof verification: Groth16 verification requires a fixed number of elliptic curve operations regardless of the circuit's complexity, consuming approximately 200,000 compute units on Solana.
  • No transaction graph: Unlike account-based mixing, ZKMix stores no mapping between deposits and withdrawals. The on-chain state reveals only that some commitment was deposited and some nullifier hash was spent, with no link between the two.