Privacy Architecture in VoidDex: Integrating Railgun's zkSNARK System

Building a privacy-first DEX aggregator means understanding how zero-knowledge proofs can hide transaction details while still allowing on-chain verification. VoidDex integrates with Railgun to provide this privacy layer, letting users swap tokens without exposing their activity on the blockchain.

![Connecting your wallet to VoidDex](/images/menu/void-dex/unlock-wallet.jpg)

## Client-Side First: Your Keys Never Leave

Before diving into the technical details, the most important architectural decision: **all sensitive operations happen in your browser**. The VoidDex server never sees:

- Your wallet private key
- Your Railgun viewing key (used to decrypt your private balance)
- Your Railgun spending key (used to generate proofs)
- Your unencrypted private balance
- Which UTXOs belong to you

The server only handles non-sensitive operations: fetching DEX quotes, optimizing routes, and relaying already-signed transactions. Zero-knowledge proofs are generated entirely in-browser using WASM modules. This isn't just a security feature - it's the foundation of the entire privacy model. If the server could see your keys, there would be no privacy.

## The Privacy Problem

Every transaction on Ethereum is public. When you swap tokens on Uniswap, anyone can see your address, the tokens involved, and the amounts. This creates several problems:

- **Front-running**: MEV bots see your pending transaction and execute their own first
- **Sandwich attacks**: Bots manipulate prices before and after your swap
- **Surveillance**: Your entire financial history is linked to your address
- **Profiling**: Exchanges and services can analyze your trading patterns

Railgun solves this with zkSNARKs (Zero-Knowledge Succinct Non-Interactive Arguments of Knowledge), which let you prove you have tokens without revealing which specific tokens are yours.

## How Railgun Privacy Works

Railgun maintains a privacy pool of shielded tokens. When you shield tokens, they enter this pool and become indistinguishable from other shielded tokens. The system uses a UTXO model similar to Bitcoin but with zero-knowledge proofs.

```mermaid
flowchart TD
subgraph Public["Public Layer"]
Wallet[Your Wallet]
Contract[Railgun Contract]
end

subgraph Private["Private Layer"]
Pool[Privacy Pool]
Balance[Your Private Balance]
end

Wallet -->|Shield| Contract
Contract -->|Deposit| Pool
Pool -.->|Your encrypted UTXOs| Balance
Balance -->|Generate Proof| Contract
Contract -->|Unshield| Wallet
```

Your private balance consists of encrypted UTXOs (Unspent Transaction Outputs) that only you can decrypt using your viewing key. When you want to spend, you generate a proof that:

1. You own valid UTXOs in the pool
2. The UTXOs haven't been spent before
3. The amounts add up correctly

The proof reveals none of the actual UTXO details.

## Client-Side Architecture

![Creating a private wallet in VoidDex](/images/menu/void-dex/setting-up-privacy.jpg)

All privacy operations in VoidDex happen in the browser. The Railgun SDK loads WASM modules for cryptographic operations:

```typescript:src/lib/railgun/init.ts
// Initialize Railgun SDK in the browser
import {
startRailgunEngine,
loadProvider,
createRailgunWallet
} from '@railgun-community/wallet';

async function initializePrivacy(chainId: number) {
// Load the proving system (downloads ~40MB of circuit data)
await startRailgunEngine(
'voiddex',
{}, // No database persistence
false, // Not a mobile environment
undefined,
{
// Artifact download configuration
downloadUrl: 'https://voiddex.com/artifacts',
}
);

// Connect to blockchain
const provider = await loadProvider({
chainId,
provider: window.ethereum,
});

return provider;
}
```

Wallet creation generates the cryptographic keys needed for privacy:

```typescript:src/lib/railgun/wallet.ts
async function createPrivateWallet(mnemonic: string) {
const wallet = await createRailgunWallet(
mnemonic,
{
// Encryption key for local storage
encryptionKey: await deriveEncryptionKey(password),
}
);

return {
// For receiving private transfers
railgunAddress: wallet.railgunAddress,
// For decrypting your balance
viewingKey: wallet.viewingPrivateKey,
// For spending (never leaves the browser)
spendingKey: wallet.spendingPrivateKey,
};
}
```

## The Shield Operation

![Shielding tokens into your private balance](/images/menu/void-dex/swap.jpg)

Shielding moves tokens from your public wallet into the privacy pool:

```typescript:src/lib/railgun/shield.ts
import { generateShieldProof, populateShield } from '@railgun-community/wallet';

async function shieldTokens(
tokenAddress: string,
amount: bigint,
railgunAddress: string
) {
// Step 1: Generate the proof (happens locally)
const proof = await generateShieldProof(
{
chainId,
tokenAddress,
tokenAmount: amount,
railgunAddress,
}
);

// Step 2: Build the transaction
const { transaction } = await populateShield(
chainId,
proof,
);

// Step 3: User signs and broadcasts normally
const signer = await getSigner();
const tx = await signer.sendTransaction(transaction);

return tx.hash;
}
```

The shield transaction is public (everyone sees you deposited tokens), but your subsequent activity with those tokens is private.

## Private Transfers

Once tokens are shielded, you can transfer them privately to other Railgun addresses:

```typescript:src/lib/railgun/transfer.ts
import {
generateTransferProof,
populateProvedTransaction
} from '@railgun-community/wallet';

async function privateTransfer(
tokenAddress: string,
amount: bigint,
recipientRailgunAddress: string
) {
// Generate proof that you have sufficient balance
const proof = await generateTransferProof(
{
chainId,
tokenAddress,
tokenAmounts: [{ tokenAddress, amount }],
recipientAddress: recipientRailgunAddress,
}
);

// Build transaction with proof embedded
const { transaction } = await populateProvedTransaction(
chainId,
proof,
);

// Broadcast through Waku P2P (more private than direct RPC)
const txHash = await broadcastViaWaku(transaction);

return txHash;
}
```

The blockchain sees that *someone* transferred *some amount* of tokens, but not who or how much specifically.

## Private Swaps via Adapt Contracts

![Executing a private swap](/images/menu/void-dex/swap.jpg)

The key innovation for VoidDex is Railgun's "adapt contract" system. It allows private funds to interact with any DeFi protocol:

```mermaid
sequenceDiagram
participant Browser
participant Railgun
participant VoidDex Router
participant Uniswap

Browser->>Browser: Generate ZK proof
Browser->>Railgun: Submit private transaction
Note over Railgun: Verify proof
Railgun->>VoidDex Router: Call adapt(swapParams)
Note over VoidDex Router: Tokens temporarily public
VoidDex Router->>Uniswap: Execute swap
Uniswap-->>VoidDex Router: Return output tokens
VoidDex Router-->>Railgun: Return tokens
Railgun-->>Browser: Re-shield output
```

The flow works like this:

1. Your browser generates a proof spending your private UTXOs
2. The proof specifies VoidDex Router as the "adapt contract"
3. Railgun verifies the proof and calls the router with the tokens
4. The router executes the swap on Uniswap/Curve/etc.
5. Output tokens are automatically re-shielded to your private balance

```typescript:src/lib/railgun/swap.ts
import { generateCrossContractCallProof } from '@railgun-community/wallet';

async function privateSwap(
fromToken: string,
toToken: string,
amount: bigint,
routeData: RouteData
) {
// Encode the swap call for VoidDex Router
const swapCalldata = encodeSwapCall({
tokenIn: fromToken,
tokenOut: toToken,
amountIn: amount,
minAmountOut: routeData.minOutput,
dexId: routeData.dexId,
dexData: routeData.encodedPath,
});

// Generate proof that includes the cross-contract call
const proof = await generateCrossContractCallProof(
{
chainId,
tokenAddress: fromToken,
tokenAmount: amount,
crossContractCalls: [{
to: VOIDDEX_ROUTER_ADDRESS,
data: swapCalldata,
value: 0n,
}],
// Where to receive output tokens (back to your private balance)
receiverRailgunAddress: myRailgunAddress,
receiverTokenAddress: toToken,
}
);

return broadcastViaWaku(proof);
}
```

## Waku P2P Broadcasting

For maximum privacy, VoidDex uses Waku P2P to broadcast transactions instead of direct RPC calls:

```typescript:src/lib/waku/broadcast.ts
// Browser sends to Waku network
import { Waku } from 'js-waku';

async function broadcastViaWaku(transaction: string) {
const waku = await Waku.create();

// Connect to VoidDex broadcaster nodes
await waku.dial('/dns4/broadcaster.voiddex.com/tcp/8000/wss/p2p/...');

// Encrypt transaction for broadcaster
const encrypted = await encryptForBroadcaster(transaction);

// Send to the broadcaster topic
await waku.relay.send({
contentTopic: '/voiddex/1/broadcast/proto',
payload: encrypted,
});

// Wait for confirmation
return waitForConfirmation(waku);
}
```

The broadcaster nodes receive encrypted transactions, verify the proofs, pay the gas, and submit to the blockchain. Users pay two types of fees: a fixed 0.25% Railgun unshield fee (which goes to the Railgun protocol), and a dynamic broadcaster fee that varies per-transaction based on current gas costs.

## Balance Scanning

To see your private balance, the SDK scans the blockchain for encrypted notes addressed to your viewing key:

```typescript:src/lib/railgun/balance.ts
import { refreshBalances, getWalletBalances } from '@railgun-community/wallet';

async function getPrivateBalance(walletId: string, chainId: number) {
// Scan recent blocks for new encrypted notes
await refreshBalances(chainId, walletId);

// Decrypt notes that belong to you
const balances = await getWalletBalances(walletId, chainId);

return balances.map(b => ({
token: b.tokenAddress,
amount: b.balance,
utxoCount: b.utxoCount,
}));
}
```

This scanning is computationally intensive on first load (needs to process historical blocks) but incremental afterward.

## Security Considerations

**Viewing Key Protection**: The viewing key lets anyone see your balance. VoidDex encrypts it with your password before storing in localStorage:

```typescript:src/lib/railgun/security.ts
async function secureViewingKey(viewingKey: string, password: string) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = await deriveKey(password, salt);
const encrypted = await encrypt(viewingKey, key);

localStorage.setItem('voiddex_vk', JSON.stringify({
salt: Array.from(salt),
ciphertext: encrypted,
}));
}
```

**Spending Key Security**: The spending key never touches VoidDex servers. It's derived from your mnemonic and used only in-browser for proof generation.

**Proof Verification**: All proofs are verified on-chain by Railgun's contracts. Invalid proofs are rejected, protecting against malicious inputs.

![Unshielding tokens back to a public address](/images/menu/void-dex/receive-privately.jpg)

## Privacy Limitations

Railgun privacy has some limitations:

- **Shield/Unshield Visibility**: These transactions are public; observers know you're using Railgun
- **Timing Analysis**: Sophisticated observers might correlate shield/unshield timing
- **Amount Inference**: If pool liquidity is low, amounts might be inferable
- **RPC Privacy**: Your RPC provider sees your queries (mitigated by Waku)

For best privacy:
1. Shield tokens and wait before using them
2. Don't unshield exact amounts you shielded
3. Use the Waku broadcaster instead of direct RPC
4. Maintain some shielded balance at all times