Skip to content

feat: resolve .bit hostnames via Namecoin ElectrumX#24

Draft
mstrofnone wants to merge 2 commits into
hzrd149:masterfrom
mstrofnone:feat/nip05-namecoin
Draft

feat: resolve .bit hostnames via Namecoin ElectrumX#24
mstrofnone wants to merge 2 commits into
hzrd149:masterfrom
mstrofnone:feat/nip05-namecoin

Conversation

@mstrofnone
Copy link
Copy Markdown

@mstrofnone mstrofnone commented May 21, 2026

Why

Today an nsite hostname is either <npub>.gateway.tld (or the base36 named-site variant) or a CNAME pointing to one. Both anchor the user's identity to a DNS registrar and the TLS PKI.

Adding .bit resolution lets an nsite live at <yourname>.bit with no DNS or TLS dependency on a traditional registrar — the name is published to the Namecoin blockchain, the gateway looks up the latest NAME_UPDATE value, extracts a nostr pubkey, and serves the same site it would have served from npub1….gateway.tld.

This is the sovereign-naming endpoint of the nsite stack: Blossom for storage, Nostr for the index, Namecoin for the name.

What

A new sibling resolver in src/services/namecoin.ts, hooked into the existing resolvePubkeyFromHostname flow:

parseNsiteHostname (npub / snapshot / canonical label)
  ↓ (if no match and hostname ends in .bit)
resolveNamecoinHostname  ← new
  ↓ (if still no match)
resolveDns(hostname, "CNAME")

The Namecoin path:

  1. Strips .bit, takes the registrable label, prepends d/ to build the Namecoin name (alice.bitd/alice).
  2. Opens a short-lived WSS connection to a public Namecoin ElectrumX server and calls blockchain.name.show (with a fallback to blockchain.namecoin.name_show).
  3. Expands any ifa-0001 §"import" chain on the returned value before extraction (see "Import-chain support" below).
  4. Parses the returned JSON value and extracts a hex pubkey, tolerating four shapes that show up across the existing .bit ecosystem:
    • { "pubkey": "<hex>" }
    • { "npub": "npub1…" }
    • { "nostr": "<hex>" } (simple NIP-05 form)
    • { "nostr": { "names": { "_": "<hex>", … } } } (extended NIP-05 form, with _-root priority then first valid hex)
  5. Returns a ResolvedSiteAddress pointing at the root site (kind 15128) by default. If the JSON value includes an explicit "nsite": "<identifier>" hint, returns a named-site address (kind 35128).

Results are cached in the existing ["dns", domain] KV namespace so a hit on either resolver path serves the next request from cache.

Import-chain support (ifa-0001 §"import")

Namecoin caps each name's value at 520 bytes, which makes apex records crowded fast. ifa-0001 §"import" lets a name delegate shared blocks into a sibling name (typically dd/<name>) via an "import" key on the JSON value. Without import-chain handling, a NIP-05 lookup against a record using this pattern silently fails: the resolver sees the apex value, finds no nostr field, and gives up — never consulting the imported sibling that actually carries the nostr.names block.

This is the canonical demo target's setup. d/testls (testls.bit) is essentially:

{ "import": "dd/testls", "ip": "107.152.38.155" }

…and dd/testls holds the actual nostr.names block. With this PR both testls.bit and named identities like m@testls.bit resolve correctly through the import.

A new module src/services/namecoin-import.ts implements expandImports(root, fetcher, maxDepth=4). It:

  • returns the root unchanged when there is no import key (non-import records pay zero extra I/O);
  • accepts all four shorthand forms of the import value ("d/foo", ["d/foo"], ["d/foo","sel"], [["d/foo","sel"],…]);
  • walks the imported value via the DNS-dotted Subdomain Selector using the ifa-0001 §"map" rules (exact label > * wildcard > "" default);
  • merges with importer-wins semantics; null in the importer suppresses the imported counterpart per spec;
  • recurses to depth 4 by default with cycle protection on (name,selector) pairs;
  • treats lookup failures (missing name, malformed JSON, transient network error) as the empty object, so a flaky ElectrumX shard cannot nuke an otherwise resolvable record.

The expansion runs between fetching the apex value and extracting the nostr field; imported sibling names go through the same server-fallback machinery as the apex lookup.

Spec

NIP draft: nostr-protocol/nips#2349.

Import-chain semantics: ifa-0001 §"import" (https://github.com/namecoin/proposals/blob/master/ifa-0001.md).

Landing the resolver shape upstream into the nsite spec (nsite.md) is intentionally a separate PR — this change only touches the runtime resolver.

No breaking changes

  • Existing npub / snapshot / canonical-label / CNAME resolution paths are untouched.
  • The Namecoin path triggers only when the hostname ends in .bit. Every other hostname follows the previous code path bit-for-bit.
  • Records without an import key follow the original code path with zero extra ElectrumX queries (regression-tested).
  • No new dependencies. Deno's built-in WebSocket and JSON are sufficient.

Trust model

  • The gateway queries public Namecoin ElectrumX servers over WSS. Default list: wss://nmc2.bitcoins.sk:57004, wss://electrumx.testls.space:50004.
  • Operators can override the server list via NSITE_NAMECOIN_ELECTRUMX_SERVERS (comma-separated wss://host:port).
  • The path uses Deno's standard TLS verification. Operators that want to pin to the Namecoin community's self-signed certs can front the gateway with their own TLS-terminating proxy.
  • The returned pubkey is treated identically to a CNAME-derived pubkey: the gateway then fetches the user's manifest and blobs from their advertised relays / Blossom servers, exactly as before.

Future work

  • Optional support for a locally-run Namecoin node / local ElectrumX (env var pointing at e.g. ws://localhost:50004). The current code already accepts any wss:// URL via NSITE_NAMECOIN_ELECTRUMX_SERVERS, but ws:// localhost support and a curated fallback chain are out of scope for this PR.
  • Spec PR against nsite.md to document the resolver shape, once this lands and the wire format settles.
  • Cert pinning for the public ElectrumX operators (Deno doesn't expose a custom CA hook for WSS today; revisit when it does).

Testing

Locally on Deno 2.7.14:

  • deno fmt --check — passes on all changed files. Two unrelated files (CHANGELOG.md, deno.json) fail deno fmt --check on master already; left untouched.
  • deno check main.ts — passes.
  • deno test41 passed, 0 failed, 1 ignored. The ignored test is the live-network integration test, gated behind NSITE_NAMECOIN_INTEGRATION=1.

Unit tests cover:

  • .bit hostname detection (case, trailing dot, multi-label).
  • Hostname → Namecoin name parsing.
  • JSON pubkey extraction across all four shapes above.
  • nsite identifier hint handling.
  • Root-vs-named-site ResolvedSiteAddress shape.
  • Rejection of malformed JSON / non-hex pubkeys / missing pubkey.
  • Import-chain handling: shorthand parsing (string / array / pair-array / canonical), importer-wins merge, null-suppression, depth-4 recursion happy path, depth budget truncation, cycle protection, lenient I/O (lookup returns null / throws / returns malformed JSON), DNS-order selector walk, wildcard fallback.
  • Integration: testls.bit-style apex/sibling split resolves both bare and named NIP-05 identities; no-import regression guard (records without import make zero extra ElectrumX queries); importer wins on nostr.names; resolution survives an unreachable import target when the importer has its own names.

m added 2 commits May 21, 2026 12:37
Adds a third path to the hostname resolver: if the hostname ends in
.bit, query a Namecoin ElectrumX server over WSS for the latest
NAME_UPDATE value of d/<name> and extract a nostr pubkey from the
resulting JSON.

Both the simple { "nostr": "<hex>" } form and the extended NIP-05
form ({ "nostr": { "names": { "_": "<hex>" } } }) are accepted,
matching the spec draft at nostr-protocol/nips#2349. An optional
"nsite" identifier hint can point at a named site (kind 35128);
otherwise the resolver returns the root site (kind 15128).

The new path is a sibling of the existing npub-subdomain / CNAME
resolution: if the hostname doesn't end in .bit the resolver is
skipped entirely. Resolution is cached via the existing DNS pubkey
cache.

No new dependencies. Uses Deno's built-in WebSocket. Operators can
override the ElectrumX server list via NSITE_NAMECOIN_ELECTRUMX_SERVERS
(comma-separated wss://host:port).
Some Namecoin records hit the 520-byte per-name limit and delegate
shared blocks (most commonly the nostr.names map) into a sibling name
via an "import" key on the JSON value, per ifa-0001 §"import". Without
import-chain handling, the resolver sees the apex value, finds no
nostr field, and gives up — never consulting the imported sibling
that actually carries the names block.

The canonical demo target testls.bit uses exactly this pattern: the
apex d/testls is essentially `{ "import": "dd/testls", "ip": "..." }`
and dd/testls holds the nostr.names block. With this commit both
`testls.bit` and `m@testls.bit` resolve correctly.

Adds a new module src/services/namecoin-import.ts implementing
expandImports(root, fetcher, maxDepth=4). It:

  - returns root unchanged when there is no `import` key (zero extra
    I/O for the common case);
  - accepts all four shorthand forms of the `import` value
    ("d/foo" | ["d/foo"] | ["d/foo","sel"] | [["d/foo","sel"],…]);
  - walks the imported value via the DNS-dotted Subdomain Selector
    using the ifa-0001 §"map" rules (exact > "*" > "");
  - merges with importer-wins semantics; null in the importer
    suppresses the imported counterpart;
  - recurses to depth 4 by default with cycle protection on
    (name,selector) pairs;
  - treats lookup failures (missing name, malformed JSON, transient
    network error) as the empty object so a flaky ElectrumX shard
    cannot nuke an otherwise resolvable record.

resolveNamecoinHostname now expands imports between fetching the
apex value and extracting the nostr field, using the same ElectrumX
server-fallback path for sibling lookups. The synchronous
resolveFromNamecoinValue helper is preserved for callers that have
the value in hand and do not need import-chain support; a new async
resolveFromNamecoinValueAsync(rawValue, fetcher) wraps the import
expansion for tests and future callers.

20 new hermetic tests cover the unit-level resolver (shorthand
parsing, importer-wins, null suppression, depth budget, cycles,
lenient I/O, selector walk, wildcard fallback) and integration with
the full Namecoin resolver pipeline (testls.bit-style apex/sibling
split, no-import regression guard, importer wins on nostr.names,
local-names survival when the import target is unreachable).
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.

1 participant