Skip to content

Instantly share code, notes, and snippets.

@MdSadiqMd
Created February 27, 2026 14:27
Show Gist options
  • Select an option

  • Save MdSadiqMd/d3578c0883c67b7115cb271c9f704ddf to your computer and use it in GitHub Desktop.

Select an option

Save MdSadiqMd/d3578c0883c67b7115cb271c9f704ddf to your computer and use it in GitHub Desktop.
```mermaid
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)
D->>R: GET /info → RSA pubkey (n, e) + treasury Solana address
D->>D: token_id = CSPRNG(256 bits)
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
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)
R->>R: signed_blinded = RSA_BlindSign(blinded_token, RSA_privkey)
R-->>D: signed_blinded_token
D->>D: signature = RSA_Unblind(signed_blinded, r, RSA_pubkey)
D->>D: Encrypt & store (token_id, signature) in localStorage
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)
D->>D: nullifier = CSPRNG(256 bits)
D->>D: secret = CSPRNG(256 bits)
D->>D: commitment = Poseidon(DOMAIN_COMMIT, nullifier, secret, amount)
D->>D: ECDH shared_secret = X25519(ephemeral_priv, relayer_ecdh_pub)
D->>D: ciphertext = AES-256-GCM(payload, shared_secret)
D->>Tor: Encrypted {token_id, signature, commitment, amount}
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: Assert H(token_id) ∉ UsedTokenStore (disk-persisted)
R->>R: Persist H(token_id) to UsedTokenStore (atomic write + SHA-256 checksum)
R->>R: Insert commitment into local Poseidon Merkle tree (depth=20)
R->>R: merkle_root = recompute root
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.
S->>P: Update on-chain Merkle root + next_index
S->>S: Init UsedToken PDA [seeds: "used_token", token_hash]
S->>S: Init EncryptedNote PDA [seeds: "note", pool, index]
R-->>Tor: {tx_signature, leaf_index}
Tor-->>D: Forward response
D->>D: Encrypt & store (nullifier, secret, leaf_index) in localStorage
end
rect rgb(80, 50, 50)
Note over D,SA: Phase 3 — Withdrawal (Groth16 ZK proof, zero-knowledge of depositor)
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)
D->>R: GET /pool/{bucket_id} → current merkle_root
D->>R: GET /proof/{bucket_id}/{leaf_index} → siblings[], pathIndices[]
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)
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
Tor->>R: Forward (different exit node)
R->>S: request_withdrawal(proof, nullifier_hash, stealth_addr, binding_hash, delay)
S->>S: CPI → ZK Verifier: Groth16 verify (proof_a negated, VK, public_inputs)
S->>S: Assert nullifier_hash ∉ NullifierRegistry
S->>S: Init PendingWithdrawal PDA [seeds: "pending", pool, tx_id]
S->>S: Set execute_after = Clock::get() + random_delay
Note over S: Timelock: 1-24 hours (user-chosen random delay)
R->>R: Pre-fund recipient if balance < rent-exempt minimum (890,880 lamports)
R->>R: Pre-fund treasury PDA if needed
R->>S: execute_withdrawal(tx_id)
S->>S: Assert Clock::get() ≥ execute_after
S->>S: Init Nullifier PDA [seeds: "nullifier", hash] (marks spent)
P->>SA: Credit (amount - fee) lamports via try_borrow_mut_lamports
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)
D->>D: Load stealth secret key from 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."
end
```
### On-Chain Trace Analysis
```mermaid
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]
B2[Groth16 ZK-SNARK<br/>+ Nullifier + Binding Hash]
B3[X25519 ECDH Stealth Address<br/>+ BN254 Field Reduction]
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
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 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
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment