import { aggregateSignatures } from "@chainsafe/blst";
import { BitArray } from "@chainsafe/ssz";
import { SYNC_COMMITTEE_SIZE, SYNC_COMMITTEE_SUBNET_SIZE } from "@lodestar/params";
import { G2_POINT_AT_INFINITY } from "@lodestar/state-transition";
import { ssz } from "@lodestar/types";
import { MapDef, toRootHex } from "@lodestar/utils";
import { InsertOutcome, OpPoolError, OpPoolErrorCode } from "./types.js";
import { pruneBySlot, signatureFromBytesNoCheck } from "./utils.js";
/**
 * SyncCommittee aggregates are only useful for the next block they have signed.
 */
const SLOTS_RETAINED = 8;
/**
 * The maximum number of distinct `SyncAggregateFast` that will be stored in each slot.
 *
 * This is a DoS protection measure.
 */
const MAX_ITEMS_PER_SLOT = 512;
// SyncContributionAndProofPool constructor ensures SYNC_COMMITTEE_SUBNET_SIZE is multiple of 8
const SYNC_COMMITTEE_SUBNET_BYTES = SYNC_COMMITTEE_SUBNET_SIZE / 8;
/**
 * Cache SyncCommitteeContribution and seen ContributionAndProof.
 * This is used for SignedContributionAndProof validation and block factory.
 * This stays in-memory and should be pruned per slot.
 */
export class SyncContributionAndProofPool {
    config;
    clock;
    metrics;
    logger;
    bestContributionBySubnetRootBySlot = new MapDef(() => new MapDef(() => new Map()));
    lowestPermissibleSlot = 0;
    constructor(config, clock, metrics = null, logger = null) {
        this.config = config;
        this.clock = clock;
        this.metrics = metrics;
        this.logger = logger;
        // Param guarantee for optimizations below that merge syncSubcommitteeBits as bytes
        if (SYNC_COMMITTEE_SUBNET_SIZE % 8 !== 0) {
            throw Error("SYNC_COMMITTEE_SUBNET_SIZE must be multiple of 8");
        }
        metrics?.opPool.syncContributionAndProofPool.size.addCollect(() => this.onScrapeMetrics(metrics));
    }
    /** Returns current count of unique SyncContributionFast by block root and subnet */
    get size() {
        let count = 0;
        for (const bestContributionByRootBySubnet of this.bestContributionBySubnetRootBySlot.values()) {
            for (const bestContributionByRoot of bestContributionByRootBySubnet.values()) {
                count += bestContributionByRoot.size;
            }
        }
        return count;
    }
    /**
     * Only call this once we pass all validation.
     */
    add(contributionAndProof, syncCommitteeParticipants, priority) {
        const { contribution } = contributionAndProof;
        const { slot, beaconBlockRoot } = contribution;
        const rootHex = toRootHex(beaconBlockRoot);
        // Reject if too old.
        if (slot < this.lowestPermissibleSlot) {
            return InsertOutcome.Old;
        }
        // Reject ContributionAndProofs of previous slots
        // for api ContributionAndProofs, we allow them to be added to the pool
        if (!priority && slot < this.clock.slotWithPastTolerance(this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000)) {
            return InsertOutcome.Late;
        }
        // Limit object per slot
        const bestContributionBySubnetByRoot = this.bestContributionBySubnetRootBySlot.getOrDefault(slot);
        if (bestContributionBySubnetByRoot.size >= MAX_ITEMS_PER_SLOT) {
            throw new OpPoolError({ code: OpPoolErrorCode.REACHED_MAX_PER_SLOT });
        }
        const bestContributionBySubnet = bestContributionBySubnetByRoot.getOrDefault(rootHex);
        const subnet = contribution.subcommitteeIndex;
        const bestContribution = bestContributionBySubnet.get(subnet);
        if (bestContribution) {
            return replaceIfBetter(bestContribution, contribution, syncCommitteeParticipants);
        }
        bestContributionBySubnet.set(subnet, contributionToFast(contribution, syncCommitteeParticipants));
        return InsertOutcome.NewData;
    }
    /**
     * This is for producing blocks, the same to process_sync_committee_contributions in the spec.
     */
    getAggregate(slot, prevBlockRoot) {
        const opPoolMetrics = this.metrics?.opPool.syncContributionAndProofPool;
        const bestContributionBySubnetByRoot = this.bestContributionBySubnetRootBySlot.getOrDefault(slot);
        opPoolMetrics?.getAggregateRoots.set(bestContributionBySubnetByRoot.size);
        const prevBlockRootHex = toRootHex(prevBlockRoot);
        const bestContributionBySubnet = bestContributionBySubnetByRoot.get(prevBlockRootHex) ?? new Map();
        opPoolMetrics?.getAggregateSubnets.set(bestContributionBySubnet.size);
        if (bestContributionBySubnet.size === 0) {
            opPoolMetrics?.getAggregateReturnsEmpty.inc();
            // this may happen, see https://github.com/ChainSafe/lodestar/issues/7299
            const availableRoots = Array.from(bestContributionBySubnetByRoot.keys()).join(",");
            this.logger?.warn("SyncContributionAndProofPool.getAggregate: no contributions for root", {
                slot,
                root: prevBlockRootHex,
                availableRoots,
            });
            // Must return signature as G2_POINT_AT_INFINITY when participating bits are empty
            // https://github.com/ethereum/consensus-specs/blob/30f2a076377264677e27324a8c3c78c590ae5e20/specs/altair/bls.md#eth2_fast_aggregate_verify
            return {
                syncCommitteeBits: ssz.altair.SyncCommitteeBits.defaultValue(),
                syncCommitteeSignature: G2_POINT_AT_INFINITY,
            };
        }
        return aggregate(bestContributionBySubnet, this.metrics);
    }
    /**
     * Prune per head slot.
     * SyncCommittee aggregates are only useful for the next block they have signed.
     * We don't want to prune by clock slot in case there's a long period of skipped slots.
     */
    prune(headSlot) {
        pruneBySlot(this.bestContributionBySubnetRootBySlot, headSlot, SLOTS_RETAINED);
        this.lowestPermissibleSlot = Math.max(headSlot - SLOTS_RETAINED, 0);
    }
    onScrapeMetrics(metrics) {
        const poolMetrics = metrics.opPool.syncContributionAndProofPool;
        poolMetrics.size.set(this.size);
        const previousSlot = this.clock.currentSlot - 1;
        const contributionBySubnetByBlockRoot = this.bestContributionBySubnetRootBySlot.getOrDefault(previousSlot);
        poolMetrics.blockRootsPerSlot.set(contributionBySubnetByBlockRoot.size);
        let index = 0;
        for (const contributionsBySubnet of contributionBySubnetByBlockRoot.values()) {
            let participationCount = 0;
            for (const contribution of contributionsBySubnet.values()) {
                participationCount += contribution.numParticipants;
            }
            poolMetrics.subnetsByBlockRoot.set({ index }, contributionsBySubnet.size);
            poolMetrics.participantsByBlockRoot.set({ index }, participationCount);
            index++;
        }
    }
}
/**
 * Mutate bestContribution if new contribution has more participants
 */
export function replaceIfBetter(bestContribution, newContribution, newNumParticipants) {
    const { numParticipants } = bestContribution;
    if (newNumParticipants <= numParticipants) {
        return InsertOutcome.NotBetterThan;
    }
    bestContribution.syncSubcommitteeBits = newContribution.aggregationBits;
    bestContribution.numParticipants = newNumParticipants;
    bestContribution.syncSubcommitteeSignature = newContribution.signature;
    return InsertOutcome.NewData;
}
/**
 * Format `contribution` into an efficient data structure to aggregate later.
 */
export function contributionToFast(contribution, numParticipants) {
    return {
        // No need to clone, aggregationBits are not mutated, only replaced
        syncSubcommitteeBits: contribution.aggregationBits,
        numParticipants,
        // No need to deserialize, signatures are not aggregated until when calling .getAggregate()
        syncSubcommitteeSignature: contribution.signature,
    };
}
/**
 * Aggregate best contributions of each subnet into SyncAggregate
 * @returns SyncAggregate to be included in block body.
 */
export function aggregate(bestContributionBySubnet, metrics = null) {
    // check for empty/undefined bestContributionBySubnet earlier
    const syncCommitteeBits = BitArray.fromBitLen(SYNC_COMMITTEE_SIZE);
    const signatures = [];
    let participationCount = 0;
    for (const [subnet, bestContribution] of bestContributionBySubnet.entries()) {
        participationCount += bestContribution.numParticipants;
        const byteOffset = subnet * SYNC_COMMITTEE_SUBNET_BYTES;
        for (let i = 0; i < SYNC_COMMITTEE_SUBNET_BYTES; i++) {
            syncCommitteeBits.uint8Array[byteOffset + i] = bestContribution.syncSubcommitteeBits.uint8Array[i];
        }
        signatures.push(signatureFromBytesNoCheck(bestContribution.syncSubcommitteeSignature));
    }
    metrics?.opPool.syncContributionAndProofPool.getAggregateParticipants.set(participationCount);
    return {
        syncCommitteeBits,
        syncCommitteeSignature: aggregateSignatures(signatures).toBytes(),
    };
}
//# sourceMappingURL=syncContributionAndProofPool.js.map