@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
395 lines • 17.8 kB
JavaScript
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