Skip to content

feat: opt-in Namecoin (.bit) name whitelist via ElectrumX resolution#126

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

feat: opt-in Namecoin (.bit) name whitelist via ElectrumX resolution#126
mstrofnone wants to merge 2 commits into
barrydeen:masterfrom
mstrofnone:feat/nip05-namecoin

Conversation

@mstrofnone
Copy link
Copy Markdown

@mstrofnone mstrofnone commented May 21, 2026

Why

Sovereign-relay operators increasingly want to publish (and accept) chain-anchored identities that don't depend on DNS. Namecoin (.bit) names are owner-controlled, on-chain, and survive registrar/DNS-provider failure modes. Letting HAVEN whitelist by .bit name means operators can hand out me@example.bit to friends and family and have HAVEN figure out the underlying npub at startup, without anyone having to copy hex around.

What

A small, opt-in addition: a new env var WHITELISTED_NAMECOIN_NAMES_FILE pointing at a JSON array of Namecoin identifiers (me@me.bit, d/example, id/alice, …). At startup, each name is resolved to its underlying Nostr pubkey via public Namecoin ElectrumX servers and merged into the same in-memory whitelist already populated from WHITELISTED_NPUBS_FILE.

  • New pkg/nip05namecoin/ subpackage: identifier parser, ElectrumX TCP/TCP+TLS client with a pinned-cert trust store (no WebSocket — HAVEN runs server-side, raw TCP is always available), Namecoin name-index script + scripthash derivation, and the startup-resolver helper.
  • config.go extension: a WhitelistedNamecoinNames field, a getNamesFromFile helper that mirrors getNpubsFromFile, and a single call to nip05namecoin.ResolveNamesToPubkeys after the existing whitelist is built.
  • .env.example, README.md, and whitelisted_namecoin_names.example.json get documentation/example entries.

No changes to any relay policy. Once resolution finishes, every existing access-control code path (MustBeWhitelistedToPost, MustBeWhitelistedToQuery, etc.) keys off the same WhitelistedPubKeys map and Just Works.

Blast radius

  • Additive. Operators who don't set WHITELISTED_NAMECOIN_NAMES_FILE see zero behavioural change — the resolver is never called.
  • Best-effort. If the file is present but ElectrumX is unreachable, individual name resolution fails or nostr field on-chain is missing, HAVEN logs a warning and keeps starting. The operator can fix the upstream issue and restart at their leisure.
  • No new public APIs on relays. The new package is internal to config-load; relay policies are untouched.
  • No new mandatory dependencies. Uses nbd-wtf/go-nostr (already imported) and the Go standard library.

Spec reference

  • nostr-protocol/nips#2349 — Namecoin extension to NIP-05.
  • Reference implementations cross-checked: rust-nostr/nostr#1367 (Rust) and the Kotlin Amethyst port (used as the source of the pinned cert bundle and scripthash test vector).

Trust model

  • TLS chains are checked against the system trust store first. The pinned bundle and SHA-256 fingerprints are a fallback for the self-signed certs that Namecoin ElectrumX operators have historically served.
  • Pinned certs and the default server list are compile-time constants in pkg/nip05namecoin/servers.go. Operators who prefer to point HAVEN at a local Namecoin daemon's ElectrumX bridge or a private mirror can patch this list in a fork; we're happy to add a runtime override env var if maintainers want one.
  • No private keys, no signing. The package only does name lookups.

Live verification

The d/testls name-index scripthash that public Namecoin ElectrumX servers expose for the long-standing test name is b519574e96740a4b3627674a0708e71a73e654a95117fc828b8e177a0579ab42 — there's a unit test (TestElectrumScriptHashDTestLS) asserting our script + scripthash code reproduces it, which is the same fingerprint Amethyst and the Rust reference implementation use.

Maintenance status caveat

I noticed the README banner says HAVEN is in maintenance mode (bug fixes only). This PR is deliberately framed as a low-impact additive opt-in — no surface-area expansion in policies, no new mandatory dependencies, and zero behaviour change for existing operators. If you'd rather not take any feature work right now, I completely understand; happy to scope this down further (e.g. ship the package without the config wiring), park it as a fork, or close.

Testing

go build ./...                       # clean
go vet ./...                         # clean
go test ./pkg/nip05namecoin/...      # ok
go test ./...                        # ok (no regressions)

Unit tests cover: identifier parsing (incl. NIP-21 nostr: prefix, mixed case, @ shape, d//id/ shape, junk inputs), nostr value extraction (simple form, extended names+relays form, id/ form, fallback-to-root, invalid pubkey rejection), the name-index script encoding, the scripthash for both empty input and the d/testls reference value, OP_PUSHDATA1/2/4 round-trips, NAME_UPDATE script parsing, pinned cert PEM parse, LoadNamesFile (empty/parses/malformed/missing), and ResolveNamesToPubkeys with a mock resolver (success+failure mix, empty pubkey ignored, nil-target safety, no-names short-circuit).

Network-touching integration tests are guarded behind HAVEN_NAMECOIN_INTEGRATION (none shipped in this PR — happy to add some if you'd like).


Update: import-chain support (ifa-0001 §"import")

A second commit on this branch adds support for the ifa-0001 §"import" chain. Real-world Namecoin records — including the long-running testls.bit demo target — push up against the 520-byte per-name limit and split their nostr.names block into a sibling name (typically dd/<name>) via an "import" key on the apex JSON value. Without this, NIP-05 lookups against those records silently fail: the resolver sees the apex value, finds no nostr field, and returns null without ever consulting the imported sibling.

pkg/nip05namecoin/import.go adds an expandImports pass that runs once between "fetched the apex value" and "extract the nostr field". Behaviour follows the spec:

  • Four shorthand forms accepted (bare string, [name], [name, selector], canonical [[name, sel], ...]).
  • Importer-wins merge with JSON-null semantic suppression; nested objects merge recursively, non-objects replace wholesale.
  • Selector walk on the imported value's map tree (exact > "*" > "" per label, DNS rightmost-first).
  • Default depth budget of 4 (the spec minimum). Deeper chains are silently truncated and the importer's own fields still apply.
  • (name|selector) visited-set cycle protection scoped to one top-level expansion.
  • Lenient I/O: a missing / malformed / panicking lookup is treated as {} so transient ElectrumX hiccups don't nuke an otherwise resolvable record.

Zero blast radius for non-import records. A regression guard test pins that records without an "import" key issue exactly one ElectrumX query (the apex name) and never consult any sibling.

Test count added: 22 (16 unit + 6 integration). Reference suite parity: all 20 distinct behaviours from the Kotlin reference (NamecoinImportTest.kt) are covered; some closely-related cases are merged where natural for Go.

go test ./...        # ok, no regressions
go vet ./...         # clean
gofmt -l pkg/...     # clean

Adds a new optional WHITELISTED_NAMECOIN_NAMES_FILE env. On startup,
each .bit / d/ / id/ identifier in the file is resolved to its
underlying Nostr pubkey via public Namecoin ElectrumX servers and
merged into the existing in-memory whitelist (alongside any pubkeys
loaded from WHITELISTED_NPUBS_FILE).

- new pkg/nip05namecoin: parser, ElectrumX client (TCP/TCP+TLS,
  pinned-cert trust store), name-index script + scripthash, and the
  whitelist resolver helper. Unit tests cover the parser, script
  encoding, scripthash derivation (incl. the d/testls reference
  value), pinned-cert parse, and the resolver helper with a mock
  ElectrumX backend.
- config.go: load names file via nip05namecoin.LoadNamesFile, then
  call nip05namecoin.ResolveNamesToPubkeys after the existing
  whitelist is built. Resolution failures log a warning and continue
  so an operator can bring their pod online later.
- .env.example, README.md, whitelisted_namecoin_names.example.json:
  documentation and example config.

Spec: nostr-protocol/nips#2349
The 520-byte per-name limit on Namecoin makes apex records (d/<name>)
crowded enough that real-world deployments delegate shared blocks
(like nostr.names) into a sibling name via an "import" key on the
JSON value. Without import-chain handling, a NIP-05 lookup against
the canonical demo target testls.bit silently fails: the resolver
sees the apex value, finds no nostr field, and returns null — never
consulting the imported sibling dd/testls that actually carries
the names block.

Adds pkg/nip05namecoin/import.go implementing expandImports per
ifa-0001 §"import":

  - four shorthand forms accepted (bare string, single-arr,
    pair-arr-with-selector, canonical array-of-arrays)
  - importer-wins merge with JSON-null semantic suppression
  - recursive descent into nested objects on merge
  - selector walk on the imported value's map tree using
    exact > "*" wildcard > "" default per label (DNS rightmost-first)
  - default depth budget of 4 (spec minimum); deeper chains are
    silently truncated and the importer's own fields still apply
  - (name|selector) visited-set cycle protection scoped to one
    top-level expansion
  - lenient I/O: missing/malformed/panicking lookup is treated as
    {} so transient ElectrumX hiccups can't nuke an otherwise
    resolvable record

Wires the expansion into extractNostrFromValue, and refactors the
QueryIdentifier path so the same code can be exercised hermetically
in tests via a queryIdentifierWithLookup helper that takes an
in-memory lookup. Records without an import key short-circuit and
issue exactly one ElectrumX query (regression guard test included).

Adds 22 tests covering the 20 distinct behaviours pinned by the
reference suite: passthrough, all four shorthand forms, canonical
array, importer-wins, null suppression, depth-4 happy path,
budget-truncation, missing/malformed/panicking lookup, malformed
import value, cycle, multi-label DNS-order selector walk,
wildcard fallback, plus six integration tests through
queryIdentifierWithLookup (bare resolution across import, named
local-part across import, zero-extra-I/O guard, importer-wins
on nostr.names, failed-import-doesn't-break-local-names, and
import-target-lacks-nostr error surfacing).

Spec: https://github.com/namecoin/proposals/blob/master/ifa-0001.md
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