Nostr Relay Hash Table is a client side library for deterministically mapping pubkeys to relays without central coordination.

If you're building Nostr apps you can use this to make them more decentralized by publishing to and reading from the deterministic relay set for a given key.

It uses Kademlia-style XOR-distance routing. Each pubkey is deterministically assigned to the relays with URLs that hash closest by XOR distance to the pubkey's hash, enabling clients to independently discover where any participating pubkey publishes without central coordination. It does not use Kademlia style recursive lookup, instead building an exhaustive relay list using broadcast discovery. See the "about" section below for an explanation.

You can use this to publish and discover a pubkey's events like kind 0 metadata and NIP-65 relay lists, or any other event, avoiding coordination on large centralized relays.

Install

npm i nostr-dht

Or import from a CDN.

Use

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

To differentiate by namespace, e.g. on a per-application basis, you can append or hash the namespace with the npub namespace + npub. This will yield a set of relays unique to the namespace and npub. You also use any other string in place of the npub to find the "closest" relay set to that string.

Example

Browser:

<script type="module">
  import { discoverRelays, getClosestRelays } from 'https://cdn.jsdelivr.net/npm/nostr-dht@latest/nostr-dht.js';

  const bootstrapRelays = ["wss://relay.damus.io", "wss://relay.snort.social", "wss://nos.lol"];

  // Discover relays
  const relays = await discoverRelays(bootstrapRelays, /* { previousRelays: localStorage["relays"] } */);

  // Cache relays
  // localStorage["relays"] = JSON.stringify(relays);

  // Find the 8 closest relays for a given npub
  const npub = "npub1m2f3j22hf90mt8mw788pne6fg7c8j2mw4gd3xjsptspjdeqf05dqhr54wn";
  let closestRelays = await getClosestRelays(npub, relays, { n: 8 });

  console.log(`\nTop 8 relays for ${npub}:`);
  closestRelays.forEach(url => console.log(url));
</script>

In Node.js the code is the same but use import { discoverRelays, getClosestRelays } from 'nostr-dht'; instead.

Filters

You can also filter relays from the hash table, for example filtering out auth_required etc, and ensuring a certain max_message_length.

// Find the 8 closest relays that match a filter
const filter = (relay) => {
    const limitations = relay.nip11?.limitation;
    const maxMessageLengthOk = !limitations?.max_message_length || limitations.max_message_length > 50000;
    const authRequiredOk = !limitations?.auth_required;
    const paymentRequiredOk = !limitations?.payment_required;
    const restrictedWritesOk = !limitations?.restricted_writes;
    return maxMessageLengthOk && authRequiredOk && paymentRequiredOk && restrictedWritesOk;
};
closestRelays = await getClosestRelays(npub, relays, { n: 8, filter });

console.log(`\nTop 8 relays for ${npub} that are free to write to:`);
closestRelays.forEach(url => console.log(url));

// Find the 8 closest relays that also have old events
closestRelays = await getClosestRelays(npub, relays, { n: 8, filter, checkOldestEvent: true });

console.log(`\nTop 8 relays for ${npub} that are free to write to and have old events:`);
closestRelays.forEach(url => console.log(url));

Command-line

You can also run nostr-dht from the command line to discover relays and find the closest ones for a given npub.

npx nostr-dht [npub]

If you don't provide an npub, it will use a default test npub.

About

How it works.

  • The library starts with a small set of bootstrap relays.
  • From those it builds a list of relays by filtering for kind:10002 & kind:3 events.
  • Each relay is then hashed and stored.
  • To look up an npub (or any other string) the value is hashed.
  • The hash is XOR-distance compared with the relay list hashes.
  • The result is a set of N relays "close" to the npub.

Goal

The goal of this library is to reduce centralization on events on large relays.

Eclipse Attacks

The main threat to participants in any DHT is the eclipse attack. The attacker chooses node IDs such that they occupy and control the closest nodes to the target. This DHT implementation is vulnerable in that it would be trivial to brute-force a set of relay URLs that hash to values close to a given npub.

However, the design of Nostr is such that doing this is basically pointless. Because events are cryptographically signed, the worst the malicious relay set can do is not republish the events from that npub, censoring that npub from the DHT for the purposes of discovery. The npub can (and should) still redundantly publish to other reliable relays like they do now. Only discovery on this DHT is censored, and the DHT is not the only discovery mechanism. Clients using this DHT can simply fall back to the usual methods of relay discovery using e.g. NIP-65, follow lists, profiles, etc. This basically removes the incentive to mount an attack since the attack is practically ineffectual, simply re-centralizing discovery for that one npub.