Skip to content

MdSadiqMd/TraceZero

Repository files navigation

TraceZero

Privacy-preserving transactions on Solana using ZK proofs, blind signatures, and Tor routing

Demo Video

Architecture

Protocol Flow (Updated with Security Fixes)

sequenceDiagram
    autonumber
    participant W as User Wallet
    participant D as dApp (Browser)
    participant Tor as Tor Network
    participant R as Relayer
    participant T as Treasury Wallet
    participant DW as Deposit Wallet
    participant S as Solana Program
    participant P as Pool PDA
    participant SA as Stealth Address
    participant Dst as Destination Wallet

    rect rgb(60, 60, 90)
        Note over W,T: Phase 1 — Credit Acquisition (visible on-chain, cryptographically unlinkable)<br/>RSA Auth | Blind Sig Verification
        D->>R: GET /info → RSA pubkey (n, e) + treasury Solana address
        Note over D: No version exposure
        D->>D: token_id = CSPRNG(256 bits) Secure random
        D->>D: r = random blinding factor
        D->>D: blinded_token = RSA_Blind(token_id, r, RSA_pubkey)
        W->>T: SystemProgram.transfer(amount + fee_bps) to Treasury Wallet
        Note over W,T: Treasury Wallet ≠ Deposit Wallet<br/>Separate keypairs, no on-chain link<br/> Wallet separation (anti-correlation)
        D->>D: Await TX finalized confirmation + RPC propagation delay
        D->>R: POST /sign {blinded_token, payment_tx_sig, payer_pubkey}
        R->>S: Fetch TX, verify pre/post balances (treasury received ≥ expected)
        Note over R: Payment verification
        R->>R: signed_blinded = RSA_BlindSign(blinded_token, RSA_privkey)
        Note over R: RSA signature
        R-->>D: signed_blinded_token
        D->>D: signature = RSA_Unblind(signed_blinded, r, RSA_pubkey)
        D->>D: Verify signature
        D->>D: Encrypt & store (token_id, signature) in localStorage
        Note over D: AES-256-GCM encryption (C01)<br/>PBKDF2 100k iterations
        Note over R: Relayer signed blinded_token without seeing token_id.<br/>Blinding factor r is never transmitted.<br/>Linking blinded ↔ unblinded is computationally infeasible.
    end

    rect rgb(40, 80, 60)
        Note over D,P: Phase 2 — Deposit via Tor (unlinkable to Phase 1, can be hours/days later)<br/>Tor Security (H06-H11) | Idempotency | Rate Limiting
        D->>D: nullifier = CSPRNG(256 bits) Secure random
        D->>D: secret = CSPRNG(256 bits)
        D->>D: commitment = Poseidon(DOMAIN_COMMIT, nullifier, secret, amount)
        Note over D: Poseidon consistency<br/>Domain tags documented 
        D->>D: ECDH shared_secret = X25519(ephemeral_priv, relayer_ecdh_pub)
        Note over D: Per-request ephemeral keys <br/>Pinned relayer pubkey
        D->>D: ciphertext = AES-256-GCM(payload, shared_secret)
        Note over D: E2E encryption
        D->>Tor: Encrypted {token_id, signature, commitment, amount}
        Note over Tor: Circuit isolation <br/>Multi-gateway <br/>Health checks <br/>SOCKS hardening <br/>Tor-only mode <br/>TLS enforcement
        Tor->>R: Forward (exit IP ≠ user IP)
        R->>R: Decrypt payload via ECDH + AES-256-GCM
        R->>R: RSA_Verify(token_id, signature, RSA_pubkey)
        R->>R: Check idempotency: commitment exists?
        R->>R: Assert H(token_id) ∉ UsedTokenStore (disk-persisted)
        Note over R: Token replay prevention
        R->>R: Persist H(token_id) to UsedTokenStore (atomic write + SHA-256 checksum)
        R->>R: Check rate limits
        R->>R: Insert commitment into local Poseidon Merkle tree (depth=20)
        R->>R: merkle_root = recompute root
        R->>R: Check balance ≥ required
        DW->>S: deposit(bucket_id, commitment, token_hash, encrypted_note, merkle_root)
        Note over DW,S: Deposit Wallet is signer + fee payer.<br/>User wallet NEVER appears in this TX.<br/>Atomic operations
        S->>S: Verify merkle_root on-chain
        S->>P: Update on-chain Merkle root + next_index
        S->>S: Init CommitmentRecord PDA
        S->>S: Init UsedToken PDA [seeds: "used_token", token_hash]
        S->>S: Init EncryptedNote PDA [seeds: "note", pool, index]
        S->>P: Increment anonymity_set_size
        R-->>Tor: {tx_signature, leaf_index}
        Tor-->>D: Forward response
        D->>D: Encrypt & store (nullifier, secret, leaf_index) in localStorage
        Note over D: AES-256-GCM encryption
    end

    rect rgb(80, 50, 50)
        Note over D,SA: Phase 3 — Withdrawal (Groth16 ZK proof, zero-knowledge of depositor)<br/>Proof Verification | Point Validation | Recipient Validation
        D->>D: stealth_seed = SHA-256(X25519_ECDH(eph_priv, view_pub) ‖ spend_pub)
        D->>D: stealth_keypair = Ed25519_FromSeed(stealth_seed)
        D->>D: Assert stealth_pub[0] & 0xE0 == 0 (BN254 field compatibility)
        Note over D: Field reduction
        D->>R: GET /pool/{bucket_id} → current merkle_root
        D->>R: GET /proof/{bucket_id}/{leaf_index} → siblings[], pathIndices[]
        Note over R: Multi-account root validation<br/>35 historical roots supported
        D->>D: Verify Merkle proof locally (recompute root from commitment)
        D->>D: fee = amount × fee_bps ÷ 10000
        D->>D: Groth16 prove (WASM, ~10s in browser)
        Note over D: Public inputs: root, nullifierHash, recipient, amount, relayer, fee<br/>Public output: bindingHash = Poseidon(DOMAIN_BIND, nullifierHash, recipient, relayer, fee)<br/>Private inputs: nullifier, secret, pathElements[20], pathIndices[20]<br/>Proves: ∃ leaf ∈ MerkleTree s.t. leaf = Poseidon(DOMAIN_COMMIT, nullifier, secret, amount)<br/>Domain tags (L09)
        D->>D: Verify proof locally before submission (snarkjs.groth16.verify)
        D->>Tor: {proof_a, proof_b, proof_c, public_inputs, nullifier_hash, recipient, binding_hash, delay_hours}
        Note over D,Tor: Different Tor circuit than Phase 2<br/>Circuit isolation (H06)<br/>Secure random delay
        Tor->>R: Forward (different exit node)
        R->>R: Check rate limits
        R->>S: request_withdrawal(proof, nullifier_hash, stealth_addr, binding_hash, delay)
        S->>S: Validate recipient address
        Note over S: BN254 field constraint + not zero + not system program
        S->>S: CPI → ZK Verifier: Groth16 verify (proof_a negated, VK, public_inputs)
        Note over S: Proof verification<br/>Point validation
        S->>S: Assert nullifier_hash ∉ NullifierRegistry
        S->>S: Init PendingWithdrawal PDA [seeds: "pending", pool, tx_id]
        S->>P: Decrement anonymity_set_size
        Note over S: Deposit now "locked" for withdrawal
        S->>S: Set execute_after = Clock::get() + random_delay
        Note over S: Timelock: 1-24 hours (user-chosen random delay)<br/>Secure random
        R->>R: Pre-fund recipient if balance < rent-exempt minimum (890,880 lamports)
        R->>R: Pre-fund treasury PDA if needed
        R->>R: Wait 500ms for settlement
        R->>S: execute_withdrawal(tx_id)
        S->>S: Assert Clock::get() ≥ execute_after
        S->>S: Validate recipient can receive lamports
        S->>S: Init Nullifier PDA [seeds: "nullifier", hash] (marks spent)
        P->>SA: Credit (amount - fee) lamports via try_borrow_mut_lamports
        Note over P,SA: Atomic transfer <br/> Balance conservation
        P->>T: Credit fee lamports to treasury
    end

    rect rgb(70, 70, 40)
        Note over SA,Dst: Phase 4 — Claim / Sweep (plain SOL transfer, no protocol involvement)<br/>Stealth keys encrypted
        D->>D: Unlock encrypted storage with password
        D->>D: Load stealth secret key from encrypted localStorage
        D->>D: Reconstruct Ed25519 Keypair from stored secret key
        SA->>Dst: SystemProgram.transfer(balance - 5000 lamports TX fee)
        Note over SA,Dst: Stealth → Destination is visible on-chain,<br/>but Stealth → Deposit link is broken by ZK proof.<br/>Observer sees: "unknown address sent SOL to destination."<br/>Privacy guide recommends fresh wallet + delays
    end
Loading

On-Chain Trace Analysis (Updated with Security Fixes)

flowchart LR
    subgraph TX1["TX1: Credit Purchase"]
        UW[User Wallet] -->|SOL + fee| TW[Treasury Wallet]
    end

    subgraph TX2["TX2: Pool Deposit"]
        DWallet[Deposit Wallet] -->|deposit instruction| Pool[Pool PDA]
    end

    subgraph TX3["TX3: Withdrawal Execution"]
        Pool -->|ZK-verified transfer| Stealth[Stealth Address]
    end

    subgraph TX4["TX4: Claim / Sweep"]
        Stealth -->|SystemProgram.transfer| Dest[Destination Wallet]
    end

    TX1 ~~~ TX2
    TX2 ~~~ TX3
    TX3 ~~~ TX4

    B1["🔒 RSA Blind Signature RFC 9474<br/>+ Treasury ≠ Deposit Wallet<br/>+ Blind Sig Verification (M01)<br/>+ Encrypted Storage (C01)"]
    B2["🔒 Groth16 ZK-SNARK<br/>+ Nullifier Tracking (C02)<br/>+ Binding Hash<br/>+ Multi-Account Roots (M11)<br/>+ Field Reduction (M02)"]
    B3["🔒 X25519 ECDH Stealth Address<br/>+ BN254 Field Reduction<br/>+ Recipient Validation (M13)<br/>+ Stealth Address (H03)"]
    B4["🔒 Network Security<br/>+ Tor Circuit Isolation <br/>+ Multi-Gateway <br/>+ E2E Encryption <br/>+ Tor-Only Mode <br/>+ TLS Enforcement <br/>+ Health Checks"]

    TX1 -.-|link broken by| B1
    B1 -.-|unlinkable| TX2
    TX2 -.-|link broken by| B2
    B2 -.-|zero-knowledge| TX3
    TX3 -.-|link broken by| B3
    B3 -.-|one-time address| TX4
    
    B4 -.->|protects all phases| TX1
    B4 -.->|protects all phases| TX2
    B4 -.->|protects all phases| TX3

    style B1 fill:#c0392b,color:#fff,stroke:none
    style B2 fill:#c0392b,color:#fff,stroke:none
    style B3 fill:#c0392b,color:#fff,stroke:none
    style B4 fill:#16a085,color:#fff,stroke:none
    style UW fill:#2c3e50,color:#fff
    style TW fill:#2c3e50,color:#fff
    style DWallet fill:#27ae60,color:#fff
    style Pool fill:#8e44ad,color:#fff
    style Stealth fill:#d35400,color:#fff
    style Dest fill:#2c3e50,color:#fff
Loading

What Blockchain Explorers See:

  1. TX1: User paid treasury (looks like any service payment)
  2. TX2: Deposit wallet deposited to pool (no user link, different wallet)
  3. TX3: Pool transferred to stealth address (ZK proof valid, no link to deposit)
  4. TX4: Stealth transferred to destination (no link to pool)

What They CANNOT See:

  • Link between TX1 and TX2 (blind signature + wallet separation)
  • Link between TX2 and TX3 (ZK proof reveals nothing)
  • Link between TX3 and TX4 (stealth address one-time use)
  • User's IP address (Tor network)
  • Deposit secrets (encrypted storage)
  • Which deposit corresponds to which withdrawal (anonymity set)

Privacy Primitives

Primitive Purpose Where Used
RSA-2048 Blind Signatures (RFC 9474) Cryptographic unlinkability between credit payment and token redemption Phase 1 → Phase 2 boundary
Poseidon Hash (domain-separated) ZK-friendly commitment: H(DOMAIN, nullifier, secret, amount) Commitment, nullifier hash, binding hash, Merkle tree
Groth16 ZK-SNARK (Circom, depth-20 Merkle) Prove Merkle membership without revealing leaf index or commitment Withdrawal proof (~200k CU on-chain verification)
Nullifier Registry (on-chain PDA) Prevent double-spend without revealing which deposit was consumed Withdrawal execution
Binding Hash (Poseidon public output) Cryptographically bind proof to specific recipient, relayer, and fee Prevents proof front-running and parameter substitution
X25519 ECDH + Ed25519 Stealth Addresses One-time unlinkable recipient addresses (BN254 field-compatible) Withdrawal recipient generation
ECDH + AES-256-GCM Payload Encryption End-to-end encryption of deposit requests (Tor exit node protection) Deposit request payload
Tor Onion Routing (Docker SOCKS5 gateway) Network-layer IP anonymity, different circuits per phase Deposit + withdrawal request submission
Treasury / Deposit Wallet Separation Break on-chain trace chain: pool → deposit wallet → ??? (dead end) Relayer dual-wallet architecture
Randomized Timelock (1-24h) Prevent timing correlation between request and execution Withdrawal PendingWithdrawal PDA
Fixed Denomination Pools (7 buckets) Prevent amount-based correlation across deposits/withdrawals All pool operations

Technology Stack

  • Blockchain: Solana (Anchor framework)
  • ZK Proofs: Groth16 (Circom + snarkjs)
  • Blind Signatures: RSA-2048 (RFC 9474)
  • Network Privacy: Tor (Docker)
  • Frontend: TanStack Start + React
  • Backend: Rust (Axum)

Security

Key Management

TraceZero implements secure key management practices:

  • RSA Private Key: Stored in crates/relayer/rsa_signing_key.der with 600 permissions (owner read/write only)
  • Treasury Keypair: Stored in ~/.config/tracezero/treasury.json outside project directory with 600 permissions
  • Automatic Permission Setting: Keys are automatically created with secure permissions
  • Minimal Logging: Key paths are logged at debug level only, not in production logs

Verification

Run the security test suite:

./scripts/test-security-fixes.sh

License

MIT

About

A privacy proxy for your crypto transactions (ZK Proofs)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors