Skip to content

Conversation

chr15m
Copy link

@chr15m chr15m commented Aug 14, 2025

This NIP defines a distributed hash table (DHT) protocol for Nostr relays to enable decentralized relay discovery. The protocol allows a client to deterministically locate the NIP-65 relay list for a participating npub without first connecting to the npub's NIP-65 relay set and without centralizing on popular relays.

This aims solve a chicken-and-egg problem with npub relay discovery, further decentralizing the network and increasing censorship resistance. The DHT can be trivially extended by clients for other uses and event types in future, without modifying the relay implementation further.

Demo

A simplified interactive demo to get an idea for how the DHT would work, is available here:

https://chr15m.github.io/nostr-relay-research/

Stats

On the same repository as the demo above I ran some research on relays in the wild:

$ ./relay-stats.sh

Total relays probed: 379
Number of relays that failed to provide a valid NIP-11 response: 78 (20.58%)

Summary of relay software [counts]:
    129 strfry               git+https://github.com/hoytech/strfry.git
     84 nostr-rs-relay       https://git.sr.ht/~gheartsfield/nostr-rs-relay
     33 nostream             git+https://github.com/cameri/nostream.git
     27 haven                https://github.com/bitvora/haven
     10 wot-relay            https://github.com/bitvora/wot-relay
     10 khatru               https://github.com/fiatjaf/khatru
...

Per the stats above, if this NIP was implemented in one or two of the top four relays, a sufficiently large set of DHT nodes would be available to get pretty good decentralization during relay discovery (and other future uses).

...
Distribution of NIP-65 counts (most relays don't respond to `COUNT`):
   2269006 relay.nostr.band
   2269006 feeds.nostr.band (duplicate?)
    210084 relay.lumina.rocks
     79145 chorus.tealeaf.dev
      2442 relay.nostrfreedom.net
        25 greensoul.space
         2 relay.devvul.com
         2 nostr.polyserv.xyz
         1 tamby.mjex.me
         1 relay.nostrr.de
         1 relay.diablocanyon1.com

Though the sample size is small, the COUNT result shows a long tailed power-law distribution where a large number of NIP-65 events are centralized on a few relays. If this NIP is deployed it would hopefully lead to a less skewed distribution as users will have less incentive to centralize on popular relays.

Endorsement

I'd like to offer this endorsement in support of my PR.

Sorry, but you really do not understand any of this.
Not Nostr, not Pubky, not DNS.
--bitcoinerrorlog, replying on Nostr

@chr15m chr15m changed the title Relay discovery via DHT NIP Relay discovery via DHT Aug 14, 2025
@evd0kim
Copy link

evd0kim commented Aug 16, 2025

Good idea.
I was thinking to use pkarr or its version for the same thing.

Maybe it could be done without protocol interventions. because relay discovery could be facilitated in many different ways.

Copy link
Member

@staab staab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand DHTs very well, so take this with a grain of salt, but here are a few thoughts:

  • This seems a little convoluted, is it for relay discovery, NIP 65 event discovery, or is it a general purpose sha256-based DHT? I see how making it general purpose would incentivise more nodes to be run, but it seems weird to have such heterogenous data types living in the same table.
  • Related, poisoning attacks are mitigated by connecting to relays and verifying signatures, but this seems like a limitation on the general-purpose use of the DHT, since some use cases might not have the same kind of validations.
  • Addresses or event ids seem like natural keys (rather than using the hash of a pubkey to point to a nip 65 event, use a hash of the event's address).

@fiatjaf
Copy link
Member

fiatjaf commented Aug 18, 2025

Thank you for the simple description of the DHT functionality.

Why hash the npub in order to get the user id? Isn't it better to just use the pubkey? We don't want that because it's not evenly distributed?

My honest thoughts about this are: 1) we don't really need DHTs, because hardcoding some well-known relays is ok, just like it's ok to hardcode the IP addresses of some DNS servers; 2) pubkeys are already very cryptic to share, so sharing an nprofile1... string with embedded relays doesn't make that worse, and solves the bootstrapping problem specially if the profile being shared is known to be banned by these relays or doesn't like them.

But of course more decentralization always helps, so I like the idea of having a DHT for this, just in case. But then wouldn't it be better to have an alternative system? Not involving existing relays, but only dedicated DHT nodes?

@chr15m
Copy link
Author

chr15m commented Aug 21, 2025

Hey all,

Thanks for your time giving feedback.

@evd0kim

I was thinking to use pkarr or its version for the same thing.

Pkarr is based on the BitTorrent Mainline DHT which uses ed25519 keys which are incompatible with secp256k1 that Nostr uses. So people would need a way to find out a user's ed25519 key from the npub. Could still be interesting though. Putting a Mainline DHT proxy into Nostr relays would unlock some interesting use-cases.

@staab

is it a general purpose sha256-based DHT

Yep exactly. It's a simple way to get a deterministic lookup from any-sha256 -> set of relays. The relay side implementation is really just a way to get access to the set of all relays (and their hashes) to properly and deterministically implement that lookup. So I focused on the use-case of getting NIP-65 relay lists, but it is general purpose.

it seems weird to have such heterogenous data types living in the same table

Normally when you implement a DHT you have to specify the table storage mechanism. BitTorrent goes into detail about this for example in BEP0005 and BEP0044. In our case, relays already have the storage protocol sorted out in the form of the generic event storage that relays already implement. The "DHT storage" here is simply writing an event to the relay using the existing websocket mechanism, which is already very open ended and heterogeneous. Restricting that somehow would be more complicated than just leaving it how it works already.

verifying signatures, this seems like a limitation on the general-purpose use of the DHT

Again, this is already happening as a normal part of the protocol. The NIP is supposed to point out that this is working and there's nothing to change or implement. That should probably be more clearly worded.

Addresses or event ids seem like natural keys (rather than using the hash of a pubkey to point to a nip 65 event, use a hash of the event's address).

You could do that too, yes. You could share the hash of the event's address and the user could look it up that way.

@fiatjaf

Why hash the npub in order to get the user id? Isn't it better to just use the pubkey? We don't want that because it's not evenly distributed?

Yes good point! We can just use the pubkey.

My honest thoughts about this are: 1) we don't really need DHTs, because hardcoding some well-known relays is ok, just like it's ok to hardcode the IP addresses of some DNS servers;

I guess the difference is hardcoding the IP address of DNS servers happens an the OS level, whereas I'm thinking at the application level. Applications generally don't have IP addresses hardcoded.

On the other hand, in Bittorrent apps they do hardcode some torrent trackers for bootstrapping the DHT. I guess hard coding a couple of big relays is similar to that.

  1. pubkeys are already very cryptic to share, so sharing an nprofile1... string with embedded relays doesn't make that worse, and solves the bootstrapping problem specially if the profile being shared is known to be banned by these relays or doesn't like them.

Wow, I had no idea I could do this, thank you. It's right there in nostr-tools README. 😅 let nprofile = nip19.nprofileEncode({ pubkey: pk, relays })

I am building small web apps where the user can sync their data via relays. At the moment the user has to share an ncryptsec with themself to sync on another device. It would be good if there was a format to share an ncryptsec bundled with the set of relays. However I guess they can just use some well known relays to get the actual relay set using NIP-65.

But of course more decentralization always helps, so I like the idea of having a DHT for this, just in case. But then wouldn't it be better to have an alternative system? Not involving existing relays, but only dedicated DHT nodes?

The good thing about relays is they exist, in stark contrast with the majority of decentralization ideas (and this DHT proposal).

An interesting alternative would be a client based DHT which goes via the relays without needing any changes to relay software. Will have a think about this.

One other thing I realized is a client can still compute a hash -> relay set without the full DHT implementation, as long as two clients share a largely similar relay list. While the set of relays is small (~1000) this is feasible. It might be possible for clients to build a relay list for this purpose by querying relays for random for NIP-65 events and extracting as many relay URLs as they can.

Thanks very much for your responses. I think I can actually solve my main issues without needing a relay DHT for now. Maybe this proposal can serve as a source of ideas for any future DHT developments.

@evd0kim
Copy link

evd0kim commented Aug 21, 2025

Putting a Mainline DHT proxy into Nostr relays would unlock some interesting use-cases.

Yes. Pubky App uses intermediate servers, "homeservers", which have an access to DHT (since there is no library for that to work from browsers) so one solution would be a "bridge" that does that for Web apps. However, such solution defeats the purpose.

@rabble
Copy link
Collaborator

rabble commented Sep 24, 2025

I don't have a lot to say about the details of the DHT implementation, but i think if this were paired with query that could route DHT queries through a relay, and we could add that nip support to several of the relay servers, it would let clients which can't directly query the DHT use a random public relay that does talk to the DHT as the gateway. The big relays then become more discovery gateways and less concentrated. This is more open than the home server, let's us have DHT's for npub-> relay discovery, and shouldn't be hard to implement.

@chr15m
Copy link
Author

chr15m commented Sep 26, 2025

Ok, I figured out a way to drastically under-engineer this and make it 100% client side.

It works like this:

  • Connect to a few bootstrap relays.
  • Filter for kind: 10002 events.
  • Build a list of relays from that and hash every relay URL.
  • To find an npub's deterministic relay set, compare hash distance with relay hashes using the Kademlia XOR hash distance metric.

This basically achieves most of what I wanted with this draft NIP, without changing relay software. Bootstrapping the relay list and keeping it updated can be done infrequently and the cached list can be used over and over to determine a deterministic set of relays to associate with an npub, or any string. My primary use case is decentralized private web apps where I don't want the hardcoded relays to be relied on so hard.

I made a small zero dependency client side library that uses this technique to build a DHT mapping pubkeys to relay sets using client only queries (depends on ws but only in Node).

https://github.com/chr15m/nostr-dht

Browser demo: https://chr15m.github.io/nostr-dht/ (paste your npub)

Node library & demo:

npm i nostr-dht
npx nostr-dht npub1...
import { discoverRelays, getClosestRelays } from 'nostr-dht';
const relays = await discoverRelays(boostrapRelays);
const closestRelays = await getClosestRelays(npub, relays);

@fiatjaf @staab I would love your feedback if you're not too busy. I'd be happy for this code to end up in nostr-tools if it turns out not to be batshit crazy.

@vitorpamplona
Copy link
Collaborator

vitorpamplona commented Sep 26, 2025

You can also use all the event hints to build a bigger relay list of options for each key. On the new Amethyst, I track all pubkey hints and nprofile uris inside contents from all event kinds and put them into a 4MB bloom filter that can hold the relay list of about 1 million keys (with around 10 hint relays each). In that way, I discover even fringe community relays the user is using and can hit them to download stuff I can't find on the user's own outbox relays. It's probably not the most effective data structure, but it works quite well for the memory limitations in mobile apps.

@chr15m
Copy link
Author

chr15m commented Sep 26, 2025

@vitorpamplona wow that sounds comprehensive. Relay lists for 1 million keys in 4mb is insane! 🤯

I did some basic spot-check testing to see how many relays each kind yielded:

Running ./find_rich_relays.sh
Fetching a sample of up to 1000 events for kinds: 3 10 17 41 9734 10002 10006 10007 10019 10050 10051 30002 30166 30617 34550 444
This may take a moment...
connecting to wss://relay.damus.io... ok.
connecting to wss://relay.snort.social... ok.
connecting to wss://nos.lol... ok.
Kind 10002: 518 unique relays found
Kind 3: 148 unique relays found
Kind 10050: 39 unique relays found
Kind 30617: 8 unique relays found
Kind 9734: 5 unique relays found
Kind 30002: 4 unique relays found
Kind 10007: 4 unique relays found
Kind 10051: 3 unique relays found
Kind 30166: 1 unique relays found

Once I saw the basic power law curve with 10002 having the majority of unique relays I settled on that for this script. For DHT purposes if the user is picking the top 10 to 20 they are going to have enough overlap even if the relay list is not exactly the same or out of date.

Actually since it's so easy to make compound filters I might as well update it to use the top 3 kinds. 🤔

For small web apps I guess we don't have access to anywhere near the number of events Amethyst is seeing.

@chr15m
Copy link
Author

chr15m commented Sep 26, 2025

@vitorpamplona I'm curious if you have a sense for how many total relays there are out there? I wonder how nostr.watch builds its list?

@vitorpamplona
Copy link
Collaborator

vitorpamplona commented Sep 26, 2025

A regular session on Amethyst will capture about 850 normalized, unique, non-DM relay hints, but if you remove all the local relays (.local, 127..) which are not usually accessible, then it goes around 527. Now, that is just the view of one user and their follows/feeds. Our Push Notification server pulls events from a total of 3105 unique inbox relays (DM + 10002)

@vitorpamplona
Copy link
Collaborator

vitorpamplona commented Sep 26, 2025

Also, I am getting ready to automatically re-broadcast kind 10002 to relays that we couldn't find them. Our users pick their preferred Index relays, and if the app sees a kind 10002 that we know their indexers don't have (we already have an empty EOSE for that filter), it will automatically send it back there. We do something similar today when any relay sends an outdated replaceable. This new algorithm just adds relays that didn't send anything into the mix.

Meaning, libraries and clients can automatically "fix" any indexing logic that we like to make it easier for clients to find the user's information.

@chr15m
Copy link
Author

chr15m commented Sep 27, 2025

3105 unique inbox relays

Whoa! So that's a lot of small private instances that are largely invisible to the network except for their owners?

BTW in nostr-dht I'm not just removing 127 + localhost but a bunch of other invalid relays I saw. See isValidRelay for all checks:

https://github.com/chr15m/nostr-dht/blob/f7f7601e525b0fb5a1a686454b5fae621a540f44/nostr-dht.js#L44

I also removed non-SSL servers for the purposes of the DHT only using high quality nodes.

Thanks for working on Nostr tech. 🙏

@v0l
Copy link
Member

v0l commented Sep 27, 2025

I think the title of the PR is wrong, it should be DHT proxy via Nostr, in DTAN-server we use DHT to discover relays (other dtan-server instances) using the SHA1("nostr:2003") info hash.

I expected this PR to be the same as that but it isnt.

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.

7 participants