@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
683 lines (604 loc) • 28.2 kB
text/typescript
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;
}
}