@waku/discovery
Version:
Contains various discovery mechanisms: DNS Discovery (EIP-1459, Peer Exchange, Local Peer Cache Discovery.
165 lines • 6.3 kB
JavaScript
import { TypedEventEmitter } from "@libp2p/interface";
import { peerDiscoverySymbol as symbol } from "@libp2p/interface";
import { encodeRelayShard, Logger } from "@waku/utils";
import { DEFAULT_PEER_EXCHANGE_REQUEST_NODES, DEFAULT_PEER_EXCHANGE_TAG_NAME, DEFAULT_PEER_EXCHANGE_TAG_TTL, DEFAULT_PEER_EXCHANGE_TAG_VALUE, PeerExchangeCodec } from "./constants.js";
import { PeerExchange } from "./peer_exchange.js";
const log = new Logger("peer-exchange-discovery");
export class PeerExchangeDiscovery extends TypedEventEmitter {
components;
peerExchange;
options;
isStarted = false;
queryingPeers = new Set();
peerExpirationRecords = new Map();
continuousDiscoveryInterval = null;
constructor(components, options = {}) {
super();
this.components = components;
this.peerExchange = new PeerExchange(components);
this.options = {
...options,
TTL: options.TTL ?? DEFAULT_PEER_EXCHANGE_TAG_TTL
};
this.handleDiscoveredPeer = this.handleDiscoveredPeer.bind(this);
}
/**
* Start Peer Exchange.
* Subscribe to "peer:identify" events and handle them.
*/
start() {
if (this.isStarted) {
return;
}
log.info("Starting peer exchange node discovery, discovering peers");
this.isStarted = true;
this.components.events.addEventListener("peer:identify", this.handleDiscoveredPeer);
this.continuousDiscoveryInterval = setInterval(() => {
void this.handlePeriodicDiscovery();
}, this.options.TTL);
}
/**
* Stop Peer Exchange.
* Unsubscribe from "peer:identify" events.
*/
stop() {
if (!this.isStarted) {
return;
}
log.info("Stopping peer exchange node discovery");
this.isStarted = false;
this.queryingPeers.clear();
this.peerExpirationRecords.clear();
if (this.continuousDiscoveryInterval) {
clearInterval(this.continuousDiscoveryInterval);
}
this.components.events.removeEventListener("peer:identify", this.handleDiscoveredPeer);
}
get [symbol]() {
return true;
}
get [Symbol.toStringTag]() {
return "@waku/peer-exchange";
}
async handleDiscoveredPeer(event) {
void this.runQuery(event.detail.peerId, event.detail.protocols);
}
async handlePeriodicDiscovery() {
const connections = this.components.connectionManager.getConnections();
await Promise.all(connections.map(async (connection) => {
try {
const peerIdStr = connection.remotePeer.toString();
const shouldQuery = this.peerExpirationRecords.has(peerIdStr)
? this.peerExpirationRecords.get(peerIdStr) <= Date.now()
: true;
if (!shouldQuery) {
return null;
}
const peer = await this.components.peerStore.get(connection.remotePeer);
return this.runQuery(connection.remotePeer, peer.protocols);
}
catch (error) {
log.warn("Error getting peer info", error);
return null;
}
}));
}
async runQuery(peerId, protocols) {
if (!protocols.includes(PeerExchangeCodec) ||
this.queryingPeers.has(peerId.toString())) {
log.info(`Skipping peer ${peerId} as it is already querying or does not support peer exchange`);
return;
}
try {
this.queryingPeers.add(peerId.toString());
await this.query(peerId);
}
catch (error) {
log.error("Error querying peer", error);
}
this.peerExpirationRecords.set(peerId.toString(), Date.now() + this.options.TTL);
this.queryingPeers.delete(peerId.toString());
}
async query(peerId) {
const peerIdStr = peerId.toString();
log.info(`Querying peer exchange for ${peerIdStr}`);
const { error, peerInfos } = await this.peerExchange.query({
numPeers: DEFAULT_PEER_EXCHANGE_REQUEST_NODES,
peerId
});
if (error) {
log.error(`Peer exchange query to ${peerIdStr} failed`, error);
return;
}
for (const { ENR } of peerInfos) {
if (!ENR) {
log.warn(`No ENR in peerInfo object from ${peerIdStr}, skipping`);
continue;
}
const { peerInfo, shardInfo } = ENR;
if (!peerInfo) {
log.warn(`No peerInfo in ENR from ${peerIdStr}, skipping`);
continue;
}
const hasPrevShardInfo = await this.hasShardInfo(peerInfo.id);
const metadata = !hasPrevShardInfo && shardInfo
? { metadata: { shardInfo: encodeRelayShard(shardInfo) } }
: undefined;
// merge is smart enough to overwrite only changed parts
await this.components.peerStore.merge(peerInfo.id, {
tags: {
[DEFAULT_PEER_EXCHANGE_TAG_NAME]: {
value: DEFAULT_PEER_EXCHANGE_TAG_VALUE
}
},
...metadata,
...(peerInfo.multiaddrs && {
multiaddrs: peerInfo.multiaddrs
})
});
log.info(`Discovered peer: ${peerInfo.id.toString()}`);
this.dispatchEvent(new CustomEvent("peer", {
detail: {
id: peerInfo.id,
multiaddrs: peerInfo.multiaddrs
}
}));
}
}
async hasShardInfo(peerId) {
try {
const peer = await this.components.peerStore.get(peerId);
if (!peer) {
return false;
}
return peer.metadata.has("shardInfo");
}
catch (err) {
log.warn(`Error getting shard info for ${peerId.toString()}`, err);
}
return false;
}
}
export function wakuPeerExchangeDiscovery(options = {}) {
return (components) => new PeerExchangeDiscovery(components, options);
}
//# sourceMappingURL=peer_exchange_discovery.js.map