UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

395 lines • 17.8 kB
import { ENR } from "@chainsafe/enr"; import { ssz, sszTypesFor } from "@lodestar/types"; import { fromHex } from "@lodestar/utils"; import { multiaddr } from "@multiformats/multiaddr"; import { formatNodePeer } from "../../api/impl/node/utils.js"; import { ClockEvent } from "../../util/clock.js"; import { peerIdFromString, peerIdToString } from "../../util/peerId.js"; import { FORK_EPOCH_LOOKAHEAD, getActiveForkBoundaries } from "../forks.js"; import { Eth2Gossipsub, getCoreTopicsAtFork } from "../gossip/index.js"; import { createNodeJsLibp2p } from "../libp2p/index.js"; import { MetadataController } from "../metadata.js"; import { PeerRpcScoreStore } from "../peers/index.js"; import { PeerManager } from "../peers/peerManager.js"; import { PeersData } from "../peers/peersData.js"; import { ReqRespBeaconNode } from "../reqresp/ReqRespBeaconNode.js"; import { LocalStatusCache } from "../statusCache.js"; import { AttnetsService } from "../subnets/attnetsService.js"; import { SyncnetsService } from "../subnets/syncnetsService.js"; import { getConnectionsMap } from "../util.js"; import { createNetworkCoreMetrics } from "./metrics.js"; /** * This class is meant to work both: * - In a libp2p worker * - In the main thread * * libp2p holds the reference to the TCP transport socket. libp2p is in a worker, what components * must be in a worker too? * - MetadataController: Read by ReqRespBeaconNode, written by AttnetsService + SyncnetsService * - PeerRpcScoreStore * - ReqRespBeaconNode: Must be in worker, depends on libp2p * - Eth2Gossipsub: Must be in worker, depends on libp2p * - AttnetsService * - SyncnetsService * - PeerManager * - NetworkProcessor: Must be in the main thread, depends on chain */ export class NetworkCore { constructor(modules) { // Internal state this.forkBoundariesByEpoch = new Map(); this.closed = false; /** * Handle subscriptions through fork boundary transitions, @see FORK_EPOCH_LOOKAHEAD */ this.onEpoch = async (epoch) => { try { // Compute prev and next fork boundary shifted, so next boundary is still next at forkEpoch + FORK_EPOCH_LOOKAHEAD const activeBoundaries = getActiveForkBoundaries(this.config, epoch); for (let i = 0; i < activeBoundaries.length; i++) { // Only when a new fork boundary is scheduled post this one if (activeBoundaries[i + 1] !== undefined) { const prevBoundary = activeBoundaries[i]; const nextBoundary = activeBoundaries[i + 1]; const nextBoundaryEpoch = nextBoundary.epoch; // Before fork boundary transition if (epoch === nextBoundaryEpoch - FORK_EPOCH_LOOKAHEAD) { // Don't subscribe to new fork boundary if the node is not subscribed to any topic if (await this.isSubscribedToGossipCoreTopics()) { this.subscribeCoreTopicsAtBoundary(this.config, nextBoundary); this.logger.info("Subscribing gossip topics for next fork boundary", nextBoundary); } else { this.logger.info("Skipping subscribing gossip topics for next fork boundary", nextBoundary); } this.attnetsService.subscribeSubnetsNextBoundary(nextBoundary); this.syncnetsService.subscribeSubnetsNextBoundary(nextBoundary); } // On fork boundary transition if (epoch === nextBoundaryEpoch) { // updateEth2Field() MUST be called with clock epoch, onEpoch event is emitted in response to clock events this.metadata.updateEth2Field(epoch); this.reqResp.registerProtocolsAtBoundary(nextBoundary); } // After fork boundary transition if (epoch === nextBoundaryEpoch + FORK_EPOCH_LOOKAHEAD) { this.logger.info("Unsubscribing gossip topics of previous fork boundary", prevBoundary); this.unsubscribeCoreTopicsAtBoundary(this.config, prevBoundary); this.attnetsService.unsubscribeSubnetsPrevBoundary(prevBoundary); this.syncnetsService.unsubscribeSubnetsPrevBoundary(prevBoundary); } } } } catch (e) { this.logger.error("Error on BeaconGossipHandler.onEpoch", { epoch }, e); } }; this.libp2p = modules.libp2p; this.gossip = modules.gossip; this.reqResp = modules.reqResp; this.attnetsService = modules.attnetsService; this.syncnetsService = modules.syncnetsService; this.peerManager = modules.peerManager; this.peersData = modules.peersData; this.metadata = modules.metadata; this.logger = modules.logger; this.config = modules.config; this.clock = modules.clock; this.statusCache = modules.statusCache; this.metrics = modules.metrics; this.opts = modules.opts; this.clock.on(ClockEvent.epoch, this.onEpoch); } static async init({ opts, config, privateKey, peerStoreDir, logger, metricsRegistry, events, clock, getReqRespHandler, activeValidatorCount, initialStatus, }) { const libp2p = await createNodeJsLibp2p(privateKey, opts, { peerStoreDir, metrics: Boolean(metricsRegistry), metricsRegistry: metricsRegistry ?? undefined, }); const metrics = metricsRegistry ? createNetworkCoreMetrics(metricsRegistry) : null; const peersData = new PeersData(); const peerRpcScores = new PeerRpcScoreStore(opts, metrics); const statusCache = new LocalStatusCache(initialStatus); // Bind discv5's ENR to local metadata // resolve circular dependency by setting `discv5` variable after the peer manager is instantiated // biome-ignore lint/style/useConst: <explanation> let discv5; const onMetadataSetValue = function onMetadataSetValue(key, value) { discv5?.setEnrValue(key, value).catch((e) => logger.error("error on setEnrValue", { key }, e)); }; const metadata = new MetadataController({}, { config, logger, onSetValue: onMetadataSetValue }); const reqResp = new ReqRespBeaconNode({ config, libp2p, metadata, peerRpcScores, logger, events, metrics, peersData, statusCache, getHandler: getReqRespHandler, }, opts); const gossip = new Eth2Gossipsub(opts, { config, libp2p, logger, metricsRegister: metricsRegistry, eth2Context: { activeValidatorCount, currentSlot: clock.currentSlot, currentEpoch: clock.currentEpoch, }, peersData, events, }); // Note: should not be necessary, already called in createNodeJsLibp2p() await libp2p.start(); await reqResp.start(); // should be called before AttnetsService constructor so that node subscribe to deterministic attnet topics await gossip.start(); const enr = opts.discv5?.enr; const nodeId = enr ? fromHex(ENR.decodeTxt(enr).nodeId) : null; const attnetsService = new AttnetsService(config, clock, gossip, metadata, logger, metrics, nodeId, opts); const syncnetsService = new SyncnetsService(config, clock, gossip, metadata, logger, metrics, opts); const peerManager = await PeerManager.init({ privateKey, libp2p, gossip, reqResp, attnetsService, syncnetsService, logger, metrics, clock, config, peerRpcScores, events, peersData, statusCache, }, opts); // Network spec decides version changes based on clock epoch, not head epoch const boundary = config.getForkBoundaryAtEpoch(clock.currentEpoch); // Register only ReqResp protocols relevant to clock's epoch reqResp.registerProtocolsAtBoundary(boundary); // Bind discv5's ENR to local metadata // biome-ignore lint/complexity/useLiteralKeys: `discovery` is a private attribute discv5 = peerManager["discovery"]?.discv5; // Initialize ENR with clock's fork metadata.upstreamValues(clock.currentEpoch); return new NetworkCore({ libp2p, reqResp, gossip, attnetsService, syncnetsService, peerManager, peersData, metadata, logger, config, clock, statusCache, metrics, opts, }); } /** Destroy this instance. Can only be called once. */ async close() { if (this.closed) return; this.clock.off(ClockEvent.epoch, this.onEpoch); // Must goodbye and disconnect before stopping libp2p await this.peerManager.goodbyeAndDisconnectAllPeers(); this.logger.debug("network sent goodbye to all peers"); await this.peerManager.close(); this.logger.debug("network peerManager closed"); await this.gossip.stop(); this.logger.debug("network gossip closed"); await this.reqResp.stop(); await this.reqResp.unregisterAllProtocols(); this.logger.debug("network reqResp closed"); this.attnetsService.close(); this.syncnetsService.close(); await this.libp2p.stop(); this.logger.debug("network lib2p closed"); this.closed = true; } async scrapeMetrics() { return [ (await this.metrics?.register.metrics()) ?? "", // biome-ignore lint/complexity/useLiteralKeys: `discovery` is a private attribute (await this.peerManager["discovery"]?.discv5.scrapeMetrics()) ?? "", ] .filter((str) => str.length > 0) .join("\n\n"); } async updateStatus(status) { this.statusCache.update(status); } async reportPeer(peer, action, actionName) { this.peerManager.reportPeer(peerIdFromString(peer), action, actionName); } async reStatusPeers(peers) { this.peerManager.reStatusPeers(peers); } /** * Request att subnets up `toSlot`. Network will ensure to mantain some peers for each */ async prepareBeaconCommitteeSubnets(subscriptions) { this.attnetsService.addCommitteeSubscriptions(subscriptions); if (subscriptions.length > 0) this.peerManager.onCommitteeSubscriptions(); } async prepareSyncCommitteeSubnets(subscriptions) { this.syncnetsService.addCommitteeSubscriptions(subscriptions); if (subscriptions.length > 0) this.peerManager.onCommitteeSubscriptions(); } /** * Subscribe to all gossip events. Safe to call multiple times */ async subscribeGossipCoreTopics() { if (!(await this.isSubscribedToGossipCoreTopics())) { this.logger.info("Subscribed gossip core topics"); } for (const boundary of getActiveForkBoundaries(this.config, this.clock.currentEpoch)) { this.subscribeCoreTopicsAtBoundary(this.config, boundary); } } /** * Unsubscribe from all gossip events. Safe to call multiple times */ async unsubscribeGossipCoreTopics() { for (const boundary of this.forkBoundariesByEpoch.values()) { this.unsubscribeCoreTopicsAtBoundary(this.config, boundary); } } async isSubscribedToGossipCoreTopics() { return this.forkBoundariesByEpoch.size > 0; } sendReqRespRequest(data) { const peerId = peerIdFromString(data.peerId); return this.reqResp.sendRequestWithoutEncoding(peerId, data.method, data.versions, data.requestData); } async publishGossip(topic, data, opts) { const { recipients } = await this.gossip.publish(topic, data, opts); return recipients.length; } // REST API queries async getNetworkIdentity() { // biome-ignore lint/complexity/useLiteralKeys: `discovery` is a private attribute const enr = await this.peerManager["discovery"]?.discv5.enr(); const discoveryAddresses = [ enr?.getLocationMultiaddr("tcp")?.toString() ?? null, enr?.getLocationMultiaddr("udp")?.toString() ?? null, ].filter((addr) => Boolean(addr)); return { peerId: peerIdToString(this.libp2p.peerId), enr: enr?.encodeTxt() || "", discoveryAddresses, p2pAddresses: this.libp2p.getMultiaddrs().map((m) => m.toString()), metadata: this.metadata.json, }; } getConnectionsByPeer() { const m = new Map(); for (const [k, v] of getConnectionsMap(this.libp2p).entries()) { m.set(k, v.value); } return m; } async getConnectedPeers() { return this.peerManager.getConnectedPeerIds().map(peerIdToString); } async getConnectedPeerCount() { return this.peerManager.getConnectedPeerIds().length; } // Debug async connectToPeer(peerIdStr, multiaddrStrArr) { const peer = peerIdFromString(peerIdStr); await this.libp2p.peerStore.merge(peer, { multiaddrs: multiaddrStrArr.map(multiaddr) }); await this.libp2p.dial(peer); } async disconnectPeer(peerIdStr) { await this.libp2p.hangUp(peerIdFromString(peerIdStr)); } _dumpPeer(peerIdStr, connections) { const peerData = this.peersData.connectedPeers.get(peerIdStr); const fork = this.config.getForkName(this.clock.currentSlot); return { ...formatNodePeer(peerIdStr, connections), agentVersion: peerData?.agentVersion ?? "NA", status: peerData?.status ? ssz.phase0.Status.toJson(peerData.status) : null, metadata: peerData?.metadata ? sszTypesFor(fork).Metadata.toJson(peerData.metadata) : null, agentClient: String(peerData?.agentClient ?? "Unknown"), lastReceivedMsgUnixTsMs: peerData?.lastReceivedMsgUnixTsMs ?? 0, lastStatusUnixTsMs: peerData?.lastStatusUnixTsMs ?? 0, connectedUnixTsMs: peerData?.connectedUnixTsMs ?? 0, }; } async dumpPeer(peerIdStr) { const connections = this.getConnectionsByPeer().get(peerIdStr); return connections ? this._dumpPeer(peerIdStr, connections) : undefined; } async dumpPeers() { return Array.from(this.getConnectionsByPeer().entries()).map(([peerIdStr, connections]) => this._dumpPeer(peerIdStr, connections)); } async dumpPeerScoreStats() { return this.peerManager.dumpPeerScoreStats(); } async dumpGossipPeerScoreStats() { return this.gossip.dumpPeerScoreStats(); } async dumpDiscv5KadValues() { // biome-ignore lint/complexity/useLiteralKeys: `discovery` is a private attribute return (await this.peerManager["discovery"]?.discv5?.kadValues())?.map((enr) => enr.encodeTxt()) ?? []; } async dumpMeshPeers() { const meshPeers = {}; for (const topic of this.gossip.getTopics()) { meshPeers[topic] = this.gossip.getMeshPeers(topic); } return meshPeers; } async writeNetworkThreadProfile() { throw new Error("Method not implemented, please configure network thread"); } async writeDiscv5Profile(durationMs, dirpath) { // biome-ignore lint/complexity/useLiteralKeys: `discovery` is a private attribute return this.peerManager["discovery"]?.discv5.writeProfile(durationMs, dirpath) ?? "no discv5"; } writeNetworkHeapSnapshot() { throw new Error("Method not implemented, please configure network thread"); } writeDiscv5HeapSnapshot(prefix, dirpath) { // biome-ignore lint/complexity/useLiteralKeys: `discovery` is a private attribute return this.peerManager["discovery"]?.discv5.writeHeapSnapshot(prefix, dirpath) ?? Promise.resolve("no discv5"); } subscribeCoreTopicsAtBoundary(config, boundary) { if (this.forkBoundariesByEpoch.has(boundary.epoch)) return; this.forkBoundariesByEpoch.set(boundary.epoch, boundary); const { subscribeAllSubnets, disableLightClientServer } = this.opts; for (const topic of getCoreTopicsAtFork(config, boundary.fork, { subscribeAllSubnets, disableLightClientServer, })) { this.gossip.subscribeTopic({ ...topic, boundary }); } } unsubscribeCoreTopicsAtBoundary(config, boundary) { if (!this.forkBoundariesByEpoch.has(boundary.epoch)) return; this.forkBoundariesByEpoch.delete(boundary.epoch); const { subscribeAllSubnets, disableLightClientServer } = this.opts; for (const topic of getCoreTopicsAtFork(config, boundary.fork, { subscribeAllSubnets, disableLightClientServer, })) { this.gossip.unsubscribeTopic({ ...topic, boundary }); } } } //# sourceMappingURL=networkCore.js.map