GRAIL
Privacy

Private trading

How one private buy or sell moves through Railgun and Uniswap in a single transaction, and what the broadcaster does and does not see.

One transaction, one proof

A private trade is a Railgun cross-contract call through the RelayAdapt contract. For a buy, the steps inside the transaction are:

  1. 1
    Unshield WETH from the private balance to RelayAdapt. Railgun deducts 0.25%. A fee note to the broadcaster is part of the same proof.
  2. 2
    Approve SwapRouter02 to spend the WETH.
  3. 3
    Swap with exactInputSingle in the token's 1% pool, with RelayAdapt as the recipient and a minimum output set by the user's slippage.
  4. 4
    Shield the tokens, and any leftovers, back to the same private address. Railgun deducts 0.25%.

A sell is the same with the assets swapped. The proof commits to the calls, so a broadcaster cannot change the trade or its minimum output. If the swap would give less than the minimum, the whole transaction reverts and the funds stay in the private balance.

To the pool and to every terminal, the trader is RelayAdapt. Grail's indexer marks a swap as private when RelayAdapt is its recipient or the transaction's target.

In the browser

The Railgun engine runs in a Web Worker, so the page stays responsive. It uses the SDK's standard sequence: gasEstimateForUnprovenCrossContractCalls, generateCrossContractCallsProof, then populateProvedCrossContractCalls, with snarkjs Groth16 as the prover. Grail uses the classic RelayAdapt contract, with the SDK's EIP-7702 path turned off.

Measured on SepoliaTime
Building the proof8 to 13 seconds
Click to confirmed trade25 to 36 seconds in early runs, 79 seconds through the full site
Tokens spendable after a buyAbout 3 to 5 minutes
WETH spendable after a sellAbout 12 minutes

Mainnet numbers will be added here once private trades have run against the live launchpad.

Why trades do not wait an hour

A trade's output is a new shield, so it might be expected to go through the standby again. As we read the POI node's code (handleRelayAdaptUnknownShield in private-proof-of-innocence), it does not when RelayAdapt shields in the same transaction as an unshield and every unshield in it has a valid Proof of Innocence. If the proof is still missing an hour later, the shield falls back to the normal standby.

So the browser submits that proof automatically after every trade. It keeps doing so while the tab is open, and it picks up where it stopped when the wallet next opens. That is why outputs take minutes instead of an hour, and why the app asks people to keep the tab open just after a trade.

The broadcaster

If the user's wallet sent the transaction, its address would be the sender and the trade would be linked to it. The broadcaster sends it instead, from its own gas wallet, so the user signs nothing with a public wallet.

  1. 1
    Quote. GET /fees returns its 0zk address, the fee token (WETH), a fee rate of gas cost plus 10%, and a gas price with a 25% buffer. A quote is valid for two minutes.
  2. 2
    Prove. The browser adds a WETH fee note for the broadcaster to the proof, and binds the quoted gas price as the minimum.
  3. 3
    Send. POST /broadcast takes the raw transaction. The broadcaster accepts only transactions to RelayAdapt or the Railgun proxy, decrypts its fee note from the calldata with its viewing key, simulates the transaction, checks that the fee covers the gas, and sends it.
The broadcaster seesIt does not see
The request's IP address, unless the user hides itWhose private balance paid
The public content of the transactionThe user's keys, phrase or other notes
Its own fee noteAny link to the user's public wallet

The broadcaster keeps no user accounts. Requests are limited to 30 per minute per IP. It collects fees in its own private balance and periodically unshields them to pay for its own gas. It reports itself unavailable when its gas wallet could not pay for a 6M-gas transaction at the current price, so during a sharp gas spike private transactions can pause until gas falls. Funds stay in the private balance in the meantime.

Today Grail's broadcaster is the only sender for Grail trades. Using Railgun's public broadcaster network as well would make a larger crowd, and that remains an open option.

Withdrawals

A withdrawal is an unshield through the same broadcaster, as ETH or as the token. Railgun deducts 0.25%. The network fee comes from private WETH. With Max, the browser estimates the gas for the whole balance when the withdrawal is sent, works out the fee from the fresh quote and withdraws the rest, so nothing is left behind and a change in gas price between click and send does not break it.

Grail is an independent project built on Railgun's public contracts and SDK. Railgun contributors do not run, review or endorse Grail, and they did not write this documentation. Their own documentation is at docs.railgun.org.