UNPKG

@waku/discovery

Version:

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

318 lines (270 loc) 8.79 kB
import { TypedEventEmitter } from "@libp2p/interface"; import { peerDiscoverySymbol as symbol } from "@libp2p/interface"; import type { IdentifyResult, PeerDiscovery, PeerDiscoveryEvents, PeerId, PeerInfo } from "@libp2p/interface"; import { type Libp2pComponents, type PeerExchangeQueryResult, ShardInfo, 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 interface Options { /** * Tag a bootstrap peer with this name before "discovering" it (default: 'bootstrap') */ tagName?: string; /** * The bootstrap peer tag will have this value (default: 50) */ tagValue?: number; /** * Cause the bootstrap peer tag to be removed after this number of ms (default: 2 minutes) */ tagTTL?: number; /** * The interval between queries to a peer (default: 10 seconds) * The interval will increase by a factor of an incrementing number (starting at 1) * until it reaches the maximum attempts before backoff */ queryInterval?: number; /** * The number of attempts before the queries to a peer are aborted (default: 3) */ maxRetries?: number; } interface CustomDiscoveryEvent extends PeerDiscoveryEvents { "waku:peer-exchange:started": CustomEvent<boolean>; } 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<CustomDiscoveryEvent> implements PeerDiscovery { private readonly components: Libp2pComponents; private readonly peerExchange: WakuPeerExchange; private readonly options: Options; private isStarted: boolean; private queryingPeers: Set<string> = new Set(); private queryAttempts: Map<string, number> = new Map(); private readonly handleDiscoveredPeer = ( event: CustomEvent<IdentifyResult> ): void => { 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}`) ); }; public constructor(components: Libp2pComponents, options: Options = {}) { super(); this.components = components; this.peerExchange = new WakuPeerExchange(components); this.options = options; this.isStarted = false; } /** * Start emitting events */ public start(): void { 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 */ public stop(): void { 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 ); } public get [symbol](): true { return true; } public get [Symbol.toStringTag](): string { return "@waku/peer-exchange"; } private readonly startRecurringQueries = async ( peerId: PeerId ): Promise<void> => { 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); }; private async query(peerId: PeerId): Promise<PeerExchangeQueryResult> { 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<PeerInfo>("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<PeerInfo>("peer", { detail: { id: peerId, multiaddrs: peerInfo.multiaddrs } }) ); } return { error: null, peerInfos }; } private abortQueriesForPeer(peerIdStr: string): void { log.info(`Aborting queries for peer: ${peerIdStr}`); this.queryingPeers.delete(peerIdStr); this.queryAttempts.delete(peerIdStr); } private async checkPeerInfoDiff( peerInfo: PeerInfo, shardInfo?: ShardInfo ): Promise<{ hasMultiaddrDiff: boolean; hasShardDiff: boolean }> { 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: boolean = 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(): ( components: Libp2pComponents ) => PeerExchangeDiscovery { return (components: Libp2pComponents) => new PeerExchangeDiscovery(components); }