feat: Namecoin (.bit) NIP-05 resolution#1779
Open
mstrofnone wants to merge 2 commits into
Open
Conversation
Adds optional Namecoin .bit verification to the existing NIP-05 pipeline.
Resolution is wire-compatible with Amethyst (Kotlin), dart-nostr, and
the Nostur Swift port, sharing the same ElectrumX scripthash flow and
ifa-0001 record parser.
The hook is a single suffix branch in NamesAPI.verify(): any identifier
matching .bit / d/ / id/ is routed through NamecoinService.resolve()
which speaks JSON-RPC over TLS to a small default set of public
ElectrumX servers and parses the latest tx output's name script.
One small fix over the existing Swift template: parseNameScript also
accepts OP_NAME_FIRSTUPDATE (0x52), not just OP_NAME_UPDATE (0x53).
Names still in their first-update window otherwise resolve to nil even
when the chain has the data. The chosen test fixture (mstrofnone.bit)
exercises exactly that code path.
- One ElectrumX client (Network.framework TLS, TOFU cert pin)
- One name-value parser (domain + identity namespaces, both
extended 'nostr.names' tree and short 'nostr':'hex' / 'nostr.pubkey')
- One service singleton + in-memory cache (1h TTL, 500 entries)
- One hook in NamesAPI.verify() suffix branch
- 17 unit tests covering identifier parsing, JSON extraction, script
parsing for both UPDATE and FIRSTUPDATE, and end-to-end with a
mock ElectrumX client
|
CLA Assistant Lite bot: Thank you for your submission -- we really appreciate it! We'd love it if you'd sign our Contributor License Agreement so we can accept your contribution. You can sign the CLA by copying the text below, pasting it in the Add a comment text field at the bottom of this pull request, and clicking the Comment button to post it. I have read the CLA Document and I hereby sign the CLA mstrofnone seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. |
Namecoin's 520-byte per-name limit forces records like testls.bit to
delegate their nostr.names directory into a sibling name via the
ifa-0001 "import" key. Without import-chain handling, NIP-05 lookups
against those records silently fail: the resolver sees the apex value,
finds no nostr field, and returns nil.
This adds NamecoinImportResolver, which recursively merges imported
records into the importing one before the existing extractor runs.
Implements ifa-0001 §"import":
- Canonical array-of-arrays plus the three shorthand value forms
("d/foo", ["d/foo"], ["d/foo","sel"]) that real-world records use.
- Importer-wins merge, with JSON null in the importer suppressing the
imported counterpart (semantic suppression per spec).
- Recursion depth of 4 (the spec minimum) with cycle protection via a
visited (name, selector) set.
- Subdomain selector resolved via the imported value's "map" tree
using exact-label > "*" wildcard > "" default at each level.
- Lenient I/O: a failed lookup, malformed JSON, or malformed import
value is treated as the empty object, so transient ElectrumX
failures do not nuke an otherwise resolvable record.
- Records without an "import" key skip the expander entirely and pay
zero extra ElectrumX I/O cost.
NamecoinResolver wires expandImports into both performLookup and
performLookupDetailed, between the JSON parse and the nostr field
extraction. The fetcher reuses the same IElectrumXClient instance and
server list as the parent query.
Adds NamecoinImportTests (20 hermetic tests, no network): 16 pure-unit
tests covering each shorthand form, importer-wins, null suppression,
depth-4 recursion, depth budget truncation, lenient lookup failure,
malformed JSON, malformed import value, cycle protection, multi-label
selector descent, and wildcard fallback; plus 4 integration tests
through NamecoinResolver covering the testls.bit pattern, detailed
outcome success and noNostrField paths, and the zero-extra-I/O
guarantee for non-import records.
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.
Summary
Adds optional Namecoin .bit NIP-05 verification to nos.
Any identifier ending in
.bit(or starting withd//id/) isrouted through a small ElectrumX client and matched against the
Namecoin chain's published
nostrfield instead of the standard/.well-known/nostr.jsonHTTPS lookup. Everything else falls throughto the existing HTTP path unchanged.
Implementation
Direct port of the resolver from
nostur-com/nostur-ios-public#60. Same wire
format, same
ElectrumXClientsurface, same suffix-branch hook in theverify call site.
One small correctness fix over the Nostur template: the name-script
parser now accepts OP_NAME_FIRSTUPDATE (0x52) in addition to
OP_NAME_UPDATE (0x53). Names that are still in their first-update
window otherwise resolve to
nileven when the chain has the data — aknown bug in several other ports across the ecosystem. The chosen test
fixture (
mstrofnone.bit) exercises exactly that code path.Why
.bitgives Nostr users a DNS-free, censorship-resistant identitylayer. NIP-05 verification is the natural entry point — same UX
(
name@example.bit), no UI changes, falls back gracefully if any ofthe default ElectrumX servers are unreachable.
Scope
ElectrumXClient(Network.framework TLS + TOFU cert pinning)d/andid/namespaces; both the extendednostr.namestree and the short"nostr":"hex"/nostr.pubkeyforms)
NamecoinServiceactor singleton + in-memory cache(1h TTL, 500 entries)
NamesAPI.verify()— a singleifbranch in front ofthe existing HTTP flow
.bitidentifiers render exactly like verified DNS ones)
NamecoinResolverTestsWire format
ifa-0001 + ElectrumX
scripthash.get_history+transaction.get+Namecoin name-script parsing. Same as
Amethyst (Kotlin, merged),
dart-nostr (merged),
Nostur (Swift, in review),
and in-review across the JS ecosystem. See the
N1 NIP draft.
Try it
mstrofnone.bitresolves tonpub1gvv9ahktvavf9qjtrgm62le7gplmmchd5usp5wpfhr85hf79kncqj8xchs_@mstrofnone.bitis equivalent.bitfalls through to the existingHTTP NIP-05 path
The wire path was live-verified against the public Namecoin
ElectrumX network (
nmc2.bitcoins.sk:57002) before opening this PR.Footprint
1421 lines added across 9 files (1106 lines of resolver + 315 lines of
tests; 13 lines of integration in
NamesAPI.swift; one Xcode projectentry per new file).
A full Xcode build needs signing on the reviewer's side; static type
check passes against
iphonesimulatorSDK. CI should be a clean run.Update (2026-05-24): ifa-0001 import-chain support
Follow-up commit
11609401adds full support for the ifa-0001 §"import"chain so that real-world
.bitrecords liketestls.bitresolveend-to-end.
Why this is needed
Namecoin imposes a 520-byte limit on each name's value. Apex records
that carry a
nostr.namesdirectory (and any other shared blocks)quickly run up against that limit, so the ifa-0001
spec
defines an
importkey that lets the apex delegate shared sectionsinto a sibling name — conventionally
dd/<name>. The canonical demotarget
testls.bituses this pattern:d/testlscarries only{"import":"dd/testls", ...}and thenostr.namesblock lives atdd/testls. Without import handling the resolver sees nonostrfield at the apex and returns nil — the lookup silently fails.
What the new resolver does
NamecoinImportResolver.expandImportsis called once between theJSON parse and the existing
nostrfield extraction in bothperformLookupandperformLookupDetailed. Records without animportkey short-circuit immediately and pay zero extraElectrumX I/O.
For records that do declare an import, the resolver implements the
full ifa-0001 §"import" semantics:
[["d/foo","sel"], ...]plus the three shorthand forms(
"d/foo",["d/foo"],["d/foo","sel"]) that show up inreal-world records.
imported value; a JSON
nullin the importer suppresses theimported key (semantic suppression per spec).
(name, selector)set so cycles liked/a → d/b → d/aterminate.maptree using exact-label >
*wildcard >""default at each level,walked DNS-right-to-left.
importvalue → treated as{}. The importer's own fields stillapply, so transient ElectrumX hiccups can't nuke an otherwise
resolvable record.
importkey is stripped before the extractor sees theobject.
Tests
NosTests/Service/NamecoinImportTests.swiftadds 20 hermetic tests(no network, in-memory ElectrumX double):
records, each shorthand form, importer-wins, null suppression,
4-level recursion, depth-budget truncation, lenient lookup failure,
malformed JSON, malformed
importvalue, cycle protection,multi-label selector descent, wildcard fallback, etc.
NamecoinResolvercovering thetestls.bitpattern,resolveDetailedreturning.successand.noNostrFieldacross an import, and a regression guard assertingthat non-import records issue exactly one ElectrumX query.
Files
Nos/Service/Namecoin/NamecoinImportResolver.swift(new)Nos/Service/Namecoin/NamecoinResolver.swift(wiring)NosTests/Service/NamecoinImportTests.swift(new)Nos.xcodeproj/project.pbxproj(target membership for the two newfiles)
The diff is scoped to the import path. Existing public APIs,
extractor behaviour, and caching are unchanged.