Docs/Relayer/Overview

Relayer Overview

What relayers are and why they exist

Relayer Overview

Relayers are a critical infrastructure component of ZKMix that solve a fundamental problem in privacy protocols: how can a user withdraw funds to a brand-new address that has no SOL to pay for transaction fees? Without relayers, the user would need to fund the new address first, creating an on-chain link between their existing identity and the fresh address -- defeating the entire purpose of the mixer.

Why Relayers Are Necessary

Consider the standard withdrawal flow without a relayer:

  1. Alice deposits 1 SOL into the mixer from Address A.
  2. Alice wants to withdraw to a fresh Address B that has never been used.
  3. Address B has zero SOL balance -- it cannot pay the transaction fee (~0.000005 SOL base fee + priority fee).
  4. Alice must send SOL to Address B from some existing address to cover gas.
  5. That funding transaction creates an on-chain link that an observer can trace.

This is the "gas problem" that affects all privacy protocols on account-based blockchains. On Bitcoin-style UTXO chains, the transaction fee can be deducted from the transaction output itself. On Solana (and Ethereum), the fee payer must have a pre-existing balance.

Relayers solve this by acting as intermediaries who submit the withdrawal transaction on behalf of the user. The relayer pays the transaction fee and is compensated by taking a small portion of the withdrawal amount.

How Relayers Work

The relayer flow works as follows:

+--------+                    +---------+                  +----------+
|  User  |                    | Relayer |                  | On-chain |
+--------+                    +---------+                  +----------+
    |                              |                            |
    | 1. Generate ZK proof         |                            |
    |   (off-chain)                |                            |
    |                              |                            |
    | 2. POST /relay               |                            |
    |   {proof, nullifier_hash,    |                            |
    |    recipient, relayer_fee}   |                            |
    |----------------------------->|                            |
    |                              |                            |
    |                              | 3. Validate proof params   |
    |                              |                            |
    |                              | 4. Build and sign tx       |
    |                              |    (relayer pays gas)      |
    |                              |                            |
    |                              | 5. Submit tx to Solana     |
    |                              |--------------------------->|
    |                              |                            |
    |                              |                            | 6. Verify proof on-chain
    |                              |                            | 7. Transfer funds:
    |                              |                            |    - (denom - fee) -> recipient
    |                              |                            |    - fee -> relayer
    |                              |                            |
    |                              | 8. Confirm tx              |
    |                              |<---------------------------|
    |                              |                            |
    | 9. Return tx signature       |                            |
    |<-----------------------------|                            |
    |                              |                            |

The key insight is that the relayer never sees the user's secret or nullifier. The relayer only receives the proof and public inputs, which reveal nothing about which deposit is being claimed. The relayer simply acts as a transaction submission service.

What the Relayer Receives

  • The Groth16 proof (256 bytes)
  • The nullifier hash (public input, 32 bytes)
  • The Merkle root (public input, 32 bytes)
  • The recipient address (public input)
  • The relayer fee amount (public input)
  • The relayer's own address (to receive the fee)

What the Relayer Does NOT Receive

  • The user's secret
  • The user's nullifier (only the hash)
  • The leaf index of the deposit
  • The Merkle path
  • Any link between the deposit and withdrawal

Decentralized Relayer Network

ZKMix supports a decentralized network of relayers rather than relying on a single centralized service. Anyone can run a relayer by following the setup guide. This decentralization provides several benefits:

Censorship Resistance

If only one relayer existed, it could censor specific withdrawals by refusing to submit them. With multiple independent relayers, users can choose any relayer that is willing to process their transaction. Even if some relayers censor certain addresses, the user can switch to another relayer.

Competitive Fees

Multiple relayers compete on fee levels. If one relayer charges 0.5% and another charges 0.3%, users will prefer the cheaper option. This market dynamic keeps fees low and prevents any single relayer from extracting excessive rents.

Uptime and Reliability

A decentralized network provides redundancy. If one relayer goes offline, users can seamlessly switch to another. The SDK automatically selects from a registry of available relayers based on fee, latency, and reliability.

Relayer Discovery

Relayers register themselves in an on-chain registry that the SDK queries to discover available relayers:

rust
#[account]
pub struct RelayerRegistry {
    pub relayers: Vec<RelayerInfo>,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct RelayerInfo {
    pub address: Pubkey,           // Relayer's fee recipient address
    pub url: String,               // HTTP endpoint URL
    pub fee_percentage: u16,       // Fee in basis points (e.g., 50 = 0.5%)
    pub supported_pools: Vec<Pubkey>, // Pools this relayer serves
    pub stake: u64,                // SOL staked as collateral
    pub is_active: bool,
}

The SDK fetches this registry and presents available relayers to the user, or selects one automatically based on configurable preferences.

Trust Model

The relayer trust model is designed so that relayers have minimal power and cannot steal or censor funds in any permanent way.

What Relayers CAN Do

  • Refuse to relay: A relayer can choose not to submit a transaction. However, the user can simply use a different relayer.
  • Delay transactions: A relayer could accept a relay request and delay submission. The user can set a timeout and retry with another relayer.
  • Front-run (limited): A relayer could theoretically attempt to extract the withdrawal for themselves. However, the recipient address is a public input to the proof -- the proof is only valid for the specified recipient. The relayer cannot change the recipient without invalidating the proof.
  • Observe timing: The relayer knows when a withdrawal request is made, which could be used for timing analysis. Users concerned about this should add random delays before submitting to a relayer.

What Relayers CANNOT Do

  • Steal funds: The proof cryptographically binds the withdrawal to the specified recipient address. Even though the relayer submits the transaction, the funds are sent to the recipient, not the relayer. The relayer only receives the pre-agreed fee, which is also a public input to the proof.
  • Learn the deposit source: The zero-knowledge proof reveals nothing about which deposit is being withdrawn. The relayer gains no information about the depositor's identity.
  • Double-spend: The relayer cannot replay a withdrawal. The nullifier is marked as spent on-chain during the first withdrawal, and any subsequent attempt with the same nullifier will be rejected by the program.
  • Modify the withdrawal amount: The denomination is fixed by the pool. The relayer fee is a public input to the proof. The relayer cannot change the fee without invalidating the proof.
  • Permanently censor a user: Because the relayer network is decentralized and the user can always submit the transaction directly (if they have SOL for gas), no relayer can permanently prevent a withdrawal.

Security Summary

ConcernMitigated?How
Fund theftYesRecipient is bound in the proof
Identity deanonymizationYesZK proof reveals nothing about the depositor
Double-spendingYesOn-chain nullifier tracking
Fee manipulationYesFee is a public input in the proof
CensorshipYesDecentralized relayer network
Timing analysisPartialMitigated by random delays; not eliminated

Using a Relayer with the SDK

The SDK provides a straightforward interface for withdrawing through a relayer:

typescript
import { ZKMix } from "@zkmix/sdk";

const zkmix = new ZKMix({ connection, cluster: "mainnet-beta" });

// Withdraw using automatic relayer selection
const result = await zkmix.withdraw({
  deposit: savedDepositNote,
  recipient: newAddress,
  useRelayer: true, // Automatically selects the best available relayer
});

console.log(`Withdrawal tx: ${result.txSignature}`);
console.log(`Relayer fee paid: ${result.relayerFee} lamports`);

// Or specify a relayer manually
const result = await zkmix.withdraw({
  deposit: savedDepositNote,
  recipient: newAddress,
  relayerUrl: "https://relayer.example.com",
});

When useRelayer is set to true, the SDK queries the relayer registry, selects the relayer with the lowest fee that supports the relevant pool, generates the proof locally, and submits it to the relayer's HTTP API. The entire process is handled transparently.