Implementation Guidance for Wallet Developers
So you’re building a Zcash wallet and you want your users’ migration traffic to stay private. Good — this is the practical how-to. The network-privacy analysis works out why; here we get to how. The short version: run the recommended hybrid — sync over the dVPN, broadcast over the mixnet — and don’t skip the baseline hygiene, because the network can’t do that part for you. Skim the threat model once so the L1/L2/L3 names below land, then come back.
What technology stack do you use?
A short decision — and why each answer falls out the way it does.
Then you are mixnet-only. mixFetch (WASM + TypeScript) is the one way to carry traffic over the Nym network from the browser, and it suits the broadcast side — sending transactions over the mixnet. Compact-block sync over the mixnet is impractically slow and not recommended: bulk transfers do not belong on the mixnet. The dVPN is unavailable in the browser at all — the sandbox cannot open a raw UDP WireGuard socket, and the QUIC-bridge fallback is rejected because the sandbox enforces Web PKI while the bridges use self-signed certificates. You still owe the full baseline hygiene below.
Then you get the full hybrid. Use nym-smolmix to send transactions over the mixnet, and the 2-hop dVPN mode (1-hop is also configurable) to sync compact blocks quickly. On mobile, reuse the Rust crates through uniffi for Swift (iOS) and Kotlin (Android) bindings rather than reimplementing the stack. Then add the baseline hygiene — client-side sending delays and grid-quantized block requests — because the transport does not provide it.
What do these do?
mixFetch, nym-smolmix and nym-smol-dvpn all run user-space IP stacks. Your application uses standard primitives — tokio async read/write traits, or sockets bound by crates like hyper (HTTP) or tonic (gRPC) — and any traffic handed to the stack is carried over the Nym network in one of two modes:
Note the category error to avoid: in-transit mixing does not protect you against the server you are talking to (L2). No amount of mixing hides what the destination sees; only the absence of session state, plus the client-side discipline below, does. See the threat model and the analysis.
Baseline hygiene (what your app must add)
Whatever the transport, the destination (L2) still sees each request’s content and its arrival time. These duties are always the wallet’s — the same measures shown on the baseline hygiene page — but you don’t have to build them yourself: nym-swizzle-zcash implements them (use the library, or read on for how it fits the stack):
Since the linking key is the requested range (V3, content), above all its start height, the fix is to make ranges collide: quantize every requested range to a network-wide grid, so that every client resuming anywhere in the same grid cell emits exactly the same request.
The cost is re-downloaded cover blocks only: for catch-ups above a day, typically 1.5x to 2x the blocks actually needed, never more than 3x; below a day the cost is bounded in absolute terms, at most one grid cell (~0.5 MB of compact blocks).
Implemented by nym-swizzle-zcash — use the library →
Broadcast timing is a V2 (timing) leak at the destination. In-transit mixing does not remove it (the destination still observes wall-clock arrival time), so the wallet must decorrelate broadcasts itself.
Implemented by nym-swizzle-zcash — use the library →
The parameters are part of the protocol, not per-wallet tuning: the mechanism works by every wallet emitting the identical rule.
And rotate: a fixed exit or IPR is a linking key that fails P2 at the destination within a session, so rotate the exit or IPR per request — or, with an SP-integrated lightwalletd, use a fresh sender tag and its own SURBs per request. Full detail is on the baseline hygiene page.
Shaping what goes on the wire: the swizzle crates
The transport crates above carry the bytes; the swizzle layer decides what goes on the wire and when. It discharges the baseline hygiene for you, and it works the same over any of the transports on this page.
Where do I get these?
nym-smolmix
Follow the instructions at nym.com/docs/developers/smolmix — copy the dependency block from there, since crate names and versions move with releases. See the tcp.rs example for making HTTP requests.
nym-smol-dvpn
Follow the instructions in the smol-dvpn repo. There is a zcash compact-block sync example:
async fn run() -> Result<(), BoxError> {
common::init_logging();
common::init_crypto();
// 1. Provision: deposit NYM + issue zk-nym ticketbooks (reused across
// runs from the credential store), then register a random two-hop
// tunnel (entry + exit gateways).
let cli = common::parse_cli()?;
let session = common::new_session("zcash-sync").await;
let result = async {
let reg = common::register(&session, &cli).await?;
common::print_gateway("entry", ®.entry.gateway);
if let Some(exit) = reg.exit.as_ref() {
common::print_gateway("exit", &exit.gateway);
}
// 2. Bring up the userspace WireGuard tunnel.
let tunnel: Tunnel = common::build_tunnel(®, /* quic = */ false).await?;
// 3. Build a TLS gRPC channel to lightwalletd whose transport dials
// through the tunnel, and stream compact blocks over it.
let channel = Endpoint::from_static("https://zec.rocks:443")
.connect_with_connector(TlsWrap::h2(tunnel.connector()))
.await?;
info!("syncing last {BLOCKS} blocks from {LWD} through the tunnel …");
let synced = sync_last_blocks(channel, BLOCKS).await?;
info!("PASS: synced {synced} blocks through the tunnel");
// 4. Tear down (bounded — live teardown can be slow).
let _ = tokio::time::timeout(Duration::from_secs(5), tunnel.shutdown()).await;
Ok::<(), BoxError>(())
}
.await;
// Close the session's credential store cleanly (stored tickets are retained).
session.shutdown().await;
result
}nym-swizzle-zcash
The hygiene policy layer described above — see the full walkthrough, the crate README and the runnable wallet_sync example:
[dependencies]
nym-swizzle-zcash = { git = "https://github.com/nymtech/nym", branch = "develop" }
# optional: derive Serialize/Deserialize on BroadcastPlan
# nym-swizzle-zcash = { git = "...", branch = "develop", features = ["serde"] }Before you ship
Access to the Nym network in either mode requires NYM. Acquire it ahead of the migration — for example by swapping ZEC for NYM with Nym’s swap API.
Anonymity depends on crowding: your protection grows as more wallets share the same exits, IPRs, or SP-integrated service. Coordinate on shared exit-gateway sets rather than each wallet choosing its own.