Skip to content

Instantly share code, notes, and snippets.

@BoltTouring
Last active March 4, 2026 17:13
Show Gist options
  • Select an option

  • Save BoltTouring/dde944661df330ec5119af8ef94159e1 to your computer and use it in GitHub Desktop.

Select an option

Save BoltTouring/dde944661df330ec5119af8ef94159e1 to your computer and use it in GitHub Desktop.
Sending bitcoin without addresses

Bridging Nostr Identity to Bitcoin Silent Payments

TL;DR - Never cut and paste a bitcoin address again (eventually)

We added NIP-05 resolution to Sparrow Wallet so you can send bitcoin to a Nostr identity like _@bushbashjapan.fyi — and it resolves to a Silent Payment address from the recipient's Nostr profile. No more copying and pasting 118-character sp1... addresses. We also added Silent Payment address support to Jumble, a Nostr client, so users can publish their SP address in their kind 0 profile and share it via QR code.

The Problem

Silent Payments (BIP 352) give Bitcoin users reusable, private addresses — but the addresses are long and unwieldy. BIP 353 solves this with DNS-based human-readable names (user@domain), but it requires domain owners to configure DNSSEC-signed TXT records containing bitcoin: URIs. That's a non-trivial DNS setup. NIP-05 also uses the user@domain format and also requires a domain — but the barrier is lower. You only need to serve a static nostr.json file, and many Nostr services provide NIP-05 identities for free. More importantly, NIP-05 separates identity verification (the domain) from payment data (Nostr relays). Your SP address lives in your Nostr profile, not in DNS records — so you can update it anytime without touching DNS configuration. Millions of Nostr users already have NIP-05 identities and publish profile metadata (kind 0 events) that can carry arbitrary fields — including a Silent Payment address. The missing piece: no wallet connects these two systems.

The Solution

Architecture

When a user types user@domain into Sparrow's send field, the resolution chain is:

user@domain
    │
    ▼
┌─────────────────────┐
│  BIP 353 (DNS)      │──── Found? ──→ Use it (existing Sparrow behavior)
│  DNS TXT record     │
└─────────────────────┘
    │ Not found
    ▼
┌─────────────────────┐
│  NIP-05 (Nostr)     │
│  1. GET nostr.json  │
│  2. Extract pubkey  │
│  3. Query relays    │
│  4. Read kind 0     │
│  5. Extract "sp"    │
└─────────────────────┘
    │
    ▼
  Silent Payment Address (sp1...)
    │
    ▼
  Existing Sparrow SP send flow

BIP 353 is checked first. NIP-05 only fires as a fallback if DNS doesn't resolve. This respects the existing standard while extending reach to every Nostr user with an SP address in their profile.

How NIP-05 Resolution Works

  1. HTTP lookup: GET https://domain/.well-known/nostr.json?name=user → returns the user's hex pubkey
  2. Relay discovery: Check nostr.json for relay hints; fall back to popular relays (purplepag.es, relay.damus.io, nos.lol, relay.nostr.band) if none provided
  3. Profile fetch: Open a WebSocket to relays, send a Nostr REQ for kind 0 events filtered by the pubkey
  4. Signature verification: Verify the event's BIP-340 Schnorr signature against the pubkey (see Security section)
  5. SP extraction: Parse the "sp" field from the profile metadata JSON
  6. Address parsing: Validate and parse as a SilentPaymentAddress, feed into Sparrow's existing SP send flow

Implementation Details

Sparrow (wallet side) — Fork: BoltTouring/sparrow Modified PaymentController.java to add NIP-05 as a fallback in the address resolution chain. When the DNS Service succeeds with an empty result or fails, tryNip05Resolution() is called. It checks a Caffeine cache first, then launches an async Nip05PaymentService that runs the full resolution on a background thread. The resolved SP address is fed into the existing setSilentPaymentAddress() method — so the entire downstream flow (UTXO selection, transaction building, signing) works unchanged with zero modifications. Drongo (core library) — Fork: BoltTouring/drongo Added a nip05 package with four classes:

  • Nip05Resolver — Core resolution logic using Java's built-in HttpClient and WebSocket (no external Nostr library needed). JSON parsing uses regex patterns to avoid adding a JSON dependency to Drongo. Includes full Schnorr signature verification of Nostr events.
  • Nip05Payment — Data record: (hrn, spAddress, nostrPubkey)
  • Nip05PaymentCache — Caffeine cache with 1-hour TTL
  • Nip05Exception — Custom exception type Jumble (Nostr client) — Fork: BoltTouring/jumble Added sp field support to kind 0 profiles:
  • Display SP address on profiles with truncation (matches npub display format)
  • Copy button for the full SP address
  • QR code dialog (responsive: Dialog on desktop, Drawer on mobile)
  • Profile editor field with sp1 prefix validation

Key Design Decisions

  1. BIP 353 first, NIP-05 fallback — Respects the DNS standard. NIP-05 only activates when DNS comes back empty.
  2. Fallback relays — Most nostr.json files don't include relay hints, so we maintain a list of well-known relays to query.
  3. No external Nostr library — Used Java's built-in HTTP and WebSocket APIs to avoid adding dependencies to Drongo's module system.
  4. Minimal scope — Only touches the send flow. No Nostr contacts integration, no deep protocol coupling.
  5. Pattern matching existing code — The Nip05PaymentService mirrors the existing DnsPaymentResolver pattern (async JavaFX Service, Caffeine cache, property binding, context menu).

Security: Event Signature Verification

Relays are untrusted intermediaries. A malicious relay could serve a fake kind 0 event with a substituted SP address, redirecting funds to an attacker. To prevent this, the resolver cryptographically verifies every Nostr event before trusting the sp field:

  1. Pubkey match — The event's pubkey must match the pubkey returned by the NIP-05 lookup
  2. Event ID verification — The event ID is recomputed from the canonical NIP-01 serialization ([0, pubkey, created_at, kind, tags, content]) and compared to the claimed ID. Unicode escapes are normalized to ensure consistent hashing across different relay implementations.
  3. Schnorr signature verification — The event's sig is verified as a valid BIP-340 Schnorr signature over the event ID using Sparrow's existing libsecp256k1 bindings If any check fails, the event is rejected and the resolution aborts. A relay cannot forge a valid signature without the user's Nostr private key. Trust model: The NIP-05 identity lookup (nostr.json) relies on HTTPS/TLS. If an attacker compromises the domain, they could map a username to their own pubkey — which would then resolve to their own (legitimately signed) profile with their SP address. This is the same trust model as email and the web: you trust the domain to tell the truth about who owns a username. BIP 353 is stronger here because DNSSEC provides a cryptographic proof chain, which is why it's checked first.

What This Enables

Any Nostr user can now receive bitcoin privately by:

  1. Generating a Silent Payment address in any SP-capable wallet
  2. Adding it to their Nostr profile's sp field (via Jumble or any client that supports it)
  3. Sharing their NIP-05 identity (user@domain) Senders using Sparrow just type the NIP-05 address — no need to handle raw SP addresses, no DNS configuration required on the recipient's side.

Testing

Unit tests cover all parsing logic (pubkey extraction, relay extraction, SP address extraction, event content parsing with unicode). A live integration test resolves _@bushbashjapan.fyi end-to-end — from NIP-05 lookup through relay query to SP address extraction, with full Schnorr signature verification.

# Run all NIP-05 tests
./gradlew :drongo:test --tests "com.sparrowwallet.drongo.nip05.Nip05ResolverTest"
# Run live integration test
./gradlew :drongo:test --tests "com.sparrowwallet.drongo.nip05.Nip05ResolverTest.liveResolveTest"
# Launch Sparrow with isolated data directory
./gradlew run --args="--dir /tmp/sparrow-test"

Repositories

What's Next

  • Needs wallets like Sparrow to implement this lookup
  • Encourage other Nostr clients to support the sp field in kind 0 profiles
  • Once Silent Payments proliferates, never cut and paste an on-chain address again.

Related Standards

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment