Files
amethyst/docs/namecoin-nip05-design.md
T
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

310 lines
16 KiB
Markdown

# 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`
- `ProxiedSocketFactory``SocketFactory` 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:
```json
{
"nostr": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9"
}
```
**Extended form** — multiple users with relay hints (mirrors NIP-05 JSON structure):
```json
{
"nostr": {
"names": {
"_": "aaaa...0001",
"alice": "bbbb...0002"
},
"relays": {
"bbbb...0002": ["wss://relay.example.com"]
}
}
}
```
### Identity namespace (`id/`)
Namecoin `id/` names store personal identity data:
```json
{
"nostr": "cccc...0003"
}
```
Or with relay hints:
```json
{
"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 `NamecoinNameService``ElectrumxClient` → 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.kt``SocketFactory` 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
```bash
./gradlew :quartz:jvmTest --tests "*NamecoinNameResolverTest*"
```
### Manual testing (emulator or device)
Build and install the debug APK:
```bash
./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:
```bash
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:
```json
{
"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)