UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

128 lines 5.88 kB
import { SYNC_COMMITTEE_SUBNET_COUNT } from "@lodestar/params"; import { computeStartSlotAtEpoch } from "@lodestar/state-transition"; import { ssz } from "@lodestar/types"; import { ClockEvent } from "../../util/clock.js"; import { getActiveForkBoundaries } from "../forks.js"; import { GossipType } from "../gossip/index.js"; import { SubnetMap } from "../peers/utils/index.js"; const gossipType = GossipType.sync_committee; /** * Manage sync committee subnets. Sync committees are long (~27h) so there aren't random long-lived subscriptions */ export class SyncnetsService { constructor(config, clock, gossip, metadata, logger, metrics, opts) { this.config = config; this.clock = clock; this.gossip = gossip; this.metadata = metadata; this.logger = logger; this.metrics = metrics; this.opts = opts; /** * All currently subscribed subnets. Syncnets do not have additional long-lived * random subscriptions since the committees are already active for long periods of time. * Also, the node will aggregate through the entire period to simplify the validator logic. * So `subscriptionsCommittee` represents subnets to find peers and aggregate data. * This class will tell gossip to subscribe and un-subscribe. * If a value exists for `SubscriptionId` it means that gossip subscription is active in network.gossip */ this.subscriptionsCommittee = new SubnetMap(); /** * Run per epoch, clean-up operations that are not urgent */ this.onEpoch = (epoch) => { try { const slot = computeStartSlotAtEpoch(epoch); // Unsubscribe to a committee subnet from subscriptionsCommittee. this.unsubscribeSubnets(this.subscriptionsCommittee.getExpired(slot)); } catch (e) { this.logger.error("Error on SyncnetsService.onEpoch", { epoch }, e); } }; if (metrics) { metrics.syncnetsService.subscriptionsCommittee.addCollect(() => this.onScrapeLodestarMetrics(metrics)); } this.clock.on(ClockEvent.epoch, this.onEpoch); } close() { this.clock.off(ClockEvent.epoch, this.onEpoch); } /** * Get all active subnets for the hearbeat. */ getActiveSubnets() { return this.subscriptionsCommittee.getActiveTtl(this.clock.currentSlot); } /** * Called from the API when validator is a part of a committee. */ addCommitteeSubscriptions(subscriptions) { // Trigger gossip subscription first, in batch if (subscriptions.length > 0) { this.subscribeToSubnets(subscriptions.map((sub) => sub.subnet)); } // Then, register the subscriptions for (const { subnet, slot } of subscriptions) { this.subscriptionsCommittee.request({ subnet, toSlot: slot }); } // For syncnets regular subscriptions are persisted in the ENR this.updateMetadata(); } /** Call ONLY ONCE: Two epoch before the fork, re-subscribe all existing random subscriptions to the new fork */ subscribeSubnetsNextBoundary(boundary) { this.logger.info("Subscribing to random attnets for next fork boundary", boundary); for (const subnet of this.subscriptionsCommittee.getAll()) { this.gossip.subscribeTopic({ type: gossipType, boundary, subnet }); } } /** Call ONLY ONCE: Two epochs after the fork, un-subscribe all subnets from the old fork */ unsubscribeSubnetsPrevBoundary(boundary) { this.logger.info("Unsubscribing from random attnets of previous fork boundary", boundary); for (let subnet = 0; subnet < SYNC_COMMITTEE_SUBNET_COUNT; subnet++) { if (!this.opts?.subscribeAllSubnets) { this.gossip.unsubscribeTopic({ type: gossipType, boundary, subnet }); } } } /** Update ENR */ updateMetadata() { const subnets = ssz.altair.SyncSubnets.defaultValue(); for (const subnet of this.subscriptionsCommittee.getAll()) { subnets.set(subnet, true); } // Only update metadata if necessary, setting `metadata.[key]` triggers a write to disk if (!ssz.altair.SyncSubnets.equals(subnets, this.metadata.syncnets)) { this.metadata.syncnets = subnets; } } /** Tigger a gossip subcription only if not already subscribed */ subscribeToSubnets(subnets) { const boundaries = getActiveForkBoundaries(this.config, this.clock.currentEpoch); for (const subnet of subnets) { if (!this.subscriptionsCommittee.has(subnet)) { for (const boundary of boundaries) { this.gossip.subscribeTopic({ type: gossipType, boundary, subnet }); } this.metrics?.syncnetsService.subscribeSubnets.inc({ subnet }); } } } /** Trigger a gossip un-subscrition only if no-one is still subscribed */ unsubscribeSubnets(subnets) { const boundaries = getActiveForkBoundaries(this.config, this.clock.currentEpoch); for (const subnet of subnets) { // No need to check if active in subscriptionsCommittee since we only have a single SubnetMap if (!this.opts?.subscribeAllSubnets) { for (const boundary of boundaries) { this.gossip.unsubscribeTopic({ type: gossipType, boundary, subnet }); } this.metrics?.syncnetsService.unsubscribeSubnets.inc({ subnet }); } } } onScrapeLodestarMetrics(metrics) { metrics.syncnetsService.subscriptionsCommittee.set(this.subscriptionsCommittee.size); } } //# sourceMappingURL=syncnetsService.js.map