Add Namecoin .bit support to NIP-05 resolution#123
Open
mstrofnone wants to merge 8 commits into
Open
Conversation
Introduce a new, self-contained package that speaks the line-delimited JSON-RPC 2.0 dialect of the Electrum protocol against Namecoin ElectrumX servers. The client performs a scripthash-based name_show: build the canonical name-index script, compute its reversed-SHA-256 scripthash, request transaction history, then fetch and parse the latest NAME_UPDATE output. Supports sequential fallback across the three public Namecoin ElectrumX servers and surfaces definitive blockchain answers (NameNotFound, NameExpired) separately from transport failures so callers can react differently. Ported 1:1 from the Kotlin reference in Amethyst (vitorpamplona/amethyst PR #1937), which is running in production on Android and iOS (Nostur PR fiatjaf#60). Signed-off-by: mstrofnone <mstrofnone@users.noreply.github.com>
Bitcoin-style push-data encoding plus a NAME_UPDATE script parser for the Namecoin opcodes (OP_NAME_UPDATE=0x53, OP_2DROP=0x6d, OP_DROP=0x75, OP_RETURN=0x6a). Handles OP_PUSHDATA1/2/4 and short direct pushes. Also adds the public entry points IsDotBit and QueryIdentifier whose signatures mirror fiatjaf.com/nostr/nip05 so the package can be used as a drop-in fall-through. Recognises the four accepted input shapes: example.bit, alice@example.bit, d/example, id/alice. Pulls the `nostr` field out of the resolved value JSON, supporting both the simple (`"nostr":"hex"`) and extended (`names`/`relays`) forms. Signed-off-by: mstrofnone <mstrofnone@users.noreply.github.com>
The public Namecoin ElectrumX ecosystem serves self-signed certs — neither electrumx.testls.space:50002 nor nmc2.bitcoins.sk:57002 chain to a system root. Pin their PEM certs verbatim from the Kotlin reference and build a custom tls.Config that accepts a peer cert if EITHER the system trust store OR the pinned pool OR a raw SHA-256 fingerprint match validates it. This deliberately keeps the bar higher than the common `InsecureSkipVerify + trust-all` shortcut, while still working with operator cert rotation inside the pinned set. Stdlib-only — crypto/tls, crypto/x509, crypto/sha256, encoding/pem. Signed-off-by: mstrofnone <mstrofnone@users.noreply.github.com>
Four intercept points, all gated on nip05nmc.IsDotBit(input):
- helpers.go parsePubKey: check dot-bit before the existing
nip05.IsValidIdentifier branch.
- helpers.go decodeTagValue (letter == 'p'): same.
- fetch.go: try resolveNip05OrDotBit() which wraps both, so one
branch handles both identifier families.
- decode.go: identical resolveNip05OrDotBit() fall-through.
The Namecoin path uses a longer timeout (30s) than the 3s used for
HTTPS NIP-05 because it makes four round-trips over TLS — roughly
2s observed against electrumx.testls.space in practice.
Zero new third-party dependencies.
Signed-off-by: mstrofnone <mstrofnone@users.noreply.github.com>
Add a short example under the other fetch examples showing a Namecoin identifier being resolved end-to-end. Signed-off-by: mstrofnone <mstrofnone@users.noreply.github.com>
Unit tests cover:
- IsDotBit / parseIdentifier shape acceptance
- name-index script construction (known hex vector for "d/testls")
- Electrum scripthash computation (verified against sha256 of empty)
- push-data round-trip for direct push / OP_PUSHDATA1 / OP_PUSHDATA2
- NAME_UPDATE output script parsing
- All value-JSON shapes (simple, extended names/relays, id/ object)
- Pinned cert PEM parse sanity — guards against accidental paste
corruption.
Integration tests (`go test -tags=integration`) hit
electrumx.testls.space:50002 and assert the two known-good fixtures
M captured across amethyst / nostur:
testls.bit → 460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c
m@testls.bit → 6cdebccabda1dfa058ab85352a79509b592b2bdfa0370325e28ec1cb4f18667d
Signed-off-by: mstrofnone <mstrofnone@users.noreply.github.com>
`nak fetch nostr:m@testls.bit` was being rewritten with a local-part of "nostr:m" because the leading URI scheme was left intact. Strip it in IsDotBit and parseIdentifier so the local-part matches the Namecoin name correctly and fetch returns the right profile event. Signed-off-by: mstrofnone <mstrofnone@users.noreply.github.com>
Owner
|
This should go on https://viewsource.win/fiatjaf.com/nostrlib instead, maybe as a subpackage of But I'll have to think about it first. |
mstrofnone
added a commit
to mstrofnone/nostrlib-nip05-namecoin
that referenced
this pull request
Apr 19, 2026
Proposed drop-in subpackage that resolves Namecoin .bit NIP-05 identifiers via ElectrumX. API mirrors nip05.QueryIdentifier so it slots into existing call sites. Ported from the Kotlin reference in Amethyst and the Swift port in Nostur. Context: fiatjaf/nak#123 — fiatjaf suggested this belongs in nostrlib. nostrlib lives on a publish-only git server, so this GitHub mirror is the drop-in shape staged for review. Refs: fiatjaf/nak#123 Signed-off-by: mstrofnone <mstrofnone@users.noreply.github.com>
Author
|
Done. Published the subpackage three ways so you can pick what works for you:
API mirrors I'll close this PR if/when the subpackage lands in nostrlib — or now, if you'd rather track discussion over there. |
mstrofnone
added a commit
to mstrofnone/nips
that referenced
this pull request
May 3, 2026
…dance + browser implementations Driven by review of CodyTseng/jumble#774 (browser-resident wss:// ElectrumX transport) and fiatjaf/nak#123 (transport- and server-agnostic library API shape). B1 changes ---------- - ElectrumX over **WebSocket** (ws://, wss://) added as a first-class transport alongside ElectrumX over TCP+TLS and local Namecoin Core RPC. Same JSON-RPC method set, same wire JSON, just newline-framed inside WebSocket messages. - Browsers cannot open raw TCP, so this transport is the only one available to browser-resident clients (Jumble, noStrudel, Iris, Snort, Coracle, ...). Calling that out explicitly removes a silent omission in the spec. - Mixed-content guidance: https:// pages MUST default to wss://; implementations that auto-pick a server SHOULD filter their server list by page scheme rather than failing closed. - Web Crypto subtle.digest is the recommended scripthash primitive in browsers; reversed-byte-order rule restated in case readers are porting from a TCP implementation. - Server authenticity in browsers: cannot pin SHA-256 fingerprints (the validated cert isn't exposed to JS), so browsers SHOULD auto-select only servers with publicly-rooted certs. Native clients keep the pinned-self-signed path. - Server set is now described as **caller-configurable** rather than 'should ship defaults' \u2014 the spec hands the choice of server list (and transport) to the integrator, matching fiatjaf's nak#123 feedback. - Reference deployments section calls out namebrow.se as a no-Namecoin-software preview path (used in the jumble#774 review thread to demo testls.bit). README changes -------------- - Added Jumble #774, noStrudel #352, Iris, Snort, Coracle to the reference implementations list. - Folded fiatjaf/nak#123 + nostrlib-nip05-namecoin together with the NIP-34 patch event publication path; nak-nmc is now positioned as a standalone wrapper rather than the canonical implementation. - New 'API-shape guidance for libraries' section codifying the transport-agnostic, caller-supplies-servers pattern fiatjaf called for in the nak#123 discussion: the lookup function shouldn't bake in a server list or a transport; pinned TLS material belongs in an optional plug-in, not the core API.
Records like testls.bit delegate their nostr.names block into a
sibling name (typically dd/<name>) via an "import" key on the apex
JSON value, per ifa-0001 §"import". Without import-chain handling
the resolver sees the apex value, finds no nostr field, and returns
null — never consulting the imported sibling that actually carries
the names map.
This change adds an expandImports pass between the apex fetch and
the nostr extractor:
- Four shorthand forms for the import value:
"d/foo" / ["d/foo"] / ["d/foo","sel"] / [["d/foo","sel"], ...]
- Selector walk on the imported value follows ifa-0001 §"map"
(exact label > "*" wildcard > "" default, right-to-left).
- Importer-wins merge, recursive on nested objects. JSON null in
the importer suppresses the imported key (semantic delete).
- Recursion budget defaults to 4 (the spec minimum) with cycle
protection on (name, selector) pairs.
- Lenient I/O: missing names, malformed JSON, or panicking lookups
collapse to {} so transient ElectrumX hiccups never nuke an
otherwise resolvable record.
- Non-import records pay zero extra I/O — the expander short-
circuits on root["import"] not present.
QueryIdentifier is refactored to take a nameValueLookup callback
internally; the public signature is unchanged, but the new
queryIdentifierWithLookup makes the resolver fully unit-testable
without a live ElectrumX.
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.
Teach
nakto resolve Namecoin.bitNIP-05 identifiers.Motivation
NIP-05 verification relies on DNS + HTTPS today. That's a known
censorship surface — any DNS registrar or CA in the chain can
unilaterally break a handle. Namecoin is a decentralised alternative
that has been around since 2011 and settles to its own blockchain,
so a
.bitidentifier is owned by whoever holds the private key,full stop.
This pattern already ships in production on Amethyst (Android) —
vitorpamplona/amethyst#1937.
It uses the exact same protocol this PR ports: read the
d/<name>Namecoin record via ElectrumX, pull the
nostrfield out of itsJSON value.
What changes
New self-contained package
nip05nmc/. Pure Go, stdlib-only(plus the
fiatjaf.com/nostrtypes thatnakalready pulls infor
nostr.PubKey/nostr.ProfilePointer). No new dependencies.Four intercept points in existing files (
helpers.go,fetch.go,decode.go), each gated onnip05nmc.IsDotBit(input)before falling through tonip05.IsValidIdentifier. Diff is minimal:Pinned TLS trust for the two public ElectrumX servers
(
electrumx.testls.space,nmc2.bitcoins.sk). Both serveself-signed certs. The TLS config tries the system trust store
first, then the pinned pool, then a raw SHA-256 fingerprint —
it is deliberately NOT a trust-all.
Unit and integration tests. Unit tests cover script building,
Electrum scripthash derivation, NAME_UPDATE parsing, and value-JSON
shape handling. Integration tests (
-tags=integration) hit a realserver and assert two known-good fixtures.
How it works
IsDotBit(input)acceptsexample.bit,alice@example.bit,d/exampleandid/alice. A matching input is translated to acanonical
d/<domain>(orid/<name>) and the package builds theNamecoin name-index script, derives the Electrum scripthash
(reversed-SHA-256), and queries
blockchain.scripthash.get_historyover a TCP+TLS socket. It fetches the latest NAME_UPDATE transaction,
parses the name value out of the output script, and extracts the
nostrfield. Definitive blockchain answers (NameNotFound /NameExpired) short-circuit the server-fallback loop.
Example
nak decode testls.bitandnak event --tag p=m@testls.bitworkthe same way.
About scope
I know Namecoin is out of left field for nak, and that adding a TLS
client for a sibling blockchain to a Nostr CLI is a real scope
decision — not a trivial one. If you'd rather not carry this, totally
fine: happy to keep it on a fork. I wanted to open the PR primarily
to find out whether it's welcome, and to make the port visible for
anyone else interested.
Either way, a standalone wrapper (
nak-nmc) that resolves.bitand shells out to
nakis also being published — so users get thefeature regardless.
— mstrofnone
Update (2026-05-24): ifa-0001 import-chain support
Added
nip05nmc/import.go+nip05nmc/import_test.goso the resolverwalks the ifa-0001 §"import"
chain before extracting the
nostrfield. Distribution choice: inlineport, not a library dependency —
nip05nmc/is an own implementation(no upstream
nostrlib-nip05-namecoinimport ingo.mod), so droppinga single source-level file in alongside its tests keeps the diff
self-contained and avoids pulling a new module into this repo.
Why this matters
Records like the canonical demo target
testls.bitkeep theirnostr.namesblock in a sibling name (dd/testls) and link to itvia
"import": "dd/testls"on the apex. Without import support boththe bare lookup (
testls.bit) and the named local-part lookup(
m@testls.bit) silently fail: the resolver sees the apex value,finds no
nostrfield, and returnsnil. The 520-byte per-nameceiling on Namecoin makes this delegation pattern common; ifa-0001
formalises it.
Behaviour
importkey incur zero extra I/O— the expander short-circuits on the root JSON object.
"d/foo",["d/foo"],["d/foo","selector"], and the canonical[["d/foo","sel"], ...].then
*wildcard, then""default, walked right-to-left(rightmost DNS label first).
nullin the importer suppresses the imported key (semantic delete).
spec minimum). Visited
(name, selector)pairs are tracked pertop-level call so
A → B → Acycles terminate. When the budgetis exhausted the partial merge still applies — the importer's own
fields are never lost.
lookups all collapse to
{}so transient ElectrumX hiccups don'tnuke an otherwise resolvable record.
Wiring
QueryIdentifieris refactored to take anameValueLookupcallbackinternally (the public signature is unchanged). The new
queryIdentifierWithLookupmakes the resolver unit-testable withouta live ElectrumX — used by 6 integration tests in
import_test.gothat exercise the apex+sibling pattern end-to-end.
Test coverage
22 new tests (16 unit + 6 integration), all passing:
go vet ./nip05nmc/...is clean.