feat: resolve .bit hostnames via Namecoin ElectrumX#24
Draft
mstrofnone wants to merge 2 commits into
Draft
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
.bitresolution lets an nsite live at<yourname>.bitwith no DNS or TLS dependency on a traditional registrar — the name is published to the Namecoin blockchain, the gateway looks up the latestNAME_UPDATEvalue, extracts a nostr pubkey, and serves the same site it would have served fromnpub1….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 existingresolvePubkeyFromHostnameflow:The Namecoin path:
.bit, takes the registrable label, prependsd/to build the Namecoin name (alice.bit→d/alice).blockchain.name.show(with a fallback toblockchain.namecoin.name_show)..bitecosystem:{ "pubkey": "<hex>" }{ "npub": "npub1…" }{ "nostr": "<hex>" }(simple NIP-05 form){ "nostr": { "names": { "_": "<hex>", … } } }(extended NIP-05 form, with_-root priority then first valid hex)ResolvedSiteAddresspointing at the root site (kind15128) by default. If the JSON value includes an explicit"nsite": "<identifier>"hint, returns a named-site address (kind35128).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 nonostrfield, and gives up — never consulting the imported sibling that actually carries thenostr.namesblock.This is the canonical demo target's setup.
d/testls(testls.bit) is essentially:{ "import": "dd/testls", "ip": "107.152.38.155" }…and
dd/testlsholds the actualnostr.namesblock. With this PR bothtestls.bitand named identities likem@testls.bitresolve correctly through the import.A new module
src/services/namecoin-import.tsimplementsexpandImports(root, fetcher, maxDepth=4). It:importkey (non-import records pay zero extra I/O);importvalue ("d/foo",["d/foo"],["d/foo","sel"],[["d/foo","sel"],…]);*wildcard >""default);nullin the importer suppresses the imported counterpart per spec;(name,selector)pairs;The expansion runs between fetching the apex value and extracting the
nostrfield; 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
npub/ snapshot / canonical-label / CNAME resolution paths are untouched..bit. Every other hostname follows the previous code path bit-for-bit.importkey follow the original code path with zero extra ElectrumX queries (regression-tested).WebSocketand JSON are sufficient.Trust model
wss://nmc2.bitcoins.sk:57004,wss://electrumx.testls.space:50004.NSITE_NAMECOIN_ELECTRUMX_SERVERS(comma-separatedwss://host:port).Future work
ws://localhost:50004). The current code already accepts anywss://URL viaNSITE_NAMECOIN_ELECTRUMX_SERVERS, butws://localhost support and a curated fallback chain are out of scope for this PR.nsite.mdto document the resolver shape, once this lands and the wire format settles.Testing
Locally on Deno 2.7.14:
deno fmt --check— passes on all changed files. Two unrelated files (CHANGELOG.md,deno.json) faildeno fmt --checkonmasteralready; left untouched.deno check main.ts— passes.deno test— 41 passed, 0 failed, 1 ignored. The ignored test is the live-network integration test, gated behindNSITE_NAMECOIN_INTEGRATION=1.Unit tests cover:
.bithostname detection (case, trailing dot, multi-label).nsiteidentifier hint handling.ResolvedSiteAddressshape.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.testls.bit-style apex/sibling split resolves both bare and named NIP-05 identities; no-import regression guard (records withoutimportmake zero extra ElectrumX queries); importer wins onnostr.names; resolution survives an unreachable import target when the importer has its own names.