getting started

Architecture

System architecture and component relationships

System Overview#

Mini Veil consists of four major components: the on-chain Solana program, the off-chain relayer, the browser-based frontend, and the core cryptographic SDK.

Architecture Note

The relayer manages the Merkle tree, not the browser. This is a deliberate design choice: the browser generates proofs but never holds the full tree. The relayer accumulates deposit events, maintains the in-memory Poseidon tree, and submits root updates via multisig transactions.

Component Map#

┌──────────────────────────────────────────────────┐
│                    User Wallet                     │
│  ┌────────────┐    ┌──────────────────┐           │
│  │ PrivateSend │    │  ScanClaimCard   │           │
│  │  (shield +  │    │  (detect +       │           │
│  │   prove +   │    │   claim stealth) │           │
│  │   unshield) │    └──────────────────┘           │
│  └─────┬───────┘                                   │
│        │                                            │
│  ┌─────▼──────────────────────────────────────┐    │
│  │     @mini-veil/core (SDK)                   │    │
│  │  keys() │ mixer() │ stealth() │ eta()       │    │
│  └──────────────────────────────────────────────┘    │
└──────────────────────┬───────────────────────────┘
                       │
          Instructions │ Events
                       │
┌──────────────────────▼───────────────────────────┐
│              Solana Program (BPF)                  │
│  ┌────────────┐  ┌──────────┐  ┌──────────────┐  │
│  │  MixerState │  │  Vault   │  │  KeyRegistry │  │
│  │  PDA        │  │  PDA     │  │  PDA         │  │
│  └────────────┘  └──────────┘  └──────────────┘  │
│  ┌────────────┐  ┌──────────┐                     │
│  │  Nullifier │  │Multisig  │                     │
│  │  PDA       │  │PDA       │                     │
│  └────────────┘  └──────────┘                     │
│  shield() │ unshield() │ update_root()            │
│  announce_stealth() │ claim_stealth()             │
│  register_keys() │ init_mixer()                   │
└──────────────────────┬───────────────────────────┘
                       │
          onLogs(PROGRAM_ID)
                       │
┌──────────────────────▼───────────────────────────┐
│                Off-chain Relayer                   │
│                                                    │
│  ┌────────────────┐  ┌────────────────────────┐  │
│  │ PoseidonMerkle  │  │   Express Server       │  │
│  │ Tree (in-mem)   │  │  /proof  /next-counter │  │
│  └────────────────┘  │  /health                │  │
│                       └────────────────────────┘  │
│  processDepositEvent() → pushRootUpdate()          │
│  (2-of-3 multisig signing)                         │
└────────────────────────────────────────────────────┘

Component Responsibilities#

Solana Program#

  • Maintains MixerState accounts with current Merkle root and total_shielded
  • Holds pool funds in system-owned Vault PDAs
  • Verifies Groth16 proofs via alt_bn128 pairing syscalls
  • Manages nullifier PDAs for double-spend prevention
  • Stores registered keys per wallet in KeyRegistry PDAs
  • Enforces 2-of-3 multisig for root updates

Relayer#

  • Subscribes to program logs via connection.onLogs(PROGRAM_ID)
  • Parses DepositEvent logs and inserts commitments into the in-memory Poseidon tree
  • Batches deposits until threshold is reached, then submits update_root via multisig
  • Serves Merkle proofs via /proof?commitment=0x...&denomination=...
  • Manages per-key counters via /next-counter?key=0x...&denomination=...
  • Configurable threshold (default 1 for MVP)

Frontend (Browser)#

  • PrivateSend component: key registration check, shield (if unspent), proof generation, bundled unshield + announce
  • ScanClaimCard component: scans for stealth announcements, decrypts, derives keys, claims funds
  • Uses snarkjs for client-side Groth16 proving (~2-5s)
  • Communicates with relayer for proofs and counters

Core SDK (@mini-veil/core) npm package coming soon#

  • Key derivation and management
  • Poseidon hashing (arity 1-3)
  • Deposit note creation (nullifier, commitment)
  • Groth16 proof generation and packing
  • Stealth address generation and scanning
  • ScalarSigner for stealth key signing
  • ETA confidential amounts — coming soon

Data Flow#

  1. User imports spending key (derived from wallet signature)
  2. PrivateSend queries relayer for counter and existing proof
  3. If note is unspent and no pending proof, shield transaction is sent
  4. Relayer picks up DepositEvent, inserts into tree, resolves pending proof requests
  5. Frontend polls /proof, receives Merkle path
  6. Frontend generates Groth16 proof via snarkjs
  7. Bundled unshield + announce_stealth transaction is submitted
  8. Recipient's ScanClaimCard detects via RPC log scanning
  9. Recipient claims stealth payment via ScalarSigner transaction