Docs
/architecture
/Overview

Overview

System design of Veil

System Architecture

Veil follows a hybrid architecture to balance developer experience (Python) with performance (Rust).

Layer 1: Python SDK (veil)

The user-facing API layer providing high-level primitives for privacy-preserving transactions.

VeilClient

Main entry point for all privacy operations:

Methods:

  • shield() / shield_async() - Convert public tokens to private commitments
  • private_transfer() / private_transfer_async() - Transfer privately between notes
  • unshield() / unshield_async() - Withdraw to public addresses
  • verify_proof() - Client-side proof verification
  • is_nullifier_spent() - Check if a nullifier has been used
  • get_merkle_root() - Query current on-chain Merkle root
  • get_commitment_index() - Find leaf index of a commitment

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

RelayerClient

IP privacy and gas abstraction layer:

Methods:

  • add_relayer(url, priority) - Register a relayer endpoint
  • add_default_relayers() - Add known public relayers
  • select_relayer(strategy) - Choose optimal relayer (lowest_fee, fastest, balanced, random)
  • estimate_fee(transaction) - Get fee quote from relayer
  • submit_private_transfer(request) - Submit via relayer (hides IP)
  • get_relayer_status(url) - Health check and metrics

Benefits:

  • Breaks IP-to-transaction link
  • No direct connection to Solana RPC
  • Typical fee: 0.3% (30 basis points)

Types Module

Data structures and type definitions:

  • Note - Private note with amount, blinding, commitment
  • PrivateTransaction - Transaction result with status, signature, secrets
  • CommitmentData - Commitment manipulation (to_hex, from_hex)
  • EncryptedNote - ECDH-encrypted note data
  • TransactionStatus - Enum for transaction states
  • RelayerInfo - Relayer metadata and stats
  • MerkleProof - Merkle path and verification data

Utilities

  • generate_secret() - Cryptographically secure random secret generation
  • commitment_to_hex() / hex_to_commitment() - Serialization helpers
  • validate_solana_address() - Address validation
  • validate_secret() - Secret format validation

Solana Integration

  • SolanaClient: Async RPC client for blockchain queries and submissions
  • InstructionBuilder: Constructs Anchor instructions for the privacy program
  • Account Fetching: Queries on-chain state (pool, nullifiers, Merkle root)

Layer 2: Rust Core (veil-core)

The high-performance cryptographic engine, exposed to Python via PyO3 bindings.

Crypto Module

Pedersen Commitments

C = amount * G + blinding * H
  • Curve: BN254 G1 subgroup
  • Generators: G (standard), H (nothing-up-my-sleeve construction)
  • Properties: Perfectly hiding, computationally binding
  • Size: 32 bytes per commitment

Circuit-Safe Nullifier Derivation

spending_key = Poseidon(secret, "NYX_SPENDING_KEY")
nullifier = Poseidon(spending_key, Hash(leaf_index || "NYX_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: "NYX_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: 5-10 seconds (client-side, depends on hardware)
  • Proof Size: 256 bytes (proof_a: 64, proof_b: 128, proof_c: 64)
  • Library: ark-groth16 from arkworks-rs ecosystem

Public Inputs (96 bytes):

  1. merkle_root (32 bytes) - Current Merkle tree root
  2. nullifier (32 bytes) - Nullifier to prevent double-spend
  3. new_commitment (32 bytes) - Output commitment

Private Inputs (Witness):

  • secret (32 bytes) - Master secret
  • input_amount, input_blinding - Input note data
  • output_blinding - Output note blinding factor
  • merkle_path (20 × 32 bytes) - Merkle proof siblings
  • leaf_index - Position in Merkle tree
  • Asset IDs and metadata

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

MVP Mode (Testing Only)

  • Proof System: Ed25519 signature-based
  • Proof Size: 96 bytes (64 signature + 32 pubkey)
  • Proving Time: <1ms (for rapid development)
  • Security: NOT production-ready, testing only
  • Auto-Detection: On-chain program detects proof length

Bindings Module

PyO3 interface exposing Rust functions to Python:

Exposed Functions:

  • generate_commitment(amount, secret) → bytes
  • generate_nullifier(spending_key, leaf_index) → bytes
  • generate_proof(witness_json) → bytes (proof)
  • verify_proof(proof, public_inputs) → bool
  • poseidon_hash(inputs) → bytes
  • merkle_root(leaves) → bytes
  • encrypt_note(amount, blinding, recipient_pubkey) → EncryptedNote
  • decrypt_note(encrypted, secret_key)Option<Note>

Error Handling:

  • Rust errors converted to Python exceptions
  • Detailed error messages for debugging
  • Type conversions handled automatically

Layer 3: Solana On-Chain Program

Anchor-based smart contract handling verification and state management.

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
  • Initialization: One-time setup via initialize instruction

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)
  • Creation: Anchor init constraint ensures uniqueness

Instructions

1. Initialize

  • Creates the privacy pool
  • Sets authority and initial Merkle root
  • One-time operation per deployment

2. Shield / ShieldSOL

  • Deposits public tokens → creates private commitment
  • Adds commitment to Merkle tree
  • Transfers tokens to pool vault

3. Transfer

  • Inputs: Proof, nullifier, new commitment, encrypted note
  • Verification: zkSNARK proof via groth16-solana
  • Checks: Nullifier not spent, Merkle root in history
  • 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

5. UpdateRelayer (Future)

  • Manages relayer registry
  • Sets fee limits and permissions

6. WithdrawFees (Future)

  • Allows relayers to claim accumulated fees

Groth16 Verification

Integration:

  • Uses groth16-solana crate for on-chain verification
  • Verification cost: ~200,000 compute units
  • Verification time: <1 second

Process:

  1. Deserialize proof (256 bytes) and public inputs (96 bytes)
  2. Perform BN254 pairing checks
  3. Verify equation: e(A, B) = e(α, β) · e(C, δ) · e(public_inputs, γ)
  4. Accept transaction if verification passes

Proof Auto-Detection:

if proof.len() == 256 {
    verify_groth16(proof, public_inputs)
} else if proof.len() == 96 {
    verify_mvp(proof, public_inputs)  // Dev mode only
} else {
    return Err(InvalidProofSize)
}

Nullifier Tracking

PDA-Based Approach:

  • Each nullifier gets its own PDA account
  • PDA derivation: [b"nullifier", pool_id, nullifier_bytes]
  • Anchor's init constraint prevents duplicate creation
  • Attempting to reuse a nullifier fails automatically

Benefits:

  • O(1) lookup via PDA derivation (no iteration)
  • Efficient storage (~0.00089 SOL rent per nullifier)
  • Cryptographically guaranteed uniqueness
  • Relayer pays rent (incentivizes efficient gas usage)

Root History Buffer

Purpose: Front-running protection

Implementation:

root_history: [MerkleRoot; 30]
current_root_index: u8  // Circular buffer index

Operation:

  • When Merkle tree updates, new root added to buffer
  • Old root (>30 updates ago) is overwritten
  • Proofs valid for any root in history window
  • ~1 minute validity window (30 roots × 2 sec/block)

Benefits:

  • Users have time to submit proof after generation
  • Concurrent transactions don't invalidate each other's proofs
  • No race conditions during proof generation period

Compute Unit Analysis

OperationCU CostNotes
Shield~50,000Simple addition to Merkle tree
Transfer~200,000Includes Groth16 verification
Unshield~150,000Verification + token transfer
Merkle Update~10,000Hash computation for path
Nullifier Check~5,000PDA derivation and lookup

Total Transaction Cost:

  • Transfer: 200k CU ≈ 0.0002 SOL (at 5,000 lamports/CU)
  • Plus Solana base fee: ~0.000005 SOL
  • Plus relayer fee (if used): ~0.3% of amount

Performance Characteristics

Client-Side (Python + Rust)

OperationTimeRAMCPU
Generate Secret<1msMinimalMinimal
Create Commitment<10ms<10 MB1 core
Generate Proof5-10s2-4 GBMulti-core (beneficial)
Verify Proof (local)<100ms<100 MB1 core
Encrypt Note<5msMinimalMinimal
Scan 1000 Notes~5s<50 MB1 core

On-Chain (Solana)

MetricValue
Proof Verification~200k CU (~0.0002 SOL)
Block Time~400-600ms (Solana average)
Confirmation~2-3 seconds (finalized)
ThroughputLimited by Solana TPS (~3,000/s)

Scalability

  • Merkle Tree: Depth 20 supports ~1M leaves
  • Concurrent Users: Unlimited (stateless proofs)
  • Transaction Ordering: Root history allows parallel submissions
  • Storage Growth: ~128 bytes per nullifier (bounded by leaves)

Security Model

Assumptions:

  1. Discrete Log: BN254 discrete log is hard
  2. Trusted Setup: At least one MPC participant is honest
  3. Solana Security: Blockchain provides integrity and availability
  4. Randomness: OsRng provides secure randomness

Guarantees:

  • Amount privacy (information-theoretic)
  • Sender/recipient unlinkability (computational)
  • Double-spend prevention (protocol-enforced)
  • Front-running protection (30-root buffer)

Limitations:

  • Timing analysis at shield/unshield boundaries
  • IP exposure without relayers
  • Trusted setup required (MPC ceremony in Phase 4)

See also: