docs(internal): Tor transport redesign plan + payment-feel report + forum post
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
# Goblin Privacy Transport Redesign
|
||||
|
||||
**Status:** Decided — implementation plan · **Date:** 2026-07-04 · **Author:** Architecture
|
||||
**Scope:** Replace the Nym-mixnet transport that carries Goblin's Nostr traffic with embedded Tor, GRIM-style, and rebuild the one privacy property Tor lacks (send/receive timing unlinkability) on our own relay.
|
||||
**Hard constraints (owner):** No clearnet money path · No NYM tokens · No rented/metered privacy bandwidth · In-process only (no sidecar daemon, no system VPN/TUN — must work on Android and iOS).
|
||||
|
||||
---
|
||||
|
||||
## The decision (read this first)
|
||||
|
||||
**Goblin is dropping the Nym mixnet and returning to Tor, embedded in-process, exactly the way our sister wallet GRIM already ships it.** The wallet keeps everything else it does today — payments are still gift-wrapped Nostr messages that our own relay stores and forwards, so a recipient can be paid while offline. The only thing that changes is the private pipe *underneath* the relay connection: instead of tunnelling to the relay across the Nym mixnet, the wallet dials the relay's **pinned `.onion` address over embedded arti** (Tor written in Rust).
|
||||
|
||||
We do not invent our own Tor engine. We **copy GRIM's** — a proven ~1,300-line engine in four files (`grim/src/tor/`) that already runs in production on desktop and Android. And because a mixnet's real gift was hiding the *timing* link between a sender uploading a payment and a recipient downloading it — something Tor alone does not do — **we rebuild that timing privacy on our own relay**: it holds each incoming gift-wrap and releases it to the recipient after a short randomized (Poisson) delay. Same fuzzing a mixnet performs, done by the one server we fully control, unmetered, and unable to collapse the way rented mixnet bandwidth did.
|
||||
|
||||
This trades exactly one thing: a real distributed mixnet resists a *global passive adversary* who can watch the whole internet at once. A single relay plus Tor does not. That adversary is out of scope for a low-value payments wallet, and Tor never claimed to stop it either. In return we get a transport that is mature, free, unmetered, has a huge anonymity set, is lighter on the battery, and — measured where the user actually waits — **faster**.
|
||||
|
||||
---
|
||||
|
||||
## Why we are leaving the mixnet: confirmed root cause
|
||||
|
||||
This is not a transient outage we can wait out. It is **removal by design**, traced to Nym's own source:
|
||||
|
||||
- A credential-less Nym client is granted a hard-coded `FREE_TESTNET_BANDWIDTH_VALUE = 64 GiB`, and that grant **expires at the next UTC-midnight rollover** (`ecash_today()`).
|
||||
- An entry gateway only honours that free grant while `enforce_zk_nyms = false`.
|
||||
- Public entry gateways are flipping `enforce_zk_nyms = true` **network-wide** as Nym rolls out paid, NYM-token-gated "zk-nym ticketbook" bandwidth (≈ 225 NYM for ~25 GB).
|
||||
|
||||
So the free tier the wallet floated on is **testnet scaffolding Nym is actively deleting**, and its failure recurs *on a schedule*. It went dark on us more than once. The only supported replacement — paid ticketbooks — **requires holding NYM tokens** (an owner red line) and offers no sats/Lightning purchase path. A payments wallet cannot stand on a foundation that disappears at midnight and can only be rented back with a specific speculative token. The mixnet, as we were able to consume it, is a dead end.
|
||||
|
||||
Tor has none of these properties. It is free, unmetered, has no token, no bonding, no grant to expire, and the largest anonymity set of any deployed privacy network. It runs in-process on a phone, and GRIM has already proven the whole embedded path.
|
||||
|
||||
---
|
||||
|
||||
## Threat model: what we are actually protecting
|
||||
|
||||
We reason about leakage at six levels, sender → relay → receiver:
|
||||
|
||||
| Level | Leak | Status under the Tor plan |
|
||||
|---|---|---|
|
||||
| **L1 network** | sender/receiver IP as seen by the relay and on-path observers | **Covered by Tor** — an onion connection has no exit node, so the relay and every hop see a Tor address, never the phone's IP. |
|
||||
| **L2 timing** | correlation of send-time with receive-time (the mixnet's core property) | **Rebuilt on our relay** — Poisson release delay (see below), plus NIP-59 timestamp backdating already in the wallet. |
|
||||
| **L3 volume** | message-size correlation | NIP-44 v2 padding blunts; gift-wraps are already near-uniform at payments volume. |
|
||||
| **L4 relay-visible** | recipient p-tags, kinds, subscription filters, connection cadence | NIP-59 gift-wrap blunts; unchanged by the transport swap. |
|
||||
| **L5 content** | message plaintext | **Already solved** — NIP-44 v2 inside NIP-59 gift-wrap (kind 1059). |
|
||||
| **L6 long-term intersection** | repeated-pattern deanonymization | NIP-59 blunts; unchanged. |
|
||||
|
||||
Our **realistic adversary** is the relay operator, ISPs, near-endpoint observers, and chain-analysts — **not a global passive adversary (GPA)**. GPA-resistance is explicitly out of scope for a low-value payments wallet; Tor itself states plainly that it "does not defend against" an attacker who can watch **both ends** of a circuit. We inherit that boundary knowingly and, for the adversary that actually matters, we cover every level.
|
||||
|
||||
---
|
||||
|
||||
## The architecture
|
||||
|
||||
The whole change is contained because Goblin already isolates its transport behind one trait. Payments still ride the relay-mediated, store-and-forward model unchanged. Four things move:
|
||||
|
||||
**1. The relay hosts an onion service.** Plain, mature **system Tor** runs on the relay's own server (`torrc`: `HiddenServiceDir` + `HiddenServicePort 443 127.0.0.1:443`) and forwards the onion straight to the relay's existing secure-websocket port. This is battle-tested C-Tor doing the hosting half — the strongest, most-audited part of the Tor codebase — and it replaces the custom `floonet-mixexit` byte-pipe binary entirely.
|
||||
|
||||
**2. The wallet dials that onion over embedded arti.** Goblin only needs the **dialing half** of Tor — connect *out* to the relay's onion. It never hosts a service (that is what makes it simpler than GRIM, which hosts an onion to *receive*). `arti-client`'s `TorClient::connect()` returns a `DataStream` that implements `AsyncRead + AsyncWrite` — a drop-in for the byte source the wallet feeds to its websocket layer today.
|
||||
|
||||
**3. We copy GRIM's Tor engine rather than write our own.** GRIM's `src/tor/` is ~1,300 lines across four files (`config.rs`, `mod.rs`, `tor.rs`, `types.rs`) and is already in production. Two technical choices we inherit verbatim because GRIM already paid for them:
|
||||
- **arti 0.43 across the whole arti family** (`arti-client`, `tor-rtcompat`, `tor-config`, `tor-hsservice`, `tor-hsrproxy`, `tor-keymgr`, `tor-llcrypto`, `tor-hscrypto` — all `0.43.0`).
|
||||
- **The native-tls Tor runtime** (`TokioNativeTlsRuntime`), **not rustls**. This deliberately sidesteps the rustls crypto-provider (ring) conflict we fought all through the Nym era. We take GRIM's known-good TLS path and never re-open that wound.
|
||||
|
||||
**4. The two code seams that carry private traffic switch to Tor; the money node does not.** Exactly two paths leave the device with anything sensitive on them: the **relay websocket**, and the **one HTTP helper** that carries all the small lookups (names at `goblin.st`, relay hints, the pinned-pool refresh, price, avatars). Both re-route through arti. The **Grin blockchain node stays on the clear internet exactly as today, unchanged** — it never sees who is paying whom, only opaque transaction data, and Tor-wrapping it would buy nothing but latency.
|
||||
|
||||
**The pin lives where the old Nym exit pin lived.** Our relay-pool gist already carries a per-relay field for exactly this, and its parser is deliberately tolerant of unknown fields (no `deny_unknown_fields`, `version` stays `1`). So we add an `onion` address next to each relay entry and **older builds simply ignore it** — no flag day, no schema break. The plumbing that already resolves the co-located Nym `exit` for a relay URL generalizes one-for-one to resolve its `onion`.
|
||||
|
||||
---
|
||||
|
||||
## Recovering timing privacy: Poisson on the relay
|
||||
|
||||
Here is the one property we cannot get from Tor for free, and how we rebuild it.
|
||||
|
||||
A mixnet's genuine value was **timing unlinkability**: even someone who can see traffic near both ends cannot match "this sender uploaded at 10:01:03" to "that recipient downloaded at 10:01:04," because the mixnet deliberately shuffles and delays messages so the two events do not line up. Tor is low-latency by design and does *not* do this — a payment flows through as fast as the circuit allows.
|
||||
|
||||
We rebuild it in the one place we fully own: **our relay holds each incoming gift-wrap and releases it to the recipient after a randomized, exponentially-distributed (Poisson) delay.** This is the same fuzzing a mixnet performs, collapsed onto a single hop we operate ourselves — unmetered, always on, and immune to the rented-bandwidth failure that just killed Nym for us.
|
||||
|
||||
**The elegant part: this costs the user nothing they can see.** The sender's on-screen "Sent" clears the moment the relay **confirms it holds the message** — not when the recipient receives it. Delivery to the recipient is already asynchronous and invisible (they might be offline for hours). The Poisson delay lands *entirely inside that already-invisible gap.* We buy back the mixnet's timing privacy at **zero visible latency cost**. And it stacks on top of a fuzz the wallet already applies: NIP-59 backdates every gift-wrap's timestamp by a random offset of up to two days, so even the timestamps on the wire are decorrelated from real send time.
|
||||
|
||||
**The honest trade-off, stated plainly:** a real distributed mixnet spreads its mixing across many independent nodes, which is what lets it resist a global passive adversary watching the entire network at once. Our single relay plus Tor does not — a party who could simultaneously observe our relay *and* the recipient's Tor guard *and* correlate the Poisson-delayed release could, in principle, still link the two ends. That is the global-adversary threat, it is out of scope for a Grin payments wallet, and Tor never defended against it either. For the adversary who actually exists — the relay operator, an ISP, a network snoop, a chain-analyst — relay-side Poisson closes the timing gap. We give up one theoretical guarantee and gain reliability, speed, and battery.
|
||||
|
||||
---
|
||||
|
||||
## What we must preserve (load-bearing, transport-agnostic)
|
||||
|
||||
Three things in today's wallet are not Nym-specific and must survive the swap intact.
|
||||
|
||||
**1. The "connection is genuinely live and carrying traffic" readiness signal.** The UI carefully refuses to show "Connected" until a relay is *actually subscribed and carrying traffic on the current tunnel*, not merely until the pipe opened (`transport_ready()` / `warm_up()` / relay-gated generation counter in `src/nym/nymproc.rs`). This is what prevents a reassuring-but-false "Connected" over a tunnel that cannot yet deliver. **Keep the mechanism; re-point it at Tor** — readiness becomes "arti has bootstrapped, the onion circuit is up, and a required relay is subscribed on it."
|
||||
|
||||
**2. The read-back confirm on sending (keep verbatim).** The wallet does **not** report a payment "Sent" on a transport-write success. It performs a genuine read-back: after publishing, it polls the target relays for the event id until one confirms it actually **holds the gift-wrap**, or a timeout is hit — in which case it surfaces failure so the caller retries instead of silently dropping money (`src/nostr/client.rs`, the SILENT-LOSS GUARD loop). This is pure money-safety and is completely transport-agnostic. **Keep the logic exactly as written.** Only the *comments* that say "over the scoped Nym exit / mixnet transport" need their wording generalized to "the transport"; the guard itself does not change a line, and it is in fact the very mechanism that makes relay-side Poisson safe (the sender is told "Sent" precisely when the relay confirms it holds the message).
|
||||
|
||||
**3. User-facing copy that says "Nym" / "Mixnet."** The strings need rewording to Tor across all six locales (`locales/en.yml` plus `tr`, `fr`, `de`, `ru`, `zh-CN`): `connected_nym` ("Connected over Nym" → "Connected over Tor"), `connecting_nym`, `nym_ready`, `mixnet_routing`, `network_value` ("MW + Nym mixnet + nostr"), `privacy_value` ("Mimblewimble + Nym"), `over_mixnet`, `rates_note`, `send_like_message_body`, `row_delivery_val`, `row_privacy_val`, and the onboarding `intro` that describes "a five-hop network." These become plain, honest Tor language — see the forum post at the end for the tone.
|
||||
|
||||
---
|
||||
|
||||
## Mobile reality (told straight)
|
||||
|
||||
**Android is solved.** GRIM already ships embedded Tor on Android and the recipe is copyable. It comes down to two things: **build Tor into the app's native library** (arti compiles into the same `.so` the rest of the Rust already lives in — no separate process, no sidecar), and **set a few environment variables before the Rust runtime starts.** The critical one is `ARTI_FS_DISABLE_PERMISSION_CHECKS=true` (GRIM sets it in `MainActivity.java` before loading native code): arti's `fs-mistrust` layer normally refuses to start if its state directory has "too-open" Unix permissions, and Android's app-sandbox filesystem always trips that check — so without this flag Tor simply never boots on a phone. This is a known, small, copy-paste fix, not a research problem.
|
||||
|
||||
**iOS is green-field and needs its own spike.** The arti library should compile for iOS — nothing about it is Android-specific — but nobody on our side has shipped it there yet, so we treat it as unproven until we have. Two things to flag going in: (a) we run **without pluggable-transport bridges** on iOS, because the platform won't let an app spawn the helper processes those need — plain Tor only, which is fine for our reach-the-relay use case; and (b) the same `fs-mistrust` and state-directory questions need answering against iOS's sandbox. **Action: an iOS Tor spike is a named prerequisite before we claim iOS support.** Android does not wait on it.
|
||||
|
||||
---
|
||||
|
||||
## Integration surface (code anchors)
|
||||
|
||||
The transport is isolated behind one trait, so this is a contained swap, not a rewrite.
|
||||
|
||||
| Seam | Location today | Change |
|
||||
|---|---|---|
|
||||
| Transport trait | `nostr-relay-pool` `WebSocketTransport::connect(url, mode, timeout) -> (WebSocketSink, WebSocketStream)`; impl `NymWebSocketTransport` at **`src/nym/transport.rs:47`** | New arti-backed impl in a new `src/tor/`, engine copied from GRIM. |
|
||||
| Byte source | Nym `open_stream` at **`src/nym/streamexit.rs`** (returns `AsyncRead + AsyncWrite`) | Swap for arti `TorClient::connect(<onion>:443)` → `DataStream`. Keep the rest of the path. |
|
||||
| TLS + ws wrap | `client_async_tls(url, stream)` at **`src/nym/transport.rs:126` / `:158`** (SNI = relay host) | **Unchanged.** |
|
||||
| Injection point | `Client::builder().websocket_transport(...)` in **`src/nostr/client.rs`** | Inject `TorWebSocketTransport`. |
|
||||
| HTTP chokepoint | `http_request_bytes()` **`src/nym/mod.rs:71`** / `http_request()` **`src/nym/mod.rs:111`** — carries NIP-05 / name authority (`src/nostr/nip05.rs`), NIP-11 hints, pool gist, price, avatars | Re-route through arti: Tor→onion for the `goblin.st` name authority (pin a second onion); Tor→clearnet for the gist + price + avatars. **Grin node stays clearnet as today.** |
|
||||
| Pin plumbing (forward-safe) | `PoolRelay` at **`src/nostr/pool.rs:86`**; `exit: Option<String>` at **`:105`**; serde tolerant (no `deny_unknown_fields`); `exit_for()` **`:157`**, `exit_for_host()` **`:170`**, `has_exit()` **`:187`** | Add an `onion` field beside `exit` in the gist **without breaking old builds**; keep `version:1`. `exit_for*`/`has_exit` generalize to `onion_for*`/`has_onion`. |
|
||||
| Readiness lifecycle (**preserve**) | `warm_up()` **`src/nym/nymproc.rs:107`**, `transport_ready()` relay-gated readiness **`:127`–`:135`** | Re-point at Tor: bootstrapped + onion up + relay subscribed. Keep the gating semantics. |
|
||||
| Confirm-before-sent (**preserve verbatim**) | SILENT-LOSS GUARD read-back loop, doc at **`src/nostr/client.rs:63`** | Unchanged logic. Only reword the Nym-specific comments to "the transport." |
|
||||
| Footprint retired | `src/nym/*` = **2,842 lines** (`nymproc` 1073, `dns` 662, `streamexit` 465, `mod` 411, `transport` 231) + `nym-sdk`/`smolmix` path-deps + `floonet-mixexit` | Deleted once Tor is proven. Net code likely **shrinks** — most of `nymproc` exists only to fight Nym flakiness (gateway race / probe / condemn). |
|
||||
|
||||
The live pool already ships the floonet relay inline (`pool.rs:69` pins `wss://relay.floonet.dev` with its Nym `exit`). We add its `onion` alongside, in the same gist, and roll forward.
|
||||
|
||||
---
|
||||
|
||||
## What the wallet will feel like on Tor
|
||||
|
||||
An honest, moment-by-moment read on whether each everyday interaction gets faster, stays the same, or gets slower than it was on the mixnet. Short version: on pure speed, Tor is a lateral-to-favorable move everywhere the user actually waits.
|
||||
|
||||
**Receiving / listening — same-to-better, and never a spinner.** The wallet does not poll for incoming payments. It holds a **live, open subscription** to the relay, and payments are *pushed* down that already-open connection and processed with no human watching. That is invisible on either transport. The difference is that on Tor each pushed message arrives quicker (no per-hop mixing delay), and the deliberate Poisson privacy-delay hides inside the already-invisible delivery gap. Nothing the user sees changes; what is behind it is faster. **Verdict: same-to-better.**
|
||||
|
||||
**Sending — faster where it matters most.** The only part of a send the user actually watches is the marquee moment: find the recipient's relay, publish the wrapped payment, and wait for the relay to confirm it holds it. On the mixnet that moment paid three separate taxes — a per-message mixing delay, a fixed ~3-second stream-settle, and multi-fragment delivery lag. On Tor all three are gone. So this spinner should feel **shorter**, not longer. Everything after it — building the reply, finalizing, posting the finished transaction to the Grin node — is background work, and the node post is plain clear-internet exactly as before. **Verdict: faster where it matters most.**
|
||||
|
||||
**Invoices / requests — same-to-faster, dominated by the human.** There is no separate "invoice" message type on the wire; a request is the same gift-wrapped DM as a payment, just with a human approval gate in front of it. Transport latency matters even less here because a person is tapping "approve." **Verdict: same-to-faster, dominated by the human step.**
|
||||
|
||||
**Name and identity lookups — fast, unchanged in feel.** Resolving `name@goblin.st` is a single HTTP GET behind a short "Searching…" spinner — the one genuinely blocking lookup in the app. Everything else (reverse name lookups, avatars, relay hints, pool refresh, price) is cached, quiet background work the user never waits on. A warm keep-alive connection pool over Tor makes repeat lookups cheap. **Verdict: fast and unchanged in feel — provided we keep the connection warm.**
|
||||
|
||||
**Cold start — simpler, comparable, warm-able.** The mixnet needed *two* separate mixnet clients racing each other for bandwidth grants, plus a whole sequencer, just to get connected "in seconds instead of a minute." Tor is **one** bootstrap — dramatically simpler. It overlaps with app launch, so it is mostly invisible; its only visible edge is the very first send of a session if Tor has not finished bootstrapping yet, which would surface as a slightly longer first "Sending…." Warming the circuit at launch hides even that. **Verdict: simpler, comparable, warm-able.**
|
||||
|
||||
**Battery / always-listening — improves.** A persistent Tor circuit is lighter than a live mixnet client: there is no continuous cover-traffic machinery to run and no per-hop delay work to perform. The wallet's existing "the connection died, rebuild it" logic and its background/foreground handling map cleanly onto Tor circuits. **Verdict: battery should improve.**
|
||||
|
||||
**Net verdict.** On pure speed and feel, Tor is lateral-to-favorable everywhere the user actually waits — and better in the two places they wait most (sending and cold start). What we trade is not speed. It is the mixnet's global-adversary timing guarantee, and we largely rebuild even that, for the realistic adversary, with relay-side Poisson.
|
||||
|
||||
---
|
||||
|
||||
## Phased implementation plan
|
||||
|
||||
### Phase 0 — iOS + mobile spike (de-risk before commit)
|
||||
**Goal:** prove the copied engine drops into the seam on the platforms we are less sure of.
|
||||
**Tasks:** (a) stand up `arti-client` → connect a test `.onion:443` → `client_async_tls` → Nostr ws handshake, proving `DataStream` satisfies the transport seam on desktop; (b) confirm the Android recipe holds in a Goblin dev build (native-lib link + `ARTI_FS_DISABLE_PERMISSION_CHECKS`); (c) **the iOS spike** — does arti compile and bootstrap inside the iOS sandbox, plain-Tor (no PT bridges), and where does `fs-mistrust` land.
|
||||
**Exit criteria:** a Nostr ws session to the relay over an onion, in acceptable time, on a real Android device; a clear yes/no + punch-list for iOS.
|
||||
**Risk:** low on desktop/Android (GRIM shipped it); iOS unknown, which is why it is Phase 0.
|
||||
|
||||
### Phase 1 — Onion service on the relay
|
||||
**Goal:** the relay is reachable over a stable `.onion`.
|
||||
**Tasks:** system-Tor `torrc` onion service on the floonet box fronting the relay's ws port; publish and pin the `onion` in the gist beside the existing entry; add a second onion for the `goblin.st` name authority. Enable Vanguards on the service side.
|
||||
**Validation:** `torify` a plain Nostr client to the onion and complete a handshake.
|
||||
**Risk:** low — mature C-Tor doing the hosting half.
|
||||
|
||||
### Phase 2 — Wallet transport swap
|
||||
**Goal:** all Nostr + HTTP traffic rides Tor; Nym is off the live path.
|
||||
**Tasks:** copy GRIM's `src/tor/` into Goblin; implement `WebSocketTransport` against arti `DataStream`; re-route `http_request*` (Tor→onion for the authority, Tor→clearnet for gist/price/avatars); re-point `warm_up()`/`transport_ready()` at Tor readiness; keep the confirm-before-sent guard; **no clearnet fallback for the relay path — fail loudly**.
|
||||
**Validation:** a real payment between two devices completes over the onion, with the read-back confirm firing.
|
||||
**Risk:** low-medium — mitigated by copying a proven engine and native-tls.
|
||||
|
||||
### Phase 3 — Relay-side Poisson (timing privacy)
|
||||
**Goal:** send-time and receive-time are decorrelated for the realistic adversary.
|
||||
**Tasks:** the relay holds each inbound gift-wrap and releases it to the recipient after an exponentially-distributed delay (mean tuned so it stays inside the invisible delivery gap); confirm the sender's "Sent" still fires on *hold*, not on delivery, so the delay is free to the user.
|
||||
**Validation:** relay logs + on-path capture show release timing decorrelated from arrival timing; sender UX latency unchanged.
|
||||
**Risk:** low — it is a delay queue on a server we own.
|
||||
|
||||
### Phase 4 — Copy reword + iOS follow-through
|
||||
**Goal:** the product says "Tor," honestly, everywhere; iOS is either shipped or explicitly deferred.
|
||||
**Tasks:** reword all Nym/Mixnet strings across the six locales; publish the forum post; land iOS per the Phase-0 punch-list or file it as a tracked follow-up.
|
||||
**Validation:** locale drift test green; no "Nym"/"Mixnet" left in user-facing copy.
|
||||
**Risk:** low.
|
||||
|
||||
### Phase 5 — Retire Nym
|
||||
**Goal:** shed the dead dependency and shrink the binary.
|
||||
**Tasks:** delete `src/nym/*`, the `nym-sdk`/`smolmix` path-deps, and `floonet-mixexit`; shrink the Android native lib; simplify the all-in-one floonet package to **relay + torrc-onion + name-authority-onion**.
|
||||
**Validation:** clean build, smaller Android lib, green E2E.
|
||||
**Risk:** low — deletion after the replacement is proven.
|
||||
|
||||
---
|
||||
|
||||
## Migration & rollout
|
||||
|
||||
The pin format is forward-safe (serde-tolerant, `version:1` preserved), so rollout is graceful:
|
||||
|
||||
1. Ship the **Tor-capable build** via the in-app updater (GitHub releases).
|
||||
2. Run the **system-Tor onion service alongside** the existing relay through the transition.
|
||||
3. As users update, they pick up the `onion` pin from the gist and connect over Tor.
|
||||
4. **Old Nym pins go dark gracefully** — Nym is failing on its own schedule anyway, so there is no working state to "cut over" from and no regression to manage.
|
||||
|
||||
No coordinated flag day. The network is already in the failure state the new build fixes.
|
||||
|
||||
---
|
||||
|
||||
## Risks & mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| arti **onion-client** maturity ("not yet as secure as C-Tor") | Mature **system-Tor** hosts the service side; Vanguards on the service; onion-client is actively hardening and we are only the dialer. |
|
||||
| **iOS** unproven | Named Phase-0 spike; Android does not wait on it; plain-Tor (no PT bridges) is the accepted iOS mode. |
|
||||
| **Android build** friction | Direct copy of GRIM's working recipe — native-lib link + `ARTI_FS_DISABLE_PERMISSION_CHECKS`; native-tls dodges the rustls/ring provider conflict. |
|
||||
| **Bootstrap latency** on first send | Warm the circuit at launch; keep the "Connecting…" readiness UX; the confirm-before-sent guard makes a slow first send safe, never lost. |
|
||||
| **Poisson delay tuned too long** (feels laggy to recipient) | Tune mean to sit inside the already-async delivery gap; sender "Sent" fires on relay-hold regardless, so it is never on the user's critical path. |
|
||||
| **Single-relay availability** | The relay was always a required component; onion descriptor HA via a 24/7 host (and onionbalance if needed). |
|
||||
|
||||
---
|
||||
|
||||
## Far-future option (not part of this plan)
|
||||
|
||||
If we ever want to close the one remaining gap — resistance to a global passive adversary — a self-run, tokenless, unbonded mixnet layer could in principle sit *on top of* this Tor foundation (wallet → Tor → our own entry → mixnodes → relay). We are **not building it.** It leans on exactly the free Nym grant that is being deleted, it adds latency, and it defends against a threat a Grin payments wallet does not face. It is noted here only so the door is documented, not opened. **Tor is the plan.**
|
||||
|
||||
---
|
||||
|
||||
## Bottom line
|
||||
|
||||
Return to Tor, embedded in-process, copied straight from GRIM's proven engine (arti 0.43, native-tls), dialing the relay's pinned onion — and rebuild the mixnet's one real advantage, timing unlinkability, as a Poisson release delay on the relay we already own and fully control. Keep the relay-mediated store-and-forward model, the genuine-readiness signal, and the read-back confirm-before-sent guard exactly as they are. Android is solved today; iOS gets a named spike. We give up one theoretical guarantee against a global adversary that is out of scope, and in exchange the wallet becomes more reliable, lighter on the battery, faster where users wait — and free of any token or rented bandwidth that can go dark at midnight.
|
||||
|
||||
---
|
||||
|
||||
## Forum post: why we're moving from the mixnet to Tor
|
||||
|
||||
*(Ready to publish, owner's voice.)*
|
||||
|
||||
**We're switching Goblin's private plumbing from the Nym mixnet back to Tor. Here's the honest why.**
|
||||
|
||||
When I built Goblin's privacy layer on the Nym mixnet, I meant it. A mixnet is the strongest metadata-privacy tool we have — it doesn't just hide your IP, it deliberately shuffles the timing of messages so nobody can even tell that *you sending* and *someone receiving* are the same payment. For a money wallet, that's exactly the property you want. I wasn't hedging when I picked it.
|
||||
|
||||
But a payments wallet has to stand on ground that doesn't move, and the ground moved. The free bandwidth tier that Goblin relied on turns out to be temporary testnet scaffolding that Nym is actively removing — it's written right into their code to expire, and their public gateways are switching over to a paid model. And the paid model means holding a specific crypto token to buy bandwidth. That's not a foundation I'm willing to build your money on. It went dark on us more than once, on a schedule, and "your payments work unless it's the wrong time of day" is not something I'll ship.
|
||||
|
||||
So we're going back to **Tor** — the most battle-tested privacy network on the internet, with no token, no rented bandwidth, and nothing to expire. And we're embedding it **right inside the app**, the same way our sibling wallet GRIM already does. No separate program to install, no server in the middle you have to trust. It just works, on your phone, out of the box.
|
||||
|
||||
Here's what that means for you, in plain terms. Tor hides your IP address — from our own relay and from your internet provider — so nobody watching the network can see it's *you* sending a payment. And the one thing Tor doesn't do on its own — that clever timing-shuffle a mixnet does — **we rebuild ourselves**: our relay briefly holds each payment and releases it on a randomized delay, so nobody can match "you sent" to "they received." You won't feel that delay, because it happens in the gap where a payment is already in flight. Your screen says "Sent" the instant our relay has your payment safely in hand.
|
||||
|
||||
And honestly? It's **better** day to day. Tor is faster for the moments you actually wait on — sending a payment, opening the app — because it skips the mixnet's built-in delays. It's easier on your battery. And it's simpler and more reliable, because there's no metered bandwidth left to run out.
|
||||
|
||||
I'll be straight about the one thing we give up: a full mixnet can resist an adversary powerful enough to watch the *entire* internet at once. Tor doesn't claim to stop that, and neither do we — it's not the threat a Grin payments wallet realistically faces. For every attacker that actually exists, you're covered.
|
||||
|
||||
Faster, more reliable, works on your phone, no tokens, no rented bandwidth to fail. That's the trade, and I'm glad to make it.
|
||||
Reference in New Issue
Block a user