hi zcashers,
i’ve published lwd-mixnet-proxy, two small rust programs that carry a light wallet’s grpc connection over the nym mixnet. the wallet doesn’t change, the server doesn’t change, and neither one knows the mixnet is there.
wallet --tcp--> [lwd-mixnet-client] --mixnet--> [lwd-mixnet-server] --tcp--> light-client server
each half just moves bytes. neither parses grpc. a mixnet stream is an ordinary read/write stream, so the connection passes through as is. the wallet points at a local port. the serving half reads its upstream from config, so it works in front of any light-client server, not only lightwalletd-rs.
MIT, and setup is in the readme.
an address you can point at
i’m running the serving half on testnet:
HDfv77FgcWJTf4NFrxCvHRzZx4Zppc2pLiFUx6g2Lt3v.6VqPTWwZdmdFKEAyGW93hsPULp5XwEiVZh3GwHoXoqoK@3xLD3rpA5XWqdzfbTNs2E38tGp9K11QkU9RfQwxJGyC4
you still run the client half yourself, on your own machine. that’s the part that matters: your traffic enters the mixnet before it leaves your box, so the address above is the only piece i can host for you. testnet only for now. it’s an anonymous endpoint with nothing but a stream cap in front of it, so treat it as an experiment i might have to take down, not something to depend on.
one thing to know before you use it: it forwards to my public testnet lightwalletd-rs, the same one you can already reach directly. if you sync against that and submit through this, i see both sides and the mixnet bought you nothing. see below.
where it came from
while building lightwalletd-rs i kept running into the same thing. the calls that use the most bandwidth are the ones that leak the least. downloading blocks is the heavy part and tells the server almost nothing, since the wallet fetches everything and decrypts locally. the cheap calls are the ones that hurt. sending a transaction ties it to your ip. asking about a transparent address hands the address over.
that’s a good match for a mixnet, which delays and shuffles packets, so correlating by timing gets much harder. this has come up here before, most visibly in the 2024 nym grant thread.
so i put a mixnet listener inside lightwalletd-rs, measured it for three days, and then took it back out. the sdk pulls in 756 crates, more than the server itself, and there’s no way to trim it, so everyone building the server would carry it for the few operators who want it. reasoning in adr 0029. a separate process made more sense. this repo is that process.
what i found while measuring
this is the part i’d most like other people to check.
a stream can open, get accepted on the far side, and quietly lose its first payload. the sender’s write returns Ok. the receiver’s accept() fires and its read never returns. no error on either side, no timeout, both ends hang forever.
i took grpc and my own code out of the picture and reproduced it in about 200 lines using nothing but the sdk. the rate moved between 2% and 51% across sessions with the same settings, on two sdk versions. it isn’t steady either: in one 400-trial run, failures nearly tripled from the first half to the second. more reply blocks cut the failure rate (51% with one, 26% with four hundred) but cost 5.3x the latency. nothing i tried got to zero.
grpc libraries handle errors all day. they don’t handle silence. anything built here has to fix that first, and i suspect it’s why earlier attempts stopped at the demo stage.
the reproduction lives in the lightwalletd-rs repo, in contrib/nym, with the raw output of every run next to it. if you know the sdk better than i do, it’s yours.
what the proxy does about it
three things:
- probe first. before the wallet sends anything, the client half runs its own small round trip on the fresh stream and drops the stream if nothing comes back in time. it’s the same round trip the transport loses, so the probe gets eaten instead of the wallet’s first request.
- open three, keep the first that answers. you only learn about a silent failure when the deadline expires, so retrying one at a time pays that deadline every single time. measured side by side in the same window: p99 of 31.3 s opening one at a time, 6.3 s opening three.
- a watchdog once bytes are moving. if the other end goes quiet, close the connection. the wallet gets a closed socket, and its grpc library already knows what to do with that.
in the run that settled the design, one afternoon on one network path, streams were failing 34.67% of the time. what a wallet would have seen: 0 failures in 300 connections, 1.2 s median to connect, 6.3 s at p99. every run is written up in docs/measurements/, including the two i had to throw out and why.
what it’s good for
good: sending transactions, transparent address queries, single transaction lookups. these leak a lot, cost few bytes, and latency barely matters when a block takes 75 seconds anyway.
bad: syncing. at this throughput, a full sync from an old birthday moves tens of gigabytes at seconds per round trip. that’s days. it’s not a drop-in replacement for a normal connection, and i’ve tried to keep the readme from suggesting it is.
one rule matters more than the software: don’t sync and submit through the same server. if an operator watches an address sync and then receives an anonymous transaction a minute later, timing joins the two. with few users there’s no crowd to hide in. sync through one instance, submit through another. it’s free.
before you leave it running
a connected mixnet client sends cover traffic the whole time it’s up, around 2 mbps. that’s what the traffic analysis resistance is made of, and it’s also a real bill on a metered connection.
both halves export two failure rates taken from the same connection attempts: what the transport does, and what a wallet sees. read both. the first swings by an order of magnitude between afternoons, so on its own it mostly tells you which afternoon it is.
where i’d like help
- has anyone kept a nym client up for days? mine logged
Not enough bandwidthsix minutes into a run, and no test of mine lasted longer than two hours. that’s thin evidence for something meant to run for weeks. the instance above is my attempt at an answer, and i’ll post what it does over the next few weeks, but one machine on one network path is still one data point. - wallet and sdk devs: can a wallet point at two endpoints? the proxy moves bytes, so it can’t pick calls by method. sending only the sensitive ones over the mixnet means sync goes to one endpoint and submission to another. what does that cost on your side?
- anyone who knows nym well: does the silent stream failure ring a bell? a confirmation or a correction would both help.
this is early software. v0.1.0 is tagged, it hasn’t been audited, and the testnet instance above is the only one i know of running anywhere.
if you try it, tell me what breaks. questions welcome.
