import { ENR } from "@chainsafe/enr";
import { ATTESTATION_SUBNET_COUNT, ForkSeq, SYNC_COMMITTEE_SUBNET_COUNT } from "@lodestar/params";
import { bytesToInt, pruneSetToMax, sleep, toHex } from "@lodestar/utils";
import { getCustodyGroups } from "../../util/dataColumns.js";
import { Discv5Worker } from "../discv5/index.js";
import { getLibp2pError } from "../libp2p/error.js";
import { ENRKey, SubnetType } from "../metadata.js";
import { computeNodeId } from "../subnets/interface.js";
import { getConnectionsMap, prettyPrintPeerId } from "../util.js";
import { ScoreState } from "./score/index.js";
import { deserializeEnrSubnets, zeroAttnets, zeroSyncnets } from "./utils/enrSubnetsDeserialize.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;
var QueryStatusCode;
(function (QueryStatusCode) {
    QueryStatusCode[QueryStatusCode["NotActive"] = 0] = "NotActive";
    QueryStatusCode[QueryStatusCode["Active"] = 1] = "Active";
})(QueryStatusCode || (QueryStatusCode = {}));
export { DiscoveredPeerStatus };
var DiscoveredPeerStatus;
(function (DiscoveredPeerStatus) {
    DiscoveredPeerStatus["bad_score"] = "bad_score";
    DiscoveredPeerStatus["already_connected"] = "already_connected";
    DiscoveredPeerStatus["already_dialing"] = "already_dialing";
    DiscoveredPeerStatus["error"] = "error";
    DiscoveredPeerStatus["attempt_dial"] = "attempt_dial";
    DiscoveredPeerStatus["cached"] = "cached";
    DiscoveredPeerStatus["dropped"] = "dropped";
    DiscoveredPeerStatus["no_multiaddrs"] = "no_multiaddrs";
    DiscoveredPeerStatus["transport_incompatible"] = "transport_incompatible";
    DiscoveredPeerStatus["peer_cooling_down"] = "peer_cooling_down";
})(DiscoveredPeerStatus || (DiscoveredPeerStatus = {}));
export { NotDialReason };
var NotDialReason;
(function (NotDialReason) {
    NotDialReason["not_contain_requested_sampling_groups"] = "not_contain_requested_sampling_groups";
    NotDialReason["not_contain_requested_attnet_syncnet_subnets"] = "not_contain_requested_attnet_syncnet_subnets";
    NotDialReason["no_multiaddrs"] = "no_multiaddrs";
})(NotDialReason || (NotDialReason = {}));
/**
 * PeerDiscovery discovers and dials new peers, and executes discv5 queries.
 * Currently relies on discv5 automatic periodic queries.
 */
export class PeerDiscovery {
    discv5;
    libp2p;
    clock;
    peerRpcScores;
    metrics;
    logger;
    config;
    cachedENRs = new Map();
    randomNodeQuery = { code: QueryStatusCode.NotActive };
    peersToConnect = 0;
    subnetRequests = {
        attnets: new Map(),
        syncnets: new Map(),
    };
    transports;
    custodyGroupQueries;
    discv5StartMs;
    discv5FirstQueryDelayMs;
    connectToDiscv5BootnodesOnStart = false;
    constructor(modules, opts, discv5) {
        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, opts) {
        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() {
        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, custodyGroupRequests, subnetRequests = []) {
        const subnetsToDiscoverPeers = [];
        const cachedENRsToDial = new Map();
        // Iterate in reverse to consider first the most recent ENRs
        const cachedENRsReverse = [];
        const pendingDials = new Set(this.libp2p.services.components.connectionManager
            .getDialQueue()
            .map((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();
        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
     */
    async runFindRandomNodeQuery() {
        // 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);
        }
        finally {
            this.randomNodeQuery = { code: QueryStatusCode.NotActive };
            timer?.();
        }
    }
    /**
     * Progressively called by libp2p as a result of peer discovery or updates to its peer store
     */
    onDiscoveredPeer = (evt) => {
        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.
     */
    onDiscoveredENR = async (enr) => {
        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.
     */
    handleDiscoveredPeer(peerId, multiaddrTCP, multiaddrQUIC, attnets, syncnets, custodySubnetCount) {
        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.peerId?.equals(peerId))) {
                return DiscoveredPeerStatus.already_dialing;
            }
            const forkSeq = this.config.getForkSeq(this.clock.currentSlot);
            // Should dial peer?
            const cachedPeer = {
                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);
            return DiscoveredPeerStatus.error;
        }
    }
    shouldDialPeer(peer) {
        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.
     */
    async dialPeer(cachedPeer) {
        // 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),
        });
        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);
            this.metrics?.discovery.dialError.inc({ reason: getLibp2pError(e) });
            this.logger.debug("Error dialing discovered peer", { peer: peerIdShort }, e);
        }
    }
    /** Check if there is 1+ open connection with this peer */
    isPeerConnected(peerIdStr) {
        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) {
    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;
    }
}
//# sourceMappingURL=discover.js.map