Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The NostrService relay thread

Summary. NostrService is the long-running, per-wallet engine that connects to relays (over Tor), publishes payment messages, watches for incoming ones, and exposes send progress to the UI. Each open wallet has its own service and its own relay pool; there is no global connection.

Motivation

A wallet that pays by message needs a persistent worker: something that keeps relay sockets alive, subscribes for gift wraps addressed to you, runs the send pipeline off the UI thread, and survives for the life of the open wallet. Bundling that into one owned object (started on Wallet::open, stopped on Wallet::close) keeps the relay lifecycle tied to the wallet lifecycle and keeps per-wallet state (keys, rate limits, send status) isolated.

How it works

When a wallet opens with Nostr enabled, it spawns a NostrService on a dedicated tokio runtime. The service:

  • Holds the decrypted keys in memory only (never re-serialized to disk) and builds a nostr-sdk client whose relay transport is the Tor websocket transport when Tor routing is on, so every relay socket runs over Tor; in clearnet mode the same client dials the relays directly.
  • Subscribes for kind 1059 gift wraps addressed to your key, with a 3-day lookback (NIP-59 randomizes timestamps up to ~2 days into the past, so the window must be generous). Incoming events flow into the ingest policy.
  • Runs the send pipeline: build rumor → seal → gift wrap → publish to your relays and the recipient’s DM relays. Progress is published to the UI through an atomic send_phase (IDLE → WORKING → SENT / FAILED, plus REQUEST_BLOCKED), with a human-readable reason on failure.
  • Rate-limits incoming senders to blunt spam: a known contact may send ~30 events/hour, an unknown key ~10/hour.
  • Re-verifies names on a rolling basis (a few contacts per tick, on a periodic heartbeat) so a contact whose name was reassigned or released is caught.
  • Reports relay liveness to the transport layer: the service tells the Tor client which circuit generation its relays are connected on, which both drives the honest “Connected over Tor” indicator and lets the transport rebuild a circuit that can’t carry relay traffic.
  • Serializes cancel vs. finalize with a lock, so a user-initiated cancel can’t race a concurrent auto-finalize of the same slate.
  • Raises a system notification on Android for the two events worth interrupting you for: a payment landing (AutoReceive) and someone requesting one (SurfaceRequest), each a one-shot, fail-open alert that fires once per event and never delays ingest itself. The persistent “Listening for payments” notification is separate: it just says the service is alive, and stays up the whole time a wallet is open.

It also answers one-shot queries the UI needs: fetch_profile_blocking() (pull a kind 0 profile to verify a pasted key), nprofile() (your shareable NIP-19 profile with relay hints), and nsec() (plaintext key for an explicit user backup only).

Reference

In goblin/src/nostr/client.rs:

  • NostrService struct: keys, client (relay pool), rt_handle, connected, per-sender rate map, send_phase (atomic) + last_send_error, cancel_finalize_lock.
  • send_phase constants: IDLE=0, WORKING=1, SENT=2, FAILED=3, REQUEST_BLOCKED=4.
  • One-shots: public_key(), nprofile(), nsec(), keys(), fetch_profile_blocking().
  • Lifecycle hooks: Wallet::open / close / start_sync in goblin/src/wallet/wallet.rs; jobs arrive as WalletTask::Nostr*.
  • Relay transport: the arti-backed Tor websocket transport (goblin/src/tor/).

References