libp2p
Version:
JavaScript implementation of libp2p, a modular peer to peer network stack
414 lines • 19.2 kB
JavaScript
/* 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