Private, unlinkable XLM transfers powered by zk-SNARKs on Soroban.
Caligo is a zero-knowledge mixer protocol that enables private transfers of XLM on the Stellar network. Using Groth16 zk-SNARK proofs and Poseidon hashing, Caligo breaks the on-chain link between depositors and recipients — making transactions truly private on Stellar.
Experimental Software — Security Review Required
This protocol is under active development and has not yet undergone a third-party security audit. It is provided for research, education, and testing purposes only. Do not use with real funds on mainnet until a comprehensive security review has been completed by an independent auditor. Use at your own risk.
Stellar transactions are fully transparent by default — every transfer, sender, and recipient is visible on the public ledger. Caligo solves this by introducing a privacy layer for Stellar using zero-knowledge cryptography:
- Deposit a fixed amount of XLM into a shielded pool
- Withdraw to any fresh address using a zk-SNARK proof
- No link between deposit and withdrawal is visible on-chain
- Optional relayer routing hides your network identity
Caligo is inspired by privacy protocols like Tornado Cash, rebuilt from scratch for Stellar's Soroban smart contract platform.
- Zero-Knowledge Proofs — Groth16 proofs on BN254 verify withdrawal eligibility without revealing deposit identity
- Poseidon Hashing — Circuit-optimized hash function (~240 R1CS constraints vs ~25,000 for SHA-256)
- Fixed-Denomination Pools — Uniform deposit sizes maximize the anonymity set
- Double-Spend Protection — Nullifier tracking prevents any deposit from being withdrawn twice
- Encrypted Note Backup — AES-256-GCM encrypted deposit notes with PBKDF2 key derivation
- Relayer Network — Permissionless relayer registration with on-chain fee caps
- Soroban Native — Built entirely on Stellar's Soroban smart contract platform
┌──────────────────────────────────────────────────────────────────┐
│ Client SDK (TypeScript) │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────────┐ │
│ │ Crypto │ │ Prover │ │ Wallet │ │ Relayer Discovery │ │
│ │ Poseidon │ │ snarkjs │ │ Notes │ │ Fee Estimation │ │
│ │ Merkle │ │ Groth16 │ │ Backup │ │ Relay Submission │ │
│ └────┬─────┘ └────┬─────┘ └────┬────┘ └────────┬─────────┘ │
└───────┼──────────────┼────────────┼────────────────┼─────────────┘
│ │ │ │
▼ ▼ │ ▼
┌──────────────┐ ┌──────────────┐ │ ┌──────────────────────┐
│ MixerPool │ │ Indexer │ │ │ Relayer Registry │
│ (Soroban) │ │ (Rust) │ │ │ (Soroban) │
│ │ │ │ │ │ │
│ • deposit() │ │ • Event poll │ │ │ • register() │
│ • withdraw() │ │ • Merkle │ │ │ • get_active() │
│ • verify() │ │ mirror │ │ │ • fee cap │
│ • nullifiers │ │ • REST API │ │ │ enforcement │
└──────────────┘ └──────────────┘ │ └──────────────────────┘
│ │
└───────────────────────────┘
Stellar Network
| Component | Language | Description |
|---|---|---|
| MixerPool Contract | Rust (Soroban) | Core privacy pool — accepts deposits, verifies Groth16 proofs, pays withdrawals |
| RelayerRegistry Contract | Rust (Soroban) | Permissionless relayer registration with fee cap enforcement |
| ZK Circuits | Circom 2 | Groth16 withdrawal proof circuit with Poseidon hashing and Merkle inclusion |
| Client SDK | TypeScript | Secret generation, proof creation (with Web Worker support), note management, relayer discovery |
| Relayer Server | Rust (axum) | Receives proof payloads from clients, broadcasts withdrawal transactions, earns fees |
| Indexer | Rust (axum) | Off-chain Soroban event listener, Merkle tree mirror, REST API, optional PostgreSQL persistence |
- Client generates a random
secretandnullifier(32 bytes each) - Computes
commitment = Poseidon(secret, nullifier) - Sends exactly the pool denomination (e.g., 100 XLM) + commitment to the MixerPool contract
- Contract appends commitment to the on-chain Merkle tree and updates root history
- Client saves an encrypted deposit note locally
- Client fetches the Merkle path for their commitment from the indexer
- Generates a Groth16 proof proving:
- Knowledge of
secretandnullifiersuch thatPoseidon(secret, nullifier)is in the Merkle tree Poseidon(nullifier) == nullifierHash(for double-spend tracking)- The proof is bound to a specific
recipient,relayer, andfee
- Knowledge of
- Submits the proof to the MixerPool contract (directly or via relayer)
- Contract verifies the proof, checks the nullifier hasn't been spent, and transfers funds
The proof reveals nothing about which deposit is being withdrawn.
caligo/
├── contracts/
│ ├── mixer_pool/ # Core mixer pool contract (Soroban)
│ │ ├── src/lib.rs # deposit(), withdraw(), verify()
│ │ └── src/tests.rs # Contract unit tests
│ └── relayer_registry/ # Relayer management contract
│ ├── src/lib.rs # register(), deactivate(), queries
│ └── src/tests.rs # Registry unit tests
├── circuits/
│ ├── withdraw.circom # Main withdrawal proof circuit
│ ├── merkle.circom # Merkle inclusion proof component
│ ├── poseidon.circom # Poseidon hash component
│ └── build/ # Compiled circuit artifacts
├── client/
│ ├── src/
│ │ ├── crypto/ # Poseidon, secrets, encryption, Merkle tree
│ │ ├── proof/ # snarkjs Groth16 prover/verifier + Web Worker
│ │ ├── wallet/ # Note store with encrypted backup
│ │ ├── sdk/ # MixerSDK high-level interface
│ │ └── relayer/ # Relayer discovery and fee estimation
│ └── tests/ # Unit, integration, cross-validation, E2E tests
├── relayer/
│ └── src/ # Relay server: validates proofs, broadcasts txs
├── indexer/
│ └── src/ # Event listener, Merkle mirror, REST API, PostgreSQL
├── scripts/
│ └── deploy.sh # Testnet deployment automation
├── plan.md # Full architecture specification
├── .env.example # Configuration template
└── Cargo.toml # Rust workspace root
- Rust (latest stable) with
wasm32-unknown-unknowntarget - Soroban CLI (
stellar-cliorsoroban-cli) - Node.js (v18+) and npm
- Circom 2 and snarkjs (for circuit compilation)
# Clone the repository
git clone https://github.com/GrimyFishTank/caligo.git
cd caligo
# Install Rust + Soroban toolchain
rustup target add wasm32-unknown-unknown
cargo install --locked soroban-cli
# Install circuit tools
npm install -g circom snarkjs
# Install client SDK dependencies
cd client && npm install && cd ..# Build all Soroban contracts
stellar contract build
# Or build individually
stellar contract build --manifest-path contracts/mixer_pool/Cargo.toml
stellar contract build --manifest-path contracts/relayer_registry/Cargo.toml# Compile circuit and run trusted setup
npm run build:circuit
npm run setup
# This produces:
# circuits/build/withdraw_js/withdraw.wasm (proving WASM)
# circuits/build/withdraw_0001.zkey (proving key)
# circuits/build/verification_key.json (verification key)# Contract tests (Rust)
cargo test
# Client SDK tests (TypeScript)
cd client && npm test
# E2E tests (requires circuit artifacts)
cd client && npx jest tests/e2e.test.ts- Copy
.env.exampleto.envand set yourDEPLOYER_SECRET_KEY - Fund your account via Stellar Friendbot
- Run the deployment script:
bash scripts/deploy.shThis will:
- Build both contracts to WASM
- Deploy MixerPool and RelayerRegistry to Soroban testnet
- Initialize contracts with default parameters
- Output the contract IDs for your
.env
| Variable | Default | Description |
|---|---|---|
POOL_DENOMINATION |
10000000000 |
Pool size in stroops (100 XLM) |
POOL_MAX_FEE |
1000000000 |
Max relayer fee (10 XLM) |
POOL_TREE_DEPTH |
20 |
Merkle tree depth (2^20 = ~1M deposits) |
POOL_ROOT_HISTORY_SIZE |
500 |
Number of historical roots kept valid |
RELAYER_MAX_FEE_BPS |
100 |
Max relayer fee in basis points (1%) |
| Context | Hash Function | Rationale |
|---|---|---|
| Inside ZK circuits | Poseidon | ~240 R1CS constraints per hash |
| Client-side Merkle tree | Poseidon (circomlibjs) | Must match circuit exactly |
| On-chain (non-circuit) | SHA-256 (Soroban host fn) | Native, efficient |
| Address encoding | SHA-256 → mod p | Deterministic field element from Stellar address |
Groth16 on the BN254 curve was chosen for V1 because:
- Smallest proof size (~256 bytes)
- Lowest verifier cost (critical for Soroban's instruction budget)
- Benchmarked at ~23ms native, ~70-117ms estimated WASM — well within Soroban limits
The tradeoff is a circuit-specific trusted setup ceremony — required before mainnet deployment.
All three layers (contract, circuit, client SDK) must produce identical outputs for:
- Poseidon hashing:
light-poseidon(Rust) ↔circomlibjs(TypeScript) ↔circomlib(Circom) - Address-to-field conversion:
SHA-256(strkey_utf8) mod BN254_FIELD_ORDER - Merkle tree computation: Identical zero-value initialization and Poseidon node hashing
Cross-validation tests verify Rust and TypeScript implementations produce matching outputs for pinned test vectors.
- Unlinkability: Deposits and withdrawals cannot be correlated by on-chain observers
- Double-spend prevention: Nullifier hashes are stored permanently; reuse is rejected
- Proof binding: Recipient, relayer, and fee are public inputs — proof is invalid if any are changed
- Root validation: Only roots in the contract's history window are accepted
- Fee caps: On-chain enforcement prevents relayer fee inflation
- The Groth16 trusted setup ceremony is performed honestly (at least 1 honest participant)
- Users withdraw to fresh addresses with no prior transaction history
- Users securely back up their encrypted deposit notes
- The BN254 curve and Poseidon hash function remain cryptographically secure
- Anonymity set — Privacy strength depends on pool activity. Low-volume pools offer weaker privacy.
- Note loss — Lost deposit notes mean permanently lost funds (no on-chain recovery in V1)
- Recipient visibility — Withdrawal destination is a public input (use fresh addresses)
- Root expiry — Users must withdraw within the root history window (default: 500 deposits)
- Proof generation time — Client-side proof generation takes 5-15 seconds on mobile devices
| Endpoint | Method | Description |
|---|---|---|
/merkle-path?commitment=0x... |
GET | Returns Merkle proof for a commitment |
/pool-state |
GET | Returns pool info (deposit count, root, denomination) |
/roots |
GET | Returns the current root history |
/health |
GET | Health check |
import { MixerSDK } from 'caligo-client';
const sdk = new MixerSDK(config);
// Deposit
const { note, commitment } = await sdk.prepareDeposit();
await sdk.finalizeDeposit(commitment, depositorKeypair);
// Withdraw (direct)
const withdrawal = await sdk.prepareWithdrawal(note, recipientAddress);
await sdk.finalizeWithdrawal(withdrawal, recipientKeypair);
// Withdraw (via relayer)
const relayer = await selectCheapestRelayer(registryRelayers);
await submitRelayRequest(relayer, withdrawalPayload);// MixerPool
fn deposit(env, depositor: Address, commitment: BytesN<32>)
fn withdraw(env, proof: BytesN<256>, root: BytesN<32>,
nullifier_hash: BytesN<32>, recipient: Address,
relayer: Address, fee: i128)
fn get_root(env) -> BytesN<32>
fn is_nullifier_spent(env, nullifier_hash: BytesN<32>) -> bool
// RelayerRegistry
fn register(env, relayer: Address, endpoint: String, fee_bps: u32)
fn get_active_relayers(env) -> Vec<RelayerInfo>
fn deactivate(env, caller: Address, relayer: Address)Caligo includes 120+ tests across all components:
| Suite | Count | Coverage |
|---|---|---|
| Contract unit tests (MixerPool) | 18+ | Deposits, withdrawals, nullifiers, root history, fee caps, address encoding |
| Contract unit tests (RelayerRegistry) | 15 | Registration, deactivation, fee limits, queries |
| Client crypto tests | 34 | Poseidon, encryption, Merkle tree, address encoding |
| Client wallet tests | 6 | Note store, encrypted backup/restore |
| Client relayer tests | 8 | Discovery, fee estimation, selection |
| Client worker-prover tests | 4 | Web Worker wrapper, main-thread fallback |
| Cross-validation tests | 6 | Rust ↔ TypeScript hash consistency |
| E2E integration tests | 6 | Full deposit → proof → verify cycle |
| Relayer server tests | 6 | Request validation, address parsing, hex encoding |
| Indexer unit tests | 9 | Merkle tree, Poseidon hashing, proof reconstruction |
| Indexer benchmarks | 1 | BN254 pairing cost measurement |
# Run all tests
cargo test # Rust contracts
cd client && npm test # TypeScript SDK (62 tests)
cargo test --manifest-path indexer/Cargo.toml # Indexer (10 tests)
cargo test --manifest-path relayer/Cargo.toml # Relayer (6 tests)- Trusted setup ceremony — Multi-party computation with 10+ independent contributors
- Third-party security audit — Contracts, circuits, and client SDK
- Testnet public beta — Deploy to Stellar testnet with monitoring
- Mainnet deployment — After audit completion and ceremony
- Web Worker proof generation — snarkjs runs in a Web Worker with main-thread fallback
- PostgreSQL indexer storage — Optional persistent storage backend (build with
--features postgres) - Relayer server — Standalone relay binary with validation, rate limiting, and fee tracking
- Batch withdrawal processing — Aggregate multiple withdrawals to reduce per-tx cost
- Circuit optimization — Reduce R1CS constraint count for faster proof generation
- WASM verifier optimization — Profile and optimize the on-chain Groth16 verifier
- Multi-asset pools — Support USDC, wBTC, and other Stellar tokens via SAC
- On-chain encrypted note storage — Recover deposit notes from chain history
- PLONK upgrade — Universal trusted setup, easier circuit iteration
- Confidential amounts — Variable deposit sizes with range proofs
- Stealth address generation — Automatically derive fresh recipient addresses
- Rollup layer — Batch proofs to reduce per-transaction Soroban fees
- Shielded wallet — Full private balance management beyond mixer pools
- Cross-chain bridges — Privacy-preserving transfers between Stellar and other chains
- Mobile SDK — Native iOS/Android proof generation
| Layer | Technology |
|---|---|
| Smart Contracts | Rust, Soroban SDK v22 |
| ZK Circuits | Circom 2, snarkjs |
| Proving Scheme | Groth16 (BN254) |
| Hash (in-circuit) | Poseidon |
| Hash (on-chain) | SHA-256 (Soroban host fn) |
| Client SDK | TypeScript, circomlibjs |
| Indexer | Rust, axum, tokio |
| Encryption | AES-256-GCM, PBKDF2 (600K iterations) |
| Stellar SDK | @stellar/stellar-sdk |
Contributions are welcome. Please open an issue to discuss proposed changes before submitting a pull request.
# Run contract tests with output
cargo test -- --nocapture
# Run client tests in watch mode
cd client && npx jest --watch
# Benchmark Groth16 verifier cost
cargo test --manifest-path indexer/Cargo.toml --test bench_pairing -- --nocaptureMIT — Copyright (c) 2026 GrimyFishTank
- Tornado Cash — Original mixer protocol design inspiration
- circomlib — Poseidon hash circuit implementation
- snarkjs — Groth16 proving system
- Soroban — Stellar smart contract platform
- arkworks — Rust elliptic curve and pairing library
Caligo — Privacy for Stellar. Zero-knowledge proofs. Unlinkable transactions. Private XLM transfers on Soroban.