Voidgun Protocol
Privacy-via-proxy using Railgun protocol integration
1. Protocol Overview
Voidgun enables privacy pool access through Railgun's battle-tested Groth16 circuits. Keys are derived deterministically from your wallet signature.
| Component | Purpose | Technology |
|---|---|---|
| Key Derivation | Derive Baby Jubjub keys from wallet signature | BIP39/BIP32, EdDSA |
| Note Encryption | Encrypt shielded balance commitments | ChaCha20-Poly1305, AES-GCM |
| Proof Generation | Generate validity proofs for transfers | Groth16, ark-circom |
| Merkle Sync | Track commitment tree from chain events | Poseidon hash, depth-16 |
2. Deployment Architecture
Voidgun uses a privacy-via-proxy architecture. Your wallet connects to a Cloudflare Worker that routes to Ethereum RPC and a private VPC service that runs the proxy + prover.
Cloudflare Worker
read/submit txs
private VPC service
| RPC Method | Description |
|---|---|
| voidgun_init | Initialize wallet with signature, derive keys |
| voidgun_sync | Sync wallet state from chain events |
| voidgun_shield | Build shield calldata for deposit into Railgun pool |
| voidgun_shieldedBalance | Get shielded balance for a token |
| voidgun_address | Get 0zk receiving address |
| voidgun_unshield | Withdraw tokens to public address |
3. Mainnet Shield/Unshield Flow
Mainnet shield/unshield runs entirely through the Voidgun RPC (privacy-via-proxy). The wallet signs approvals and shields locally, while the proxy generates ZK proofs and submits unshield transactions with the relayer key.
privacy-via-proxy
on-chain txs
| Step | RPC Methods | Crypto Location | On-Chain Tx |
|---|---|---|---|
| Initialize keys | voidgun_init | Domain signature in wallet; key derivation in proxy | None |
| Sync state | voidgun_sync | Proxy loads events + updates Merkle cache | None |
| Approve USDC | eth_sendRawTransaction | Wallet signs approval locally | USDC approve |
| Shield 1 USDC | voidgun_shield | Wallet signs shield tx locally | Railgun relay deposit |
| Unshield 1 USDC | voidgun_unshield | Proxy generates ZK proof + relayer signs | Railgun relay withdraw |
| Verify balances | voidgun_sync | Proxy updates cached notes | None |
4. Key Derivation Flow
Your Railgun wallet keys are derived deterministically from a single wallet signature. No new seed phrase needed.
Click "Next Step" to walk through the derivation flow...
// Key derivation paths (Baby Jubjub curve) const SPENDING_PATH = "m/44'/1984'/0'/0'"; // EdDSA signing const VIEWING_PATH = "m/420'/1984'/0'/0'"; // Note decryption // Derive from wallet signature let entropy = keccak256(signature)[0..16]; let mnemonic = bip39_from_entropy(entropy); let master = bip32_derive_master(mnemonic); let spending_key = master.derive(SPENDING_PATH); let viewing_key = master.derive(VIEWING_PATH);
5. Note Structure
Shielded balances are stored as encrypted Notes in a Merkle tree. Each note commits to a value without revealing it.
NOTE (Private Data)
┌─────────────────────────────────────────────────────┐
│ │
│ npk ────────┐ Note Public Key │
│ │ = Poseidon(masterPublicKey, │
│ │ random) │
│ │ │
│ token ──────┼──┐ ERC20 contract address │
│ │ │ │
│ value ──────┼──┼─┐ Amount (e.g. 10 ETH) │
│ │ │ │ │
│ random ─────┼──┼─┼─┐ Random blinding factor │
│ │ │ │ │ │
└───────────────┼──┼─┼─┼──────────────────────────────┘
│ │ │ │
v v v v
┌─────────────────┐
│ Poseidon │ Hash function
│ (npk, token, │
│ value, rand) │
└────────┬────────┘
│
v
┌─────────────────────────────────────────────────────┐
│ COMMITMENT (On-Chain) │
│ │
│ 32-byte hash stored in Merkle tree leaf │
│ Hides all note details from observers │
│ │
└─────────────────────────────────────────────────────┘
NULLIFIER (Revealed on Spend)
┌─────────────────────────────────────────────────────┐
│ │
│ = Poseidon(nullifyingKey, leafIndex) │
│ │
│ - Unique per note (prevents double-spend) │
│ - Cannot be linked back to commitment │
│ - Published on-chain when note is consumed │
│ │
└─────────────────────────────────────────────────────┘
6. Merkle Tree
Commitments are stored in a depth-16 Poseidon Merkle tree. The tree is synced from on-chain Shield and Transact events.
Click a path button to visualize the Merkle proof...
| Property | Value |
|---|---|
| Depth | 16 levels |
| Capacity | 65,536 commitments per tree (new tree created when full) |
| Hash | Poseidon (BN254 scalar field) |
| Proof Size | 16 sibling hashes (512 bytes) |
7. Shielded Transfer
A transfer consumes input notes and creates output notes, all verified by a Groth16 proof.
Input Note
Output Note (Recipient)
Change Note (Sender)
Circuit Variants
Named by inputs x outputs count
| Variant | Inputs | Outputs | Use Case |
|---|---|---|---|
| 01x01 | 1 | 1 | Simple send (no change) |
| 02x02 | 2 | 2 | Standard transfer with change |
| 08x02 | 8 | 2 | Consolidation (merge notes) |
8. Groth16 Proof Generation
Proofs are generated using ark-circom with Railgun's trusted setup artifacts from IPFS.
// Proof generation with ark-circom let prover = RailgunProver::new(artifact_store); // Build witness for 2-input, 2-output transfer let witness = TransactWitness { merkle_root, bound_params_hash, nullifiers: [nf1, nf2], commitments: [cm_out, cm_change], // ... private inputs }; // Generate Groth16 proof (~2-5 seconds) let proof = prover.prove(&variant, &witness).await?; // Verify locally before submitting let valid = prover.verify_proof(&variant, &proof).await?;
| Artifact | Size | Purpose |
|---|---|---|
| WASM | ~5 MB | Witness calculation (circom) |
| ZKEY | ~50-200 MB | Proving key (Groth16) |
| VKEY | ~5 KB | Verification key (JSON) |