Privacy Documentation

How Privacy Works

A deep dive into the cryptography, architecture, and security model that makes UTXOpia a universal shielded pool for BTC, SOL, USDC, and any token on Solana.

The Problem

Why Tokens Need Privacy

Every blockchain transaction is permanently public. Whether you're using BTC, SOL, or USDC — your balances, transfers, and trading patterns are visible to anyone. UTXOpia shields all your tokens in a single privacy pool.

Comparison
Tokens
Traditional: Single asset (wBTC)
Private: Multi-token (BTC, SOL, USDC, USDT)
Balances
Traditional: Visible on-chain
Private: Hidden as commitments
Transfers
Traditional: Traceable amounts
Private: ZK-proven, zero knowledge
Addresses
Traditional: Linkable & reusable
Private: One-time stealth addresses
Deposits
Traditional: Public token minting
Private: Shielded Merkle insertion
Withdrawals
Traditional: Traceable burn + send
Private: Unlinkable via nullifiers
Custody
Traditional: Multisig / MPC
Private: Ika dWallet · Solana-controlled
Protocol Flow

End-to-End Journey

From shielding any token to private transfers to withdrawal — every step preserves your privacy across BTC, SOL, USDC, and more.

ShieldBTC / SOL / USDC
VerifySPV (BTC only)
CommitMerkle Tree
TransferZK Proof
UnshieldSPL / BTC
01

Shield Any Token

Deposit BTC via Taproot, or shield SOL/USDC/USDT directly from your Solana wallet. Every token enters the same privacy pool — a shared Merkle tree where all commitments look identical regardless of token type or amount.

BTC: Taproot + SPV · SPL: Shield (disc=12)
02

BTC SPV Verification

Bitcoin deposits require a special step: the backend submits an SPV Merkle inclusion proof to the on-chain BTC light client. The Solana program independently validates the Bitcoin transaction was confirmed in a real block — trustless cross-chain verification without any oracle.

On-chain header chain · 6+ confirmations
03

Commitment Creation

Your deposit becomes Poseidon(npk, tokenId, amount) — a cryptographic commitment. The token_id is derived from the SPL mint address: Poseidon(reduce(mint), 0). All tokens share the same depth-16 Merkle tree, making deposits indistinguishable.

Poseidon hash · Token-agnostic · 65,536 leaves
04

Private Transfer

Every transfer uses a Groth16 zero-knowledge proof that consumes N input notes and produces M output notes. The proof verifies balance conservation, token consistency, nullifier uniqueness, and Merkle membership — all without revealing any values. The same circuit works for BTC, SOL, USDC, or any shielded token.

Groth16 · 256 bytes · Token-agnostic circuit
05

Stealth Receive

Recipients use one-time stealth addresses generated via the Dual-Key Stealth Address Protocol (EIP-5564) — X25519 ECDH against the recipient's viewing public key. Each deposit or transfer creates a fresh, unlinkable address. The recipient scans announcements with their viewing key to find their notes; senders can opt-in to a separate XChaCha20-Poly1305 memo so they retain their own outgoing history.

DKSAP · X25519 ECDH · Ed25519 viewing keys
06

Unshield or Withdraw

Exit the privacy pool in two ways: unshield SPL tokens back to your Solana wallet instantly, or withdraw BTC via an Ika dWallet whose authority is controlled by this Solana program (2PC-MPC, no off-chain signer committee). Both operations use a JoinSplit proof — the nullifier prevents double-spending without revealing which note you're spending.

SPL: instant · BTC: Ika dWallet (Solana-controlled)
Cryptography

Under the Hood

The cryptographic primitives that make shielded transactions possible.

Commitment Scheme

Poseidon(npk, token_id, amount)

Each note is a Poseidon hash of the note public key, token ID, and amount. The token_id = Poseidon(reduce(mint), 0) makes commitments token-specific — the same circuit verifies BTC, SOL, USDC, or any token. Only the owner knows the preimage.

Nullifier Generation

Poseidon(nullSecret(spendKey), leafIndex)

When spending a note, the nullifier is derived from a per-wallet null-secret (deterministically derived from your spending key) and the note's Merkle leaf index. You manage the spending key; the null-secret is generated for you. Publishing a nullifier prevents double-spending without revealing which note was consumed.

Master Public Key

MPK = Poseidon(spendPub, derivedNullSecret)

The MPK binds the Baby Jubjub spending public key to the wallet's derived null-secret. Per-note public keys come from NPK = Poseidon(MPK, random), giving each note a unique cryptographic identity. Both inputs ultimately trace back to a single spending key — you never manage the null-secret directly.

JoinSplit Circuit

JoinSplit(N, M, depth=16)

A single parameterized circom template handles all transfer types. Inputs: N note nullifiers + Merkle proofs. Outputs: M new commitments. The circuit verifies balance (Σin = Σout), nullifier validity, Merkle membership, and EdDSA-Poseidon signatures — all in one Groth16 proof. Each variant (1×1, 1×2, 2×1, 2×2, etc.) is a separate Groth16 setup; N + M ≤ 14.

EdDSA-Poseidon Signatures

Sign(spendingKey, message)

Transaction authorization uses EdDSA over the Poseidon hash function on the Baby Jubjub curve. The message includes the Merkle root, bound parameters hash, all nullifiers, and all output commitments — binding the proof to a specific state and preventing a relayer from re-targeting it.

Stealth Key Agreement (DKSAP)

sharedSecret = X25519(ephemeral, viewKey)

Following the Dual-Key Stealth Address Protocol (EIP-5564). Senders generate a random ephemeral keypair and compute a shared secret with the recipient's viewing public key — derived from your viewing key via Ed25519→X25519 conversion. The shared secret derives the one-time note public key. Only the recipient can scan announcements using their viewing private key to detect incoming notes; even repeat payments are unlinkable on-chain.

Sender Memo Channel

XChaCha20-Poly1305(ovk, plaintext, AAD = commitment || leafIdx)

An opt-in second event per output, encrypted under the sender's outgoing viewing key (ovk = SHA-256(viewKey ‖ "utxopia.ovk.v1")). Lets the sender (or an auditor holding ovk) later recover their own outgoing history — recipient-only encryption alone wouldn't allow this. AAD binds each memo to its tree leaf: any tamper or re-targeting attempt fails the Poly1305 tag cleanly.

Key Architecture

Dual-Key Model

Two keys give you full control: one to spend, one to observe. The nullifier secret used during proving is derived automatically from your spending key — you never see it, copy it, or back it up separately.

Spending Key

Baby Jubjub elliptic curve keypair. Signs all JoinSplit transactions using EdDSA-Poseidon. The nullifier secret used inside the circuit is deterministically derived from this key, so a single 32-byte seed is enough to back up the entire wallet.

Signs transactions (EdDSA-Poseidon)
Derives the nullifier secret
Generates the Master Public Key (MPK)

Viewing Key

Ed25519 keypair used exclusively for scanning stealth announcements. Detects incoming notes by matching the note public key (NPK). Derives the outgoing-viewing key (ovk) used for sender-memo decryption, so a single export key recovers both incoming and outgoing history. Share with auditors or compliance officers — they can read your transaction history but never spend your funds.

Scans stealth announcements (incoming)
Derives ovk for sender-memo (outgoing)
Shareable for selective disclosure
Auditable Disclosure

Privacy with Receipts

UTXOpia isn't an unaccountable mixer. Compliance tooling is built into the protocol — across four layers — so users can prove what they need to prove without surrendering custody. Each layer has its own deployment status; check your own posture at /compliance.

Auditor Toolkit (DelegatedViewKey)

Live

Issue a slot-scoped, encrypted viewing key for your accountant or auditor. They drop it into the in-browser audit page, decrypt client-side, and walk away with a CSV of IN/OUT records over a chosen slot range. PBKDF2 + AES-GCM at rest; each issuance is tagged with a delegation ID so you keep a record of who you handed which key to.

scripts/auditor/issue.ts · sdk/src/auditor.ts · /audit

Outgoing Sender Memos

Live

Per-output XChaCha20-Poly1305 envelopes encrypted to the sender's outgoing viewing key. AAD = commitment || leafIndex prevents move-the-memo attacks. Rust transact (disc 13) emits per output when memos are attached; SDK helper buildSenderMemosForTransact composes them client-side; /api/relay forwards them opaquely (viewing keys never leave the client); auditor honors ViewPermissions.INCOMING_ONLY to suppress OUT records when the delegation forbids them.

sdk/src/sender-memo.ts · web/src/app/api/relay/route.ts · sdk/src/auditor.ts

Selective Disclosure Proofs

Live

Prove statements about your shielded holdings without revealing values: ownership-with-threshold (you control commitment X for at least amount Y of token T) and range-sum (sum across N notes ≤ ceiling, with N ∈ {4, 8, 16}). Circuits compiled, prover wired into the SDK, CLIs ship in scripts/auditor/. range-sum N=16 uses a chunked Poseidon attestation since circomlib's hash caps at arity 16.

circuits/build/ownership · scripts/auditor/prove-ownership.ts · scripts/auditor/prove-range-sum.ts

Per-stealth-address compliance toggle (v2)

Live

Recipients self-publish two pieces on their `.utxopia.sol` SNS subdomain: a `complianceFlags` byte (bit 0 = AUDITOR_DISCLOSABLE) plus an optional 32-byte auditor Solana pubkey. Senders see both in the Send wizard chip — the flag tells them disclosure is OK, the pubkey tells them who specifically. Owners flip the flag / set the pubkey via the Settings page or via `scripts/sns-set-compliance.ts <subdomain> --enable --auditor <base58>`. Reader is back-compat with both the legacy 65-byte and v1 66-byte (flag-only) records.

sdk/src/sns-resolver.ts · scripts/sns-set-compliance.ts · components/settings/preferences-form.tsx
Security

Security & Compliance

Privacy doesn't mean unaccountable. Multiple layers of security and compliance are built into the protocol.

On-Chain Policy Gate

Signing policy lives in the Solana program itself: amount limits, fee bounds, paused state, and destination whitelist are checked on-chain before the program issues the Ika `approve_message` CPI. A compromised backend cannot drain funds by submitting forged sighashes.

Ika dWallet Custody

BTC is held by an Ika dWallet whose authority is a PDA derived from this Solana program (`["__ika_cpi_authority"]`). 2PC-MPC means the Ika network and our program must both participate in every signature — no single key, no off-chain signer committee. Pre-alpha runs a single mock signer; real distributed MPC ships at Ika mainnet.

Trustless Verification

Bitcoin deposits are verified on-chain via SPV proofs against a light client tracking BTC block headers. The Solana program validates Merkle inclusion directly — no oracle or trusted third party.

Double-Spend Prevention

Each note can only be spent once. Publishing a nullifier (derived from spending key + leaf index) marks the note as consumed. The on-chain program rejects duplicate nullifiers permanently.

Auditable CPI Trail

Every redemption emits an `approve_message` CPI on-chain, with the sighash, dWallet ID, and signature scheme recorded as inner instructions in the Solana transaction. The full signing history is reconstructable from RPC alone — no separate audit log to operate.

Ready to Go Private?

Shield BTC, SOL, USDC, or USDT. Transfer privately. Withdraw anonymously.