import { KZG_COMMITMENT_INCLUSION_PROOF_DEPTH, KZG_COMMITMENT_SUBTREE_INDEX0, isForkPostElectra, } from "@lodestar/params";
import { computeEpochAtSlot, computeStartSlotAtEpoch, getBlockHeaderProposerSignatureSetByHeaderSlot, getBlockHeaderProposerSignatureSetByParentStateSlot, } from "@lodestar/state-transition";
import { ssz } from "@lodestar/types";
import { byteArrayEquals, toRootHex, verifyMerkleBranch } from "@lodestar/utils";
import { kzg } from "../../util/kzg.js";
import { BlobSidecarErrorCode, BlobSidecarGossipError, BlobSidecarValidationError } from "../errors/blobSidecarError.js";
import { GossipAction } from "../errors/gossipValidation.js";
import { RegenCaller } from "../regen/index.js";
export async function validateGossipBlobSidecar(fork, chain, blobSidecar, subnet) {
    const blobSlot = blobSidecar.signedBlockHeader.message.slot;
    // [REJECT] The sidecar's index is consistent with `MAX_BLOBS_PER_BLOCK` -- i.e. `blob_sidecar.index < MAX_BLOBS_PER_BLOCK`.
    const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(blobSlot));
    if (blobSidecar.index >= maxBlobsPerBlock) {
        throw new BlobSidecarGossipError(GossipAction.REJECT, {
            code: BlobSidecarErrorCode.INDEX_TOO_LARGE,
            blobIdx: blobSidecar.index,
            maxBlobsPerBlock,
        });
    }
    // [REJECT] The sidecar is for the correct subnet -- i.e. `compute_subnet_for_blob_sidecar(sidecar.index) == subnet_id`.
    if (computeSubnetForBlobSidecar(fork, chain.config, blobSidecar.index) !== subnet) {
        throw new BlobSidecarGossipError(GossipAction.REJECT, {
            code: BlobSidecarErrorCode.INVALID_INDEX,
            blobIdx: blobSidecar.index,
            subnet,
        });
    }
    // [IGNORE] The sidecar is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) --
    // i.e. validate that sidecar.slot <= current_slot (a client MAY queue future blocks for processing at
    // the appropriate slot).
    const currentSlotWithGossipDisparity = chain.clock.currentSlotWithGossipDisparity;
    if (currentSlotWithGossipDisparity < blobSlot) {
        throw new BlobSidecarGossipError(GossipAction.IGNORE, {
            code: BlobSidecarErrorCode.FUTURE_SLOT,
            currentSlot: currentSlotWithGossipDisparity,
            blockSlot: blobSlot,
        });
    }
    // [IGNORE] The sidecar is from a slot greater than the latest finalized slot -- i.e. validate that
    // sidecar.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch)
    const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint();
    const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch);
    if (blobSlot <= finalizedSlot) {
        throw new BlobSidecarGossipError(GossipAction.IGNORE, {
            code: BlobSidecarErrorCode.WOULD_REVERT_FINALIZED_SLOT,
            blockSlot: blobSlot,
            finalizedSlot,
        });
    }
    // Check if the block is already known. We know it is post-finalization, so it is sufficient to check the fork choice.
    //
    // In normal operation this isn't necessary, however it is useful immediately after a
    // reboot if the `observed_block_producers` cache is empty. In that case, without this
    // check, we will load the parent and state from disk only to find out later that we
    // already know this block.
    const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blobSidecar.signedBlockHeader.message);
    const blockHex = toRootHex(blockRoot);
    if (chain.forkChoice.getBlockHexDefaultStatus(blockHex) !== null) {
        throw new BlobSidecarGossipError(GossipAction.IGNORE, { code: BlobSidecarErrorCode.ALREADY_KNOWN, root: blockHex });
    }
    // TODO: freetheblobs - check for badblock
    // TODO: freetheblobs - check that its first blob with valid signature
    // _[IGNORE]_ The blob's block's parent (defined by `sidecar.block_parent_root`) has been seen (via both
    // gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is
    // retrieved).
    const parentRoot = toRootHex(blobSidecar.signedBlockHeader.message.parentRoot);
    const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentRoot);
    if (parentBlock === null) {
        // If fork choice does *not* consider the parent to be a descendant of the finalized block,
        // then there are two more cases:
        //
        // 1. We have the parent stored in our database. Because fork-choice has confirmed the
        //    parent is *not* in our post-finalization DAG, all other blocks must be either
        //    pre-finalization or conflicting with finalization.
        // 2. The parent is unknown to us, we probably want to download it since it might actually
        //    descend from the finalized root.
        // (Non-Lighthouse): Since we prune all blocks non-descendant from finalized checking the `db.block` database won't be useful to guard
        // against known bad fork blocks, so we throw PARENT_UNKNOWN for cases (1) and (2)
        throw new BlobSidecarGossipError(GossipAction.IGNORE, {
            code: BlobSidecarErrorCode.PARENT_UNKNOWN,
            parentRoot,
            blockRoot: blockHex,
            slot: blobSlot,
        });
    }
    // [REJECT] The blob is from a higher slot than its parent.
    if (parentBlock.slot >= blobSlot) {
        throw new BlobSidecarGossipError(GossipAction.IGNORE, {
            code: BlobSidecarErrorCode.NOT_LATER_THAN_PARENT,
            parentSlot: parentBlock.slot,
            slot: blobSlot,
        });
    }
    // getBlockSlotState also checks for whether the current finalized checkpoint is an ancestor of the block.
    // As a result, we throw an IGNORE (whereas the spec says we should REJECT for this scenario).
    // this is something we should change this in the future to make the code airtight to the spec.
    // [IGNORE] The block's parent (defined by block.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is retrieved).
    // [REJECT] The block's parent (defined by block.parent_root) passes validation.
    const blockState = await chain.regen
        .getBlockSlotState(parentBlock, blobSlot, { dontTransferCache: true }, RegenCaller.validateGossipBlock)
        .catch(() => {
        throw new BlobSidecarGossipError(GossipAction.IGNORE, {
            code: BlobSidecarErrorCode.PARENT_UNKNOWN,
            parentRoot,
            blockRoot: blockHex,
            slot: blobSlot,
        });
    });
    // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey.
    const signature = blobSidecar.signedBlockHeader.signature;
    if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blobSlot, blockHex, signature)) {
        const signatureSet = getBlockHeaderProposerSignatureSetByParentStateSlot(chain.config, blockState.slot, blobSidecar.signedBlockHeader);
        // Don't batch so verification is not delayed
        if (!(await chain.bls.verifySignatureSets([signatureSet], { verifyOnMainThread: true }))) {
            throw new BlobSidecarGossipError(GossipAction.REJECT, {
                code: BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID,
                blockRoot: blockHex,
                index: blobSidecar.index,
                slot: blobSlot,
            });
        }
        chain.seenBlockInputCache.markVerifiedProposerSignature(blobSlot, blockHex, signature);
    }
    // verify if the blob inclusion proof is correct
    if (!validateBlobSidecarInclusionProof(blobSidecar)) {
        throw new BlobSidecarGossipError(GossipAction.REJECT, {
            code: BlobSidecarErrorCode.INCLUSION_PROOF_INVALID,
            slot: blobSidecar.signedBlockHeader.message.slot,
            blobIdx: blobSidecar.index,
        });
    }
    //  _[IGNORE]_ The sidecar is the only sidecar with valid signature received for the tuple
    // `(sidecar.block_root, sidecar.index)`
    //
    // This is already taken care of by the way we group the blobs in getFullBlockInput helper
    // but may be an error can be thrown there for this
    // _[REJECT]_ The sidecar is proposed by the expected `proposer_index` for the block's slot in the
    // context of the current shuffling (defined by `block_parent_root`/`slot`)
    // If the `proposer_index` cannot immediately be verified against the expected shuffling, the sidecar
    // MAY be queued for later processing while proposers for the block's branch are calculated -- in such
    // a case _do not_ `REJECT`, instead `IGNORE` this message.
    const proposerIndex = blobSidecar.signedBlockHeader.message.proposerIndex;
    if (blockState.epochCtx.getBeaconProposer(blobSlot) !== proposerIndex) {
        throw new BlobSidecarGossipError(GossipAction.REJECT, {
            code: BlobSidecarErrorCode.INCORRECT_PROPOSER,
            proposerIndex,
        });
    }
    // blob, proof and commitment as a valid BLS G1 point gets verified in batch validation
    try {
        await validateBlobsAndBlobProofs([blobSidecar.kzgCommitment], [blobSidecar.blob], [blobSidecar.kzgProof]);
    }
    catch (_e) {
        throw new BlobSidecarGossipError(GossipAction.REJECT, {
            code: BlobSidecarErrorCode.INVALID_KZG_PROOF,
            blobIdx: blobSidecar.index,
        });
    }
}
/**
 * Validate some blob sidecars in a block
 *
 * Requires the block to be known to the node
 *
 * NOTE: chain is optional to skip signature verification. Helpful for testing purposes and so that can control whether
 * signature gets checked depending on the reqresp method that is being checked
 */
export async function validateBlockBlobSidecars(chain, blockSlot, blockRoot, blockBlobCount, blobSidecars) {
    if (blobSidecars.length === 0) {
        return;
    }
    if (blockBlobCount === 0) {
        throw new BlobSidecarValidationError({
            code: BlobSidecarErrorCode.INCORRECT_SIDECAR_COUNT,
            slot: blockSlot,
            expected: blockBlobCount,
            actual: blobSidecars.length,
        });
    }
    // Hash the first sidecar block header and compare the rest via (cheaper) equality
    const firstSidecarSignedBlockHeader = blobSidecars[0].signedBlockHeader;
    const firstSidecarBlockHeader = firstSidecarSignedBlockHeader.message;
    const firstBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(firstSidecarBlockHeader);
    if (!byteArrayEquals(blockRoot, firstBlockRoot)) {
        throw new BlobSidecarValidationError({
            code: BlobSidecarErrorCode.INCORRECT_BLOCK,
            slot: blockSlot,
            blobIdx: 0,
            expected: toRootHex(blockRoot),
            actual: toRootHex(firstBlockRoot),
        }, "BlobSidecar doesn't match corresponding block");
    }
    if (chain !== null) {
        const blockRootHex = toRootHex(blockRoot);
        const signature = firstSidecarSignedBlockHeader.signature;
        if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blockSlot, blockRootHex, signature)) {
            const signatureSet = getBlockHeaderProposerSignatureSetByHeaderSlot(chain.config, firstSidecarSignedBlockHeader);
            if (!(await chain.bls.verifySignatureSets([signatureSet], {
                verifyOnMainThread: true,
            }))) {
                throw new BlobSidecarValidationError({
                    code: BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID,
                    blockRoot: blockRootHex,
                    slot: blockSlot,
                    index: blobSidecars[0].index,
                });
            }
            chain.seenBlockInputCache.markVerifiedProposerSignature(blockSlot, blockRootHex, signature);
        }
    }
    const commitments = [];
    const blobs = [];
    const proofs = [];
    for (let i = 0; i < blobSidecars.length; i++) {
        const blobSidecar = blobSidecars[i];
        const blobIndex = blobSidecar.index;
        if (i !== 0 &&
            !ssz.phase0.SignedBeaconBlockHeader.equals(blobSidecar.signedBlockHeader, firstSidecarSignedBlockHeader)) {
            throw new BlobSidecarValidationError({
                code: BlobSidecarErrorCode.INCORRECT_BLOCK,
                slot: blockSlot,
                blobIdx: blobIndex,
                expected: toRootHex(blockRoot),
                actual: "unknown - compared via equality",
            }, "BlobSidecar doesn't match corresponding block");
        }
        if (!validateBlobSidecarInclusionProof(blobSidecar)) {
            throw new BlobSidecarValidationError({
                code: BlobSidecarErrorCode.INCLUSION_PROOF_INVALID,
                slot: blockSlot,
                blobIdx: blobIndex,
            }, "BlobSidecar inclusion proof invalid");
        }
        commitments.push(blobSidecar.kzgCommitment);
        blobs.push(blobSidecar.blob);
        proofs.push(blobSidecar.kzgProof);
    }
    // Final batch KZG proof verification
    let reason = undefined;
    try {
        if (!(await kzg.asyncVerifyBlobKzgProofBatch(blobs, commitments, proofs))) {
            reason = "Invalid verifyBlobKzgProofBatch";
        }
    }
    catch (e) {
        reason = e.message;
    }
    if (reason !== undefined) {
        throw new BlobSidecarValidationError({
            code: BlobSidecarErrorCode.INVALID_KZG_PROOF_BATCH,
            slot: blockSlot,
            reason,
        }, "BlobSidecar has invalid KZG proof batch");
    }
}
export async function validateBlobsAndBlobProofs(expectedKzgCommitments, blobs, proofs) {
    // assert verify_aggregate_kzg_proof(blobs, expected_kzg_commitments, kzg_aggregated_proof)
    let isProofValid;
    try {
        isProofValid = await kzg.asyncVerifyBlobKzgProofBatch(blobs, expectedKzgCommitments, proofs);
    }
    catch (e) {
        e.message = `Error on verifyBlobKzgProofBatch: ${e.message}`;
        throw e;
    }
    if (!isProofValid) {
        throw Error("Invalid verifyBlobKzgProofBatch");
    }
}
export function validateBlobSidecarInclusionProof(blobSidecar) {
    return verifyMerkleBranch(ssz.deneb.KZGCommitment.hashTreeRoot(blobSidecar.kzgCommitment), blobSidecar.kzgCommitmentInclusionProof, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH, KZG_COMMITMENT_SUBTREE_INDEX0 + blobSidecar.index, blobSidecar.signedBlockHeader.message.bodyRoot);
}
function computeSubnetForBlobSidecar(fork, config, blobIndex) {
    return (blobIndex % (isForkPostElectra(fork) ? config.BLOB_SIDECAR_SUBNET_COUNT_ELECTRA : config.BLOB_SIDECAR_SUBNET_COUNT));
}
//# sourceMappingURL=blobSidecar.js.map