import { routes } from "@lodestar/api";
import { getSafeExecutionBlockHash } from "@lodestar/fork-choice";
import { ForkSeq, SLOTS_PER_EPOCH, isForkPostBellatrix } from "@lodestar/params";
import { StateHashTreeRootSource, computeEpochAtSlot, computeTimeAtSlot, } from "@lodestar/state-transition";
import { fromHex, isErrorAborted, sleep } from "@lodestar/utils";
import { GENESIS_SLOT, ZERO_HASH_HEX } from "../constants/constants.js";
import { BuilderStatus } from "../execution/builder/http.js";
import { ClockEvent } from "../util/clock.js";
import { isQueueErrorAborted } from "../util/queue/index.js";
import { ForkchoiceCaller } from "./forkChoice/index.js";
import { getPayloadAttributesForSSE, prepareExecutionPayload } from "./produceBlock/produceBlockBody.js";
import { RegenCaller } from "./regen/index.js";
// TODO GLOAS: re-evaluate this timing
/* With 12s slot times, this scheduler will run 4s before the start of each slot (`12 - 0.6667 * 12 = 4`). */
export const PREPARE_NEXT_SLOT_BPS = 6667;
/* We don't want to do more epoch transition than this */
const PREPARE_EPOCH_LIMIT = 1;
/**
 * At Bellatrix, if we are responsible for proposing in next slot, we want to prepare payload
 * 4s before the start of next slot at PREPARE_NEXT_SLOT_BPS of the current slot.
 *
 * For all forks, when clock reaches PREPARE_NEXT_SLOT_BPS of slot before an epoch, we want to prepare for the next epoch
 * transition from our head so that:
 * + validators vote for block head on time through attestation
 * + validators propose blocks on time
 * + For Bellatrix, to compute proposers of next epoch so that we can prepare new payloads
 *
 */
export class PrepareNextSlotScheduler {
    chain;
    config;
    metrics;
    logger;
    signal;
    constructor(chain, config, metrics, logger, signal) {
        this.chain = chain;
        this.config = config;
        this.metrics = metrics;
        this.logger = logger;
        this.signal = signal;
        this.chain.clock.on(ClockEvent.slot, this.prepareForNextSlot);
        this.signal.addEventListener("abort", () => {
            this.chain.clock.off(ClockEvent.slot, this.prepareForNextSlot);
        }, { once: true });
    }
    /**
     * Use clockSlot instead of clockEpoch to schedule the task at more exact time.
     */
    prepareForNextSlot = async (clockSlot) => {
        const prepareSlot = clockSlot + 1;
        const prepareEpoch = computeEpochAtSlot(prepareSlot);
        const nextEpoch = computeEpochAtSlot(clockSlot) + 1;
        const isEpochTransition = prepareEpoch === nextEpoch;
        const fork = this.config.getForkName(prepareSlot);
        // Early return if we are pre-genesis
        //  or we are pre-bellatrix and this is not an epoch transition
        if (prepareSlot <= GENESIS_SLOT || (ForkSeq[fork] < ForkSeq.bellatrix && !isEpochTransition)) {
            return;
        }
        try {
            // At PREPARE_NEXT_SLOT_BPS (~67%) of the current slot we prepare payload for the next slot
            // or precompute epoch transition
            await sleep(this.config.getSlotComponentDurationMs(PREPARE_NEXT_SLOT_BPS), this.signal);
            // calling updateHead() here before we produce a block to reduce reorg possibility
            const headBlock = this.chain.recomputeForkChoiceHead(ForkchoiceCaller.prepareNextSlot);
            const { slot: headSlot, blockRoot: headRoot } = headBlock;
            // PS: previously this was comparing slots, but that gave no leway on the skipped
            // slots on epoch bounday. Making it more fluid.
            if (prepareSlot - headSlot > PREPARE_EPOCH_LIMIT * SLOTS_PER_EPOCH) {
                this.metrics?.precomputeNextEpochTransition.count.inc({ result: "skip" }, 1);
                this.logger.debug("Skipping PrepareNextSlotScheduler - head slot is too behind current slot", {
                    nextEpoch,
                    headSlot,
                    clockSlot,
                });
                return;
            }
            this.logger.verbose("Running prepareForNextSlot", {
                nextEpoch,
                prepareSlot,
                headSlot,
                headRoot,
                isEpochTransition,
            });
            const precomputeEpochTransitionTimer = isEpochTransition
                ? this.metrics?.precomputeNextEpochTransition.duration.startTimer()
                : null;
            const start = Date.now();
            // No need to wait for this or the clock drift
            // Pre Bellatrix: we only do precompute state transition for the last slot of epoch
            // For Bellatrix, we always do the `processSlots()` to prepare payload for the next slot
            const prepareState = await this.chain.regen.getBlockSlotState(headBlock, prepareSlot, 
            // the slot 0 of next epoch will likely use this Previous Root Checkpoint state for state transition so we transfer cache here
            // the resulting state with cache will be cached in Checkpoint State Cache which is used for the upcoming block processing
            // for other slots dontTransferCached=true because we don't run state transition on this state
            { dontTransferCache: !isEpochTransition }, RegenCaller.precomputeEpoch);
            if (isForkPostBellatrix(fork)) {
                const proposerIndex = prepareState.epochCtx.getBeaconProposer(prepareSlot);
                const feeRecipient = this.chain.beaconProposerCache.get(proposerIndex);
                let updatedPrepareState = prepareState;
                let updatedHeadRoot = headRoot;
                if (feeRecipient) {
                    // If we are proposing next slot, we need to predict if we can proposer-boost-reorg or not
                    const proposerHead = this.chain.predictProposerHead(clockSlot);
                    const { slot: proposerHeadSlot, blockRoot: proposerHeadRoot } = proposerHead;
                    // If we predict we can reorg, update prepareState with proposer head block
                    if (proposerHeadRoot !== headRoot || proposerHeadSlot !== headSlot) {
                        this.logger.verbose("Weak head detected. May build on parent block instead", {
                            proposerHeadSlot,
                            proposerHeadRoot,
                            headSlot,
                            headRoot,
                        });
                        this.metrics?.weakHeadDetected.inc();
                        updatedPrepareState = (await this.chain.regen.getBlockSlotState(proposerHead, prepareSlot, 
                        // only transfer cache if epoch transition because that's the state we will use to stateTransition() the 1st block of epoch
                        { dontTransferCache: !isEpochTransition }, RegenCaller.predictProposerHead));
                        updatedHeadRoot = proposerHeadRoot;
                    }
                    // Update the builder status, if enabled shoot an api call to check status
                    this.chain.updateBuilderStatus(clockSlot);
                    if (this.chain.executionBuilder?.status === BuilderStatus.enabled) {
                        this.chain.executionBuilder.checkStatus().catch((e) => {
                            this.logger.error("Builder disabled as the check status api failed", { prepareSlot }, e);
                        });
                    }
                    const preparationTime = computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
                    this.metrics?.blockPayload.payloadAdvancePrepTime.observe(preparationTime);
                    const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice);
                    const finalizedBlockHash = this.chain.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
                    // awaiting here instead of throwing an async call because there is no other task
                    // left for scheduler and this gives nice sematics to catch and log errors in the
                    // try/catch wrapper here.
                    await prepareExecutionPayload(this.chain, this.logger, fork, // State is of execution type
                    fromHex(updatedHeadRoot), safeBlockHash, finalizedBlockHash, updatedPrepareState, feeRecipient);
                    this.logger.verbose("PrepareNextSlotScheduler prepared new payload", {
                        prepareSlot,
                        proposerIndex,
                        feeRecipient,
                    });
                }
                this.computeStateHashTreeRoot(updatedPrepareState, isEpochTransition);
                // If emitPayloadAttributes is true emit a SSE payloadAttributes event
                if (this.chain.opts.emitPayloadAttributes === true &&
                    this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes)) {
                    const data = getPayloadAttributesForSSE(fork, this.chain, {
                        prepareState: updatedPrepareState,
                        prepareSlot,
                        parentBlockRoot: fromHex(headRoot),
                        // The likely consumers of this API are builders and will anyway ignore the
                        // feeRecipient, so just pass zero hash for now till a real use case arises
                        feeRecipient: "0x0000000000000000000000000000000000000000000000000000000000000000",
                    });
                    this.chain.emitter.emit(routes.events.EventType.payloadAttributes, { data, version: fork });
                }
            }
            else {
                this.computeStateHashTreeRoot(prepareState, isEpochTransition);
            }
            // assuming there is no reorg, it caches the checkpoint state & helps avoid doing a full state transition in the next slot
            //  + when gossip block comes, we need to validate and run state transition
            //  + if next slot is a skipped slot, it'd help getting target checkpoint state faster to validate attestations
            if (isEpochTransition) {
                this.metrics?.precomputeNextEpochTransition.count.inc({ result: "success" }, 1);
                const previousHits = this.chain.regen.updatePreComputedCheckpoint(headRoot, nextEpoch);
                if (previousHits === 0) {
                    this.metrics?.precomputeNextEpochTransition.waste.inc();
                }
                this.metrics?.precomputeNextEpochTransition.hits.set(previousHits ?? 0);
                this.logger.verbose("Completed PrepareNextSlotScheduler epoch transition", {
                    nextEpoch,
                    headSlot,
                    prepareSlot,
                    previousHits,
                    durationMs: Date.now() - start,
                });
                precomputeEpochTransitionTimer?.();
            }
        }
        catch (e) {
            if (!isErrorAborted(e) && !isQueueErrorAborted(e)) {
                this.metrics?.precomputeNextEpochTransition.count.inc({ result: "error" }, 1);
                this.logger.error("Failed to run prepareForNextSlot", { nextEpoch, isEpochTransition, prepareSlot }, e);
            }
        }
    };
    computeStateHashTreeRoot(state, isEpochTransition) {
        // cache HashObjects for faster hashTreeRoot() later, especially for computeNewStateRoot() if we need to produce a block at slot 0 of epoch
        // see https://github.com/ChainSafe/lodestar/issues/6194
        const hashTreeRootTimer = this.metrics?.stateHashTreeRootTime.startTimer({
            source: isEpochTransition ? StateHashTreeRootSource.prepareNextEpoch : StateHashTreeRootSource.prepareNextSlot,
        });
        state.hashTreeRoot();
        hashTreeRootTimer?.();
    }
}
//# sourceMappingURL=prepareNextSlot.js.map