- 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
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 injectedSocketFactorylambda for proxy/Tor support.NamecoinNameResolver— Identifier parsing, value extraction, NIP-05 mapping. Accepts aserverListProviderlambda for dynamic server selection.NamecoinLookupCache— LRU cache with TTLNamecoinNameResolverTest— Unit tests for parsing and value extraction
-
amethyst/(app) — Android integration and Tor-aware wiring.NamecoinNameService— Application singleton, initialized with a proxy-awareElectrumxClientProxiedSocketFactory—SocketFactoryimplementation that routes through a SOCKS5 proxy (Tor)RoleBasedHttpClientBuilder.socketFactoryForNip05()— Returns a proxy-aware or defaultSocketFactorybased 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
ElectrumxClientaccepts asocketFactory: () -> SocketFactorylambda (evaluated at each connection, not captured at construction)ProxiedSocketFactorycreates sockets routed through ajava.net.Proxy(SOCKS5)RoleBasedHttpClientBuilder.socketFactoryForNip05()checks the user's NIP-05 Tor settings and returns eitherSocketFactory.getDefault()or aProxiedSocketFactorywith the active Tor SOCKS proxy- 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 liveStateFlow - 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:
Nip05Clientgains an optionalnamecoinResolverparameter- On
verify()andget(), if the identifier matches.bit/d//id/, it routes toNamecoinNameResolverinstead of the HTTP fetcher - Non-Namecoin identifiers are completely unaffected
AppModuleswires 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:
- A
namecoinResolvedUserflow watches the search input with a 400ms debounce - If the input matches any Namecoin format (
d/*,id/*,*.bit,*@*.bit), it resolves viaNamecoinNameService→ElectrumxClient→ blockchain - The resolved pubkey is used to get/create a
UserinLocalCache - 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()andclear()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
trustAllCertsoption 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+expiresInagainst 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 injectedSocketFactoryfor proxy supportquartz/.../nip05/namecoin/NamecoinNameResolver.kt— Identifier parsing, value extraction, dynamic server selectionquartz/.../nip05/namecoin/NamecoinLookupCache.kt— LRU cache with TTLquartz/src/jvmTest/.../NamecoinNameResolverTest.kt— Unit tests
New files (amethyst/)
amethyst/.../service/namecoin/NamecoinNameService.kt— App singleton, initialized with proxy-aware ElectrumxClientamethyst/.../model/privacyOptions/ProxiedSocketFactory.kt—SocketFactorythat routes through SOCKS5 proxy (Tor)
Modified files
amethyst/.../AppModules.kt— Wires up resolver with Tor-aware socket factory and server list provideramethyst/.../model/privacyOptions/RoleBasedHttpClientBuilder.kt— AddedsocketFactoryForNip05()for proxy-aware socket creationamethyst/.../ui/screen/loggedIn/search/SearchBarViewModel.kt— Namecoin search resolutionquartz/.../nip05DnsIdentifiers/Nip05Client.kt— Route.bitidentifiers 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:
- Search for
m@testls.bit— should resolve via onion server - Verify no direct clearnet connections to ElectrumX servers (use
tcpdump) - 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→ resolvesmentry → pubkey6cdebcca...18667dtestls.bit→ resolves root_entry → falls back to first available entryd/testls→ same astestls.bit(root lookup)