UNPKG

libp2p

Version:

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

414 lines • 19.2 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'; const defaultOptions = { maxParallelDials: MAX_PARALLEL_DIALS, maxDialQueueLength: MAX_DIAL_QUEUE_LENGTH, maxPeerAddrsToDial: MAX_PEER_ADDRS_TO_DIAL, dialTimeout: DIAL_TIMEOUT, resolvers: { dnsaddr: dnsaddrResolver } }; export class DialQueue { queue; components; addressSorter; maxPeerAddrsToDial; maxDialQueueLength; dialTimeout; shutDownController; connections; log; resolvers; constructor(components, init = {}) { 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() { this.shutDownController = new AbortController(); setMaxListeners(Infinity, this.shutDownController.signal); } /** * Clears any pending dials */ stop() { 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, options = {}) { 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 }); } async dialPeer(options, signal) { const peerId = options.peerId; const multiaddrs = options.multiaddrs; const failedMultiaddrs = new Set(); // 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 = []; 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 = []; // 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('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) { this.log.error('could not update last dial failure key for %p', peerId, err); } // dial successful, return the connection return conn; } catch (err) { 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) { 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 async calculateMultiaddrs(peerId, multiaddrs = new Set(), options = {}) { const addrs = [...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) { 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) { 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(); 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 = []; 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, options = {}) { 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; } } //# sourceMappingURL=dial-queue.js.map