Skip to content

feat: Namecoin (.bit) NIP-05 resolution#1779

Open
mstrofnone wants to merge 2 commits into
planetary-social:mainfrom
mstrofnone:feat/namecoin-bit-nip05
Open

feat: Namecoin (.bit) NIP-05 resolution#1779
mstrofnone wants to merge 2 commits into
planetary-social:mainfrom
mstrofnone:feat/namecoin-bit-nip05

Conversation

@mstrofnone
Copy link
Copy Markdown

@mstrofnone mstrofnone commented May 19, 2026

Summary

Adds optional Namecoin .bit NIP-05 verification to nos.

Any identifier ending in .bit (or starting with d/ / id/) is
routed through a small ElectrumX client and matched against the
Namecoin chain's published nostr field instead of the standard
/.well-known/nostr.json HTTPS lookup. Everything else falls through
to the existing HTTP path unchanged.

Implementation

Direct port of the resolver from
nostur-com/nostur-ios-public#60. Same wire
format, same ElectrumXClient surface, same suffix-branch hook in the
verify 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 nil even when the chain has the data — a
known bug in several other ports across the ecosystem. The chosen test
fixture (mstrofnone.bit) exercises exactly that code path.

Why

.bit gives Nostr users a DNS-free, censorship-resistant identity
layer. NIP-05 verification is the natural entry point — same UX
(name@example.bit), no UI changes, falls back gracefully if any of
the default ElectrumX servers are unreachable.

Scope

  • One ElectrumXClient (Network.framework TLS + TOFU cert pinning)
  • One name-value parser (d/ and id/ namespaces; both the extended
    nostr.names tree and the short "nostr":"hex" / nostr.pubkey
    forms)
  • One NamecoinService actor singleton + in-memory cache
    (1h TTL, 500 entries)
  • One hook in NamesAPI.verify() — a single if branch in front of
    the existing HTTP flow
  • No UI changes (the existing NIP05View covers it; verified .bit
    identifiers render exactly like verified DNS ones)
  • 17 unit tests in NamecoinResolverTests

Wire 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.bit resolves to npub1gvv9ahktvavf9qjtrgm62le7gplmmchd5usp5wpfhr85hf79kncqj8xchs
  • _@mstrofnone.bit is equivalent
  • Anything that doesn't end in .bit falls through to the existing
    HTTP 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 project
entry per new file).

A full Xcode build needs signing on the reviewer's side; static type
check passes against iphonesimulator SDK. CI should be a clean run.

Update (2026-05-24): ifa-0001 import-chain support

Follow-up commit 11609401 adds full support for the ifa-0001 §"import"
chain so that real-world .bit records like testls.bit resolve
end-to-end.

Why this is needed

Namecoin imposes a 520-byte limit on each name's value. Apex records
that carry a nostr.names directory (and any other shared blocks)
quickly run up against that limit, so the ifa-0001
spec

defines an import key that lets the apex delegate shared sections
into a sibling name — conventionally dd/<name>. The canonical demo
target testls.bit uses this pattern: d/testls carries only
{"import":"dd/testls", ...} and the nostr.names block lives at
dd/testls. Without import handling the resolver sees no nostr
field at the apex and returns nil — the lookup silently fails.

What the new resolver does

NamecoinImportResolver.expandImports is called once between the
JSON parse and the existing nostr field extraction in both
performLookup and performLookupDetailed. Records without an
import key short-circuit immediately and pay zero extra
ElectrumX I/O
.

For records that do declare an import, the resolver implements the
full ifa-0001 §"import" semantics:

  • All four value shapes the spec defines: canonical
    [["d/foo","sel"], ...] plus the three shorthand forms
    ("d/foo", ["d/foo"], ["d/foo","sel"]) that show up in
    real-world records.
  • Importer-wins merge. Keys in the importing object replace the
    imported value; a JSON null in the importer suppresses the
    imported key (semantic suppression per spec).
  • Recursion depth 4 (the spec minimum) with a visited
    (name, selector) set so cycles like d/a → d/b → d/a terminate.
  • Subdomain selector resolved against the imported value's map
    tree using exact-label > * wildcard > "" default at each level,
    walked DNS-right-to-left.
  • Lenient I/O. Lookup returns nil, malformed JSON, malformed
    import value → treated as {}. The importer's own fields still
    apply, so transient ElectrumX hiccups can't nuke an otherwise
    resolvable record.
  • The merged import key is stripped before the extractor sees the
    object.

Tests

NosTests/Service/NamecoinImportTests.swift adds 20 hermetic tests
(no network, in-memory ElectrumX double):

  • 16 pure unit tests on the resolver: passthrough on non-import
    records, each shorthand form, importer-wins, null suppression,
    4-level recursion, depth-budget truncation, lenient lookup failure,
    malformed JSON, malformed import value, cycle protection,
    multi-label selector descent, wildcard fallback, etc.
  • 4 integration tests through NamecoinResolver covering the
    testls.bit pattern, resolveDetailed returning .success and
    .noNostrField across an import, and a regression guard asserting
    that 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 new
    files)

The diff is scoped to the import path. Existing public APIs,
extractor behaviour, and caching are unchanged.

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
@mstrofnone mstrofnone requested a review from mplorentz as a code owner May 19, 2026 13:58
@github-actions
Copy link
Copy Markdown

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.
You can retrigger this bot by commenting recheck in this Pull Request

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.
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