Files
amethyst/docs/namecoin-nip05-design.md
M 3f39f96e81 fix: route ElectrumX through Tor proxy, add onion server
- ElectrumxClient accepts injected SocketFactory (lambda) so connections
  respect the user's Tor/proxy settings instead of leaking their IP
  through raw sockets
- Add ProxiedSocketFactory for SOCKS5 proxy routing
- Add .onion ElectrumX server as primary when Tor is enabled, with
  electrumx.testls.space as clearnet fallback
- NamecoinNameResolver accepts serverListProvider lambda for dynamic
  server selection based on current Tor settings
- RoleBasedHttpClientBuilder.socketFactoryForNip05() bridges Amethyst's
  Tor settings to the socket factory
- NamecoinNameService now requires explicit init with proxy-aware client
- Remove dead code: Nip05NamecoinAdapter (never referenced),
  NamecoinVerificationDisplay (never called)
- Update design documentation
2026-03-03 06:28:01 +11:00

16 KiB

Namecoin NIP-05 Resolution — Design Document

Overview

This patch adds Namecoin blockchain-based NIP-05 identity verification to Amethyst. Users can set their nip05 field to a .bit domain (e.g. alice@example.bit) or a direct Namecoin name (d/example, id/alice), and Amethyst will resolve it via the Namecoin blockchain instead of HTTP.

This is censorship-resistant identity verification: no web server to seize, no DNS to hijack, no TLS certificate to revoke. The name-to-pubkey mapping lives in Namecoin UTXOs.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                        Amethyst App                         │
├─────────────────────────────────────────────────────────────┤
│  Nip05Client                                                │
│    ├── HTTP path (existing) ── Nip05Fetcher                 │
│    └── Namecoin path (new)  ── NamecoinNameResolver         │
│                                    │                        │
│  NamecoinNameService (singleton)   │                        │
│    └── NamecoinLookupCache         │                        │
│                                    │                        │
│  RoleBasedHttpClientBuilder        │                        │
│    └── socketFactoryForNip05()  ───┤ (Tor-aware sockets)    │
│                                    │                        │
│  ProxiedSocketFactory              │                        │
│    └── SOCKS5 proxy routing     ───┘                        │
├────────────────────────────────────┼────────────────────────┤
│                        Quartz Library                       │
├────────────────────────────────────┼────────────────────────┤
│  NamecoinNameResolver              │                        │
│    ├── parseIdentifier()           │                        │
│    ├── extractFromDomainValue()    │ (d/ namespace)         │
│    ├── extractFromIdentityValue()  │ (id/ namespace)        │
│    └── serverListProvider()        │ (Tor/clearnet routing) │
│                                    │                        │
│  ElectrumxClient                   │                        │
│    ├── buildNameIndexScript()      │                        │
│    ├── electrumScriptHash()        │                        │
│    ├── parseNameScript()           │                        │
│    └── socketFactory()          ───┤ (injected, proxy-aware)│
│                                    ▼                        │
│                  ┌───────────────────────────┐              │
│                  │    ElectrumX Server        │              │
│                  │  (clearnet or .onion)      │              │
│                  └───────────────────────────┘              │
└─────────────────────────────────────────────────────────────┘

Layer Separation

  • quartz/ (library) — Protocol-level logic. No Android dependencies.

    • ElectrumxClient — TCP/TLS connection to ElectrumX, JSON-RPC, script parsing. Accepts an injected SocketFactory lambda for proxy/Tor support.
    • NamecoinNameResolver — Identifier parsing, value extraction, NIP-05 mapping. Accepts a serverListProvider lambda for dynamic server selection.
    • NamecoinLookupCache — LRU cache with TTL
    • NamecoinNameResolverTest — Unit tests for parsing and value extraction
  • amethyst/ (app) — Android integration and Tor-aware wiring.

    • NamecoinNameService — Application singleton, initialized with a proxy-aware ElectrumxClient
    • ProxiedSocketFactorySocketFactory implementation that routes through a SOCKS5 proxy (Tor)
    • RoleBasedHttpClientBuilder.socketFactoryForNip05() — Returns a proxy-aware or default SocketFactory based on current Tor settings

Tor & Proxy Integration

The ElectrumX connection respects the user's Tor settings to prevent IP leaks:

Problem

The original ElectrumxClient used raw java.net.Socket / SSLSocket directly, bypassing OkHttp entirely. This meant Namecoin lookups would leak the user's real IP even when they had configured Tor for NIP-05 verification traffic.

Solution

  1. ElectrumxClient accepts a socketFactory: () -> SocketFactory lambda (evaluated at each connection, not captured at construction)
  2. ProxiedSocketFactory creates sockets routed through a java.net.Proxy (SOCKS5)
  3. RoleBasedHttpClientBuilder.socketFactoryForNip05() checks the user's NIP-05 Tor settings and returns either SocketFactory.getDefault() or a ProxiedSocketFactory with the active Tor SOCKS proxy
  4. SSL is layered on top of the (possibly proxied) base socket via SSLSocketFactory.createSocket(socket, host, port, autoClose), preserving the proxy tunnel

Server Selection

When Tor is enabled for NIP-05 traffic, the server list switches to prioritize onion routing:

Setting Primary server Fallback
Tor off electrumx.testls.space:50002 ulrichard.ch:50006, nmc2.lelux.fi:50006
Tor on .onion:50002 (see below) electrumx.testls.space:50002 (via Tor)

The serverListProvider lambda in NamecoinNameResolver is evaluated at resolution time, so toggling Tor settings takes effect immediately without restarting the app.

Dynamic Evaluation

Both the socket factory and server list are provided as lambdas, not captured values. This means:

  • Toggling Tor on/off in settings takes effect on the next Namecoin lookup
  • The proxy port is read from DualHttpClientManager's live StateFlow
  • No app restart or singleton reconstruction needed

ElectrumX Protocol — How Name Resolution Works

Namecoin names are stored as UTXOs with NAME_UPDATE scripts. The ElectrumX server indexes these by a canonical "name index script hash", allowing lookup via standard Electrum protocol methods.

Resolution Steps

1. Build canonical name index script
   OP_NAME_UPDATE(0x53) + push(name_bytes) + push(empty) + OP_2DROP(0x6d) + OP_DROP(0x75) + OP_RETURN(0x6a)

2. Compute Electrum-style scripthash
   SHA-256(script) → reverse bytes → hex encode

3. Query transaction history
   → blockchain.scripthash.get_history(scripthash)
   ← [{tx_hash, height}, ...]

4. Fetch latest transaction (last entry = most recent name update)
   → blockchain.transaction.get(tx_hash, verbose=true)
   ← {vout: [{scriptPubKey: {hex: "53..."}}]}

5. Parse NAME_UPDATE script from transaction output
   Script: OP_NAME_UPDATE <push(name)> <push(value_json)> OP_2DROP OP_DROP <address_script>
   Extract: name string + JSON value

6. Extract Nostr pubkey from the JSON value

Why Not blockchain.name.get_value_proof?

The Electrum-NMC fork of ElectrumX advertises a blockchain.name.get_value_proof method (protocol v1.4.3), but in practice this method expects a scripthash parameter, not a name string. The scripthash-based approach described above works with both the Namecoin ElectrumX fork and stock ElectrumX pointed at a Namecoin node, as long as the server has a name index.

Namecoin Value Formats

Domain namespace (d/)

Namecoin d/ names store domain configuration as JSON. Two Nostr formats are supported:

Simple form — single pubkey for the root domain:

{
  "nostr": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9"
}

Extended form — multiple users with relay hints (mirrors NIP-05 JSON structure):

{
  "nostr": {
    "names": {
      "_": "aaaa...0001",
      "alice": "bbbb...0002"
    },
    "relays": {
      "bbbb...0002": ["wss://relay.example.com"]
    }
  }
}

Identity namespace (id/)

Namecoin id/ names store personal identity data:

{
  "nostr": "cccc...0003"
}

Or with relay hints:

{
  "nostr": {
    "pubkey": "dddd...0004",
    "relays": ["wss://relay.example.com"]
  }
}

Identifier Formats

User input Namecoin name Local part Namespace
alice@example.bit d/example alice DOMAIN
_@example.bit d/example _ DOMAIN
example.bit d/example _ DOMAIN
d/example d/example _ DOMAIN
id/alice id/alice _ IDENTITY

NIP-05 Integration

The integration is minimal and non-invasive:

  1. Nip05Client gains an optional namecoinResolver parameter
  2. On verify() and get(), if the identifier matches .bit / d/ / id/, it routes to NamecoinNameResolver instead of the HTTP fetcher
  3. Non-Namecoin identifiers are completely unaffected
  4. AppModules wires up the resolver with Tor-aware socket factory and dynamic server selection

Search Integration

The search bar resolves Namecoin identifiers in real-time via SearchBarViewModel:

  1. A namecoinResolvedUser flow watches the search input with a 400ms debounce
  2. If the input matches any Namecoin format (d/*, id/*, *.bit, *@*.bit), it resolves via NamecoinNameServiceElectrumxClient → blockchain
  3. The resolved pubkey is used to get/create a User in LocalCache
  4. The Namecoin-resolved user is prepended to the standard local search results (deduplicated)

This means typing alice@example.bit, example.bit, d/example, or id/alice into the search bar will query the Namecoin blockchain and show the resolved user profile at the top of results.

Default ElectrumX Servers

Clearnet (Tor off)

Server Port TLS Notes
electrumx.testls.space 50002 Yes (self-signed) Primary
ulrichard.ch 50006 Yes Fallback
nmc2.lelux.fi 50006 Yes Fallback

Tor (Tor on for NIP-05)

Server Port TLS Notes
i665jpwsq46zlsdbnj4axgzd3s56uzey5uhotsnxzsknzbn36jaddsid.onion 50002 Yes (self-signed) Primary — onion service for electrumx.testls.space
electrumx.testls.space 50002 Yes (self-signed) Fallback (routed through Tor SOCKS proxy)

The trustAllCerts flag is set for servers with self-signed certificates. Users can configure custom servers via NamecoinNameService.setCustomServers().

Caching

  • LRU cache with configurable max entries (default 500) and TTL (default 1 hour)
  • Cache key is the normalized (lowercased, trimmed) identifier
  • Both positive and negative results are cached
  • Cache is invalidated on TTL expiry; manual invalidate() and clear() are available

Security Considerations

  • Tor integration: ElectrumX connections are routed through the user's Tor SOCKS proxy when NIP-05 Tor settings are enabled. This prevents IP leaks to ElectrumX servers. The onion server is preferred when Tor is active, providing end-to-end onion routing.
  • Self-signed certificates: The primary ElectrumX server uses a self-signed TLS cert. The trustAllCerts option accepts any certificate for that server. This is acceptable because the Namecoin blockchain itself provides the trust anchor — we verify names against on-chain data, not the transport layer. A MITM could return stale data but cannot forge name registrations.
  • Name expiry: Namecoin names expire after ~36,000 blocks (~250 days) if not renewed. The current implementation does not check expiry. Future work should compare the name's height + expiresIn against the current block height.
  • Server trust: The client trusts that the ElectrumX server returns accurate transaction data. For higher assurance, SPV proof verification could be added in the future.
  • Dynamic proxy evaluation: Socket factory and server list are evaluated per-request (via lambdas), ensuring Tor setting changes take effect immediately without stale socket reuse.

Files Changed

New files (quartz/)

  • quartz/.../nip05/namecoin/ElectrumxClient.kt — ElectrumX TCP/TLS client with injected SocketFactory for proxy support
  • quartz/.../nip05/namecoin/NamecoinNameResolver.kt — Identifier parsing, value extraction, dynamic server selection
  • quartz/.../nip05/namecoin/NamecoinLookupCache.kt — LRU cache with TTL
  • quartz/src/jvmTest/.../NamecoinNameResolverTest.kt — Unit tests

New files (amethyst/)

  • amethyst/.../service/namecoin/NamecoinNameService.kt — App singleton, initialized with proxy-aware ElectrumxClient
  • amethyst/.../model/privacyOptions/ProxiedSocketFactory.ktSocketFactory that routes through SOCKS5 proxy (Tor)

Modified files

  • amethyst/.../AppModules.kt — Wires up resolver with Tor-aware socket factory and server list provider
  • amethyst/.../model/privacyOptions/RoleBasedHttpClientBuilder.kt — Added socketFactoryForNip05() for proxy-aware socket creation
  • amethyst/.../ui/screen/loggedIn/search/SearchBarViewModel.kt — Namecoin search resolution
  • quartz/.../nip05DnsIdentifiers/Nip05Client.kt — Route .bit identifiers to Namecoin resolver

Testing

Unit tests

./gradlew :quartz:jvmTest --tests "*NamecoinNameResolverTest*"

Manual testing (emulator or device)

Build and install the debug APK:

./gradlew assemblePlayDebug
adb install -r amethyst/build/outputs/apk/play/debug/amethyst-play-universal-debug.apk

Search bar tests — open the search bar and enter each of these:

Search query Expected result What it tests
m@testls.bit Resolves to Vitor Pamplona's profile NIP-05 style user@domain.bit
testls.bit Resolves to Vitor Pamplona's profile (root _ entry) Bare domain .bit lookup
d/testls Resolves to Vitor Pamplona's profile Direct d/ namespace
id/someuser Resolves if registered on-chain Direct id/ namespace

Tor tests — enable Tor and set "NIP-05 verifications via Tor" to on:

  1. Search for m@testls.bit — should resolve via onion server
  2. Verify no direct clearnet connections to ElectrumX servers (use tcpdump)
  3. Toggle Tor off — next search should use clearnet servers

Verification test — if a profile has a .bit address in its nip05 field, the NIP-05 badge should verify via the blockchain instead of HTTP.

Network verification — to confirm ElectrumX calls are being made:

adb root
adb shell tcpdump -i any -nn port 50002 or port 50006

With Tor off, you should see TCP connections to 162.212.154.52:50002 (electrumx.testls.space). With Tor on, you should see connections to the local Tor SOCKS port only (no direct ElectrumX connections).

Live test data

The name d/testls is registered on the Namecoin blockchain (block 551519+, last updated block 814278) with value:

{
  "nostr": {
    "names": {
      "m": "6cdebccabda1dfa058ab85352a79509b592b2bdfa0370325e28ec1cb4f18667d"
    }
  }
}

This means:

  • m@testls.bit → resolves m entry → pubkey 6cdebcca...18667d
  • testls.bit → resolves root _ entry → falls back to first available entry
  • d/testls → same as testls.bit (root lookup)