Skip to content

feat(nip05): Namecoin .bit identifier resolution via ElectrumX#43

Closed
mstrofnone wants to merge 1 commit into
ethicnology:developfrom
mstrofnone:feat/nip05-namecoin
Closed

feat(nip05): Namecoin .bit identifier resolution via ElectrumX#43
mstrofnone wants to merge 1 commit into
ethicnology:developfrom
mstrofnone:feat/nip05-namecoin

Conversation

@mstrofnone

Copy link
Copy Markdown

Summary

Adds NIP-05 identifier resolution backed by Namecoin .bit names — the cypherpunk twin of DNS-based NIP-05.

A user can drop alice@example.bit, example.bit, d/example, or id/alice into anywhere Nip5.fetch(...) is called today, and dart-nostr will resolve it through a public Namecoin ElectrumX server instead of https://example.com/.well-known/nostr.json.

This is the Dart port of an already-shipping, multi-language ecosystem of .bit NIP-05 resolvers — see References below for the Kotlin / Swift / Go / JS / Rust ports this matches behaviour-for-behaviour.

API surface

Mirrors DnsIdentifier 1:1 so call sites don't have to branch:

DNS NIP-05 Namecoin NIP-05
Detect (n/a) NamecoinIdentifier.isBit(id)
Fetch DnsIdentifier.fetch(id) NamecoinIdentifier.fetch(id, {client})
Verify DnsIdentifier.verify(...) NamecoinIdentifier.verify(..., {client})
Lookup URL DnsIdentifier.verificationUrl(id) NamecoinIdentifier.lookupUri(id)

Plus a thin dispatcher for callers that accept either form:

final pointer = await NostrIdentifier.fetch(input);
// .bit → NamecoinIdentifier.fetch
// otherwise → DnsIdentifier.fetch

NamecoinIdentifier.fetch returns the same DnsData type that DnsIdentifier.fetch returns, so any consumer that already handles a Future<DnsData?> works unchanged.

Accepted identifier shapes

Case-insensitive, optional nostr: URI prefix tolerated:

  • alice@example.bit
  • example.bit (uses the _ root entry)
  • d/example (domain namespace)
  • id/alice (identity namespace)

Name-value shapes supported

Both the simple and extended forms documented in namecoin/proposals ifa-0001 and the .bit NIP-05 draft:

// Simple form (root-only)
{ "nostr": "<64-hex-pubkey>" }

// Extended form (multi-user + relays)
{
  "nostr": {
    "names":  { "_": "<root-pubkey>", "alice": "<alice-pubkey>" },
    "relays": { "<alice-pubkey>": ["wss://relay.example.com/"] }
  }
}

// id/ namespace shape
{ "nostr": { "pubkey": "<hex>", "relays": ["wss://r.example.com/"] } }

Implementation

  • Pure Dart, no new mandatory dependencies.
  • WebSocket-over-TLS transport via dart:io WebSocket.connect, with a pluggable HttpClient for callers that need pinned-cert trust (the public Namecoin ElectrumX servers ship self-signed certs).
  • Failover across 3 default servers (electrumx.testls.space:50004, nmc2.bitcoins.sk:57004, 46.229.238.187:57004). Definitive misses short-circuit; transport errors fall through.
  • Name-script parsing handles direct push / OP_PUSHDATA1 / OP_PUSHDATA2. OP_PUSHDATA4 accepted defensively (Namecoin values fit in 520 bytes per consensus rules).
  • Expiry check at NameExpireDepth = 36000 blocks (~250 days, matches consensus.nNameExpirationDepth in Namecoin Core).

The implementation is split into 5 small files under lib/src/namecoin/:

File Purpose
script.dart Build name-index script + parse NAME_UPDATE outputs
electrumx_server.dart ElectrumxServer type + default server list
electrumx_client.dart ElectrumxClient interface + DefaultElectrumxClient impl
identifier.dart Parse .bit / d/ / id/ shapes (private)
value.dart Extract pubkey + relays from name-value JSON (private)

Public surface exposed through lib/nostr.dart:

  • NamecoinIdentifier / Nip5Namecoin (public-facing, mirrors DnsIdentifier)
  • NostrIdentifier (dispatcher)
  • ElectrumxClient / DefaultElectrumxClient (transport, pluggable)
  • ElectrumxServer / defaultElectrumxServers
  • NameNotFoundException / NameExpiredException / ElectrumxUnreachableException

Tests

29 new test cases in test/nips/nip_005_namecoin_test.dart:

Group Cases What it covers
isBit 4 Detection across .bit / d/ / id/ / nostr: shapes
parseIdentifier 7 All shape combinations + uppercase normalisation + edge cases
lookupUri 2 Pseudo-URI generation, error path
extractNostrFromValue 8 Simple, extended, id/, malformed, invalid pubkey
Namecoin script utilities 5 Hash determinism, NAME_UPDATE round-trip, PUSHDATA1/PUSHDATA2
Live Namecoin blockchain 3 m@testls.bit, testls.bit, non-existent — same pattern as damus@damus.io in nip_005_test.dart

449/449 tests pass (420 existing + 29 new). Run with:

dart test test/nips/nip_005_namecoin_test.dart

Live verification

Captured against the real Namecoin blockchain just before opening this PR:

$ dart run .scratch_verify.dart
--- m@testls.bit ---
  pubkey: 6cdebccabda1dfa058ab85352a79509b592b2bdfa0370325e28ec1cb4f18667d
  domain: testls.bit
  name:   m
--- testls.bit ---
  pubkey: 460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c
--- nonexistent.bit ---
  result: null

Same pubkeys as the JS port (nostr-tools PR #533), the Go port (nostrlib-nip05-namecoin), and the Kotlin/Amethyst port — so any client mixing dart-nostr with one of those agrees byte-for-byte on identity.

Back-compat

  • No breaking changes. Existing DnsIdentifier / Nip5 is untouched.
  • All new types are additive: NamecoinIdentifier, Nip5Namecoin typedef, NostrIdentifier, ElectrumxClient, DefaultElectrumxClient, ElectrumxServer, defaultElectrumxServers, three exception types.
  • web_socket_channel was already a transitive dependency (via nip_046); this PR uses dart:io WebSocket directly so no new lines in pubspec.yaml.
  • Existing 420 tests still pass without modification.

Security properties

  • No name-value inheritance from ancestor records. alice@sub.example.bit only consults d/example's names.alice, not _'s pubkey. (Tested explicitly.)
  • Hex pubkey validation — rejects anything that isn't 64 hex chars before returning DnsData.
  • NameNotFound/NameExpired short-circuit — definitive blockchain answers from one server propagate immediately rather than burning the rest of the failover list.
  • Self-signed certs are opt-in. Default DefaultElectrumxClient uses platform trust; callers must explicitly inject an HttpClient with badCertificateCallback for the public test servers. (See _trustAllHttpClient in the test file for the pattern; production should pin specific cert fingerprints.)

References

This PR ports an already-shipping pattern. The Dart implementation matches:

A follow-up PR will add the relay-resolver counterpart (wss://relay.example.bit/ → real wss endpoint with TLSA pin records). Posting that as a separate PR so each is independently reviewable.

Checklist

  • dart analyze — no issues
  • dart format — formatted
  • dart test — 449/449 pass
  • Live-verified on Namecoin chain via ElectrumX
  • No new mandatory dependencies
  • Public APIs documented (dartdoc on every method)
  • Anti-inheritance security property tested

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
@ethicnology

Copy link
Copy Markdown
Owner

Hey @mstrofnone this is very appreciated !

I was going to implement it myself, at some point…

I'm busy this week but be sure I will review that over this week-end or the next one

@ethicnology

Copy link
Copy Markdown
Owner

merged in #44

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants