Zero-knowledge private payments on Solana
Sunflower shields SOL using Twisted ElGamal on Ristretto255, sigma proofs verified on-chain (same family as Token-2022 confidential transfers), and an optional Arcium MXE hybrid path for confidential compute. Real tokens sit in an SPL vault; balances are encrypted ciphertexts — not SNARKs, but rigorous elliptic-curve zero-knowledge.
Dual-path architecture
Two parallel privacy designs share the same frontend. Path A is the account model you use in the simple wallet; Path B adds an Umbra-style UTXO mixer with Merkle commitments.
ElGamal shielded accounts
- • SPL SOL in
vault_authorityATA - • Per-user
ShieldedAccountPDA with ciphertext_lo / ciphertext_hi - •
shield·transfer_private·unshield - • Pending credits merged via
apply_pending_balance
Program: DNgdrd77NG3z17F5wJJFuGYHKk9dF9HccKKpCQSXos1S
Commitments + nullifiers
- • SHA256 commitments in depth-10 Merkle tree (1,024 leaves)
- •
deposit_utxo→ SPL into pool + insert leaf - •
claim_utxowith Merkle proof + nullifier spend - • No ElGamal sigma proofs on this path — hash privacy instead
See advanced /dev panel for full mixer flow.
Cryptography & proofs
Privacy comes from encrypting 48-bit amounts as two 16-bit ElGamal chunks, then proving statements about ciphertexts without revealing keys or amounts. Proofs are sigma protocols made non-interactive via Fiat–Shamir — verified with solana-curve25519 syscalls on-chain.
functionsTwisted ElGamal encryption
Curve: Ristretto255 · G = basepoint · H = HashToPoint("privacy_vault_H_generator_v1")
Keygen: sk ← random, pk = sk · G
Encrypt m: d = r · G, e = m · H + r · pk → Ciphertext (d, e) [64 bytes]
Homomorphic: Enc(m1) + Enc(m2) = Enc(m1 + m2)
Balance: ciphertext_lo ← amount & 0xFFFF
ciphertext_hi ← amount >> 16PubkeyValidityProof
Prover knows sk such that pk = sk · G
s · G == R + c · pk
Used in: initialize_shielded_account, shield, transfer_private
CiphertextEqualityProof
Two ciphertexts under different keys encrypt the same amount m
Four Ristretto equations (r1…r4, s1…s3)
Used in: Private send — amount hidden, balance math enforced homomorphically
Private transfer verification
After sigma proofs pass, the program enforces homomorphic balance consistency (not itself a ZK proof):
new_source + transfer == old_source (for lo and hi chunks) Off-chain: crypto_helper plan-transfer → CiphertextEqualityProof + new ciphertexts On-chain: transfer_private verifies proofs + updates encrypted balances Amount: NOT visible on-chain during private send
End-to-end pipeline
Four execution zones. The browser never sends your ElGamal secret key to the server; heavy math runs in Rust via Next.js API routes calling crypto_helper.
Phantom signs txs · ElGamal sk/pk in sessionStorage · decrypt balance locally
privacy_vault program · vault_authority SPL custody · ShieldedAccount PDAs
crypto_helper: encrypt, prove_pubkey_validity, prove_ciphertext_equality, decrypt-balance
plan_transfer_prep circuit · hybrid path with transfer_private_arcium + replay ID
Arcium Bridge: 7Rj21r9ujnVCjHzNXahP3wYPJ8i9zzBcYpanyxGGjrtK
MXE account: 7Ab8P5AzcH7F8HhPXPXbGLtXHUwfCUwbcGA9TQWpjr4t · cluster offset 456
Technology stack
Solana / Anchor
On-chain program + PDAs
Ristretto255 ElGamal
Twisted ElGamal ciphertexts
Sigma proofs
Fiat–Shamir NIZKs (Token-2022 family)
crypto_helper (Rust)
Off-chain prover via Next.js API
Arcium MXE
Confidential compute on devnet cluster 456
UTXO mixer (Path B)
SHA256 commitments + Merkle depth 10
Try it on devnet
Connect Phantom on Devnet, shield SOL, send privately with sigma-verified transfers, and decrypt your balance only in your browser. Full cryptography details in finalreport.md.
Launch Privacy Vault