@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,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 = 100_000_000;
|
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,109 @@
|
|
1
|
+
{
|
2
|
+
"name": "@waku/discovery",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"description": "Contains various discovery mechanisms: DNS Discovery (EIP-1459, Peer Exchange, Local Peer Cache Discovery.",
|
5
|
+
"types": "./dist/index.d.ts",
|
6
|
+
"module": "./dist/index.js",
|
7
|
+
"exports": {
|
8
|
+
".": {
|
9
|
+
"types": "./dist/index.d.ts",
|
10
|
+
"import": "./dist/index.js"
|
11
|
+
}
|
12
|
+
},
|
13
|
+
"type": "module",
|
14
|
+
"author": "Waku Team",
|
15
|
+
"homepage": "https://github.com/waku-org/js-waku/tree/master/packages/discovery#readme",
|
16
|
+
"repository": {
|
17
|
+
"type": "git",
|
18
|
+
"url": "https://github.com/waku-org/js-waku.git"
|
19
|
+
},
|
20
|
+
"bugs": {
|
21
|
+
"url": "https://github.com/waku-org/js-waku/issues"
|
22
|
+
},
|
23
|
+
"license": "MIT OR Apache-2.0",
|
24
|
+
"keywords": [
|
25
|
+
"waku",
|
26
|
+
"decentralized",
|
27
|
+
"secure",
|
28
|
+
"communication",
|
29
|
+
"web3",
|
30
|
+
"ethereum",
|
31
|
+
"dapps",
|
32
|
+
"privacy"
|
33
|
+
],
|
34
|
+
"scripts": {
|
35
|
+
"build": "run-s build:**",
|
36
|
+
"build:esm": "tsc",
|
37
|
+
"build:bundle": "rollup --config rollup.config.js",
|
38
|
+
"fix": "run-s fix:*",
|
39
|
+
"fix:lint": "eslint src *.js --fix",
|
40
|
+
"check": "run-s check:*",
|
41
|
+
"check:lint": "eslint src --ext .ts",
|
42
|
+
"check:spelling": "cspell \"{README.md,src/**/*.ts}\"",
|
43
|
+
"check:tsc": "tsc -p tsconfig.dev.json",
|
44
|
+
"prepublish": "npm run build",
|
45
|
+
"reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build",
|
46
|
+
"test": "NODE_ENV=test run-s test:*",
|
47
|
+
"test:node": "NODE_ENV=test TS_NODE_PROJECT=./tsconfig.dev.json mocha",
|
48
|
+
"test:browser": "NODE_ENV=test karma start karma.conf.cjs"
|
49
|
+
},
|
50
|
+
"engines": {
|
51
|
+
"node": ">=18"
|
52
|
+
},
|
53
|
+
"dependencies": {
|
54
|
+
"@waku/interfaces": "0.0.23",
|
55
|
+
"@waku/proto": "^0.0.6",
|
56
|
+
"@waku/enr": "0.0.22",
|
57
|
+
"@waku/core": "0.0.28",
|
58
|
+
"@waku/utils": "0.0.16",
|
59
|
+
"debug": "^4.3.4",
|
60
|
+
"dns-query": "^0.11.2",
|
61
|
+
"hi-base32": "^0.5.1",
|
62
|
+
"uint8arrays": "^5.0.1"
|
63
|
+
},
|
64
|
+
"devDependencies": {
|
65
|
+
"@libp2p/peer-id": "^4.0.4",
|
66
|
+
"@libp2p/peer-id-factory": "^4.0.5",
|
67
|
+
"@multiformats/multiaddr": "^12.0.0",
|
68
|
+
"@rollup/plugin-commonjs": "^25.0.7",
|
69
|
+
"@rollup/plugin-json": "^6.0.0",
|
70
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
71
|
+
"@types/chai": "^4.3.11",
|
72
|
+
"@types/node-localstorage": "^1.3.3",
|
73
|
+
"@waku/build-utils": "*",
|
74
|
+
"chai": "^4.3.10",
|
75
|
+
"chai-as-promised": "^7.1.1",
|
76
|
+
"cspell": "^8.6.1",
|
77
|
+
"mocha": "^10.3.0",
|
78
|
+
"node-localstorage": "^3.0.5",
|
79
|
+
"npm-run-all": "^4.1.5",
|
80
|
+
"rollup": "^4.12.0",
|
81
|
+
"sinon": "^17.0.1"
|
82
|
+
},
|
83
|
+
"peerDependencies": {
|
84
|
+
"@waku/core": "0.0.27",
|
85
|
+
"@waku/enr": "0.0.21",
|
86
|
+
"@waku/interfaces": "0.0.22",
|
87
|
+
"@waku/proto": "0.0.6",
|
88
|
+
"@waku/utils": "0.0.15",
|
89
|
+
"@libp2p/interface": "^1.1.2"
|
90
|
+
},
|
91
|
+
"peerDependenciesMeta": {
|
92
|
+
"@waku/interfaces": {
|
93
|
+
"optional": true
|
94
|
+
},
|
95
|
+
"@libp2p/interface": {
|
96
|
+
"optional": true
|
97
|
+
}
|
98
|
+
},
|
99
|
+
"files": [
|
100
|
+
"dist",
|
101
|
+
"bundle",
|
102
|
+
"src/**/*.ts",
|
103
|
+
"!**/*.spec.*",
|
104
|
+
"!**/*.json",
|
105
|
+
"CHANGELOG.md",
|
106
|
+
"LICENSE",
|
107
|
+
"README.md"
|
108
|
+
]
|
109
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import type { NodeCapabilityCount } from "@waku/interfaces";
|
2
|
+
|
3
|
+
export const enrTree = {
|
4
|
+
TEST: "enrtree://AOGYWMBYOUIMOENHXCHILPKY3ZRFEULMFI4DOM442QSZ73TT2A7VI@test.waku.nodes.status.im",
|
5
|
+
SANDBOX:
|
6
|
+
"enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im"
|
7
|
+
};
|
8
|
+
|
9
|
+
export const DEFAULT_BOOTSTRAP_TAG_NAME = "bootstrap";
|
10
|
+
export const DEFAULT_BOOTSTRAP_TAG_VALUE = 50;
|
11
|
+
export const DEFAULT_BOOTSTRAP_TAG_TTL = 100_000_000;
|
12
|
+
|
13
|
+
export const DEFAULT_NODE_REQUIREMENTS: Partial<NodeCapabilityCount> = {
|
14
|
+
store: 2,
|
15
|
+
filter: 1,
|
16
|
+
lightPush: 1
|
17
|
+
};
|
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
|
+
}
|