@waku/discovery 0.0.1

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 (56) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/bundle/index.js +25137 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/dns/constants.d.ts +9 -0
  5. package/dist/dns/constants.js +13 -0
  6. package/dist/dns/constants.js.map +1 -0
  7. package/dist/dns/dns.d.ts +32 -0
  8. package/dist/dns/dns.js +160 -0
  9. package/dist/dns/dns.js.map +1 -0
  10. package/dist/dns/dns_discovery.d.ts +24 -0
  11. package/dist/dns/dns_discovery.js +95 -0
  12. package/dist/dns/dns_discovery.js.map +1 -0
  13. package/dist/dns/dns_over_https.d.ts +25 -0
  14. package/dist/dns/dns_over_https.js +72 -0
  15. package/dist/dns/dns_over_https.js.map +1 -0
  16. package/dist/dns/enrtree.d.ts +33 -0
  17. package/dist/dns/enrtree.js +76 -0
  18. package/dist/dns/enrtree.js.map +1 -0
  19. package/dist/dns/fetch_nodes.d.ts +13 -0
  20. package/dist/dns/fetch_nodes.js +133 -0
  21. package/dist/dns/fetch_nodes.js.map +1 -0
  22. package/dist/dns/index.d.ts +3 -0
  23. package/dist/dns/index.js +4 -0
  24. package/dist/dns/index.js.map +1 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.js +10 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/local-peer-cache/index.d.ts +24 -0
  29. package/dist/local-peer-cache/index.js +106 -0
  30. package/dist/local-peer-cache/index.js.map +1 -0
  31. package/dist/peer-exchange/index.d.ts +2 -0
  32. package/dist/peer-exchange/index.js +3 -0
  33. package/dist/peer-exchange/index.js.map +1 -0
  34. package/dist/peer-exchange/rpc.d.ts +22 -0
  35. package/dist/peer-exchange/rpc.js +41 -0
  36. package/dist/peer-exchange/rpc.js.map +1 -0
  37. package/dist/peer-exchange/waku_peer_exchange.d.ts +21 -0
  38. package/dist/peer-exchange/waku_peer_exchange.js +80 -0
  39. package/dist/peer-exchange/waku_peer_exchange.js.map +1 -0
  40. package/dist/peer-exchange/waku_peer_exchange_discovery.d.ts +53 -0
  41. package/dist/peer-exchange/waku_peer_exchange_discovery.js +136 -0
  42. package/dist/peer-exchange/waku_peer_exchange_discovery.js.map +1 -0
  43. package/package.json +109 -0
  44. package/src/dns/constants.ts +17 -0
  45. package/src/dns/dns.ts +215 -0
  46. package/src/dns/dns_discovery.ts +144 -0
  47. package/src/dns/dns_over_https.ts +83 -0
  48. package/src/dns/enrtree.ts +123 -0
  49. package/src/dns/fetch_nodes.ts +181 -0
  50. package/src/dns/index.ts +3 -0
  51. package/src/index.ts +21 -0
  52. package/src/local-peer-cache/index.ts +160 -0
  53. package/src/peer-exchange/index.ts +11 -0
  54. package/src/peer-exchange/rpc.ts +44 -0
  55. package/src/peer-exchange/waku_peer_exchange.ts +111 -0
  56. package/src/peer-exchange/waku_peer_exchange_discovery.ts +238 -0
@@ -0,0 +1,111 @@
1
+ import { BaseProtocol } from "@waku/core/lib/base_protocol";
2
+ import { EnrDecoder } from "@waku/enr";
3
+ import {
4
+ IPeerExchange,
5
+ Libp2pComponents,
6
+ PeerExchangeQueryParams,
7
+ PeerExchangeResult,
8
+ ProtocolError,
9
+ PubsubTopic
10
+ } from "@waku/interfaces";
11
+ import { isDefined } from "@waku/utils";
12
+ import { Logger } from "@waku/utils";
13
+ import all from "it-all";
14
+ import * as lp from "it-length-prefixed";
15
+ import { pipe } from "it-pipe";
16
+ import { Uint8ArrayList } from "uint8arraylist";
17
+
18
+ import { PeerExchangeRPC } from "./rpc.js";
19
+
20
+ export const PeerExchangeCodec = "/vac/waku/peer-exchange/2.0.0-alpha1";
21
+
22
+ const log = new Logger("peer-exchange");
23
+
24
+ /**
25
+ * Implementation of the Peer Exchange protocol (https://rfc.vac.dev/spec/34/)
26
+ */
27
+ export class WakuPeerExchange extends BaseProtocol implements IPeerExchange {
28
+ /**
29
+ * @param components - libp2p components
30
+ */
31
+ constructor(components: Libp2pComponents, pubsubTopics: PubsubTopic[]) {
32
+ super(PeerExchangeCodec, components, log, pubsubTopics);
33
+ }
34
+
35
+ /**
36
+ * Make a peer exchange query to a peer
37
+ */
38
+ async query(params: PeerExchangeQueryParams): Promise<PeerExchangeResult> {
39
+ const { numPeers } = params;
40
+ const rpcQuery = PeerExchangeRPC.createRequest({
41
+ numPeers: BigInt(numPeers)
42
+ });
43
+
44
+ const peer = await this.peerStore.get(params.peerId);
45
+ if (!peer) {
46
+ return {
47
+ peerInfos: null,
48
+ error: ProtocolError.NO_PEER_AVAILABLE
49
+ };
50
+ }
51
+
52
+ const stream = await this.getStream(peer);
53
+
54
+ const res = await pipe(
55
+ [rpcQuery.encode()],
56
+ lp.encode,
57
+ stream,
58
+ lp.decode,
59
+ async (source) => await all(source)
60
+ );
61
+
62
+ try {
63
+ const bytes = new Uint8ArrayList();
64
+ res.forEach((chunk) => {
65
+ bytes.append(chunk);
66
+ });
67
+
68
+ const { response } = PeerExchangeRPC.decode(bytes);
69
+ if (!response) {
70
+ log.error(
71
+ "PeerExchangeRPC message did not contains a `response` field"
72
+ );
73
+ return {
74
+ peerInfos: null,
75
+ error: ProtocolError.EMPTY_PAYLOAD
76
+ };
77
+ }
78
+
79
+ const peerInfos = await Promise.all(
80
+ response.peerInfos
81
+ .map((peerInfo) => peerInfo.enr)
82
+ .filter(isDefined)
83
+ .map(async (enr) => {
84
+ return { ENR: await EnrDecoder.fromRLP(enr) };
85
+ })
86
+ );
87
+
88
+ return {
89
+ peerInfos,
90
+ error: null
91
+ };
92
+ } catch (err) {
93
+ log.error("Failed to decode push reply", err);
94
+ return {
95
+ peerInfos: null,
96
+ error: ProtocolError.DECODE_FAILED
97
+ };
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ *
104
+ * @returns A function that creates a new peer exchange protocol
105
+ */
106
+ export function wakuPeerExchange(
107
+ pubsubTopics: PubsubTopic[]
108
+ ): (components: Libp2pComponents) => WakuPeerExchange {
109
+ return (components: Libp2pComponents) =>
110
+ new WakuPeerExchange(components, pubsubTopics);
111
+ }
@@ -0,0 +1,238 @@
1
+ import { CustomEvent, TypedEventEmitter } from "@libp2p/interface";
2
+ import { peerDiscoverySymbol as symbol } from "@libp2p/interface";
3
+ import type {
4
+ IdentifyResult,
5
+ PeerDiscovery,
6
+ PeerDiscoveryEvents,
7
+ PeerId,
8
+ PeerInfo
9
+ } from "@libp2p/interface";
10
+ import {
11
+ Libp2pComponents,
12
+ PeerExchangeResult,
13
+ PubsubTopic,
14
+ Tags
15
+ } from "@waku/interfaces";
16
+ import { encodeRelayShard, Logger } from "@waku/utils";
17
+
18
+ import { PeerExchangeCodec, WakuPeerExchange } from "./waku_peer_exchange.js";
19
+
20
+ const log = new Logger("peer-exchange-discovery");
21
+
22
+ const DEFAULT_PEER_EXCHANGE_REQUEST_NODES = 10;
23
+ const DEFAULT_PEER_EXCHANGE_QUERY_INTERVAL_MS = 10 * 1000;
24
+ const DEFAULT_MAX_RETRIES = 3;
25
+
26
+ export interface Options {
27
+ /**
28
+ * Tag a bootstrap peer with this name before "discovering" it (default: 'bootstrap')
29
+ */
30
+ tagName?: string;
31
+
32
+ /**
33
+ * The bootstrap peer tag will have this value (default: 50)
34
+ */
35
+ tagValue?: number;
36
+
37
+ /**
38
+ * Cause the bootstrap peer tag to be removed after this number of ms (default: 2 minutes)
39
+ */
40
+ tagTTL?: number;
41
+ /**
42
+ * The interval between queries to a peer (default: 10 seconds)
43
+ * The interval will increase by a factor of an incrementing number (starting at 1)
44
+ * until it reaches the maximum attempts before backoff
45
+ */
46
+ queryInterval?: number;
47
+ /**
48
+ * The number of attempts before the queries to a peer are aborted (default: 3)
49
+ */
50
+ maxRetries?: number;
51
+ }
52
+
53
+ export const DEFAULT_PEER_EXCHANGE_TAG_NAME = Tags.PEER_EXCHANGE;
54
+ const DEFAULT_PEER_EXCHANGE_TAG_VALUE = 50;
55
+ const DEFAULT_PEER_EXCHANGE_TAG_TTL = 100_000_000;
56
+
57
+ export class PeerExchangeDiscovery
58
+ extends TypedEventEmitter<PeerDiscoveryEvents>
59
+ implements PeerDiscovery
60
+ {
61
+ private readonly components: Libp2pComponents;
62
+ private readonly peerExchange: WakuPeerExchange;
63
+ private readonly options: Options;
64
+ private isStarted: boolean;
65
+ private queryingPeers: Set<string> = new Set();
66
+ private queryAttempts: Map<string, number> = new Map();
67
+
68
+ private readonly handleDiscoveredPeer = (
69
+ event: CustomEvent<IdentifyResult>
70
+ ): void => {
71
+ const { protocols, peerId } = event.detail;
72
+
73
+ if (
74
+ !protocols.includes(PeerExchangeCodec) ||
75
+ this.queryingPeers.has(peerId.toString())
76
+ )
77
+ return;
78
+
79
+ this.queryingPeers.add(peerId.toString());
80
+ this.startRecurringQueries(peerId).catch((error) =>
81
+ log.error(`Error querying peer ${error}`)
82
+ );
83
+ };
84
+
85
+ constructor(
86
+ components: Libp2pComponents,
87
+ pubsubTopics: PubsubTopic[],
88
+ options: Options = {}
89
+ ) {
90
+ super();
91
+ this.components = components;
92
+ this.peerExchange = new WakuPeerExchange(components, pubsubTopics);
93
+ this.options = options;
94
+ this.isStarted = false;
95
+ }
96
+
97
+ /**
98
+ * Start emitting events
99
+ */
100
+ start(): void {
101
+ if (this.isStarted) {
102
+ return;
103
+ }
104
+
105
+ log.info("Starting peer exchange node discovery, discovering peers");
106
+
107
+ // might be better to use "peer:identify" or "peer:update"
108
+ this.components.events.addEventListener(
109
+ "peer:identify",
110
+ this.handleDiscoveredPeer
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Remove event listener
116
+ */
117
+ stop(): void {
118
+ if (!this.isStarted) return;
119
+ log.info("Stopping peer exchange node discovery");
120
+ this.isStarted = false;
121
+ this.queryingPeers.clear();
122
+ this.components.events.removeEventListener(
123
+ "peer:identify",
124
+ this.handleDiscoveredPeer
125
+ );
126
+ }
127
+
128
+ get [symbol](): true {
129
+ return true;
130
+ }
131
+
132
+ get [Symbol.toStringTag](): string {
133
+ return "@waku/peer-exchange";
134
+ }
135
+
136
+ private readonly startRecurringQueries = async (
137
+ peerId: PeerId
138
+ ): Promise<void> => {
139
+ const peerIdStr = peerId.toString();
140
+ const {
141
+ queryInterval = DEFAULT_PEER_EXCHANGE_QUERY_INTERVAL_MS,
142
+ maxRetries = DEFAULT_MAX_RETRIES
143
+ } = this.options;
144
+
145
+ log.info(
146
+ `Querying peer: ${peerIdStr} (attempt ${
147
+ this.queryAttempts.get(peerIdStr) ?? 1
148
+ })`
149
+ );
150
+
151
+ await this.query(peerId);
152
+
153
+ const currentAttempt = this.queryAttempts.get(peerIdStr) ?? 1;
154
+
155
+ if (currentAttempt > maxRetries) {
156
+ this.abortQueriesForPeer(peerIdStr);
157
+ return;
158
+ }
159
+
160
+ setTimeout(() => {
161
+ this.queryAttempts.set(peerIdStr, currentAttempt + 1);
162
+ this.startRecurringQueries(peerId).catch((error) => {
163
+ log.error(`Error in startRecurringQueries: ${error}`);
164
+ });
165
+ }, queryInterval * currentAttempt);
166
+ };
167
+
168
+ private async query(peerId: PeerId): Promise<PeerExchangeResult> {
169
+ const { error, peerInfos } = await this.peerExchange.query({
170
+ numPeers: DEFAULT_PEER_EXCHANGE_REQUEST_NODES,
171
+ peerId
172
+ });
173
+
174
+ if (error) {
175
+ log.error("Peer exchange query failed", error);
176
+ return { error, peerInfos: null };
177
+ }
178
+
179
+ for (const _peerInfo of peerInfos) {
180
+ const { ENR } = _peerInfo;
181
+ if (!ENR) {
182
+ log.warn("No ENR in peerInfo object, skipping");
183
+ continue;
184
+ }
185
+
186
+ const { peerId, peerInfo, shardInfo } = ENR;
187
+ if (!peerId || !peerInfo) {
188
+ continue;
189
+ }
190
+
191
+ const hasPeer = await this.components.peerStore.has(peerId);
192
+ if (hasPeer) {
193
+ continue;
194
+ }
195
+
196
+ // update the tags for the peer
197
+ await this.components.peerStore.save(peerId, {
198
+ tags: {
199
+ [DEFAULT_PEER_EXCHANGE_TAG_NAME]: {
200
+ value: this.options.tagValue ?? DEFAULT_PEER_EXCHANGE_TAG_VALUE,
201
+ ttl: this.options.tagTTL ?? DEFAULT_PEER_EXCHANGE_TAG_TTL
202
+ }
203
+ },
204
+ ...(shardInfo && {
205
+ metadata: {
206
+ shardInfo: encodeRelayShard(shardInfo)
207
+ }
208
+ })
209
+ });
210
+
211
+ log.info(`Discovered peer: ${peerId.toString()}`);
212
+
213
+ this.dispatchEvent(
214
+ new CustomEvent<PeerInfo>("peer", {
215
+ detail: {
216
+ id: peerId,
217
+ multiaddrs: peerInfo.multiaddrs
218
+ }
219
+ })
220
+ );
221
+ }
222
+
223
+ return { error: null, peerInfos };
224
+ }
225
+
226
+ private abortQueriesForPeer(peerIdStr: string): void {
227
+ log.info(`Aborting queries for peer: ${peerIdStr}`);
228
+ this.queryingPeers.delete(peerIdStr);
229
+ this.queryAttempts.delete(peerIdStr);
230
+ }
231
+ }
232
+
233
+ export function wakuPeerExchangeDiscovery(
234
+ pubsubTopics: PubsubTopic[]
235
+ ): (components: Libp2pComponents) => PeerExchangeDiscovery {
236
+ return (components: Libp2pComponents) =>
237
+ new PeerExchangeDiscovery(components, pubsubTopics);
238
+ }