UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

683 lines (604 loc) 28.2 kB
import type {PeerId, PeerInfo, PendingDial, PrivateKey} from "@libp2p/interface"; import {Multiaddr} from "@multiformats/multiaddr"; import {ENR} from "@chainsafe/enr"; import {BeaconConfig} from "@lodestar/config"; import {LoggerNode} from "@lodestar/logger/node"; import {ATTESTATION_SUBNET_COUNT, ForkSeq, SYNC_COMMITTEE_SUBNET_COUNT} from "@lodestar/params"; import {CustodyIndex, SubnetID} from "@lodestar/types"; import {bytesToInt, pruneSetToMax, sleep, toHex} from "@lodestar/utils"; import {IClock} from "../../util/clock.js"; import {getCustodyGroups} from "../../util/dataColumns.js"; import {NetworkCoreMetrics} from "../core/metrics.js"; import {Discv5Worker} from "../discv5/index.js"; import {LodestarDiscv5Opts} from "../discv5/types.js"; import {Libp2p} from "../interface.js"; import {getLibp2pError} from "../libp2p/error.js"; import {ENRKey, SubnetType} from "../metadata.js"; import {NetworkConfig} from "../networkConfig.js"; import {computeNodeId} from "../subnets/interface.js"; import {getConnectionsMap, prettyPrintPeerId} from "../util.js"; import {IPeerRpcScoreStore, ScoreState} from "./score/index.js"; import {deserializeEnrSubnets, zeroAttnets, zeroSyncnets} from "./utils/enrSubnetsDeserialize.js"; import {type CustodyGroupQueries} from "./utils/prioritizePeers.js"; /** Max number of cached ENRs after discovering a good peer */ const MAX_CACHED_ENRS = 100; /** Max age a cached ENR will be considered for dial */ const MAX_CACHED_ENR_AGE_MS = 5 * 60 * 1000; export type PeerDiscoveryOpts = { discv5FirstQueryDelayMs: number; discv5: LodestarDiscv5Opts; connectToDiscv5Bootnodes?: boolean; }; export type PeerDiscoveryModules = { privateKey: PrivateKey; networkConfig: NetworkConfig; libp2p: Libp2p; clock: IClock; peerRpcScores: IPeerRpcScoreStore; metrics: NetworkCoreMetrics | null; logger: LoggerNode; }; type PeerIdStr = string; enum QueryStatusCode { NotActive, Active, } type QueryStatus = {code: QueryStatusCode.NotActive} | {code: QueryStatusCode.Active; count: number}; export enum DiscoveredPeerStatus { bad_score = "bad_score", already_connected = "already_connected", already_dialing = "already_dialing", error = "error", attempt_dial = "attempt_dial", cached = "cached", dropped = "dropped", no_multiaddrs = "no_multiaddrs", transport_incompatible = "transport_incompatible", peer_cooling_down = "peer_cooling_down", } export enum NotDialReason { not_contain_requested_sampling_groups = "not_contain_requested_sampling_groups", not_contain_requested_attnet_syncnet_subnets = "not_contain_requested_attnet_syncnet_subnets", no_multiaddrs = "no_multiaddrs", } type UnixMs = number; /** * Maintain peersToConnect to avoid having too many topic peers at some point. * See https://github.com/ChainSafe/lodestar/issues/5741#issuecomment-1643113577 */ type SubnetRequestInfo = { toUnixMs: UnixMs; // when node is stable this should be 0 peersToConnect: number; }; export type SubnetDiscvQueryMs = { subnet: SubnetID; type: SubnetType; toUnixMs: UnixMs; maxPeersToDiscover: number; }; type CachedENR = { peerId: PeerId; multiaddrTCP?: Multiaddr; multiaddrQUIC?: Multiaddr; subnets: Record<SubnetType, boolean[]>; addedUnixMs: number; // custodyGroups is null for pre-fulu custodyGroups: number[] | null; }; /** * PeerDiscovery discovers and dials new peers, and executes discv5 queries. * Currently relies on discv5 automatic periodic queries. */ export class PeerDiscovery { readonly discv5: Discv5Worker; private libp2p: Libp2p; private readonly clock: IClock; private peerRpcScores: IPeerRpcScoreStore; private metrics: NetworkCoreMetrics | null; private logger: LoggerNode; private config: BeaconConfig; private cachedENRs = new Map<PeerIdStr, CachedENR>(); private randomNodeQuery: QueryStatus = {code: QueryStatusCode.NotActive}; private peersToConnect = 0; private subnetRequests: Record<SubnetType, Map<number, SubnetRequestInfo>> = { attnets: new Map(), syncnets: new Map(), }; private transports: string[]; private custodyGroupQueries: CustodyGroupQueries; private discv5StartMs: number; private discv5FirstQueryDelayMs: number; private connectToDiscv5BootnodesOnStart: boolean | undefined = false; constructor(modules: PeerDiscoveryModules, opts: PeerDiscoveryOpts, discv5: Discv5Worker) { const {libp2p, clock, peerRpcScores, metrics, logger, networkConfig} = modules; this.libp2p = libp2p; this.clock = clock; this.peerRpcScores = peerRpcScores; this.metrics = metrics; this.logger = logger; this.config = networkConfig.config; this.discv5 = discv5; this.custodyGroupQueries = new Map(); this.discv5StartMs = 0; this.discv5StartMs = Date.now(); this.discv5FirstQueryDelayMs = opts.discv5FirstQueryDelayMs; this.connectToDiscv5BootnodesOnStart = opts.connectToDiscv5Bootnodes; this.libp2p.addEventListener("peer:discovery", this.onDiscoveredPeer); this.discv5.on("discovered", this.onDiscoveredENR); const numBootEnrs = opts.discv5.bootEnrs.length; if (numBootEnrs === 0) { this.logger.error("PeerDiscovery: discv5 has no boot enr"); } else { this.logger.verbose("PeerDiscovery: number of bootEnrs", {bootEnrs: numBootEnrs}); } if (this.connectToDiscv5BootnodesOnStart) { // In devnet scenarios, especially, we want more control over which peers we connect to. // Only dial the discv5.bootEnrs if the option // network.connectToDiscv5Bootnodes has been set to true. for (const bootENR of opts.discv5.bootEnrs) { this.onDiscoveredENR(ENR.decodeTxt(bootENR)).catch((e) => this.logger.error("error onDiscoveredENR bootENR", {}, e) ); } } if (metrics) { metrics.discovery.cachedENRsSize.addCollect(() => { metrics.discovery.cachedENRsSize.set(this.cachedENRs.size); metrics.discovery.peersToConnect.set(this.peersToConnect); // PeerDAS metrics const groupsToConnect = Array.from(this.custodyGroupQueries.values()); const groupPeersToConnect = groupsToConnect.reduce((acc, elem) => acc + elem, 0); metrics.discovery.custodyGroupPeersToConnect.set(groupPeersToConnect); metrics.discovery.custodyGroupsToConnect.set(groupsToConnect.filter((elem) => elem > 0).length); for (const type of [SubnetType.attnets, SubnetType.syncnets]) { const subnetPeersToConnect = Array.from(this.subnetRequests[type].values()).reduce( (acc, {peersToConnect}) => acc + peersToConnect, 0 ); metrics.discovery.subnetPeersToConnect.set({type}, subnetPeersToConnect); metrics.discovery.subnetsToConnect.set({type}, this.subnetRequests[type].size); } }); } // Transport tags vary by library: @libp2p/tcp uses '@libp2p/tcp', @chainsafe/libp2p-quic uses 'quic' // Normalize to simple 'tcp' / 'quic' strings for matching this.transports = libp2p.services.components.transportManager .getTransports() .map((t) => t[Symbol.toStringTag]) .map((tag) => { if (tag?.includes("tcp")) return "tcp"; if (tag?.includes("quic")) return "quic"; return tag; }); } static async init(modules: PeerDiscoveryModules, opts: PeerDiscoveryOpts): Promise<PeerDiscovery> { const discv5 = await Discv5Worker.init({ discv5: opts.discv5, privateKey: modules.privateKey, metrics: modules.metrics ?? undefined, logger: modules.logger, config: modules.networkConfig.config, genesisTime: modules.clock.genesisTime, }); return new PeerDiscovery(modules, opts, discv5); } async stop(): Promise<void> { this.libp2p.removeEventListener("peer:discovery", this.onDiscoveredPeer); this.discv5.off("discovered", this.onDiscoveredENR); await this.discv5.close(); } /** * Request to find peers, both on specific subnets and in general * pre-fulu custodyGroupRequests is empty */ discoverPeers( peersToConnect: number, custodyGroupRequests: CustodyGroupQueries, subnetRequests: SubnetDiscvQueryMs[] = [] ): void { const subnetsToDiscoverPeers: SubnetDiscvQueryMs[] = []; const cachedENRsToDial = new Map<PeerIdStr, CachedENR>(); // Iterate in reverse to consider first the most recent ENRs const cachedENRsReverse: CachedENR[] = []; const pendingDials = new Set( this.libp2p.services.components.connectionManager .getDialQueue() .map((pendingDial: PendingDial) => pendingDial.peerId?.toString()) ); for (const [id, cachedENR] of this.cachedENRs.entries()) { if ( // time expired or Date.now() - cachedENR.addedUnixMs > MAX_CACHED_ENR_AGE_MS || // already dialing pendingDials.has(id) ) { this.cachedENRs.delete(id); } else if (!this.peerRpcScores.isCoolingDown(id)) { cachedENRsReverse.push(cachedENR); } } cachedENRsReverse.reverse(); this.peersToConnect += peersToConnect; // starting from PeerDAS, we need to prioritize column subnet peers first in order to have stable subnet sampling const groupsToDiscover = new Set<CustodyIndex>(); let groupPeersToDiscover = 0; const forkSeq = this.config.getForkSeq(this.clock.currentSlot); if (forkSeq >= ForkSeq.fulu) { group: for (const [group, maxPeersToConnect] of custodyGroupRequests) { let cachedENRsInGroup = 0; for (const cachedENR of cachedENRsReverse) { if (cachedENR.custodyGroups?.includes(group)) { cachedENRsToDial.set(cachedENR.peerId.toString(), cachedENR); if (++cachedENRsInGroup >= maxPeersToConnect) { continue group; } } const groupPeersToConnect = Math.max(maxPeersToConnect - cachedENRsInGroup, 0); this.custodyGroupQueries.set(group, groupPeersToConnect); groupsToDiscover.add(group); groupPeersToDiscover += groupPeersToConnect; } } } subnet: for (const subnetRequest of subnetRequests) { // Get cached ENRs from the discovery service that are in the requested `subnetId`, but not connected yet let cachedENRsInSubnet = 0; // only dial attnet/syncnet peers if subnet sampling peers are stable if (groupPeersToDiscover === 0) { for (const cachedENR of cachedENRsReverse) { if (cachedENR.subnets[subnetRequest.type][subnetRequest.subnet]) { cachedENRsToDial.set(cachedENR.peerId.toString(), cachedENR); if (++cachedENRsInSubnet >= subnetRequest.maxPeersToDiscover) { continue subnet; } } } } const subnetPeersToConnect = Math.max(subnetRequest.maxPeersToDiscover - cachedENRsInSubnet, 0); // Extend the toUnixMs for this subnet const prevUnixMs = this.subnetRequests[subnetRequest.type].get(subnetRequest.subnet)?.toUnixMs; const newUnixMs = prevUnixMs !== undefined && prevUnixMs > subnetRequest.toUnixMs ? prevUnixMs : subnetRequest.toUnixMs; this.subnetRequests[subnetRequest.type].set(subnetRequest.subnet, { toUnixMs: newUnixMs, peersToConnect: subnetPeersToConnect, }); // Query a discv5 query if more peers are needed subnetsToDiscoverPeers.push(subnetRequest); } // If subnetRequests won't connect enough peers for peersToConnect, add more if (cachedENRsToDial.size < peersToConnect) { for (const cachedENR of cachedENRsReverse) { cachedENRsToDial.set(cachedENR.peerId.toString(), cachedENR); if (cachedENRsToDial.size >= peersToConnect) { break; } } } // Queue an outgoing connection request to the cached peers that are on `s.subnet_id`. // If we connect to the cached peers before the discovery query starts, then we potentially // save a costly discovery query. for (const [id, cachedENRToDial] of cachedENRsToDial) { this.cachedENRs.delete(id); void this.dialPeer(cachedENRToDial); } // Run a discv5 subnet query to try to discover new peers const shouldRunFindRandomNodeQuery = subnetsToDiscoverPeers.length > 0 || cachedENRsToDial.size < peersToConnect; if (shouldRunFindRandomNodeQuery) { void this.runFindRandomNodeQuery(); } this.logger.debug("Discover peers outcome", { peersToConnect, peersAvailableToDial: cachedENRsToDial.size, subnetsToDiscover: subnetsToDiscoverPeers.length, groupsToDiscover: Array.from(groupsToDiscover).join(","), groupPeersToDiscover, shouldRunFindRandomNodeQuery, }); } /** * Request discv5 to find peers if there is no query in progress */ private async runFindRandomNodeQuery(): Promise<void> { // Delay the 1st query after starting discv5 // See https://github.com/ChainSafe/lodestar/issues/3423 const msSinceDiscv5Start = Date.now() - this.discv5StartMs; if (msSinceDiscv5Start <= this.discv5FirstQueryDelayMs) { await sleep(this.discv5FirstQueryDelayMs - msSinceDiscv5Start); } // Run a general discv5 query if one is not already in progress if (this.randomNodeQuery.code === QueryStatusCode.Active) { this.metrics?.discovery.findNodeQueryRequests.inc({action: "ignore"}); return; } this.metrics?.discovery.findNodeQueryRequests.inc({action: "start"}); // Use async version to prevent blocking the event loop // Time to completion of this function is not critical, in case this async call add extra lag this.randomNodeQuery = {code: QueryStatusCode.Active, count: 0}; const timer = this.metrics?.discovery.findNodeQueryTime.startTimer(); try { const enrs = await this.discv5.findRandomNode(); this.metrics?.discovery.findNodeQueryEnrCount.inc(enrs.length); } catch (e) { this.logger.error("Error on discv5.findNode()", {}, e as Error); } finally { this.randomNodeQuery = {code: QueryStatusCode.NotActive}; timer?.(); } } /** * Progressively called by libp2p as a result of peer discovery or updates to its peer store */ private onDiscoveredPeer = (evt: CustomEvent<PeerInfo>): void => { const {id, multiaddrs} = evt.detail; // libp2p may send us PeerInfos without multiaddrs https://github.com/libp2p/js-libp2p/issues/1873 if (!multiaddrs || multiaddrs.length === 0) { this.metrics?.discovery.discoveredStatus.inc({status: DiscoveredPeerStatus.no_multiaddrs}); return; } // Select multiaddrs by protocol rather than index — libp2p discovery events // don't guarantee ordering or number of addresses const multiaddrTCP = multiaddrs.find((ma) => ma.toString().includes("/tcp/")); const multiaddrQUIC = multiaddrs.find((ma) => ma.toString().includes("/quic-v1")); const attnets = zeroAttnets; const syncnets = zeroSyncnets; const status = this.handleDiscoveredPeer(id, multiaddrTCP, multiaddrQUIC, attnets, syncnets, undefined); this.logger.debug("Discovered peer via libp2p", {peer: prettyPrintPeerId(id), status}); this.metrics?.discovery.discoveredStatus.inc({status}); }; /** * Progressively called by discv5 as a result of any query. */ private onDiscoveredENR = async (enr: ENR): Promise<void> => { if (this.randomNodeQuery.code === QueryStatusCode.Active) { this.randomNodeQuery.count++; } const peerId = enr.peerId; // At least one transport is known to be present, checked inside the worker const multiaddrTCP = enr.getLocationMultiaddr(ENRKey.tcp); const multiaddrQUIC = enr.getLocationMultiaddr(ENRKey.quic); if (!multiaddrTCP && !multiaddrQUIC) { this.logger.warn("Discv5 worker sent enr without any transport multiaddr", {enr: enr.encodeTxt()}); this.metrics?.discovery.discoveredStatus.inc({status: DiscoveredPeerStatus.no_multiaddrs}); return; } // Are this fields mandatory? const attnetsBytes = enr.kvs.get(ENRKey.attnets); // 64 bits const syncnetsBytes = enr.kvs.get(ENRKey.syncnets); // 4 bits const custodyGroupCountBytes = enr.kvs.get(ENRKey.cgc); // not preserialized value, is byte representation of number if (custodyGroupCountBytes === undefined) { this.logger.debug("peer discovered with no cgc, using default/miniumn", { custodyRequirement: this.config.CUSTODY_REQUIREMENT, peer: prettyPrintPeerId(peerId), }); } // Use faster version than ssz's implementation that leverages pre-cached. // Some nodes don't serialize the bitfields properly, encoding the syncnets as attnets, // which cause the ssz implementation to throw on validation. deserializeEnrSubnets() will // never throw and treat too long or too short bitfields as zero-ed const attnets = attnetsBytes ? deserializeEnrSubnets(attnetsBytes, ATTESTATION_SUBNET_COUNT) : zeroAttnets; const syncnets = syncnetsBytes ? deserializeEnrSubnets(syncnetsBytes, SYNC_COMMITTEE_SUBNET_COUNT) : zeroSyncnets; const custodyGroupCount = custodyGroupCountBytes ? bytesToInt(custodyGroupCountBytes, "be") : undefined; const status = this.handleDiscoveredPeer(peerId, multiaddrTCP, multiaddrQUIC, attnets, syncnets, custodyGroupCount); this.logger.debug("Discovered peer via discv5", { peer: prettyPrintPeerId(peerId), status, cgc: custodyGroupCount, }); this.metrics?.discovery.discoveredStatus.inc({status}); }; /** * Progressively called by peer discovery as a result of any query. */ private handleDiscoveredPeer( peerId: PeerId, multiaddrTCP: Multiaddr | undefined, multiaddrQUIC: Multiaddr | undefined, attnets: boolean[], syncnets: boolean[], custodySubnetCount?: number ): DiscoveredPeerStatus { const nodeId = computeNodeId(peerId); this.logger.debug("handleDiscoveredPeer", {nodeId: toHex(nodeId), peerId: peerId.toString()}); try { // Check if peer is not banned or disconnected if (this.peerRpcScores.getScoreState(peerId) !== ScoreState.Healthy) { return DiscoveredPeerStatus.bad_score; } const peerIdStr = peerId.toString(); // check if peer has a cool-down period applied for reconnection. Is possible that a peer has a // "healthy" score but has disconnected us and we are letting the reconnection cool-down before // they are eligible for reconnection if (this.peerRpcScores.isCoolingDown(peerIdStr)) { return DiscoveredPeerStatus.peer_cooling_down; } // Ignore connected peers. TODO: Is this check necessary? if (this.isPeerConnected(peerIdStr)) { return DiscoveredPeerStatus.already_connected; } // ignore peers if they don't share any transport with us const hasTcpMatch = this.transports.includes("tcp") && multiaddrTCP; const hasQuicMatch = this.transports.includes("quic") && multiaddrQUIC; if (!hasTcpMatch && !hasQuicMatch) { return DiscoveredPeerStatus.transport_incompatible; } // Ignore dialing peers if ( this.libp2p.services.components.connectionManager .getDialQueue() .find((pendingDial: PendingDial) => pendingDial.peerId?.equals(peerId)) ) { return DiscoveredPeerStatus.already_dialing; } const forkSeq = this.config.getForkSeq(this.clock.currentSlot); // Should dial peer? const cachedPeer: CachedENR = { peerId, multiaddrTCP, multiaddrQUIC, subnets: {attnets, syncnets}, addedUnixMs: Date.now(), // for pre-fulu, custodyGroups is null custodyGroups: forkSeq >= ForkSeq.fulu ? getCustodyGroups(this.config, nodeId, custodySubnetCount ?? this.config.CUSTODY_REQUIREMENT) : null, }; // Only dial peer if necessary if (this.shouldDialPeer(cachedPeer)) { void this.dialPeer(cachedPeer); return DiscoveredPeerStatus.attempt_dial; } // Add to pending good peers with a last seen time this.cachedENRs.set(peerId.toString(), cachedPeer); const dropped = pruneSetToMax(this.cachedENRs, MAX_CACHED_ENRS); // If the cache was already full, count the peer as dropped return dropped > 0 ? DiscoveredPeerStatus.dropped : DiscoveredPeerStatus.cached; } catch (e) { this.logger.error("Error onDiscovered", {}, e as Error); return DiscoveredPeerStatus.error; } } private shouldDialPeer(peer: CachedENR): boolean { const forkSeq = this.config.getForkSeq(this.clock.currentSlot); if (forkSeq >= ForkSeq.fulu && peer.custodyGroups !== null) { // pre-fulu `this.custodyGroupQueries` is empty // starting from fulu, we need to make sure we have stable subnet sampling peers first // given SAMPLES_PER_SLOT = 8 and 100 peers, we have 800 custody columns from peers // with NUMBER_OF_CUSTODY_GROUPS = 128, we have 800 / 128 = 6.25 peers per column in average // it would not be hard to find TARGET_SUBNET_PEERS(6) peers per sampling columns columns and TARGET_GROUP_PEERS_PER_SUBNET(4) peers per non-sampling columns // after some first heartbeats, we should have no more column requested, then go with conditions of prior forks let hasMatchingGroup = false; let custodyGroupRequestCount = 0; for (const [group, peersToConnect] of this.custodyGroupQueries.entries()) { if (peersToConnect <= 0) { this.custodyGroupQueries.delete(group); } else if (peer.custodyGroups.includes(group)) { this.custodyGroupQueries.set(group, Math.max(0, peersToConnect - 1)); hasMatchingGroup = true; custodyGroupRequestCount += peersToConnect; } } // if subnet sampling peers are not stable and this peer is not in the requested columns, ignore it if (custodyGroupRequestCount > 0 && !hasMatchingGroup) { this.metrics?.discovery.notDialReason.inc({reason: NotDialReason.not_contain_requested_sampling_groups}); return false; } } // logics up to Deneb fork for (const type of [SubnetType.attnets, SubnetType.syncnets]) { for (const [subnet, {toUnixMs, peersToConnect}] of this.subnetRequests[type].entries()) { if (toUnixMs < Date.now() || peersToConnect === 0) { // Prune all requests so that we don't have to loop again // if we have low subnet peers then PeerManager will update us again with subnet + toUnixMs + peersToConnect this.subnetRequests[type].delete(subnet); } else { // not expired and peersToConnect > 0 // if we have enough subnet peers, no need to dial more or we may have performance issues // see https://github.com/ChainSafe/lodestar/issues/5741#issuecomment-1643113577 if (peer.subnets[type][subnet]) { this.subnetRequests[type].set(subnet, {toUnixMs, peersToConnect: Math.max(peersToConnect - 1, 0)}); return true; } } } } // ideally we may want to leave this cheap condition at the top of the function // however we want to also update peersToConnect in this.subnetRequests // the this.subnetRequests[type] gradually has 0 subnet so this function should be cheap enough if (this.peersToConnect > 0) { return true; } this.metrics?.discovery.notDialReason.inc({reason: NotDialReason.not_contain_requested_attnet_syncnet_subnets}); return false; } /** * Handles DiscoveryEvent::QueryResult * Peers that have been returned by discovery requests are dialed here if they are suitable. */ private async dialPeer(cachedPeer: CachedENR): Promise<void> { // we dial a peer when: // - this.peersToConnect > 0 // - or the peer subscribes to a subnet that we want // If this.peersToConnect is 3 while we need to dial 5 subnet peers, in that case we want this.peersToConnect // to be 0 instead of a negative value. The next heartbeat may increase this.peersToConnect again if some dials // are not successful. this.peersToConnect = Math.max(this.peersToConnect - 1, 0); const {peerId, multiaddrTCP, multiaddrQUIC} = cachedPeer; // Must add the multiaddrs array to the address book before dialing // https://github.com/libp2p/js-libp2p/blob/aec8e3d3bb1b245051b60c2a890550d262d5b062/src/index.js#L638 const peer = await this.libp2p.peerStore.merge(peerId, { multiaddrs: [multiaddrQUIC, multiaddrTCP].filter(Boolean) as Multiaddr[], }); if (peer.addresses.length === 0) { this.metrics?.discovery.notDialReason.inc({reason: NotDialReason.no_multiaddrs}); return; } // Note: PeerDiscovery adds the multiaddrs beforehand const peerIdShort = prettyPrintPeerId(peerId); this.logger.debug("Dialing discovered peer", { peer: peerIdShort, addresses: peer.addresses.map((a) => a.multiaddr.toString()).join(", "), }); this.metrics?.discovery.dialAttempts.inc(); const timer = this.metrics?.discovery.dialTime.startTimer(); // Note: `libp2p.dial()` is what libp2p.connectionManager autoDial calls // Note: You must listen to the connected events to listen for a successful conn upgrade try { await this.libp2p.dial(peerId); timer?.({status: "success"}); this.logger.debug("Dialed discovered peer", {peer: peerIdShort}); } catch (e) { timer?.({status: "error"}); formatLibp2pDialError(e as Error); this.metrics?.discovery.dialError.inc({reason: getLibp2pError(e as Error)}); this.logger.debug("Error dialing discovered peer", {peer: peerIdShort}, e as Error); } } /** Check if there is 1+ open connection with this peer */ private isPeerConnected(peerIdStr: PeerIdStr): boolean { const connections = getConnectionsMap(this.libp2p).get(peerIdStr); return Boolean(connections?.value.some((connection) => connection.status === "open")); } } /** * libp2p errors with extremely noisy errors here, which are deeply nested taking 30-50 lines. * Some known errors: * ``` * Error: The operation was aborted * Error: stream ended before 1 bytes became available * Error: Error occurred during XX handshake: Error occurred while verifying signed payload: Peer ID doesn't match libp2p public key * ``` * * Also the error's message is not properly formatted, where the error message is indented and includes the full stack * ``` * { * emessage: '\n' + * ' Error: stream ended before 1 bytes became available\n' + * ' at /home/lion/Code/eth2.0/lodestar/node_modules/it-reader/index.js:37:9\n' + * ' at runMicrotasks (<anonymous>)\n' + * ' at decoder (/home/lion/Code/eth2.0/lodestar/node_modules/it-length-prefixed/src/decode.js:113:22)\n' + * ' at first (/home/lion/Code/eth2.0/lodestar/node_modules/it-first/index.js:11:20)\n' + * ' at Object.exports.read (/home/lion/Code/eth2.0/lodestar/node_modules/multistream-select/src/multistream.js:31:15)\n' + * ' at module.exports (/home/lion/Code/eth2.0/lodestar/node_modules/multistream-select/src/select.js:21:19)\n' + * ' at Upgrader._encryptOutbound (/home/lion/Code/eth2.0/lodestar/node_modules/libp2p/src/upgrader.js:397:36)\n' + * ' at Upgrader.upgradeOutbound (/home/lion/Code/eth2.0/lodestar/node_modules/libp2p/src/upgrader.js:176:11)\n' + * ' at ClassIsWrapper.dial (/home/lion/Code/eth2.0/lodestar/node_modules/libp2p-tcp/src/index.js:49:18)' * } * ``` * * Tracking issue https://github.com/libp2p/js-libp2p/issues/996 */ function formatLibp2pDialError(e: Error): void { const errorMessage = e.message.trim(); const newlineIndex = errorMessage.indexOf("\n"); e.message = newlineIndex !== -1 ? errorMessage.slice(0, newlineIndex) : errorMessage; if ( e.message.includes("The operation was aborted") || e.message.includes("stream ended before 1 bytes became available") || e.message.includes("The operation was aborted") ) { e.stack = undefined; } }