Skip to content

Add Namecoin .bit support to NIP-05 resolution#123

Open
mstrofnone wants to merge 8 commits into
fiatjaf:masterfrom
mstrofnone:feat/namecoin-nip05
Open

Add Namecoin .bit support to NIP-05 resolution#123
mstrofnone wants to merge 8 commits into
fiatjaf:masterfrom
mstrofnone:feat/namecoin-nip05

Conversation

@mstrofnone
Copy link
Copy Markdown

@mstrofnone mstrofnone commented Apr 19, 2026

Teach nak to resolve Namecoin .bit NIP-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 .bit identifier 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 nostr field out of its
JSON value.

What changes

  • New self-contained package nip05nmc/. Pure Go, stdlib-only
    (plus the fiatjaf.com/nostr types that nak already pulls in
    for nostr.PubKey / nostr.ProfilePointer). No new dependencies.

  • Four intercept points in existing files (helpers.go,
    fetch.go, decode.go), each gated on
    nip05nmc.IsDotBit(input) before falling through to
    nip05.IsValidIdentifier. Diff is minimal:

     README.md | 12 ++++
     decode.go |  3 +-
     fetch.go  |  4 +-
     go.sum    |  2 -
     helpers.go| 33 ++++++-
    
  • Pinned TLS trust for the two public ElectrumX servers
    (electrumx.testls.space, nmc2.bitcoins.sk). Both serve
    self-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 real
    server and assert two known-good fixtures.

How it works

IsDotBit(input) accepts example.bit, alice@example.bit,
d/example and id/alice. A matching input is translated to a
canonical d/<domain> (or id/<name>) and the package builds the
Namecoin name-index script, derives the Electrum scripthash
(reversed-SHA-256), and queries blockchain.scripthash.get_history
over a TCP+TLS socket. It fetches the latest NAME_UPDATE transaction,
parses the name value out of the output script, and extracts the
nostr field. Definitive blockchain answers (NameNotFound /
NameExpired) short-circuit the server-fallback loop.

Example

$ nak fetch nostr:m@testls.bit | jq -r .pubkey | head -1
6cdebccabda1dfa058ab85352a79509b592b2bdfa0370325e28ec1cb4f18667d

nak decode testls.bit and nak event --tag p=m@testls.bit work
the 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 .bit
and shells out to nak is also being published — so users get the
feature regardless.

— mstrofnone


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

Added nip05nmc/import.go + nip05nmc/import_test.go so the resolver
walks the ifa-0001 §"import"
chain before extracting the nostr field. Distribution choice: inline
port
, not a library dependency — nip05nmc/ is an own implementation
(no upstream nostrlib-nip05-namecoin import in go.mod), so dropping
a 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.bit keep their
nostr.names block in a sibling name (dd/testls) and link to it
via "import": "dd/testls" on the apex. Without import support both
the bare lookup (testls.bit) and the named local-part lookup
(m@testls.bit) silently fail: the resolver sees the apex value,
finds no nostr field, and returns nil. The 520-byte per-name
ceiling on Namecoin makes this delegation pattern common; ifa-0001
formalises it.

Behaviour

  • Trigger. Records without an import key incur zero extra I/O
    — the expander short-circuits on the root JSON object.
  • Shorthand forms. All four ifa-0001 shapes are accepted:
    "d/foo", ["d/foo"], ["d/foo","selector"], and the canonical
    [["d/foo","sel"], ...].
  • Selector walk follows ifa-0001 §"map" rules: exact label wins,
    then * wildcard, then "" default, walked right-to-left
    (rightmost DNS label first).
  • Importer-wins merge, recursive on nested objects. JSON null
    in the importer suppresses the imported key (semantic delete).
  • Recursion + cycle protection. Default depth budget is 4 (the
    spec minimum). Visited (name, selector) pairs are tracked per
    top-level call so A → B → A cycles terminate. When the budget
    is exhausted the partial merge still applies — the importer's own
    fields are never lost.
  • Lenient I/O. Missing names, malformed JSON, and panicking
    lookups all collapse to {} so transient ElectrumX hiccups don't
    nuke an otherwise resolvable record.

Wiring

QueryIdentifier is refactored to take a nameValueLookup callback
internally (the public signature is unchanged). The new
queryIdentifierWithLookup makes the resolver unit-testable without
a live ElectrumX — used by 6 integration tests in import_test.go
that exercise the apex+sibling pattern end-to-end.

Test coverage

22 new tests (16 unit + 6 integration), all passing:

$ go test ./nip05nmc/...
ok      github.com/fiatjaf/nak/nip05nmc    1.0s

go vet ./nip05nmc/... is clean.

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>
@fiatjaf
Copy link
Copy Markdown
Owner

fiatjaf commented Apr 19, 2026

This should go on https://viewsource.win/fiatjaf.com/nostrlib instead, maybe as a subpackage of nip05.

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>
@mstrofnone
Copy link
Copy Markdown
Author

Done. Published the subpackage three ways so you can pick what works for you:

  • GitHub mirror (drop-in shape for fiatjaf.com/nostr/nip05/namecoin):
    https://github.com/mstrofnone/nostrlib-nip05-namecoin
    — a namecoin/ dir that drops directly into nip05/ in nostrlib. Builds clean against fiatjaf.com/nostr@v0.0.0-20260416191442-f50b7b0f8dcb (the version nak currently pins). See PROPOSAL.md for the pitch and tradeoffs.
  • NIP-34 git patch event (kind:1617) against nostrlib, parent commit f50b7b0f8dcb, addressed to your npub:
    nostr:nevent1qqs8yepuma694y6saym8ny4wtfzymlgu2htusvweyjzpamc0rgn8pyqpz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3qgvv9ahktvavf9qjtrgm62le7gplmmchd5usp5wpfhr85hf79kncq8jdtz6
    Accepted by relay.ngit.dev, gitnostr.com, relay.damus.io, nos.lol, relay.primal.net, nostr.mom.
  • Standalone wrapper for users who want this today without touching nak or nostrlib:
    https://github.com/mstrofnone/nak-nmc

API mirrors nip05.QueryIdentifier so if you decide to land it as nip05/namecoin it's a straight drop-in. Happy to reshape — rename ResolveToJSONValueFetch, drop the pinned certs for a caller-supplied servers API, or split into a separate nip05namecoinclient package — whatever you'd prefer.

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

2 participants