Overview
System design of Mirage
Mirage follows a hybrid architecture to balance developer experience (Python) with performance (Rust), plus optional chain-agnostic payment support via x402.
Mirage uses a unified Python package with embedded Rust backend:
mirage/
├── mirage/ # Python SDK
│ ├── __init__.py
│ ├── client.py # PrivacyClient
│ ├── types.py
│ ├── utils.py
│ └── x402/ # x402 payment support (included)
│
├── src/ # Rust cryptography (PyO3 bindings)
│ ├── crypto/ # Commitments, Poseidon, Merkle, encryption
│ ├── proof/ # Groth16 circuits
│ └── lib.rs # PyO3 bindings
│
├── packages/
│ └── mirage-solana/ # On-chain Anchor program (separate)
│ └── programs/
│
├── Cargo.toml # Rust configuration
├── pyproject.toml # Python package configuration (maturin)
└── Makefile # Build commands
Installation: pip install mirage-solana (x402 support included by default)
The user-facing API layer providing high-level primitives for privacy-preserving transactions.
PrivacyClient
Main entry point for all privacy operations:
Methods:
shield_assets()/shield_assets_async()- Convert public tokens to private commitmentsprivate_transfer()/private_transfer_async()- Transfer privately between notesunshield_assets()/unshield_assets_async()- Withdraw to public addressesverify_proof()- Client-side proof verificationis_nullifier_spent()- Check if a nullifier has been usedget_merkle_root()- Query current on-chain Merkle root
Async vs Sync:
- Async methods (
*_async()): Submit to blockchain, returns transaction signature - Sync methods: Offline computation only, returns transaction data for later submission
- Useful for air-gapped signing and testing scenarios
X402Client
Chain-agnostic payment client for micropayments:
Methods:
pay_for_access(resource_url, price_usd, merchant_address)- Execute x402 payment flow- Supports 15+ blockchain networks
Benefits:
- Pay for privacy services using any supported blockchain
- No need for native SOL for gas in some workflows
- Cross-chain payment flexibility
Types Module
Data structures and type definitions:
ShieldRequest- Request to shield assetsTransferRequest- Request for private transferUnshieldRequest- Request to unshield assetsPrivateTransaction- Transaction result with status, signature, secretsCommitmentData- Commitment manipulation (to_hex, from_hex)TransactionStatus- Enum for transaction statesPaymentConfig/PaymentResponse- x402 payment types
Utilities
generate_secret()- Cryptographically secure random secret generationcommitment_to_hex()/hex_to_bytes()- Serialization helpersvalidate_solana_address()- Address validation
The high-performance cryptographic engine, exposed to Python via PyO3 bindings.
Crypto Module
Pedersen Commitments
C = amount * G + blinding * H
- Curve: BN254 G1 subgroup
- Domain Separator:
MIRAGE_PROTOCOL_PEDERSEN_H_V1 - Properties: Perfectly hiding, computationally binding
- Size: 32 bytes per commitment
Circuit-Safe Nullifier Derivation
spending_key = Poseidon(secret, "MIRAGE_SPENDING_KEY")
nullifier = Poseidon(spending_key, Hash(leaf_index || "MIRAGE_NULLIFIER"))
- Two-step process prevents secret exposure in constraints
- Security: Unlinkable to commitment without knowing secret
- Size: 32 bytes per nullifier
Poseidon Hash Function
- Parameters: t=3 (width), RF=8 (full rounds), RP=57 (partial rounds)
- S-box: x^5 over BN254 scalar field
- Efficiency: Optimized for zkSNARK circuits (~200 constraints per hash)
- Usage: Merkle tree, nullifier derivation, commitment blinding
ECDH Note Encryption
- Key Exchange: ECDH on BN254 G1 curve
- Symmetric Cipher: ChaCha20-Poly1305 AEAD
- Plaintext: 48 bytes (amount + blinding + asset_id)
- Ciphertext: 64 bytes (48 data + 16 MAC) + 32 bytes ephemeral key = 96 bytes total
- Domain Separator:
MIRAGE_NOTE_ENCRYPTION_V1 - Property: Forward secrecy via ephemeral keys
Merkle Tree
- Depth: 20 levels (supports ~1,048,576 leaves)
- Hash Function: Poseidon
- Optimization: Filled subtrees for O(log n) insertions
- Zero Hashes: Precomputed for empty nodes
- Storage: On-chain state maintained by Solana program
Proof Module
Groth16 zkSNARKs
- Circuit Name:
TransferCircuit - Constraints: ~7,000 R1CS constraints
- Proving Time: 2-5 seconds (client-side, depends on hardware)
- Proof Size: 256 bytes (proof_a: 64, proof_b: 128, proof_c: 64)
- Library:
ark-groth16from arkworks-rs ecosystem
Public Inputs (96 bytes):
merkle_root(32 bytes) - Current Merkle tree rootnullifier(32 bytes) - Nullifier to prevent double-spendnew_commitment(32 bytes) - Output commitment
Private Inputs (Witness):
secret(32 bytes) - Master secretinput_amount,input_blinding- Input note dataoutput_blinding- Output note blinding factormerkle_path(20 × 32 bytes) - Merkle proof siblingsleaf_index- Position in Merkle tree
Circuit Constraints Breakdown:
- Spending key derivation: ~400 constraints
- Input commitment verification: ~500 constraints
- Merkle membership proof: ~4,000 constraints (20 Poseidon hashes)
- Nullifier derivation: ~400 constraints
- Output commitment: ~500 constraints
- Range checks and validation: ~1,200 constraints
PyO3 Bindings
Rust functions exposed to Python via mirage._rust_core:
Exposed Functions:
generate_commitment(amount, secret)→ bytesgenerate_nullifier(spending_key, leaf_index)→ bytesgenerate_proof(witness_json)→ bytes (proof)verify_proof(proof, public_inputs)→ boolposeidon_hash(inputs)→ bytesencrypt_note(amount, blinding, recipient_pubkey)→ EncryptedNotedecrypt_note(encrypted, secret_key)→Option<Note>
Anchor-based smart contract handling verification and state management.
Program ID: 6p1tzefXSST8j72qcj5EU3pAcY5qSc3HnCfFqc2gWxjM
State Accounts
PrivacyPool Account (~1692 bytes)
pub struct PrivacyPool {
pub authority: Pubkey, // 32 bytes
pub merkle_root: [u8; 32], // 32 bytes
pub leaf_count: u64, // 8 bytes
pub root_history: [MerkleRoot; 30], // 960 bytes (32 × 30)
pub current_root_index: u8, // 1 byte
// ... additional fields
}- PDA Seeds:
[b"privacy_pool"] - Purpose: Main state for the privacy protocol
NullifierMarker Account (~128 bytes each)
pub struct NullifierMarker {
pub pool: Pubkey, // 32 bytes
pub nullifier: [u8; 32], // 32 bytes
pub spent_at: u64, // 8 bytes (slot number)
}- PDA Seeds:
[b"nullifier", pool.key(), &nullifier] - Purpose: Marks a nullifier as spent (double-spend prevention)
Instructions
1. Initialize
- Creates the privacy pool
- Sets authority and initial Merkle root
2. Shield / ShieldSOL
- Deposits public tokens → creates private commitment
- Adds commitment to Merkle tree
3. Transfer
- Inputs: Proof, nullifier, new commitment, encrypted note
- Verification: zkSNARK proof via
groth16-solana - Actions: Create nullifier PDA, add commitment, update root history
4. Unshield / UnshieldSOL
- Similar to transfer but outputs to public address
- Withdraws tokens from pool vault to recipient
Compute Unit Analysis
| Operation | CU Cost | Notes |
|---|---|---|
| Shield | ~50,000 | Simple addition to Merkle tree |
| Transfer | ~250,000 | Includes Groth16 verification |
| Unshield | ~200,000 | Verification + token transfer |
| Groth16 Verification | ~200,000 | BN254 pairing checks |
Optional chain-agnostic payment layer for paying for privacy services.
x402 Flow
1. Request Resource
User → X402Client.pay_for_access()
→ HTTP GET resource_url
→ 402 Payment Required response
2. Payment Construction
→ Parse X-Accept-Payment header
→ Generate payment payload (amount, merchant, nonce)
→ Sign payload (EIP-712 for EVM, native for Solana)
3. Payment Verification
→ POST to facilitator /verify
→ Facilitator validates signature, balance, amount
→ Returns verification_id
4. Resource Access
→ Retry HTTP GET with X-PAYMENT header
→ Server validates payment
→ 200 OK with X-PAYMENT-RESPONSE
→ User receives access token
Supported Networks
EVM Networks:
- Base, Base Sepolia
- Polygon, Polygon Amoy
- Avalanche, Avalanche Fuji
- IoTeX, Peaq, Sei, XLayer
Solana:
- Mainnet, Devnet
Client-Side (Python + Rust)
| Operation | Time | RAM | CPU |
|---|---|---|---|
| Generate Secret | <1ms | Minimal | Minimal |
| Create Commitment | <10ms | <10 MB | 1 core |
| Generate Proof | 2-5s | 2-4 GB | Multi-core (beneficial) |
| Verify Proof (local) | <100ms | <100 MB | 1 core |
| Encrypt Note | <5ms | Minimal | Minimal |
| x402 Payment | ~500ms | Minimal | Minimal |
On-Chain (Solana)
| Metric | Value |
|---|---|
| Proof Verification | ~250k CU (~0.00025 SOL) |
| Block Time | ~400-600ms (Solana average) |
| Confirmation | ~2-3 seconds (finalized) |
Cryptographic Assumptions:
- Discrete Log: BN254 discrete log is hard
- Trusted Setup: At least one MPC participant is honest
- Solana Security: Blockchain provides integrity and availability
- Randomness:
OsRngprovides secure randomness
Guarantees:
- Amount privacy (information-theoretic)
- Sender/recipient unlinkability (computational)
- Double-spend prevention (protocol-enforced)
- Front-running protection (30-root buffer)
x402 Security:
- EIP-712 signatures for EVM chains
- Nonce uniqueness prevents replay attacks
- Facilitator validation before settlement
See also:
- Data Flow - Detailed transaction flows
- Solana Program - On-chain implementation details
- Privacy Model - Privacy guarantees and limitations