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