Railgun over the mixnet
Nym hides the network layer: every Ethereum RPC call goes through the mixnet via mixFetch, so the RPC node and your ISP cannot link you to the query. Railgun hides the application layer, because shielded notes break the on-chain link between sender, receiver, and amount. This demo covers just the shield step on Sepolia: a deposit of testnet ETH into a private note. It does not do private transfers or unshielding.
What you can do here
This page is interactive. You bring up a mixnet tunnel, derive a Railgun wallet, and broadcast a real shield transaction on the Sepolia testnet, with every Ethereum RPC call routed through the mixnet. The shield lands on chain, so you can open it on Etherscan, but the IP that submitted it is the Nym exit’s, not yours.
How it works
The ENS demo swapped one provider’s transport, but Railgun constructs its own providers internally, so routing only our provider would leak the engine’s RPC to clearnet. Instead this demo installs a global ethers transport: FetchRequest.registerGetUrl routes every ethers HTTP call in the page through mixFetch, including the ones the Railgun engine makes.
import { FetchRequest } from 'ethers';
import { mixFetch } from '@nymproject/mix-fetch';
// Every ethers HTTP request in the process now goes through the mixnet.
FetchRequest.registerGetUrl(async (req) => {
const res = await mixFetch(req.url, {
method: req.method,
headers: req.headers,
body: (req.body ?? undefined) as BodyInit | undefined,
});
return {
statusCode: res.status,
statusMessage: res.statusText,
headers: Object.fromEntries(res.headers),
body: new Uint8Array(await res.arrayBuffer()),
};
});registerGetUrl is global static state on the FetchRequest class, so this only works if ethers is a single instance across your bundle. If your app and Railgun resolve to different ethers copies, the handler installs on one and the engine uses the other. Pin the exact version Railgun peer-depends on.
One caveat the real version adds: browser fetch decompresses gzip transparently and mixFetch does not, so the demo runs a DecompressionStream over each response before handing it back to ethers.
The full demo, including the Railgun engine config, is in components/demos/railgun, and the shared adapter with the gzip handling is in shared/mixfetch.ts.
Live: shield ETH into Railgun over the mixnet
Need test ETH? Top up the address shown below at a Sepolia faucet.
Loading the demo…
What to expect
- Engine init is the slow part. The first RPC calls hit Sepolia over a cold mixnet route, paying TCP-connect and TLS-handshake time, so an ethers request can time out on the first try. The demo retries, and the second attempt finds the connection pool warm.
- Shielding makes several RPC calls (gas estimate, fee data, broadcast, receipt), each a mixnet round trip. The broadcast step retries idempotently: the tx hash is fixed before broadcasting, so a dropped response can be re-sent or detected as already on chain.
- Rate limiting. If RPC calls start failing with 403/429 or connection errors, the exit IP is flagged: disconnect, tick Use random IPR, reload, and reconnect for a fresh exit.