@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
617 lines • 29.3 kB
JavaScript
import { BitArray } from "@chainsafe/ssz";
import { SLOTS_PER_EPOCH, SYNC_COMMITTEE_SUBNET_COUNT } from "@lodestar/params";
import { withTimeout } from "@lodestar/utils";
import { GOODBYE_KNOWN_CODES, GoodByeReasonCode, Libp2pEvent } from "../../constants/index.js";
import { NetworkEvent } from "../events.js";
import { SubnetType } from "../metadata.js";
import { ReqRespMethod } from "../reqresp/ReqRespBeaconNode.js";
import { getConnection, getConnectionsMap, prettyPrintPeerId, prettyPrintPeerIdStr } from "../util.js";
import { ClientKind, getKnownClientFromAgentVersion } from "./client.js";
import { PeerDiscovery } from "./discover.js";
import { NO_COOL_DOWN_APPLIED } from "./score/constants.js";
import { ScoreState, updateGossipsubScores } from "./score/index.js";
import { assertPeerRelevance, getConnectedPeerIds, hasSomeConnectedPeer, prioritizePeers, renderIrrelevantPeerType, } from "./utils/index.js";
/** heartbeat performs regular updates such as updating reputations and performing discovery requests */
const HEARTBEAT_INTERVAL_MS = 30 * 1000;
/** The time in seconds between PING events. We do not send a ping if the other peer has PING'd us */
const PING_INTERVAL_INBOUND_MS = 15 * 1000; // Offset to not ping when outbound reqs
const PING_INTERVAL_OUTBOUND_MS = 20 * 1000;
/** The time in seconds between re-status's peers. */
const STATUS_INTERVAL_MS = 5 * 60 * 1000;
/** Expect a STATUS request from on inbound peer for some time. Afterwards the node does a request */
const STATUS_INBOUND_GRACE_PERIOD = 15 * 1000;
/** Internal interval to check PING and STATUS timeouts */
const CHECK_PING_STATUS_INTERVAL = 10 * 1000;
/** A peer is considered long connection if it's >= 1 day */
const LONG_PEER_CONNECTION_MS = 24 * 60 * 60 * 1000;
/** Ref https://github.com/ChainSafe/lodestar/issues/3423 */
const DEFAULT_DISCV5_FIRST_QUERY_DELAY_MS = 1000;
/**
* Tag peer when it's relevant and connecting to our node.
* When node has > maxPeer (55), libp2p randomly prune peers if we don't tag peers in use.
* See https://github.com/ChainSafe/lodestar/issues/4623#issuecomment-1374447934
**/
const PEER_RELEVANT_TAG = "relevant";
/** Tag value of PEER_RELEVANT_TAG */
const PEER_RELEVANT_TAG_VALUE = 100;
/** Change pruning behavior once the head falls behind */
const STARVATION_THRESHOLD_SLOTS = SLOTS_PER_EPOCH * 2;
/** Percentage of peers to attempt to prune when starvation threshold is met */
const STARVATION_PRUNE_RATIO = 0.05;
/**
* Relative factor of peers that are allowed to have a negative gossipsub score without penalizing them in lodestar.
*/
const ALLOWED_NEGATIVE_GOSSIPSUB_FACTOR = 0.1;
var RelevantPeerStatus;
(function (RelevantPeerStatus) {
RelevantPeerStatus["Unknown"] = "unknown";
RelevantPeerStatus["relevant"] = "relevant";
RelevantPeerStatus["irrelevant"] = "irrelevant";
})(RelevantPeerStatus || (RelevantPeerStatus = {}));
/**
* Performs all peer management functionality in a single grouped class:
* - Ping peers every `PING_INTERVAL_MS`
* - Status peers every `STATUS_INTERVAL_MS`
* - Execute discovery query if under target peers
* - Execute discovery query if need peers on some subnet: TODO
* - Disconnect peers if over target peers
*/
export class PeerManager {
constructor(modules, opts, discovery) {
this.intervals = [];
/**
* Must be called when network ReqResp receives incoming requests
*/
this.onRequest = ({ peer, request }) => {
try {
const peerData = this.connectedPeers.get(peer.toString());
if (peerData) {
peerData.lastReceivedMsgUnixTsMs = Date.now();
}
switch (request.method) {
case ReqRespMethod.Ping:
this.onPing(peer, request.body);
return;
case ReqRespMethod.Goodbye:
this.onGoodbye(peer, request.body);
return;
case ReqRespMethod.Status:
this.onStatus(peer, request.body);
return;
}
}
catch (e) {
this.logger.error("Error onRequest handler", {}, e);
}
};
/**
* The libp2p Upgrader has successfully upgraded a peer connection on a particular multiaddress
* This event is routed through the connectionManager
*
* Registers a peer as connected. The `direction` parameter determines if the peer is being
* dialed or connecting to us.
*/
this.onLibp2pPeerConnect = async (evt) => {
const { direction, status, remotePeer } = evt.detail;
this.logger.verbose("peer connected", { peer: prettyPrintPeerId(remotePeer), direction, status });
// NOTE: The peerConnect event is not emitted here here, but after asserting peer relevance
this.metrics?.peerConnectedEvent.inc({ direction, status });
// libp2p may emit closed connection, we don't want to handle it
// see https://github.com/libp2p/js-libp2p/issues/1565
if (this.connectedPeers.has(remotePeer.toString()) || status !== "open") {
return;
}
// On connection:
// - Outbound connections: send a STATUS and PING request
// - Inbound connections: expect to be STATUS'd, schedule STATUS and PING for latter
// NOTE: libp2p may emit two "peer:connect" events: One for inbound, one for outbound
// If that happens, it's okay. Only the "outbound" connection triggers immediate action
const now = Date.now();
const peerData = {
lastReceivedMsgUnixTsMs: direction === "outbound" ? 0 : now,
// If inbound, request after STATUS_INBOUND_GRACE_PERIOD
lastStatusUnixTsMs: direction === "outbound" ? 0 : now - STATUS_INTERVAL_MS + STATUS_INBOUND_GRACE_PERIOD,
connectedUnixTsMs: now,
relevantStatus: RelevantPeerStatus.Unknown,
direction,
peerId: remotePeer,
status: null,
metadata: null,
agentVersion: null,
agentClient: null,
encodingPreference: null,
};
this.connectedPeers.set(remotePeer.toString(), peerData);
if (direction === "outbound") {
//this.pingAndStatusTimeouts();
void this.requestPing(remotePeer);
void this.requestStatus(remotePeer, this.statusCache.get());
}
this.libp2p.services.identify
.identify(evt.detail)
.then((result) => {
const agentVersion = result.agentVersion;
if (agentVersion) {
peerData.agentVersion = agentVersion;
peerData.agentClient = getKnownClientFromAgentVersion(agentVersion);
}
})
.catch((err) => {
this.logger.debug("Error setting agentVersion for the peer", { peerId: peerData.peerId.toString() }, err);
});
};
/**
* The libp2p Upgrader has ended a connection
*/
this.onLibp2pPeerDisconnect = (evt) => {
const { direction, status, remotePeer } = evt.detail;
const peerIdStr = remotePeer.toString();
let logMessage = "onLibp2pPeerDisconnect";
const logContext = {
peerId: prettyPrintPeerIdStr(peerIdStr),
direction,
status,
};
// Some clients do not send good-bye requests (Nimbus) so check for inbound disconnects and apply reconnection
// cool-down period to prevent automatic reconnection by Discovery
if (direction === "inbound") {
// prevent automatic/immediate reconnects
const coolDownMin = this.peerRpcScores.applyReconnectionCoolDown(peerIdStr, GoodByeReasonCode.INBOUND_DISCONNECT);
logMessage += ". Enforcing a reconnection cool-down period";
logContext.coolDownMin = coolDownMin;
}
// remove the ping and status timer for the peer
this.connectedPeers.delete(peerIdStr);
this.logger.verbose(logMessage, logContext);
this.networkEventBus.emit(NetworkEvent.peerDisconnected, { peer: peerIdStr });
this.metrics?.peerDisconnectedEvent.inc({ direction });
this.libp2p.peerStore
.merge(remotePeer, { tags: { [PEER_RELEVANT_TAG]: undefined } })
.catch((e) => this.logger.verbose("cannot untag peer", { peerId: peerIdStr }, e));
};
this.libp2p = modules.libp2p;
this.logger = modules.logger;
this.metrics = modules.metrics;
this.reqResp = modules.reqResp;
this.gossipsub = modules.gossip;
this.attnetsService = modules.attnetsService;
this.syncnetsService = modules.syncnetsService;
this.statusCache = modules.statusCache;
this.clock = modules.clock;
this.config = modules.config;
this.peerRpcScores = modules.peerRpcScores;
this.networkEventBus = modules.events;
this.connectedPeers = modules.peersData.connectedPeers;
this.opts = opts;
this.discovery = discovery;
const { metrics } = modules;
if (metrics) {
metrics.peers.addCollect(() => this.runPeerCountMetrics(metrics));
}
this.libp2p.services.components.events.addEventListener(Libp2pEvent.connectionOpen, this.onLibp2pPeerConnect);
this.libp2p.services.components.events.addEventListener(Libp2pEvent.connectionClose, this.onLibp2pPeerDisconnect);
this.networkEventBus.on(NetworkEvent.reqRespRequest, this.onRequest);
this.lastStatus = this.statusCache.get();
// On start-up will connected to existing peers in libp2p.peerStore, same as autoDial behaviour
this.heartbeat();
this.intervals = [
setInterval(this.pingAndStatusTimeouts.bind(this), CHECK_PING_STATUS_INTERVAL),
setInterval(this.heartbeat.bind(this), HEARTBEAT_INTERVAL_MS),
setInterval(this.updateGossipsubScores.bind(this), this.gossipsub.scoreParams.decayInterval ?? HEARTBEAT_INTERVAL_MS),
];
}
static async init(modules, opts) {
// opts.discv5 === null, discovery is disabled
const discovery = opts.discv5
? await PeerDiscovery.init(modules, {
discv5FirstQueryDelayMs: opts.discv5FirstQueryDelayMs ?? DEFAULT_DISCV5_FIRST_QUERY_DELAY_MS,
discv5: opts.discv5,
connectToDiscv5Bootnodes: opts.connectToDiscv5Bootnodes,
})
: null;
return new PeerManager(modules, opts, discovery);
}
async close() {
await this.discovery?.stop();
this.libp2p.services.components.events.removeEventListener(Libp2pEvent.connectionOpen, this.onLibp2pPeerConnect);
this.libp2p.services.components.events.removeEventListener(Libp2pEvent.connectionClose, this.onLibp2pPeerDisconnect);
this.networkEventBus.off(NetworkEvent.reqRespRequest, this.onRequest);
for (const interval of this.intervals)
clearInterval(interval);
}
/**
* Return peers with at least one connection in status "open"
*/
getConnectedPeerIds() {
return getConnectedPeerIds(this.libp2p);
}
/**
* Efficiently check if there is at least one peer connected
*/
hasSomeConnectedPeer() {
return hasSomeConnectedPeer(this.libp2p);
}
async goodbyeAndDisconnectAllPeers() {
await Promise.all(
// Filter by peers that support the goodbye protocol: {supportsProtocols: [goodbyeProtocol]}
this.getConnectedPeerIds().map(async (peer) => this.goodbyeAndDisconnect(peer, GoodByeReasonCode.CLIENT_SHUTDOWN)));
}
/**
* Run after validator subscriptions request.
*/
onCommitteeSubscriptions() {
// TODO:
// Only if the slot is more than epoch away, add an event to start looking for peers
// Request to run heartbeat fn
this.heartbeat();
}
reportPeer(peer, action, actionName) {
this.peerRpcScores.applyAction(peer, action, actionName);
}
/**
* The app layer needs to refresh the status of some peers. The sync have reached a target
*/
reStatusPeers(peers) {
for (const peer of peers) {
const peerData = this.connectedPeers.get(peer);
if (peerData) {
// Set to 0 to trigger a status request after calling pingAndStatusTimeouts()
peerData.lastStatusUnixTsMs = 0;
}
}
this.pingAndStatusTimeouts();
}
dumpPeerScoreStats() {
return this.peerRpcScores.dumpPeerScoreStats();
}
/**
* Handle a PING request + response (rpc handler responds with PONG automatically)
*/
onPing(peer, seqNumber) {
// if the sequence number is unknown update the peer's metadata
const metadata = this.connectedPeers.get(peer.toString())?.metadata;
if (!metadata || metadata.seqNumber < seqNumber) {
void this.requestMetadata(peer);
}
}
/**
* Handle a METADATA request + response (rpc handler responds with METADATA automatically)
*/
onMetadata(peer, metadata) {
// Store metadata always in case the peer updates attnets but not the sequence number
// Trust that the peer always sends the latest metadata (From Lighthouse)
const peerData = this.connectedPeers.get(peer.toString());
if (peerData) {
peerData.metadata = {
seqNumber: metadata.seqNumber,
attnets: metadata.attnets,
syncnets: metadata.syncnets ?? BitArray.fromBitLen(SYNC_COMMITTEE_SUBNET_COUNT),
};
}
}
/**
* Handle a GOODBYE request (rpc handler responds automatically)
*/
onGoodbye(peer, goodbye) {
const reason = GOODBYE_KNOWN_CODES[goodbye.toString()] || "";
this.logger.verbose("Received goodbye request", { peer: prettyPrintPeerId(peer), goodbye, reason });
this.metrics?.peerGoodbyeReceived.inc({ reason });
const conn = getConnection(this.libp2p, peer.toString());
if (conn && Date.now() - conn.timeline.open > LONG_PEER_CONNECTION_MS) {
this.metrics?.peerLongConnectionDisconnect.inc({ reason });
}
void this.disconnect(peer);
}
/**
* Handle a STATUS request + response (rpc handler responds with STATUS automatically)
*/
onStatus(peer, status) {
// reset the to-status timer of this peer
const peerData = this.connectedPeers.get(peer.toString());
if (peerData) {
peerData.lastStatusUnixTsMs = Date.now();
peerData.status = status;
}
let isIrrelevant;
try {
const irrelevantReasonType = assertPeerRelevance(status, this.statusCache.get(), this.clock.currentSlot);
if (irrelevantReasonType === null) {
isIrrelevant = false;
}
else {
isIrrelevant = true;
this.logger.debug("Irrelevant peer", {
peer: prettyPrintPeerId(peer),
reason: renderIrrelevantPeerType(irrelevantReasonType),
});
}
}
catch (e) {
this.logger.error("Irrelevant peer - unexpected error", { peer: prettyPrintPeerId(peer) }, e);
isIrrelevant = true;
}
if (isIrrelevant) {
if (peerData)
peerData.relevantStatus = RelevantPeerStatus.irrelevant;
void this.goodbyeAndDisconnect(peer, GoodByeReasonCode.IRRELEVANT_NETWORK);
return;
}
// Peer is usable, send it to the rangeSync
// NOTE: Peer may not be connected anymore at this point, potential race condition
// libp2p.connectionManager.get() returns not null if there's +1 open connections with `peer`
if (peerData && peerData.relevantStatus !== RelevantPeerStatus.relevant) {
this.libp2p.peerStore
.merge(peer, {
// ttl = undefined means it's never expired
tags: { [PEER_RELEVANT_TAG]: { ttl: undefined, value: PEER_RELEVANT_TAG_VALUE } },
})
.catch((e) => this.logger.verbose("cannot tag peer", { peerId: peer.toString() }, e));
peerData.relevantStatus = RelevantPeerStatus.relevant;
}
if (getConnection(this.libp2p, peer.toString())) {
this.networkEventBus.emit(NetworkEvent.peerConnected, { peer: peer.toString(), status });
}
}
async requestMetadata(peer) {
try {
this.onMetadata(peer, await this.reqResp.sendMetadata(peer));
}
catch (_e) {
// TODO: Downvote peer here or in the reqResp layer
}
}
async requestPing(peer) {
try {
this.onPing(peer, await this.reqResp.sendPing(peer));
// If peer replies a PING request also update lastReceivedMsg
const peerData = this.connectedPeers.get(peer.toString());
if (peerData)
peerData.lastReceivedMsgUnixTsMs = Date.now();
}
catch (_e) {
// TODO: Downvote peer here or in the reqResp layer
}
}
async requestStatus(peer, localStatus) {
try {
this.onStatus(peer, await this.reqResp.sendStatus(peer, localStatus));
}
catch (_e) {
// TODO: Failed to get peer latest status: downvote but don't disconnect
}
}
async requestStatusMany(peers) {
try {
const localStatus = this.statusCache.get();
await Promise.all(peers.map(async (peer) => this.requestStatus(peer, localStatus)));
}
catch (e) {
this.logger.verbose("Error requesting new status to peers", {}, e);
}
}
/**
* The Peer manager's heartbeat maintains the peer count and maintains peer reputations.
* It will request discovery queries if the peer count has not reached the desired number of peers.
* NOTE: Discovery should only add a new query if one isn't already queued.
*/
heartbeat() {
// timer is safe without a try {} catch (_e) {}, in case of error the metric won't register and timer is GC'ed
const timer = this.metrics?.peerManager.heartbeatDuration.startTimer();
const connectedPeers = this.getConnectedPeerIds();
// Decay scores before reading them. Also prunes scores
this.peerRpcScores.update();
// ban and disconnect peers with bad score, collect rest of healthy peers
const connectedHealthyPeers = [];
for (const peer of connectedPeers) {
switch (this.peerRpcScores.getScoreState(peer)) {
case ScoreState.Banned:
void this.goodbyeAndDisconnect(peer, GoodByeReasonCode.BANNED);
break;
case ScoreState.Disconnected:
void this.goodbyeAndDisconnect(peer, GoodByeReasonCode.SCORE_TOO_LOW);
break;
case ScoreState.Healthy:
connectedHealthyPeers.push(peer);
}
}
const status = this.statusCache.get();
const starved =
// while syncing progress is happening, we aren't starved
this.lastStatus.headSlot === status.headSlot &&
// if the head falls behind the threshold, we are starved
this.clock.currentSlot - status.headSlot > STARVATION_THRESHOLD_SLOTS;
this.lastStatus = status;
this.metrics?.peerManager.starved.set(starved ? 1 : 0);
const { peersToDisconnect, peersToConnect, attnetQueries, syncnetQueries } = prioritizePeers(connectedHealthyPeers.map((peer) => {
const peerData = this.connectedPeers.get(peer.toString());
return {
id: peer,
direction: peerData?.direction ?? null,
status: peerData?.status ?? null,
attnets: peerData?.metadata?.attnets ?? null,
syncnets: peerData?.metadata?.syncnets ?? null,
score: this.peerRpcScores.getScore(peer),
};
}),
// Collect subnets which we need peers for in the current slot
this.attnetsService.getActiveSubnets(), this.syncnetsService.getActiveSubnets(), {
...this.opts,
status,
starved,
starvationPruneRatio: STARVATION_PRUNE_RATIO,
starvationThresholdSlots: STARVATION_THRESHOLD_SLOTS,
});
const queriesMerged = [];
for (const { type, queries } of [
{ type: SubnetType.attnets, queries: attnetQueries },
{ type: SubnetType.syncnets, queries: syncnetQueries },
]) {
if (queries.length > 0) {
let count = 0;
for (const query of queries) {
count += query.maxPeersToDiscover;
queriesMerged.push({
subnet: query.subnet,
type,
maxPeersToDiscover: query.maxPeersToDiscover,
toUnixMs: 1000 * (this.clock.genesisTime + query.toSlot * this.config.SECONDS_PER_SLOT),
});
}
this.metrics?.peersRequestedSubnetsToQuery.inc({ type }, queries.length);
this.metrics?.peersRequestedSubnetsPeerCount.inc({ type }, count);
}
}
// disconnect first to have more slots before we dial new peers
for (const [reason, peers] of peersToDisconnect) {
this.metrics?.peersRequestedToDisconnect.inc({ reason }, peers.length);
for (const peer of peers) {
void this.goodbyeAndDisconnect(peer, GoodByeReasonCode.TOO_MANY_PEERS);
}
}
if (this.discovery) {
try {
this.metrics?.peersRequestedToConnect.inc(peersToConnect);
this.discovery.discoverPeers(peersToConnect, queriesMerged);
}
catch (e) {
this.logger.error("Error on discoverPeers", {}, e);
}
}
// Prune connectedPeers map in case it leaks. It has happen in previous nodes,
// disconnect is not always called for all peers
if (this.connectedPeers.size > connectedPeers.length * 1.1) {
const actualConnectedPeerIds = new Set(connectedPeers.map((peerId) => peerId.toString()));
for (const peerIdStr of this.connectedPeers.keys()) {
if (!actualConnectedPeerIds.has(peerIdStr)) {
this.connectedPeers.delete(peerIdStr);
this.metrics?.leakedConnectionsCount.inc();
}
}
}
timer?.();
this.logger.debug("peerManager heartbeat result", {
peersToDisconnect: peersToDisconnect.size,
peersToConnect: peersToConnect,
attnetQueries: attnetQueries.length,
syncnetQueries: syncnetQueries.length,
});
}
updateGossipsubScores() {
const gossipsubScores = new Map();
for (const peerIdStr of this.connectedPeers.keys()) {
gossipsubScores.set(peerIdStr, this.gossipsub.getScore(peerIdStr));
}
const toIgnoreNegativePeers = Math.ceil(this.opts.targetPeers * ALLOWED_NEGATIVE_GOSSIPSUB_FACTOR);
updateGossipsubScores(this.peerRpcScores, gossipsubScores, toIgnoreNegativePeers);
}
pingAndStatusTimeouts() {
const now = Date.now();
const peersToStatus = [];
for (const peer of this.connectedPeers.values()) {
// Every interval request to send some peers our seqNumber and process theirs
// If the seqNumber is different it must request the new metadata
const pingInterval = peer.direction === "inbound" ? PING_INTERVAL_INBOUND_MS : PING_INTERVAL_OUTBOUND_MS;
if (now > peer.lastReceivedMsgUnixTsMs + pingInterval) {
void this.requestPing(peer.peerId);
}
// TODO: Consider sending status request to peers that do support status protocol
// {supportsProtocols: getStatusProtocols()}
// Every interval request to send some peers our status, and process theirs
// Must re-check if this peer is relevant to us and emit an event if the status changes
// So the sync layer can update things
if (now > peer.lastStatusUnixTsMs + STATUS_INTERVAL_MS) {
peersToStatus.push(peer.peerId);
}
}
if (peersToStatus.length > 0) {
void this.requestStatusMany(peersToStatus);
}
}
async disconnect(peer) {
try {
await this.libp2p.hangUp(peer);
}
catch (e) {
this.logger.debug("Unclean disconnect", { peer: prettyPrintPeerId(peer) }, e);
}
}
async goodbyeAndDisconnect(peer, goodbye) {
const reason = GOODBYE_KNOWN_CODES[goodbye.toString()] || "";
const peerIdStr = peer.toString();
try {
this.metrics?.peerGoodbyeSent.inc({ reason });
const conn = getConnection(this.libp2p, peerIdStr);
if (conn && Date.now() - conn.timeline.open > LONG_PEER_CONNECTION_MS) {
this.metrics?.peerLongConnectionDisconnect.inc({ reason });
}
// Wrap with shorter timeout than regular ReqResp requests to speed up shutdown
await withTimeout(() => this.reqResp.sendGoodbye(peer, BigInt(goodbye)), 1_000);
}
catch (e) {
this.logger.verbose("Failed to send goodbye", { peer: prettyPrintPeerId(peer) }, e);
}
finally {
await this.disconnect(peer);
// prevent automatic/immediate reconnects
const coolDownMin = this.peerRpcScores.applyReconnectionCoolDown(peerIdStr, goodbye);
if (coolDownMin === NO_COOL_DOWN_APPLIED) {
this.logger.verbose("Disconnected a peer", { peerId: prettyPrintPeerIdStr(peerIdStr) });
}
else {
this.logger.verbose("Disconnected a peer. Enforcing a reconnection cool-down period", {
peerId: prettyPrintPeerIdStr(peerIdStr),
coolDownMin,
});
}
}
}
/** Register peer count metrics */
async runPeerCountMetrics(metrics) {
let total = 0;
const peersByDirection = new Map();
const peersByClient = new Map();
const now = Date.now();
// peerLongLivedAttnets metric is a count
metrics.peerLongLivedAttnets.reset();
metrics.peerScoreByClient.reset();
metrics.peerConnectionLength.reset();
metrics.peerGossipScoreByClient.reset();
// reset client counts _for each client_ to 0
for (const client of Object.values(ClientKind)) {
peersByClient.set(client, 0);
}
for (const connections of getConnectionsMap(this.libp2p).values()) {
const openCnx = connections.value.find((cnx) => cnx.status === "open");
if (openCnx) {
const direction = openCnx.direction;
peersByDirection.set(direction, 1 + (peersByDirection.get(direction) ?? 0));
const peerId = openCnx.remotePeer;
const peerData = this.connectedPeers.get(peerId.toString());
const client = peerData?.agentClient ?? ClientKind.Unknown;
peersByClient.set(client, 1 + (peersByClient.get(client) ?? 0));
const attnets = peerData?.metadata?.attnets;
// TODO: Consider optimizing by doing observe in batch
metrics.peerLongLivedAttnets.observe(attnets ? attnets.getTrueBitIndexes().length : 0);
metrics.peerScoreByClient.observe({ client }, this.peerRpcScores.getScore(peerId));
metrics.peerGossipScoreByClient.observe({ client }, this.peerRpcScores.getGossipScore(peerId));
metrics.peerConnectionLength.observe((now - openCnx.timeline.open) / 1000);
total++;
}
}
for (const [direction, peers] of peersByDirection.entries()) {
metrics.peersByDirection.set({ direction }, peers);
}
for (const [client, peers] of peersByClient.entries()) {
metrics.peersByClient.set({ client }, peers);
}
let syncPeers = 0;
for (const peer of this.connectedPeers.values()) {
if (peer.relevantStatus === RelevantPeerStatus.relevant) {
syncPeers++;
}
}
metrics.peers.set(total);
metrics.peersSync.set(syncPeers);
}
}
//# sourceMappingURL=peerManager.js.map