CanonexDocs
Start

Canonex documentation

Canonex is a non-custodial privacy layer for Solana built on the Privacy Pools model. You shield funds once, act privately inside a shared pool, and exit to a fresh address with a zero-knowledge proof that your funds belong to a clean association set. The proof reveals nothing about which deposit was yours.

Classic mixers give everyone the same anonymity, including thieves, which is why exchanges treat their withdrawals as suspect. Privacy Pools separates the two: honest users prove membership in a set that excludes illicit deposits, so their privacy does not come bundled with someone else's exploit.

Shield
0% protocol fee
Network rent and gas only
Private send
0% protocol fee
Relayer gas only
Swap
0.20%
Plus Jupiter route cost
Withdraw
0.25%
Plus relayer gas
Status
Program audit: [AUDITOR], [DATE]. Per-token deposit caps are raised gradually as the anonymity set grows. Fee values are launch parameters and can be changed by governance.
Start

How it works

Every action falls into one of four zones. The first and last touch the public chain. Everything in between happens inside the pool as encrypted notes and zero-knowledge proofs.

01 · Public
Shield
Your wallet deposits into the pool. The last trace of you.
02 · Screened
Approval
The ASP adds the deposit's label to the clean set.
03 · Sealed
Private actions
Send, swap, hold. Notes in, notes out.
04 · Public
Exit clean
Withdraw with a proof of clean origin.

The three building blocks

  • Notes. A private balance is a set of notes. Each note commits to an amount, a token, a label and a secret. Only your viewing key can read it, only your spending key can spend it.
  • Proofs. Every private action is a Groth16 proof generated in your browser and verified by the Solana program. The program never sees your keys, balances or history.
  • The association set. A Merkle tree of approved deposit labels, published on-chain by the Association Set Provider. Withdrawals must prove their label is in it.
Start

Quick start

  1. Connect Phantom (or any Solana wallet) and create your vault. Save the recovery key shown once. Without it a lost browser means lost funds.
  2. Shield SOL, USDC or a supported SPL token with one signature.
  3. Wait for approval. Most deposits are screened within minutes. You can watch the status in the app.
  4. Act privately. Send to a cnx1… address, swap through Jupiter, or simply hold.
  5. Withdraw to a fresh address. Let some time pass and avoid exact amounts that match your deposit.
Using Canonex

Shield

Shielding moves tokens from your public wallet into the pool vault and creates a note for you. SOL is wrapped to wSOL automatically. SPL and Token-2022 mints are supported, except tokens with features that could let a third party move or block pool funds: permanent delegates, transfer hooks, transfer fees and pausable mints.

The deposit instruction assigns the note a label, a unique identifier of this deposit. From now on every note created from this money carries the same label. The ASP judges the label, not you. See Labels and lineage.

Visible on-chain
Your address, token and amount at the moment of shielding are public. Privacy starts after the deposit, not before it.
Using Canonex

Approval window

After a deposit lands, the Association Set Provider screens its source address and funding history. When it passes, the ASP inserts the label into the association set and publishes a new root on-chain.

OutcomeWhat it meansWhat you can do
PendingScreening in progress. Typical wait: minutes, up to [24h] for large deposits.Hold and send privately. Withdraw after approval.
ApprovedLabel is in the clean set. All notes descended from it qualify.Everything, including private withdrawal.
DeclinedFunds trace to an exploit, drainer, sanctioned or high-risk source.Ragequit back to the original address. Funds are never frozen.

The delay is deliberate. Most hacks are identified within hours. A short window means stolen funds cannot rush through the pool before the industry has flagged them.

Using Canonex

Send privately

A private send spends one or two of your notes and creates up to two new ones: one for the recipient, one for your change. The recipient is a shielded address starting with cnx1, which encodes their viewing public key and a checksum.

The recipient's note is encrypted to their viewing key and posted with the transaction. Their client finds it by trial-decrypting new notes. No protocol fee is charged; the relayer's network fee is paid from the notes being spent.

Lineage carries over
The recipient's note inherits the label of the money that funded it. If you send approved funds, the recipient can withdraw them privately too. Notes with different labels are never merged into one.
Using Canonex

Swap

Swaps are routed through Jupiter from the pool vault, so the DEX sees only the pool. A swap runs in two steps:

  1. swap_prepare spends your input note into a single-use escrow, with the exact input amount and your minimum output bound into the proof.
  2. swap_execute performs the route and mints an output note for you with the same label. If the output falls below your minimum, swap_refund returns the input as a new note.

Because the minimum output is inside the proof, a relayer cannot sandwich you beyond the slippage you signed for.

Using Canonex

Withdraw

A withdrawal proves three things at once: you own an unspent note in the pool, its label is in a recent association set root, and the recipient, relayer and fee are exactly the ones you chose. Then the program sends tokens to any address. By default the app derives a one-time address from your vault.

Good habits

  • Withdraw to an address that has never touched your public wallet.
  • Avoid the exact amount you deposited. Partial and round amounts blend in.
  • Let time pass. A withdrawal minutes after a unique deposit is easy to match.
  • Do not fund the fresh address from your main wallet afterwards. Gas is paid by the relayer.
Using Canonex

Ragequit

Ragequit is the guarantee that nobody can lock your money. The original depositor can always exit a deposit publicly, back to the address it came from, whether or not the ASP approved it. It requires the deposit's secret and a signature from the depositing wallet.

Ragequit gives up privacy for that deposit, because the exit visibly links to the entry. It exists for declined deposits and for anyone who no longer trusts the current association set.

Concepts

Notes and nullifiers

A note is never stored in the clear. The pool stores only its commitment, a Poseidon hash that binds the note's contents without revealing them:

// all hashes are Poseidon over the BN254 scalar field
precommitment  = H(nullifier, secret)
commitment     = H(value, mint, label, precommitment)
nullifierHash  = H(nullifier)

When you spend a note, you publish its nullifierHash. The program records it and refuses any second spend with the same value. Since only you know the nullifier, nobody can link the nullifierHash back to the commitment it came from. That is the core of the unlinkability: the chain learns that some note was spent, never which.

Concepts

The two trees

Canonex keeps two Merkle trees. Every private withdrawal proves membership in both.

State tree · on-chain
Every commitment ever made
Depth 26, about 67 million leaves. Append-only. The program updates it and keeps the last 64 roots so proofs built a few blocks ago stay valid.
Association set · published by ASP
Every approved label
Depth 26. Built off-chain by the ASP; only the root goes on-chain via update_asp_root. The last 16 roots are accepted.

Separating the trees is what makes the design work. The state tree guarantees the money exists. The association set guarantees it is clean. Neither tree tells the verifier which leaf is yours.

Concepts

Labels and lineage

A label is assigned at deposit and never changes:

scope = H(programId, mint)
label = H(scope, depositIndex)

The transaction circuit enforces that every output note carries the label of the notes it spends. Money keeps its origin as it moves: if you send half of an approved deposit to a friend, their note has the same approved label. If a deposit is later found to be stolen, every note descended from it falls out of the clean set at once, while unrelated users are untouched.

The label is hidden inside the commitment. The withdrawal proof shows only that some label in the association set matches, never which one.

Consequence
Receiving a private payment means receiving its lineage. If the sender's deposit is removed from the set, that payment can no longer be withdrawn privately. Accept large private payments only from counterparties you trust.
Concepts

Association Set Provider

The ASP is an off-chain service that indexes deposits, scores their sources and publishes the clean set. It has one power: including or excluding a label.

The ASP canThe ASP cannot
Decline a deposit from a flagged sourceMove, freeze or seize any funds
Remove a label later if new evidence appearsBlock ragequit to the original address
Publish a new rootSee balances, transfers or who withdrew

What gets screened

  • Sanctioned addresses and entities on public lists.
  • Wallets tied to protocol exploits, drainer kits and phishing campaigns on Solana.
  • Rug deployer clusters and funds received directly from them.
  • Deposits that hop through fresh wallets to dodge the checks above.

The scoring rules are published and versioned. $CNX holders vote on which lists the default ASP uses. Later versions allow several ASPs: a withdrawal then chooses which association set to prove against, from strict institutional sets to a minimal sanctions-only set.

Concepts

What a proof proves

The Transaction(26, 2, 2) circuit spends up to two notes and creates up to two. For a withdrawal it proves, without revealing private inputs:

  1. Ownership. You know the nullifier and secret of each input note.
  2. Existence. Each input commitment is a leaf under a recent state root.
  3. Clean origin. The shared label is a leaf under a recent association set root.
  4. No double spend. Each published nullifierHash is derived correctly.
  5. Conservation. Inputs equal outputs plus the public amount plus fees.
  6. Same lineage. All inputs and outputs carry one label.
  7. Binding. Recipient, relayer, fee and encrypted outputs hash to extDataHash, so nobody can redirect the funds after you sign.
Public inputPurpose
stateRootMust match one of the last 64 state roots
aspRootMust match one of the last 16 association set roots
nullifierHash[2]Recorded to block double spends
outCommitment[2]Appended to the state tree
publicAmountNet tokens leaving (withdraw) or entering (shield) the pool
mintToken of the pool being used
extDataHashBinds recipient, relayer, fee and note ciphertexts

Proofs are Groth16 over BN254, verified on-chain with Solana's alt_bn128 syscalls in one instruction. In-browser proving takes a few seconds on a modern laptop.

Concepts

Public vs private

ActionVisible on-chainSealed
ShieldYour address, token, amount, timeThe note you received, its secret
SendThat a transfer happened, relayer fee, notes spent (1 or 2)Sender, recipient, amount, token
SwapToken pair and route through Jupiter, amountsWho swapped and their balance
WithdrawRecipient address, token, amount, timeWhich deposit it came from
RagequitEverything: it links exit to entryNothing
Concepts

Fees and parameters

ActionProtocol feeRelayer fee
Shield0%None, you sign directly
Private send0%Network gas, quoted live
Swap0.20%Network gas, quoted live
Withdraw0.25%Network gas, quoted live
Ragequit0%None, you sign directly

Protocol fees go to the treasury: [50%] buys back and burns $CNX, [30%] funds relayer rewards, [20%] funds audits and the ASP. Holders above [THRESHOLD] $CNX get a reduced fee.

ParameterLaunch value
State tree depth26
State root history64 roots
ASP root history16 roots
Notes per transaction2 in, 2 out
Per-deposit cap[CAP] per token, raised over time
Upgrade timelock72 hours
Concepts

Keys and recovery

Your vault is derived from one random 32-byte root seed, generated in your browser:

seed            32 random bytes
spendingKey   = HKDF(seed, "cnx/spend")    // signs note spends inside proofs
viewingKey    = HKDF(seed, "cnx/view")     // X25519, decrypts incoming notes
oneTimeAddr_i = HKDF(seed, "cnx/ota" || i) // fresh withdrawal addresses

The seed is stored only in your browser, encrypted with your passphrase (Argon2id, then AES-256-GCM). The recovery key is shown once at setup. Note ciphertexts use X25519 key agreement and ChaCha20-Poly1305.

A viewing key can be exported read-only, scoped to a date range, for an accountant or auditor. It reveals your history but cannot spend.

No reset
There is no server copy. Lose both the browser data and the recovery key, and the notes cannot be spent by anyone, including us.
Concepts

Security model

  • Non-custodial. Funds sit in a program-owned vault and move only with a valid proof or a depositor's ragequit.
  • Bound transactions. Recipient, fee and swap limits are inside the proof. A relayer can delay a transaction, never change it.
  • Isolated escrows. Each swap uses a single-use escrow account, so a failed route cannot touch the main vault.
  • Limited admin. A [3-of-5] multisig can pause new deposits and register assets. It cannot pause withdrawals or ragequit, and cannot move funds.
  • Timelocked upgrades. Any program upgrade is announced on-chain 72 hours before it can execute, so users can leave first.
  • Trusted setup. Circuit keys come from a public multi-party ceremony. One honest participant is enough for soundness.
Concepts

Limits and risks

  • Anonymity set. Privacy is only as large as the crowd. Each token pool is its own set, and the clean set is smaller than the whole pool. The app warns when a set is small.
  • Timing and amounts. Unique amounts and fast round trips can be matched by anyone. Use the habits in Withdraw.
  • Issuer freezes. Tokens like USDC can be frozen by their issuer, including inside the vault.
  • ASP trust. A wrongly declined deposit can still ragequit, but loses privacy. A wrongly approved one weakens the set for everyone.
  • Smart contract risk. Audits reduce risk, they do not remove it. Deposit caps limit exposure early on.
  • Regulation. Rules on privacy tools differ by country. Check yours.
Build

SDK

@canonex/sdk handles keys, note sync, in-browser proving and relayer calls. @canonex/crypto exposes the raw primitives.

import { Canonex } from '@canonex/sdk';

const cnx = await Canonex.connect({ cluster: 'mainnet-beta', wallet });
await cnx.unlock(passphrase);

// shield 10 SOL
const dep = await cnx.shield({ mint: 'SOL', amount: 10_000_000_000n });

// wait until the ASP includes the deposit label
await cnx.waitForApproval(dep.label, { timeoutMs: 3_600_000 });

// private send to a shielded address
await cnx.send({ to: 'cnx1q…', mint: 'SOL', amount: 2_000_000_000n });

// withdraw to a fresh one-time address
const to = cnx.nextOneTimeAddress();
await cnx.withdraw({ mint: 'SOL', amount: 5_000_000_000n, to, asp: 'default' });
Build

Relayer and indexer API

Plain HTTPS, no accounts. Relayers do not log IP addresses or request bodies. Anyone can run a relayer by staking $CNX.

EndpointReturns
GET /v1/infoRelayer address, stake, supported mints
GET /v1/quoteGas fee for an action and mint
POST /v1/relaySubmits a proof, returns the signature
GET /v1/swap/quoteJupiter route and minimum output
GET /v1/tree/stateCommitments since an index, current root
GET /v1/tree/aspAssociation set leaves and current root
GET /v1/nullifiersSpent nullifier hashes since a slot
GET /v1/label/:labelPending, approved or declined
Build

On-chain program

Program ID: [PROGRAM ID]. Written in Rust with Anchor. Circuits in Circom.

InstructionWhat it does
depositTakes tokens, assigns a label, appends a commitment
transactPrivate send or withdraw: verifies proof, records nullifiers, appends outputs
ragequitReturns a pending or declined deposit to its origin
swap_prepareSpends a note into a bound escrow
swap_executeRuns the Jupiter route, mints the output note
swap_refundReturns escrowed input if the minimum was not met
update_asp_rootASP signer publishes a new association set root
register_assetOpens a pool for a new mint (multisig)
set_* / accept_adminGovernance, behind the 72h timelock
Help

FAQ

Is Canonex a mixer?

It uses the same cryptography, but not the same rules. A mixer hides everyone together. Canonex lets you prove your funds come from a set that excludes illicit deposits, without revealing which deposit was yours.

Can the team or the ASP freeze my funds?

No. The ASP can only leave a deposit out of the clean set. Ragequit back to your original address always works, and the admin cannot pause withdrawals.

Who decides what is clean?

The ASP applies published, versioned rules. $CNX holders vote on the lists it uses, and later versions let you choose between several association sets.

What if a relayer goes offline?

Use another relayer, or submit the proof yourself from any funded wallet. That wallet becomes visible as the fee payer, so pick one with no link to you.

Is it fully anonymous?

No system is. Privacy depends on the size of the set, on timing and on amounts. Canonex removes the on-chain link; your habits decide how hard the rest is to guess.

I lost my recovery key.

If your browser still has the vault, export a new recovery key from settings right away. If both are gone, the funds cannot be recovered.

How is this related to Privacy Pools on Ethereum?

Canonex implements the Privacy Pools model described in the 2023 paper by Buterin, Illum, Nadler, Schär and Soleimani, adapted to Solana with private sends and swaps inside the pool. It is an independent project.

Help

Glossary

TermMeaning
NoteA private balance unit: value, token, label, secret.
CommitmentHash of a note, stored in the state tree.
Nullifier hashPublished when a note is spent. Prevents double spends without revealing the note.
LabelDeposit identifier carried by every note descended from it.
Association setMerkle tree of approved labels. Its root lives on-chain.
ASPAssociation Set Provider. Screens deposits and publishes the set.
RagequitPublic exit of a deposit back to its origin address.
RelayerSubmits proofs and pays gas. Cannot change a transaction.
Anonymity setThe crowd your note hides among.
Viewing keyRead-only key that decrypts your notes.

Private by default. Clean by proof.