feat(relay): Namecoin .bit relay resolver + TLSA pin records#44
Conversation
Adds NIP-05 resolution backed by Namecoin .bit names — the cypherpunk
twin of DNS-based NIP-05.
API mirrors DnsIdentifier 1:1 so callers don't have to branch:
NamecoinIdentifier.isBit(id)
NamecoinIdentifier.fetch(id, {client})
NamecoinIdentifier.verify(identifier, pubkey, {client})
NamecoinIdentifier.lookupUri(id)
NostrIdentifier.fetch(id, {client}) // dispatches by shape
NostrIdentifier.verify(...)
Accepted shapes (case-insensitive, optional nostr: URI prefix):
- alice@example.bit
- example.bit
- d/<name>
- id/<name>
Both name-value forms are supported:
- simple: "nostr": "hex-pubkey"
- extended: "nostr": { "names": {...}, "relays": {...} }
Implementation
--------------
- pure Dart, no new mandatory dependencies
- WebSocket-over-TLS transport via dart:io WebSocket.connect, with
pluggable HttpClient for pinned-cert trust
- failover across 3 default servers; definitive misses short-circuit
- name-script parsing handles direct push / OP_PUSHDATA1 / OP_PUSHDATA2
- expiry check at NameExpireDepth = 36000 blocks (~250 days)
Ported from the canonical Go reference at
mstrofnone/nostrlib-nip05-namecoin and the JS twin in nostr-tools
PR #533. Behaviour matches the Kotlin Amethyst port.
Tests
-----
29 new test cases in test/nips/nip_005_namecoin_test.dart:
- isBit / parseIdentifier shape detection (10 cases)
- JSON value extraction across simple, extended, and id/ shapes (8)
- script utilities incl. PUSHDATA1/2 round-trips (5)
- lookupUri (2)
- 3 live-Namecoin blockchain smoke tests (m@testls.bit,
testls.bit, non-existent .bit) — same pattern as the existing
damus@damus.io live-DNS tests in nip_005_test.dart
449/449 tests pass (420 existing + 29 new).
Live-verified on the real Namecoin blockchain via ElectrumX:
m@testls.bit → 6cdebccabda1dfa058ab85352a79509b592b2bdfa0370325e28ec1cb4f18667d
testls.bit → 460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c
nonexistent → null
Back-compat
-----------
- No breaking changes. Existing DnsIdentifier (Nip5) untouched.
- New types are additive: NamecoinIdentifier, NostrIdentifier,
ElectrumxClient, DefaultElectrumxClient, ElectrumxServer,
defaultElectrumxServers, NameNotFoundException,
NameExpiredException, ElectrumxUnreachableException.
- web_socket_channel was already a transitive dep (used by nip_046);
this PR uses dart:io WebSocket directly so no new dependency lines
in pubspec.yaml.
References
----------
- Go: mstrofnone/nostrlib-nip05-namecoin
- JS: nbd-wtf/nostr-tools PR #533
- Kotlin: vitorpamplona/amethyst PRs #1547 / #2595 / #2612
- Swift: nostur-com/nostur-ios-public PR #60
- Spec: namecoin/proposals ifa-0001
Adds resolver-only support for `wss://example.bit/` Nostr relay URLs: look up the Namecoin record, walk `map` for subdomains, return the real `wss://...` URL plus any TLSA pin records and Tor aliases the record advertises. This is the second half of the Namecoin .bit Nostr support. The first half (NIP-05 identity resolution) is in the parent commit 0cf86b7. API surface ----------- class NamecoinRelayResolver { static bool isBitUrl(String wssUrl); Future<RelayResolution?> resolve(String wssUrl); void invalidate(String host); void clear(); } class RelayResolution { final String canonicalUrl; // wss://relay.example.bit/ final String? clearnetUrl; // wss://real.host/ (or null for onion-only) final List<String> candidates; final List<TlsaRecord> tlsaRecords; final List<String> onionEndpoints; } class TlsaRecord { bool matchesCertificate(List<int> certDer); bool matchesSpki(List<int> spkiDer); bool get requiresPkixValidation; // plus full RFC 6698 enums: TlsaUsage, TlsaSelector, TlsaMatchingType } What it does ------------ Given `wss://relay.testls.bit/`: 1. Splits the host into `(d/testls, ["relay"])` per ifa-0001 §map. ONE ElectrumX call regardless of subdomain depth. 2. Walks `d/testls`'s value JSON: `map.relay` (exact > * > null, honouring the "" empty-key default rule). 3. Returns: - candidates - all ws[s]:// URLs from `relay` / `relays` / `nostr.relay` / `nostr.relays` / pubkey-keyed `nostr.relays[<pk>]` (in that priority order) - tlsaRecords - 4-element `[usage, selector, matchingType, base64]` arrays from the same node's `tls` field - onionEndpoints - `tor` (string or array) AND `_tor.txt` shapes; bare onions promoted to `ws://x.onion/` What it does NOT do ------------------- This is a **resolver only**. It does NOT: - Open WebSockets (host wires the returned URL into their own client) - Mutate `HttpClient.badCertificateCallback` (host layers TLSA on top) - Decide whether to prefer Tor (host's job, based on user settings) Spec safety properties (all tested) ----------------------------------- - **No ancestor inheritance** of `relay` / `tls` / `tor`. A subdomain only gets what its walked node declares, NEVER what an ancestor `d/parent` declared. Otherwise a parent could silently authorise a subdomain it doesn't control. - **`d/<sub>.<parent>` is never queried.** Multi-label hosts always walk the parent's `map` tree. (Tested with subdomain-trap regression test.) - **Multi-label .onion REJECTED.** `evil.legit.onion` cannot appear under `tor` / `_tor` so a record can't redirect to a subdomain of someone else's hidden service. - **Non-onion under `tor` / `_tor` REJECTED.** Tor field can't redirect to clearnet. - **TLSA malformed records skipped, not crashed.** Wrong arity, non-base64, out-of-range codes all fall through. - **Tolerant base64 decoding** for TLSA: handles whitespace, unpadded, and url-safe alphabet (matches the Kotlin reference). Tests ----- 77 new test cases across 3 files: - test/namecoin/tlsa_test.dart 19 cases * tryParse: arity, range, format, whitespace, padding, url-safe * matching matrix: every selector × matchingType combo * requiresPkixValidation across all 4 usages - test/namecoin/record_parser_test.dart 39 cases * parseHostFlat: bare, subdomain, multi-label, edge cases * walkSubdomain: exact, wildcard, empty-key, anti-inheritance * parseRelayUrls: priority, dedup, subdomain walk, scheme filter * parseTlsaRecords: subdomain walk, malformed skip * parseTorEndpoints: string, array, _tor.txt, multi-label reject - test/namecoin/relay_resolver_test.dart 19 cases * isBitUrl, mock resolution, path preservation, TLSA passthrough * Tor passthrough, caching (positive + negative), invalidate * subdomain-trap regression * 3 live-Namecoin blockchain smoke tests against testls.bit 526/526 tests pass (449 baseline + 77 new). Live verification ----------------- Captured against the real Namecoin blockchain just before opening this PR: --- wss://relay.testls.bit/ --- canonical: wss://relay.testls.bit/ clearnet: wss://relay.testls.bit/ candidates: [wss://relay.testls.bit/] tlsa: 1 record(s) - daneTa/subjectPublicKeyInfo/sha256: m14YT5aSELhdDbZXOKFcSj2kK59XzV5lkiUlElBZh4A= onion: [ws://6cbn4rskfdr647otej7gpqlmpqcmj723vg2eoeuu7ljbwu6cpdebozyd.onion/] That hits the same Namecoin record that the Kotlin Amethyst, Swift Nostur, JS nostr-tools, and Go nostrlib-nip05-namecoin ports all hit, with byte-for-byte agreement on the resolved fields. Back-compat ----------- - No breaking changes. NIP-05 path from the previous commit untouched. - All new types are additive: NamecoinRelayResolver, RelayResolution, TlsaRecord, TlsaUsage, TlsaSelector, TlsaMatchingType, ParsedHostFlat, parseHostFlat, walkSubdomain, parseRelayUrls, parseTlsaRecords, parseTorEndpoints. - Reuses the ElectrumxClient + DefaultElectrumxClient transport from the NIP-05 PR, so identity + relay resolution share one ElectrumX round-trip per host. References ---------- - Spec: namecoin/proposals ifa-0001 §map / §tls - RFC: RFC 6698 (TLSA) - Kotlin: vitorpamplona/amethyst PR #2595 (the canonical reference) - Issue: PR #2612 (multi-label map walk for NIP-05)
|
Hey you can try to resolve Alice should have ifa-0001 like This might interest you as well: |
|
@mstrofnone love the work. Tested PR-44 end to end against my own Namecoin ElectrumX ( Scripthash, name_show, extended-form I've merged this into an integration branch (
Do you want to collaborate on this branch ? |
|
Hi @ethicnology! For 1. it's a problem but I like the rationale here https://www.namecoin.org/docs/faq/#is-squatting-a-problem--what-can-be-done-about-it and having a/ethic, b/ethic, c/ethic or any other scheme is just confusing for end users. I recently found this https://github.com/btcjt/titan/blob/main/docs/name-protocol.md and https://njump.me/nevent1qqsq0dg65gs2kpln4rghlzcskq2tnw4cksyhhn2g0nllgzk47dhahhqprdmhxue69uhhyetvv9ujuam9wd6x2unwvf6xxtnrdakj7q3qpc57ls4rad5kvsp733suhzl2d4u9y7h4upt952a2pucnalc59teqxpqqqqqqz7tfg4z there isn't a way to avoid this race and namecoin has been around since 2011 and for cents you can register a name.
I also sent you a couple of DMs on matrix but the meat of it is can you add your electrumx to amethyst as a PR? |
ethicnology/dart-nostr#44 merged 2026-05-16. First Dart impl and the first non-Amethyst impl to ship the full N1 + N2 + N3 surface (NIP-05 over .bit, relay subdomain resolver, TLSA pin records) in one library. 526/526 tests pass including live-blockchain smoke tests against testls.bit with byte-for-byte agreement against the Kotlin / Swift / TypeScript / Go ports on the same on-chain records. - README.md: add to the per-language reference implementations list. - N3.md: add to the per-language TLSA mirror enumeration.
Summary
Adds a resolver-only library for
.bitNostr relays:wss://example.bit/→ realwss://...URL via Namecoind/examplelookupwss://relay.example.bit/→ walksd/example'smap.relayper ifa-0001, never queriesd/relay.exampletor/_tor.txt.onionendpoints so Tor-aware callers can route through themThis is the second half of the Namecoin
.bitNostr support; the first half (NIP-05 identity) is in #43 and lands its shared transport. Both PRs are independently reviewable but PR 2 depends on PR 1'sElectrumxClient/DefaultElectrumxClienttypes — please merge PR 1 first.API surface
The intended host-app integration:
What it does NOT do
This is a resolver only. It deliberately doesn't:
HttpClient'sbadCertificateCallback— host layers TLSA on top in a portable way.The split is so the library stays portable across VM/web/Flutter targets where the TLS stack differs, while the host app (this repo's
disputecompanion, or any other consumer) wires the platform-specific bits.Spec safety properties (all tested)
relay/tls/torparseRelayUrls subdomain walk: ancestor relay does NOT leak(and the same fortlsaandtor)d/<sub>.<parent>never queriedsubdomain trap: never queries d/sub.parent— explicitly verifies onlyd/parentwas queriedparseTorEndpoints multi-label .onion REJECTEDtor/_torREJECTEDparseTorEndpoints non-onion strings droppedtolerates unpadded base64,tolerates url-safe alphabet,strips whitespace inside base64parseTlsaRecords skips malformed entriesTests
77 new test cases across 3 files:
test/namecoin/tlsa_test.darttryParseshape/range/format/base64 quirks; matching matrix across everyselector × matchingTypecombo;requiresPkixValidationacross all 4 usagestest/namecoin/record_parser_test.dartparseHostFlat,walkSubdomain(exact/wildcard/empty-key/anti-inheritance),parseRelayUrls,parseTlsaRecords,parseTorEndpointstest/namecoin/relay_resolver_test.dartisBitUrl; mock resolution; path preservation; TLSA + Tor passthrough; positive + negative caching; subdomain-trap regression; 3 live-Namecoin blockchain smoke tests againsttestls.bit526/526 tests pass (449 baseline + 77 new). Run with:
dart test test/namecoin/Live verification
Captured against the real Namecoin blockchain just before opening this PR:
The
relay.testls.bitresolution exercises the full pipeline: subdomain split →d/testlsElectrumX lookup →map.relaywalk → relay extraction → TLSA decode → Tor extraction. Same record, same fields the Kotlin Amethyst port reads via PR #2595.Implementation
5 new files under
lib/src/namecoin/:tlsa.dartTlsaRecord+ tolerant base64 decoderrecord_parser.dartparseHostFlat,walkSubdomain,parseRelayUrls,parseTlsaRecords,parseTorEndpointsrelay_resolver.dartNamecoinRelayResolver+ cachePlus
lib/nostr.dartexports.Reuses the
ElectrumxClient/DefaultElectrumxClienttransport from #43 unchanged — identity + relay resolution share one ElectrumX round-trip per host when a single client is reused.Back-compat
NamecoinRelayResolver,RelayResolution,TlsaRecord,TlsaUsage,TlsaSelector,TlsaMatchingType,ParsedHostFlat,parseHostFlat,walkSubdomain,parseRelayUrls,parseTlsaRecords,parseTorEndpoints.pointycastle(already a dep).References
vitorpamplona/amethystPR #2595 (158 tests)ethicnology/dart-nostr#43Checklist
dart analyze— no issuesdart format— formatteddart test— 526/526 passrelay.testls.bitresolves to real wss + TLSA + onion)