@waku/discovery
Version:
Contains various discovery mechanisms: DNS Discovery (EIP-1459, Peer Exchange, Local Peer Cache Discovery.
182 lines • 7.71 kB
JavaScript
import { TypedEventEmitter } from "@libp2p/interface";
import { peerDiscoverySymbol as symbol } from "@libp2p/interface";
import { Tags } from "@waku/interfaces";
import { decodeRelayShard, encodeRelayShard, Logger } from "@waku/utils";
import { PeerExchangeCodec, WakuPeerExchange } from "./waku_peer_exchange.js";
const log = new Logger("peer-exchange-discovery");
const DEFAULT_PEER_EXCHANGE_REQUEST_NODES = 10;
const DEFAULT_PEER_EXCHANGE_QUERY_INTERVAL_MS = 10 * 1000;
const DEFAULT_MAX_RETRIES = 3;
export const DEFAULT_PEER_EXCHANGE_TAG_NAME = Tags.PEER_EXCHANGE;
const DEFAULT_PEER_EXCHANGE_TAG_VALUE = 50;
const DEFAULT_PEER_EXCHANGE_TAG_TTL = 100_000_000;
export class PeerExchangeDiscovery extends TypedEventEmitter {
components;
peerExchange;
options;
isStarted;
queryingPeers = new Set();
queryAttempts = new Map();
handleDiscoveredPeer = (event) => {
const { protocols, peerId } = event.detail;
if (!protocols.includes(PeerExchangeCodec) ||
this.queryingPeers.has(peerId.toString()))
return;
this.queryingPeers.add(peerId.toString());
this.startRecurringQueries(peerId).catch((error) => log.error(`Error querying peer ${error}`));
};
constructor(components, options = {}) {
super();
this.components = components;
this.peerExchange = new WakuPeerExchange(components);
this.options = options;
this.isStarted = false;
}
/**
* Start emitting events
*/
start() {
if (this.isStarted) {
return;
}
this.dispatchEvent(new CustomEvent("waku:peer-exchange:started", { detail: true }));
log.info("Starting peer exchange node discovery, discovering peers");
// might be better to use "peer:identify" or "peer:update"
this.components.events.addEventListener("peer:identify", this.handleDiscoveredPeer);
}
/**
* Remove event listener
*/
stop() {
if (!this.isStarted)
return;
log.info("Stopping peer exchange node discovery");
this.isStarted = false;
this.queryingPeers.clear();
this.components.events.removeEventListener("peer:identify", this.handleDiscoveredPeer);
}
get [symbol]() {
return true;
}
get [Symbol.toStringTag]() {
return "@waku/peer-exchange";
}
startRecurringQueries = async (peerId) => {
const peerIdStr = peerId.toString();
const { queryInterval = DEFAULT_PEER_EXCHANGE_QUERY_INTERVAL_MS, maxRetries = DEFAULT_MAX_RETRIES } = this.options;
log.info(`Querying peer: ${peerIdStr} (attempt ${this.queryAttempts.get(peerIdStr) ?? 1})`);
await this.query(peerId);
const currentAttempt = this.queryAttempts.get(peerIdStr) ?? 1;
if (currentAttempt > maxRetries) {
this.abortQueriesForPeer(peerIdStr);
return;
}
setTimeout(() => {
this.queryAttempts.set(peerIdStr, currentAttempt + 1);
this.startRecurringQueries(peerId).catch((error) => {
log.error(`Error in startRecurringQueries: ${error}`);
});
}, queryInterval * currentAttempt);
};
async query(peerId) {
const { error, peerInfos } = await this.peerExchange.query({
numPeers: DEFAULT_PEER_EXCHANGE_REQUEST_NODES,
peerId
});
if (error) {
log.error("Peer exchange query failed", error);
return { error, peerInfos: null };
}
for (const _peerInfo of peerInfos) {
const { ENR } = _peerInfo;
if (!ENR) {
log.warn("No ENR in peerInfo object, skipping");
continue;
}
const { peerId, peerInfo, shardInfo } = ENR;
if (!peerId || !peerInfo) {
continue;
}
const hasPeer = await this.components.peerStore.has(peerId);
if (hasPeer) {
const { hasMultiaddrDiff, hasShardDiff } = await this.checkPeerInfoDiff(peerInfo, shardInfo);
if (hasMultiaddrDiff || hasShardDiff) {
log.info(`Peer ${peerId.toString()} has updated multiaddrs or shardInfo, updating`);
if (hasMultiaddrDiff) {
log.info(`Peer ${peerId.toString()} has updated multiaddrs, updating`);
await this.components.peerStore.patch(peerId, {
multiaddrs: peerInfo.multiaddrs
});
}
if (hasShardDiff && shardInfo) {
log.info(`Peer ${peerId.toString()} has updated shardInfo, updating`);
await this.components.peerStore.merge(peerId, {
metadata: {
shardInfo: encodeRelayShard(shardInfo)
}
});
this.dispatchEvent(new CustomEvent("peer", {
detail: {
id: peerId,
multiaddrs: peerInfo.multiaddrs
}
}));
}
continue;
}
}
// update the tags for the peer
await this.components.peerStore.save(peerId, {
tags: {
[DEFAULT_PEER_EXCHANGE_TAG_NAME]: {
value: this.options.tagValue ?? DEFAULT_PEER_EXCHANGE_TAG_VALUE,
ttl: this.options.tagTTL ?? DEFAULT_PEER_EXCHANGE_TAG_TTL
}
},
...(shardInfo && {
metadata: {
shardInfo: encodeRelayShard(shardInfo)
}
}),
...(peerInfo.multiaddrs && {
multiaddrs: peerInfo.multiaddrs
})
});
log.info(`Discovered peer: ${peerId.toString()}`);
this.dispatchEvent(new CustomEvent("peer", {
detail: {
id: peerId,
multiaddrs: peerInfo.multiaddrs
}
}));
}
return { error: null, peerInfos };
}
abortQueriesForPeer(peerIdStr) {
log.info(`Aborting queries for peer: ${peerIdStr}`);
this.queryingPeers.delete(peerIdStr);
this.queryAttempts.delete(peerIdStr);
}
async checkPeerInfoDiff(peerInfo, shardInfo) {
const { id: peerId } = peerInfo;
const peer = await this.components.peerStore.get(peerId);
const existingMultiaddrs = peer.addresses.map((a) => a.multiaddr.toString());
const newMultiaddrs = peerInfo.multiaddrs.map((ma) => ma.toString());
const hasMultiaddrDiff = existingMultiaddrs.some((ma) => !newMultiaddrs.includes(ma));
let hasShardDiff = false;
const existingShardInfoBytes = peer.metadata.get("shardInfo");
if (existingShardInfoBytes) {
const existingShardInfo = decodeRelayShard(existingShardInfoBytes);
if (existingShardInfo || shardInfo) {
hasShardDiff =
existingShardInfo.clusterId !== shardInfo?.clusterId ||
existingShardInfo.shards.some((shard) => !shardInfo?.shards.includes(shard));
}
}
return { hasMultiaddrDiff, hasShardDiff };
}
}
export function wakuPeerExchangeDiscovery() {
return (components) => new PeerExchangeDiscovery(components);
}
//# sourceMappingURL=waku_peer_exchange_discovery.js.map