UNPKG

@waku/discovery

Version:

Contains various discovery mechanisms: DNS Discovery (EIP-1459, Peer Exchange, Local Peer Cache Discovery.

182 lines 7.71 kB
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