UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

617 lines • 29.3 kB
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