use crate::metrics::{self, scrape_for_metrics};
use crate::{ForkChoiceStore, InvalidationOperation};
use fixed_bytes::FixedBytesExtended;
use logging::crit;
use proto_array::{
    Block as ProtoBlock, ExecutionStatus, JustifiedBalances, LatestMessage, PayloadStatus,
    ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold,
};
use ssz_derive::{Decode, Encode};
use state_processing::{
    per_block_processing::errors::AttesterSlashingValidationError, per_epoch_processing,
};
use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::marker::PhantomData;
use std::time::Duration;
use superstruct::superstruct;
use tracing::{debug, instrument, warn};
use types::{
    AbstractExecPayload, AttestationShufflingId, AttesterSlashingRef, BeaconBlockRef, BeaconState,
    BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, ExecPayload, ExecutionBlockHash,
    Hash256, IndexedAttestationRef, IndexedPayloadAttestation, RelativeEpoch, SignedBeaconBlock,
    Slot,
};

#[derive(Debug)]
pub enum Error<T> {
    InvalidAttestation(InvalidAttestation),
    InvalidPayloadAttestation(InvalidPayloadAttestation),
    InvalidAttesterSlashing(AttesterSlashingValidationError),
    InvalidBlock(InvalidBlock),
    ProtoArrayStringError(String),
    ProtoArrayError(proto_array::Error),
    InvalidProtoArrayBytes(String),
    InvalidLegacyProtoArrayBytes(String),
    FailedToProcessInvalidExecutionPayload(String),
    FailedToProcessValidExecutionPayload(String),
    MissingProtoArrayBlock(Hash256),
    UnknownAncestor {
        ancestor_slot: Slot,
        descendant_root: Hash256,
    },
    InconsistentOnTick {
        previous_slot: Slot,
        time: Slot,
    },
    BeaconStateError(BeaconStateError),
    AttemptToRevertJustification {
        store: Slot,
        state: Slot,
    },
    ForkChoiceStoreError(T),
    UnableToSetJustifiedCheckpoint(T),
    AfterBlockFailed(T),
    ProposerHeadError(T),
    InvalidAnchor {
        block_slot: Slot,
        state_slot: Slot,
    },
    InvalidPayloadStatus {
        block_slot: Slot,
        block_root: Hash256,
        payload_verification_status: PayloadVerificationStatus,
    },
    MissingJustifiedBlock {
        justified_checkpoint: Checkpoint,
    },
    MissingFinalizedBlock {
        finalized_checkpoint: Checkpoint,
    },
    WrongSlotForGetProposerHead {
        current_slot: Slot,
        fc_store_slot: Slot,
    },
    ProposerBoostNotExpiredForGetProposerHead {
        proposer_boost_root: Hash256,
    },
    UnrealizedVoteProcessing(state_processing::EpochProcessingError),
    ValidatorStatuses(BeaconStateError),
    ChainSpecError(String),
    DoesNotDescendFromFinalizedCheckpoint,
}

impl<T> From<InvalidAttestation> for Error<T> {
    fn from(e: InvalidAttestation) -> Self {
        Error::InvalidAttestation(e)
    }
}

impl<T> From<InvalidPayloadAttestation> for Error<T> {
    fn from(e: InvalidPayloadAttestation) -> Self {
        Error::InvalidPayloadAttestation(e)
    }
}

impl<T> From<AttesterSlashingValidationError> for Error<T> {
    fn from(e: AttesterSlashingValidationError) -> Self {
        Error::InvalidAttesterSlashing(e)
    }
}

impl<T> From<state_processing::EpochProcessingError> for Error<T> {
    fn from(e: state_processing::EpochProcessingError) -> Self {
        Error::UnrealizedVoteProcessing(e)
    }
}

impl<T> From<BeaconStateError> for Error<T> {
    fn from(e: BeaconStateError) -> Self {
        Error::BeaconStateError(e)
    }
}

#[derive(Debug, Clone, Copy)]
/// Controls how fork choice should behave when restoring from a persisted fork choice.
pub enum ResetPayloadStatuses {
    /// Reset all payload statuses back to "optimistic".
    Always,
    /// Only reset all payload statuses back to "optimistic" when an "invalid" block is present.
    OnlyWithInvalidPayload,
}

impl ResetPayloadStatuses {
    /// When `should_always_reset == True`, return `ResetPayloadStatuses::Always`.
    pub fn always_reset_conditionally(should_always_reset: bool) -> Self {
        if should_always_reset {
            ResetPayloadStatuses::Always
        } else {
            ResetPayloadStatuses::OnlyWithInvalidPayload
        }
    }
}

#[derive(Debug)]
pub enum InvalidBlock {
    UnknownParent(Hash256),
    FutureSlot {
        current_slot: Slot,
        block_slot: Slot,
    },
    FinalizedSlot {
        finalized_slot: Slot,
        block_slot: Slot,
    },
    NotFinalizedDescendant {
        finalized_root: Hash256,
        block_ancestor: Option<Hash256>,
    },
}

#[derive(Debug)]
pub enum InvalidAttestation {
    /// The attestations aggregation bits were empty when they shouldn't be.
    EmptyAggregationBitfield,
    /// The `attestation.data.beacon_block_root` block is unknown.
    UnknownHeadBlock { beacon_block_root: Hash256 },
    /// The `attestation.data.slot` is not from the same epoch as `data.target.epoch` and therefore
    /// the attestation is invalid.
    BadTargetEpoch { target: Epoch, slot: Slot },
    /// The target root of the attestation points to a block that we have not verified.
    UnknownTargetRoot(Hash256),
    /// The attestation is for an epoch in the future (with respect to the gossip clock disparity).
    FutureEpoch {
        attestation_epoch: Epoch,
        current_epoch: Epoch,
    },
    /// The attestation is for an epoch in the past (with respect to the gossip clock disparity).
    PastEpoch {
        attestation_epoch: Epoch,
        current_epoch: Epoch,
    },
    /// The attestation references a target root that does not match what is stored in our
    /// database.
    InvalidTarget {
        attestation: Hash256,
        local: Hash256,
    },
    /// The attestation is attesting to a state that is later than itself. (Viz., attesting to the
    /// future).
    AttestsToFutureBlock { block: Slot, attestation: Slot },
    /// Post-Gloas: attestation index must be 0 or 1.
    InvalidAttestationIndex { index: u64 },
    /// A same-slot attestation has a non-zero index, which is invalid post-Gloas.
    InvalidSameSlotAttestationIndex { slot: Slot },
    /// Post-Gloas: attestation with index == 1 (payload_present) requires the block's
    /// payload to have been received (`root in store.payload_states`).
    PayloadNotReceived { beacon_block_root: Hash256 },
}

#[derive(Debug, Clone, PartialEq)]
pub enum InvalidPayloadAttestation {
    /// The payload attestation's attesting indices were empty.
    EmptyAggregationBitfield,
    /// The `payload_attestation.data.beacon_block_root` block is unknown.
    UnknownHeadBlock { beacon_block_root: Hash256 },
    /// The payload attestation is attesting to a block that is later than itself.
    AttestsToFutureBlock { block: Slot, attestation: Slot },
    /// A gossip payload attestation must be for the current slot.
    PayloadAttestationNotCurrentSlot {
        attestation_slot: Slot,
        current_slot: Slot,
    },
    /// One or more payload attesters are not part of the PTC.
    PayloadAttestationAttestersNotInPtc {
        attesting_indices_len: usize,
        attesting_indices_in_ptc: usize,
    },
}

/// The import status of a block's parent, as seen by fork choice.
#[allow(clippy::large_enum_variant)]
pub enum ParentImportStatus {
    /// The parent block is imported and the child's bid commits to a parent payload known to fork
    /// choice.
    Imported(ProtoBlock),
    /// The parent block is not known to fork choice.
    UnknownBlock,
    /// The parent block is known, but the child's bid commits to a payload not known to fork choice.
    UnknownPayload,
}

impl<T> From<String> for Error<T> {
    fn from(e: String) -> Self {
        Error::ProtoArrayStringError(e)
    }
}

impl<T> From<proto_array::Error> for Error<T> {
    fn from(e: proto_array::Error) -> Self {
        Error::ProtoArrayError(e)
    }
}

/// Indicates if a block has been verified by an execution payload.
///
/// There is no variant for "invalid", since such a block should never be added to fork choice.
#[derive(Clone, Copy, Debug, PartialEq, Encode, Decode)]
#[ssz(enum_behaviour = "tag")]
pub enum PayloadVerificationStatus {
    /// An EL has declared the execution payload to be valid.
    Verified,
    /// An EL has not yet made a determination about the execution payload.
    Optimistic,
    /// The block is either pre-merge-fork, or prior to the terminal PoW block.
    Irrelevant,
}

impl PayloadVerificationStatus {
    /// Returns `true` if the payload was optimistically imported.
    pub fn is_optimistic(&self) -> bool {
        match self {
            PayloadVerificationStatus::Verified => false,
            PayloadVerificationStatus::Optimistic => true,
            PayloadVerificationStatus::Irrelevant => false,
        }
    }
}

/// Calculate how far `slot` lies from the start of its epoch.
///
/// ## Specification
///
/// Equivalent to:
///
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#compute_slots_since_epoch_start
pub fn compute_slots_since_epoch_start<E: EthSpec>(slot: Slot) -> Slot {
    slot - slot
        .epoch(E::slots_per_epoch())
        .start_slot(E::slots_per_epoch())
}

/// Calculate the first slot in `epoch`.
///
/// ## Specification
///
/// Equivalent to:
///
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#compute_start_slot_at_epoch
fn compute_start_slot_at_epoch<E: EthSpec>(epoch: Epoch) -> Slot {
    epoch.start_slot(E::slots_per_epoch())
}

/// Used for queuing attestations from the current slot. Only contains the minimum necessary
/// information about the attestation.
#[derive(Clone, PartialEq, Encode, Decode)]
pub struct QueuedAttestation {
    slot: Slot,
    attesting_indices: Vec<u64>,
    block_root: Hash256,
    target_epoch: Epoch,
    /// Per Gloas spec: `payload_present = attestation.data.index == 1`.
    payload_present: bool,
}

/// Legacy queued attestation without payload_present (pre-Gloas, schema V28).
#[derive(Clone, PartialEq, Encode, Decode)]
pub struct QueuedAttestationV28 {
    slot: Slot,
    attesting_indices: Vec<u64>,
    block_root: Hash256,
    target_epoch: Epoch,
}

impl<'a, E: EthSpec> From<IndexedAttestationRef<'a, E>> for QueuedAttestation {
    fn from(a: IndexedAttestationRef<'a, E>) -> Self {
        Self {
            slot: a.data().slot,
            attesting_indices: a.attesting_indices_to_vec(),
            block_root: a.data().beacon_block_root,
            target_epoch: a.data().target.epoch,
            payload_present: a.data().index == 1,
        }
    }
}

/// Returns all values in `self.queued_attestations` that have a slot that is earlier than the
/// current slot. Also removes those values from `self.queued_attestations`.
fn dequeue_attestations(
    current_slot: Slot,
    queued_attestations: &mut Vec<QueuedAttestation>,
) -> Vec<QueuedAttestation> {
    let remaining = queued_attestations.split_off(
        queued_attestations
            .iter()
            .position(|a| a.slot >= current_slot)
            .unwrap_or(queued_attestations.len()),
    );

    metrics::inc_counter_by(
        &metrics::FORK_CHOICE_DEQUEUED_ATTESTATIONS,
        queued_attestations.len() as u64,
    );

    std::mem::replace(queued_attestations, remaining)
}

/// Denotes whether an attestation we are processing was received from a block or from gossip.
/// Equivalent to the `is_from_block` `bool` in:
///
/// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#validate_on_attestation
#[derive(Clone, Copy)]
pub enum AttestationFromBlock {
    True,
    False,
}

/// Parameters which are cached between calls to `ForkChoice::get_head`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ForkchoiceUpdateParameters {
    /// The most recent result of running `ForkChoice::get_head`.
    pub head_root: Hash256,
    pub head_hash: Option<ExecutionBlockHash>,
    pub justified_hash: Option<ExecutionBlockHash>,
    pub finalized_hash: Option<ExecutionBlockHash>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ForkChoiceView {
    pub head_block_root: Hash256,
    pub justified_checkpoint: Checkpoint,
    pub finalized_checkpoint: Checkpoint,
}

/// Provides an implementation of "Ethereum 2.0 Phase 0 -- Beacon Chain Fork Choice":
///
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#ethereum-20-phase-0----beacon-chain-fork-choice
///
/// ## Detail
///
/// This struct wraps `ProtoArrayForkChoice` and provides:
///
/// - Management of the justified state and caching of balances.
/// - Queuing of attestations from the current slot.
pub struct ForkChoice<T, E> {
    /// Storage for `ForkChoice`, modelled off the spec `Store` object.
    fc_store: T,
    /// The underlying representation of the block DAG.
    proto_array: ProtoArrayForkChoice,
    /// Attestations that arrived at the current slot and must be queued for later processing.
    queued_attestations: Vec<QueuedAttestation>,
    /// Stores a cache of the values required to be sent to the execution layer.
    forkchoice_update_parameters: ForkchoiceUpdateParameters,
    _phantom: PhantomData<E>,
}

impl<T, E> PartialEq for ForkChoice<T, E>
where
    T: ForkChoiceStore<E> + PartialEq,
    E: EthSpec,
{
    fn eq(&self, other: &Self) -> bool {
        self.fc_store == other.fc_store
            && self.proto_array == other.proto_array
            && self.queued_attestations == other.queued_attestations
    }
}

impl<T, E> ForkChoice<T, E>
where
    T: ForkChoiceStore<E>,
    E: EthSpec,
{
    /// Instantiates `Self` from an anchor (genesis or another finalized checkpoint).
    pub fn from_anchor(
        fc_store: T,
        anchor_block_root: Hash256,
        anchor_block: &SignedBeaconBlock<E>,
        anchor_state: &BeaconState<E>,
        current_slot: Option<Slot>,
        spec: &ChainSpec,
    ) -> Result<Self, Error<T::Error>> {
        // Sanity check: the anchor must lie on an epoch boundary.
        if anchor_state.slot() % E::slots_per_epoch() != 0 {
            return Err(Error::InvalidAnchor {
                block_slot: anchor_block.slot(),
                state_slot: anchor_state.slot(),
            });
        }

        let finalized_block_slot = anchor_block.slot();
        let finalized_block_state_root = anchor_block.state_root();
        let current_epoch_shuffling_id =
            AttestationShufflingId::new(anchor_block_root, anchor_state, RelativeEpoch::Current)
                .map_err(Error::BeaconStateError)?;
        let next_epoch_shuffling_id =
            AttestationShufflingId::new(anchor_block_root, anchor_state, RelativeEpoch::Next)
                .map_err(Error::BeaconStateError)?;

        let (execution_status, execution_payload_parent_hash, execution_payload_block_hash) =
            if let Ok(signed_bid) = anchor_block.message().body().signed_execution_payload_bid() {
                // Gloas: execution status is irrelevant post-Gloas; payload validation
                // is decoupled from beacon blocks.
                (
                    ExecutionStatus::irrelevant(),
                    Some(signed_bid.message.parent_block_hash),
                    Some(signed_bid.message.block_hash),
                )
            } else if let Ok(execution_payload) = anchor_block.message().execution_payload() {
                // Pre-Gloas forks: do not set payload hashes, they are only used post-Gloas.
                if execution_payload.is_default_with_empty_roots() {
                    (ExecutionStatus::irrelevant(), None, None)
                } else {
                    // Assume that this payload is valid, since the anchor should be a
                    // trusted block and state.
                    (
                        ExecutionStatus::Valid(execution_payload.block_hash()),
                        None,
                        None,
                    )
                }
            } else {
                // Pre-merge: no execution payload at all.
                (ExecutionStatus::irrelevant(), None, None)
            };

        // If the current slot is not provided, use the value that was last provided to the store.
        let current_slot = current_slot.unwrap_or_else(|| fc_store.get_current_slot());

        let proto_array = ProtoArrayForkChoice::new::<E>(
            current_slot,
            finalized_block_slot,
            finalized_block_state_root,
            *fc_store.justified_checkpoint(),
            *fc_store.finalized_checkpoint(),
            current_epoch_shuffling_id,
            next_epoch_shuffling_id,
            execution_status,
            execution_payload_parent_hash,
            execution_payload_block_hash,
            anchor_block.message().proposer_index(),
            spec,
        )?;

        let mut fork_choice = Self {
            fc_store,
            proto_array,
            queued_attestations: vec![],
            // This will be updated during the next call to `Self::get_head`.
            forkchoice_update_parameters: ForkchoiceUpdateParameters {
                head_hash: None,
                justified_hash: None,
                finalized_hash: None,
                // This will be updated during the next call to `Self::get_head`.
                head_root: Hash256::zero(),
            },
            _phantom: PhantomData,
        };

        // Ensure that `fork_choice.forkchoice_update_parameters.head_root` is updated.
        fork_choice.get_head(current_slot, spec)?;

        Ok(fork_choice)
    }

    /// Returns cached information that can be used to issue a `forkchoiceUpdated` message to an
    /// execution engine.
    ///
    /// These values are updated each time `Self::get_head` is called.
    pub fn get_forkchoice_update_parameters(&self) -> ForkchoiceUpdateParameters {
        self.forkchoice_update_parameters
    }

    /// Returns the block root of an ancestor of `block_root` at the given `slot`. (Note: `slot` refers
    /// to the block that is *returned*, not the one that is supplied.)
    ///
    /// The result may be `Ok(None)` if the block does not descend from the finalized block. This
    /// is an artifact of proto-array, sometimes it contains descendants of blocks that have been
    /// pruned.
    ///
    /// ## Specification
    ///
    /// Equivalent to:
    ///
    /// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#get_ancestor
    fn get_ancestor(
        &self,
        block_root: Hash256,
        ancestor_slot: Slot,
    ) -> Result<Option<Hash256>, Error<T::Error>>
    where
        T: ForkChoiceStore<E>,
        E: EthSpec,
    {
        let block = self
            .proto_array
            .get_block(&block_root)
            .ok_or(Error::MissingProtoArrayBlock(block_root))?;

        match block.slot.cmp(&ancestor_slot) {
            Ordering::Greater => Ok(self
                .proto_array
                .core_proto_array()
                .iter_block_roots(&block_root)
                // Search for a slot that is **less than or equal to** the target slot. We check
                // for lower slots to account for skip slots.
                .find(|(_, slot)| *slot <= ancestor_slot)
                .map(|(root, _)| root)),
            // Root is older than queried slot, thus a skip slot. Return most recent root prior
            // to slot.
            Ordering::Less => Ok(Some(block_root)),
            Ordering::Equal => Ok(Some(block_root)),
        }
    }

    /// Run the fork choice rule to determine the head.
    ///
    /// ## Specification
    ///
    /// Is equivalent to:
    ///
    /// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#get_head
    #[instrument(skip_all, level = "debug")]
    pub fn get_head(
        &mut self,
        system_time_current_slot: Slot,
        spec: &ChainSpec,
    ) -> Result<(Hash256, PayloadStatus), Error<T::Error>> {
        // Provide the slot (as per the system clock) to the `fc_store` and then return its view of
        // the current slot. The `fc_store` will ensure that the `current_slot` is never
        // decreasing, a property which we must maintain.
        let current_slot = self.update_time(system_time_current_slot)?;

        let store = &mut self.fc_store;

        let (head_root, head_payload_status) = self.proto_array.find_head::<E>(
            *store.justified_checkpoint(),
            *store.finalized_checkpoint(),
            store.justified_balances(),
            store.proposer_boost_root(),
            store.equivocating_indices(),
            current_slot,
            spec,
        )?;

        // Cache some values for the next forkchoiceUpdate call to the execution layer.
        // For Gloas blocks, `execution_status` is Irrelevant (no embedded payload).
        // If the payload envelope was received (Full), use the bid's block_hash as the
        // execution chain head. Otherwise fall back to the parent hash (Pending) or None.
        // For justified/finalized hashes we always use the bid's parent_block_hash, since the
        // payload from the justified/finalized block is not itself justified/finalized due to
        // being applied immediately prior to the next block.
        let head_hash = self.get_block(&head_root).and_then(|b| {
            b.execution_status
                .block_hash()
                .or(match head_payload_status {
                    PayloadStatus::Full => b.execution_payload_block_hash,
                    PayloadStatus::Pending | PayloadStatus::Empty => {
                        b.execution_payload_parent_hash
                    }
                })
        });
        let justified_root = self.justified_checkpoint().root;
        let finalized_root = self.finalized_checkpoint().root;
        let justified_hash = self.get_block(&justified_root).and_then(|b| {
            b.execution_status
                .block_hash()
                .or(b.execution_payload_parent_hash)
        });
        let finalized_hash = self.get_block(&finalized_root).and_then(|b| {
            b.execution_status
                .block_hash()
                .or(b.execution_payload_parent_hash)
        });
        self.forkchoice_update_parameters = ForkchoiceUpdateParameters {
            head_root,
            head_hash,
            justified_hash,
            finalized_hash,
        };

        Ok((head_root, head_payload_status))
    }

    /// Get the block to build on as proposer, taking into account proposer re-orgs.
    ///
    /// You *must* call `get_head` for the proposal slot prior to calling this function and pass
    /// in the result of `get_head` as `canonical_head`.
    #[instrument(level = "debug", skip_all)]
    pub fn get_proposer_head(
        &self,
        current_slot: Slot,
        canonical_head: Hash256,
        re_org_head_threshold: ReOrgThreshold,
        re_org_parent_threshold: ReOrgThreshold,
        max_epochs_since_finalization: Epoch,
    ) -> Result<ProposerHeadInfo, ProposerHeadError<Error<proto_array::Error>>> {
        // Ensure that fork choice has already been updated for the current slot. This prevents
        // us from having to take a write lock or do any dequeueing of attestations in this
        // function.
        let fc_store_slot = self.fc_store.get_current_slot();
        if current_slot != fc_store_slot {
            return Err(ProposerHeadError::Error(
                Error::WrongSlotForGetProposerHead {
                    current_slot,
                    fc_store_slot,
                },
            ));
        }

        // Similarly, the proposer boost for the previous head should already have expired.
        let proposer_boost_root = self.fc_store.proposer_boost_root();
        if !proposer_boost_root.is_zero() {
            return Err(ProposerHeadError::Error(
                Error::ProposerBoostNotExpiredForGetProposerHead {
                    proposer_boost_root,
                },
            ));
        }

        self.proto_array
            .get_proposer_head::<E>(
                current_slot,
                canonical_head,
                self.fc_store.justified_balances(),
                re_org_head_threshold,
                re_org_parent_threshold,
                max_epochs_since_finalization,
            )
            .map_err(ProposerHeadError::convert_inner_error)
    }

    pub fn get_preliminary_proposer_head(
        &self,
        canonical_head: Hash256,
        re_org_head_threshold: ReOrgThreshold,
        re_org_parent_threshold: ReOrgThreshold,
        max_epochs_since_finalization: Epoch,
    ) -> Result<ProposerHeadInfo, ProposerHeadError<Error<proto_array::Error>>> {
        let current_slot = self.fc_store.get_current_slot();
        self.proto_array
            .get_proposer_head_info::<E>(
                current_slot,
                canonical_head,
                self.fc_store.justified_balances(),
                re_org_head_threshold,
                re_org_parent_threshold,
                max_epochs_since_finalization,
            )
            .map_err(ProposerHeadError::convert_inner_error)
    }

    /// Return information about:
    ///
    /// - The LMD head of the chain.
    /// - The FFG checkpoints.
    ///
    /// The information is "cached" since the last call to `Self::get_head`.
    ///
    /// ## Notes
    ///
    /// The finalized/justified checkpoints are determined from the fork choice store. Therefore,
    /// it's possible that the state corresponding to `get_state(get_block(head_block_root))` will
    /// have *differing* finalized and justified information.
    pub fn cached_fork_choice_view(&self) -> ForkChoiceView {
        ForkChoiceView {
            head_block_root: self.forkchoice_update_parameters.head_root,
            justified_checkpoint: self.justified_checkpoint(),
            finalized_checkpoint: self.finalized_checkpoint(),
        }
    }

    /// Mark a Gloas payload envelope as valid and received.
    ///
    /// This must only be called for valid Gloas payloads.
    pub fn on_valid_payload_envelope_received(
        &mut self,
        block_root: Hash256,
    ) -> Result<(), Error<T::Error>> {
        self.proto_array
            .on_valid_payload_envelope_received(block_root)
            .map_err(Error::FailedToProcessValidExecutionPayload)
    }

    /// Pre-Gloas only.
    ///
    /// See `ProtoArrayForkChoice::process_execution_payload_validation` for documentation.
    pub fn on_valid_execution_payload(
        &mut self,
        block_root: Hash256,
    ) -> Result<(), Error<T::Error>> {
        self.proto_array
            .process_execution_payload_validation(block_root)
            .map_err(Error::FailedToProcessValidExecutionPayload)
    }

    /// Pre-Gloas only.
    ///
    /// See `ProtoArrayForkChoice::process_execution_payload_invalidation` for documentation.
    pub fn on_invalid_execution_payload(
        &mut self,
        op: &InvalidationOperation,
    ) -> Result<(), Error<T::Error>> {
        self.proto_array
            .process_execution_payload_invalidation::<E>(op, self.finalized_checkpoint())
            .map_err(Error::FailedToProcessInvalidExecutionPayload)
    }

    /// Add `block` to the fork choice DAG.
    ///
    /// - `block_root` is the root of `block.
    /// - The root of `state` matches `block.state_root`.
    ///
    /// ## Specification
    ///
    /// Approximates:
    ///
    /// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#on_block
    ///
    /// It only approximates the specification since it does not run the `state_transition` check.
    /// That should have already been called upstream and it's too expensive to call again.
    ///
    /// ## Notes:
    ///
    /// The supplied block **must** pass the `state_transition` function as it will not be run
    /// here.
    #[allow(clippy::too_many_arguments)]
    #[instrument(
        name = "fork_choice_on_block",
        skip_all,
        fields(
            fork_choice_block_delay = ?block_delay
        ))]
    pub fn on_block<Payload: AbstractExecPayload<E>>(
        &mut self,
        system_time_current_slot: Slot,
        block: BeaconBlockRef<E, Payload>,
        block_root: Hash256,
        block_delay: Duration,
        state: &BeaconState<E>,
        payload_verification_status: PayloadVerificationStatus,
        canonical_head_proposer_index: u64,
        spec: &ChainSpec,
    ) -> Result<(), Error<T::Error>> {
        let _timer = metrics::start_timer(&metrics::FORK_CHOICE_ON_BLOCK_TIMES);

        // If this block has already been processed we do not need to reprocess it.
        // We check this immediately in case re-processing the block mutates some property of the
        // global fork choice store, e.g. the justified checkpoints or the proposer boost root.
        if self.proto_array.contains_block(&block_root) {
            return Ok(());
        }

        // Provide the slot (as per the system clock) to the `fc_store` and then return its view of
        // the current slot. The `fc_store` will ensure that the `current_slot` is never
        // decreasing, a property which we must maintain.
        let current_slot = self.update_time(system_time_current_slot)?;

        // Parent block must be known.
        let parent_block = self
            .proto_array
            .get_block(&block.parent_root())
            .ok_or_else(|| Error::InvalidBlock(InvalidBlock::UnknownParent(block.parent_root())))?;

        // Blocks cannot be in the future. If they are, their consideration must be delayed until
        // they are in the past.
        //
        // Note: presently, we do not delay consideration. We just drop the block.
        if block.slot() > current_slot {
            return Err(Error::InvalidBlock(InvalidBlock::FutureSlot {
                current_slot,
                block_slot: block.slot(),
            }));
        }

        // Check that block is later than the finalized epoch slot (optimization to reduce calls to
        // get_ancestor).
        let finalized_slot =
            compute_start_slot_at_epoch::<E>(self.fc_store.finalized_checkpoint().epoch);
        if block.slot() <= finalized_slot {
            return Err(Error::InvalidBlock(InvalidBlock::FinalizedSlot {
                finalized_slot,
                block_slot: block.slot(),
            }));
        }

        // Check block is a descendant of the finalized block at the checkpoint finalized slot.
        //
        // Note: the specification uses `hash_tree_root(block)` instead of `block.parent_root` for
        // the start of this search. I claim that since `block.slot > finalized_slot` it is
        // equivalent to use the parent root for this search. Doing so reduces a single lookup
        // (trivial), but more importantly, it means we don't need to have added `block` to
        // `self.proto_array` to do this search. See:
        //
        // https://github.com/ethereum/eth2.0-specs/pull/1884
        let block_ancestor = self.get_ancestor(block.parent_root(), finalized_slot)?;
        let finalized_root = self.fc_store.finalized_checkpoint().root;
        if block_ancestor != Some(finalized_root) {
            return Err(Error::InvalidBlock(InvalidBlock::NotFinalizedDescendant {
                finalized_root,
                block_ancestor,
            }));
        }

        let attestation_threshold = spec.get_attestation_due::<E>(block.slot());

        // Add proposer score boost if the block is the first timely block for this slot and its
        // proposer matches the expected proposer on the canonical chain (per spec
        // `update_proposer_boost_root`, introduced in v1.7.0-alpha.5).
        let is_before_attesting_interval = block_delay < attestation_threshold;

        let is_first_block = self.fc_store.proposer_boost_root().is_zero();
        let is_canonical_proposer = block.proposer_index() == canonical_head_proposer_index;
        if current_slot == block.slot()
            && is_before_attesting_interval
            && is_first_block
            && is_canonical_proposer
        {
            self.fc_store.set_proposer_boost_root(block_root);
        }

        // Update store with checkpoints if necessary
        self.update_checkpoints(
            state.current_justified_checkpoint(),
            state.finalized_checkpoint(),
            || {
                state
                    .get_state_root_at_epoch_start(state.current_justified_checkpoint().epoch)
                    .map_err(Into::into)
            },
        )?;

        // Update unrealized justified/finalized checkpoints.
        let block_epoch = block.slot().epoch(E::slots_per_epoch());

        // If the parent checkpoints are already at the same epoch as the block being imported,
        // it's impossible for the unrealized checkpoints to differ from the parent's. This
        // holds true because:
        //
        // 1. A child block cannot have lower FFG checkpoints than its parent.
        // 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`.
        // 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`.
        //
        // This is an optimization. It should reduce the amount of times we run
        // `process_justification_and_finalization` by approximately 1/3rd when the chain is
        // performing optimally.
        let parent_checkpoints = parent_block
            .unrealized_justified_checkpoint
            .zip(parent_block.unrealized_finalized_checkpoint)
            .filter(|(parent_justified, parent_finalized)| {
                parent_justified.epoch == block_epoch && parent_finalized.epoch + 1 == block_epoch
            });

        let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) =
            if let Some((parent_justified, parent_finalized)) = parent_checkpoints {
                (parent_justified, parent_finalized)
            } else {
                let justification_and_finalization_state =
                    if block.fork_name_unchecked().altair_enabled() {
                        // NOTE: Processing justification & finalization requires the progressive
                        // balances cache, but we cannot initialize it here as we only have an
                        // immutable reference. The state *should* have come straight from block
                        // processing, which initialises the cache, but if we add other `on_block`
                        // calls in future it could be worth passing a mutable reference.
                        per_epoch_processing::altair::process_justification_and_finalization(state)?
                    } else {
                        let mut validator_statuses =
                            per_epoch_processing::base::ValidatorStatuses::new(state, spec)
                                .map_err(Error::ValidatorStatuses)?;
                        validator_statuses
                            .process_attestations(state)
                            .map_err(Error::ValidatorStatuses)?;
                        per_epoch_processing::base::process_justification_and_finalization(
                            state,
                            &validator_statuses.total_balances,
                            spec,
                        )?
                    };

                (
                    justification_and_finalization_state.current_justified_checkpoint(),
                    justification_and_finalization_state.finalized_checkpoint(),
                )
            };

        // Update best known unrealized justified & finalized checkpoints
        if unrealized_justified_checkpoint.epoch
            > self.fc_store.unrealized_justified_checkpoint().epoch
        {
            // Justification has recently updated therefore the justified state root should be in
            // range of the head state's `state_roots` vector.
            let unrealized_justified_state_root =
                state.get_state_root_at_epoch_start(unrealized_justified_checkpoint.epoch)?;

            self.fc_store.set_unrealized_justified_checkpoint(
                unrealized_justified_checkpoint,
                unrealized_justified_state_root,
            );
        }
        if unrealized_finalized_checkpoint.epoch
            > self.fc_store.unrealized_finalized_checkpoint().epoch
        {
            self.fc_store
                .set_unrealized_finalized_checkpoint(unrealized_finalized_checkpoint);
        }

        // If block is from past epochs, try to update store's justified & finalized checkpoints right away
        if block.slot().epoch(E::slots_per_epoch()) < current_slot.epoch(E::slots_per_epoch()) {
            self.pull_up_store_checkpoints(
                unrealized_justified_checkpoint,
                unrealized_finalized_checkpoint,
                || {
                    // In the case where we actually update justification, it must be that the
                    // unrealized justification is recent and in range of the `state_roots` vector.
                    state
                        .get_state_root_at_epoch_start(unrealized_justified_checkpoint.epoch)
                        .map_err(Into::into)
                },
            )?;
        }

        let target_slot = block
            .slot()
            .epoch(E::slots_per_epoch())
            .start_slot(E::slots_per_epoch());
        let target_root = if block.slot() == target_slot {
            block_root
        } else {
            *state
                .get_block_root(target_slot)
                .map_err(Error::BeaconStateError)?
        };

        self.fc_store
            .on_verified_block(block, block_root, state)
            .map_err(Error::AfterBlockFailed)?;

        let execution_status = if let Ok(execution_payload) = block.body().execution_payload() {
            let block_hash = execution_payload.block_hash();

            if block_hash == ExecutionBlockHash::zero() {
                // The block is post-merge-fork, but pre-terminal-PoW block. We don't need to verify
                // the payload.
                ExecutionStatus::irrelevant()
            } else {
                match payload_verification_status {
                    PayloadVerificationStatus::Verified => ExecutionStatus::Valid(block_hash),
                    PayloadVerificationStatus::Optimistic => {
                        ExecutionStatus::Optimistic(block_hash)
                    }
                    // It would be a logic error to declare a block irrelevant if it has an
                    // execution payload with a non-zero block hash.
                    PayloadVerificationStatus::Irrelevant => {
                        return Err(Error::InvalidPayloadStatus {
                            block_slot: block.slot(),
                            block_root,
                            payload_verification_status,
                        });
                    }
                }
            }
        } else {
            // There is no payload to verify.
            ExecutionStatus::irrelevant()
        };

        let (execution_payload_parent_hash, execution_payload_block_hash) =
            if let Ok(signed_bid) = block.body().signed_execution_payload_bid() {
                (
                    Some(signed_bid.message.parent_block_hash),
                    Some(signed_bid.message.block_hash),
                )
            } else {
                (None, None)
            };

        // This does not apply a vote to the block, it just makes fork choice aware of the block so
        // it can still be identified as the head even if it doesn't have any votes.
        self.proto_array.process_block::<E>(
            ProtoBlock {
                slot: block.slot(),
                root: block_root,
                parent_root: Some(block.parent_root()),
                target_root,
                current_epoch_shuffling_id: AttestationShufflingId::new(
                    block_root,
                    state,
                    RelativeEpoch::Current,
                )
                .map_err(Error::BeaconStateError)?,
                next_epoch_shuffling_id: AttestationShufflingId::new(
                    block_root,
                    state,
                    RelativeEpoch::Next,
                )
                .map_err(Error::BeaconStateError)?,
                state_root: block.state_root(),
                justified_checkpoint: state.current_justified_checkpoint(),
                finalized_checkpoint: state.finalized_checkpoint(),
                execution_status,
                unrealized_justified_checkpoint: Some(unrealized_justified_checkpoint),
                unrealized_finalized_checkpoint: Some(unrealized_finalized_checkpoint),
                execution_payload_parent_hash,
                execution_payload_block_hash,
                proposer_index: Some(block.proposer_index()),
            },
            current_slot,
            spec,
            block_delay,
        )?;

        Ok(())
    }

    /// Update checkpoints in store if necessary
    fn update_checkpoints(
        &mut self,
        justified_checkpoint: Checkpoint,
        finalized_checkpoint: Checkpoint,
        justified_state_root_producer: impl FnOnce() -> Result<Hash256, Error<T::Error>>,
    ) -> Result<(), Error<T::Error>> {
        // Update justified checkpoint.
        if justified_checkpoint.epoch > self.fc_store.justified_checkpoint().epoch {
            let justified_state_root = justified_state_root_producer()?;
            self.fc_store
                .set_justified_checkpoint(justified_checkpoint, justified_state_root)
                .map_err(Error::UnableToSetJustifiedCheckpoint)?;
        }

        // Update finalized checkpoint.
        if finalized_checkpoint.epoch > self.fc_store.finalized_checkpoint().epoch {
            self.fc_store.set_finalized_checkpoint(finalized_checkpoint);
        }

        Ok(())
    }

    /// Validates the `epoch` against the current time according to the fork choice store.
    ///
    /// ## Specification
    ///
    /// Equivalent to:
    ///
    /// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#validate_target_epoch_against_current_time
    fn validate_target_epoch_against_current_time(
        &self,
        target_epoch: Epoch,
    ) -> Result<(), InvalidAttestation> {
        let slot_now = self.fc_store.get_current_slot();
        let epoch_now = slot_now.epoch(E::slots_per_epoch());

        // Attestation must be from the current or previous epoch.
        if target_epoch > epoch_now {
            return Err(InvalidAttestation::FutureEpoch {
                attestation_epoch: target_epoch,
                current_epoch: epoch_now,
            });
        } else if target_epoch + 1 < epoch_now {
            return Err(InvalidAttestation::PastEpoch {
                attestation_epoch: target_epoch,
                current_epoch: epoch_now,
            });
        }
        Ok(())
    }

    /// Validates the `indexed_attestation` for application to fork choice.
    ///
    /// ## Specification
    ///
    /// Equivalent to:
    ///
    /// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#validate_on_attestation
    fn validate_on_attestation(
        &self,
        indexed_attestation: IndexedAttestationRef<E>,
        is_from_block: AttestationFromBlock,
        spec: &ChainSpec,
    ) -> Result<(), InvalidAttestation> {
        // There is no point in processing an attestation with an empty bitfield. Reject
        // it immediately.
        //
        // This is not in the specification, however it should be transparent to other nodes. We
        // return early here to avoid wasting precious resources verifying the rest of it.
        if indexed_attestation.attesting_indices_is_empty() {
            return Err(InvalidAttestation::EmptyAggregationBitfield);
        }

        let target = indexed_attestation.data().target;

        if matches!(is_from_block, AttestationFromBlock::False) {
            self.validate_target_epoch_against_current_time(target.epoch)?;
        }

        if target.epoch != indexed_attestation.data().slot.epoch(E::slots_per_epoch()) {
            return Err(InvalidAttestation::BadTargetEpoch {
                target: target.epoch,
                slot: indexed_attestation.data().slot,
            });
        }

        // Attestation target must be for a known block.
        //
        // We do not delay the block for later processing to reduce complexity and DoS attack
        // surface.
        if !self.proto_array.contains_block(&target.root) {
            return Err(InvalidAttestation::UnknownTargetRoot(target.root));
        }

        // Load the block for `attestation.data.beacon_block_root`.
        //
        // This indirectly checks to see if the `attestation.data.beacon_block_root` is in our fork
        // choice. Any known, non-finalized block should be in fork choice, so this check
        // immediately filters out attestations that attest to a block that has not been processed.
        //
        // Attestations must be for a known block. If the block is unknown, we simply drop the
        // attestation and do not delay consideration for later.
        let block = self
            .proto_array
            .get_block(&indexed_attestation.data().beacon_block_root)
            .ok_or(InvalidAttestation::UnknownHeadBlock {
                beacon_block_root: indexed_attestation.data().beacon_block_root,
            })?;

        // If an attestation points to a block that is from an earlier slot than the attestation,
        // then all slots between the block and attestation must be skipped. Therefore if the block
        // is from a prior epoch to the attestation, then the target root must be equal to the root
        // of the block that is being attested to.
        let expected_target = if target.epoch > block.slot.epoch(E::slots_per_epoch()) {
            indexed_attestation.data().beacon_block_root
        } else {
            block.target_root
        };

        if expected_target != target.root {
            return Err(InvalidAttestation::InvalidTarget {
                attestation: target.root,
                local: expected_target,
            });
        }

        // Attestations must not be for blocks in the future. If this is the case, the attestation
        // should not be considered.
        if block.slot > indexed_attestation.data().slot {
            return Err(InvalidAttestation::AttestsToFutureBlock {
                block: block.slot,
                attestation: indexed_attestation.data().slot,
            });
        }

        if spec
            .fork_name_at_slot::<E>(indexed_attestation.data().slot)
            .gloas_enabled()
        {
            let index = indexed_attestation.data().index;

            // Post-Gloas: attestation index must be 0 or 1.
            if index > 1 {
                return Err(InvalidAttestation::InvalidAttestationIndex { index });
            }

            // Same-slot attestations must have index == 0.
            if indexed_attestation.data().slot == block.slot && index != 0 {
                return Err(InvalidAttestation::InvalidSameSlotAttestationIndex {
                    slot: block.slot,
                });
            }

            // index == 1 (payload_present) requires the block's payload to have been received.
            // TODO(gloas): could optimise by adding `payload_received` to `Block`
            if index == 1
                && !self
                    .proto_array
                    .is_payload_received(&indexed_attestation.data().beacon_block_root)
            {
                return Err(InvalidAttestation::PayloadNotReceived {
                    beacon_block_root: indexed_attestation.data().beacon_block_root,
                });
            }
        }

        Ok(())
    }

    /// Validates a payload attestation for application to fork choice.
    fn validate_on_payload_attestation(
        &self,
        indexed_payload_attestation: &IndexedPayloadAttestation<E>,
    ) -> Result<(), InvalidPayloadAttestation> {
        // This check is from `is_valid_indexed_payload_attestation`, but we do it immediately to
        // avoid wasting time on junk attestations.
        if indexed_payload_attestation.attesting_indices.is_empty() {
            return Err(InvalidPayloadAttestation::EmptyAggregationBitfield);
        }

        // PTC attestation must be for a known block. If block is unknown, delay consideration until
        // the block is found (responsibility of caller).
        let block = self
            .proto_array
            .get_block(&indexed_payload_attestation.data.beacon_block_root)
            .ok_or(InvalidPayloadAttestation::UnknownHeadBlock {
                beacon_block_root: indexed_payload_attestation.data.beacon_block_root,
            })?;

        // Not strictly part of the spec, but payload attestations to future slots are MORE INVALID
        // than payload attestations to blocks at previous slots.
        if block.slot > indexed_payload_attestation.data.slot {
            return Err(InvalidPayloadAttestation::AttestsToFutureBlock {
                block: block.slot,
                attestation: indexed_payload_attestation.data.slot,
            });
        }

        Ok(())
    }

    /// Register `attestation` with the fork choice DAG so that it may influence future calls to
    /// `Self::get_head`.
    ///
    /// ## Specification
    ///
    /// Approximates:
    ///
    /// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#on_attestation
    ///
    /// It only approximates the specification since it does not perform
    /// `is_valid_indexed_attestation` since that should already have been called upstream and it's
    /// too expensive to call again.
    ///
    /// ## Notes:
    ///
    /// The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it
    /// will not be run here.
    pub fn on_attestation(
        &mut self,
        system_time_current_slot: Slot,
        attestation: IndexedAttestationRef<E>,
        is_from_block: AttestationFromBlock,
        spec: &ChainSpec,
    ) -> Result<(), Error<T::Error>> {
        let _timer = metrics::start_timer(&metrics::FORK_CHOICE_ON_ATTESTATION_TIMES);

        self.update_time(system_time_current_slot)?;

        // Ignore any attestations to the zero hash.
        //
        // This is an edge case that results from the spec aliasing the zero hash to the genesis
        // block. Attesters may attest to the zero hash if they have never seen a block.
        //
        // We have two options here:
        //
        //  1. Apply all zero-hash attestations to the genesis block.
        //  2. Ignore all attestations to the zero hash.
        //
        // (1) becomes weird once we hit finality and fork choice drops the genesis block. (2) is
        // fine because votes to the genesis block are not useful; all validators implicitly attest
        // to genesis just by being present in the chain.
        if attestation.data().beacon_block_root == Hash256::zero() {
            return Ok(());
        }

        self.validate_on_attestation(attestation, is_from_block, spec)?;

        // Per Gloas spec: `payload_present = attestation.data.index == 1`.
        let payload_present = spec
            .fork_name_at_slot::<E>(attestation.data().slot)
            .gloas_enabled()
            && attestation.data().index == 1;

        if attestation.data().slot < self.fc_store.get_current_slot() {
            for validator_index in attestation.attesting_indices_iter() {
                self.proto_array.process_attestation(
                    *validator_index as usize,
                    attestation.data().beacon_block_root,
                    attestation.data().slot,
                    payload_present,
                )?;
            }
        } else {
            // The spec declares:
            //
            // ```
            // Attestations can only affect the fork choice of subsequent slots.
            // Delay consideration in the fork choice until their slot is in the past.
            // ```
            self.queued_attestations
                .push(QueuedAttestation::from(attestation));
        }

        Ok(())
    }

    /// Register a payload attestation with the fork choice DAG.
    ///
    /// `ptc` is the PTC committee for the attestation's slot: a list of validator indices
    /// ordered by committee position. Each attesting validator index is resolved to its
    /// position within `ptc` (its `ptc_index`) before being applied to the proto-array.
    pub fn on_payload_attestation(
        &mut self,
        system_time_current_slot: Slot,
        payload_attestation: &IndexedPayloadAttestation<E>,
        is_from_block: AttestationFromBlock,
        ptc: &[usize],
    ) -> Result<(), Error<T::Error>> {
        self.update_time(system_time_current_slot)?;

        if payload_attestation.data.beacon_block_root.is_zero() {
            return Ok(());
        }

        // TODO(gloas): Should ignore wrong-slot payload attestations at the caller, they could
        // have been processed at the correct slot when received on gossip, but then have the
        // wrong-slot by the time they make it to here (TOCTOU).
        // TODO(gloas): Consider inlining validate_on_payload_attestation here to look more like the spec.
        self.validate_on_payload_attestation(payload_attestation)?;

        // PTC votes can only change the vote for their assigned beacon block, return early otherwise.
        let block = self
            .proto_array
            .get_block(&payload_attestation.data.beacon_block_root)
            .ok_or(InvalidPayloadAttestation::UnknownHeadBlock {
                beacon_block_root: payload_attestation.data.beacon_block_root,
            })?;
        if block.slot != payload_attestation.data.slot {
            return Ok(());
        }

        // Gossip payload attestations must be for the current slot.
        if matches!(is_from_block, AttestationFromBlock::False)
            && payload_attestation.data.slot != self.fc_store.get_current_slot()
        {
            return Err(
                InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot {
                    attestation_slot: payload_attestation.data.slot,
                    current_slot: self.fc_store.get_current_slot(),
                }
                .into(),
            );
        }

        // Resolve validator indices to all PTC committee positions. A validator may
        // appear multiple times in the PTC committee.
        let mut ptc_indices = vec![];
        let mut validators_found = 0;
        for validator_index in payload_attestation.attesting_indices.iter() {
            let mut found = false;
            for (ptc_index, &ptc_validator_index) in ptc.iter().enumerate() {
                if ptc_validator_index == *validator_index as usize {
                    ptc_indices.push(ptc_index);
                    found = true;
                }
            }
            if found {
                validators_found += 1;
            }
        }

        // Check that all the attesters are in the PTC
        if validators_found != payload_attestation.attesting_indices.len() {
            return Err(
                InvalidPayloadAttestation::PayloadAttestationAttestersNotInPtc {
                    attesting_indices_len: payload_attestation.attesting_indices.len(),
                    attesting_indices_in_ptc: validators_found,
                }
                .into(),
            );
        }

        for &ptc_index in &ptc_indices {
            self.proto_array.process_payload_attestation(
                payload_attestation.data.beacon_block_root,
                ptc_index,
                payload_attestation.data.payload_present,
                payload_attestation.data.blob_data_available,
            )?;
        }

        Ok(())
    }

    /// Apply an attester slashing to fork choice.
    ///
    /// We assume that the attester slashing provided to this function has already been verified.
    pub fn on_attester_slashing(&mut self, slashing: AttesterSlashingRef<'_, E>) {
        let _timer = metrics::start_timer(&metrics::FORK_CHOICE_ON_ATTESTER_SLASHING_TIMES);

        let attesting_indices_set = |att: IndexedAttestationRef<'_, E>| {
            att.attesting_indices_iter()
                .copied()
                .collect::<BTreeSet<_>>()
        };
        let att1_indices = attesting_indices_set(slashing.attestation_1());
        let att2_indices = attesting_indices_set(slashing.attestation_2());
        self.fc_store
            .extend_equivocating_indices(att1_indices.intersection(&att2_indices).copied());
    }

    /// Call `on_tick` for all slots between `fc_store.get_current_slot()` and the provided
    /// `current_slot`. Returns the value of `self.fc_store.get_current_slot`.
    pub fn update_time(&mut self, current_slot: Slot) -> Result<Slot, Error<T::Error>> {
        while self.fc_store.get_current_slot() < current_slot {
            let previous_slot = self.fc_store.get_current_slot();
            // Note: we are relying upon `on_tick` to update `fc_store.time` to ensure we don't
            // get stuck in a loop.
            self.on_tick(previous_slot + 1)?
        }

        // Process any attestations that might now be eligible.
        self.process_attestation_queue()?;

        Ok(self.fc_store.get_current_slot())
    }

    /// Called whenever the current time increases.
    ///
    /// ## Specification
    ///
    /// Equivalent to:
    ///
    /// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#on_tick
    fn on_tick(&mut self, time: Slot) -> Result<(), Error<T::Error>> {
        let store = &mut self.fc_store;
        let previous_slot = store.get_current_slot();

        if time > previous_slot + 1 {
            return Err(Error::InconsistentOnTick {
                previous_slot,
                time,
            });
        }

        // Update store time.
        store.set_current_slot(time);

        let current_slot = store.get_current_slot();

        // Reset proposer boost if this is a new slot.
        if current_slot > previous_slot {
            store.set_proposer_boost_root(Hash256::zero());
        }

        // Not a new epoch, return.
        if !(current_slot > previous_slot
            && compute_slots_since_epoch_start::<E>(current_slot) == 0)
        {
            return Ok(());
        }

        // Update the justified/finalized checkpoints based upon the
        // best-observed unrealized justification/finality.
        let unrealized_justified_checkpoint = *self.fc_store.unrealized_justified_checkpoint();
        let unrealized_justified_state_root = self.fc_store.unrealized_justified_state_root();
        let unrealized_finalized_checkpoint = *self.fc_store.unrealized_finalized_checkpoint();
        self.pull_up_store_checkpoints(
            unrealized_justified_checkpoint,
            unrealized_finalized_checkpoint,
            || Ok(unrealized_justified_state_root),
        )?;

        Ok(())
    }

    fn pull_up_store_checkpoints(
        &mut self,
        unrealized_justified_checkpoint: Checkpoint,
        unrealized_finalized_checkpoint: Checkpoint,
        unrealized_justified_state_root_producer: impl FnOnce() -> Result<Hash256, Error<T::Error>>,
    ) -> Result<(), Error<T::Error>> {
        self.update_checkpoints(
            unrealized_justified_checkpoint,
            unrealized_finalized_checkpoint,
            unrealized_justified_state_root_producer,
        )
    }

    /// Processes and removes from the queue any queued attestations which may now be eligible for
    /// processing due to the slot clock incrementing.
    fn process_attestation_queue(&mut self) -> Result<(), Error<T::Error>> {
        for attestation in dequeue_attestations(
            self.fc_store.get_current_slot(),
            &mut self.queued_attestations,
        ) {
            for validator_index in attestation.attesting_indices.iter() {
                self.proto_array.process_attestation(
                    *validator_index as usize,
                    attestation.block_root,
                    attestation.slot,
                    attestation.payload_present,
                )?;
            }
        }

        Ok(())
    }

    /// Returns `true` if the block is known **and** a descendant of the finalized root.
    pub fn contains_block(&self, block_root: &Hash256) -> bool {
        self.proto_array.contains_block(block_root)
            && self.is_finalized_checkpoint_or_descendant(*block_root)
    }

    /// Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root.
    pub fn get_block(&self, block_root: &Hash256) -> Option<ProtoBlock> {
        if self.is_finalized_checkpoint_or_descendant(*block_root) {
            self.proto_array.get_block(block_root)
        } else {
            None
        }
    }

    /// Returns whether the execution payload for a block has been received.
    ///
    /// Returns `false` for unknown blocks and pre-Gloas nodes.
    pub fn is_payload_received(&self, block_root: &Hash256) -> bool {
        self.proto_array.is_payload_received(block_root)
            && self.is_finalized_checkpoint_or_descendant(*block_root)
    }

    /// Returns `true` if the block's parent is imported (and, for a post-Gloas FULL child, its
    /// parent's payload is imported too). See [`Self::get_parent_import_status`].
    pub fn is_parent_imported(&self, block: &SignedBeaconBlock<E>) -> bool {
        matches!(
            self.get_parent_import_status(block),
            ParentImportStatus::Imported(_)
        )
    }

    /// Returns the import status of the parent of `block`.
    ///
    /// A post-Gloas FULL child also requires the parent's payload (committed to by the child's bid)
    /// to have been received by fork choice.
    pub fn get_parent_import_status(&self, block: &SignedBeaconBlock<E>) -> ParentImportStatus {
        if let Some(parent_block) = self.get_block(&block.parent_root()) {
            let Some(parent_block_hash) = parent_block.execution_payload_block_hash else {
                // Pre-Gloas parent: payload is embedded in the block, so treat as imported.
                return ParentImportStatus::Imported(parent_block);
            };
            if block.is_parent_block_full(parent_block_hash)
                && !self.is_payload_received(&block.parent_root())
            {
                ParentImportStatus::UnknownPayload
            } else {
                ParentImportStatus::Imported(parent_block)
            }
        } else {
            ParentImportStatus::UnknownBlock
        }
    }

    /// Called by the proposer to decide whether to build on the full or empty parent.
    pub fn should_build_on_full(
        &self,
        block_root: &Hash256,
        parent_payload_status: PayloadStatus,
    ) -> Result<bool, Error<T::Error>> {
        self.proto_array
            .should_build_on_full::<E>(block_root, parent_payload_status)
            .map_err(Error::ProtoArrayStringError)
    }

    /// Returns whether the proposer should extend the execution payload chain of the given block.
    pub fn should_extend_payload(&self, block_root: &Hash256) -> Result<bool, Error<T::Error>> {
        let proposer_boost_root = self.fc_store.proposer_boost_root();
        self.proto_array
            .should_extend_payload::<E>(block_root, proposer_boost_root)
            .map_err(Error::ProtoArrayStringError)
    }

    /// Returns an `ExecutionStatus` if the block is known **and** a descendant of the finalized root.
    pub fn get_block_execution_status(&self, block_root: &Hash256) -> Option<ExecutionStatus> {
        if self.is_finalized_checkpoint_or_descendant(*block_root) {
            self.proto_array.get_block_execution_status(block_root)
        } else {
            None
        }
    }

    /// Returns the canonical payload status of a block. See
    /// `ProtoArrayForkChoice::get_canonical_payload_status`.
    pub fn get_canonical_payload_status(
        &self,
        block_root: &Hash256,
        spec: &ChainSpec,
    ) -> Result<PayloadStatus, Error<T::Error>> {
        if self.is_finalized_checkpoint_or_descendant(*block_root) {
            let current_slot = self.fc_store.get_current_slot();
            let proposer_boost_root = self.fc_store.proposer_boost_root();
            self.proto_array
                .get_canonical_payload_status::<E>(
                    block_root,
                    current_slot,
                    proposer_boost_root,
                    spec,
                )
                .map_err(Error::ProtoArrayError)
        } else {
            Err(Error::DoesNotDescendFromFinalizedCheckpoint)
        }
    }

    /// Returns the weight for the given block root.
    pub fn get_block_weight(&self, block_root: &Hash256) -> Option<u64> {
        self.proto_array.get_weight(block_root)
    }

    /// Returns the `ProtoBlock` for the justified checkpoint.
    ///
    /// ## Notes
    ///
    /// This does *not* return the "best justified checkpoint". It returns the justified checkpoint
    /// that is used for computing balances.
    pub fn get_justified_block(&self) -> Result<ProtoBlock, Error<T::Error>> {
        let justified_checkpoint = self.justified_checkpoint();
        self.get_block(&justified_checkpoint.root)
            .ok_or(Error::MissingJustifiedBlock {
                justified_checkpoint,
            })
    }

    /// Returns the `ProtoBlock` for the finalized checkpoint.
    pub fn get_finalized_block(&self) -> Result<ProtoBlock, Error<T::Error>> {
        let finalized_checkpoint = self.finalized_checkpoint();
        self.get_block(&finalized_checkpoint.root)
            .ok_or(Error::MissingFinalizedBlock {
                finalized_checkpoint,
            })
    }

    /// Return `true` if `block_root` is equal to the finalized checkpoint, or a known descendant of it.
    pub fn is_finalized_checkpoint_or_descendant(&self, block_root: Hash256) -> bool {
        self.proto_array
            .is_finalized_checkpoint_or_descendant::<E>(block_root, self.finalized_checkpoint())
    }

    pub fn is_descendant(&self, ancestor_root: Hash256, descendant_root: Hash256) -> bool {
        self.proto_array
            .is_descendant(ancestor_root, descendant_root)
    }

    /// Returns `Ok(true)` if `block_root` has been imported optimistically or deemed invalid.
    ///
    /// Returns `Ok(false)` if `block_root`'s execution payload has been elected as fully VALID, if
    /// it is a pre-Bellatrix block or if it is before the PoW terminal block.
    ///
    /// In the case where the block could not be found in fork-choice, it returns the
    /// `execution_status` of the current finalized block.
    ///
    /// This function assumes the `block_root` exists.
    pub fn is_optimistic_or_invalid_block(
        &self,
        block_root: &Hash256,
    ) -> Result<bool, Error<T::Error>> {
        if let Some(status) = self.get_block_execution_status(block_root) {
            Ok(status.is_optimistic_or_invalid())
        } else {
            Ok(self
                .get_finalized_block()?
                .execution_status
                .is_optimistic_or_invalid())
        }
    }

    /// The same as `is_optimistic_block` but does not fallback to `self.get_finalized_block`
    /// when the block cannot be found.
    ///
    /// Intended to be used when checking if the head has been imported optimistically or is
    /// invalid.
    pub fn is_optimistic_or_invalid_block_no_fallback(
        &self,
        block_root: &Hash256,
    ) -> Result<bool, Error<T::Error>> {
        if let Some(status) = self.get_block_execution_status(block_root) {
            Ok(status.is_optimistic_or_invalid())
        } else {
            Err(Error::MissingProtoArrayBlock(*block_root))
        }
    }

    /// Return the current finalized checkpoint.
    pub fn finalized_checkpoint(&self) -> Checkpoint {
        *self.fc_store.finalized_checkpoint()
    }

    /// Return the justified checkpoint.
    pub fn justified_checkpoint(&self) -> Checkpoint {
        *self.fc_store.justified_checkpoint()
    }

    pub fn unrealized_justified_checkpoint(&self) -> Checkpoint {
        *self.fc_store.unrealized_justified_checkpoint()
    }

    pub fn unrealized_finalized_checkpoint(&self) -> Checkpoint {
        *self.fc_store.unrealized_finalized_checkpoint()
    }

    /// Returns the latest message for a given validator, if any.
    ///
    /// Returns `block_root, block_slot, payload_present`.
    ///
    /// ## Notes
    ///
    /// It may be prudent to call `Self::update_time` before calling this function,
    /// since some attestations might be queued and awaiting processing.
    ///
    /// This function is only used in tests.
    pub fn latest_message(&self, validator_index: usize) -> Option<LatestMessage> {
        self.proto_array.latest_message(validator_index)
    }

    /// Returns a reference to the underlying fork choice DAG.
    pub fn proto_array(&self) -> &ProtoArrayForkChoice {
        &self.proto_array
    }

    /// Returns a mutable reference to `proto_array`.
    /// Should only be used in testing.
    pub fn proto_array_mut(&mut self) -> &mut ProtoArrayForkChoice {
        &mut self.proto_array
    }

    /// Returns a reference to the underlying `fc_store`.
    pub fn fc_store(&self) -> &T {
        &self.fc_store
    }

    /// Returns a reference to the currently queued attestations.
    pub fn queued_attestations(&self) -> &[QueuedAttestation] {
        &self.queued_attestations
    }

    /// Returns the store's `proposer_boost_root`.
    pub fn proposer_boost_root(&self) -> Hash256 {
        self.fc_store.proposer_boost_root()
    }

    /// Prunes the underlying fork choice DAG.
    pub fn prune(&mut self) -> Result<(), Error<T::Error>> {
        let finalized_root = self.fc_store.finalized_checkpoint().root;

        self.proto_array
            .maybe_prune(finalized_root)
            .map_err(Into::into)
    }

    /// Instantiate `Self` from some `PersistedForkChoice` generated by a earlier call to
    /// `Self::to_persisted`.
    pub fn proto_array_from_persisted(
        persisted_proto_array: proto_array::core::SszContainer,
        justified_balances: JustifiedBalances,
        reset_payload_statuses: ResetPayloadStatuses,
    ) -> Result<ProtoArrayForkChoice, Error<T::Error>> {
        let mut proto_array = ProtoArrayForkChoice::from_container(
            persisted_proto_array.clone(),
            justified_balances.clone(),
        )
        .map_err(Error::InvalidProtoArrayBytes)?;
        let contains_invalid_payloads = proto_array.contains_invalid_payloads();

        debug!(
            ?reset_payload_statuses,
            contains_invalid_payloads, "Restoring fork choice from persisted"
        );

        // Exit early if there are no "invalid" payloads, if requested.
        if matches!(
            reset_payload_statuses,
            ResetPayloadStatuses::OnlyWithInvalidPayload
        ) && !contains_invalid_payloads
        {
            return Ok(proto_array);
        }

        // Reset all blocks back to being "optimistic". This helps recover from an EL consensus
        // fault where an invalid payload becomes valid.
        if let Err(e) = proto_array.set_all_blocks_to_optimistic::<E>() {
            // If there is an error resetting the optimistic status then log loudly and revert
            // back to a proto-array which does not have the reset applied. This indicates a
            // significant error in Lighthouse and warrants detailed investigation.
            crit!(
                error = ?e,
                info = "please report this error",
                "Failed to reset payload statuses"
            );
            ProtoArrayForkChoice::from_container(persisted_proto_array, justified_balances)
                .map_err(Error::InvalidProtoArrayBytes)
        } else {
            debug!("Successfully reset all payload statuses");
            Ok(proto_array)
        }
    }

    /// Instantiate `Self` from some `PersistedForkChoice` generated by a earlier call to
    /// `Self::to_persisted`.
    pub fn from_persisted(
        persisted: PersistedForkChoice,
        reset_payload_statuses: ResetPayloadStatuses,
        fc_store: T,
        spec: &ChainSpec,
    ) -> Result<Self, Error<T::Error>> {
        let justified_balances = fc_store.justified_balances().clone();
        let proto_array = Self::proto_array_from_persisted(
            persisted.proto_array,
            justified_balances,
            reset_payload_statuses,
        )?;

        let current_slot = fc_store.get_current_slot();

        let mut fork_choice = Self {
            fc_store,
            proto_array,
            queued_attestations: vec![],
            // Will be updated in the following call to `Self::get_head`.
            forkchoice_update_parameters: ForkchoiceUpdateParameters {
                head_hash: None,
                justified_hash: None,
                finalized_hash: None,
                // Will be updated in the following call to `Self::get_head`.
                head_root: Hash256::zero(),
            },
            _phantom: PhantomData,
        };

        // If a call to `get_head` fails, the only known cause is because the only head with viable
        // FFG properties is has an invalid payload. In this scenario, set all the payloads back to
        // an optimistic status so that we can have a head to start from.
        if let Err(e) = fork_choice.get_head(current_slot, spec) {
            warn!(
                info = "resetting all payload statuses and retrying",
                error = ?e,
                "Could not find head on persisted FC"
            );
            // Although we may have already made this call whilst loading `proto_array`, try it
            // again since we may have mutated the `proto_array` during `get_head` and therefore may
            // get a different result.
            fork_choice
                .proto_array
                .set_all_blocks_to_optimistic::<E>()?;
            // If the second attempt at finding a head fails, return an error since we do not
            // expect this scenario.
            fork_choice.get_head(current_slot, spec)?;
        }

        Ok(fork_choice)
    }

    /// Takes a snapshot of `Self` and stores it in `PersistedForkChoice`, allowing this struct to
    /// be instantiated again later.
    pub fn to_persisted(&self) -> PersistedForkChoice {
        PersistedForkChoice {
            proto_array: self.proto_array().as_ssz_container(),
        }
    }

    /// Update the global metrics `DEFAULT_REGISTRY` with info from the fork choice
    pub fn scrape_for_metrics(&self) {
        scrape_for_metrics(self);
    }
}

/// Helper struct that is used to encode/decode the state of the `ForkChoice` as SSZ bytes.
///
/// This is used when persisting the state of the fork choice to disk.
#[superstruct(
    variants(V28, V29),
    variant_attributes(derive(Encode, Decode, Clone)),
    no_enum
)]
pub struct PersistedForkChoice {
    #[superstruct(only(V28))]
    pub proto_array_v28: proto_array::core::SszContainerV28,
    #[superstruct(only(V29))]
    pub proto_array: proto_array::core::SszContainerV29,
    #[superstruct(only(V28))]
    pub queued_attestations_v28: Vec<QueuedAttestationV28>,
}

pub type PersistedForkChoice = PersistedForkChoiceV29;

impl From<PersistedForkChoiceV28> for PersistedForkChoiceV29 {
    fn from(v28: PersistedForkChoiceV28) -> Self {
        Self {
            proto_array: v28.proto_array_v28.into(),
        }
    }
}

impl From<PersistedForkChoiceV29> for PersistedForkChoiceV28 {
    fn from(v29: PersistedForkChoiceV29) -> Self {
        Self {
            proto_array_v28: v29.proto_array.into(),
            queued_attestations_v28: vec![],
        }
    }
}

#[cfg(test)]
mod tests {
    use types::MainnetEthSpec;

    use super::*;

    type E = MainnetEthSpec;

    #[test]
    fn slots_since_epoch_start() {
        for epoch in 0..3 {
            for slot in 0..E::slots_per_epoch() {
                let input = epoch * E::slots_per_epoch() + slot;
                assert_eq!(compute_slots_since_epoch_start::<E>(Slot::new(input)), slot)
            }
        }
    }

    #[test]
    fn start_slot_at_epoch() {
        for epoch in 0..3 {
            assert_eq!(
                compute_start_slot_at_epoch::<E>(Epoch::new(epoch)),
                epoch * E::slots_per_epoch()
            )
        }
    }

    fn get_queued_attestations() -> Vec<QueuedAttestation> {
        (1..4)
            .map(|i| QueuedAttestation {
                slot: Slot::new(i),
                attesting_indices: vec![],
                block_root: Hash256::zero(),
                target_epoch: Epoch::new(0),
                payload_present: false,
            })
            .collect()
    }

    fn get_slots(queued_attestations: &[QueuedAttestation]) -> Vec<u64> {
        queued_attestations.iter().map(|a| a.slot.into()).collect()
    }

    fn test_queued_attestations(current_time: Slot) -> (Vec<u64>, Vec<u64>) {
        let mut queued = get_queued_attestations();
        let dequeued = dequeue_attestations(current_time, &mut queued);

        (get_slots(&queued), get_slots(&dequeued))
    }

    #[test]
    fn dequeing_attestations() {
        let (queued, dequeued) = test_queued_attestations(Slot::new(0));
        assert_eq!(queued, vec![1, 2, 3]);
        assert!(dequeued.is_empty());

        let (queued, dequeued) = test_queued_attestations(Slot::new(1));
        assert_eq!(queued, vec![1, 2, 3]);
        assert!(dequeued.is_empty());

        let (queued, dequeued) = test_queued_attestations(Slot::new(2));
        assert_eq!(queued, vec![2, 3]);
        assert_eq!(dequeued, vec![1]);

        let (queued, dequeued) = test_queued_attestations(Slot::new(3));
        assert_eq!(queued, vec![3]);
        assert_eq!(dequeued, vec![1, 2]);

        let (queued, dequeued) = test_queued_attestations(Slot::new(4));
        assert!(queued.is_empty());
        assert_eq!(dequeued, vec![1, 2, 3]);
    }
}
