UNPKG

@waku/discovery

Version:

Contains various discovery mechanisms: DNS Discovery (EIP-1459, Peer Exchange, Local Peer Cache Discovery.

160 lines 6.41 kB
import { EnrDecoder } from "@waku/enr"; import { Logger } from "@waku/utils"; import { DnsOverHttps } from "./dns_over_https.js"; import { ENRTree } from "./enrtree.js"; import { fetchNodesUntilCapabilitiesFulfilled, yieldNodesUntilCapabilitiesFulfilled } from "./fetch_nodes.js"; const log = new Logger("discovery:dns"); export class DnsNodeDiscovery { dns; _DNSTreeCache; _errorTolerance = 10; static async dnsOverHttp(dnsClient) { if (!dnsClient) { dnsClient = await DnsOverHttps.create(); } return new DnsNodeDiscovery(dnsClient); } /** * Returns a list of verified peers listed in an EIP-1459 DNS tree. Method may * return fewer peers than requested if @link wantedNodeCapabilityCount requires * larger quantity of peers than available or the number of errors/duplicate * peers encountered by randomized search exceeds the sum of the fields of * @link wantedNodeCapabilityCount plus the @link _errorTolerance factor. */ async getPeers(enrTreeUrls, wantedNodeCapabilityCount) { const networkIndex = Math.floor(Math.random() * enrTreeUrls.length); const { publicKey, domain } = ENRTree.parseTree(enrTreeUrls[networkIndex]); const context = { domain, publicKey, visits: {} }; const peers = await fetchNodesUntilCapabilitiesFulfilled(wantedNodeCapabilityCount, this._errorTolerance, () => this._search(domain, context)); log.info("retrieved peers: ", peers.map((peer) => { return { id: peer.peerId?.toString(), multiaddrs: peer.multiaddrs?.map((ma) => ma.toString()) }; })); return peers; } constructor(dns) { this._DNSTreeCache = {}; this.dns = dns; } /** * {@inheritDoc getPeers} */ async *getNextPeer(enrTreeUrls, wantedNodeCapabilityCount) { const networkIndex = Math.floor(Math.random() * enrTreeUrls.length); const { publicKey, domain } = ENRTree.parseTree(enrTreeUrls[networkIndex]); const context = { domain, publicKey, visits: {} }; for await (const peer of yieldNodesUntilCapabilitiesFulfilled(wantedNodeCapabilityCount, this._errorTolerance, () => this._search(domain, context))) { yield peer; } } /** * Runs a recursive, randomized descent of the DNS tree to retrieve a single * ENR record as an ENR. Returns null if parsing or DNS resolution fails. */ async _search(subdomain, context) { try { const entry = await this._getTXTRecord(subdomain, context); context.visits[subdomain] = true; let next; let branches; const entryType = getEntryType(entry); try { switch (entryType) { case ENRTree.ROOT_PREFIX: next = ENRTree.parseAndVerifyRoot(entry, context.publicKey); return await this._search(next, context); case ENRTree.BRANCH_PREFIX: branches = ENRTree.parseBranch(entry); next = selectRandomPath(branches, context); return await this._search(next, context); case ENRTree.RECORD_PREFIX: return EnrDecoder.fromString(entry); default: return null; } } catch (error) { log.error(`Failed to search DNS tree ${entryType} at subdomain ${subdomain}: ${error}`); return null; } } catch (error) { log.error(`Failed to retrieve TXT record at subdomain ${subdomain}: ${error}`); return null; } } /** * Retrieves the TXT record stored at a location from either * this DNS tree cache or via DNS query. * * @throws if the TXT Record contains non-UTF-8 values. */ async _getTXTRecord(subdomain, context) { if (this._DNSTreeCache[subdomain]) { return this._DNSTreeCache[subdomain]; } // Location is either the top level tree entry host or a subdomain of it. const location = subdomain !== context.domain ? `${subdomain}.${context.domain}` : context.domain; const response = await this.dns.resolveTXT(location); if (!response.length) throw new Error("Received empty result array while fetching TXT record"); if (!response[0].length) throw new Error("Received empty TXT record"); // Branch entries can be an array of strings of comma delimited subdomains, with // some subdomain strings split across the array elements const result = response.join(""); this._DNSTreeCache[subdomain] = result; return result; } } function getEntryType(entry) { if (entry.startsWith(ENRTree.ROOT_PREFIX)) return ENRTree.ROOT_PREFIX; if (entry.startsWith(ENRTree.BRANCH_PREFIX)) return ENRTree.BRANCH_PREFIX; if (entry.startsWith(ENRTree.RECORD_PREFIX)) return ENRTree.RECORD_PREFIX; return ""; } /** * Returns a randomly selected subdomain string from the list provided by a branch * entry record. * * The client must track subdomains which are already resolved to avoid * going into an infinite loop b/c branch entries can contain * circular references. It’s in the client’s best interest to traverse the * tree in random order. */ function selectRandomPath(branches, context) { // Identify domains already visited in this traversal of the DNS tree. // Then filter against them to prevent cycles. const circularRefs = {}; for (const [idx, subdomain] of branches.entries()) { if (context.visits[subdomain]) { circularRefs[idx] = true; } } // If all possible paths are circular... if (Object.keys(circularRefs).length === branches.length) { throw new Error("Unresolvable circular path detected"); } // Randomly select a viable path let index; do { index = Math.floor(Math.random() * branches.length); } while (circularRefs[index]); return branches[index]; } //# sourceMappingURL=dns.js.map