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";
/**
 * Manage sync committee subnets. Sync committees are long (~27h) so there aren't random long-lived subscriptions
 */
export class SyncnetsService {
    config;
    clock;
    gossip;
    metadata;
    logger;
    metrics;
    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
     */
    subscriptionsCommittee = new SubnetMap();
    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;
        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.sync_committee, 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.sync_committee, boundary, subnet });
            }
        }
    }
    /**
     * Run per epoch, clean-up operations that are not urgent
     */
    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);
        }
    };
    /** 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.sync_committee, 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.sync_committee, boundary, subnet });
                }
                this.metrics?.syncnetsService.unsubscribeSubnets.inc({ subnet });
            }
        }
    }
    onScrapeLodestarMetrics(metrics) {
        metrics.syncnetsService.subscriptionsCommittee.set(this.subscriptionsCommittee.size);
    }
}
//# sourceMappingURL=syncnetsService.js.map