@waku/discovery 0.0.2-434be7b.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/bundle/index.js +27179 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/dns/constants.d.ts +9 -0
  4. package/dist/dns/constants.js +13 -0
  5. package/dist/dns/constants.js.map +1 -0
  6. package/dist/dns/dns.d.ts +32 -0
  7. package/dist/dns/dns.js +160 -0
  8. package/dist/dns/dns.js.map +1 -0
  9. package/dist/dns/dns_discovery.d.ts +24 -0
  10. package/dist/dns/dns_discovery.js +95 -0
  11. package/dist/dns/dns_discovery.js.map +1 -0
  12. package/dist/dns/dns_over_https.d.ts +25 -0
  13. package/dist/dns/dns_over_https.js +72 -0
  14. package/dist/dns/dns_over_https.js.map +1 -0
  15. package/dist/dns/enrtree.d.ts +33 -0
  16. package/dist/dns/enrtree.js +76 -0
  17. package/dist/dns/enrtree.js.map +1 -0
  18. package/dist/dns/fetch_nodes.d.ts +13 -0
  19. package/dist/dns/fetch_nodes.js +133 -0
  20. package/dist/dns/fetch_nodes.js.map +1 -0
  21. package/dist/dns/index.d.ts +3 -0
  22. package/dist/dns/index.js +4 -0
  23. package/dist/dns/index.js.map +1 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +10 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/local-peer-cache/index.d.ts +24 -0
  28. package/dist/local-peer-cache/index.js +106 -0
  29. package/dist/local-peer-cache/index.js.map +1 -0
  30. package/dist/peer-exchange/index.d.ts +2 -0
  31. package/dist/peer-exchange/index.js +3 -0
  32. package/dist/peer-exchange/index.js.map +1 -0
  33. package/dist/peer-exchange/rpc.d.ts +22 -0
  34. package/dist/peer-exchange/rpc.js +41 -0
  35. package/dist/peer-exchange/rpc.js.map +1 -0
  36. package/dist/peer-exchange/waku_peer_exchange.d.ts +21 -0
  37. package/dist/peer-exchange/waku_peer_exchange.js +80 -0
  38. package/dist/peer-exchange/waku_peer_exchange.js.map +1 -0
  39. package/dist/peer-exchange/waku_peer_exchange_discovery.d.ts +53 -0
  40. package/dist/peer-exchange/waku_peer_exchange_discovery.js +136 -0
  41. package/dist/peer-exchange/waku_peer_exchange_discovery.js.map +1 -0
  42. package/package.json +1 -0
  43. package/src/dns/constants.ts +16 -0
  44. package/src/dns/dns.ts +215 -0
  45. package/src/dns/dns_discovery.ts +144 -0
  46. package/src/dns/dns_over_https.ts +83 -0
  47. package/src/dns/enrtree.ts +123 -0
  48. package/src/dns/fetch_nodes.ts +181 -0
  49. package/src/dns/index.ts +3 -0
  50. package/src/index.ts +21 -0
  51. package/src/local-peer-cache/index.ts +160 -0
  52. package/src/peer-exchange/index.ts +11 -0
  53. package/src/peer-exchange/rpc.ts +44 -0
  54. package/src/peer-exchange/waku_peer_exchange.ts +111 -0
  55. package/src/peer-exchange/waku_peer_exchange_discovery.ts +238 -0
@@ -0,0 +1,53 @@
1
+ import { TypedEventEmitter } from "@libp2p/interface";
2
+ import { peerDiscoverySymbol as symbol } from "@libp2p/interface";
3
+ import type { PeerDiscovery, PeerDiscoveryEvents } from "@libp2p/interface";
4
+ import { Libp2pComponents, PubsubTopic, Tags } from "@waku/interfaces";
5
+ export interface Options {
6
+ /**
7
+ * Tag a bootstrap peer with this name before "discovering" it (default: 'bootstrap')
8
+ */
9
+ tagName?: string;
10
+ /**
11
+ * The bootstrap peer tag will have this value (default: 50)
12
+ */
13
+ tagValue?: number;
14
+ /**
15
+ * Cause the bootstrap peer tag to be removed after this number of ms (default: 2 minutes)
16
+ */
17
+ tagTTL?: number;
18
+ /**
19
+ * The interval between queries to a peer (default: 10 seconds)
20
+ * The interval will increase by a factor of an incrementing number (starting at 1)
21
+ * until it reaches the maximum attempts before backoff
22
+ */
23
+ queryInterval?: number;
24
+ /**
25
+ * The number of attempts before the queries to a peer are aborted (default: 3)
26
+ */
27
+ maxRetries?: number;
28
+ }
29
+ export declare const DEFAULT_PEER_EXCHANGE_TAG_NAME = Tags.PEER_EXCHANGE;
30
+ export declare class PeerExchangeDiscovery extends TypedEventEmitter<PeerDiscoveryEvents> implements PeerDiscovery {
31
+ private readonly components;
32
+ private readonly peerExchange;
33
+ private readonly options;
34
+ private isStarted;
35
+ private queryingPeers;
36
+ private queryAttempts;
37
+ private readonly handleDiscoveredPeer;
38
+ constructor(components: Libp2pComponents, pubsubTopics: PubsubTopic[], options?: Options);
39
+ /**
40
+ * Start emitting events
41
+ */
42
+ start(): void;
43
+ /**
44
+ * Remove event listener
45
+ */
46
+ stop(): void;
47
+ get [symbol](): true;
48
+ get [Symbol.toStringTag](): string;
49
+ private readonly startRecurringQueries;
50
+ private query;
51
+ private abortQueriesForPeer;
52
+ }
53
+ export declare function wakuPeerExchangeDiscovery(pubsubTopics: PubsubTopic[]): (components: Libp2pComponents) => PeerExchangeDiscovery;
@@ -0,0 +1,136 @@
1
+ import { CustomEvent, TypedEventEmitter } from "@libp2p/interface";
2
+ import { peerDiscoverySymbol as symbol } from "@libp2p/interface";
3
+ import { Tags } from "@waku/interfaces";
4
+ import { encodeRelayShard, Logger } from "@waku/utils";
5
+ import { PeerExchangeCodec, WakuPeerExchange } from "./waku_peer_exchange.js";
6
+ const log = new Logger("peer-exchange-discovery");
7
+ const DEFAULT_PEER_EXCHANGE_REQUEST_NODES = 10;
8
+ const DEFAULT_PEER_EXCHANGE_QUERY_INTERVAL_MS = 10 * 1000;
9
+ const DEFAULT_MAX_RETRIES = 3;
10
+ export const DEFAULT_PEER_EXCHANGE_TAG_NAME = Tags.PEER_EXCHANGE;
11
+ const DEFAULT_PEER_EXCHANGE_TAG_VALUE = 50;
12
+ const DEFAULT_PEER_EXCHANGE_TAG_TTL = 100000000;
13
+ export class PeerExchangeDiscovery extends TypedEventEmitter {
14
+ components;
15
+ peerExchange;
16
+ options;
17
+ isStarted;
18
+ queryingPeers = new Set();
19
+ queryAttempts = new Map();
20
+ handleDiscoveredPeer = (event) => {
21
+ const { protocols, peerId } = event.detail;
22
+ if (!protocols.includes(PeerExchangeCodec) ||
23
+ this.queryingPeers.has(peerId.toString()))
24
+ return;
25
+ this.queryingPeers.add(peerId.toString());
26
+ this.startRecurringQueries(peerId).catch((error) => log.error(`Error querying peer ${error}`));
27
+ };
28
+ constructor(components, pubsubTopics, options = {}) {
29
+ super();
30
+ this.components = components;
31
+ this.peerExchange = new WakuPeerExchange(components, pubsubTopics);
32
+ this.options = options;
33
+ this.isStarted = false;
34
+ }
35
+ /**
36
+ * Start emitting events
37
+ */
38
+ start() {
39
+ if (this.isStarted) {
40
+ return;
41
+ }
42
+ log.info("Starting peer exchange node discovery, discovering peers");
43
+ // might be better to use "peer:identify" or "peer:update"
44
+ this.components.events.addEventListener("peer:identify", this.handleDiscoveredPeer);
45
+ }
46
+ /**
47
+ * Remove event listener
48
+ */
49
+ stop() {
50
+ if (!this.isStarted)
51
+ return;
52
+ log.info("Stopping peer exchange node discovery");
53
+ this.isStarted = false;
54
+ this.queryingPeers.clear();
55
+ this.components.events.removeEventListener("peer:identify", this.handleDiscoveredPeer);
56
+ }
57
+ get [symbol]() {
58
+ return true;
59
+ }
60
+ get [Symbol.toStringTag]() {
61
+ return "@waku/peer-exchange";
62
+ }
63
+ startRecurringQueries = async (peerId) => {
64
+ const peerIdStr = peerId.toString();
65
+ const { queryInterval = DEFAULT_PEER_EXCHANGE_QUERY_INTERVAL_MS, maxRetries = DEFAULT_MAX_RETRIES } = this.options;
66
+ log.info(`Querying peer: ${peerIdStr} (attempt ${this.queryAttempts.get(peerIdStr) ?? 1})`);
67
+ await this.query(peerId);
68
+ const currentAttempt = this.queryAttempts.get(peerIdStr) ?? 1;
69
+ if (currentAttempt > maxRetries) {
70
+ this.abortQueriesForPeer(peerIdStr);
71
+ return;
72
+ }
73
+ setTimeout(() => {
74
+ this.queryAttempts.set(peerIdStr, currentAttempt + 1);
75
+ this.startRecurringQueries(peerId).catch((error) => {
76
+ log.error(`Error in startRecurringQueries: ${error}`);
77
+ });
78
+ }, queryInterval * currentAttempt);
79
+ };
80
+ async query(peerId) {
81
+ const { error, peerInfos } = await this.peerExchange.query({
82
+ numPeers: DEFAULT_PEER_EXCHANGE_REQUEST_NODES,
83
+ peerId
84
+ });
85
+ if (error) {
86
+ log.error("Peer exchange query failed", error);
87
+ return { error, peerInfos: null };
88
+ }
89
+ for (const _peerInfo of peerInfos) {
90
+ const { ENR } = _peerInfo;
91
+ if (!ENR) {
92
+ log.warn("No ENR in peerInfo object, skipping");
93
+ continue;
94
+ }
95
+ const { peerId, peerInfo, shardInfo } = ENR;
96
+ if (!peerId || !peerInfo) {
97
+ continue;
98
+ }
99
+ const hasPeer = await this.components.peerStore.has(peerId);
100
+ if (hasPeer) {
101
+ continue;
102
+ }
103
+ // update the tags for the peer
104
+ await this.components.peerStore.save(peerId, {
105
+ tags: {
106
+ [DEFAULT_PEER_EXCHANGE_TAG_NAME]: {
107
+ value: this.options.tagValue ?? DEFAULT_PEER_EXCHANGE_TAG_VALUE,
108
+ ttl: this.options.tagTTL ?? DEFAULT_PEER_EXCHANGE_TAG_TTL
109
+ }
110
+ },
111
+ ...(shardInfo && {
112
+ metadata: {
113
+ shardInfo: encodeRelayShard(shardInfo)
114
+ }
115
+ })
116
+ });
117
+ log.info(`Discovered peer: ${peerId.toString()}`);
118
+ this.dispatchEvent(new CustomEvent("peer", {
119
+ detail: {
120
+ id: peerId,
121
+ multiaddrs: peerInfo.multiaddrs
122
+ }
123
+ }));
124
+ }
125
+ return { error: null, peerInfos };
126
+ }
127
+ abortQueriesForPeer(peerIdStr) {
128
+ log.info(`Aborting queries for peer: ${peerIdStr}`);
129
+ this.queryingPeers.delete(peerIdStr);
130
+ this.queryAttempts.delete(peerIdStr);
131
+ }
132
+ }
133
+ export function wakuPeerExchangeDiscovery(pubsubTopics) {
134
+ return (components) => new PeerExchangeDiscovery(components, pubsubTopics);
135
+ }
136
+ //# sourceMappingURL=waku_peer_exchange_discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"waku_peer_exchange_discovery.js","sourceRoot":"","sources":["../../src/peer-exchange/waku_peer_exchange_discovery.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1 @@
1
+ {"name":"@waku/discovery","version":"0.0.2-434be7b.0","description":"Contains various discovery mechanisms: DNS Discovery (EIP-1459, Peer Exchange, Local Peer Cache Discovery.","types":"./dist/index.d.ts","module":"./dist/index.js","react-native":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","react-native":"./dist/index.js"}},"type":"module","author":"Waku Team","homepage":"https://github.com/waku-org/js-waku/tree/master/packages/discovery#readme","repository":{"type":"git","url":"https://github.com/waku-org/js-waku.git"},"bugs":{"url":"https://github.com/waku-org/js-waku/issues"},"license":"MIT OR Apache-2.0","keywords":["waku","decentralized","secure","communication","web3","ethereum","dapps","privacy"],"scripts":{"build":"run-s build:**","build:esm":"tsc","build:bundle":"rollup --config rollup.config.js","fix":"run-s fix:*","fix:lint":"eslint src *.js --fix","check":"run-s check:*","check:lint":"eslint src --ext .ts","check:spelling":"cspell \"{README.md,src/**/*.ts}\"","check:tsc":"tsc -p tsconfig.dev.json","prepublish":"npm run build","reset-hard":"git clean -dfx -e .idea && git reset --hard && npm i && npm run build","test":"NODE_ENV=test run-s test:*","test:node":"NODE_ENV=test TS_NODE_PROJECT=./tsconfig.dev.json mocha","test:browser":"NODE_ENV=test karma start karma.conf.cjs"},"engines":{"node":">=18"},"dependencies":{"@waku/interfaces":"0.0.23-434be7b.0","@waku/proto":"0.0.7-434be7b.0","@waku/enr":"0.0.22-434be7b.0","@waku/utils":"0.0.16-434be7b.0","debug":"^4.3.4","dns-query":"^0.11.2","hi-base32":"^0.5.1","uint8arrays":"^5.0.1"},"devDependencies":{"@libp2p/peer-id":"^4.0.4","@libp2p/peer-id-factory":"^4.0.5","@multiformats/multiaddr":"^12.0.0","@rollup/plugin-commonjs":"^25.0.7","@rollup/plugin-json":"^6.0.0","@rollup/plugin-node-resolve":"^15.2.3","@types/chai":"^4.3.11","@types/node-localstorage":"^1.3.3","@waku/build-utils":"*","chai":"^4.3.10","chai-as-promised":"^7.1.1","cspell":"^8.3.2","mocha":"^10.3.0","node-localstorage":"^3.0.5","npm-run-all":"^4.1.5","rollup":"^4.12.0","sinon":"^17.0.1"},"peerDependencies":{"@waku/core":"0.0.28-434be7b.0","@waku/enr":"0.0.22-434be7b.0","@waku/interfaces":"0.0.23-434be7b.0","@waku/proto":"0.0.7-434be7b.0","@waku/utils":"0.0.16-434be7b.0","@libp2p/interface":"^1.1.2"},"peerDependenciesMeta":{"@waku/interfaces":{"optional":true},"@libp2p/interface":{"optional":true}},"files":["dist","bundle","src/**/*.ts","!**/*.spec.*","!**/*.json","CHANGELOG.md","LICENSE","README.md"]}
@@ -0,0 +1,16 @@
1
+ import type { NodeCapabilityCount } from "@waku/interfaces";
2
+
3
+ export const enrTree = {
4
+ TEST: "enrtree://AO47IDOLBKH72HIZZOXQP6NMRESAN7CHYWIBNXDXWRJRZWLODKII6@test.wakuv2.nodes.status.im",
5
+ PROD: "enrtree://ANEDLO25QVUGJOUTQFRYKWX6P4Z4GKVESBMHML7DZ6YK4LGS5FC5O@prod.wakuv2.nodes.status.im"
6
+ };
7
+
8
+ export const DEFAULT_BOOTSTRAP_TAG_NAME = "bootstrap";
9
+ export const DEFAULT_BOOTSTRAP_TAG_VALUE = 50;
10
+ export const DEFAULT_BOOTSTRAP_TAG_TTL = 100_000_000;
11
+
12
+ export const DEFAULT_NODE_REQUIREMENTS: Partial<NodeCapabilityCount> = {
13
+ store: 2,
14
+ filter: 1,
15
+ lightPush: 1
16
+ };
package/src/dns/dns.ts ADDED
@@ -0,0 +1,215 @@
1
+ import { ENR, EnrDecoder } from "@waku/enr";
2
+ import type {
3
+ DnsClient,
4
+ IEnr,
5
+ NodeCapabilityCount,
6
+ SearchContext
7
+ } from "@waku/interfaces";
8
+ import { Logger } from "@waku/utils";
9
+
10
+ import { DnsOverHttps } from "./dns_over_https.js";
11
+ import { ENRTree } from "./enrtree.js";
12
+ import {
13
+ fetchNodesUntilCapabilitiesFulfilled,
14
+ yieldNodesUntilCapabilitiesFulfilled
15
+ } from "./fetch_nodes.js";
16
+
17
+ const log = new Logger("discovery:dns");
18
+
19
+ export class DnsNodeDiscovery {
20
+ private readonly dns: DnsClient;
21
+ private readonly _DNSTreeCache: { [key: string]: string };
22
+ private readonly _errorTolerance: number = 10;
23
+
24
+ public static async dnsOverHttp(
25
+ dnsClient?: DnsClient
26
+ ): Promise<DnsNodeDiscovery> {
27
+ if (!dnsClient) {
28
+ dnsClient = await DnsOverHttps.create();
29
+ }
30
+ return new DnsNodeDiscovery(dnsClient);
31
+ }
32
+
33
+ /**
34
+ * Returns a list of verified peers listed in an EIP-1459 DNS tree. Method may
35
+ * return fewer peers than requested if @link wantedNodeCapabilityCount requires
36
+ * larger quantity of peers than available or the number of errors/duplicate
37
+ * peers encountered by randomized search exceeds the sum of the fields of
38
+ * @link wantedNodeCapabilityCount plus the @link _errorTolerance factor.
39
+ */
40
+ async getPeers(
41
+ enrTreeUrls: string[],
42
+ wantedNodeCapabilityCount: Partial<NodeCapabilityCount>
43
+ ): Promise<IEnr[]> {
44
+ const networkIndex = Math.floor(Math.random() * enrTreeUrls.length);
45
+ const { publicKey, domain } = ENRTree.parseTree(enrTreeUrls[networkIndex]);
46
+ const context: SearchContext = {
47
+ domain,
48
+ publicKey,
49
+ visits: {}
50
+ };
51
+
52
+ const peers = await fetchNodesUntilCapabilitiesFulfilled(
53
+ wantedNodeCapabilityCount,
54
+ this._errorTolerance,
55
+ () => this._search(domain, context)
56
+ );
57
+ log.info(
58
+ "retrieved peers: ",
59
+ peers.map((peer) => {
60
+ return {
61
+ id: peer.peerId?.toString(),
62
+ multiaddrs: peer.multiaddrs?.map((ma) => ma.toString())
63
+ };
64
+ })
65
+ );
66
+ return peers;
67
+ }
68
+
69
+ public constructor(dns: DnsClient) {
70
+ this._DNSTreeCache = {};
71
+ this.dns = dns;
72
+ }
73
+
74
+ /**
75
+ * {@inheritDoc getPeers}
76
+ */
77
+ async *getNextPeer(
78
+ enrTreeUrls: string[],
79
+ wantedNodeCapabilityCount: Partial<NodeCapabilityCount>
80
+ ): AsyncGenerator<IEnr> {
81
+ const networkIndex = Math.floor(Math.random() * enrTreeUrls.length);
82
+ const { publicKey, domain } = ENRTree.parseTree(enrTreeUrls[networkIndex]);
83
+ const context: SearchContext = {
84
+ domain,
85
+ publicKey,
86
+ visits: {}
87
+ };
88
+
89
+ for await (const peer of yieldNodesUntilCapabilitiesFulfilled(
90
+ wantedNodeCapabilityCount,
91
+ this._errorTolerance,
92
+ () => this._search(domain, context)
93
+ )) {
94
+ yield peer;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Runs a recursive, randomized descent of the DNS tree to retrieve a single
100
+ * ENR record as an ENR. Returns null if parsing or DNS resolution fails.
101
+ */
102
+ private async _search(
103
+ subdomain: string,
104
+ context: SearchContext
105
+ ): Promise<ENR | null> {
106
+ try {
107
+ const entry = await this._getTXTRecord(subdomain, context);
108
+ context.visits[subdomain] = true;
109
+
110
+ let next: string;
111
+ let branches: string[];
112
+
113
+ const entryType = getEntryType(entry);
114
+ try {
115
+ switch (entryType) {
116
+ case ENRTree.ROOT_PREFIX:
117
+ next = ENRTree.parseAndVerifyRoot(entry, context.publicKey);
118
+ return await this._search(next, context);
119
+ case ENRTree.BRANCH_PREFIX:
120
+ branches = ENRTree.parseBranch(entry);
121
+ next = selectRandomPath(branches, context);
122
+ return await this._search(next, context);
123
+ case ENRTree.RECORD_PREFIX:
124
+ return EnrDecoder.fromString(entry);
125
+ default:
126
+ return null;
127
+ }
128
+ } catch (error) {
129
+ log.error(
130
+ `Failed to search DNS tree ${entryType} at subdomain ${subdomain}: ${error}`
131
+ );
132
+ return null;
133
+ }
134
+ } catch (error) {
135
+ log.error(
136
+ `Failed to retrieve TXT record at subdomain ${subdomain}: ${error}`
137
+ );
138
+ return null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Retrieves the TXT record stored at a location from either
144
+ * this DNS tree cache or via DNS query.
145
+ *
146
+ * @throws if the TXT Record contains non-UTF-8 values.
147
+ */
148
+ private async _getTXTRecord(
149
+ subdomain: string,
150
+ context: SearchContext
151
+ ): Promise<string> {
152
+ if (this._DNSTreeCache[subdomain]) {
153
+ return this._DNSTreeCache[subdomain];
154
+ }
155
+
156
+ // Location is either the top level tree entry host or a subdomain of it.
157
+ const location =
158
+ subdomain !== context.domain
159
+ ? `${subdomain}.${context.domain}`
160
+ : context.domain;
161
+
162
+ const response = await this.dns.resolveTXT(location);
163
+
164
+ if (!response.length)
165
+ throw new Error("Received empty result array while fetching TXT record");
166
+ if (!response[0].length) throw new Error("Received empty TXT record");
167
+
168
+ // Branch entries can be an array of strings of comma delimited subdomains, with
169
+ // some subdomain strings split across the array elements
170
+ const result = response.join("");
171
+
172
+ this._DNSTreeCache[subdomain] = result;
173
+ return result;
174
+ }
175
+ }
176
+
177
+ function getEntryType(entry: string): string {
178
+ if (entry.startsWith(ENRTree.ROOT_PREFIX)) return ENRTree.ROOT_PREFIX;
179
+ if (entry.startsWith(ENRTree.BRANCH_PREFIX)) return ENRTree.BRANCH_PREFIX;
180
+ if (entry.startsWith(ENRTree.RECORD_PREFIX)) return ENRTree.RECORD_PREFIX;
181
+
182
+ return "";
183
+ }
184
+
185
+ /**
186
+ * Returns a randomly selected subdomain string from the list provided by a branch
187
+ * entry record.
188
+ *
189
+ * The client must track subdomains which are already resolved to avoid
190
+ * going into an infinite loop b/c branch entries can contain
191
+ * circular references. It’s in the client’s best interest to traverse the
192
+ * tree in random order.
193
+ */
194
+ function selectRandomPath(branches: string[], context: SearchContext): string {
195
+ // Identify domains already visited in this traversal of the DNS tree.
196
+ // Then filter against them to prevent cycles.
197
+ const circularRefs: { [key: number]: boolean } = {};
198
+ for (const [idx, subdomain] of branches.entries()) {
199
+ if (context.visits[subdomain]) {
200
+ circularRefs[idx] = true;
201
+ }
202
+ }
203
+ // If all possible paths are circular...
204
+ if (Object.keys(circularRefs).length === branches.length) {
205
+ throw new Error("Unresolvable circular path detected");
206
+ }
207
+
208
+ // Randomly select a viable path
209
+ let index;
210
+ do {
211
+ index = Math.floor(Math.random() * branches.length);
212
+ } while (circularRefs[index]);
213
+
214
+ return branches[index];
215
+ }
@@ -0,0 +1,144 @@
1
+ import {
2
+ CustomEvent,
3
+ PeerDiscovery,
4
+ PeerDiscoveryEvents,
5
+ TypedEventEmitter
6
+ } from "@libp2p/interface";
7
+ import { peerDiscoverySymbol as symbol } from "@libp2p/interface";
8
+ import type { PeerInfo } from "@libp2p/interface";
9
+ import type {
10
+ DnsDiscOptions,
11
+ DnsDiscoveryComponents,
12
+ IEnr,
13
+ NodeCapabilityCount
14
+ } from "@waku/interfaces";
15
+ import { encodeRelayShard, Logger } from "@waku/utils";
16
+
17
+ import {
18
+ DEFAULT_BOOTSTRAP_TAG_NAME,
19
+ DEFAULT_BOOTSTRAP_TAG_TTL,
20
+ DEFAULT_BOOTSTRAP_TAG_VALUE,
21
+ DEFAULT_NODE_REQUIREMENTS
22
+ } from "./constants.js";
23
+ import { DnsNodeDiscovery } from "./dns.js";
24
+
25
+ const log = new Logger("peer-discovery-dns");
26
+
27
+ /**
28
+ * Parse options and expose function to return bootstrap peer addresses.
29
+ */
30
+ export class PeerDiscoveryDns
31
+ extends TypedEventEmitter<PeerDiscoveryEvents>
32
+ implements PeerDiscovery
33
+ {
34
+ private nextPeer: (() => AsyncGenerator<IEnr>) | undefined;
35
+ private _started: boolean;
36
+ private _components: DnsDiscoveryComponents;
37
+ private _options: DnsDiscOptions;
38
+
39
+ constructor(components: DnsDiscoveryComponents, options: DnsDiscOptions) {
40
+ super();
41
+ this._started = false;
42
+ this._components = components;
43
+ this._options = options;
44
+
45
+ const { enrUrls } = options;
46
+ log.info("Use following EIP-1459 ENR Tree URLs: ", enrUrls);
47
+ }
48
+
49
+ /**
50
+ * Start discovery process
51
+ */
52
+ async start(): Promise<void> {
53
+ log.info("Starting peer discovery via dns");
54
+
55
+ this._started = true;
56
+
57
+ if (this.nextPeer === undefined) {
58
+ let { enrUrls } = this._options;
59
+ if (!Array.isArray(enrUrls)) enrUrls = [enrUrls];
60
+
61
+ const { wantedNodeCapabilityCount } = this._options;
62
+ const dns = await DnsNodeDiscovery.dnsOverHttp();
63
+
64
+ this.nextPeer = dns.getNextPeer.bind(
65
+ dns,
66
+ enrUrls,
67
+ wantedNodeCapabilityCount
68
+ );
69
+ }
70
+
71
+ for await (const peerEnr of this.nextPeer()) {
72
+ if (!this._started) {
73
+ return;
74
+ }
75
+
76
+ const { peerInfo, shardInfo } = peerEnr;
77
+
78
+ if (!peerInfo) {
79
+ continue;
80
+ }
81
+
82
+ const tagsToUpdate = {
83
+ [DEFAULT_BOOTSTRAP_TAG_NAME]: {
84
+ value: this._options.tagValue ?? DEFAULT_BOOTSTRAP_TAG_VALUE,
85
+ ttl: this._options.tagTTL ?? DEFAULT_BOOTSTRAP_TAG_TTL
86
+ }
87
+ };
88
+
89
+ let isPeerChanged = false;
90
+ const isPeerExists = await this._components.peerStore.has(peerInfo.id);
91
+
92
+ if (isPeerExists) {
93
+ const peer = await this._components.peerStore.get(peerInfo.id);
94
+ const hasBootstrapTag = peer.tags.has(DEFAULT_BOOTSTRAP_TAG_NAME);
95
+
96
+ if (!hasBootstrapTag) {
97
+ isPeerChanged = true;
98
+ await this._components.peerStore.merge(peerInfo.id, {
99
+ tags: tagsToUpdate
100
+ });
101
+ }
102
+ } else {
103
+ isPeerChanged = true;
104
+ await this._components.peerStore.save(peerInfo.id, {
105
+ tags: tagsToUpdate,
106
+ ...(shardInfo && {
107
+ metadata: {
108
+ shardInfo: encodeRelayShard(shardInfo)
109
+ }
110
+ })
111
+ });
112
+ }
113
+
114
+ if (isPeerChanged) {
115
+ this.dispatchEvent(
116
+ new CustomEvent<PeerInfo>("peer", { detail: peerInfo })
117
+ );
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Stop emitting events
124
+ */
125
+ stop(): void {
126
+ this._started = false;
127
+ }
128
+
129
+ get [symbol](): true {
130
+ return true;
131
+ }
132
+
133
+ get [Symbol.toStringTag](): string {
134
+ return "@waku/bootstrap";
135
+ }
136
+ }
137
+
138
+ export function wakuDnsDiscovery(
139
+ enrUrls: string[],
140
+ wantedNodeCapabilityCount: Partial<NodeCapabilityCount> = DEFAULT_NODE_REQUIREMENTS
141
+ ): (components: DnsDiscoveryComponents) => PeerDiscoveryDns {
142
+ return (components: DnsDiscoveryComponents) =>
143
+ new PeerDiscoveryDns(components, { enrUrls, wantedNodeCapabilityCount });
144
+ }
@@ -0,0 +1,83 @@
1
+ import type { DnsClient } from "@waku/interfaces";
2
+ import { Logger } from "@waku/utils";
3
+ import { bytesToUtf8 } from "@waku/utils/bytes";
4
+ import { Endpoint, query, wellknown } from "dns-query";
5
+
6
+ const log = new Logger("dns-over-https");
7
+
8
+ export class DnsOverHttps implements DnsClient {
9
+ /**
10
+ * Create new Dns-Over-Http DNS client.
11
+ *
12
+ * @param endpoints The endpoints for Dns-Over-Https queries;
13
+ * Defaults to using dns-query's API..
14
+ * @param retries Retries if a given endpoint fails.
15
+ *
16
+ * @throws {code: string} If DNS query fails.
17
+ */
18
+ public static async create(
19
+ endpoints?: Endpoint[],
20
+ retries?: number
21
+ ): Promise<DnsOverHttps> {
22
+ const _endpoints = endpoints ?? (await wellknown.endpoints("doh"));
23
+
24
+ return new DnsOverHttps(_endpoints, retries);
25
+ }
26
+
27
+ private constructor(
28
+ private endpoints: Endpoint[],
29
+ private retries: number = 3
30
+ ) {}
31
+
32
+ /**
33
+ * Resolves a TXT record
34
+ *
35
+ * @param domain The domain name
36
+ *
37
+ * @throws if the query fails
38
+ */
39
+ async resolveTXT(domain: string): Promise<string[]> {
40
+ let answers;
41
+ try {
42
+ const res = await query(
43
+ {
44
+ question: { type: "TXT", name: domain }
45
+ },
46
+ {
47
+ endpoints: this.endpoints,
48
+ retries: this.retries
49
+ }
50
+ );
51
+ answers = res.answers;
52
+ } catch (error) {
53
+ log.error("query failed: ", error);
54
+ throw new Error("DNS query failed");
55
+ }
56
+
57
+ if (!answers) throw new Error(`Could not resolve ${domain}`);
58
+
59
+ const data = answers.map((a) => a.data) as
60
+ | Array<string | Uint8Array>
61
+ | Array<Array<string | Uint8Array>>;
62
+
63
+ const result: string[] = [];
64
+
65
+ data.forEach((d) => {
66
+ if (typeof d === "string") {
67
+ result.push(d);
68
+ } else if (Array.isArray(d)) {
69
+ d.forEach((sd) => {
70
+ if (typeof sd === "string") {
71
+ result.push(sd);
72
+ } else {
73
+ result.push(bytesToUtf8(sd));
74
+ }
75
+ });
76
+ } else {
77
+ result.push(bytesToUtf8(d));
78
+ }
79
+ });
80
+
81
+ return result;
82
+ }
83
+ }