@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.
- package/CHANGELOG.md +23 -0
- package/bundle/index.js +25137 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/dns/constants.d.ts +9 -0
- package/dist/dns/constants.js +13 -0
- package/dist/dns/constants.js.map +1 -0
- package/dist/dns/dns.d.ts +32 -0
- package/dist/dns/dns.js +160 -0
- package/dist/dns/dns.js.map +1 -0
- package/dist/dns/dns_discovery.d.ts +24 -0
- package/dist/dns/dns_discovery.js +95 -0
- package/dist/dns/dns_discovery.js.map +1 -0
- package/dist/dns/dns_over_https.d.ts +25 -0
- package/dist/dns/dns_over_https.js +72 -0
- package/dist/dns/dns_over_https.js.map +1 -0
- package/dist/dns/enrtree.d.ts +33 -0
- package/dist/dns/enrtree.js +76 -0
- package/dist/dns/enrtree.js.map +1 -0
- package/dist/dns/fetch_nodes.d.ts +13 -0
- package/dist/dns/fetch_nodes.js +133 -0
- package/dist/dns/fetch_nodes.js.map +1 -0
- package/dist/dns/index.d.ts +3 -0
- package/dist/dns/index.js +4 -0
- package/dist/dns/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/local-peer-cache/index.d.ts +24 -0
- package/dist/local-peer-cache/index.js +106 -0
- package/dist/local-peer-cache/index.js.map +1 -0
- package/dist/peer-exchange/index.d.ts +2 -0
- package/dist/peer-exchange/index.js +3 -0
- package/dist/peer-exchange/index.js.map +1 -0
- package/dist/peer-exchange/rpc.d.ts +22 -0
- package/dist/peer-exchange/rpc.js +41 -0
- package/dist/peer-exchange/rpc.js.map +1 -0
- package/dist/peer-exchange/waku_peer_exchange.d.ts +21 -0
- package/dist/peer-exchange/waku_peer_exchange.js +80 -0
- package/dist/peer-exchange/waku_peer_exchange.js.map +1 -0
- package/dist/peer-exchange/waku_peer_exchange_discovery.d.ts +53 -0
- package/dist/peer-exchange/waku_peer_exchange_discovery.js +136 -0
- package/dist/peer-exchange/waku_peer_exchange_discovery.js.map +1 -0
- package/package.json +109 -0
- package/src/dns/constants.ts +17 -0
- package/src/dns/dns.ts +215 -0
- package/src/dns/dns_discovery.ts +144 -0
- package/src/dns/dns_over_https.ts +83 -0
- package/src/dns/enrtree.ts +123 -0
- package/src/dns/fetch_nodes.ts +181 -0
- package/src/dns/index.ts +3 -0
- package/src/index.ts +21 -0
- package/src/local-peer-cache/index.ts +160 -0
- package/src/peer-exchange/index.ts +11 -0
- package/src/peer-exchange/rpc.ts +44 -0
- package/src/peer-exchange/waku_peer_exchange.ts +111 -0
- 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
|
+
}
|