import { ATTESTATION_SUBNET_COUNT, DOMAIN_BEACON_ATTESTER, ForkName, ForkSeq, SLOTS_PER_EPOCH, isForkPostElectra, isForkPostGloas, } from "@lodestar/params";
import { ShufflingError, ShufflingErrorCode, computeEpochAtSlot, computeSigningRoot, computeStartSlotAtEpoch, createIndexedSignatureSetFromComponents, } from "@lodestar/state-transition";
import { isElectraSingleAttestation, ssz, } from "@lodestar/types";
import { assert, toRootHex } from "@lodestar/utils";
import { sszDeserializeSingleAttestation } from "../../network/gossip/topic.js";
import { getShufflingDependentRoot } from "../../util/dependentRoot.js";
import { getAggregationBitsFromAttestationSerialized, getAttDataFromSignedAggregateAndProofElectra, getAttDataFromSignedAggregateAndProofPhase0, getAttesterIndexFromSingleAttestationSerialized, getCommitteeIndexFromSingleAttestationSerialized, getSignatureFromAttestationSerialized, getSignatureFromSingleAttestationSerialized, } from "../../util/sszBytes.js";
import { wrapError } from "../../util/wrapError.js";
import { AttestationError, AttestationErrorCode, GossipAction } from "../errors/index.js";
import { RegenCaller } from "../regen/index.js";
import { PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, } from "../seenCache/seenAttestationData.js";
/**
 * Verify gossip attestations of the same attestation data. The main advantage is we can batch verify bls signatures
 * through verifySignatureSetsSameMessage bls api to improve performance.
 *   - If there are less than 2 signatures (minSameMessageSignatureSetsToBatch), verify each signature individually with batchable = true
 *   - do not prioritize bls signature set
 */
export async function validateGossipAttestationsSameAttData(fork, chain, attestationOrBytesArr, 
// for unit test, consumers do not need to pass this
step0ValidationFn = validateAttestationNoSignatureCheck) {
    if (attestationOrBytesArr.length === 0) {
        return { results: [], batchableBls: false };
    }
    // step0: do all verifications except for signature verification
    // this for await pattern below seems to be bad but it's not
    // for seen AttestationData, it's the same to await Promise.all() pattern
    // for unseen AttestationData, the 1st call will be cached and the rest will be fast
    const step0ResultOrErrors = [];
    for (const attestationOrBytes of attestationOrBytesArr) {
        const { subnet } = attestationOrBytes;
        const resultOrError = await wrapError(step0ValidationFn(fork, chain, attestationOrBytes, subnet));
        step0ResultOrErrors.push(resultOrError);
    }
    // step1: verify signatures of all valid attestations
    // map new index to index in resultOrErrors
    const newIndexToOldIndex = new Map();
    const signatureSets = [];
    let newIndex = 0;
    const step0Results = [];
    for (const [i, resultOrError] of step0ResultOrErrors.entries()) {
        if (resultOrError.err) {
            continue;
        }
        step0Results.push(resultOrError.result);
        newIndexToOldIndex.set(newIndex, i);
        signatureSets.push(resultOrError.result.signatureSet);
        newIndex++;
    }
    let signatureValids;
    const batchableBls = signatureSets.length >= chain.opts.minSameMessageSignatureSetsToBatch;
    if (batchableBls) {
        // all signature sets should have same signing root since we filtered in network processor
        signatureValids = await chain.bls.verifySignatureSetsSameMessage(signatureSets.map((set) => {
            const publicKey = chain.pubkeyCache.getOrThrow(set.index);
            return { publicKey, signature: set.signature };
        }), signatureSets[0].signingRoot);
    }
    else {
        // don't want to block the main thread if there are too few signatures
        signatureValids = await Promise.all(signatureSets.map((set) => chain.bls.verifySignatureSets([set], { batchable: true })));
    }
    // phase0 post validation
    for (const [i, sigValid] of signatureValids.entries()) {
        const oldIndex = newIndexToOldIndex.get(i);
        if (oldIndex == null) {
            // should not happen
            throw Error(`Cannot get old index for index ${i}`);
        }
        const { validatorIndex, attestation } = step0Results[i];
        const targetEpoch = attestation.data.target.epoch;
        if (sigValid) {
            // Now that the attestation has been fully verified, store that we have received a valid attestation from this validator.
            //
            // It's important to double check that the attestation still hasn't been observed, since
            // there can be a race-condition if we receive two attestations at the same time and
            // process them in different threads.
            if (chain.seenAttesters.isKnown(targetEpoch, validatorIndex)) {
                step0ResultOrErrors[oldIndex] = {
                    err: new AttestationError(GossipAction.IGNORE, {
                        code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN,
                        targetEpoch,
                        validatorIndex,
                    }),
                };
            }
            // valid
            chain.seenAttesters.add(targetEpoch, validatorIndex);
        }
        else {
            step0ResultOrErrors[oldIndex] = {
                err: new AttestationError(GossipAction.REJECT, {
                    code: AttestationErrorCode.INVALID_SIGNATURE,
                }),
            };
        }
    }
    return {
        results: step0ResultOrErrors,
        batchableBls,
    };
}
/**
 * Validate attestations from api
 * - no need to deserialize attestation
 * - no subnet
 * - prioritize bls signature set
 */
export async function validateApiAttestation(fork, chain, attestationOrBytes) {
    const prioritizeBls = true;
    const subnet = null;
    try {
        const step0Result = await validateAttestationNoSignatureCheck(fork, chain, attestationOrBytes, subnet);
        const { attestation, signatureSet, validatorIndex } = step0Result;
        const isValid = await chain.bls.verifySignatureSets([signatureSet], { batchable: true, priority: prioritizeBls });
        if (isValid) {
            const targetEpoch = attestation.data.target.epoch;
            chain.seenAttesters.add(targetEpoch, validatorIndex);
            return step0Result;
        }
        throw new AttestationError(GossipAction.IGNORE, {
            code: AttestationErrorCode.INVALID_SIGNATURE,
        });
    }
    catch (err) {
        if (err instanceof ShufflingError && err.type.code === ShufflingErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE) {
            throw new AttestationError(GossipAction.IGNORE, {
                code: AttestationErrorCode.BAD_TARGET_EPOCH,
            });
        }
        throw err;
    }
}
/**
 * Only deserialize the single attestation if needed, use the cached AttestationData instead
 * This is to avoid deserializing similar attestation multiple times which could help the gc
 */
async function validateAttestationNoSignatureCheck(fork, chain, attestationOrBytes, 
/** Optional, to allow verifying attestations through API with unknown subnet */
subnet) {
    // Do checks in this order:
    // - do early checks (w/o indexed attestation)
    // - > obtain indexed attestation and committes per slot
    // - do middle checks w/ indexed attestation
    // - > verify signature
    // - do late checks w/ a valid signature
    // verify_early_checks
    // Run the checks that happen before an indexed attestation is constructed.
    let attestationOrCache;
    let attDataKey = null;
    if (attestationOrBytes.serializedData) {
        // gossip
        const attSlot = attestationOrBytes.attSlot;
        attDataKey = getSeenAttDataKeyFromGossipAttestation(attestationOrBytes);
        const committeeIndexForLookup = isForkPostElectra(fork)
            ? (getCommitteeIndexFromAttestationOrBytes(fork, attestationOrBytes) ?? 0)
            : PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX;
        const cachedAttData = attDataKey !== null ? chain.seenAttestationDatas.get(attSlot, committeeIndexForLookup, attDataKey) : null;
        if (cachedAttData === null) {
            const attestation = sszDeserializeSingleAttestation(fork, attestationOrBytes.serializedData);
            // only deserialize on the first AttestationData that's not cached
            attestationOrCache = { attestation, cache: null };
        }
        else {
            attestationOrCache = { attestation: null, cache: cachedAttData, serializedData: attestationOrBytes.serializedData };
        }
    }
    else {
        // api
        attDataKey = null;
        attestationOrCache = { attestation: attestationOrBytes.attestation, cache: null };
    }
    const attData = attestationOrCache.attestation
        ? attestationOrCache.attestation.data
        : attestationOrCache.cache.attestationData;
    const attSlot = attData.slot;
    const attEpoch = computeEpochAtSlot(attSlot);
    const attTarget = attData.target;
    const targetEpoch = attTarget.epoch;
    let committeeIndex;
    if (attestationOrCache.attestation) {
        if (isElectraSingleAttestation(attestationOrCache.attestation)) {
            // api or first time validation of a gossip attestation
            committeeIndex = attestationOrCache.attestation.committeeIndex;
            if (isForkPostGloas(fork)) {
                // [REJECT] `attestation.data.index < 2`.
                if (attData.index >= 2) {
                    throw new AttestationError(GossipAction.REJECT, {
                        code: AttestationErrorCode.INVALID_PAYLOAD_STATUS_VALUE,
                        attDataIndex: attData.index,
                    });
                }
                // [REJECT] `attestation.data.index == 0` if `block.slot == attestation.data.slot`.
                const block = chain.forkChoice.getBlockDefaultStatus(attData.beaconBlockRoot);
                // block being null will be handled by `verifyHeadBlockAndTargetRoot`
                if (block !== null && block.slot === attSlot && attData.index !== 0) {
                    throw new AttestationError(GossipAction.REJECT, {
                        code: AttestationErrorCode.PREMATURELY_INDICATED_PAYLOAD_PRESENT,
                    });
                }
            }
            else {
                // [REJECT] attestation.data.index == 0
                if (attData.index !== 0) {
                    throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX });
                }
            }
        }
        else {
            // phase0 attestation
            committeeIndex = attData.index;
        }
    }
    else {
        // found a seen AttestationData
        committeeIndex = attestationOrCache.cache.committeeIndex;
    }
    chain.metrics?.gossipAttestation.attestationSlotToClockSlot.observe({ caller: RegenCaller.validateGossipAttestation }, chain.clock.currentSlot - attSlot);
    if (!attestationOrCache.cache) {
        // [REJECT] The attestation's epoch matches its target -- i.e. attestation.data.target.epoch == compute_epoch_at_slot(attestation.data.slot)
        if (targetEpoch !== attEpoch) {
            throw new AttestationError(GossipAction.REJECT, {
                code: AttestationErrorCode.BAD_TARGET_EPOCH,
            });
        }
        // Pre-deneb:
        // [IGNORE] attestation.data.slot is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (within a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
        //  -- i.e. attestation.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= attestation.data.slot
        // (a client MAY queue future attestations for processing at the appropriate slot).
        // Post-deneb:
        // [IGNORE] `attestation.data.slot` is equal to or earlier than the `current_slot` (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance)
        // -- i.e. `attestation.data.slot <= current_slot`
        //   (a client MAY queue future attestation for processing at the appropriate slot).
        // [IGNORE] the epoch of `attestation.data.slot` is either the current or previous epoch
        //   (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance)
        // -- i.e. `compute_epoch_at_slot(attestation.data.slot) in (get_previous_epoch(state), get_current_epoch(state))`
        verifyPropagationSlotRange(fork, chain, attestationOrCache.attestation.data.slot);
    }
    let aggregationBits = null;
    let validatorCommitteeIndex = null;
    if (!isForkPostElectra(fork)) {
        // [REJECT] The attestation is unaggregated -- that is, it has exactly one participating validator
        // (len([bit for bit in attestation.aggregation_bits if bit]) == 1, i.e. exactly 1 bit is set).
        // > TODO: Do this check **before** getting the target state but don't recompute zipIndexes
        aggregationBits = attestationOrCache.attestation
            ? attestationOrCache.attestation.aggregationBits
            : getAggregationBitsFromAttestationSerialized(attestationOrCache.serializedData);
        if (aggregationBits === null) {
            throw new AttestationError(GossipAction.REJECT, {
                code: AttestationErrorCode.INVALID_SERIALIZED_BYTES,
            });
        }
        const bitIndex = aggregationBits.getSingleTrueBit();
        if (bitIndex === null) {
            throw new AttestationError(GossipAction.REJECT, {
                code: AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET,
            });
        }
        validatorCommitteeIndex = bitIndex;
    }
    let committeeValidatorIndices;
    let getSigningRoot;
    let expectedSubnet;
    if (attestationOrCache.cache) {
        committeeValidatorIndices = attestationOrCache.cache.committeeValidatorIndices;
        const signingRoot = attestationOrCache.cache.signingRoot;
        getSigningRoot = () => signingRoot;
        expectedSubnet = attestationOrCache.cache.subnet;
    }
    else {
        // Attestations must be for a known block. If the block is unknown, we simply drop the
        // attestation and do not delay consideration for later.
        //
        // TODO (LH): Enforce a maximum skip distance for unaggregated attestations.
        // [IGNORE] The block being voted for (attestation.data.beacon_block_root) has been seen (via both gossip
        // and non-gossip sources) (a client MAY queue attestations for processing once block is retrieved).
        const attHeadBlock = verifyHeadBlockAndTargetRoot(chain, attestationOrCache.attestation.data.beaconBlockRoot, attestationOrCache.attestation.data.target.root, attSlot, attEpoch, RegenCaller.validateGossipAttestation, chain.opts.maxSkipSlots);
        // [REJECT] The block being voted for (attestation.data.beacon_block_root) passes validation.
        // > Altready check in `verifyHeadBlockAndTargetRoot()`
        // [IGNORE] The current finalized_checkpoint is an ancestor of the block defined by attestation.data.beacon_block_root
        // -- i.e. get_ancestor(store, attestation.data.beacon_block_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root
        // > Altready check in `verifyHeadBlockAndTargetRoot()`
        // [REJECT] The attestation's target block is an ancestor of the block named in the LMD vote
        //  --i.e. get_ancestor(store, attestation.data.beacon_block_root, compute_start_slot_at_epoch(attestation.data.target.epoch)) == attestation.data.target.root
        // > Altready check in `verifyHeadBlockAndTargetRoot()`
        const shuffling = await getShufflingForAttestationVerification(chain, attEpoch, attHeadBlock, RegenCaller.validateGossipAttestation);
        // [REJECT] The committee index is within the expected range
        // -- i.e. data.index < get_committee_count_per_slot(state, data.target.epoch)
        committeeValidatorIndices = getCommitteeValidatorIndices(shuffling, attSlot, committeeIndex);
        getSigningRoot = () => getAttestationDataSigningRoot(chain.config, attData);
        expectedSubnet = computeSubnetForSlot(shuffling, attSlot, committeeIndex);
    }
    let validatorIndex;
    if (!isForkPostElectra(fork)) {
        // The validity of aggregation bits are already checked above
        assert.notNull(aggregationBits);
        assert.notNull(validatorCommitteeIndex);
        validatorIndex = committeeValidatorIndices[validatorCommitteeIndex];
        // [REJECT] The number of aggregation bits matches the committee size
        // -- i.e. len(attestation.aggregation_bits) == len(get_beacon_committee(state, data.slot, data.index)).
        // > TODO: Is this necessary? Lighthouse does not do this check.
        if (aggregationBits.bitLen !== committeeValidatorIndices.length) {
            throw new AttestationError(GossipAction.REJECT, {
                code: AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS,
            });
        }
    }
    else {
        if (attestationOrCache.attestation) {
            validatorIndex = attestationOrCache.attestation.attesterIndex;
        }
        else {
            const attesterIndex = getAttesterIndexFromSingleAttestationSerialized(attestationOrCache.serializedData);
            if (attesterIndex === null) {
                throw new AttestationError(GossipAction.REJECT, {
                    code: AttestationErrorCode.INVALID_SERIALIZED_BYTES,
                });
            }
            validatorIndex = attesterIndex;
        }
        // [REJECT] The attester is a member of the committee -- i.e.
        // `attestation.attester_index in get_beacon_committee(state, attestation.data.slot, index)`.
        // Position of the validator in its committee
        validatorCommitteeIndex = committeeValidatorIndices.indexOf(validatorIndex);
        if (validatorCommitteeIndex === -1) {
            throw new AttestationError(GossipAction.REJECT, {
                code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE,
            });
        }
    }
    // LH > verify_middle_checks
    // Run the checks that apply to the indexed attestation before the signature is checked.
    //   Check correct subnet
    //   The attestation is the first valid attestation received for the participating validator for the slot, attestation.data.slot.
    // [REJECT] The attestation is for the correct subnet
    // -- i.e. compute_subnet_for_attestation(committees_per_slot, attestation.data.slot, attestation.data.index) == subnet_id,
    // where committees_per_slot = get_committee_count_per_slot(state, attestation.data.target.epoch),
    // which may be pre-computed along with the committee information for the signature check.
    if (subnet !== null && subnet !== expectedSubnet) {
        throw new AttestationError(GossipAction.REJECT, {
            code: AttestationErrorCode.INVALID_SUBNET_ID,
            received: subnet,
            expected: expectedSubnet,
        });
    }
    // [IGNORE] There has been no other valid attestation seen on an attestation subnet that has an
    // identical attestation.data.target.epoch and participating validator index.
    if (chain.seenAttesters.isKnown(targetEpoch, validatorIndex)) {
        throw new AttestationError(GossipAction.IGNORE, {
            code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN,
            targetEpoch,
            validatorIndex,
        });
    }
    // [REJECT] The signature of attestation is valid.
    const attestingIndices = [validatorIndex];
    let signatureSet;
    let attDataRootHex;
    const signature = attestationOrCache.attestation
        ? attestationOrCache.attestation.signature
        : !isForkPostElectra(fork)
            ? getSignatureFromAttestationSerialized(attestationOrCache.serializedData)
            : getSignatureFromSingleAttestationSerialized(attestationOrCache.serializedData);
    if (signature === null) {
        throw new AttestationError(GossipAction.REJECT, {
            code: AttestationErrorCode.INVALID_SERIALIZED_BYTES,
        });
    }
    if (attestationOrCache.cache) {
        // there could be up to 6% of cpu time to compute signing root if we don't clone the signature set
        signatureSet = createIndexedSignatureSetFromComponents(validatorIndex, attestationOrCache.cache.signingRoot, signature);
        attDataRootHex = attestationOrCache.cache.attDataRootHex;
    }
    else {
        signatureSet = createIndexedSignatureSetFromComponents(validatorIndex, getSigningRoot(), signature);
        // add cached attestation data before verifying signature
        attDataRootHex = toRootHex(ssz.phase0.AttestationData.hashTreeRoot(attData));
        if (attDataKey) {
            // for pre-electra, committee index key is 0. See SeenAttestationDatas.add() documentation
            const committeeIndexKey = isForkPostElectra(fork)
                ? committeeIndex
                : PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX;
            chain.seenAttestationDatas.add(attSlot, committeeIndexKey, attDataKey, {
                committeeValidatorIndices,
                committeeIndex,
                signingRoot: signatureSet.signingRoot,
                subnet: expectedSubnet,
                // precompute this to be used in forkchoice
                // root of AttestationData was already cached during getIndexedAttestationSignatureSet
                attDataRootHex,
                attestationData: attData,
            });
        }
    }
    // no signature check, leave that for step1
    const indexedAttestation = {
        attestingIndices,
        data: attData,
        signature,
    };
    const attestation = attestationOrCache.attestation
        ? attestationOrCache.attestation
        : !isForkPostElectra(fork)
            ? {
                // Aggregation bits are already asserted above to not be null
                aggregationBits: aggregationBits,
                data: attData,
                signature,
            }
            : {
                committeeIndex,
                attesterIndex: validatorIndex,
                data: attData,
                signature,
            };
    return {
        attestation,
        indexedAttestation,
        subnet: expectedSubnet,
        attDataRootHex,
        signatureSet,
        validatorIndex,
        committeeIndex,
        validatorCommitteeIndex,
        committeeSize: committeeValidatorIndices.length,
    };
}
/**
 * Verify that the `attestation` is within the acceptable gossip propagation range, with reference
 * to the current slot of the `chain`.
 *
 * Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
 * Note: We do not queue future attestations for later processing
 */
export function verifyPropagationSlotRange(fork, chain, attestationSlot) {
    // slot with future tolerance of MAXIMUM_GOSSIP_CLOCK_DISPARITY
    const latestPermissibleSlot = chain.clock.slotWithFutureTolerance(chain.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000);
    if (attestationSlot > latestPermissibleSlot) {
        throw new AttestationError(GossipAction.IGNORE, {
            code: AttestationErrorCode.FUTURE_SLOT,
            latestPermissibleSlot,
            attestationSlot,
        });
    }
    // Post deneb the attestations are valid for current as well as previous epoch
    // while pre deneb they are valid for ATTESTATION_PROPAGATION_SLOT_RANGE
    //
    // see: https://github.com/ethereum/consensus-specs/pull/3360
    if (ForkSeq[fork] < ForkSeq.deneb) {
        const currentSlot = chain.clock.currentSlot;
        const withinPastDisparity = currentSlot > 0 && chain.clock.isCurrentSlotGivenGossipDisparity(currentSlot - 1);
        const earliestPermissibleSlot = Math.max(
        // Pre-Deneb propagation is time-bounded: an attestation remains valid at the exact old
        // boundary `compute_time_at_slot(slot + range + 1) + MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
        // Model that boundary by extending the lower slot bound by one additional slot only while
        // the clock still considers the previous slot current given gossip disparity.
        currentSlot - chain.config.ATTESTATION_PROPAGATION_SLOT_RANGE - (withinPastDisparity ? 1 : 0), 0);
        if (attestationSlot < earliestPermissibleSlot) {
            throw new AttestationError(GossipAction.IGNORE, {
                code: AttestationErrorCode.PAST_SLOT,
                earliestPermissibleSlot,
                attestationSlot,
            });
        }
    }
    else {
        const attestationEpoch = computeEpochAtSlot(attestationSlot);
        // upper bound for current epoch is same as epoch of latestPermissibleSlot
        const latestPermissibleCurrentEpoch = computeEpochAtSlot(latestPermissibleSlot);
        if (attestationEpoch > latestPermissibleCurrentEpoch) {
            throw new AttestationError(GossipAction.IGNORE, {
                code: AttestationErrorCode.FUTURE_EPOCH,
                currentEpoch: latestPermissibleCurrentEpoch,
                attestationEpoch,
            });
        }
        // lower bound for previous epoch is same as epoch of earliestPermissibleSlot
        const currentEpochWithPastTolerance = computeEpochAtSlot(chain.clock.slotWithPastTolerance(chain.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000));
        const earliestPermissiblePreviousEpoch = Math.max(currentEpochWithPastTolerance - 1, 0);
        if (attestationEpoch < earliestPermissiblePreviousEpoch) {
            throw new AttestationError(GossipAction.IGNORE, {
                code: AttestationErrorCode.PAST_EPOCH,
                previousEpoch: earliestPermissiblePreviousEpoch,
                attestationEpoch,
            });
        }
    }
}
/**
 * Verify:
 * 1. head block is known
 * 2. attestation's target block is an ancestor of the block named in the LMD vote
 */
export function verifyHeadBlockAndTargetRoot(chain, beaconBlockRoot, targetRoot, attestationSlot, attestationEpoch, caller, maxSkipSlots) {
    const headBlock = verifyHeadBlockIsKnown(chain, beaconBlockRoot);
    // Lighthouse rejects the attestation, however Lodestar only ignores considering it's not against the spec
    // it's more about a DOS protection to us
    // With verifyPropagationSlotRange() and maxSkipSlots = 32, it's unlikely we have to regenerate states in queue
    // to validate beacon_attestation and aggregate_and_proof
    const slotDistance = attestationSlot - headBlock.slot;
    chain.metrics?.gossipAttestation.headSlotToAttestationSlot.observe({ caller }, slotDistance);
    if (maxSkipSlots !== undefined && slotDistance > maxSkipSlots) {
        throw new AttestationError(GossipAction.IGNORE, {
            code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS,
            attestationSlot,
            headBlockSlot: headBlock.slot,
        });
    }
    verifyAttestationTargetRoot(headBlock, targetRoot, attestationEpoch);
    return headBlock;
}
/**
 * Get a shuffling for attestation verification from the ShufflingCache.
 * - if blockEpoch is attEpoch, use current shuffling of head state
 * - if blockEpoch is attEpoch - 1, use next shuffling of head state
 * - if blockEpoch is less than attEpoch - 1, dial head state to attEpoch - 1, and add to ShufflingCache
 *
 * This implementation does not require to dial head state to attSlot at fork boundary because we always get domain of attSlot
 * in consumer context.
 *
 * This is similar to the old getStateForAttestationVerification
 * see https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L566
 */
export async function getShufflingForAttestationVerification(chain, attEpoch, attHeadBlock, regenCaller) {
    const blockEpoch = computeEpochAtSlot(attHeadBlock.slot);
    const shufflingDependentRoot = getShufflingDependentRoot(chain.forkChoice, attEpoch, blockEpoch, attHeadBlock);
    const shuffling = await chain.shufflingCache.get(attEpoch, shufflingDependentRoot);
    if (shuffling) {
        // most of the time, we should get the shuffling from cache
        chain.metrics?.gossipAttestation.shufflingCacheHit.inc({ caller: regenCaller });
        return shuffling;
    }
    chain.metrics?.gossipAttestation.shufflingCacheMiss.inc({ caller: regenCaller });
    try {
        // for the 1st time of the same epoch and dependent root, it awaits for the regen state
        // from the 2nd time, it should use the same cached promise and it should reach the above code
        chain.metrics?.gossipAttestation.shufflingCacheRegenHit.inc({ caller: regenCaller });
        return await chain.regenStateForAttestationVerification(attEpoch, shufflingDependentRoot, attHeadBlock, regenCaller);
    }
    catch (e) {
        chain.metrics?.gossipAttestation.shufflingCacheRegenMiss.inc({ caller: regenCaller });
        throw new AttestationError(GossipAction.IGNORE, {
            code: AttestationErrorCode.MISSING_STATE_TO_VERIFY_ATTESTATION,
            error: e,
        });
    }
}
/**
 * Different version of getAttestationDataSigningRoot in state-transition which doesn't require a state.
 */
export function getAttestationDataSigningRoot(config, data) {
    const slot = computeStartSlotAtEpoch(data.target.epoch);
    // previously, we call `domain = config.getDomain(state.slot, DOMAIN_BEACON_ATTESTER, slot)`
    // at fork boundary, it's required to dial to target epoch https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L573
    // instead of that, just use the fork at slot in the attestation data
    const fork = config.getForkName(slot);
    const domain = config.getDomainAtFork(fork, DOMAIN_BEACON_ATTESTER);
    return computeSigningRoot(ssz.phase0.AttestationData, data, domain);
}
/**
 * Checks if the `attestation.data.beaconBlockRoot` is known to this chain.
 *
 * The block root may not be known for two reasons:
 *
 * 1. The block has never been verified by our application.
 * 2. The block is prior to the latest finalized block.
 *
 * Case (1) is the exact thing we're trying to detect. However case (2) is a little different, but
 * it's still fine to ignore here because there's no need for us to handle attestations that are
 * already finalized.
 */
function verifyHeadBlockIsKnown(chain, beaconBlockRoot) {
    // TODO (LH): Enforce a maximum skip distance for unaggregated attestations.
    const headBlock = chain.forkChoice.getBlockDefaultStatus(beaconBlockRoot);
    if (headBlock === null) {
        throw new AttestationError(GossipAction.IGNORE, {
            code: AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT,
            root: toRootHex(beaconBlockRoot),
        });
    }
    return headBlock;
}
/**
 * Verifies that the `attestation.data.target.root` is indeed the target root of the block at
 * `attestation.data.beacon_block_root`.
 */
function verifyAttestationTargetRoot(headBlock, targetRoot, attestationEpoch) {
    // Check the attestation target root.
    const headBlockEpoch = computeEpochAtSlot(headBlock.slot);
    if (headBlockEpoch > attestationEpoch) {
        // The epoch references an invalid head block from a future epoch.
        //
        // This check is not in the specification, however we guard against it since it opens us up
        // to weird edge cases during verification.
        //
        // Whilst this attestation *technically* could be used to add value to a block, it is
        // invalid in the spirit of the protocol. Here we choose safety over profit.
        //
        // Reference:
        // https://github.com/ethereum/consensus-specs/pull/2001#issuecomment-699246659
        throw new AttestationError(GossipAction.REJECT, {
            code: AttestationErrorCode.INVALID_TARGET_ROOT,
            targetRoot: toRootHex(targetRoot),
            expected: null,
        });
    }
    const expectedTargetRoot = headBlockEpoch === attestationEpoch
        ? // If the block is in the same epoch as the attestation, then use the target root
            // from the block.
            headBlock.targetRoot
        : // If the head block is from a previous epoch then skip slots will cause the head block
            // root to become the target block root.
            //
            // We know the head block is from a previous epoch due to a previous check.
            headBlock.blockRoot;
    // TODO: Do a fast comparision to convert and compare byte by byte
    if (expectedTargetRoot !== toRootHex(targetRoot)) {
        // Reject any attestation with an invalid target root.
        throw new AttestationError(GossipAction.REJECT, {
            code: AttestationErrorCode.INVALID_TARGET_ROOT,
            targetRoot: toRootHex(targetRoot),
            expected: expectedTargetRoot,
        });
    }
}
/**
 * Get a list of validator indices in the given committee
 * attestationIndex - Index of the committee in shuffling.committees
 */
export function getCommitteeValidatorIndices(shuffling, attestationSlot, attestationIndex) {
    const { committees } = shuffling;
    const slotCommittees = committees[attestationSlot % SLOTS_PER_EPOCH];
    if (attestationIndex >= slotCommittees.length) {
        throw new AttestationError(GossipAction.REJECT, {
            code: AttestationErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE,
            index: attestationIndex,
        });
    }
    return slotCommittees[attestationIndex];
}
/**
 * Compute the correct subnet for a slot/committee index
 */
export function computeSubnetForSlot(shuffling, slot, committeeIndex) {
    const slotsSinceEpochStart = slot % SLOTS_PER_EPOCH;
    const committeesSinceEpochStart = shuffling.committeesPerSlot * slotsSinceEpochStart;
    return (committeesSinceEpochStart + committeeIndex) % ATTESTATION_SUBNET_COUNT;
}
/**
 * Return fork-dependent seen attestation key
 *   - for pre-electra, it's the AttestationData base64 from Attestation
 *   - for electra and later, it's the AttestationData base64 from SingleAttestation
 *   - consumers need to also pass slot + committeeIndex to get the correct SeenAttestationData
 */
export function getSeenAttDataKeyFromGossipAttestation(attestation) {
    // SeenAttDataKey is the same as gossip index
    return attestation.attDataBase64;
}
/**
 * Extract attestation data key from SignedAggregateAndProof Uint8Array to use cached data from SeenAttestationDatas
 *   - for both electra + pre-electra, it's the AttestationData base64
 *   - consumers need to also pass slot + committeeIndex to get the correct SeenAttestationData
 */
export function getSeenAttDataKeyFromSignedAggregateAndProof(fork, aggregateAndProof) {
    return isForkPostElectra(fork)
        ? getAttDataFromSignedAggregateAndProofElectra(aggregateAndProof)
        : getAttDataFromSignedAggregateAndProofPhase0(aggregateAndProof);
}
export function getCommitteeIndexFromAttestationOrBytes(fork, attestationOrBytes) {
    const isGossipAttestation = attestationOrBytes.serializedData !== null;
    if (isForkPostElectra(fork)) {
        if (isGossipAttestation) {
            return getCommitteeIndexFromSingleAttestationSerialized(ForkName.electra, attestationOrBytes.serializedData);
        }
        return attestationOrBytes.attestation.committeeIndex;
    }
    if (isGossipAttestation) {
        return getCommitteeIndexFromSingleAttestationSerialized(ForkName.phase0, attestationOrBytes.serializedData);
    }
    return attestationOrBytes.attestation.data.index;
}
/**
 * Convert pre-electra single attestation (`phase0.Attestation`) to post-electra `SingleAttestation`
 */
export function toElectraSingleAttestation(attestation, attesterIndex) {
    return {
        committeeIndex: attestation.data.index,
        attesterIndex,
        data: attestation.data,
        signature: attestation.signature,
    };
}
//# sourceMappingURL=attestation.js.map