import { routes } from "@lodestar/api";
import { ApiError } from "@lodestar/api/server";
import { PayloadStatus } from "@lodestar/fork-choice";
import { BUILDER_INDEX_SELF_BUILD, NUMBER_OF_COLUMNS, SLOTS_PER_HISTORICAL_ROOT, isForkPostBellatrix, isForkPostDeneb, isForkPostElectra, isForkPostFulu, isForkPostGloas, } from "@lodestar/params";
import { computeEpochAtSlot, computeTimeAtSlot, reconstructSignedBlockContents, signedBeaconBlockToBlinded, signedBlockToSignedHeader, } from "@lodestar/state-transition";
import { ProducedBlockSource, isDenebBlockContents, sszTypesFor, } from "@lodestar/types";
import { fromHex, sleep, toHex, toRootHex } from "@lodestar/utils";
import { BlockInputSource, isBlockInputBlobs, isBlockInputColumns } from "../../../../chain/blocks/blockInput/index.js";
import { verifyBlocksInEpoch } from "../../../../chain/blocks/verifyBlock.js";
import { ChainEvent } from "../../../../chain/emitter.js";
import { BlockError, BlockErrorCode, BlockGossipError } from "../../../../chain/errors/index.js";
import { BlockType, } from "../../../../chain/produceBlock/index.js";
import { validateGossipBlock } from "../../../../chain/validation/block.js";
import { OpSource } from "../../../../chain/validatorMonitor.js";
import { computePreFuluKzgCommitmentsInclusionProof, getBlobSidecars, kzgCommitmentToVersionedHash, reconstructBlobs, } from "../../../../util/blobs.js";
import { getDataColumnSidecarsForGloas, getDataColumnSidecarsFromBlock } from "../../../../util/dataColumns.js";
import { isOptimisticBlock } from "../../../../util/forkChoice.js";
import { kzg } from "../../../../util/kzg.js";
import { promiseAllMaybeAsync } from "../../../../util/promises.js";
import { assertUniqueItems } from "../../utils.js";
import { getBlockResponse, toBeaconHeaderResponse } from "./utils.js";
/**
 * Validator clock may be advanced from beacon's clock. If the validator requests a resource in a
 * future slot, wait some time instead of rejecting the request because it's in the future
 */
const MAX_API_CLOCK_DISPARITY_MS = 1000;
/**
 * PeerID of identity keypair to signal self for score reporting
 */
const IDENTITY_PEER_ID = ""; // TODO: Compute identity keypair
export function getBeaconBlockApi({ chain, config, metrics, network, db, }) {
    const publishBlock = async ({ signedBlockContents, broadcastValidation }, _context, opts = {}) => {
        const seenTimestampSec = Date.now() / 1000;
        const signedBlock = signedBlockContents.signedBlock;
        const slot = signedBlock.message.slot;
        const fork = config.getForkName(slot);
        const blockRoot = toRootHex(chain.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(signedBlock.message));
        // TODO GLOAS: handle new BlockInput type
        const blockForImport = chain.seenBlockInputCache.getByBlock({
            block: signedBlock,
            source: BlockInputSource.api,
            seenTimestampSec,
            blockRootHex: blockRoot,
        });
        let blobSidecars, dataColumnSidecars;
        if (isDenebBlockContents(signedBlockContents)) {
            if (isForkPostFulu(fork)) {
                const timer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer();
                // If the block was produced by this node, we will already have computed cells
                // Otherwise, we will compute them from the blobs in this function
                const cells = chain.blockProductionCache.get(blockRoot)?.cells ??
                    signedBlockContents.blobs.map((blob) => kzg.computeCells(blob));
                const cellsAndProofs = cells.map((rowCells, rowIndex) => ({
                    cells: rowCells,
                    proofs: signedBlockContents.kzgProofs.slice(rowIndex * NUMBER_OF_COLUMNS, (rowIndex + 1) * NUMBER_OF_COLUMNS),
                }));
                dataColumnSidecars = getDataColumnSidecarsFromBlock(config, signedBlock, cellsAndProofs);
                timer?.();
                blobSidecars = [];
            }
            else if (isForkPostDeneb(fork)) {
                blobSidecars = getBlobSidecars(config, signedBlock, signedBlockContents.blobs, signedBlockContents.kzgProofs);
                dataColumnSidecars = [];
            }
            else {
                throw Error(`Invalid data fork=${fork} for publish`);
            }
        }
        else {
            blobSidecars = [];
            dataColumnSidecars = [];
        }
        if (isBlockInputColumns(blockForImport)) {
            for (const dataColumnSidecar of dataColumnSidecars) {
                blockForImport.addColumn({
                    blockRootHex: blockRoot,
                    columnSidecar: dataColumnSidecar,
                    source: BlockInputSource.api,
                    seenTimestampSec,
                }, 
                // In multi-BN setups (DVT, fallback), the same block may be published to multiple nodes.
                // Data columns may arrive via gossip from another node before the API publish completes,
                // so we allow duplicates here instead of throwing an error.
                { throwOnDuplicateAdd: false });
            }
        }
        else if (isBlockInputBlobs(blockForImport)) {
            for (const blobSidecar of blobSidecars) {
                blockForImport.addBlob({
                    blockRootHex: blockRoot,
                    blobSidecar,
                    source: BlockInputSource.api,
                    seenTimestampSec,
                }, 
                // Same as above for columns
                { throwOnDuplicateAdd: false });
            }
        }
        // check what validations have been requested before broadcasting and publishing the block
        // TODO: add validation time to metrics
        broadcastValidation = broadcastValidation ?? routes.beacon.BroadcastValidation.gossip;
        // if block is locally produced, full or blinded, it already is 'consensus' validated as it went through
        // state transition to produce the stateRoot
        // bodyRoot should be the same to produced block
        const bodyRoot = toRootHex(chain.config.getForkTypes(slot).BeaconBlockBody.hashTreeRoot(signedBlock.message.body));
        const blockLocallyProduced = chain.blockProductionCache.has(blockRoot);
        const valLogMeta = { slot, blockRoot, bodyRoot, broadcastValidation, blockLocallyProduced };
        switch (broadcastValidation) {
            case routes.beacon.BroadcastValidation.gossip: {
                if (!blockLocallyProduced) {
                    try {
                        await validateGossipBlock(config, chain, signedBlock, fork);
                    }
                    catch (error) {
                        if (error instanceof BlockGossipError && error.type.code === BlockErrorCode.ALREADY_KNOWN) {
                            chain.logger.debug("Ignoring known block during publishing", valLogMeta);
                            // Blocks might already be published by another node as part of a fallback setup or DVT cluster
                            // and can reach our node by gossip before the api. The error can be ignored and should not result in a 500 response.
                            return;
                        }
                        chain.logger.error("Gossip validations failed while publishing the block", valLogMeta, error);
                        chain.persistInvalidSszValue(chain.config.getForkTypes(slot).SignedBeaconBlock, signedBlock, "api_reject_gossip_failure");
                        throw error;
                    }
                }
                chain.logger.debug("Gossip checks validated while publishing the block", valLogMeta);
                break;
            }
            case routes.beacon.BroadcastValidation.consensusAndEquivocation:
            case routes.beacon.BroadcastValidation.consensus: {
                // check if this beacon node produced the block else run validations
                if (!blockLocallyProduced) {
                    const parentBlock = chain.forkChoice.getBlockDefaultStatus(signedBlock.message.parentRoot);
                    if (parentBlock === null) {
                        chain.emitter.emit(ChainEvent.unknownParent, {
                            blockInput: blockForImport,
                            peer: IDENTITY_PEER_ID,
                            source: BlockInputSource.api,
                        });
                        chain.persistInvalidSszValue(chain.config.getForkTypes(slot).SignedBeaconBlock, signedBlock, "api_reject_parent_unknown");
                        throw new BlockError(signedBlock, {
                            code: BlockErrorCode.PARENT_UNKNOWN,
                            parentRoot: toRootHex(signedBlock.message.parentRoot),
                        });
                    }
                    try {
                        await verifyBlocksInEpoch.call(chain, parentBlock, [blockForImport], {
                            ...opts,
                            verifyOnly: true,
                            skipVerifyBlockSignatures: true,
                            skipVerifyExecutionPayload: true,
                            seenTimestampSec,
                        });
                    }
                    catch (error) {
                        chain.logger.error("Consensus checks failed while publishing the block", valLogMeta, error);
                        chain.persistInvalidSszValue(chain.config.getForkTypes(slot).SignedBeaconBlock, signedBlock, "api_reject_consensus_failure");
                        throw error;
                    }
                }
                chain.logger.debug("Consensus validated while publishing block", valLogMeta);
                if (broadcastValidation === routes.beacon.BroadcastValidation.consensusAndEquivocation) {
                    const message = `Equivocation checks not yet implemented for broadcastValidation=${broadcastValidation}`;
                    if (chain.opts.broadcastValidationStrictness === "error") {
                        throw Error(message);
                    }
                    chain.logger.warn(message, valLogMeta);
                }
                break;
            }
            case routes.beacon.BroadcastValidation.none: {
                chain.logger.debug("Skipping broadcast validation", valLogMeta);
                break;
            }
            default: {
                // error or log warning we do not support this validation
                const message = `Broadcast validation of ${broadcastValidation} type not implemented yet`;
                if (chain.opts.broadcastValidationStrictness === "error") {
                    throw Error(message);
                }
                chain.logger.warn(message, valLogMeta);
            }
        }
        // Simple implementation of a pending block queue. Keeping the block here recycles the API logic, and keeps the
        // REST request promise without any extra infrastructure.
        const msToBlockSlot = computeTimeAtSlot(config, slot, chain.genesisTime) * 1000 - Date.now();
        if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) {
            // If block is a bit early, hold it in a promise. Equivalent to a pending queue.
            await sleep(msToBlockSlot);
        }
        // TODO: Validate block
        const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime);
        metrics?.gossipBlock.elapsedTimeTillReceived.observe({ source: OpSource.api }, delaySec);
        chain.validatorMonitor?.registerBeaconBlock(OpSource.api, delaySec, signedBlock.message);
        chain.logger.info("Publishing block", valLogMeta);
        const publishPromises = [
            // Send the block, regardless of whether or not it is valid. The API
            // specification is very clear that this is the desired behavior.
            //
            // - Publish blobs and block before importing so that network can see them asap
            // - Publish block first because
            //     a) as soon as node sees block they can start processing it while data is in transit
            //     b) getting block first allows nodes to use getBlobs from local ELs and save
            //        import latency and hopefully bandwidth
            //
            () => network.publishBeaconBlock(signedBlock),
            ...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)),
            ...blobSidecars.map((blobSidecar) => () => network.publishBlobSidecar(blobSidecar)),
            () => 
            // there is no rush to persist block since we published it to gossip anyway
            chain
                .processBlock(blockForImport, opts)
                .catch((e) => {
                if (e instanceof BlockError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) {
                    chain.emitter.emit(ChainEvent.unknownParent, {
                        blockInput: blockForImport,
                        peer: IDENTITY_PEER_ID,
                        source: BlockInputSource.api,
                    });
                }
                throw e;
            }),
        ];
        const sentPeersArr = await promiseAllMaybeAsync(publishPromises);
        if (isForkPostGloas(fork)) {
            // After gloas, data columns are not published with the block but when publishing the execution payload envelope
        }
        else if (isForkPostFulu(fork)) {
            let columnsPublishedWithZeroPeers = 0;
            // sent peers per topic are logged in network.publishGossip(), here we only track metrics for it
            // starting from fulu, we have to push to 128 subnets so need to make sure we have enough sent peers per topic
            // + 1 because we publish to beacon_block first
            for (let i = 0; i < dataColumnSidecars.length; i++) {
                // + 1 because we publish to beacon_block first
                const sentPeers = sentPeersArr[i + 1];
                // sent peers could be 0 as we set `allowPublishToZeroTopicPeers=true` in network.publishDataColumnSidecar() api
                metrics?.dataColumns.sentPeersPerSubnet.observe(sentPeers);
                if (sentPeers === 0) {
                    columnsPublishedWithZeroPeers++;
                }
            }
            if (columnsPublishedWithZeroPeers > 0) {
                chain.logger.warn("Published data columns to 0 peers, increased risk of reorg", {
                    slot,
                    blockRoot,
                    columns: columnsPublishedWithZeroPeers,
                });
            }
        }
        chain.emitter.emit(routes.events.EventType.blockGossip, { slot, block: blockRoot });
        if (isBlockInputColumns(blockForImport)) {
            const dataColumns = blockForImport.getAllColumns();
            metrics?.dataColumns.bySource.inc({ source: BlockInputSource.api }, dataColumns.length);
            if (chain.emitter.listenerCount(routes.events.EventType.dataColumnSidecar)) {
                for (const dataColumnSidecar of dataColumns) {
                    chain.emitter.emit(routes.events.EventType.dataColumnSidecar, {
                        blockRoot,
                        slot,
                        index: dataColumnSidecar.index,
                        kzgCommitments: dataColumnSidecar.kzgCommitments.map(toHex),
                    });
                }
            }
        }
        else if (isBlockInputBlobs(blockForImport) && chain.emitter.listenerCount(routes.events.EventType.blobSidecar)) {
            const blobSidecars = blockForImport.getBlobs();
            const versionedHashes = blockForImport.getVersionedHashes();
            for (const blobSidecar of blobSidecars) {
                const { index, kzgCommitment } = blobSidecar;
                chain.emitter.emit(routes.events.EventType.blobSidecar, {
                    blockRoot,
                    slot,
                    index,
                    kzgCommitment: toHex(kzgCommitment),
                    versionedHash: toHex(versionedHashes[index]),
                });
            }
        }
    };
    const publishBlindedBlock = async ({ signedBlindedBlock }, context, opts = {}) => {
        const slot = signedBlindedBlock.message.slot;
        const blockRoot = toRootHex(chain.config
            .getPostBellatrixForkTypes(signedBlindedBlock.message.slot)
            .BlindedBeaconBlock.hashTreeRoot(signedBlindedBlock.message));
        const fork = config.getForkName(slot);
        if (isForkPostGloas(fork)) {
            throw new ApiError(400, `Blinded blocks are not available for post-gloas fork=${fork}`);
        }
        // Either the payload/blobs are cached from i) engine locally or ii) they are from the builder
        const producedResult = chain.blockProductionCache.get(blockRoot);
        if (producedResult !== undefined && producedResult.type !== BlockType.Blinded) {
            const source = ProducedBlockSource.engine;
            chain.logger.debug("Reconstructing the full signed block contents", { slot, blockRoot, source });
            const signedBlockContents = reconstructSignedBlockContents(fork, signedBlindedBlock, producedResult.executionPayload ?? null, producedResult.blobsBundle ?? null);
            chain.logger.info("Publishing assembled block", { slot, blockRoot, source });
            return publishBlock({ signedBlockContents }, { ...context, sszBytes: null }, opts);
        }
        const source = ProducedBlockSource.builder;
        if (isForkPostFulu(fork)) {
            await submitBlindedBlockToBuilder(chain, {
                data: signedBlindedBlock,
                bytes: context?.sszBytes,
            });
            chain.logger.info("Submitted blinded block to builder for publishing", { slot, blockRoot });
        }
        else {
            // TODO: After fulu is live and all builders support submitBlindedBlockV2, we can safely remove
            // this code block and related functions
            chain.logger.debug("Reconstructing full signed block contents", { slot, blockRoot, source });
            const signedBlockContents = await reconstructBuilderSignedBlockContents(chain, {
                data: signedBlindedBlock,
                bytes: context?.sszBytes,
            });
            // the full block is published by relay and it's possible that the block is already known to us
            // by gossip
            //
            // see: https://github.com/ChainSafe/lodestar/issues/5404
            chain.logger.info("Publishing assembled block", { slot, blockRoot, source });
            return publishBlock({ signedBlockContents }, { ...context, sszBytes: null }, { ...opts, ignoreIfKnown: true });
        }
    };
    return {
        async getBlockHeaders({ slot, parentRoot }) {
            // TODO - SLOW CODE: This code seems like it could be improved
            // If one block in the response contains an optimistic block, mark the entire response as optimistic
            let executionOptimistic = false;
            // If one block in the response is non finalized, mark the entire response as unfinalized
            let finalized = true;
            const result = [];
            if (parentRoot) {
                const finalizedBlock = await db.blockArchive.getByParentRoot(fromHex(parentRoot));
                if (finalizedBlock) {
                    result.push(toBeaconHeaderResponse(config, finalizedBlock, true));
                }
                const nonFinalizedBlocks = chain.forkChoice.getBlockSummariesByParentRoot(parentRoot);
                await Promise.all(nonFinalizedBlocks.map(async (summary) => {
                    const blockResult = await chain.getBlockByRoot(summary.blockRoot);
                    if (blockResult) {
                        const canonical = chain.forkChoice.getCanonicalBlockAtSlot(blockResult.block.message.slot);
                        if (canonical) {
                            result.push(toBeaconHeaderResponse(config, blockResult.block, canonical.blockRoot === summary.blockRoot));
                            if (isOptimisticBlock(canonical)) {
                                executionOptimistic = true;
                            }
                            // Block from hot db which only contains unfinalized blocks
                            finalized = false;
                        }
                    }
                }));
                return {
                    data: result.filter((item) => 
                    // skip if no slot filter
                    !(slot !== undefined && slot !== 0) || item.header.message.slot === slot),
                    meta: { executionOptimistic, finalized },
                };
            }
            const headSlot = chain.forkChoice.getHead().slot;
            if (!parentRoot && slot === undefined) {
                slot = headSlot;
            }
            if (slot !== undefined) {
                // future slot
                if (slot > headSlot) {
                    return { data: [], meta: { executionOptimistic: false, finalized: false } };
                }
                const canonicalBlock = await chain.getCanonicalBlockAtSlot(slot);
                // skip slot
                if (!canonicalBlock) {
                    return { data: [], meta: { executionOptimistic: false, finalized: false } };
                }
                const canonicalRoot = config
                    .getForkTypes(canonicalBlock.block.message.slot)
                    .BeaconBlock.hashTreeRoot(canonicalBlock.block.message);
                result.push(toBeaconHeaderResponse(config, canonicalBlock.block, true));
                if (!canonicalBlock.finalized) {
                    finalized = false;
                }
                // fork blocks
                // TODO: What is this logic?
                await Promise.all(chain.forkChoice.getBlockSummariesAtSlot(slot).map(async (summary) => {
                    if (isOptimisticBlock(summary)) {
                        executionOptimistic = true;
                    }
                    finalized = false;
                    if (summary.blockRoot !== toRootHex(canonicalRoot)) {
                        const blockResult = await chain.getBlockByRoot(summary.blockRoot);
                        if (blockResult) {
                            result.push(toBeaconHeaderResponse(config, blockResult.block));
                        }
                    }
                }));
            }
            return {
                data: result,
                meta: { executionOptimistic, finalized },
            };
        },
        async getBlockHeader({ blockId }) {
            const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId);
            return {
                data: toBeaconHeaderResponse(config, block, true),
                meta: { executionOptimistic, finalized },
            };
        },
        async getBlockV2({ blockId }) {
            const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId);
            return {
                data: block,
                meta: {
                    executionOptimistic,
                    finalized,
                    version: config.getForkName(block.message.slot),
                },
            };
        },
        async getBlindedBlock({ blockId }) {
            const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId);
            const fork = config.getForkName(block.message.slot);
            if (isForkPostGloas(fork)) {
                throw new ApiError(400, `Blinded blocks are not available for post-gloas fork=${fork}`);
            }
            return {
                data: isForkPostBellatrix(fork)
                    ? signedBeaconBlockToBlinded(config, block)
                    : block,
                meta: {
                    executionOptimistic,
                    finalized,
                    version: fork,
                },
            };
        },
        async getBlockAttestations({ blockId }) {
            const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId);
            const fork = config.getForkName(block.message.slot);
            if (isForkPostElectra(fork)) {
                throw new ApiError(400, `Use getBlockAttestationsV2 to retrieve block attestations for post-electra fork=${fork}`);
            }
            return {
                data: block.message.body.attestations,
                meta: { executionOptimistic, finalized },
            };
        },
        async getBlockAttestationsV2({ blockId }) {
            const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId);
            return {
                data: block.message.body.attestations,
                meta: { executionOptimistic, finalized, version: config.getForkName(block.message.slot) },
            };
        },
        async getBlockRoot({ blockId }) {
            // Fast path: From head state already available in memory get historical blockRoot
            const slot = typeof blockId === "string" ? parseInt(blockId) : blockId;
            if (!Number.isNaN(slot)) {
                const head = chain.forkChoice.getHead();
                if (slot === head.slot) {
                    return {
                        data: { root: fromHex(head.blockRoot) },
                        meta: { executionOptimistic: isOptimisticBlock(head), finalized: false },
                    };
                }
                if (slot < head.slot && head.slot <= slot + SLOTS_PER_HISTORICAL_ROOT) {
                    const state = chain.getHeadState();
                    return {
                        data: { root: state.blockRoots.get(slot % SLOTS_PER_HISTORICAL_ROOT) },
                        meta: {
                            executionOptimistic: isOptimisticBlock(head),
                            finalized: computeEpochAtSlot(slot) <= chain.forkChoice.getFinalizedCheckpoint().epoch,
                        },
                    };
                }
            }
            else if (blockId === "head") {
                const head = chain.forkChoice.getHead();
                return {
                    data: { root: fromHex(head.blockRoot) },
                    meta: { executionOptimistic: isOptimisticBlock(head), finalized: false },
                };
            }
            // Slow path
            const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId);
            return {
                data: { root: config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message) },
                meta: { executionOptimistic, finalized },
            };
        },
        publishBlock,
        publishBlindedBlock,
        async publishBlindedBlockV2(args, context, opts) {
            await publishBlindedBlock(args, context, opts);
        },
        async publishBlockV2(args, context, opts) {
            await publishBlock(args, context, opts);
        },
        async publishExecutionPayloadEnvelope({ signedExecutionPayloadEnvelope }) {
            const seenTimestampSec = Date.now() / 1000;
            const envelope = signedExecutionPayloadEnvelope.message;
            const slot = envelope.slot;
            const fork = config.getForkName(slot);
            const blockRootHex = toRootHex(envelope.beaconBlockRoot);
            if (!isForkPostGloas(fork)) {
                throw new ApiError(400, `publishExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`);
            }
            // TODO GLOAS: review checks, do we want to implement `broadcast_validation`?
            const block = chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.EMPTY);
            if (block === null) {
                throw new ApiError(404, `Block not found for beacon block root ${blockRootHex}`);
            }
            if (block.slot !== slot) {
                throw new ApiError(400, `Envelope slot ${slot} does not match block slot ${block.slot}`);
            }
            const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD;
            let dataColumnSidecars = [];
            if (isSelfBuild) {
                // For self-builds, construct and publish data column sidecars from cached block production data
                const cachedResult = chain.blockProductionCache.get(blockRootHex);
                if (cachedResult === undefined) {
                    throw new ApiError(404, `No cached block production result found for block root ${blockRootHex}`);
                }
                if (!isForkPostGloas(cachedResult.fork)) {
                    throw new ApiError(400, `Cached block production result is for pre-gloas fork=${cachedResult.fork}`);
                }
                if (cachedResult.type !== BlockType.Full) {
                    throw new ApiError(400, "Cached block production result is not full block");
                }
                if (cachedResult.cells && cachedResult.blobsBundle.commitments.length > 0) {
                    const cellsAndProofs = cachedResult.cells.map((rowCells, rowIndex) => ({
                        cells: rowCells,
                        proofs: cachedResult.blobsBundle.proofs.slice(rowIndex * NUMBER_OF_COLUMNS, (rowIndex + 1) * NUMBER_OF_COLUMNS),
                    }));
                    dataColumnSidecars = getDataColumnSidecarsForGloas(slot, envelope.beaconBlockRoot, cellsAndProofs);
                }
            }
            else {
                // TODO GLOAS: will this api be used by builders or only for self-building?
            }
            // TODO GLOAS: Verify execution payload envelope signature
            // For self-builds, the proposer signs with their own validator key
            // For external builders, verify using the builder's registered pubkey
            // Use verify_execution_payload_envelope_signature(state, signed_envelope)
            // TODO GLOAS: Process execution payload via state transition
            // Call process_execution_payload(state, signed_envelope, execution_engine)
            // TODO GLOAS: Update fork choice with the execution payload
            // Call on_execution_payload(store, signed_envelope) to update fork choice state
            // TODO GLOAS: Add envelope and data columns to block input via seenBlockInputCache
            // and trigger block import (Gloas block import requires both beacon block and envelope)
            const valLogMeta = {
                slot,
                blockRoot: blockRootHex,
                builderIndex: envelope.builderIndex,
                isSelfBuild,
                dataColumns: dataColumnSidecars.length,
            };
            // If called near a slot boundary (e.g. late in slot N-1), hold briefly so gossip aligns with slot N.
            const msToBlockSlot = computeTimeAtSlot(config, slot, chain.genesisTime) * 1000 - Date.now();
            if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) {
                await sleep(msToBlockSlot);
            }
            const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime);
            metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({ source: OpSource.api }, delaySec);
            chain.logger.info("Publishing execution payload envelope", valLogMeta);
            // Publish envelope and data columns
            const publishPromises = [
                // Gossip the signed execution payload envelope first
                () => network.publishSignedExecutionPayloadEnvelope(signedExecutionPayloadEnvelope),
                // For self-builds, publish all data column sidecars
                ...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)),
            ];
            const sentPeersArr = await promiseAllMaybeAsync(publishPromises);
            // Track metrics for data column publishing
            if (dataColumnSidecars.length > 0) {
                let columnsPublishedWithZeroPeers = 0;
                // Skip first entry (envelope), track data columns
                for (let i = 1; i < sentPeersArr.length; i++) {
                    const sentPeers = sentPeersArr[i];
                    metrics?.dataColumns.sentPeersPerSubnet.observe(sentPeers);
                    if (sentPeers === 0) {
                        columnsPublishedWithZeroPeers++;
                    }
                }
                if (columnsPublishedWithZeroPeers > 0) {
                    chain.logger.warn("Published data columns to 0 peers, increased risk of reorg", {
                        slot,
                        blockRoot: blockRootHex,
                        columns: columnsPublishedWithZeroPeers,
                    });
                }
                metrics?.dataColumns.bySource.inc({ source: BlockInputSource.api }, dataColumnSidecars.length);
                if (chain.emitter.listenerCount(routes.events.EventType.dataColumnSidecar)) {
                    // TODO GLOAS: revisit this, we likely don't wanna emit KZG commitments anymore
                    const cachedResult = chain.blockProductionCache.get(blockRootHex);
                    const kzgCommitments = cachedResult?.blobsBundle.commitments.map(toHex) ?? [];
                    for (const dataColumnSidecar of dataColumnSidecars) {
                        chain.emitter.emit(routes.events.EventType.dataColumnSidecar, {
                            blockRoot: blockRootHex,
                            slot,
                            index: dataColumnSidecar.index,
                            kzgCommitments,
                        });
                    }
                }
            }
            chain.logger.info("Published execution payload envelope", {
                ...valLogMeta,
                delaySec,
                sentPeers: sentPeersArr[0] ?? 0,
            });
        },
        async getBlobSidecars({ blockId, indices }) {
            assertUniqueItems(indices, "Duplicate indices provided");
            const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId);
            const fork = config.getForkName(block.message.slot);
            const blockRoot = sszTypesFor(fork).BeaconBlock.hashTreeRoot(block.message);
            const blockRootHex = toRootHex(blockRoot);
            let data;
            if (isForkPostFulu(fork)) {
                const { targetCustodyGroupCount } = chain.custodyConfig;
                if (targetCustodyGroupCount < NUMBER_OF_COLUMNS / 2) {
                    throw new ApiError(503, `Custody group count of ${targetCustodyGroupCount} is not sufficient to serve blob sidecars, must custody at least ${NUMBER_OF_COLUMNS / 2} data columns`);
                }
                const blobKzgCommitments = block.message.body.blobKzgCommitments;
                const blobCount = blobKzgCommitments.length;
                if (blobCount > 0) {
                    const dataColumnSidecars = await chain.getDataColumnSidecars(block.message.slot, blockRootHex);
                    if (dataColumnSidecars.length === 0) {
                        throw new ApiError(404, `dataColumnSidecars not found in db for slot=${block.message.slot} root=${toRootHex(blockRoot)} blobs=${blobCount}`);
                    }
                    for (const index of indices ?? []) {
                        if (index < 0 || index >= blobCount) {
                            throw new ApiError(400, `Invalid blob index ${index}, must be between 0 and ${blobCount - 1}`);
                        }
                    }
                    const indicesToReconstruct = indices ?? Array.from({ length: blobCount }, (_, i) => i);
                    const timer = metrics?.recoverBlobSidecars.reconstructionTime.startTimer();
                    const blobs = await reconstructBlobs(dataColumnSidecars, indicesToReconstruct);
                    timer?.();
                    metrics?.recoverBlobSidecars.blobsReconstructed.inc(indicesToReconstruct.length);
                    const signedBlockHeader = signedBlockToSignedHeader(config, block);
                    data = await Promise.all(indicesToReconstruct.map(async (index, i) => {
                        // record per column computation time
                        const compTimer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer();
                        // Reconstruct blob sidecar from blob
                        const kzgCommitment = blobKzgCommitments[index];
                        const blob = blobs[i]; // Use i since blobs only contains requested indices
                        const kzgProof = await kzg.asyncComputeBlobKzgProof(blob, kzgCommitment);
                        const kzgCommitmentInclusionProof = computePreFuluKzgCommitmentsInclusionProof(fork, block.message.body, index);
                        compTimer?.();
                        return { index, blob, kzgCommitment, kzgProof, signedBlockHeader, kzgCommitmentInclusionProof };
                    }));
                }
                else {
                    data = [];
                }
            }
            else if (isForkPostDeneb(fork)) {
                const blobSidecars = await chain.getBlobSidecars(block.message.slot, blockRootHex);
                if (!blobSidecars) {
                    throw new ApiError(404, `blobSidecars not found in db for slot=${block.message.slot} root=${toRootHex(blockRoot)}`);
                }
                data = indices ? blobSidecars.filter(({ index }) => indices.includes(index)) : blobSidecars;
            }
            else {
                data = [];
            }
            return {
                data,
                meta: {
                    executionOptimistic,
                    finalized,
                    version: config.getForkName(block.message.slot),
                },
            };
        },
        async getBlobs({ blockId, versionedHashes }) {
            assertUniqueItems(versionedHashes, "Duplicate versioned hashes provided");
            const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId);
            const fork = config.getForkName(block.message.slot);
            const blockRoot = sszTypesFor(fork).BeaconBlock.hashTreeRoot(block.message);
            const blockRootHex = toRootHex(blockRoot);
            let blobs;
            if (isForkPostFulu(fork)) {
                const { targetCustodyGroupCount } = chain.custodyConfig;
                if (targetCustodyGroupCount < NUMBER_OF_COLUMNS / 2) {
                    throw new ApiError(503, `Custody group count of ${targetCustodyGroupCount} is not sufficient to serve blobs, must custody at least ${NUMBER_OF_COLUMNS / 2} data columns`);
                }
                const blobKzgCommitments = block.message.body.blobKzgCommitments;
                const blobCount = blobKzgCommitments.length;
                if (blobCount > 0) {
                    const dataColumnSidecars = await chain.getDataColumnSidecars(block.message.slot, blockRootHex);
                    if (dataColumnSidecars.length === 0) {
                        throw new ApiError(404, `dataColumnSidecars not found in db for slot=${block.message.slot} root=${toRootHex(blockRoot)} blobs=${blobCount}`);
                    }
                    let indicesToReconstruct;
                    if (versionedHashes) {
                        const blockVersionedHashes = blobKzgCommitments.map((commitment) => toHex(kzgCommitmentToVersionedHash(commitment)));
                        indicesToReconstruct = [];
                        for (const requestedHash of versionedHashes) {
                            const index = blockVersionedHashes.findIndex((hash) => hash === requestedHash);
                            if (index === -1) {
                                throw new ApiError(400, `Versioned hash ${requestedHash} not found in block`);
                            }
                            indicesToReconstruct.push(index);
                        }
                        indicesToReconstruct.sort((a, b) => a - b);
                    }
                    else {
                        indicesToReconstruct = Array.from({ length: blobCount }, (_, i) => i);
                    }
                    const timer = metrics?.peerDas.dataColumnsReconstructionTime.startTimer();
                    blobs = await reconstructBlobs(dataColumnSidecars, indicesToReconstruct);
                    timer?.();
                    metrics?.peerDas.reconstructedColumns.inc(indicesToReconstruct.length);
                }
                else {
                    blobs = [];
                }
            }
            else if (isForkPostDeneb(fork)) {
                const blobSidecars = await chain.getBlobSidecars(block.message.slot, blockRootHex);
                if (!blobSidecars) {
                    throw new ApiError(404, `blobSidecars not found in db for slot=${block.message.slot} root=${toRootHex(blockRoot)}`);
                }
                blobs = blobSidecars.sort((a, b) => a.index - b.index).map(({ blob }) => blob);
                if (blobs.length && versionedHashes) {
                    const kzgCommitments = block.message.body.blobKzgCommitments;
                    const blockVersionedHashes = kzgCommitments.map((commitment) => toHex(kzgCommitmentToVersionedHash(commitment)));
                    const requestedIndices = [];
                    for (const requestedHash of versionedHashes) {
                        const index = blockVersionedHashes.findIndex((hash) => hash === requestedHash);
                        if (index === -1) {
                            throw new ApiError(400, `Versioned hash ${requestedHash} not found in block`);
                        }
                        requestedIndices.push(index);
                    }
                    blobs = requestedIndices.sort((a, b) => a - b).map((index) => blobs[index]);
                }
            }
            else {
                blobs = [];
            }
            return {
                data: blobs,
                meta: {
                    executionOptimistic,
                    finalized,
                },
            };
        },
    };
}
async function reconstructBuilderSignedBlockContents(chain, signedBlindedBlock) {
    const executionBuilder = chain.executionBuilder;
    if (!executionBuilder) {
        throw Error("executionBuilder required to publish SignedBlindedBeaconBlock");
    }
    return executionBuilder.submitBlindedBlock(signedBlindedBlock);
}
async function submitBlindedBlockToBuilder(chain, signedBlindedBlock) {
    const executionBuilder = chain.executionBuilder;
    if (!executionBuilder) {
        throw Error("executionBuilder required to submit SignedBlindedBeaconBlock to builder");
    }
    await executionBuilder.submitBlindedBlockNoResponse(signedBlindedBlock);
}
//# sourceMappingURL=index.js.map