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.

[W]
Wallet
->
[S]
Sign Message
->
[K]
Derive Keys
->
[P]
ZK Proofs
->
[R]
Railgun Pool
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.

[W]
Wallet/dApp
->
[CF]
RPC Proxy
Cloudflare Worker
->
[ETH]
Ethereum RPC
read/submit txs
->
[VPC]
Voidgun Proxy + Prover
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.

[W]
Wallet / E2E Runner
->
[V]
Voidgun RPC
privacy-via-proxy
->
[ETH]
Railgun Relay
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.

1
Sign
2
Hash
3
Entropy
4
Mnemonic
5
BIP32
6
Keys
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.

Root
H1
H2
H3
H4
H5
H6
C0
C1
C2
C3
C4
C5
C6
C7
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

value 10 ETH
nullifier Revealed on-chain
proof Merkle path to root

Output Note (Recipient)

value 7 ETH
npk Recipient's key
encrypted ChaCha20-Poly1305

Change Note (Sender)

value 3 ETH
npk Sender's key
encrypted ChaCha20-Poly1305

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.

[1]
Witness
->
[2]
WASM
->
[3]
ZKEY
->
[+]
Proof
// 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)