import { routes } from "@lodestar/api";
import { ForkSeq } from "@lodestar/params";
import { computeStartSlotAtEpoch } from "@lodestar/state-transition";
import { MapDef, mapValues, pruneSetToMax, sleep } from "@lodestar/utils";
import { BlockInputSource } from "../../chain/blocks/blockInput/types.js";
import { ChainEvent } from "../../chain/emitter.js";
import { GossipErrorCode } from "../../chain/errors/gossipValidation.js";
import { ClockEvent } from "../../util/clock.js";
import { callInNextEventLoop } from "../../util/eventLoop.js";
import { NetworkEvent } from "../events.js";
import { GossipType, } from "../gossip/interface.js";
import { createExtractBlockSlotRootFns } from "./extractSlotRootFns.js";
import { getGossipHandlers } from "./gossipHandlers.js";
import { createGossipQueues } from "./gossipQueues/index.js";
import { getGossipValidatorBatchFn, getGossipValidatorFn } from "./gossipValidatorFn.js";
export * from "./types.js";
/**
 * Keep up to 3 slot of unknown roots, so we don't always emit to UnknownBlock sync.
 */
const MAX_UNKNOWN_ROOTS_SLOT_CACHE_SIZE = 3;
/**
 * This is respective to gossipsub seenTTL (which is 550 * 0.7 = 385s), also it's respective
 * to beacon_attestation ATTESTATION_PROPAGATION_SLOT_RANGE (32 slots).
 * If message slots are within this window, it'll likely to be filtered by gossipsub seenCache.
 * This is mainly for DOS protection, see https://github.com/ChainSafe/lodestar/issues/5393
 */
const DEFAULT_EARLIEST_PERMISSIBLE_SLOT_DISTANCE = 32;
/**
 * True if we want to process gossip object immediately, false if we check for bls and regen
 * in order to process the gossip object.
 */
const executeGossipWorkOrderObj = {
    [GossipType.beacon_block]: { bypassQueue: true },
    [GossipType.blob_sidecar]: { bypassQueue: true },
    [GossipType.data_column_sidecar]: { bypassQueue: true },
    [GossipType.beacon_aggregate_and_proof]: {},
    [GossipType.voluntary_exit]: {},
    [GossipType.bls_to_execution_change]: {},
    [GossipType.beacon_attestation]: {},
    [GossipType.proposer_slashing]: {},
    [GossipType.attester_slashing]: {},
    [GossipType.sync_committee_contribution_and_proof]: {},
    [GossipType.sync_committee]: {},
    [GossipType.light_client_finality_update]: {},
    [GossipType.light_client_optimistic_update]: {},
    [GossipType.execution_payload]: { bypassQueue: true },
    [GossipType.payload_attestation_message]: {},
    [GossipType.execution_payload_bid]: {},
};
const executeGossipWorkOrder = Object.keys(executeGossipWorkOrderObj);
// TODO: Arbitrary constant, check metrics
const MAX_JOBS_SUBMITTED_PER_TICK = 128;
// How many attestations (aggregate + unaggregate) we keep before new ones get dropped.
const MAX_QUEUED_UNKNOWN_BLOCK_GOSSIP_OBJECTS = 16_384;
// We don't want to process too many attestations in a single tick
// As seen on mainnet, attestation concurrency metric ranges from 1000 to 2000
// so make this constant a little bit conservative
const MAX_UNKNOWN_BLOCK_GOSSIP_OBJECTS_PER_TICK = 1024;
// Same motivation to JobItemQueue, we don't want to block the event loop
const PROCESS_UNKNOWN_BLOCK_GOSSIP_OBJECTS_YIELD_EVERY_MS = 50;
export { ReprocessRejectReason };
/**
 * Reprocess reject reason for metrics
 */
var ReprocessRejectReason;
(function (ReprocessRejectReason) {
    /**
     * There are too many attestations that have unknown block root.
     */
    ReprocessRejectReason["reached_limit"] = "reached_limit";
    /**
     * The awaiting attestation is pruned per clock slot.
     */
    ReprocessRejectReason["expired"] = "expired";
})(ReprocessRejectReason || (ReprocessRejectReason = {}));
export { CannotAcceptWorkReason };
/**
 * Cannot accept work reason for metrics
 */
var CannotAcceptWorkReason;
(function (CannotAcceptWorkReason) {
    /**
     * bls is busy.
     */
    CannotAcceptWorkReason["bls"] = "bls_busy";
    /**
     * regen is busy.
     */
    CannotAcceptWorkReason["regen"] = "regen_busy";
})(CannotAcceptWorkReason || (CannotAcceptWorkReason = {}));
/**
 * Network processor handles the gossip queues and throtles processing to not overload the main thread
 * - Decides when to process work and what to process
 *
 * What triggers execute work?
 *
 * - When work is submitted
 * - When downstream workers become available
 *
 * ### PendingGossipsubMessage beacon_attestation example
 *
 * For attestations, processing the message includes the steps:
 * 1. Pre shuffling sync validation
 * 2. Retrieve shuffling: async + goes into the regen queue and can be expensive
 * 3. Pre sig validation sync validation
 * 4. Validate BLS signature: async + goes into workers through another manager
 *
 * The gossip queues should receive "backpressue" from the regen and BLS workers queues.
 * Such that enough work is processed to fill either one of the queue.
 */
export class NetworkProcessor {
    opts;
    chain;
    events;
    logger;
    metrics;
    gossipValidatorFn;
    gossipValidatorBatchFn;
    gossipQueues;
    gossipTopicConcurrency;
    extractBlockSlotRootFns = createExtractBlockSlotRootFns();
    // we may not receive the block for Attestation and SignedAggregateAndProof messages, in that case PendingGossipsubMessage needs
    // to be stored in this Map and reprocessed once the block comes
    awaitingGossipsubMessagesByRootBySlot;
    unknownBlockGossipsubMessagesCount = 0;
    unknownRootsBySlot = new MapDef(() => new Set());
    constructor(modules, opts) {
        this.opts = opts;
        const { chain, events, logger, metrics } = modules;
        this.chain = chain;
        this.events = events;
        this.metrics = metrics;
        this.logger = logger;
        this.events = events;
        this.gossipQueues = createGossipQueues();
        this.gossipTopicConcurrency = mapValues(this.gossipQueues, () => 0);
        this.gossipValidatorFn = getGossipValidatorFn(modules.gossipHandlers ?? getGossipHandlers(modules, opts), modules);
        this.gossipValidatorBatchFn = getGossipValidatorBatchFn(modules.gossipHandlers ?? getGossipHandlers(modules, opts), modules);
        events.on(NetworkEvent.pendingGossipsubMessage, this.onPendingGossipsubMessage.bind(this));
        this.chain.emitter.on(routes.events.EventType.block, this.onBlockProcessed.bind(this));
        this.chain.clock.on(ClockEvent.slot, this.onClockSlot.bind(this));
        this.awaitingGossipsubMessagesByRootBySlot = new MapDef(() => new MapDef(() => new Set()));
        // TODO: Implement queues and priorization for ReqResp incoming requests
        // Listens to NetworkEvent.reqRespIncomingRequest event
        if (metrics) {
            metrics.gossipValidationQueue.length.addCollect(() => {
                for (const topic of executeGossipWorkOrder) {
                    metrics.gossipValidationQueue.length.set({ topic }, this.gossipQueues[topic].length);
                    metrics.gossipValidationQueue.keySize.set({ topic }, this.gossipQueues[topic].keySize);
                    metrics.gossipValidationQueue.concurrency.set({ topic }, this.gossipTopicConcurrency[topic]);
                }
                metrics.reprocessGossipAttestations.countPerSlot.set(this.unknownBlockGossipsubMessagesCount);
                // specific metric for beacon_attestation topic
                metrics.gossipValidationQueue.keyAge.reset();
                for (const ageMs of this.gossipQueues.beacon_attestation.getDataAgeMs()) {
                    metrics.gossipValidationQueue.keyAge.observe(ageMs / 1000);
                }
            });
        }
        // TODO: Pull new work when available
        // this.bls.onAvailable(() => this.executeWork());
        // this.regen.onAvailable(() => this.executeWork());
    }
    async stop() {
        this.events.off(NetworkEvent.pendingGossipsubMessage, this.onPendingGossipsubMessage);
        this.chain.emitter.off(routes.events.EventType.block, this.onBlockProcessed);
        this.chain.emitter.off(ClockEvent.slot, this.onClockSlot);
    }
    dropAllJobs() {
        for (const topic of executeGossipWorkOrder) {
            this.gossipQueues[topic].clear();
        }
    }
    dumpGossipQueue(topic) {
        const queue = this.gossipQueues[topic];
        if (queue === undefined) {
            throw Error(`Unknown gossipType ${topic}, known values: ${Object.keys(this.gossipQueues).join(", ")}`);
        }
        return queue.getAll();
    }
    searchUnknownSlotRoot({ slot, root }, source, peer) {
        if (this.chain.seenBlock(root) || this.unknownRootsBySlot.getOrDefault(slot).has(root)) {
            return;
        }
        // Search for the unknown block
        this.unknownRootsBySlot.getOrDefault(slot).add(root);
        this.chain.emitter.emit(ChainEvent.unknownBlockRoot, { rootHex: root, peer, source });
    }
    onPendingGossipsubMessage(message) {
        const topicType = message.topic.type;
        const extractBlockSlotRootFn = this.extractBlockSlotRootFns[topicType];
        // check block root of Attestation and SignedAggregateAndProof messages
        if (extractBlockSlotRootFn) {
            const slotRoot = extractBlockSlotRootFn(message.msg.data, message.topic.boundary.fork);
            // if slotRoot is null, it means the msg.data is invalid
            // in that case message will be rejected when deserializing data in later phase (gossipValidatorFn)
            if (slotRoot) {
                // DOS protection: avoid processing messages that are too old
                const { slot, root } = slotRoot;
                const clockSlot = this.chain.clock.currentSlot;
                const { fork } = message.topic.boundary;
                let earliestPermissableSlot = clockSlot - DEFAULT_EARLIEST_PERMISSIBLE_SLOT_DISTANCE;
                if (ForkSeq[fork] >= ForkSeq.deneb && topicType === GossipType.beacon_attestation) {
                    // post deneb, the attestations could be in current or previous epoch
                    earliestPermissableSlot = computeStartSlotAtEpoch(this.chain.clock.currentEpoch - 1);
                }
                if (slot < earliestPermissableSlot) {
                    // TODO: Should report the dropped job to gossip? It will be eventually pruned from the mcache
                    this.metrics?.networkProcessor.gossipValidationError.inc({
                        topic: topicType,
                        error: GossipErrorCode.PAST_SLOT,
                    });
                    return;
                }
                message.msgSlot = slot;
                // check if we processed a block with this root
                // no need to check if root is a descendant of the current finalized block, it will be checked once we validate the message if needed
                if (root && !this.chain.forkChoice.hasBlockHexUnsafe(root)) {
                    this.searchUnknownSlotRoot({ slot, root }, BlockInputSource.gossip, message.propagationSource.toString());
                    if (this.unknownBlockGossipsubMessagesCount > MAX_QUEUED_UNKNOWN_BLOCK_GOSSIP_OBJECTS) {
                        // TODO: Should report the dropped job to gossip? It will be eventually pruned from the mcache
                        this.metrics?.reprocessGossipAttestations.reject.inc({ reason: ReprocessRejectReason.reached_limit });
                        return;
                    }
                    this.metrics?.reprocessGossipAttestations.total.inc();
                    const awaitingGossipsubMessagesByRoot = this.awaitingGossipsubMessagesByRootBySlot.getOrDefault(slot);
                    const awaitingGossipsubMessages = awaitingGossipsubMessagesByRoot.getOrDefault(root);
                    awaitingGossipsubMessages.add(message);
                    this.unknownBlockGossipsubMessagesCount++;
                    return;
                }
            }
        }
        // bypass the check for other messages
        this.pushPendingGossipsubMessageToQueue(message);
    }
    pushPendingGossipsubMessageToQueue(message) {
        const topicType = message.topic.type;
        const droppedCount = this.gossipQueues[topicType].add(message);
        if (droppedCount) {
            // TODO: Should report the dropped job to gossip? It will be eventually pruned from the mcache
            this.metrics?.gossipValidationQueue.droppedJobs.inc({ topic: message.topic.type }, droppedCount);
        }
        // Tentatively perform work
        this.executeWork();
    }
    async onBlockProcessed({ slot, block: rootHex, }) {
        const byRootGossipsubMessages = this.awaitingGossipsubMessagesByRootBySlot.getOrDefault(slot);
        const waitingGossipsubMessages = byRootGossipsubMessages.getOrDefault(rootHex);
        if (waitingGossipsubMessages.size === 0) {
            return;
        }
        this.metrics?.reprocessGossipAttestations.resolve.inc(waitingGossipsubMessages.size);
        const nowSec = Date.now() / 1000;
        let count = 0;
        // TODO: we can group attestations to process in batches but since we have the SeenAttestationDatas
        // cache, it may not be necessary at this time
        for (const message of waitingGossipsubMessages) {
            this.metrics?.reprocessGossipAttestations.waitSecBeforeResolve.set(nowSec - message.seenTimestampSec);
            this.pushPendingGossipsubMessageToQueue(message);
            count++;
            // don't want to block the event loop, worse case it'd wait for 16_084 / 1024 * 50ms = 800ms which is not a big deal
            if (count === MAX_UNKNOWN_BLOCK_GOSSIP_OBJECTS_PER_TICK) {
                count = 0;
                await sleep(PROCESS_UNKNOWN_BLOCK_GOSSIP_OBJECTS_YIELD_EVERY_MS);
            }
        }
        byRootGossipsubMessages.delete(rootHex);
    }
    onClockSlot(clockSlot) {
        const nowSec = Date.now() / 1000;
        for (const [slot, gossipMessagesByRoot] of this.awaitingGossipsubMessagesByRootBySlot.entries()) {
            if (slot < clockSlot) {
                for (const gossipMessages of gossipMessagesByRoot.values()) {
                    for (const message of gossipMessages) {
                        this.metrics?.reprocessGossipAttestations.reject.inc({ reason: ReprocessRejectReason.expired });
                        this.metrics?.reprocessGossipAttestations.waitSecBeforeReject.set({ reason: ReprocessRejectReason.expired }, nowSec - message.seenTimestampSec);
                        // TODO: Should report the dropped job to gossip? It will be eventually pruned from the mcache
                    }
                }
                this.awaitingGossipsubMessagesByRootBySlot.delete(slot);
            }
        }
        pruneSetToMax(this.unknownRootsBySlot, MAX_UNKNOWN_ROOTS_SLOT_CACHE_SIZE);
        this.unknownBlockGossipsubMessagesCount = 0;
    }
    executeWork() {
        // TODO: Maybe de-bounce by timing the last time executeWork was run
        this.metrics?.networkProcessor.executeWorkCalls.inc();
        let jobsSubmitted = 0;
        job_loop: while (jobsSubmitted < MAX_JOBS_SUBMITTED_PER_TICK) {
            // Check if chain can accept work before calling queue.next() since it consumes the items
            const reason = this.checkAcceptWork();
            for (const topic of executeGossipWorkOrder) {
                // beacon block is guaranteed to be processed immedately
                // reason !== null means cannot accept work
                if (reason !== null && !executeGossipWorkOrderObj[topic]?.bypassQueue) {
                    this.metrics?.networkProcessor.canNotAcceptWork.inc({ reason });
                    break job_loop;
                }
                if (this.opts.maxGossipTopicConcurrency !== undefined &&
                    this.gossipTopicConcurrency[topic] > this.opts.maxGossipTopicConcurrency) {
                    // Reached concurrency limit for topic, continue to next topic
                    continue;
                }
                const item = this.gossipQueues[topic].next();
                const numMessages = Array.isArray(item) ? item.length : 1;
                if (item) {
                    this.gossipTopicConcurrency[topic] += numMessages;
                    this.processPendingGossipsubMessage(item)
                        .finally(() => {
                        this.gossipTopicConcurrency[topic] -= numMessages;
                    })
                        .catch((e) => this.logger.error("processGossipAttestations must not throw", {}, e));
                    jobsSubmitted += numMessages;
                    // Attempt to find more work, but check canAcceptWork() again and run executeGossipWorkOrder priorization
                    continue job_loop;
                }
            }
            // No item of work available on all queues, break off job_loop
            break;
        }
        if (jobsSubmitted > 0) {
            this.metrics?.networkProcessor.jobsSubmitted.observe(jobsSubmitted);
        }
    }
    async processPendingGossipsubMessage(messageOrArray) {
        const nowSec = Date.now() / 1000;
        if (Array.isArray(messageOrArray)) {
            for (const msg of messageOrArray) {
                msg.startProcessUnixSec = nowSec;
                if (msg.queueAddedMs !== undefined) {
                    this.metrics?.gossipValidationQueue.queueTime.observe(nowSec - msg.queueAddedMs / 1000);
                }
            }
        }
        else {
            // indexed queue is not used here
            messageOrArray.startProcessUnixSec = nowSec;
        }
        const acceptanceArr = Array.isArray(messageOrArray)
            ? // for beacon_attestation topic, process attestations with same attestation data
                // we always have msgSlot in beaccon_attestation topic so the type conversion is safe
                await this.gossipValidatorBatchFn(messageOrArray)
            : [
                // for other topics
                await this.gossipValidatorFn({ ...messageOrArray, msgSlot: messageOrArray.msgSlot ?? null }),
            ];
        if (Array.isArray(messageOrArray)) {
            for (const msg of messageOrArray) {
                this.trackJobTime(msg, messageOrArray.length);
            }
        }
        else {
            this.trackJobTime(messageOrArray, 1);
        }
        // Use setTimeout to yield to the macro queue
        // This is mostly due to too many attestation messages, and a gossipsub RPC may
        // contain multiple of them. This helps avoid the I/O lag issue.
        if (Array.isArray(messageOrArray)) {
            for (const [i, msg] of messageOrArray.entries()) {
                callInNextEventLoop(() => {
                    this.events.emit(NetworkEvent.gossipMessageValidationResult, {
                        msgId: msg.msgId,
                        propagationSource: msg.propagationSource,
                        acceptance: acceptanceArr[i],
                    });
                });
            }
        }
        else {
            callInNextEventLoop(() => {
                this.events.emit(NetworkEvent.gossipMessageValidationResult, {
                    msgId: messageOrArray.msgId,
                    propagationSource: messageOrArray.propagationSource,
                    acceptance: acceptanceArr[0],
                });
            });
        }
    }
    trackJobTime(message, numJob) {
        if (message.startProcessUnixSec !== null) {
            this.metrics?.gossipValidationQueue.jobWaitTime.observe({ topic: message.topic.type }, message.startProcessUnixSec - message.seenTimestampSec);
            // if it takes 64ms to process 64 jobs, the average job time is 1ms
            this.metrics?.gossipValidationQueue.jobTime.observe({ topic: message.topic.type }, (Date.now() / 1000 - message.startProcessUnixSec) / numJob);
        }
    }
    /**
     * Return null if chain can accept work, otherwise return the reason why it cannot accept work
     */
    checkAcceptWork() {
        if (!this.chain.blsThreadPoolCanAcceptWork()) {
            return CannotAcceptWorkReason.bls;
        }
        if (!this.chain.regenCanAcceptWork()) {
            return CannotAcceptWorkReason.regen;
        }
        return null;
    }
}
//# sourceMappingURL=index.js.map