UNPKG

libp2p

Version:

JavaScript implementation of libp2p, a modular peer to peer network stack

540 lines (447 loc) • 18.7 kB
/* eslint-disable max-depth */ import { TimeoutError, DialError, AbortError } from '@libp2p/interface' import { PeerMap } from '@libp2p/peer-collections' import { PriorityQueue } from '@libp2p/utils/priority-queue' import { multiaddr } from '@multiformats/multiaddr' import { Circuit } from '@multiformats/multiaddr-matcher' import { anySignal } from 'any-signal' import { setMaxListeners } from 'main-event' import { CustomProgressEvent } from 'progress-events' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { DialDeniedError, NoValidAddressesError } from '../errors.js' import { getPeerAddress } from '../get-peer.js' import { defaultAddressSorter } from './address-sorter.js' import { DIAL_TIMEOUT, MAX_PARALLEL_DIALS, MAX_PEER_ADDRS_TO_DIAL, LAST_DIAL_FAILURE_KEY, MAX_DIAL_QUEUE_LENGTH, LAST_DIAL_SUCCESS_KEY } from './constants.js' import { resolveMultiaddr, dnsaddrResolver } from './resolvers/index.js' import { DEFAULT_DIAL_PRIORITY } from './index.js' import type { AddressSorter, ComponentLogger, Logger, Connection, ConnectionGater, Metrics, PeerId, Address, PeerStore, PeerRouting, IsDialableOptions, OpenConnectionProgressEvents, MultiaddrResolver } from '@libp2p/interface' import type { OpenConnectionOptions, TransportManager } from '@libp2p/interface-internal' import type { PriorityQueueJobOptions } from '@libp2p/utils/priority-queue' import type { DNS } from '@multiformats/dns' import type { Multiaddr } from '@multiformats/multiaddr' import type { ProgressOptions } from 'progress-events' export interface PendingDialTarget { resolve(value: any): void reject(err: Error): void } interface DialQueueJobOptions extends PriorityQueueJobOptions, ProgressOptions<OpenConnectionProgressEvents> { peerId?: PeerId multiaddrs: Set<string> } interface DialerInit { addressSorter?: AddressSorter maxParallelDials?: number maxDialQueueLength?: number maxPeerAddrsToDial?: number dialTimeout?: number resolvers?: Record<string, MultiaddrResolver> connections?: PeerMap<Connection[]> } const defaultOptions = { maxParallelDials: MAX_PARALLEL_DIALS, maxDialQueueLength: MAX_DIAL_QUEUE_LENGTH, maxPeerAddrsToDial: MAX_PEER_ADDRS_TO_DIAL, dialTimeout: DIAL_TIMEOUT, resolvers: { dnsaddr: dnsaddrResolver } } interface DialQueueComponents { peerId: PeerId metrics?: Metrics peerStore: PeerStore peerRouting: PeerRouting transportManager: TransportManager connectionGater: ConnectionGater logger: ComponentLogger dns?: DNS } export class DialQueue { public queue: PriorityQueue<Connection, DialQueueJobOptions> private readonly components: DialQueueComponents private readonly addressSorter?: AddressSorter private readonly maxPeerAddrsToDial: number private readonly maxDialQueueLength: number private readonly dialTimeout: number private shutDownController: AbortController private readonly connections: PeerMap<Connection[]> private readonly log: Logger private readonly resolvers: Record<string, MultiaddrResolver> constructor (components: DialQueueComponents, init: DialerInit = {}) { this.addressSorter = init.addressSorter this.maxPeerAddrsToDial = init.maxPeerAddrsToDial ?? defaultOptions.maxPeerAddrsToDial this.maxDialQueueLength = init.maxDialQueueLength ?? defaultOptions.maxDialQueueLength this.dialTimeout = init.dialTimeout ?? defaultOptions.dialTimeout this.connections = init.connections ?? new PeerMap() this.log = components.logger.forComponent('libp2p:connection-manager:dial-queue') this.components = components this.resolvers = init.resolvers ?? defaultOptions.resolvers this.shutDownController = new AbortController() setMaxListeners(Infinity, this.shutDownController.signal) // controls dial concurrency this.queue = new PriorityQueue({ concurrency: init.maxParallelDials ?? defaultOptions.maxParallelDials, metricName: 'libp2p_dial_queue', metrics: components.metrics }) // a started job errored this.queue.addEventListener('failure', (event) => { if (event.detail?.error.name !== AbortError.name) { this.log.error('error in dial queue - %e', event.detail) } }) } start (): void { this.shutDownController = new AbortController() setMaxListeners(Infinity, this.shutDownController.signal) } /** * Clears any pending dials */ stop (): void { this.shutDownController.abort() this.queue.abort() } /** * Connects to a given peer, multiaddr or list of multiaddrs. * * If a peer is passed, all known multiaddrs will be tried. If a multiaddr or * multiaddrs are passed only those will be dialled. * * Where a list of multiaddrs is passed, if any contain a peer id then all * multiaddrs in the list must contain the same peer id. * * The dial to the first address that is successfully able to upgrade a * connection will be used, all other dials will be aborted when that happens. */ async dial (peerIdOrMultiaddr: PeerId | Multiaddr | Multiaddr[], options: OpenConnectionOptions = {}): Promise<Connection> { const { peerId, multiaddrs } = getPeerAddress(peerIdOrMultiaddr) // make sure we don't have an existing non-limited connection to any of the // addresses we are about to dial const existingConnection = Array.from(this.connections.values()).flat().find(conn => { if (options.force === true) { return false } if (conn.limits != null) { return false } if (conn.remotePeer.equals(peerId)) { return true } return multiaddrs.find(addr => { return addr.equals(conn.remoteAddr) }) }) if (existingConnection?.status === 'open') { this.log('already connected to %a', existingConnection.remoteAddr) options.onProgress?.(new CustomProgressEvent('dial-queue:already-connected')) return existingConnection } // ready to dial, all async work finished - make sure we don't have any // pending dials in progress for this peer or set of multiaddrs const existingDial = this.queue.queue.find(job => { if (peerId?.equals(job.options.peerId) === true) { return true } // does the dial contain any of the target multiaddrs? const addresses = job.options.multiaddrs if (addresses == null) { return false } for (const multiaddr of multiaddrs) { if (addresses.has(multiaddr.toString())) { return true } } return false }) if (existingDial != null) { this.log('joining existing dial target for %p', peerId) // add all multiaddrs to the dial target for (const multiaddr of multiaddrs) { existingDial.options.multiaddrs.add(multiaddr.toString()) } options.onProgress?.(new CustomProgressEvent('dial-queue:already-in-dial-queue')) return existingDial.join(options) } if (this.queue.size >= this.maxDialQueueLength) { throw new DialError('Dial queue is full') } this.log('creating dial target for %p', peerId, multiaddrs.map(ma => ma.toString())) options.onProgress?.(new CustomProgressEvent('dial-queue:add-to-dial-queue')) return this.queue.add(async (options) => { options.onProgress?.(new CustomProgressEvent('dial-queue:start-dial')) // create abort conditions - need to do this before `calculateMultiaddrs` as // we may be about to resolve a dns addr which can time out const signal = anySignal([ this.shutDownController.signal, options.signal ]) setMaxListeners(Infinity, signal) try { return await this.dialPeer(options, signal) } finally { // clean up abort signals/controllers signal.clear() } }, { peerId, priority: options.priority ?? DEFAULT_DIAL_PRIORITY, multiaddrs: new Set(multiaddrs.map(ma => ma.toString())), signal: options.signal ?? AbortSignal.timeout(this.dialTimeout), onProgress: options.onProgress }) } private async dialPeer (options: DialQueueJobOptions, signal: AbortSignal): Promise<Connection> { const peerId = options.peerId const multiaddrs = options.multiaddrs const failedMultiaddrs = new Set<string>() // if we have no multiaddrs, only a peer id, set a flag so we will look the // peer up in the peer routing to obtain multiaddrs let forcePeerLookup = options.multiaddrs.size === 0 let dialed = 0 let dialIteration = 0 const errors: Error[] = [] this.log('starting dial to %p', peerId) // repeat this operation in case addresses are added to the dial while we // resolve multiaddrs, etc while (forcePeerLookup || multiaddrs.size > 0) { dialIteration++ // only perform peer lookup once forcePeerLookup = false // the addresses we will dial const addrsToDial: Address[] = [] // copy the addresses into a new set const addrs = new Set(options.multiaddrs) // empty the old set - subsequent dial attempts for the same peer id may // add more addresses to try multiaddrs.clear() this.log('calculating addrs to dial %p from %s', peerId, [...addrs]) // load addresses from address book, resolve and dnsaddrs, filter // undialables, add peer IDs, etc const calculatedAddrs = await this.calculateMultiaddrs(peerId, addrs, { ...options, signal }) for (const addr of calculatedAddrs) { // skip any addresses we have previously failed to dial if (failedMultiaddrs.has(addr.multiaddr.toString())) { this.log.trace('skipping previously failed multiaddr %a while dialing %p', addr.multiaddr, peerId) continue } addrsToDial.push(addr) } this.log('%s dial to %p with %s', dialIteration === 1 ? 'starting' : 'continuing', peerId, addrsToDial.map(ma => ma.multiaddr.toString())) options?.onProgress?.(new CustomProgressEvent<Address[]>('dial-queue:calculated-addresses', addrsToDial)) for (const address of addrsToDial) { if (dialed === this.maxPeerAddrsToDial) { this.log('dialed maxPeerAddrsToDial (%d) addresses for %p, not trying any others', dialed, options.peerId) throw new DialError('Peer had more than maxPeerAddrsToDial') } dialed++ try { // try to dial the address const conn = await this.components.transportManager.dial(address.multiaddr, { ...options, signal }) this.log('dial to %a succeeded', address.multiaddr) // record the successful dial and the address try { await this.components.peerStore.merge(conn.remotePeer, { multiaddrs: [ conn.remoteAddr ], metadata: { [LAST_DIAL_SUCCESS_KEY]: uint8ArrayFromString(Date.now().toString()) } }) } catch (err: any) { this.log.error('could not update last dial failure key for %p', peerId, err) } // dial successful, return the connection return conn } catch (err: any) { this.log.error('dial failed to %a', address.multiaddr, err) // ensure we don't dial it again in this attempt failedMultiaddrs.add(address.multiaddr.toString()) if (peerId != null) { // record the failed dial try { await this.components.peerStore.merge(peerId, { metadata: { [LAST_DIAL_FAILURE_KEY]: uint8ArrayFromString(Date.now().toString()) } }) } catch (err: any) { this.log.error('could not update last dial failure key for %p', peerId, err) } } // the user/dial timeout/shutdown controller signal aborted if (signal.aborted) { throw new TimeoutError(err.message) } errors.push(err) } } } if (errors.length === 1) { throw errors[0] } throw new AggregateError(errors, 'All multiaddr dials failed') } // eslint-disable-next-line complexity private async calculateMultiaddrs (peerId?: PeerId, multiaddrs: Set<string> = new Set<string>(), options: OpenConnectionOptions = {}): Promise<Address[]> { const addrs: Address[] = [...multiaddrs].map(ma => ({ multiaddr: multiaddr(ma), isCertified: false })) // if a peer id or multiaddr(s) with a peer id, make sure it isn't our peer id and that we are allowed to dial it if (peerId != null) { if (this.components.peerId.equals(peerId)) { throw new DialError('Tried to dial self') } if ((await this.components.connectionGater.denyDialPeer?.(peerId)) === true) { throw new DialDeniedError('The dial request is blocked by gater.allowDialPeer') } // if just a peer id was passed, load available multiaddrs for this peer // from the peer store if (addrs.length === 0) { this.log('loading multiaddrs for %p', peerId) try { const peer = await this.components.peerStore.get(peerId) addrs.push(...peer.addresses) this.log('loaded multiaddrs for %p', peerId, addrs.map(({ multiaddr }) => multiaddr.toString())) } catch (err: any) { if (err.name !== 'NotFoundError') { throw err } } } // if we still don't have any addresses for this peer, or the only // addresses we have are without any routing information (e.g. // `/p2p/Qmfoo`), try a lookup using the peer routing if (addrs.length === 0) { this.log('looking up multiaddrs for %p in the peer routing', peerId) try { const peerInfo = await this.components.peerRouting.findPeer(peerId, options) this.log('found multiaddrs for %p in the peer routing', peerId, addrs.map(({ multiaddr }) => multiaddr.toString())) addrs.push(...peerInfo.multiaddrs.map(multiaddr => ({ multiaddr, isCertified: false }))) } catch (err: any) { if (err.name === 'NoPeerRoutersError') { this.log('no peer routers configured', peerId) } else { this.log.error('looking up multiaddrs for %p in the peer routing failed - %e', peerId, err) } } } } // resolve addresses - this can result in a one-to-many translation when // dnsaddrs are resolved let resolvedAddresses = (await Promise.all( addrs.map(async addr => { const result = await resolveMultiaddr(addr.multiaddr, this.resolvers, { dns: this.components.dns, log: this.log, ...options }) if (result.length === 1 && result[0].equals(addr.multiaddr)) { return addr } return result.map(multiaddr => ({ multiaddr, isCertified: false })) }) )) .flat() // ensure the peer id is appended to the multiaddr if (peerId != null) { const peerIdMultiaddr = `/p2p/${peerId.toString()}` resolvedAddresses = resolvedAddresses.map(addr => { const lastComponent = addr.multiaddr.getComponents().pop() // append peer id to multiaddr if it is not already present if (lastComponent?.name !== 'p2p') { return { multiaddr: addr.multiaddr.encapsulate(peerIdMultiaddr), isCertified: addr.isCertified } } return addr }) } const filteredAddrs = resolvedAddresses.filter(addr => { // filter out any multiaddrs that we do not have transports for if (this.components.transportManager.dialTransportForMultiaddr(addr.multiaddr) == null) { return false } // if the resolved multiaddr has a PeerID but it's the wrong one, ignore it // - this can happen with addresses like bootstrap.libp2p.io that resolve // to multiple different peers const addrPeerId = addr.multiaddr.getPeerId() if (peerId != null && addrPeerId != null) { return peerId.equals(addrPeerId) } return true }) // deduplicate addresses const dedupedAddrs = new Map<string, Address>() for (const addr of filteredAddrs) { const maStr = addr.multiaddr.toString() const existing = dedupedAddrs.get(maStr) if (existing != null) { existing.isCertified = existing.isCertified || addr.isCertified || false continue } dedupedAddrs.set(maStr, addr) } const dedupedMultiaddrs = [...dedupedAddrs.values()] // make sure we actually have some addresses to dial if (dedupedMultiaddrs.length === 0) { throw new NoValidAddressesError('The dial request has no valid addresses') } const gatedAddrs: Address[] = [] for (const addr of dedupedMultiaddrs) { if (this.components.connectionGater.denyDialMultiaddr != null && await this.components.connectionGater.denyDialMultiaddr(addr.multiaddr)) { continue } gatedAddrs.push(addr) } const sortedGatedAddrs = this.addressSorter == null ? defaultAddressSorter(gatedAddrs) : gatedAddrs.sort(this.addressSorter) // make sure we actually have some addresses to dial if (sortedGatedAddrs.length === 0) { throw new DialDeniedError('The connection gater denied all addresses in the dial request') } this.log.trace('addresses for %p before filtering', peerId ?? 'unknown peer', resolvedAddresses.map(({ multiaddr }) => multiaddr.toString())) this.log.trace('addresses for %p after filtering', peerId ?? 'unknown peer', sortedGatedAddrs.map(({ multiaddr }) => multiaddr.toString())) return sortedGatedAddrs } async isDialable (multiaddr: Multiaddr | Multiaddr[], options: IsDialableOptions = {}): Promise<boolean> { if (!Array.isArray(multiaddr)) { multiaddr = [multiaddr] } try { const addresses = await this.calculateMultiaddrs(undefined, new Set(multiaddr.map(ma => ma.toString())), options) if (options.runOnLimitedConnection === false) { // return true if any resolved multiaddrs are not relay addresses return addresses.find(addr => { return !Circuit.matches(addr.multiaddr) }) != null } return true } catch (err) { this.log.trace('error calculating if multiaddr(s) were dialable', err) } return false } }