Repository navigation
nostr: add NIP-05 over Namecoin (.bit) module - #1367
Draft
mstrofnone wants to merge 2 commits into
Draft
mstrofnone wants to merge 2 commits into
mstrofnone wants to merge 2 commits into
Conversation
This was referenced May 21, 2026
added 2 commits
November 3, 2026 09:10
Adds nip05namecoin sibling to nip05 that resolves NIP-05 identifiers rooted in the Namecoin blockchain instead of DNS. Mirrors the existing nip05 module's no-transport shape: parse identifiers, derive the ElectrumX scripthash, decode the NAME_UPDATE script returned by `blockchain.transaction.get`, and extract a pubkey + relay list from the resulting name value. The actual ElectrumX call is left to the caller, matching how nip05.rs leaves DNS / HTTP to the caller. Identifiers accepted: - `[email protected]` - `example.bit` (uses the `_` root entry) - `d/example` (domain namespace) - `id/alice` (identity namespace) - A leading `nostr:` NIP-21 prefix is tolerated. Both name-value shapes from the .bit NIP-05 spec are supported: - simple: "nostr": "<hex pubkey>" - extended: "nostr": { "names": {...}, "relays": {...} } The extended-form local-part priority (exact match → "_" root → first valid entry when the caller asked for root) matches the Kotlin reference in Amethyst, the Swift port in Nostur, the Dart port that merged in dart-nostr#44, the Go reference at mstrofnone/nostrlib-nip05-namecoin, and the TypeScript port at nbd-wtf/nostr-tools#533. The module is no_std + alloc compatible — it only depends on serde_json and bitcoin_hashes::sha256, both already in the crate. Live verification: the scripthash this module produces for `d/testls` is b519574e96740a4b3627674a0708e71a73e654a95117fc828b8e177a0579ab42, which Namecoin ElectrumX servers (e.g. nmc2.bitcoins.sk:57002) recognise — `blockchain.scripthash.get_history` against it returns the `d/testls` transaction history. Public API (mirrors nip05.rs): - is_valid_identifier / is_dot_bit - NamecoinAddress::parse / .namecoin_name / .local_part / .is_domain / .electrum_script_hash - Nip05NamecoinProfile::from_json / from_raw_json - verify_from_json / verify_from_raw_json - extract_nostr_from_value (exported for callers that want other fields from the same name value — bitcoin, lightning, http, etc.) - build_name_index_script / electrum_script_hash / parse_name_update_script - DEFAULT_ELECTRUMX_SERVERS Tests: 23 new unit tests covering identifier parsing, both JSON value shapes, malformed-input null paths, the NAME_UPDATE script builder + parser (including OP_PUSHDATA1 framing), and the round-trip scripthash derivation. `cargo test -p nostr --lib` and `cargo test -p nostr --lib --all-features` both pass clean (267 / 356 tests). `cargo clippy -p nostr --lib --all-targets -- -D warnings` is also clean. A `nip05namecoin` example is added under `crates/nostr/examples/` to demonstrate the API.
The 520-byte per-name limit on Namecoin makes apex records crowded.
ifa-0001 §"import" lets a name delegate shared blocks into a sibling
name (typically `dd/<name>`) via an `"import"` key on the JSON value.
Without import-chain handling, NIP-05 lookups against records that
use this pattern silently fail: the resolver sees the apex value,
finds no `nostr` field, and returns `ImpossibleToVerify` — never
consulting the imported sibling that actually carries the
`nostr.names` block. `testls.bit`, the canonical demo target,
delegates its names block to `dd/testls` exactly this way.
Add `expand_imports` plus `Nip05NamecoinProfile::from_json_with_lookup`
/ `from_raw_json_with_lookup` and matching `verify_*_with_lookup`
entry points. The expansion accepts the four `import` value shapes
documented in ifa-0001 (bare string, single-array, pair-array with
selector, canonical array-of-arrays), walks the subdomain selector
against the imported value's `map` tree with exact-label / `*` /
default fallback, merges with importer-wins semantics (including
`null` as a suppression marker), recurses up to the spec-minimum
depth of 4, and breaks cycles with a `(name, selector)` visited set.
I/O is lenient: a `None` / malformed / unreachable import is treated
as the empty object `{}`, so transient ElectrumX hiccups don't kill
an otherwise resolvable record (the importer's own fields still
apply). The non-import code path is unchanged and pays zero extra
cost: records with no `import` key never invoke the lookup closure.
The existing sync API surface stays untouched. Callers that don't
need import resolution keep using `from_json` / `from_raw_json` /
`verify_*`; the new `_with_lookup` variants thread a synchronous
`FnMut(&str) -> Option<String>` between fetch and extract. Keeping
the closure sync mirrors the rest of the module, which owns no
transport surface — the caller drives ElectrumX.
23 new tests cover: shorthand forms, canonical array, importer-wins,
null suppression, depth-4 happy path and depth-1 truncation, lookup
returning None / malformed JSON, malformed `import` value, cycle
protection, multi-label selector descent, wildcard and empty-key
fallback, plus integration cases mirroring the `testls.bit`
deployment (bare resolution, named local-part, importer-wins on
`nostr.names`, failed-import preserves local names, no-import record
issues zero extra lookups, verify across imports).
mstrofnone
force-pushed
the
feat/nip05-namecoin
branch
from
November 2, 2026 22:11
7c7211c to
c461f44
Compare
This branch has not been deployed
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.
Adds a
nip05namecoinsibling tonip05that resolves NIP-05 identifiers rooted in the Namecoin blockchain instead of DNS, mirroring the existingnip05module's no-transport shape so callers can chain the two without reshaping their code.This is the Rust analog of the same module recently ported to several other ecosystems:
nostr-tools: nbd-wtf/nostr-tools#533 (open)fiatjaf.com/nostr/nip05/namecoin)The parser shape, local-part priority (exact →
_→ first valid), and JSON extraction rules match those implementations byte-for-byte.Identifiers accepted
[email protected]example.bit(uses the_root entry)d/example(Namecoin domain namespace)id/alice(Namecoin identity namespace)nostr:NIP-21 prefix is tolerated.Shape
I deliberately mirrored
nip05.rsrather than shipping a transport. The existingnip05.rsmodule exposes:Nip05Address::parse(identifier → URL to fetch)Nip05Profile::from_raw_json(caller-fetched JSON → profile)verify_from_raw_json…and leaves the HTTP
GETto integrators (thenostr-sdkClient, downstream apps, thereqwest-based example etc.). I followed the same convention here:NamecoinAddress::parse(identifier → Namecoin name + local-part + ElectrumX scripthash)Nip05NamecoinProfile::from_raw_json(caller-fetched JSON → profile)verify_from_raw_jsonPlus the Namecoin-specific glue callers need to drive ElectrumX themselves:
build_name_index_script(name)— the NAME_UPDATE-shaped script the ElectrumX fork indexes againstelectrum_script_hash(script)—reverse(sha256(script))hex, the format expected byblockchain.scripthash.get_historyparse_name_update_script(script)— decode the NAME_UPDATE output returned byblockchain.transaction.getand extract(name, value)DEFAULT_ELECTRUMX_SERVERS— the public Namecoin ElectrumX endpoints, matching the Kotlin / Swift / Go referencesThis keeps the module
no_std + alloccompatible (onlyserde_jsonandbitcoin_hashes::sha256, both already crate deps) and leaves transport choices (raw TCP+TLS, WSS, Tor, custom pinned certs) to the caller — same convention as the rest of the crate.If a built-in async ElectrumX client is wanted later, the obvious shape is a sibling crate next to
nostr-blossomrather than pulling tokio/networking intonostr. Happy to follow up with that if it's desired.Value-extraction support
Both name-value shapes used by
.bitNIP-05 in the wild are supported:"nostr": "<64-hex pubkey>""nostr": { "names": {"_": "...", "alice": "..."}, "relays": {"<pubkey>": ["wss://..."]} }ford/names, and"nostr": { "pubkey": "...", "relays": ["wss://..."] }forid/names.extract_nostr_from_valueis also exported so callers can inspect non-nostr fields on the same name value (bitcoin,lightning,http, etc.) without reparsing.Import-chain support (ifa-0001 §
import)The 520-byte per-name limit on Namecoin makes apex records crowded, so ifa-0001 §"import" lets a name delegate shared blocks into a sibling name (typically
dd/<name>) via an"import"key on the JSON value. The canonical demo targettestls.bituses exactly this pattern: the apexd/testlscarries only"import":"dd/testls"plus a couple of small keys, and the actualnostr.namesblock lives atdd/testls. Without import handling, NIP-05 lookups against records like this silently fail.This PR threads import expansion between the apex fetch and field extraction. New entry points alongside the existing API:
Nip05NamecoinProfile::from_json_with_lookup(addr, apex, lookup)/from_raw_json_with_lookup(addr, raw, lookup)verify_from_json_with_lookup/verify_from_raw_json_with_lookupexpand_imports(&mut value, lookup, DEFAULT_IMPORT_DEPTH)for direct useThe
lookupargument is aFnMut(&str) -> Option<String>returning the raw JSON value of an imported name (the caller drives ElectrumX, just like for the apex fetch). Behaviour follows ifa-0001:importvalue shapes: bare string,["d/foo"],["d/foo","sel"], canonical array-of-arrays.maptree in DNS order (rightmost label first) with exact-label >*>""fallback.nullin the importer acting as a suppression marker.(name, selector)set.None, malformed JSON, unreachable name) are absorbed as{}so transient ElectrumX hiccups don't kill an otherwise resolvable record.importkey pay zero extra cost: the lookup closure is never invoked. Existing call sites that usefrom_raw_jsoncontinue to work unchanged.Tests
46 unit tests under
nip05namecoin::testscover:id//pubkey/relayspath._→ first valid) for the extended form.Sha256 → reverse → hex → reverse → Sha256for the scripthash.DEFAULT_ELECTRUMX_SERVERS.importexpansion: all four shorthand shapes, importer-wins,nullsuppression, depth-4 happy path, depth-1 truncation, cycle break, multi-label selector descent, wildcard / empty-key fallback, malformedimportvalue, lookup returningNone/ malformed JSON.testls.bit: bare resolution across import, named local-part across import, importer-wins onnostr.names, failed-import preserves local names, no-import record issues zero extra lookups, verify across imports.Live verification
The scripthash this module produces for
d/testlsisb519574e96740a4b3627674a0708e71a73e654a95117fc828b8e177a0579ab42. A public Namecoin ElectrumX server (nmc2.bitcoins.sk:57002) recognises it —blockchain.scripthash.get_historyagainst that scripthash returns thed/testlstransaction history, with the latest tx hash (e26731fd1de3be8bf29dc48db5174e6bad6b625704f61ffe80287f2e3ebcde96, height 822885) matching the canonicalnamecoin-cli name_show d/testls. So the script-building and scripthash derivation are correct on real on-chain data, not just unit-test fixtures.Public API
Signatures mirror
nip05.rson purpose:is_valid_identifier(input) -> bool/is_dot_bit(input)NamecoinAddress::parse(input) -> Result<NamecoinAddress, Error>.namecoin_name(),.local_part(),.is_domain(),.electrum_script_hash()Nip05NamecoinProfile::from_json/from_raw_jsonNip05NamecoinProfile::from_json_with_lookup/from_raw_json_with_lookup(ifa-0001import)verify_from_json/verify_from_raw_jsonverify_from_json_with_lookup/verify_from_raw_json_with_lookupexpand_imports+DEFAULT_IMPORT_DEPTH(ifa-0001importexpansion building block)extract_nostr_from_value(exported for non-nostr field inspection)build_name_index_script/electrum_script_hash/parse_name_update_scriptDEFAULT_ELECTRUMX_SERVERS: &[ElectrumxServer]Out of scope / follow-ups
nip05.rsto auto-delegate. Keeping the two decoupled means callers who don't care about.bitpay nothing, and the three-line chain at the call site is clearer than implicit fall-through.queryhelper (analogous tonostr-sdk's NIP-05 fetcher) belongs in a sibling crate; I'm happy to follow up with one if maintainers want it bundled here..bitis a transport variant under NIP-05, not a separate NIP. Happy to add a note if you'd prefer.Companion ports
For reviewers who want the canonical reference: the parser and JSON-extraction rules are identical to those in
mstrofnone/nostrlib-nip05-namecoinand the test fixtures in nbd-wtf/nostr-tools#533 line up 1:1 with the ones in this PR.Happy to split, rename, restructure, or trim — whatever shape lands easiest.