Skip to content

feat: verify .bit (Namecoin) NIP-05 identifiers#161

Draft
mstrofnone wants to merge 2 commits into
fiatjaf:masterfrom
mstrofnone:feat/nip05-namecoin
Draft

feat: verify .bit (Namecoin) NIP-05 identifiers#161
mstrofnone wants to merge 2 commits into
fiatjaf:masterfrom
mstrofnone:feat/nip05-namecoin

Conversation

@mstrofnone
Copy link
Copy Markdown

@mstrofnone mstrofnone commented May 21, 2026

What & why

Today every NIP-05 identifier on a Namecoin .bit domain — e.g. alice@me.bit — renders on njump with a line-through. The SDK's NIP05Valid does HTTPS /.well-known/nostr.json, which doesn't (and can't) resolve under .bit, so the verification check always fails and the most public surface a .bit identity gets — every social-card preview njump renders for nevent/naddr codes shared on Twitter/Mastodon/HN — actively signals "broken" instead of "verified".

This PR adds a parallel verification path for .bit identifiers so they render with the correct green check when they really do point at the profile's pubkey on the Namecoin blockchain.

Tracking NIP: nostr-protocol/nips#2349.

How

  • New nip05_namecoin.go adds:
    • verifyNip05Namecoin(ctx, id, pubkey) (verified, ok bool)ok=false means "not a .bit, defer to DNS path"; otherwise resolves the name on Namecoin via ElectrumX (5s timeout, same budget as the SDK's DNS path) and checks pp.PublicKey == pubkey.
    • isNip05Valid(ctx, m) — single entry point used by profile.templ: branches to Namecoin for .bit, falls back to m.NIP05Valid(ctx) otherwise.
    • nip05IdentifierURL(identifier) / nip05NamecoinIdentifierToURL(identifier) — for .bit, links to https://explorer.namecoin.org/name/d/<name> (a sovereign explorer) instead of the dead HTTPS .well-known URL the default NIP-05 helper produces.
  • render_event.go: when the path code isn't valid bech32 and isn't a DNS NIP-05, also try namecoin.IsDotBit before 404'ing, so /alice@me.bit routes to renderProfile.
  • profile.templ: swap the strike-through guard and the explorer link to the new helpers.

No upstream fiatjaf.com/nostr / sdk changes required — this is purely an additive verification path inside njump.

Library

Uses github.com/mstrofnone/nostrlib-nip05-namecoin — a pure-Go port of the Kotlin reference implementation in Amethyst (vitorpamplona/amethyst#1937) and the Swift port in Nostur (nostur-com#60). It speaks ElectrumX over WSS with pinned certs for the default servers; the public API (IsValidIdentifier, IsDotBit, QueryIdentifier returning *nostr.ProfilePointer) intentionally mirrors fiatjaf.com/nostr/nip05 so it slots in as a drop-in fall-through.

Update — import chain support (v0.3.0)

The underlying library was extended with ifa-0001 §"import" support and njump now picks it up via the v0.3.0 bump.

Real-world .bit deployments hit Namecoin's 520-byte per-name limit and split their records across two names: the apex d/<name> stays small and delegates shared blocks (NIP-05 names, relays, TLS, map entries) into a sibling — by convention dd/<name> — via an "import" item. Without import-chain handling, NIP-05 lookups for these records silently fail: the resolver sees the apex value, finds no nostr field, and returns nothing — never consulting the imported sibling that actually carries the nostr.names block.

The library now follows the import item per ifa-0001: importer-wins precedence (with JSON null as a suppression marker), recursive merge up to four levels deep (the spec minimum), visited-set cycle protection, multi-label subdomain selectors resolved through the imported value's map tree, the three accepted shorthand value forms ("d/foo", ["d/foo"], ["d/foo","sel"]), and lenient handling of failed imports so transient ElectrumX hiccups don't kill an otherwise-resolvable record. Records without an import key short-circuit immediately and pay no extra I/O.

The canonical demo target testls.bit now resolves end-to-end: d/testls carries "import":"dd/testls" and the imported dd/testls carries the nostr.names block; njump correctly identifies and verifies the identity.

One new direct dep; go mod tidy is clean.

Tests

go test -run 'Nip05Namecoin|VerifyNip05|IsNip05' ./... — 12 cases pass (table-driven URL mapping, .bit vs DNS branch selection, match/mismatch/error against an injected resolver). The Namecoin resolver is unit-tested via a package-level queryNamecoinIdentifier indirection so the suite stays hermetic; live integration runs are gated behind NJUMP_NAMECOIN_INTEGRATION=1 in the upstream library itself.

The import-chain logic is covered by 22 new unit/integration tests in nostrlib-nip05-namecoin (no-import passthrough, all four shorthand forms, importer precedence, null suppression, depth-4 happy path, over-budget truncation, lookup-nil/error/panic, malformed JSON, malformed import value, A→B→A cycle protection, multi-label selector descending in DNS order, wildcard and empty-key fallbacks, testls.bit-style bare and named NIP-05 resolution across imports, regression guard that no-import records issue zero extra lookups, importer-wins on nested nostr.names).

go build ./... is green.

(The unrelated opengraph_test.go tests fail on master for me too — not touched here.)

Diff size

Two commits on this branch: the original .bit verification path, plus the nostrlib-nip05-namecoin v0.2.0 → v0.3.0 bump for import-chain support. No vendoring, no replace directives — Go MVS happily picks njump's newer fiatjaf.com/nostr and the ProfilePointer types unify cleanly.

Marking draft for review of the rendering / link policy before flipping to ready.

…upport per ifa-0001)

The underlying `nostrlib-nip05-namecoin` library now follows the
ifa-0001 §"import" item of a Namecoin Domain Name Object, recursively
merging values from imported sibling names (typically `dd/<name>")
into the importing object before the `nostr" field is read.

This unblocks NIP-05 verification for `.bit" domains that use the
import pattern to stay under Namecoin's 520-byte per-name limit —
notably `testls.bit", whose apex `d/testls" delegates its
`nostr.names" block to `dd/testls". Previously the resolver saw no
`nostr" field on the apex and gave up, so njump rendered such
identifiers with the strike-through verified-failure indicator.

No njump code changes: `QueryIdentifier" continues to return a
`*nostr.ProfilePointer", and the import expansion happens entirely
inside the library on the resolver path. Records that don't use
`import" pay no extra I/O.
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