Foundation slice for making Tor a wallet-level user setting (Phase 2, slice 1
of the per-user-tor plan). Transport/config only; no onboarding/settings UI,
relay-set swap, or nprofile in this slice.
Wallet-level tor_enabled (tri-state):
- NostrConfig gains tor_enabled: Option<bool> with the existing Option +
serde(default) pattern. tor_enabled() resolves None -> ON, so every legacy
nostr.toml keeps Tor with no migration; new wallets write an explicit value at
onboarding (later slice). Adds tor_enabled_is_set() + set_tor_enabled() and a
back-compat/tri-state unit test.
Clearnet path (the big lift, rebuilt after the Nym removal took it out):
- ClearnetWebSocketTransport parallel to TorWebSocketTransport: direct TLS
websocket, honoring the user's AppConfig SOCKS5 proxy (tokio-socks, already in
the tree) so a VPN/proxy user can front their traffic. Shares ws_config/
split_ws with the Tor transport.
- Transport selection moved into run_service: the wallet's resolved tor_enabled()
picks Tor vs clearnet when the one pool is built; the Tor-bootstrap wait is
skipped for clearnet wallets. restart() re-enters here, so a settings toggle
rebuilds the pool on the new transport.
- Process-global route flag (tor::set_route_over_tor/route_over_tor), mirrored
from the open wallet like set_home_domain, so free-function HTTP callers pick
the matching transport off-thread.
- tor::http_request_bytes takes a clearnet branch when the wallet is Tor-off
(no more hard-fail on "Tor not bootstrapped"); new http::clearnet_request_bytes
reuses the existing HttpClient (proxy-aware) with the same redirect behavior
and browser-like UA. NIP-05, price, relay-pool gist and NIP-11 probes all ride
it unchanged.
Status/readiness:
- New TransportStatus enum + NostrService::transport_status()/tor_routing():
exposes a "Connected (direct)" clearnet state so a Tor-off wallet does not read
as "connecting over Tor" forever. The three UI status lines are rewired to it
in a later slice; the state is exposed cleanly now.
Grin node path unchanged (stays clearnet always). cargo check --all-targets
green; targeted config/nostr/tor tests pass.
The wallet pivoted from the Nym mixnet to Tor in build134; the Nym code
had been left on disk behind an optional, always-off "nym" feature and
never ran since. Delete it entirely: no Nym/mixnet fallback and no
Nym/mixnet mentions remain. Tor is the sole transport.
- Delete src/nym/ (mod, transport, streamexit, dns, nymproc) and its
`#[cfg(feature = "nym")] pub mod nym;` declaration in lib.rs.
- Cargo.toml: drop the `nym` feature and the commented-out Nym-only
path deps (nym-sdk, smolmix, hickory-proto); rewrite the mixnet-era
comments on the rustls/tokio-rustls/arti/openssl deps to the current
Tor reality. No active dependency changed: rustls, tokio-rustls and
webpki-roots stay (used by the Tor HTTPS client in tor/mod.rs).
- settings/config.rs: remove the persisted nym_entry_gateway /
nym_last_ipr fields and their getters/setters. No serde
deny_unknown_fields, so existing on-disk configs that still carry
those keys keep deserializing (the removed keys are ignored and
dropped on next save; same path the price-cache-removal test guards).
- Rename the Nym/mixnet-named i18n keys (values already said "Tor")
across all 9 locales and their t!() call sites: connected_nym ->
connected_tor, nym_ready -> tor_ready, connecting_nym ->
connecting_tor, mixnet_routing -> tor_routing, over_mixnet ->
over_tor. Displayed strings unchanged.
- Rewrite the Nym/mixnet-referencing comments in the active Tor files
(tor/engine.rs, tor/mod.rs, nostr/client.rs, nostr/pool.rs,
nostr/mod.rs, lib.rs, wallet/wallet.rs, node/node.rs,
gui widgets/mod/onboarding, Android BackgroundService) to describe
Tor; drop the now-broken [crate::nym::*] intra-doc links. No active
code behavior changed.
The pool's `exit` schema slot and exit_for/exit_for_host/has_exit
helpers are kept (comments de-Nym'd): they are inert under Tor but let
a pool document that carries an exit still parse.
Two self-contained hardening measures:
- WebSocket frame cap: the Tor relay transport dialed every relay with
tungstenite's default 64 MiB message / 16 MiB frame ceiling. Pass a
WebSocketConfig capping both to 4 MiB via client_async_tls_with_config, so
a hostile or buggy relay can't stream a giant frame into wallet memory. The
pool only requires max_message_length >= 128 KiB and the wallet's own events
are far smaller, so 4 MiB keeps ample headroom.
- Secret zeroization: the raw NIP-44 v3 ECDH conversation keys in wrapv3.rs
are now Zeroizing<[u8; 32]> so they are scrubbed from memory on drop instead
of lingering. zeroize (already a transitive dep) is pulled in directly.
Adds a ws-config test asserting the caps sit below the tungstenite defaults
and above the pool minimum.
The goblin: scheme was registered everywhere last night, but macOS is the one
platform that delivers a scheme click as a Carbon/Apple Event (kAEGetURL), not on
argv and not through any path winit or eframe surface, so on a Mac the click went
nowhere.
Install a kAEGetURL handler straight on the shared NSAppleEventManager at startup
(macOS only), pull the URL string out of the event, and feed it to on_data, the
exact same entry the desktop argv path uses. The per-frame Goblin router then lands
the pay URI on the prefilled review screen, identical to a scanned checkout QR or a
Linux argv link. This covers both a cold launch (event queued at start-up) and a
warm click (app already running).
The bridge is entirely cfg(target_os = "macos"): zero bytes on Linux, Windows and
Android (Linux binary size byte-for-byte unchanged). It talks to the Objective-C
runtime through the classic objc crate, which is already in the macOS build graph
via nokhwa/cocoa/metal/wgpu, so it adds no new dependency tree, only its own small
handler class. It registers its own Apple Event handler rather than touching the
NSApplicationDelegate winit owns, so nothing winit does is clobbered.
- Third-party credits: replace the "Nym mixnet" row with "Tor (arti)" (0.43,
linking the arti repo), matching the other credit rows' hardcoded style. No
locale strings existed for it (credit labels are hardcoded, not localized).
- Balance: show a quiet "Updating…" line under the balance while the node is
still warming, reusing the existing wallet.syncing() signal and the
balance-updating line's muted style; it clears once the node is synced. New
t!() key goblin.home.updating added to all six locales (i18n drift green).
- Nym sweep: Cargo.toml package description "Nym mixnet" -> "Tor". The only
other user-facing Nym reference was the credit above. Remaining references
are the deliberately-dormant `nym` feature/module and internal code comments,
left as-is; README and all locale values were already Tor.
- CI (release.yml/build.yml): remove fetch-nym + AWS_LC_SYS_PREBUILT_NASM;
the default cargo build --release is Tor/arti only now (nip44 fetch kept).
- Cargo.toml: nym-sdk is optional behind the `nym` feature so the default
build never pulls it.
- Delete the dead Nym-era probes (xrelay_smoke, connect_timing,
tunnel_measure) that referenced the removed Nym API and broke `cargo test`.
- Untrack the internal E2E harnesses (e2e.rs, nostr_e2e.rs) via .gitignore;
gate `mod e2e` behind the `e2e-internal` feature so clones still compile.
- Refresh stale Nym comments in build.rs and the build scripts.
The wallet's private transport moves from the Nym mixnet to embedded Tor
(arti, copied from GRIM's engine): it dials the relay's pinned .onion, so
the relay never learns your IP, while the relay + NIP-59 gift-wrap hide the
rest - content, sender, and (via a relay-side randomized release) timing.
The Grin node stays on the clear internet as before.
Why leave the mixnet: the Nym free-tier bandwidth this depended on is being
removed upstream (the grant expires at UTC midnight; the paid path requires
holding NYM tokens), so a payments wallet can't stand on it. Tor is
unmetered, embedded in-process on mobile, faster where users wait, and
lighter on the battery.
Preserved intact: the confirm-before-sent guard, relay-gated readiness, and
the lazy warm-on-activity node polling. src/nym/ is feature-gated off (arti
and nym-sdk can't share one binary); full removal is a follow-up.
nip44 is now published, so depend on the registry crate instead of the local
path checkout. A fresh checkout (the Mac included) no longer needs ../nip44
cloned next to goblin - that missing sibling was the reported build failure.
Byte-identical source (0.3.0 was published from this tree); cargo check --lib
green. nym stays a path dep (private fork, not on crates.io).
The 180s cold connect was the public-IPR path (nested TCP over the mixnet) plus
the public-exit lottery -- NOT the scoped exit, which was removed by mistake in
3372202. Restore the co-located MixnetStream exit: a relay connects in 0-2s over
it (measured), vs 15-180s over the tunnel.
- Cold-start sequencer: streamexit::is_ready + a bounded EXIT_HEAD_START gate in
nymproc so the exit client claims its Nym bandwidth grant before the tunnel,
avoiding two-client serialization (~1min otherwise). No SDK surgery.
- Pin relay.floonet.dev as the primary money-path relay (with its co-located
exit) in PINNED_POOL; keep relay.goblin.st as a secondary through transition.
- E2E: a funded 0.1 GRIN payment finalizes in 6s over the exit across two
different Grin nodes (grincoin.org, main.gri.mw).
The exit is unpinned everywhere, so the streamexit module + the exit_for/
exit_connect forks in transport + http were dead code. Remove them; the
wallet's only mixnet path is the smolmix tunnel.
Money path:
- Scoped, unbonded Nym exit for the money-path relay: the wallet dials a
relay operator's co-located exit over a MixnetStream (src/nym/streamexit.rs)
which pipes to its one relay; hostname-validated TLS end to end, no public
DNS. Anchor + fallback (never pin-only): any exit failure degrades to the
smolmix tunnel. relay.goblin.st's exit address is pinned in the relay pool
(src/nostr/pool.rs) and the maintainer gist so it bootstraps offline.
- STREAM_SETTLE bridges the open-before-accept gap so the first TLS byte is
not dropped into a stalled handshake.
- Verified end to end: two wallets complete a real gift-wrapped Grin payment
through relay.goblin.st over the exit, finalized + posted on mainnet
(src/wallet/e2e.rs, ignored live test).
Encryption:
- Adopt NIP-44 v3 for the NIP-17 gift-wrap path (G4): src/nostr/wrapv3.rs,
nip44 path dep; v3<->v3 and v3->v2 interop.
Also: mix-DNS (src/nym/dns.rs), full localization pass, GUI polish,
avatar-ring example, Android icon/script updates, GRIM deviation notes,
xrelay + connect-timing tests.
The .msi shortcuts already carry the icon, but the bare goblin.exe had none, so
Explorer/taskbar showed the generic exe icon. build.rs now embeds
wix/Product.ico (the yellow Goblin icon) as the exe's application icon resource.
Gated to Windows hosts: winresource is a `cfg(windows)` build-dependency and the
embed fn is `#[cfg(windows)]` (with a no-op stub otherwise), so Linux/macOS/
Android builds don't compile or run it — other releases are untouched. The embed
is best-effort (warns, never fails the build) in case rc.exe is unavailable.
The vendored 'openssl' dependency existed only to statically link OpenSSL on Linux/Android (where native-tls uses it). In the global [dependencies] it also forced openssl-src to compile on Windows and macOS, which don't use OpenSSL at all (SChannel / Security.framework). On the Windows MSVC CI runner that build fails: openssl-src's perl Configure runs under Git-Bash's MSYS perl, which lacks Params::Check / Locale::Maketext::Simple. Gating the dependency to cfg(any(linux, android)) removes the pointless, fragile build on Windows/macOS and keeps the self-contained static link on Linux/Android. No behavior change on any platform.
Settings now says "Manual transaction" and the privacy row reads "Messages &
lookups" opening a new Network privacy page that tells the truth: messages,
names, price and avatars ride the Nym mixnet; the grin node connects directly.
README and lander updated to match.
Requests are messages, payments are final: declining a request now sends the
requester a void control message (NIP-17), a requester can cancel a request they
sent (cancels the local invoice and notifies the payer), and incoming requests
resolve the sender's verified @username instead of a bare npub. The Requested
amount on the success screen is centered. New NostrDecline/NostrCancel tasks and
a goblin-action control message carry it, bound to the stored counterparty.
Localization: every Goblin-screen string moved to t!() keys (370 keys) and
translated into de/fr/ru/tr/zh-CN, guarded by a key/placeholder drift test.
System-locale auto-detect now matches region locales like zh-CN.
Pay: stop reddening the amount while typing — requesting more than you hold is
a valid request, so the digits stay black. Pressing Pay without enough funds
now shakes and briefly flashes the amount red and buzzes the phone, then
settles back. Bigger scan-to-pay puck.
Settings: the username "@" moves inside the field as the "@yourname"
placeholder (a leading "@" the user types is stripped). Mixnet routing is
shortened to "All traffic" and flagged in the privacy color. The Build row
links to GitHub releases. Network reads "MW + Nym mixnet + nostr". The Nym
third-party row shows the linked SDK version instead of "socks5".
Wallet: expose GRIM's native by-hand slatepack flow as an advanced
"Slatepacks" page — paste to receive/pay/finalize, or create a payment
slatepack to hand over. The fallback for when a payment can't ride a @username.
CI: fix the red GitHub builds. nym-sdk is a path dep on ../nym, which the
runners didn't have. A composite action materializes the pinned upstream nym
commit plus Goblin's small Android webpki patch before each build; aws-lc-sys
uses prebuilt NASM on native Windows.
Builds: strip release binaries, dropping ~17 MB of debug symbols from the
desktop build.
Goblin now links nym-sdk directly and runs its SOCKS5 client on an
internal tokio runtime exposing 127.0.0.1:1080 — the same loopback seam
the transport already dials. There is no sidecar subprocess and no
bundled/embedded/sideloaded helper binary; the goblin process itself owns
:1080. This mirrors how GRIM links arti/Tor in-process. Verified live: the
mixnet comes up in ~1.4-2s (gateway persisted in ~/.goblin/nym, reused
across launches) and a relay connects in ~2s over it, with no separate
process.
- Cargo.toml: add nym-sdk (path dep on the local nym checkout, which carries
the Android webpki-roots patch) + rustls with the ring feature.
- src/lib.rs: install rustls' ring CryptoProvider at startup. Linking nym-sdk
pulls aws-lc-rs alongside our ring; with two providers present rustls 0.23
won't auto-pick a default and tokio-tungstenite/reqwest panic on the first
TLS handshake. nym uses its own explicit provider, so this only steers our
relay/HTTP TLS.
- src/nym/sidecar.rs: replace the subprocess machinery with an in-process
Socks5MixnetClient (persistent storage; ephemeral fallback) kept alive for
the process lifetime on a dedicated runtime. Drops the binary lookup, embed
extraction, init/launch, and child management.
- build.rs: drop the GOBLIN_NYM_*_BIN embed block (nothing to embed).
- src/nym/{mod,transport}.rs, src/nostr/{mod,avatar}.rs: docs now describe the
in-process client; clear stray "old Tor/arti" wording (no Tor transport code
remains — only grin-core's slatepack OnionV3Address, which is unrelated).
Also: named users now get the pubkey-seeded gradient background with their
initial composited on top (instead of the Grin mark) — gui identicon.rs gains
gradient_bg_svg and widgets.rs gains gradient_letter_avatar; avatar_any routes
named keys to it. Verified live with @nymgoblin.
Ship the bundled nym-socks5-client on Windows and Android, not just Linux:
- sidecar.rs resolves the binary per platform — nym-socks5-client.exe on
Windows; on Android the sidecar rides in the APK jniLibs as
libnym_socks5_client.so and is launched from the native-library dir (the
one exec-allowed path), located via NATIVE_LIBS_DIR.
- Restore a vendored, statically-linked OpenSSL. Upstream Grim got this from
arti's static feature; dropping arti for Nym took it with it, which broke
Android/cross builds (no system OpenSSL for the target) and left desktop
dynamically linked to libssl. Inert on Windows/macOS (SChannel/Security.fw).
- android.sh bundles the sidecar into jniLibs per ABI; scripts/nym-android.sh
cross-builds it. Onboarding copy: Tor -> the Nym mixnet.
Verified end to end on an x86_64 emulator: the sidecar extracts, launches,
initialises, and opens the mixnet SOCKS5 proxy on 127.0.0.1:1080.
Route every relay and HTTP request (nostr relays, NIP-05, price) through
a local nym-socks5-client sidecar on 127.0.0.1:1080, so all traffic
egresses via the 5-hop Nym mixnet and nothing touches the clear net.
- Add src/nym/: SOCKS5 HTTP client (reqwest socks5h), NymWebSocketTransport
for the nostr relay pool (tokio-socks dial + TLS/ws handshake over the
mixnet), and a sidecar launcher that reuses or spawns nym-socks5-client.
- Swap the nostr-sdk transport off ArtiWebSocketTransport; route nip05.rs
and price.rs off Tor; revert the clearnet username-lookup shortcut.
- Remove the embedded arti Tor client wholesale: the onion-service
listener and send-to-onion path in the wallet, the legacy transport
GUI tab, the Tor settings page, src/tor/, the webtunnel pluggable
transport (Go build + submodule), and all arti crates from Cargo.toml.
The Grin node connection is unchanged (chain data, no payment metadata,
and never used Tor). The network requester the sidecar routes through is
configured via GOBLIN_NYM_PROVIDER / NETWORK_REQUESTER at deploy time.
The read-only NIP-05 username lookups (resolve + check_availability) carry no
secret and need no anonymity — routing them over Tor is what made choosing a
username slow. They now use a fast clearnet HTTPS GET (reqwest + rustls/ring,
so it still cross-compiles to Android), falling back to Tor only if clearnet is
blocked. The anonymity-critical paths (slatepack delivery, NIP-98-signed ops)
are untouched.
tests/nostr_e2e.rs (run with --ignored):
- nip17_slatepack_roundtrip: Alice gift-wraps a slatepack payment DM with a
subject to Bob over wss://nrelay.us-ea.st; Bob unwraps, the seal-author ==
sender invariant holds, the slatepack and subject extract intact.
- nip05_registration_roundtrip: registers a fresh name on goblin.st with a
real NIP-98 signature, resolves it back to the right pubkey, releases it.
Both pass against the live deployed infrastructure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Infrastructure (P0): deployed nostr-rs-relay (wss://nrelay.us-ea.st) and the
goblin-nip05d NIP-05 service (goblin.st) on us-ea.st with TLS + DNS.
Brand & theme (P1): Goblin name/icon/data-dir (.goblin); three-theme token
system (light/dark/yellow) in gui/theme.rs with colors.rs remapped as a shim;
Geist + Geist Mono fonts; AppConfig theme/density/last_wallet_id.
Nostr subsystem (P2-P3): src/nostr/ with NIP-06 identity (seed-derived,
NIP-49 encrypted), per-wallet rkv archive, guarded ingest policy (never
auto-pays Invoice1; binds replies to the stored counterparty npub), NIP-17
send/receive pipeline, NIP-05 client. Relay traffic routed over the embedded
arti Tor client via a custom WebSocketTransport. Wired into Wallet lifecycle
and the task handler. 26 unit tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Limit loading at tix list
- Sort txs by confirmation status to show txs waiting for an action at top
- Ability to delete wallet from the list without opening
- Optimize Tor connection on account switch
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/53
- Ability to export and verify payment proofs
Some fixes:
- Migrated tx heights store from lmdb (also changed heights key from local id to slate_id to avoid conflict between wallets)
- Close address panel on wallet change
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/52
- Update common dependencies
- Optimize check of running Tor service
- Async peer saving
- Add mainnet and testnet seeds
- Remove grinnode.live from default external connections
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/43
Use node and wallet submodules to avoid dependency conflicts inside grin-wallet on grin repo update.
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/29