use crate::block_verification_types::{AsBlock, AvailableBlockData, LookupBlock, RangeSyncBlock};
use crate::custody_context::NodeCustodyType;
use crate::data_availability_checker::DataAvailabilityChecker;
use crate::graffiti_calculator::GraffitiSettings;
use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sidecars_gloas};
use crate::observed_operations::ObservationOutcome;
use crate::payload_envelope_verification::AvailableEnvelope;
pub use crate::persisted_beacon_chain::PersistedBeaconChain;
use crate::{BeaconBlockResponseWrapper, CustodyContext, get_block_root};
use crate::{
    BeaconChain, BeaconChainTypes, BlockError, ChainConfig, ServerSentEventHandler,
    StateSkipConfig,
    builder::{BeaconChainBuilder, Witness},
};
pub use crate::{
    BeaconChainError, NotifyExecutionLayer, ProduceBlockVerification,
    beacon_chain::{BEACON_CHAIN_DB_KEY, FORK_CHOICE_DB_KEY, OP_POOL_DB_KEY},
    migrate::MigratorConfig,
    single_attestation::single_attestation_to_attestation,
    sync_committee_verification::Error as SyncCommitteeError,
    validator_monitor::{ValidatorMonitor, ValidatorMonitorConfig},
};
#[cfg(feature = "arbitrary")]
use arbitrary::Arbitrary;
use bls::get_withdrawal_credentials;
use bls::{
    AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey, Signature, SignatureBytes,
};
use eth2::types::{GraffitiPolicy, SignedBlockContentsTuple};
use execution_layer::test_utils::generate_genesis_header;
use execution_layer::{
    ExecutionLayer, NewPayloadRequest, NewPayloadRequestGloas,
    auth::JwtKey,
    test_utils::{DEFAULT_JWT_SECRET, ExecutionBlockGenerator, MockBuilder, MockExecutionLayer},
};
use fixed_bytes::FixedBytesExtended;
use futures::channel::mpsc::Receiver;
pub use genesis::{DEFAULT_ETH1_BLOCK_HASH, InteropGenesisBuilder};
use int_to_bytes::int_to_bytes32;
use kzg::Kzg;
use kzg::trusted_setup::get_trusted_setup;
use logging::create_test_tracing_subscriber;
use merkle_proof::MerkleTree;
use operation_pool::ReceivedPreCapella;
use parking_lot::{Mutex, RwLockWriteGuard};
use rand::Rng;
use rand::SeedableRng;
use rand::rngs::StdRng;
use rand::seq::SliceRandom;
use rayon::prelude::*;
use sensitive_url::SensitiveUrl;
use slot_clock::{SlotClock, TestingSlotClock};
use ssz_types::{RuntimeVariableList, VariableList};
use state_processing::ConsensusContext;
use state_processing::per_block_processing::compute_timestamp_at_slot;
use state_processing::per_block_processing::{
    BlockSignatureStrategy, VerifyBlockRoot, deneb::kzg_commitment_to_versioned_hash,
    per_block_processing,
};
use state_processing::state_advance::complete_state_advance;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::str::FromStr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use store::database::interface::BeaconNodeBackend;
use store::{HotColdDB, ItemStore, MemoryStore, config::StoreConfig};
use task_executor::TaskExecutor;
use task_executor::{ShutdownReason, test_utils::TestRuntime};
use tracing::debug;
use tree_hash::TreeHash;
use typenum::U4294967296;
use types::attestation::IndexedAttestationBase;
use types::data::CustodyIndex;
use types::execution::BlockProductionVersion;
pub use types::test_utils::generate_deterministic_keypairs;
use types::*;

// 4th September 2019
pub const HARNESS_GENESIS_TIME: u64 = 1_567_552_690;
// Environment variable to read if `fork_from_env` feature is enabled.
pub const FORK_NAME_ENV_VAR: &str = "FORK_NAME";

// Pre-computed data column sidecar using a single static blob from:
// `beacon_node/execution_layer/src/test_utils/fixtures/mainnet/test_blobs_bundle.ssz`
pub const TEST_DATA_COLUMN_SIDECARS_SSZ: &[u8] =
    include_bytes!("test_utils/fixtures/test_data_column_sidecars.ssz");
pub const TEST_DATA_COLUMN_SIDECARS_GLOAS_SSZ: &[u8] =
    include_bytes!("test_utils/fixtures/test_data_column_sidecars_gloas.ssz");

// Default target aggregators to set during testing, this ensures an aggregator at each slot.
//
// You should mutate the `ChainSpec` prior to initialising the harness if you would like to use
// a different value.
pub const DEFAULT_TARGET_AGGREGATORS: u64 = u64::MAX;

// Minimum and maximum number of blobs to generate in each slot when using the `NumBlobs::Random` option (default).
#[cfg(feature = "arbitrary")]
const DEFAULT_MIN_BLOBS: usize = 1;
#[cfg(feature = "arbitrary")]
const DEFAULT_MAX_BLOBS: usize = 2;

static KZG: LazyLock<Arc<Kzg>> = LazyLock::new(|| {
    let kzg = Kzg::new_from_trusted_setup(&get_trusted_setup()).expect("should create kzg");
    Arc::new(kzg)
});

static KZG_NO_PRECOMP: LazyLock<Arc<Kzg>> = LazyLock::new(|| {
    let kzg =
        Kzg::new_from_trusted_setup_no_precomp(&get_trusted_setup()).expect("should create kzg");
    Arc::new(kzg)
});

pub fn get_kzg(spec: &ChainSpec) -> Arc<Kzg> {
    if spec.fulu_fork_epoch.is_some() {
        KZG.clone()
    } else {
        KZG_NO_PRECOMP.clone()
    }
}

pub type BaseHarnessType<E, THotStore, TColdStore> =
    Witness<TestingSlotClock, E, THotStore, TColdStore>;

pub type DiskHarnessType<E> = BaseHarnessType<E, BeaconNodeBackend, BeaconNodeBackend>;
pub type EphemeralHarnessType<E> = BaseHarnessType<E, MemoryStore, MemoryStore>;

pub type BoxedMutator<E, Hot, Cold> = Box<
    dyn FnOnce(
        BeaconChainBuilder<BaseHarnessType<E, Hot, Cold>>,
    ) -> BeaconChainBuilder<BaseHarnessType<E, Hot, Cold>>,
>;

pub type AddBlocksResult<E> = (
    HashMap<Slot, SignedBeaconBlockHash>,
    HashMap<Slot, BeaconStateHash>,
    SignedBeaconBlockHash,
    BeaconState<E>,
);

/// Indicates how the `BeaconChainHarness` should produce blocks.
#[derive(Clone, Copy, Debug)]
pub enum BlockStrategy {
    /// Produce blocks upon the canonical head (normal case).
    OnCanonicalHead,
    /// Ignore the canonical head and produce blocks upon the block at the given slot.
    ///
    /// Useful for simulating forks.
    ForkCanonicalChainAt {
        /// The slot of the parent of the first block produced.
        previous_slot: Slot,
        /// The slot of the first block produced (must be higher than `previous_slot`.
        first_slot: Slot,
    },
}

/// Indicates how the `BeaconChainHarness` should produce attestations.
#[derive(Clone, Debug)]
pub enum AttestationStrategy {
    /// All validators attest to whichever block the `BeaconChainHarness` has produced.
    AllValidators,
    /// Only the given validators should attest. All others should fail to produce attestations.
    SomeValidators(Vec<usize>),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncCommitteeStrategy {
    /// All sync committee validators sign.
    AllValidators,
    /// No validators sign.
    NoValidators,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LightClientStrategy {
    Enabled,
    Disabled,
}

/// Indicates whether the `BeaconChainHarness` should use the `state.current_sync_committee` or
/// `state.next_sync_committee` when creating sync messages or contributions.
#[derive(Clone, Debug)]
pub enum RelativeSyncCommittee {
    Current,
    Next,
}

fn make_rng() -> Mutex<StdRng> {
    // Nondeterminism in tests is a highly undesirable thing.  Seed the RNG to some arbitrary
    // but fixed value for reproducibility.
    Mutex::new(StdRng::seed_from_u64(0x0DDB1A5E5BAD5EEDu64))
}

pub fn fork_name_from_env() -> Option<ForkName> {
    if cfg!(feature = "fork_from_env") {
        let fork_name = std::env::var(FORK_NAME_ENV_VAR).unwrap_or_else(|e| {
            panic!(
                "{} env var must be defined when using fork_from_env: {:?}",
                FORK_NAME_ENV_VAR, e
            )
        });
        Some(ForkName::from_str(fork_name.as_str()).unwrap())
    } else {
        None
    }
}

/// Return a `ChainSpec` suitable for test usage.
///
/// If the `fork_from_env` feature is enabled, read the fork to use from the FORK_NAME environment
/// variable. Otherwise we default to Bellatrix as the minimum fork (we no longer support
/// starting test networks prior to Bellatrix).
pub fn test_spec<E: EthSpec>() -> ChainSpec {
    let mut spec = fork_name_from_env()
        .map(|fork| fork.make_genesis_spec(E::default_spec()))
        .unwrap_or_else(|| ForkName::Bellatrix.make_genesis_spec(E::default_spec()));

    // Set target aggregators to a high value by default.
    spec.target_aggregators_per_committee = DEFAULT_TARGET_AGGREGATORS;
    spec
}
pub fn test_da_checker<E: EthSpec>(
    spec: Arc<ChainSpec>,
    node_custody_type: NodeCustodyType,
) -> DataAvailabilityChecker<EphemeralHarnessType<E>> {
    let slot_clock = TestingSlotClock::new(
        Slot::new(0),
        Duration::from_secs(0),
        spec.get_slot_duration(),
    );
    let kzg = get_kzg(&spec);
    let ordered_custody_column_indices = generate_data_column_indices_rand_order::<E>();
    let custody_context = Arc::new(CustodyContext::new(
        node_custody_type,
        ordered_custody_column_indices,
        &spec,
    ));
    let complete_blob_backfill = false;
    DataAvailabilityChecker::new(
        complete_blob_backfill,
        slot_clock,
        kzg,
        custody_context,
        spec,
        true,
    )
    .expect("should initialise data availability checker")
}

pub struct Builder<T: BeaconChainTypes> {
    eth_spec_instance: T::EthSpec,
    spec: Option<Arc<ChainSpec>>,
    validator_keypairs: Option<Vec<Keypair>>,
    withdrawal_keypairs: Vec<Option<Keypair>>,
    chain_config: Option<ChainConfig>,
    store_config: Option<StoreConfig>,
    #[allow(clippy::type_complexity)]
    store: Option<Arc<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>>,
    initial_mutator: Option<BoxedMutator<T::EthSpec, T::HotStore, T::ColdStore>>,
    store_mutator: Option<BoxedMutator<T::EthSpec, T::HotStore, T::ColdStore>>,
    execution_layer: Option<ExecutionLayer<T::EthSpec>>,
    mock_execution_layer: Option<MockExecutionLayer<T::EthSpec>>,
    testing_slot_clock: Option<TestingSlotClock>,
    validator_monitor_config: Option<ValidatorMonitorConfig>,
    genesis_state_builder: Option<InteropGenesisBuilder<T::EthSpec>>,
    node_custody_type: NodeCustodyType,
    runtime: TestRuntime,
}

impl<E: EthSpec> Builder<EphemeralHarnessType<E>> {
    pub fn fresh_ephemeral_store(mut self) -> Self {
        let spec = self.spec.as_ref().expect("cannot build without spec");
        let validator_keypairs = self
            .validator_keypairs
            .clone()
            .expect("cannot build without validator keypairs");

        let store = Arc::new(
            HotColdDB::open_ephemeral(self.store_config.clone().unwrap_or_default(), spec.clone())
                .unwrap(),
        );
        let genesis_state_builder = self.genesis_state_builder.take().unwrap_or_else(|| {
            // Set alternating withdrawal credentials if no builder is specified.
            InteropGenesisBuilder::default().set_alternating_eth1_withdrawal_credentials()
        });

        let mutator = move |builder: BeaconChainBuilder<_>| {
            let spec = builder.get_spec();
            let header = generate_genesis_header::<E>(spec);
            let genesis_state = genesis_state_builder
                .set_opt_execution_payload_header(header.clone())
                .build_genesis_state(
                    &validator_keypairs,
                    HARNESS_GENESIS_TIME,
                    Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH),
                    spec,
                )
                .expect("should generate interop state");
            // For post-Bellatrix forks, verify the merge is complete at genesis
            if header.is_some() {
                assert!(
                    state_processing::per_block_processing::is_merge_transition_complete(
                        &genesis_state
                    )
                );
            }
            builder
                .genesis_state(genesis_state)
                .expect("should build state using recent genesis")
        };
        self.store = Some(store);
        self.store_mutator(Box::new(mutator))
    }

    /// Create a new ephemeral store that uses the specified `genesis_state`.
    pub fn genesis_state_ephemeral_store(mut self, genesis_state: BeaconState<E>) -> Self {
        let spec = self.spec.as_ref().expect("cannot build without spec");

        let store = Arc::new(
            HotColdDB::open_ephemeral(self.store_config.clone().unwrap_or_default(), spec.clone())
                .unwrap(),
        );
        let mutator = move |builder: BeaconChainBuilder<_>| {
            builder
                .genesis_state(genesis_state)
                .expect("should build state using recent genesis")
        };
        self.store = Some(store);
        self.store_mutator(Box::new(mutator))
    }

    /// Manually restore from a given `MemoryStore`.
    pub fn resumed_ephemeral_store(
        mut self,
        store: Arc<HotColdDB<E, MemoryStore, MemoryStore>>,
    ) -> Self {
        let mutator = move |builder: BeaconChainBuilder<_>| {
            builder
                .resume_from_db()
                .expect("should resume from database")
        };
        self.store = Some(store);
        self.store_mutator(Box::new(mutator))
    }
}

impl<E: EthSpec> Builder<DiskHarnessType<E>> {
    /// Disk store, start from genesis.
    pub fn fresh_disk_store(
        mut self,
        store: Arc<HotColdDB<E, BeaconNodeBackend, BeaconNodeBackend>>,
    ) -> Self {
        let validator_keypairs = self
            .validator_keypairs
            .clone()
            .expect("cannot build without validator keypairs");

        let genesis_state_builder = self.genesis_state_builder.take().unwrap_or_else(|| {
            // Set alternating withdrawal credentials if no builder is specified.
            InteropGenesisBuilder::default().set_alternating_eth1_withdrawal_credentials()
        });

        let mutator = move |builder: BeaconChainBuilder<_>| {
            let header = generate_genesis_header::<E>(builder.get_spec());
            let genesis_state = genesis_state_builder
                .set_opt_execution_payload_header(header)
                .build_genesis_state(
                    &validator_keypairs,
                    HARNESS_GENESIS_TIME,
                    Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH),
                    builder.get_spec(),
                )
                .expect("should generate interop state");
            builder
                .genesis_state(genesis_state)
                .expect("should build state using recent genesis")
        };
        self.store = Some(store);
        self.store_mutator(Box::new(mutator))
    }

    /// Disk store, resume.
    pub fn resumed_disk_store(
        mut self,
        store: Arc<HotColdDB<E, BeaconNodeBackend, BeaconNodeBackend>>,
    ) -> Self {
        let mutator = move |builder: BeaconChainBuilder<_>| {
            builder
                .resume_from_db()
                .expect("should resume from database")
        };
        self.store = Some(store);
        self.store_mutator(Box::new(mutator))
    }
}

impl<E, Hot, Cold> Builder<BaseHarnessType<E, Hot, Cold>>
where
    E: EthSpec,
    Hot: ItemStore,
    Cold: ItemStore,
{
    pub fn new(eth_spec_instance: E) -> Self {
        let runtime = TestRuntime::default();

        Self {
            eth_spec_instance,
            spec: None,
            validator_keypairs: None,
            withdrawal_keypairs: vec![],
            chain_config: None,
            store_config: None,
            store: None,
            initial_mutator: None,
            store_mutator: None,
            execution_layer: None,
            mock_execution_layer: None,
            testing_slot_clock: None,
            validator_monitor_config: None,
            genesis_state_builder: None,
            node_custody_type: NodeCustodyType::Fullnode,
            runtime,
        }
    }

    pub fn deterministic_keypairs(self, num_keypairs: usize) -> Self {
        self.keypairs(types::test_utils::generate_deterministic_keypairs(
            num_keypairs,
        ))
    }

    pub fn keypairs(mut self, validator_keypairs: Vec<Keypair>) -> Self {
        self.validator_keypairs = Some(validator_keypairs);
        self
    }

    pub fn withdrawal_keypairs(mut self, withdrawal_keypairs: Vec<Option<Keypair>>) -> Self {
        self.withdrawal_keypairs = withdrawal_keypairs;
        self
    }

    /// Initializes the BLS withdrawal keypairs for `num_keypairs` validators to
    /// the "determistic" values, regardless of wether or not the validator has
    /// a BLS or execution address in the genesis deposits.
    ///
    /// This aligns with the withdrawal commitments used in the "interop"
    /// genesis states.
    pub fn deterministic_withdrawal_keypairs(self, num_keypairs: usize) -> Self {
        self.withdrawal_keypairs(
            types::test_utils::generate_deterministic_keypairs(num_keypairs)
                .into_iter()
                .map(Option::Some)
                .collect(),
        )
    }

    pub fn default_spec(self) -> Self {
        self.spec_or_default(None)
    }

    pub fn spec(self, spec: Arc<ChainSpec>) -> Self {
        self.spec_or_default(Some(spec))
    }

    pub fn spec_or_default(mut self, spec: Option<Arc<ChainSpec>>) -> Self {
        self.spec = Some(spec.unwrap_or_else(|| Arc::new(test_spec::<E>())));
        self
    }

    /// This mutator will be run before the `store_mutator`.
    pub fn initial_mutator(mut self, mutator: BoxedMutator<E, Hot, Cold>) -> Self {
        assert!(
            self.initial_mutator.is_none(),
            "initial mutator already set"
        );
        self.initial_mutator = Some(mutator);
        self
    }

    /// This mutator will be run after the `initial_mutator`.
    pub fn store_mutator(mut self, mutator: BoxedMutator<E, Hot, Cold>) -> Self {
        assert!(self.store_mutator.is_none(), "store mutator already set");
        self.store_mutator = Some(mutator);
        self
    }

    pub fn validator_monitor_config(
        mut self,
        validator_monitor_config: ValidatorMonitorConfig,
    ) -> Self {
        self.validator_monitor_config = Some(validator_monitor_config);
        self
    }

    /// Purposefully replace the `store_mutator`.
    pub fn override_store_mutator(mut self, mutator: BoxedMutator<E, Hot, Cold>) -> Self {
        assert!(self.store_mutator.is_some(), "store mutator not set");
        self.store_mutator = Some(mutator);
        self
    }

    pub fn chain_config(mut self, chain_config: ChainConfig) -> Self {
        self.chain_config = Some(chain_config);
        self
    }

    pub fn node_custody_type(mut self, node_custody_type: NodeCustodyType) -> Self {
        self.node_custody_type = node_custody_type;
        self
    }

    pub fn execution_layer_from_url(mut self, url: &str) -> Self {
        assert!(
            self.execution_layer.is_none(),
            "execution layer already defined"
        );

        let url = SensitiveUrl::parse(url).ok();

        let config = execution_layer::Config {
            execution_endpoint: url,
            secret_file: None,
            suggested_fee_recipient: Some(Address::repeat_byte(42)),
            ..Default::default()
        };
        let execution_layer =
            ExecutionLayer::from_config(config, self.runtime.task_executor.clone()).unwrap();

        self.execution_layer = Some(execution_layer);
        self
    }

    pub fn execution_layer(mut self, el: Option<ExecutionLayer<E>>) -> Self {
        self.execution_layer = el;
        self
    }

    pub fn recalculate_fork_times_with_genesis(mut self, genesis_time: u64) -> Self {
        let mock = self
            .mock_execution_layer
            .as_mut()
            .expect("must have mock execution layer to recalculate fork times");
        let spec = self
            .spec
            .clone()
            .expect("cannot recalculate fork times without spec");
        mock.server.execution_block_generator().shanghai_time =
            spec.capella_fork_epoch.map(|epoch| {
                genesis_time
                    + spec.get_slot_duration().as_secs() * E::slots_per_epoch() * epoch.as_u64()
            });
        mock.server.execution_block_generator().cancun_time = spec.deneb_fork_epoch.map(|epoch| {
            genesis_time
                + spec.get_slot_duration().as_secs() * E::slots_per_epoch() * epoch.as_u64()
        });
        mock.server.execution_block_generator().prague_time =
            spec.electra_fork_epoch.map(|epoch| {
                genesis_time
                    + spec.get_slot_duration().as_secs() * E::slots_per_epoch() * epoch.as_u64()
            });
        mock.server.execution_block_generator().osaka_time = spec.fulu_fork_epoch.map(|epoch| {
            genesis_time
                + spec.get_slot_duration().as_secs() * E::slots_per_epoch() * epoch.as_u64()
        });
        mock.server.execution_block_generator().amsterdam_time =
            spec.gloas_fork_epoch.map(|epoch| {
                genesis_time
                    + spec.get_slot_duration().as_secs() * E::slots_per_epoch() * epoch.as_u64()
            });

        self
    }

    pub fn mock_execution_layer(mut self) -> Self {
        let mock = mock_execution_layer_from_parts::<E>(
            self.spec.clone().expect("cannot build without spec"),
            self.runtime.task_executor.clone(),
        );
        self.execution_layer = Some(mock.el.clone());
        self.mock_execution_layer = Some(mock);
        self
    }

    /// Instruct the mock execution engine to always return a "valid" response to any payload it is
    /// asked to execute.
    pub fn mock_execution_layer_all_payloads_valid(self) -> Self {
        self.mock_execution_layer
            .as_ref()
            .expect("requires mock execution layer")
            .server
            .all_payloads_valid();
        self
    }

    pub fn testing_slot_clock(mut self, slot_clock: TestingSlotClock) -> Self {
        self.testing_slot_clock = Some(slot_clock);
        self
    }

    pub fn with_genesis_state_builder(
        mut self,
        f: impl FnOnce(InteropGenesisBuilder<E>) -> InteropGenesisBuilder<E>,
    ) -> Self {
        let builder = self.genesis_state_builder.take().unwrap_or_default();
        self.genesis_state_builder = Some(f(builder));
        self
    }

    pub fn build(self) -> BeaconChainHarness<BaseHarnessType<E, Hot, Cold>> {
        let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1);

        let spec = self.spec.expect("cannot build without spec");
        let validator_keypairs = self
            .validator_keypairs
            .expect("cannot build without validator keypairs");

        let kzg = get_kzg(&spec);

        let validator_monitor_config = self.validator_monitor_config.unwrap_or_default();

        let chain_config = self.chain_config.unwrap_or_default();
        let mut builder = BeaconChainBuilder::new(self.eth_spec_instance, kzg.clone())
            .custom_spec(spec.clone())
            .store(self.store.expect("cannot build without store"))
            .store_migrator_config(
                MigratorConfig::default()
                    .blocking()
                    .epochs_per_migration(chain_config.epochs_per_migration),
            )
            .task_executor(self.runtime.task_executor.clone())
            .execution_layer(self.execution_layer)
            .shutdown_sender(shutdown_tx)
            .chain_config(chain_config)
            .node_custody_type(self.node_custody_type)
            .ordered_custody_column_indices(generate_data_column_indices_rand_order::<E>())
            .event_handler(Some(ServerSentEventHandler::new_with_capacity(5)))
            .validator_monitor_config(validator_monitor_config)
            .rng(Box::new(StdRng::seed_from_u64(42)));

        builder = if let Some(mutator) = self.initial_mutator {
            mutator(builder)
        } else {
            builder
        };

        builder = if let Some(mutator) = self.store_mutator {
            mutator(builder)
        } else {
            builder
        };

        // Initialize the slot clock only if it hasn't already been initialized.
        builder = if let Some(testing_slot_clock) = self.testing_slot_clock {
            builder.slot_clock(testing_slot_clock)
        } else if builder.get_slot_clock().is_none() {
            builder
                .testing_slot_clock(spec.get_slot_duration())
                .expect("should configure testing slot clock")
        } else {
            builder
        };

        let chain = builder.build().expect("should build");

        BeaconChainHarness {
            spec: chain.spec.clone(),
            chain: Arc::new(chain),
            validator_keypairs,
            withdrawal_keypairs: self.withdrawal_keypairs,
            shutdown_receiver: Arc::new(Mutex::new(shutdown_receiver)),
            runtime: self.runtime,
            mock_execution_layer: self.mock_execution_layer,
            mock_builder: None,
            rng: make_rng(),
        }
    }
}

pub fn mock_execution_layer_from_parts<E: EthSpec>(
    spec: Arc<ChainSpec>,
    task_executor: TaskExecutor,
) -> MockExecutionLayer<E> {
    let shanghai_time = spec.capella_fork_epoch.map(|epoch| {
        HARNESS_GENESIS_TIME
            + (spec.get_slot_duration().as_secs()) * E::slots_per_epoch() * epoch.as_u64()
    });
    let cancun_time = spec.deneb_fork_epoch.map(|epoch| {
        HARNESS_GENESIS_TIME
            + (spec.get_slot_duration().as_secs()) * E::slots_per_epoch() * epoch.as_u64()
    });
    let prague_time = spec.electra_fork_epoch.map(|epoch| {
        HARNESS_GENESIS_TIME
            + (spec.get_slot_duration().as_secs()) * E::slots_per_epoch() * epoch.as_u64()
    });
    let osaka_time = spec.fulu_fork_epoch.map(|epoch| {
        HARNESS_GENESIS_TIME
            + (spec.get_slot_duration().as_secs()) * E::slots_per_epoch() * epoch.as_u64()
    });
    let amsterdam_time = spec.gloas_fork_epoch.map(|epoch| {
        HARNESS_GENESIS_TIME
            + (spec.get_slot_duration().as_secs()) * E::slots_per_epoch() * epoch.as_u64()
    });

    let kzg = get_kzg(&spec);

    MockExecutionLayer::new(
        task_executor,
        shanghai_time,
        cancun_time,
        prague_time,
        osaka_time,
        amsterdam_time,
        Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
        spec,
        Some(kzg),
    )
}

/// A testing harness which can instantiate a `BeaconChain` and populate it with blocks and
/// attestations.
///
/// Used for testing.
pub struct BeaconChainHarness<T: BeaconChainTypes> {
    pub validator_keypairs: Vec<Keypair>,
    /// Optional BLS withdrawal keys for each validator.
    ///
    /// If a validator index is missing from this vec or their entry is `None` then either
    /// no BLS withdrawal key was set for them (they had an address from genesis) or the test
    /// initializer neglected to set this field.
    pub withdrawal_keypairs: Vec<Option<Keypair>>,

    pub chain: Arc<BeaconChain<T>>,
    pub spec: Arc<ChainSpec>,
    pub shutdown_receiver: Arc<Mutex<Receiver<ShutdownReason>>>,
    pub runtime: TestRuntime,

    pub mock_execution_layer: Option<MockExecutionLayer<T::EthSpec>>,
    pub mock_builder: Option<Arc<MockBuilder<T::EthSpec>>>,

    pub rng: Mutex<StdRng>,
}

pub type CommitteeSingleAttestations = Vec<(SingleAttestation, SubnetId)>;
pub type CommitteeAttestations<E> = Vec<(Attestation<E>, SubnetId)>;
pub type HarnessAttestations<E> =
    Vec<(CommitteeAttestations<E>, Option<SignedAggregateAndProof<E>>)>;

pub type HarnessSingleAttestations<E> = Vec<(
    CommitteeSingleAttestations,
    Option<SignedAggregateAndProof<E>>,
)>;

pub type HarnessSyncContributions<E> = Vec<(
    Vec<(SyncCommitteeMessage, usize)>,
    Option<SignedContributionAndProof<E>>,
)>;

impl<E, Hot, Cold> BeaconChainHarness<BaseHarnessType<E, Hot, Cold>>
where
    E: EthSpec,
    Hot: ItemStore,
    Cold: ItemStore,
{
    pub fn builder(eth_spec_instance: E) -> Builder<BaseHarnessType<E, Hot, Cold>> {
        create_test_tracing_subscriber();
        Builder::new(eth_spec_instance)
    }

    pub fn execution_block_generator(&self) -> RwLockWriteGuard<'_, ExecutionBlockGenerator<E>> {
        self.mock_execution_layer
            .as_ref()
            .expect("harness was not built with mock execution layer")
            .server
            .execution_block_generator()
    }

    /// Create a switch-to-compounding `ConsolidationRequest` for the given validator.
    ///
    /// Panics if the validator doesn't exist, doesn't have eth1 withdrawal credentials,
    /// or doesn't have an execution withdrawal address.
    pub fn make_switch_to_compounding_request(
        &self,
        validator_index: usize,
    ) -> ConsolidationRequest {
        let head = self.chain.canonical_head.cached_head();
        let head_state = &head.snapshot.beacon_state;
        let validator = head_state
            .get_validator(validator_index)
            .expect("validator should exist");

        assert!(
            validator.has_eth1_withdrawal_credential(&self.spec),
            "validator {validator_index} should have eth1 withdrawal credentials"
        );

        let source_address = validator
            .get_execution_withdrawal_address(&self.spec)
            .expect("validator should have execution withdrawal address");

        ConsolidationRequest {
            source_address,
            source_pubkey: validator.pubkey,
            target_pubkey: validator.pubkey,
        }
    }

    pub fn set_mock_builder(
        &mut self,
        beacon_url: SensitiveUrl,
        strict_registrations: bool,
        apply_operations: bool,
        broadcast_to_bn: bool,
    ) -> impl futures::Future<Output = ()> + use<E, Hot, Cold> {
        let mock_el = self
            .mock_execution_layer
            .as_ref()
            .expect("harness was not built with mock execution layer");

        let mock_el_url = SensitiveUrl::parse(mock_el.server.url().as_str()).unwrap();

        // Create the builder, listening on a free port.
        let (mock_builder, (addr, mock_builder_server)) = MockBuilder::new_for_testing(
            mock_el_url,
            beacon_url,
            strict_registrations,
            apply_operations,
            broadcast_to_bn,
            self.spec.clone(),
            self.runtime.task_executor.clone(),
        );

        // Set the builder URL in the execution layer now that its port is known.
        let port = addr.port();
        mock_el
            .el
            .set_builder_url(
                SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(),
                None,
                None,
                false,
            )
            .unwrap();

        self.mock_builder = Some(Arc::new(mock_builder));

        // Sanity check.
        let el_builder = self
            .chain
            .execution_layer
            .as_ref()
            .unwrap()
            .builder()
            .unwrap();
        let mock_el_builder = mock_el.el.builder().unwrap();
        assert!(Arc::ptr_eq(&el_builder, &mock_el_builder));

        mock_builder_server
    }

    pub fn get_head_block(&self) -> RangeSyncBlock<E> {
        let block = self.chain.head_beacon_block();
        let block_root = block.canonical_root();
        self.build_range_sync_block_from_store_blobs(Some(block_root), block)
    }

    pub fn get_full_block(&self, block_root: &Hash256) -> RangeSyncBlock<E> {
        let block = self
            .chain
            .get_blinded_block(block_root)
            .unwrap()
            .unwrap_or_else(|| panic!("block root does not exist in harness {block_root:?}"));
        let full_block = self.chain.store.make_full_block(block_root, block).unwrap();
        self.build_range_sync_block_from_store_blobs(Some(*block_root), Arc::new(full_block))
    }

    pub fn get_all_validators(&self) -> Vec<usize> {
        (0..self.validator_keypairs.len()).collect()
    }

    pub fn slots_per_epoch(&self) -> u64 {
        E::slots_per_epoch()
    }

    pub fn epoch_start_slot(&self, epoch: u64) -> u64 {
        let epoch = Epoch::new(epoch);
        epoch.start_slot(E::slots_per_epoch()).into()
    }

    pub fn shutdown_reasons(&self) -> Vec<ShutdownReason> {
        let mutex = self.shutdown_receiver.clone();
        let mut receiver = mutex.lock();
        std::iter::from_fn(move || match receiver.try_next() {
            Ok(Some(s)) => Some(s),
            Ok(None) => panic!("shutdown sender dropped"),
            Err(_) => None,
        })
        .collect()
    }

    pub fn get_current_state(&self) -> BeaconState<E> {
        self.chain.head_beacon_state_cloned()
    }

    pub fn get_timestamp_at_slot(&self) -> u64 {
        let state = self.get_current_state();
        compute_timestamp_at_slot(&state, state.slot(), &self.spec).unwrap()
    }

    pub fn get_current_state_and_root(&self) -> (BeaconState<E>, Hash256) {
        let head = self.chain.head_snapshot();
        let state_root = head.beacon_state_root();
        (head.beacon_state.clone(), state_root)
    }

    pub fn head_slot(&self) -> Slot {
        self.chain.canonical_head.cached_head().head_slot()
    }

    pub fn head_block_root(&self) -> Hash256 {
        self.chain.canonical_head.cached_head().head_block_root()
    }

    pub fn finalized_checkpoint(&self) -> Checkpoint {
        self.chain
            .canonical_head
            .cached_head()
            .finalized_checkpoint()
    }

    pub fn justified_checkpoint(&self) -> Checkpoint {
        self.chain
            .canonical_head
            .cached_head()
            .justified_checkpoint()
    }

    pub fn get_current_slot(&self) -> Slot {
        self.chain.slot().unwrap()
    }

    pub fn get_block(
        &self,
        block_hash: SignedBeaconBlockHash,
    ) -> Option<SignedBeaconBlock<E, BlindedPayload<E>>> {
        self.chain.get_blinded_block(&block_hash.into()).unwrap()
    }

    pub fn block_exists(&self, block_hash: SignedBeaconBlockHash) -> bool {
        self.get_block(block_hash).is_some()
    }

    pub fn get_hot_state(&self, state_hash: BeaconStateHash) -> Option<BeaconState<E>> {
        self.chain
            .store
            .load_hot_state(&state_hash.into(), true)
            .unwrap()
            .map(|(state, _)| state)
    }

    pub fn get_cold_state(&self, state_hash: BeaconStateHash) -> Option<BeaconState<E>> {
        self.chain
            .store
            .load_cold_state(&state_hash.into())
            .unwrap()
    }

    pub fn hot_state_exists(&self, state_hash: BeaconStateHash) -> bool {
        self.get_hot_state(state_hash).is_some()
    }

    pub fn cold_state_exists(&self, state_hash: BeaconStateHash) -> bool {
        self.get_cold_state(state_hash).is_some()
    }

    pub fn is_skipped_slot(&self, state: &BeaconState<E>, slot: Slot) -> bool {
        state.get_block_root(slot).unwrap() == state.get_block_root(slot - 1).unwrap()
    }

    pub fn knows_head(&self, block_hash: &SignedBeaconBlockHash) -> bool {
        self.chain
            .heads()
            .iter()
            .any(|(head, _)| *head == Hash256::from(*block_hash))
    }

    pub fn assert_knows_head(&self, head_block_root: Hash256) {
        let heads = self.chain.heads();
        if !heads.iter().any(|(head, _)| *head == head_block_root) {
            let fork_choice = self.chain.canonical_head.fork_choice_read_lock();
            if heads.is_empty() {
                let nodes = &fork_choice.proto_array().core_proto_array().nodes;
                panic!(
                    "Expected to know head block root {head_block_root:?}, but heads is empty. Nodes: {nodes:#?}"
                );
            } else {
                panic!(
                    "Expected to know head block root {head_block_root:?}, known heads {heads:#?}"
                );
            }
        }
    }

    pub async fn make_blinded_block(
        &self,
        state: BeaconState<E>,
        slot: Slot,
    ) -> (SignedBlindedBeaconBlock<E>, BeaconState<E>) {
        self.make_blinded_block_with_modifier(state, slot, |_| {})
            .await
    }

    pub async fn make_blinded_block_with_modifier(
        &self,
        mut state: BeaconState<E>,
        slot: Slot,
        block_modifier: impl FnOnce(&mut BlindedBeaconBlock<E>),
    ) -> (SignedBlindedBeaconBlock<E>, BeaconState<E>) {
        assert_ne!(slot, 0, "can't produce a block at slot 0");
        assert!(slot >= state.slot());

        // For Gloas, blinded and full blocks are structurally identical (no payload in body).
        // Produce via the Gloas path and convert to blinded.
        if self.spec.fork_name_at_slot::<E>(slot).gloas_enabled() {
            let (block_contents, _envelope, pending_state) =
                Box::pin(self.make_block_with_envelope(state, slot)).await;
            let (signed_block, _blobs) = block_contents;
            let signed_blinded = signed_block.clone_as_blinded();
            let (mut blinded_block, _signature) = signed_blinded.deconstruct();
            block_modifier(&mut blinded_block);
            let proposer_index = pending_state
                .get_beacon_proposer_index(slot, &self.spec)
                .unwrap();
            // Re-sign after modification.
            let signed_blinded = blinded_block.sign(
                &self.validator_keypairs[proposer_index].sk,
                &pending_state.fork(),
                pending_state.genesis_validators_root(),
                &self.spec,
            );
            return (signed_blinded, pending_state);
        }

        complete_state_advance(&mut state, None, slot, &self.spec)
            .expect("should be able to advance state to slot");

        state.build_caches(&self.spec).expect("should build caches");

        let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap();

        // If we produce two blocks for the same slot, they hash up to the same value and
        // BeaconChain errors out with `DuplicateFullyImported`.  Vary the graffiti so that we produce
        // different blocks each time.
        let graffiti = Graffiti::from(self.rng.lock().random::<[u8; 32]>());
        let graffiti_settings =
            GraffitiSettings::new(Some(graffiti), Some(GraffitiPolicy::PreserveUserGraffiti));

        let randao_reveal = self.sign_randao_reveal(&state, proposer_index, slot);

        // Always use the builder, so that we produce a "real" blinded payload.
        let builder_boost_factor = Some(u64::MAX);

        let BeaconBlockResponseWrapper::Blinded(block_response) = self
            .chain
            .produce_block_on_state(
                state,
                None,
                slot,
                randao_reveal,
                graffiti_settings,
                ProduceBlockVerification::VerifyRandao,
                builder_boost_factor,
                BlockProductionVersion::V3,
            )
            .await
            .unwrap()
        else {
            panic!("Should always be a blinded payload response");
        };

        let mut block = block_response.block;
        block_modifier(&mut block);

        let signed_block = block.sign(
            &self.validator_keypairs[proposer_index].sk,
            &block_response.state.fork(),
            block_response.state.genesis_validators_root(),
            &self.spec,
        );

        (signed_block, block_response.state)
    }

    /// Returns a newly created block, signed by the proposer for the given slot.
    pub async fn make_block(
        &self,
        mut state: BeaconState<E>,
        slot: Slot,
    ) -> (SignedBlockContentsTuple<E>, BeaconState<E>) {
        assert_ne!(slot, 0, "can't produce a block at slot 0");
        assert!(slot >= state.slot());

        // For Gloas forks, delegate to make_block_with_envelope and discard the envelope.
        if self.spec.fork_name_at_slot::<E>(slot).gloas_enabled() {
            let (block_contents, _envelope, state) =
                Box::pin(self.make_block_with_envelope(state, slot)).await;
            return (block_contents, state);
        }

        complete_state_advance(&mut state, None, slot, &self.spec)
            .expect("should be able to advance state to slot");

        state.build_caches(&self.spec).expect("should build caches");

        let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap();

        // If we produce two blocks for the same slot, they hash up to the same value and
        // BeaconChain errors out with `DuplicateFullyImported`.  Vary the graffiti so that we produce
        // different blocks each time.
        let graffiti = Graffiti::from(self.rng.lock().random::<[u8; 32]>());
        let graffiti_settings =
            GraffitiSettings::new(Some(graffiti), Some(GraffitiPolicy::PreserveUserGraffiti));

        let randao_reveal = self.sign_randao_reveal(&state, proposer_index, slot);

        let BeaconBlockResponseWrapper::Full(block_response) = self
            .chain
            .produce_block_on_state(
                state,
                None,
                slot,
                randao_reveal,
                graffiti_settings,
                ProduceBlockVerification::VerifyRandao,
                None,
                BlockProductionVersion::FullV2,
            )
            .await
            .unwrap()
        else {
            panic!("Should always be a full payload response");
        };

        let signed_block = Arc::new(block_response.block.sign(
            &self.validator_keypairs[proposer_index].sk,
            &block_response.state.fork(),
            block_response.state.genesis_validators_root(),
            &self.spec,
        ));

        let block_contents: SignedBlockContentsTuple<E> =
            if signed_block.fork_name_unchecked().deneb_enabled() {
                (signed_block, block_response.blob_items)
            } else {
                (signed_block, None)
            };

        (block_contents, block_response.state)
    }

    /// Returns a newly created block, signed by the proposer for the given slot,
    /// along with the execution payload envelope (for Gloas) and the post-block state.
    ///
    /// For pre-Gloas forks, the envelope is `None` and this behaves like `make_block`.
    pub async fn make_block_with_envelope(
        &self,
        mut state: BeaconState<E>,
        slot: Slot,
    ) -> (
        SignedBlockContentsTuple<E>,
        Option<SignedExecutionPayloadEnvelope<E>>,
        BeaconState<E>,
    ) {
        assert_ne!(slot, 0, "can't produce a block at slot 0");
        assert!(slot >= state.slot());

        if state.fork_name_unchecked().gloas_enabled()
            || self.spec.fork_name_at_slot::<E>(slot).gloas_enabled()
        {
            complete_state_advance(&mut state, None, slot, &self.spec)
                .expect("should be able to advance state to slot");
            state.build_caches(&self.spec).expect("should build caches");

            let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap();

            let graffiti = Graffiti::from(self.rng.lock().random::<[u8; 32]>());
            let graffiti_settings =
                GraffitiSettings::new(Some(graffiti), Some(GraffitiPolicy::PreserveUserGraffiti));
            let randao_reveal = self.sign_randao_reveal(&state, proposer_index, slot);

            // Load the parent's payload envelope and status from the cached head.
            // TODO(gloas): we may want to pass these as arguments to support cases where we build
            // on alternate chains to the head.
            let (parent_payload_status, parent_envelope) = {
                let head = self.chain.canonical_head.cached_head();
                (
                    head.head_payload_status(),
                    head.snapshot.execution_envelope.clone(),
                )
            };

            let (block, post_block_state, _consensus_block_value) = self
                .chain
                .produce_block_on_state_gloas(
                    state,
                    None,
                    parent_payload_status,
                    parent_envelope,
                    slot,
                    randao_reveal,
                    graffiti_settings,
                    ProduceBlockVerification::VerifyRandao,
                    None,
                )
                .await
                .unwrap();

            let signed_block = Arc::new(block.sign(
                &self.validator_keypairs[proposer_index].sk,
                &post_block_state.fork(),
                post_block_state.genesis_validators_root(),
                &self.spec,
            ));

            // Retrieve the cached envelope produced during block production and sign it.
            let signed_envelope = self
                .chain
                .pending_payload_envelopes
                .write()
                .remove(slot)
                .map(|envelope| {
                    let epoch = slot.epoch(E::slots_per_epoch());
                    let domain = self.spec.get_domain(
                        epoch,
                        Domain::BeaconBuilder,
                        &post_block_state.fork(),
                        post_block_state.genesis_validators_root(),
                    );
                    let message = envelope.signing_root(domain);
                    let signature = self.validator_keypairs[proposer_index].sk.sign(message);
                    SignedExecutionPayloadEnvelope {
                        message: envelope,
                        signature,
                    }
                });

            let block_contents: SignedBlockContentsTuple<E> = (signed_block, None);
            (block_contents, signed_envelope, post_block_state)
        } else {
            let (block_contents, state) = self.make_block(state, slot).await;
            (block_contents, None, state)
        }
    }

    /// Useful for the `per_block_processing` tests. Creates a block, and returns the state after
    /// caches are built but before the generated block is processed.
    pub async fn make_block_return_pre_state(
        &self,
        mut state: BeaconState<E>,
        slot: Slot,
    ) -> (SignedBlockContentsTuple<E>, BeaconState<E>) {
        assert_ne!(slot, 0, "can't produce a block at slot 0");
        assert!(slot >= state.slot());

        // For Gloas forks, delegate to make_block_with_envelope which uses the
        // Gloas-specific block production path, and return the pre-state.
        if self.spec.fork_name_at_slot::<E>(slot).gloas_enabled() {
            let pre_state = {
                let mut s = state.clone();
                complete_state_advance(&mut s, None, slot, &self.spec)
                    .expect("should be able to advance state to slot");
                s.build_caches(&self.spec).expect("should build caches");
                s
            };
            let (block_contents, _envelope, _state) =
                Box::pin(self.make_block_with_envelope(state, slot)).await;
            return (block_contents, pre_state);
        }

        complete_state_advance(&mut state, None, slot, &self.spec)
            .expect("should be able to advance state to slot");

        state.build_caches(&self.spec).expect("should build caches");

        let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap();

        // If we produce two blocks for the same slot, they hash up to the same value and
        // BeaconChain errors out with `DuplicateFullyImported`.  Vary the graffiti so that we produce
        // different blocks each time.
        let graffiti = Graffiti::from(self.rng.lock().random::<[u8; 32]>());
        let graffiti_settings =
            GraffitiSettings::new(Some(graffiti), Some(GraffitiPolicy::PreserveUserGraffiti));

        let randao_reveal = self.sign_randao_reveal(&state, proposer_index, slot);

        let pre_state = state.clone();

        let BeaconBlockResponseWrapper::Full(block_response) = self
            .chain
            .produce_block_on_state(
                state,
                None,
                slot,
                randao_reveal,
                graffiti_settings,
                ProduceBlockVerification::VerifyRandao,
                None,
                BlockProductionVersion::FullV2,
            )
            .await
            .unwrap()
        else {
            panic!("Should always be a full payload response");
        };

        let signed_block = Arc::new(block_response.block.sign(
            &self.validator_keypairs[proposer_index].sk,
            &block_response.state.fork(),
            block_response.state.genesis_validators_root(),
            &self.spec,
        ));

        let block_contents: SignedBlockContentsTuple<E> =
            if signed_block.fork_name_unchecked().deneb_enabled() {
                (signed_block, block_response.blob_items)
            } else {
                (signed_block, None)
            };
        (block_contents, pre_state)
    }

    /// Create a randao reveal for a block at `slot`.
    pub fn sign_randao_reveal(
        &self,
        state: &BeaconState<E>,
        proposer_index: usize,
        slot: Slot,
    ) -> Signature {
        let epoch = slot.epoch(E::slots_per_epoch());
        let domain = self.spec.get_domain(
            epoch,
            Domain::Randao,
            &state.fork(),
            state.genesis_validators_root(),
        );
        let message = epoch.signing_root(domain);
        let sk = &self.validator_keypairs[proposer_index].sk;
        sk.sign(message)
    }

    /// Sign a beacon block using the proposer's key.
    pub fn sign_beacon_block(
        &self,
        block: BeaconBlock<E>,
        state: &BeaconState<E>,
    ) -> SignedBeaconBlock<E> {
        let proposer_index = block.proposer_index() as usize;
        block.sign(
            &self.validator_keypairs[proposer_index].sk,
            &state.fork(),
            state.genesis_validators_root(),
            &self.spec,
        )
    }

    /// Build a Bellatrix block with the given execution payload, compute the
    /// correct state root, sign it, and import it into the chain.
    ///
    /// This bypasses the normal block production pipeline, which always requests
    /// a payload from the execution layer. That makes it possible to construct
    /// blocks with **default (zeroed) payloads** — something the EL-backed flow
    /// cannot do — which is needed to simulate the pre-merge portion of a chain
    /// that starts at Bellatrix genesis with `is_merge_transition_complete = false`.
    ///
    /// `state` is expected to be the head state *before* `slot`. It will be
    /// advanced to `slot` in-place via `complete_state_advance`, then used to
    /// derive the proposer, RANDAO reveal, and parent root. After processing,
    /// the caller should typically replace `state` with the chain's new head
    /// state (`self.get_current_state()`).
    pub async fn build_and_import_block_with_payload(
        &self,
        state: &mut BeaconState<E>,
        slot: Slot,
        execution_payload: ExecutionPayloadBellatrix<E>,
    ) {
        complete_state_advance(state, None, slot, &self.spec).expect("should advance state");
        state.build_caches(&self.spec).expect("should build caches");

        let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap();
        let randao_reveal = self.sign_randao_reveal(state, proposer_index, slot);
        let parent_root = state.latest_block_header().canonical_root();

        let mut block = BeaconBlock::Bellatrix(BeaconBlockBellatrix {
            slot,
            proposer_index: proposer_index as u64,
            parent_root,
            state_root: Hash256::zero(),
            body: BeaconBlockBodyBellatrix {
                randao_reveal,
                eth1_data: state.eth1_data().clone(),
                graffiti: Graffiti::default(),
                proposer_slashings: VariableList::empty(),
                attester_slashings: VariableList::empty(),
                attestations: VariableList::empty(),
                deposits: VariableList::empty(),
                voluntary_exits: VariableList::empty(),
                sync_aggregate: SyncAggregate::new(),
                execution_payload: FullPayloadBellatrix { execution_payload },
            },
        });

        // Run per_block_processing on a clone to compute the post-state root.
        let signed_tmp = block.clone().sign(
            &self.validator_keypairs[proposer_index].sk,
            &state.fork(),
            state.genesis_validators_root(),
            &self.spec,
        );
        let mut ctxt = ConsensusContext::new(slot).set_proposer_index(proposer_index as u64);
        let mut post_state = state.clone();
        per_block_processing(
            &mut post_state,
            &signed_tmp,
            BlockSignatureStrategy::NoVerification,
            VerifyBlockRoot::False,
            &mut ctxt,
            &self.spec,
        )
        .unwrap_or_else(|e| panic!("per_block_processing failed at slot {}: {e:?}", slot));

        let state_root = post_state.update_tree_hash_cache().unwrap();
        *block.state_root_mut() = state_root;

        let signed_block = self.sign_beacon_block(block, state);
        let block_root = signed_block.canonical_root();
        let lookup_block = LookupBlock::new(Arc::new(signed_block));
        self.chain.slot_clock.set_slot(slot.as_u64());
        self.chain
            .process_block(
                block_root,
                lookup_block,
                NotifyExecutionLayer::No,
                BlockImportSource::Lookup,
                || Ok(()),
            )
            .await
            .unwrap_or_else(|e| panic!("import failed at slot {}: {e:?}", slot));
        self.chain.recompute_head_at_current_slot().await;
    }

    #[allow(clippy::too_many_arguments)]
    pub fn produce_single_attestation_for_block(
        &self,
        slot: Slot,
        index: CommitteeIndex,
        beacon_block_root: Hash256,
        mut state: Cow<BeaconState<E>>,
        state_root: Hash256,
        aggregation_bit_index: usize,
        validator_index: usize,
    ) -> Result<SingleAttestation, BeaconChainError> {
        let epoch = slot.epoch(E::slots_per_epoch());

        if state.slot() > slot {
            return Err(BeaconChainError::CannotAttestToFutureState);
        } else if state.current_epoch() < epoch {
            let mut_state = state.to_mut();
            complete_state_advance(
                mut_state,
                Some(state_root),
                epoch.start_slot(E::slots_per_epoch()),
                &self.spec,
            )?;
            mut_state.build_committee_cache(RelativeEpoch::Current, &self.spec)?;
        }

        let committee_len = state.get_beacon_committee(slot, index)?.committee.len();

        let target_slot = epoch.start_slot(E::slots_per_epoch());
        let target_root = if state.slot() <= target_slot {
            beacon_block_root
        } else {
            *state.get_block_root(target_slot)?
        };

        let attestation: Attestation<E> = Attestation::empty_for_signing(
            index,
            committee_len,
            slot,
            beacon_block_root,
            state.current_justified_checkpoint(),
            Checkpoint {
                epoch,
                root: target_root,
            },
            false,
            &self.spec,
        )?;

        let attestation = match attestation {
            Attestation::Electra(mut attn) => {
                attn.aggregation_bits
                    .set(aggregation_bit_index, true)
                    .unwrap();
                Attestation::Electra(attn)
            }
            Attestation::Base(mut attn) => {
                attn.aggregation_bits
                    .set(aggregation_bit_index, true)
                    .unwrap();
                Attestation::Base(attn)
            }
        };

        let aggregation_bits = attestation.get_aggregation_bits();

        if aggregation_bits.len() != 1 {
            panic!("Must be an unaggregated attestation")
        }

        let aggregation_bit = *aggregation_bits.first().unwrap();

        let committee = state.get_beacon_committee(slot, index).unwrap();

        let attester_index = committee
            .committee
            .iter()
            .enumerate()
            .find_map(|(i, &index)| {
                if aggregation_bit as usize == i {
                    return Some(index);
                }
                None
            })
            .unwrap();

        let single_attestation =
            attestation.to_single_attestation_with_attester_index(attester_index as u64)?;

        let fork_name = self.spec.fork_name_at_slot::<E>(attestation.data().slot);
        let attestation: Attestation<E> =
            single_attestation_to_attestation(&single_attestation, committee.committee, fork_name)
                .unwrap();

        assert_eq!(
            single_attestation.committee_index,
            attestation.committee_index().unwrap()
        );
        assert_eq!(single_attestation.attester_index, validator_index as u64);
        Ok(single_attestation)
    }

    /// Produces an "unaggregated" attestation for the given `slot` and `index` that attests to
    /// `beacon_block_root`. The provided `state` should match the `block.state_root` for the
    /// `block` identified by `beacon_block_root`.
    ///
    /// The attestation doesn't _really_ have anything about it that makes it unaggregated per say,
    /// however this function is only required in the context of forming an unaggregated
    /// attestation. It would be an (undetectable) violation of the protocol to create a
    /// `SignedAggregateAndProof` based upon the output of this function.
    ///
    /// This function will produce attestations to optimistic blocks, which is against the
    /// specification but useful during testing.
    pub fn produce_unaggregated_attestation_for_block(
        &self,
        slot: Slot,
        index: CommitteeIndex,
        beacon_block_root: Hash256,
        mut state: Cow<BeaconState<E>>,
        state_root: Hash256,
        payload_present_override: Option<bool>,
    ) -> Result<Attestation<E>, BeaconChainError> {
        assert_eq!(
            state.get_latest_block_root(state_root),
            beacon_block_root,
            "State must match beacon block root, state slot {:?} attestation slot {:?} state root {:?}",
            state.latest_block_header().slot,
            slot,
            state_root,
        );

        let epoch = slot.epoch(E::slots_per_epoch());

        if state.slot() > slot {
            return Err(BeaconChainError::CannotAttestToFutureState);
        } else if state.current_epoch() < epoch {
            let mut_state = state.to_mut();
            complete_state_advance(
                mut_state,
                Some(state_root),
                epoch.start_slot(E::slots_per_epoch()),
                &self.spec,
            )?;
            mut_state.build_committee_cache(RelativeEpoch::Current, &self.spec)?;
        }

        let committee_len = state.get_beacon_committee(slot, index)?.committee.len();

        let target_slot = epoch.start_slot(E::slots_per_epoch());
        let target_root = if state.slot() <= target_slot {
            beacon_block_root
        } else {
            *state.get_block_root(target_slot)?
        };

        let payload_present = match payload_present_override {
            Some(payload_present) => payload_present,
            None => {
                state.fork_name_unchecked().gloas_enabled()
                    && state.latest_block_header().slot != slot
                    && self
                        .chain
                        .canonical_head
                        .block_has_canonical_payload(&beacon_block_root, &self.spec)?
            }
        };

        Ok(Attestation::empty_for_signing(
            index,
            committee_len,
            slot,
            beacon_block_root,
            state.current_justified_checkpoint(),
            Checkpoint {
                epoch,
                root: target_root,
            },
            payload_present,
            &self.spec,
        )?)
    }

    /// A list of attestations for each committee for the given slot.
    ///
    /// The first layer of the Vec is organised per committee. For example, if the return value is
    /// called `all_attestations`, then all attestations in `all_attestations[0]` will be for
    /// committee 0, whilst all in `all_attestations[1]` will be for committee 1.
    pub fn make_single_attestations(
        &self,
        attesting_validators: &[usize],
        state: &BeaconState<E>,
        state_root: Hash256,
        head_block_root: SignedBeaconBlockHash,
        attestation_slot: Slot,
    ) -> Vec<CommitteeSingleAttestations> {
        let fork = self
            .spec
            .fork_at_epoch(attestation_slot.epoch(E::slots_per_epoch()));
        self.make_single_attestations_with_opts(
            attesting_validators,
            state,
            state_root,
            head_block_root,
            attestation_slot,
            MakeAttestationOptions {
                limit: None,
                fork,
                payload_present_override: None,
            },
        )
        .0
    }

    /// A list of attestations for each committee for the given slot.
    ///
    /// The first layer of the Vec is organised per committee. For example, if the return value is
    /// called `all_attestations`, then all attestations in `all_attestations[0]` will be for
    /// committee 0, whilst all in `all_attestations[1]` will be for committee 1.
    pub fn make_unaggregated_attestations(
        &self,
        attesting_validators: &[usize],
        state: &BeaconState<E>,
        state_root: Hash256,
        head_block_root: SignedBeaconBlockHash,
        attestation_slot: Slot,
    ) -> Vec<CommitteeAttestations<E>> {
        let fork = self
            .spec
            .fork_at_epoch(attestation_slot.epoch(E::slots_per_epoch()));
        self.make_unaggregated_attestations_with_opts(
            attesting_validators,
            state,
            state_root,
            head_block_root,
            attestation_slot,
            MakeAttestationOptions {
                limit: None,
                fork,
                payload_present_override: None,
            },
        )
        .0
    }

    pub fn make_single_attestations_with_opts(
        &self,
        attesting_validators: &[usize],
        state: &BeaconState<E>,
        state_root: Hash256,
        head_block_root: SignedBeaconBlockHash,
        attestation_slot: Slot,
        opts: MakeAttestationOptions,
    ) -> (Vec<CommitteeSingleAttestations>, Vec<usize>) {
        let MakeAttestationOptions { limit, fork, .. } = opts;
        let committee_count = state.get_committee_count_at_slot(state.slot()).unwrap();
        let num_attesters = AtomicUsize::new(0);

        let (attestations, split_attesters) = state
            .get_beacon_committees_at_slot(attestation_slot)
            .expect("should get committees")
            .iter()
            .map(|bc| {
                bc.committee
                    .par_iter()
                    .enumerate()
                    .filter_map(|(i, validator_index)| {
                        if !attesting_validators.contains(validator_index) {
                            return None;
                        }

                        if let Some(limit) = limit {
                            // This atomics stuff is necessary because we're under a par_iter,
                            // and Rayon will deadlock if we use a mutex.
                            if num_attesters.fetch_add(1, Ordering::Relaxed) >= limit {
                                num_attesters.fetch_sub(1, Ordering::Relaxed);
                                return None;
                            }
                        }

                        let mut attestation = self
                            .produce_single_attestation_for_block(
                                attestation_slot,
                                bc.index,
                                head_block_root.into(),
                                Cow::Borrowed(state),
                                state_root,
                                i,
                                *validator_index,
                            )
                            .unwrap();

                        attestation.signature = {
                            let domain = self.spec.get_domain(
                                attestation.data.target.epoch,
                                Domain::BeaconAttester,
                                &fork,
                                state.genesis_validators_root(),
                            );

                            let message = attestation.data.signing_root(domain);

                            let mut agg_sig = AggregateSignature::infinity();

                            agg_sig.add_assign(
                                &self.validator_keypairs[*validator_index].sk.sign(message),
                            );

                            agg_sig
                        };

                        let subnet_id = SubnetId::compute_subnet_for_single_attestation::<E>(
                            &attestation,
                            committee_count,
                            &self.chain.spec,
                        )
                        .unwrap();

                        Some(((attestation, subnet_id), validator_index))
                    })
                    .unzip::<_, _, Vec<_>, Vec<_>>()
            })
            .unzip::<_, _, Vec<_>, Vec<_>>();

        // Flatten attesters.
        let attesters = split_attesters.into_iter().flatten().collect::<Vec<_>>();

        if let Some(limit) = limit {
            assert_eq!(limit, num_attesters.load(Ordering::Relaxed));
            assert_eq!(
                limit,
                attesters.len(),
                "failed to generate `limit` attestations"
            );
        }
        (attestations, attesters)
    }

    pub fn make_unaggregated_attestations_with_opts(
        &self,
        attesting_validators: &[usize],
        state: &BeaconState<E>,
        state_root: Hash256,
        head_block_root: SignedBeaconBlockHash,
        attestation_slot: Slot,
        opts: MakeAttestationOptions,
    ) -> (Vec<CommitteeAttestations<E>>, Vec<usize>) {
        let MakeAttestationOptions {
            limit,
            fork,
            payload_present_override,
        } = opts;
        let committee_count = state.get_committee_count_at_slot(state.slot()).unwrap();
        let num_attesters = AtomicUsize::new(0);

        let (attestations, split_attesters) = state
            .get_beacon_committees_at_slot(attestation_slot)
            .expect("should get committees")
            .iter()
            .map(|bc| {
                bc.committee
                    .par_iter()
                    .enumerate()
                    .filter_map(|(i, validator_index)| {
                        if !attesting_validators.contains(validator_index) {
                            return None;
                        }

                        if let Some(limit) = limit {
                            // This atomics stuff is necessary because we're under a par_iter,
                            // and Rayon will deadlock if we use a mutex.
                            if num_attesters.fetch_add(1, Ordering::Relaxed) >= limit {
                                num_attesters.fetch_sub(1, Ordering::Relaxed);
                                return None;
                            }
                        }

                        let mut attestation = self
                            .produce_unaggregated_attestation_for_block(
                                attestation_slot,
                                bc.index,
                                head_block_root.into(),
                                Cow::Borrowed(state),
                                state_root,
                                payload_present_override,
                            )
                            .unwrap();

                        match attestation {
                            Attestation::Base(ref mut att) => {
                                att.aggregation_bits.set(i, true).unwrap()
                            }
                            Attestation::Electra(ref mut att) => {
                                att.aggregation_bits.set(i, true).unwrap()
                            }
                        }

                        *attestation.signature_mut() = {
                            let domain = self.spec.get_domain(
                                attestation.data().target.epoch,
                                Domain::BeaconAttester,
                                &fork,
                                state.genesis_validators_root(),
                            );

                            let message = attestation.data().signing_root(domain);

                            let mut agg_sig = AggregateSignature::infinity();

                            agg_sig.add_assign(
                                &self.validator_keypairs[*validator_index].sk.sign(message),
                            );

                            agg_sig
                        };

                        let subnet_id = SubnetId::compute_subnet_for_attestation::<E>(
                            attestation.to_ref(),
                            committee_count,
                            &self.chain.spec,
                        )
                        .unwrap();

                        Some(((attestation, subnet_id), validator_index))
                    })
                    .unzip::<_, _, Vec<_>, Vec<_>>()
            })
            .unzip::<_, _, Vec<_>, Vec<_>>();

        // Flatten attesters.
        let attesters = split_attesters.into_iter().flatten().collect::<Vec<_>>();

        if let Some(limit) = limit {
            assert_eq!(limit, num_attesters.load(Ordering::Relaxed));
            assert_eq!(
                limit,
                attesters.len(),
                "failed to generate `limit` attestations"
            );
        }
        (attestations, attesters)
    }

    /// A list of sync messages for the given state.
    pub fn make_sync_committee_messages(
        &self,
        state: &BeaconState<E>,
        head_block_root: Hash256,
        message_slot: Slot,
        relative_sync_committee: RelativeSyncCommittee,
    ) -> Vec<Vec<(SyncCommitteeMessage, usize)>> {
        let sync_committee: Arc<SyncCommittee<E>> = match relative_sync_committee {
            RelativeSyncCommittee::Current => state
                .current_sync_committee()
                .expect("should be called on altair beacon state")
                .clone(),
            RelativeSyncCommittee::Next => state
                .next_sync_committee()
                .expect("should be called on altair beacon state")
                .clone(),
        };
        let fork = self
            .spec
            .fork_at_epoch(message_slot.epoch(E::slots_per_epoch()));

        sync_committee
            .pubkeys
            .as_ref()
            .chunks(E::sync_subcommittee_size())
            .map(|subcommittee| {
                subcommittee
                    .iter()
                    .enumerate()
                    .map(|(subcommittee_position, pubkey)| {
                        let validator_index = self
                            .chain
                            .validator_index(pubkey)
                            .expect("should find validator index")
                            .expect("pubkey should exist in the beacon chain");

                        let sync_message = SyncCommitteeMessage::new::<E>(
                            message_slot,
                            head_block_root,
                            validator_index as u64,
                            &self.validator_keypairs[validator_index].sk,
                            &fork,
                            state.genesis_validators_root(),
                            &self.spec,
                        );

                        (sync_message, subcommittee_position)
                    })
                    .collect()
            })
            .collect()
    }

    /// A list of attestations for each committee for the given slot.
    ///
    /// The first layer of the Vec is organised per committee. For example, if the return value is
    /// called `all_attestations`, then all attestations in `all_attestations[0]` will be for
    /// committee 0, whilst all in `all_attestations[1]` will be for committee 1.
    pub fn get_unaggregated_attestations(
        &self,
        attestation_strategy: &AttestationStrategy,
        state: &BeaconState<E>,
        state_root: Hash256,
        head_block_root: Hash256,
        attestation_slot: Slot,
    ) -> Vec<Vec<(Attestation<E>, SubnetId)>> {
        let validators: Vec<usize> = match attestation_strategy {
            AttestationStrategy::AllValidators => self.get_all_validators(),
            AttestationStrategy::SomeValidators(vals) => vals.clone(),
        };
        self.make_unaggregated_attestations(
            &validators,
            state,
            state_root,
            head_block_root.into(),
            attestation_slot,
        )
    }

    /// A list of attestations for each committee for the given slot.
    ///
    /// The first layer of the Vec is organised per committee. For example, if the return value is
    /// called `all_attestations`, then all attestations in `all_attestations[0]` will be for
    /// committee 0, whilst all in `all_attestations[1]` will be for committee 1.
    pub fn get_single_attestations(
        &self,
        attestation_strategy: &AttestationStrategy,
        state: &BeaconState<E>,
        state_root: Hash256,
        head_block_root: Hash256,
        attestation_slot: Slot,
    ) -> Vec<Vec<(SingleAttestation, SubnetId)>> {
        let validators: Vec<usize> = match attestation_strategy {
            AttestationStrategy::AllValidators => self.get_all_validators(),
            AttestationStrategy::SomeValidators(vals) => vals.clone(),
        };
        self.make_single_attestations(
            &validators,
            state,
            state_root,
            head_block_root.into(),
            attestation_slot,
        )
    }

    pub fn make_attestations(
        &self,
        attesting_validators: &[usize],
        state: &BeaconState<E>,
        state_root: Hash256,
        block_hash: SignedBeaconBlockHash,
        slot: Slot,
    ) -> HarnessAttestations<E> {
        self.make_attestations_with_limit(
            attesting_validators,
            state,
            state_root,
            block_hash,
            slot,
            None,
        )
        .0
    }

    /// Produce exactly `limit` attestations.
    ///
    /// Return attestations and vec of validator indices that attested.
    pub fn make_attestations_with_limit(
        &self,
        attesting_validators: &[usize],
        state: &BeaconState<E>,
        state_root: Hash256,
        block_hash: SignedBeaconBlockHash,
        slot: Slot,
        limit: Option<usize>,
    ) -> (HarnessAttestations<E>, Vec<usize>) {
        let fork = self.spec.fork_at_epoch(slot.epoch(E::slots_per_epoch()));
        self.make_attestations_with_opts(
            attesting_validators,
            state,
            state_root,
            block_hash,
            slot,
            MakeAttestationOptions {
                limit,
                fork,
                payload_present_override: None,
            },
        )
    }

    pub fn make_attestations_with_opts(
        &self,
        attesting_validators: &[usize],
        state: &BeaconState<E>,
        state_root: Hash256,
        block_hash: SignedBeaconBlockHash,
        slot: Slot,
        opts: MakeAttestationOptions,
    ) -> (HarnessAttestations<E>, Vec<usize>) {
        let MakeAttestationOptions { fork, .. } = opts;
        let (unaggregated_attestations, attesters) = self.make_unaggregated_attestations_with_opts(
            attesting_validators,
            state,
            state_root,
            block_hash,
            slot,
            opts,
        );

        let aggregated_attestations: Vec<Option<SignedAggregateAndProof<E>>> =
            unaggregated_attestations
                .iter()
                .map(|committee_attestations| {
                    // If there are any attestations in this committee, create an aggregate.
                    if let Some((attestation, _)) = committee_attestations.first() {
                        let bc = state
                            .get_beacon_committee(
                                attestation.data().slot,
                                attestation.committee_index().unwrap(),
                            )
                            .unwrap();

                        // Find an aggregator if one exists. Return `None` if there are no
                        // aggregators.
                        let aggregator_index = bc
                            .committee
                            .iter()
                            .find(|&validator_index| {
                                if !attesters.contains(validator_index) {
                                    return false;
                                }

                                let selection_proof = SelectionProof::new::<E>(
                                    slot,
                                    &self.validator_keypairs[*validator_index].sk,
                                    &fork,
                                    state.genesis_validators_root(),
                                    &self.spec,
                                );

                                selection_proof
                                    .is_aggregator(bc.committee.len(), &self.spec)
                                    .unwrap_or(false)
                            })
                            .copied()?;

                        let fork_name = self.spec.fork_name_at_slot::<E>(slot);

                        let aggregate = if fork_name.electra_enabled() {
                            self.chain.get_aggregated_attestation_electra(
                                slot,
                                &attestation.data().tree_hash_root(),
                                bc.index,
                            )
                        } else {
                            self.chain
                                .get_aggregated_attestation_base(attestation.data())
                        }
                        .unwrap()
                        .unwrap_or_else(|| {
                            committee_attestations.iter().skip(1).fold(
                                attestation.clone(),
                                |mut agg, (att, _)| {
                                    agg.aggregate(att.to_ref());
                                    agg
                                },
                            )
                        });

                        // If the chain is able to produce an aggregate, use that. Otherwise, build an
                        // aggregate locally.

                        let signed_aggregate = SignedAggregateAndProof::from_aggregate(
                            aggregator_index as u64,
                            aggregate.to_ref(),
                            None,
                            &self.validator_keypairs[aggregator_index].sk,
                            &fork,
                            state.genesis_validators_root(),
                            &self.spec,
                        );

                        Some(signed_aggregate)
                    } else {
                        None
                    }
                })
                .collect();

        (
            unaggregated_attestations
                .into_iter()
                .zip(aggregated_attestations)
                .collect(),
            attesters,
        )
    }

    pub fn make_sync_contributions(
        &self,
        state: &BeaconState<E>,
        block_hash: Hash256,
        slot: Slot,
        relative_sync_committee: RelativeSyncCommittee,
    ) -> HarnessSyncContributions<E> {
        let sync_messages =
            self.make_sync_committee_messages(state, block_hash, slot, relative_sync_committee);

        let sync_contributions: Vec<Option<SignedContributionAndProof<E>>> = sync_messages
            .iter()
            .enumerate()
            .map(|(subnet_id, committee_messages)| {
                // If there are any sync messages in this committee, create an aggregate.
                if let Some((sync_message, subcommittee_position)) = committee_messages.first() {
                    let sync_committee: Arc<SyncCommittee<E>> = state
                        .current_sync_committee()
                        .expect("should be called on altair beacon state")
                        .clone();

                    let aggregator_index = sync_committee
                        .get_subcommittee_pubkeys(subnet_id)
                        .unwrap()
                        .iter()
                        .find_map(|pubkey| {
                            let validator_index = self
                                .chain
                                .validator_index(pubkey)
                                .expect("should find validator index")
                                .expect("pubkey should exist in the beacon chain");

                            let selection_proof = SyncSelectionProof::new::<E>(
                                slot,
                                subnet_id as u64,
                                &self.validator_keypairs[validator_index].sk,
                                &state.fork(),
                                state.genesis_validators_root(),
                                &self.spec,
                            );

                            selection_proof
                                .is_aggregator::<E>()
                                .expect("should determine aggregator")
                                .then_some(validator_index)
                        })?;

                    let default = SyncCommitteeContribution::from_message(
                        sync_message,
                        subnet_id as u64,
                        *subcommittee_position,
                    )
                    .expect("should derive sync contribution");

                    let aggregate = committee_messages.iter().skip(1).fold(
                        default,
                        |mut agg, (sig, position)| {
                            let contribution = SyncCommitteeContribution::from_message(
                                sig,
                                subnet_id as u64,
                                *position,
                            )
                            .expect("should derive sync contribution");
                            agg.aggregate(&contribution);
                            agg
                        },
                    );

                    let signed_aggregate = SignedContributionAndProof::from_aggregate(
                        aggregator_index as u64,
                        aggregate,
                        None,
                        &self.validator_keypairs[aggregator_index].sk,
                        &state.fork(),
                        state.genesis_validators_root(),
                        &self.spec,
                    );

                    Some(signed_aggregate)
                } else {
                    None
                }
            })
            .collect();

        sync_messages.into_iter().zip(sync_contributions).collect()
    }

    pub fn make_attester_slashing(&self, validator_indices: Vec<u64>) -> AttesterSlashing<E> {
        self.make_attester_slashing_with_epochs(validator_indices, None, None, None, None)
    }

    pub fn make_attester_slashing_with_epochs(
        &self,
        validator_indices: Vec<u64>,
        source1: Option<Epoch>,
        target1: Option<Epoch>,
        source2: Option<Epoch>,
        target2: Option<Epoch>,
    ) -> AttesterSlashing<E> {
        let fork = self.chain.canonical_head.cached_head().head_fork();

        let fork_name = self.spec.fork_name_at_slot::<E>(Slot::new(0));

        let data = AttestationData {
            slot: Slot::new(0),
            index: 0,
            beacon_block_root: Hash256::zero(),
            target: Checkpoint {
                root: Hash256::zero(),
                epoch: target1.unwrap_or(fork.epoch),
            },
            source: Checkpoint {
                root: Hash256::zero(),
                epoch: source1.unwrap_or(Epoch::new(0)),
            },
        };
        let mut attestation_1 = if fork_name.electra_enabled() {
            IndexedAttestation::Electra(IndexedAttestationElectra {
                attesting_indices: VariableList::new(validator_indices).unwrap(),
                data,
                signature: AggregateSignature::infinity(),
            })
        } else {
            IndexedAttestation::Base(IndexedAttestationBase {
                attesting_indices: VariableList::new(validator_indices).unwrap(),
                data,
                signature: AggregateSignature::infinity(),
            })
        };

        let mut attestation_2 = attestation_1.clone();
        attestation_2.data_mut().index += 1;
        attestation_2.data_mut().source.epoch = source2.unwrap_or(Epoch::new(0));
        attestation_2.data_mut().target.epoch = target2.unwrap_or(fork.epoch);

        for attestation in &mut [&mut attestation_1, &mut attestation_2] {
            match attestation {
                IndexedAttestation::Base(attestation) => {
                    for i in attestation.attesting_indices.iter() {
                        let sk = &self.validator_keypairs[*i as usize].sk;

                        let genesis_validators_root = self.chain.genesis_validators_root;

                        let domain = self.chain.spec.get_domain(
                            attestation.data.target.epoch,
                            Domain::BeaconAttester,
                            &fork,
                            genesis_validators_root,
                        );
                        let message = attestation.data.signing_root(domain);

                        attestation.signature.add_assign(&sk.sign(message));
                    }
                }
                IndexedAttestation::Electra(attestation) => {
                    for i in attestation.attesting_indices.iter() {
                        let sk = &self.validator_keypairs[*i as usize].sk;

                        let genesis_validators_root = self.chain.genesis_validators_root;

                        let domain = self.chain.spec.get_domain(
                            attestation.data.target.epoch,
                            Domain::BeaconAttester,
                            &fork,
                            genesis_validators_root,
                        );
                        let message = attestation.data.signing_root(domain);

                        attestation.signature.add_assign(&sk.sign(message));
                    }
                }
            }
        }

        if fork_name.electra_enabled() {
            AttesterSlashing::Electra(AttesterSlashingElectra {
                attestation_1: attestation_1.as_electra().unwrap().clone(),
                attestation_2: attestation_2.as_electra().unwrap().clone(),
            })
        } else {
            AttesterSlashing::Base(AttesterSlashingBase {
                attestation_1: attestation_1.as_base().unwrap().clone(),
                attestation_2: attestation_2.as_base().unwrap().clone(),
            })
        }
    }

    pub fn make_attester_slashing_different_indices(
        &self,
        validator_indices_1: Vec<u64>,
        validator_indices_2: Vec<u64>,
    ) -> AttesterSlashing<E> {
        let fork_name = self.spec.fork_name_at_slot::<E>(Slot::new(0));

        let data = AttestationData {
            slot: Slot::new(0),
            index: 0,
            beacon_block_root: Hash256::zero(),
            target: Checkpoint {
                root: Hash256::zero(),
                epoch: Epoch::new(0),
            },
            source: Checkpoint {
                root: Hash256::zero(),
                epoch: Epoch::new(0),
            },
        };

        let (mut attestation_1, mut attestation_2) = if fork_name.electra_enabled() {
            let attestation_1 = IndexedAttestationElectra {
                attesting_indices: VariableList::new(validator_indices_1).unwrap(),
                data: data.clone(),
                signature: AggregateSignature::infinity(),
            };

            let attestation_2 = IndexedAttestationElectra {
                attesting_indices: VariableList::new(validator_indices_2).unwrap(),
                data,
                signature: AggregateSignature::infinity(),
            };

            (
                IndexedAttestation::Electra(attestation_1),
                IndexedAttestation::Electra(attestation_2),
            )
        } else {
            let attestation_1 = IndexedAttestationBase {
                attesting_indices: VariableList::new(validator_indices_1).unwrap(),
                data: data.clone(),
                signature: AggregateSignature::infinity(),
            };

            let attestation_2 = IndexedAttestationBase {
                attesting_indices: VariableList::new(validator_indices_2).unwrap(),
                data,
                signature: AggregateSignature::infinity(),
            };

            (
                IndexedAttestation::Base(attestation_1),
                IndexedAttestation::Base(attestation_2),
            )
        };

        attestation_2.data_mut().index += 1;

        let fork = self.chain.canonical_head.cached_head().head_fork();
        for attestation in &mut [&mut attestation_1, &mut attestation_2] {
            match attestation {
                IndexedAttestation::Base(attestation) => {
                    for i in attestation.attesting_indices.iter() {
                        let sk = &self.validator_keypairs[*i as usize].sk;

                        let genesis_validators_root = self.chain.genesis_validators_root;

                        let domain = self.chain.spec.get_domain(
                            attestation.data.target.epoch,
                            Domain::BeaconAttester,
                            &fork,
                            genesis_validators_root,
                        );
                        let message = attestation.data.signing_root(domain);

                        attestation.signature.add_assign(&sk.sign(message));
                    }
                }
                IndexedAttestation::Electra(attestation) => {
                    for i in attestation.attesting_indices.iter() {
                        let sk = &self.validator_keypairs[*i as usize].sk;

                        let genesis_validators_root = self.chain.genesis_validators_root;

                        let domain = self.chain.spec.get_domain(
                            attestation.data.target.epoch,
                            Domain::BeaconAttester,
                            &fork,
                            genesis_validators_root,
                        );
                        let message = attestation.data.signing_root(domain);

                        attestation.signature.add_assign(&sk.sign(message));
                    }
                }
            }
        }

        if fork_name.electra_enabled() {
            AttesterSlashing::Electra(AttesterSlashingElectra {
                attestation_1: attestation_1.as_electra().unwrap().clone(),
                attestation_2: attestation_2.as_electra().unwrap().clone(),
            })
        } else {
            AttesterSlashing::Base(AttesterSlashingBase {
                attestation_1: attestation_1.as_base().unwrap().clone(),
                attestation_2: attestation_2.as_base().unwrap().clone(),
            })
        }
    }

    pub fn make_proposer_slashing(&self, validator_index: u64) -> ProposerSlashing {
        self.make_proposer_slashing_at_slot(validator_index, None)
    }

    pub fn make_proposer_slashing_at_slot(
        &self,
        validator_index: u64,
        slot_override: Option<Slot>,
    ) -> ProposerSlashing {
        let mut block_header_1 = self.chain.head_beacon_block().message().block_header();
        block_header_1.proposer_index = validator_index;
        if let Some(slot) = slot_override {
            block_header_1.slot = slot;
        }

        let mut block_header_2 = block_header_1.clone();
        block_header_2.state_root = Hash256::zero();

        let sk = &self.validator_keypairs[validator_index as usize].sk;
        let fork = self.chain.canonical_head.cached_head().head_fork();
        let genesis_validators_root = self.chain.genesis_validators_root;

        let mut signed_block_headers = vec![block_header_1, block_header_2]
            .into_iter()
            .map(|block_header| {
                block_header.sign::<E>(sk, &fork, genesis_validators_root, &self.chain.spec)
            })
            .collect::<Vec<_>>();

        ProposerSlashing {
            signed_header_2: signed_block_headers.remove(1),
            signed_header_1: signed_block_headers.remove(0),
        }
    }

    pub fn make_voluntary_exit(&self, validator_index: u64, epoch: Epoch) -> SignedVoluntaryExit {
        let sk = &self.validator_keypairs[validator_index as usize].sk;
        let genesis_validators_root = self.chain.genesis_validators_root;

        VoluntaryExit {
            epoch,
            validator_index,
        }
        .sign(sk, genesis_validators_root, &self.chain.spec)
    }

    pub fn add_proposer_slashing(&self, validator_index: u64) -> Result<(), String> {
        let propposer_slashing = self.make_proposer_slashing(validator_index);
        if let ObservationOutcome::New(verified_proposer_slashing) = self
            .chain
            .verify_proposer_slashing_for_gossip(propposer_slashing)
            .expect("should verify proposer slashing for gossip")
        {
            self.chain
                .import_proposer_slashing(verified_proposer_slashing);
            Ok(())
        } else {
            Err("should observe new proposer slashing".to_string())
        }
    }

    pub fn add_attester_slashing(&self, validator_indices: Vec<u64>) -> Result<(), String> {
        let attester_slashing = self.make_attester_slashing(validator_indices);
        if let ObservationOutcome::New(verified_attester_slashing) = self
            .chain
            .verify_attester_slashing_for_gossip(attester_slashing)
            .expect("should verify attester slashing for gossip")
        {
            self.chain
                .import_attester_slashing(verified_attester_slashing);
            Ok(())
        } else {
            Err("should observe new attester slashing".to_string())
        }
    }

    pub fn add_bls_to_execution_change(
        &self,
        validator_index: u64,
        address: Address,
    ) -> Result<(), String> {
        let signed_bls_change = self.make_bls_to_execution_change(validator_index, address);
        if let ObservationOutcome::New(verified_bls_change) = self
            .chain
            .verify_bls_to_execution_change_for_gossip(signed_bls_change)
            .expect("should verify BLS to execution change for gossip")
        {
            self.chain
                .import_bls_to_execution_change(verified_bls_change, ReceivedPreCapella::No)
                .then_some(())
                .ok_or("should import BLS to execution change to the op pool".to_string())
        } else {
            Err("should observe new BLS to execution change".to_string())
        }
    }

    pub fn make_bls_to_execution_change(
        &self,
        validator_index: u64,
        address: Address,
    ) -> SignedBlsToExecutionChange {
        let keypair = self.get_withdrawal_keypair(validator_index);
        self.make_bls_to_execution_change_with_keys(
            validator_index,
            address,
            &keypair.pk,
            &keypair.sk,
        )
    }

    pub fn make_bls_to_execution_change_with_keys(
        &self,
        validator_index: u64,
        address: Address,
        pubkey: &PublicKey,
        secret_key: &SecretKey,
    ) -> SignedBlsToExecutionChange {
        let genesis_validators_root = self.chain.genesis_validators_root;
        BlsToExecutionChange {
            validator_index,
            from_bls_pubkey: pubkey.compress(),
            to_execution_address: address,
        }
        .sign(secret_key, genesis_validators_root, &self.chain.spec)
    }

    pub fn get_withdrawal_keypair(&self, validator_index: u64) -> &Keypair {
        self.withdrawal_keypairs
            .get(validator_index as usize)
            .expect("BLS withdrawal key missing from harness")
            .as_ref()
            .expect("no withdrawal key for validator")
    }

    pub fn add_voluntary_exit(
        &self,
        block: &mut BeaconBlock<E>,
        validator_index: u64,
        epoch: Epoch,
    ) {
        let exit = self.make_voluntary_exit(validator_index, epoch);
        block.body_mut().voluntary_exits_mut().push(exit).unwrap();
    }

    /// Create a new block, apply `block_modifier` to it, sign it and return it.
    ///
    /// The state returned is a pre-block state at the same slot as the produced block.
    pub async fn make_block_with_modifier(
        &self,
        state: BeaconState<E>,
        slot: Slot,
        block_modifier: impl FnOnce(&mut BeaconBlock<E>),
    ) -> (SignedBlockContentsTuple<E>, BeaconState<E>) {
        assert_ne!(slot, 0, "can't produce a block at slot 0");
        assert!(slot >= state.slot());

        let ((block, blobs), state) = self.make_block_return_pre_state(state, slot).await;

        let (mut block, _) = (*block).clone().deconstruct();

        block_modifier(&mut block);

        let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap();

        let signed_block = block.sign(
            &self.validator_keypairs[proposer_index].sk,
            &state.fork(),
            state.genesis_validators_root(),
            &self.spec,
        );
        ((Arc::new(signed_block), blobs), state)
    }

    pub async fn make_blob_with_modifier(
        &self,
        state: BeaconState<E>,
        slot: Slot,
        blob_modifier: impl FnOnce(&mut BlobsList<E>),
    ) -> (SignedBlockContentsTuple<E>, BeaconState<E>) {
        assert_ne!(slot, 0, "can't produce a block at slot 0");
        assert!(slot >= state.slot());

        let ((block, mut blobs), state) = self.make_block_return_pre_state(state, slot).await;

        let (block, _) = (*block).clone().deconstruct();

        blob_modifier(&mut blobs.as_mut().unwrap().1);

        let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap();

        let signed_block = block.sign(
            &self.validator_keypairs[proposer_index].sk,
            &state.fork(),
            state.genesis_validators_root(),
            &self.spec,
        );
        ((Arc::new(signed_block), blobs), state)
    }

    pub fn make_deposits<'a>(
        &self,
        state: &'a mut BeaconState<E>,
        num_deposits: usize,
        invalid_pubkey: Option<PublicKeyBytes>,
        invalid_signature: Option<SignatureBytes>,
    ) -> (Vec<Deposit>, &'a mut BeaconState<E>) {
        let mut datas = vec![];

        for _ in 0..num_deposits {
            let keypair = Keypair::random();
            let pubkeybytes = PublicKeyBytes::from(keypair.pk.clone());

            let mut data = DepositData {
                pubkey: pubkeybytes,
                withdrawal_credentials: Hash256::from_slice(
                    &get_withdrawal_credentials(&keypair.pk, self.spec.bls_withdrawal_prefix_byte)
                        [..],
                ),
                amount: self.spec.min_deposit_amount,
                signature: SignatureBytes::empty(),
            };

            data.signature = data.create_signature(&keypair.sk, &self.spec);

            if let Some(invalid_pubkey) = invalid_pubkey {
                data.pubkey = invalid_pubkey;
            }
            if let Some(invalid_signature) = invalid_signature.clone() {
                data.signature = invalid_signature;
            }
            datas.push(data);
        }

        // Vector containing all leaves
        let leaves = datas
            .iter()
            .map(|data| data.tree_hash_root())
            .collect::<Vec<_>>();

        // Building a VarList from leaves
        let deposit_data_list = VariableList::<_, U4294967296>::try_from(leaves.clone()).unwrap();

        // Setting the deposit_root to be the tree_hash_root of the VarList
        state.eth1_data_mut().deposit_root = deposit_data_list.tree_hash_root();
        state.eth1_data_mut().deposit_count = num_deposits as u64;
        *state.eth1_deposit_index_mut() = 0;

        // Building the merkle tree used for generating proofs
        let tree = MerkleTree::create(&leaves[..], self.spec.deposit_contract_tree_depth as usize);

        // Building proofs
        let mut proofs = vec![];
        for i in 0..leaves.len() {
            let (_, mut proof) = tree
                .generate_proof(i, self.spec.deposit_contract_tree_depth as usize)
                .expect("should generate proof");
            proof.push(Hash256::from_slice(&int_to_bytes32(leaves.len() as u64)));
            proofs.push(proof);
        }

        // Building deposits
        let deposits = datas
            .into_par_iter()
            .zip(proofs.into_par_iter())
            .map(|(data, proof)| (data, proof.try_into().unwrap()))
            .map(|(data, proof)| Deposit { proof, data })
            .collect::<Vec<_>>();

        // Pushing deposits to block body
        (deposits, state)
    }

    pub async fn process_block(
        &self,
        slot: Slot,
        block_root: Hash256,
        block_contents: SignedBlockContentsTuple<E>,
    ) -> Result<SignedBeaconBlockHash, BlockError> {
        self.set_current_slot(slot);
        let (block, blob_items) = block_contents;
        // Determine if block is available: it's available if it doesn't require blobs,
        // or if it requires blobs and we have them
        let has_blob_commitments = block
            .message()
            .body()
            .blob_kzg_commitments()
            .is_ok_and(|c| !c.is_empty());
        let is_available = !has_blob_commitments || blob_items.is_some();
        let block_hash: SignedBeaconBlockHash = if !is_available {
            self.chain
                .process_block(
                    block_root,
                    LookupBlock::new(block),
                    NotifyExecutionLayer::Yes,
                    BlockImportSource::Lookup,
                    || Ok(()),
                )
                .await?
                .try_into()
                .expect("block blobs are available")
        } else {
            let range_sync_block = self.build_range_sync_block_from_blobs(block, blob_items)?;
            self.chain
                .process_block(
                    block_root,
                    range_sync_block,
                    NotifyExecutionLayer::Yes,
                    BlockImportSource::RangeSync,
                    || Ok(()),
                )
                .await?
                .try_into()
                .expect("block blobs are available")
        };

        self.chain.recompute_head_at_current_slot().await;
        Ok(block_hash)
    }

    pub async fn process_block_result(
        &self,
        block_contents: SignedBlockContentsTuple<E>,
    ) -> Result<SignedBeaconBlockHash, BlockError> {
        let (block, blob_items) = block_contents;

        let block_root = block.canonical_root();
        // Determine if block is available: it's available if it doesn't require blobs,
        // or if it requires blobs and we have them
        let has_blob_commitments = block
            .message()
            .body()
            .blob_kzg_commitments()
            .is_ok_and(|c| !c.is_empty());
        let is_available = !has_blob_commitments || blob_items.is_some();
        let block_hash: SignedBeaconBlockHash = if is_available {
            let range_sync_block = self.build_range_sync_block_from_blobs(block, blob_items)?;
            self.chain
                .process_block(
                    block_root,
                    range_sync_block,
                    NotifyExecutionLayer::Yes,
                    BlockImportSource::RangeSync,
                    || Ok(()),
                )
                .await?
                .try_into()
                .expect("block blobs are available")
        } else {
            self.chain
                .process_block(
                    block_root,
                    LookupBlock::new(block),
                    NotifyExecutionLayer::Yes,
                    BlockImportSource::Lookup,
                    || Ok(()),
                )
                .await?
                .try_into()
                .expect("block blobs are available")
        };

        self.chain.recompute_head_at_current_slot().await;
        Ok(block_hash)
    }

    /// Verify and process (with fork choice) an execution payload envelope for a Gloas block.
    pub async fn process_envelope(
        &self,
        block_root: Hash256,
        signed_envelope: SignedExecutionPayloadEnvelope<E>,
        state: &BeaconState<E>,
        block_state_root: Hash256,
    ) {
        debug!(
            slot = %signed_envelope.slot(),
            "Processing execution payload envelope"
        );

        state_processing::envelope_processing::verify_execution_payload_envelope(
            state,
            &signed_envelope,
            state_processing::VerifySignatures::True,
            block_state_root,
            &self.spec,
        )
        .expect("should verify envelope");

        // Notify the EL of the new payload so forkchoiceUpdated can reference it.
        let block = self
            .chain
            .store
            .get_blinded_block(&block_root)
            .expect("should read block from store")
            .expect("block should exist in store");

        let bid = &block
            .message()
            .body()
            .signed_execution_payload_bid()
            .expect("Gloas block should have a payload bid")
            .message;

        let versioned_hashes = bid
            .blob_kzg_commitments
            .iter()
            .map(kzg_commitment_to_versioned_hash)
            .collect();

        let request = NewPayloadRequest::Gloas(NewPayloadRequestGloas {
            execution_payload: &signed_envelope.message.payload,
            versioned_hashes,
            parent_beacon_block_root: block.message().parent_root(),
            execution_requests: &signed_envelope.message.execution_requests,
        });

        self.chain
            .execution_layer
            .as_ref()
            .expect("harness should have execution layer")
            .notify_new_payload(request)
            .await
            .expect("newPayload should succeed");

        // Store the envelope and the data columns derived from the block.
        //
        // Production stores columns inside `import_available_execution_payload_envelope` after
        // the cache is satisfied. The harness sidesteps that flow but must still persist columns
        // or the `DataColumnMissing` invariant fires for any block with `num_expected_blobs > 0`.
        let block = self
            .chain
            .store
            .get_blinded_block(&block_root)
            .expect("should read block from store")
            .expect("block should exist in store");
        let mut ops = vec![];
        let block_with_full_payload = self
            .chain
            .store
            .make_full_block(&block_root, block.clone())
            .expect("should reconstruct full block");
        let columns =
            generate_data_column_sidecars_from_block(&block_with_full_payload, &self.spec);
        if !columns.is_empty()
            && let Some(store_op) = self.chain.get_blobs_or_columns_store_op(
                block_root,
                block.slot(),
                AvailableBlockData::DataColumns(columns),
            )
        {
            ops.push(store_op);
        }
        ops.push(store::StoreOp::PutPayloadEnvelope(
            block_root,
            std::sync::Arc::new(signed_envelope),
        ));
        self.chain
            .store
            .do_atomically_with_block_and_blobs_cache(ops)
            .expect("should persist envelope and columns");

        // Update fork choice so it knows the payload was received.
        self.chain
            .canonical_head
            .fork_choice_write_lock()
            .on_valid_payload_envelope_received(block_root)
            .expect("should update fork choice with envelope");

        // Run fork choice because the envelope could become the head.
        self.chain.recompute_head_at_current_slot().await;
    }

    /// Builds a `RangeSyncBlock` from a `SignedBeaconBlock` and blobs or data columns retrieved from
    /// the database.
    pub fn build_range_sync_block_from_store_blobs(
        &self,
        block_root: Option<Hash256>,
        block: Arc<SignedBeaconBlock<E>>,
    ) -> RangeSyncBlock<E> {
        let block_root = block_root.unwrap_or_else(|| get_block_root(&block));
        let is_gloas = block.fork_name_unchecked().gloas_enabled();
        // For Gloas, kzg commitments live in the bid (`signed_execution_payload_bid`), so the
        // body's `blob_kzg_commitments()` accessor returns Err. `num_expected_blobs` already
        // handles both shapes.
        let has_blobs = block.num_expected_blobs() > 0;
        if !has_blobs {
            return if is_gloas {
                let envelope = self
                    .chain
                    .get_payload_envelope(&block_root)
                    .unwrap()
                    .map(Arc::new)
                    .map(|envelope| AvailableEnvelope::new(envelope, vec![]));
                RangeSyncBlock::new_gloas(block, envelope).unwrap()
            } else {
                RangeSyncBlock::new(
                    block,
                    AvailableBlockData::NoData,
                    &self.chain.data_availability_checker,
                    self.chain.spec.clone(),
                )
                .unwrap()
            };
        }

        // Blobs are stored as data columns from Fulu (PeerDAS)
        if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) {
            let fork_name = self.spec.fork_name_at_epoch(block.epoch());
            let columns = self
                .chain
                .get_data_columns(&block_root, fork_name)
                .unwrap()
                .unwrap();
            let custody_columns = columns.into_iter().collect::<Vec<_>>();
            if is_gloas {
                let envelope = self
                    .chain
                    .get_payload_envelope(&block_root)
                    .unwrap()
                    .map(Arc::new)
                    .map(|envelope| AvailableEnvelope::new(envelope, custody_columns));
                RangeSyncBlock::new_gloas(block, envelope).unwrap()
            } else {
                let block_data = AvailableBlockData::new_with_data_columns(custody_columns);
                RangeSyncBlock::new(
                    block,
                    block_data,
                    &self.chain.data_availability_checker,
                    self.chain.spec.clone(),
                )
                .unwrap()
            }
        } else {
            let blobs = self.chain.get_blobs(&block_root).unwrap().blobs();
            let block_data = if let Some(blobs) = blobs {
                AvailableBlockData::new_with_blobs(blobs)
            } else {
                AvailableBlockData::NoData
            };

            RangeSyncBlock::new(
                block,
                block_data,
                &self.chain.data_availability_checker,
                self.chain.spec.clone(),
            )
            .unwrap()
        }
    }

    /// Builds a `RangeSyncBlock` from a `SignedBeaconBlock` and `BlobsList`.
    pub fn build_range_sync_block_from_blobs(
        &self,
        block: Arc<SignedBeaconBlock<E, FullPayload<E>>>,
        blob_items: Option<(KzgProofs<E>, BlobsList<E>)>,
    ) -> Result<RangeSyncBlock<E>, BlockError> {
        if block.fork_name_unchecked().gloas_enabled() {
            let columns = blob_items
                .map(|_| generate_data_column_sidecars_from_block(&block, &self.spec))
                .unwrap_or_default();
            let envelope = self
                .chain
                .get_payload_envelope(&block.canonical_root())
                .map_err(|e| BlockError::BeaconChainError(Box::new(e)))?
                .map(Arc::new)
                .map(|envelope| AvailableEnvelope::new(envelope, columns));
            return RangeSyncBlock::new_gloas(block, envelope).map_err(BlockError::InternalError);
        }

        Ok(if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) {
            let epoch = block.slot().epoch(E::slots_per_epoch());
            let sampling_columns = self.chain.sampling_columns_for_epoch(epoch);

            if blob_items.is_some_and(|(kzg_proofs, _)| !kzg_proofs.is_empty()) {
                // Note: this method ignores the actual custody columns and just take the first
                // `sampling_column_count` for testing purpose only, because the chain does not
                // currently have any knowledge of the columns being custodied.
                let columns = generate_data_column_sidecars_from_block(&block, &self.spec)
                    .into_iter()
                    .filter(|d| sampling_columns.contains(d.index()))
                    .collect::<Vec<_>>();
                let block_data = AvailableBlockData::new_with_data_columns(columns);
                RangeSyncBlock::new(
                    block,
                    block_data,
                    &self.chain.data_availability_checker,
                    self.chain.spec.clone(),
                )?
            } else {
                RangeSyncBlock::new(
                    block,
                    AvailableBlockData::NoData,
                    &self.chain.data_availability_checker,
                    self.chain.spec.clone(),
                )?
            }
        } else {
            let blobs = blob_items
                .map(|(proofs, blobs)| {
                    BlobSidecar::build_sidecars(blobs, &block, proofs, &self.spec)
                })
                .transpose()
                .unwrap();
            let block_data = if let Some(blobs) = blobs {
                AvailableBlockData::new_with_blobs(blobs)
            } else {
                AvailableBlockData::NoData
            };

            RangeSyncBlock::new(
                block,
                block_data,
                &self.chain.data_availability_checker,
                self.chain.spec.clone(),
            )?
        })
    }

    pub fn process_attestations(
        &self,
        attestations: HarnessAttestations<E>,
        state: &BeaconState<E>,
    ) {
        let num_validators = self.validator_keypairs.len();
        let mut unaggregated = Vec::with_capacity(num_validators);
        // This is an over-allocation, but it should be fine. It won't be *that* memory hungry and
        // it's nice to have fast tests.
        let mut aggregated = Vec::with_capacity(num_validators);

        for (unaggregated_attestations, maybe_signed_aggregate) in attestations.iter() {
            for (attn, subnet) in unaggregated_attestations {
                let aggregation_bits = attn.get_aggregation_bits();

                if aggregation_bits.len() != 1 {
                    panic!("Must be an unaggregated attestation")
                }

                let aggregation_bit = *aggregation_bits.first().unwrap();

                let committee = state
                    .get_beacon_committee(attn.data().slot, attn.committee_index().unwrap())
                    .unwrap();

                let attester_index = committee
                    .committee
                    .iter()
                    .enumerate()
                    .find_map(|(i, &index)| {
                        if aggregation_bit as usize == i {
                            return Some(index);
                        }
                        None
                    })
                    .unwrap();

                let single_attestation = attn
                    .to_single_attestation_with_attester_index(attester_index as u64)
                    .unwrap();

                unaggregated.push((single_attestation, Some(*subnet)));
            }

            if let Some(a) = maybe_signed_aggregate {
                aggregated.push(a)
            }
        }

        for result in self
            .chain
            .batch_verify_unaggregated_attestations_for_gossip(
                unaggregated.iter().map(|(attn, subnet)| (attn, *subnet)),
            )
            .unwrap()
        {
            let verified = result.unwrap();
            self.chain.add_to_naive_aggregation_pool(&verified).unwrap();
        }

        for result in self
            .chain
            .batch_verify_aggregated_attestations_for_gossip(aggregated.into_iter())
            .unwrap()
        {
            let verified = result.unwrap();
            self.chain
                .apply_attestation_to_fork_choice(&verified)
                .unwrap();
            self.chain.add_to_block_inclusion_pool(verified).unwrap();
        }
    }

    pub fn set_current_slot(&self, slot: Slot) {
        let current_slot = self.chain.slot().unwrap();
        let current_epoch = current_slot.epoch(E::slots_per_epoch());
        let epoch = slot.epoch(E::slots_per_epoch());
        assert!(
            epoch >= current_epoch,
            "Jumping backwards to an earlier epoch isn't well defined. \
             Please generate test blocks epoch-by-epoch instead."
        );
        self.chain.slot_clock.set_slot(slot.into());
    }

    pub async fn add_block_at_slot(
        &self,
        slot: Slot,
        state: BeaconState<E>,
    ) -> Result<
        (
            SignedBeaconBlockHash,
            SignedBlockContentsTuple<E>,
            BeaconState<E>,
        ),
        BlockError,
    > {
        self.set_current_slot(slot);
        let (block_contents, opt_envelope, new_state) =
            self.make_block_with_envelope(state, slot).await;

        let block_hash = self
            .process_block(
                slot,
                block_contents.0.canonical_root(),
                block_contents.clone(),
            )
            .await?;

        if let Some(envelope) = opt_envelope {
            let block_state_root = block_contents.0.state_root();
            self.process_envelope(block_hash.into(), envelope, &new_state, block_state_root)
                .await;
        }
        Ok((block_hash, block_contents, new_state))
    }

    pub fn attest_block(
        &self,
        state: &BeaconState<E>,
        state_root: Hash256,
        block_hash: SignedBeaconBlockHash,
        block: &SignedBeaconBlock<E>,
        validators: &[usize],
    ) {
        let attestations =
            self.make_attestations(validators, state, state_root, block_hash, block.slot());
        self.process_attestations(attestations, state);
    }

    pub fn sync_committee_sign_block(
        &self,
        state: &BeaconState<E>,
        block_hash: Hash256,
        slot: Slot,
        relative_sync_committee: RelativeSyncCommittee,
    ) {
        let sync_contributions =
            self.make_sync_contributions(state, block_hash, slot, relative_sync_committee);
        self.process_sync_contributions(sync_contributions).unwrap()
    }

    pub async fn add_attested_block_at_slot(
        &self,
        slot: Slot,
        state: BeaconState<E>,
        validators: &[usize],
    ) -> Result<(SignedBeaconBlockHash, BeaconState<E>), BlockError> {
        self.add_attested_block_at_slot_with_sync(
            slot,
            state,
            validators,
            SyncCommitteeStrategy::NoValidators,
        )
        .await
    }

    pub async fn add_attested_block_at_slot_with_sync(
        &self,
        slot: Slot,
        state: BeaconState<E>,
        validators: &[usize],
        sync_committee_strategy: SyncCommitteeStrategy,
    ) -> Result<(SignedBeaconBlockHash, BeaconState<E>), BlockError> {
        let (block_hash, block, mut new_state) = self.add_block_at_slot(slot, state).await?;
        let new_state_root = new_state.canonical_root().unwrap();
        self.attest_block(&new_state, new_state_root, block_hash, &block.0, validators);

        if sync_committee_strategy == SyncCommitteeStrategy::AllValidators
            && new_state.current_sync_committee().is_ok()
        {
            self.sync_committee_sign_block(
                &new_state,
                block_hash.into(),
                slot,
                if (slot + 1).epoch(E::slots_per_epoch())
                    % self.spec.epochs_per_sync_committee_period
                    == 0
                {
                    RelativeSyncCommittee::Next
                } else {
                    RelativeSyncCommittee::Current
                },
            );
        }

        Ok((block_hash, new_state))
    }

    pub async fn add_attested_blocks_at_slots(
        &self,
        state: BeaconState<E>,
        slots: &[Slot],
        validators: &[usize],
    ) -> AddBlocksResult<E> {
        self.add_attested_blocks_at_slots_with_sync(
            state,
            slots,
            validators,
            SyncCommitteeStrategy::NoValidators,
        )
        .await
    }

    pub async fn add_attested_blocks_at_slots_with_sync(
        &self,
        state: BeaconState<E>,
        slots: &[Slot],
        validators: &[usize],
        sync_committee_strategy: SyncCommitteeStrategy,
    ) -> AddBlocksResult<E> {
        assert!(!slots.is_empty());
        self.add_attested_blocks_at_slots_given_lbh(
            state,
            slots,
            validators,
            None,
            sync_committee_strategy,
        )
        .await
    }

    fn update_light_client_server_cache(
        &self,
        state: &BeaconState<E>,
        slot: Slot,
        block_root: Hash256,
    ) {
        let fork_name = state.fork_name(&self.spec).unwrap();
        if !fork_name.altair_enabled() {
            return;
        }

        let contributions =
            self.make_sync_contributions(state, block_root, slot, RelativeSyncCommittee::Current);

        for (_, contribution_and_proof) in contributions {
            let Some(contribution_and_proof) = contribution_and_proof else {
                continue;
            };
            let contribution = contribution_and_proof.message.contribution;
            self.chain
                .op_pool
                .insert_sync_contribution(contribution.clone())
                .unwrap();
            self.chain
                .op_pool
                .insert_sync_contribution(contribution)
                .unwrap();
        }

        let Some(sync_aggregate) = self.chain.op_pool.get_sync_aggregate(state).unwrap() else {
            return;
        };

        let _ = self
            .chain
            .light_client_server_cache
            .recompute_and_cache_updates(
                self.chain.store.clone(),
                slot,
                &block_root,
                &sync_aggregate,
                &self.spec,
            );
    }

    pub async fn add_attested_blocks_at_slots_with_lc_data(
        &self,
        mut state: BeaconState<E>,
        slots: &[Slot],
        validators: &[usize],
        mut latest_block_hash: Option<SignedBeaconBlockHash>,
        sync_committee_strategy: SyncCommitteeStrategy,
    ) -> AddBlocksResult<E> {
        assert!(slots.is_sorted(), "Slots have to be in ascending order");
        let mut block_hash_from_slot: HashMap<Slot, SignedBeaconBlockHash> = HashMap::new();
        let mut state_hash_from_slot: HashMap<Slot, BeaconStateHash> = HashMap::new();
        for slot in slots {
            let (block_hash, new_state) = self
                .add_attested_block_at_slot_with_sync(
                    *slot,
                    state,
                    validators,
                    sync_committee_strategy,
                )
                .await
                .unwrap();

            state = new_state;

            self.update_light_client_server_cache(&state, *slot, block_hash.into());

            block_hash_from_slot.insert(*slot, block_hash);
            state_hash_from_slot.insert(*slot, state.canonical_root().unwrap().into());
            latest_block_hash = Some(block_hash);
        }
        (
            block_hash_from_slot,
            state_hash_from_slot,
            latest_block_hash.unwrap(),
            state,
        )
    }

    async fn add_attested_blocks_at_slots_given_lbh(
        &self,
        mut state: BeaconState<E>,
        slots: &[Slot],
        validators: &[usize],
        mut latest_block_hash: Option<SignedBeaconBlockHash>,
        sync_committee_strategy: SyncCommitteeStrategy,
    ) -> AddBlocksResult<E> {
        assert!(slots.is_sorted(), "Slots have to be in ascending order");
        let mut block_hash_from_slot: HashMap<Slot, SignedBeaconBlockHash> = HashMap::new();
        let mut state_hash_from_slot: HashMap<Slot, BeaconStateHash> = HashMap::new();
        for slot in slots {
            // Using a `Box::pin` to reduce the stack size. Clippy was raising a lints.
            let (block_hash, new_state) = Box::pin(self.add_attested_block_at_slot_with_sync(
                *slot,
                state,
                validators,
                sync_committee_strategy,
            ))
            .await
            .unwrap();

            state = new_state;

            block_hash_from_slot.insert(*slot, block_hash);
            state_hash_from_slot.insert(*slot, state.canonical_root().unwrap().into());
            latest_block_hash = Some(block_hash);
        }
        (
            block_hash_from_slot,
            state_hash_from_slot,
            latest_block_hash.unwrap(),
            state,
        )
    }

    /// A monstrosity of great usefulness.
    ///
    /// Calls `add_attested_blocks_at_slots` for each of the chains in `chains`,
    /// taking care to batch blocks by epoch so that the slot clock gets advanced one
    /// epoch at a time.
    ///
    /// Chains is a vec of `(state, slots, validators)` tuples.
    pub async fn add_blocks_on_multiple_chains(
        &self,
        chains: Vec<(BeaconState<E>, Vec<Slot>, Vec<usize>)>,
    ) -> Vec<AddBlocksResult<E>> {
        let slots_per_epoch = E::slots_per_epoch();

        let min_epoch = chains
            .iter()
            .map(|(_, slots, _)| slots.iter().min().unwrap())
            .min()
            .unwrap()
            .epoch(slots_per_epoch);
        let max_epoch = chains
            .iter()
            .map(|(_, slots, _)| slots.iter().max().unwrap())
            .max()
            .unwrap()
            .epoch(slots_per_epoch);

        let mut chains = chains
            .into_iter()
            .map(|(state, slots, validators)| {
                (
                    state,
                    slots,
                    validators,
                    HashMap::new(),
                    HashMap::new(),
                    SignedBeaconBlockHash::from(Hash256::zero()),
                )
            })
            .collect::<Vec<_>>();

        for epoch in min_epoch.as_u64()..=max_epoch.as_u64() {
            let mut new_chains = vec![];

            for (head_state, slots, validators, mut block_hashes, mut state_hashes, head_block) in
                chains
            {
                let epoch_slots = slots
                    .iter()
                    .filter(|s| s.epoch(slots_per_epoch).as_u64() == epoch)
                    .copied()
                    .collect::<Vec<_>>();

                let (new_block_hashes, new_state_hashes, new_head_block, new_head_state) = self
                    .add_attested_blocks_at_slots_given_lbh(
                        head_state,
                        &epoch_slots,
                        &validators,
                        Some(head_block),
                        SyncCommitteeStrategy::NoValidators, // for backwards compat
                    )
                    .await;

                block_hashes.extend(new_block_hashes);
                state_hashes.extend(new_state_hashes);

                new_chains.push((
                    new_head_state,
                    slots,
                    validators,
                    block_hashes,
                    state_hashes,
                    new_head_block,
                ));
            }

            chains = new_chains;
        }

        chains
            .into_iter()
            .map(|(state, _, _, block_hashes, state_hashes, head_block)| {
                (block_hashes, state_hashes, head_block, state)
            })
            .collect()
    }

    pub fn get_finalized_checkpoints(&self) -> HashSet<SignedBeaconBlockHash> {
        let chain_dump = self.chain.chain_dump().unwrap();
        chain_dump
            .iter()
            .map(|checkpoint| checkpoint.beacon_state.finalized_checkpoint().root)
            .filter(|block_hash| *block_hash != Hash256::zero())
            .map(|hash| hash.into())
            .collect()
    }

    /// Advance the slot of the `BeaconChain`.
    ///
    /// Does not produce blocks or attestations.
    pub fn advance_slot(&self) {
        self.chain.slot_clock.advance_slot();
    }

    /// Advance the clock to `lookahead` before the start of `slot`.
    pub fn advance_to_slot_lookahead(&self, slot: Slot, lookahead: Duration) {
        let time = self.chain.slot_clock.start_of(slot).unwrap() - lookahead;
        self.chain.slot_clock.set_current_time(time);
    }

    /// Uses `Self::extend_chain` to build the chain out to the `target_slot`.
    pub async fn extend_to_slot(&self, target_slot: Slot) -> Hash256 {
        if self.chain.slot().unwrap() == self.chain.canonical_head.cached_head().head_slot() {
            self.advance_slot();
        }

        let num_slots = target_slot
            .as_usize()
            .checked_sub(self.chain.slot().unwrap().as_usize())
            .expect("target_slot must be >= current_slot")
            .checked_add(1)
            .unwrap();

        self.extend_slots(num_slots).await
    }

    /// Uses `Self::extend_chain` to `num_slots` blocks.
    ///
    /// Utilizes:
    ///
    ///  - BlockStrategy::OnCanonicalHead,
    ///  - AttestationStrategy::AllValidators,
    pub async fn extend_slots(&self, num_slots: usize) -> Hash256 {
        if self.chain.slot().unwrap() == self.chain.canonical_head.cached_head().head_slot() {
            self.advance_slot();
        }

        self.extend_chain(
            num_slots,
            BlockStrategy::OnCanonicalHead,
            AttestationStrategy::AllValidators,
        )
        .await
    }

    /// Uses `Self::extend_chain` to `num_slots` blocks.
    ///
    /// Utilizes:
    ///
    ///  - BlockStrategy::OnCanonicalHead,
    ///  - AttestationStrategy::SomeValidators(validators),
    pub async fn extend_slots_some_validators(
        &self,
        num_slots: usize,
        validators: Vec<usize>,
    ) -> Hash256 {
        if self.chain.slot().unwrap() == self.chain.canonical_head.cached_head().head_slot() {
            self.advance_slot();
        }

        self.extend_chain(
            num_slots,
            BlockStrategy::OnCanonicalHead,
            AttestationStrategy::SomeValidators(validators),
        )
        .await
    }

    /// Extend the `BeaconChain` with some blocks and attestations. Returns the root of the
    /// last-produced block (the head of the chain).
    ///
    /// Chain will be extended by `num_blocks` blocks.
    ///
    /// The `block_strategy` dictates where the new blocks will be placed.
    ///
    /// The `attestation_strategy` dictates which validators will attest to the newly created
    /// blocks.
    pub async fn extend_chain(
        &self,
        num_blocks: usize,
        block_strategy: BlockStrategy,
        attestation_strategy: AttestationStrategy,
    ) -> Hash256 {
        self.extend_chain_with_sync(
            num_blocks,
            block_strategy,
            attestation_strategy,
            SyncCommitteeStrategy::NoValidators,
            LightClientStrategy::Disabled,
        )
        .await
    }

    pub async fn extend_chain_with_light_client_data(
        &self,
        num_blocks: usize,
        block_strategy: BlockStrategy,
        attestation_strategy: AttestationStrategy,
    ) -> Hash256 {
        self.extend_chain_with_sync(
            num_blocks,
            block_strategy,
            attestation_strategy,
            SyncCommitteeStrategy::NoValidators,
            LightClientStrategy::Enabled,
        )
        .await
    }

    pub async fn extend_chain_with_sync(
        &self,
        num_blocks: usize,
        block_strategy: BlockStrategy,
        attestation_strategy: AttestationStrategy,
        sync_committee_strategy: SyncCommitteeStrategy,
        light_client_strategy: LightClientStrategy,
    ) -> Hash256 {
        let (state, slots) = match block_strategy {
            BlockStrategy::OnCanonicalHead => {
                let current_slot: u64 = self.get_current_slot().into();
                let slots: Vec<Slot> = (current_slot..(current_slot + (num_blocks as u64)))
                    .map(Slot::new)
                    .collect();
                let state = self.get_current_state();
                (state, slots)
            }
            BlockStrategy::ForkCanonicalChainAt {
                previous_slot,
                first_slot,
            } => {
                let first_slot_: u64 = first_slot.into();
                let slots: Vec<Slot> = (first_slot_..(first_slot_ + (num_blocks as u64)))
                    .map(Slot::new)
                    .collect();
                let state = self
                    .chain
                    .state_at_slot(previous_slot, StateSkipConfig::WithStateRoots)
                    .unwrap();
                (state, slots)
            }
        };
        let validators = match attestation_strategy {
            AttestationStrategy::AllValidators => self.get_all_validators(),
            AttestationStrategy::SomeValidators(vals) => vals,
        };

        let (_, _, last_produced_block_hash, _) = match light_client_strategy {
            LightClientStrategy::Enabled => {
                self.add_attested_blocks_at_slots_with_lc_data(
                    state,
                    &slots,
                    &validators,
                    None,
                    sync_committee_strategy,
                )
                .await
            }
            LightClientStrategy::Disabled => {
                self.add_attested_blocks_at_slots_with_sync(
                    state,
                    &slots,
                    &validators,
                    sync_committee_strategy,
                )
                .await
            }
        };

        last_produced_block_hash.into()
    }

    /// Deprecated: Use add_attested_blocks_at_slots() instead
    ///
    /// Creates two forks:
    ///
    ///  - The "honest" fork: created by the `honest_validators` who have built `honest_fork_blocks`
    ///    on the head
    ///  - The "faulty" fork: created by the `faulty_validators` who skipped a slot and
    ///    then built `faulty_fork_blocks`.
    ///
    /// Returns `(honest_head, faulty_head)`, the roots of the blocks at the top of each chain.
    pub async fn generate_two_forks_by_skipping_a_block(
        &self,
        honest_validators: &[usize],
        faulty_validators: &[usize],
        honest_fork_blocks: usize,
        faulty_fork_blocks: usize,
    ) -> (Hash256, Hash256) {
        let initial_head_slot = self.chain.head_snapshot().beacon_block.slot();

        // Move to the next slot so we may produce some more blocks on the head.
        self.advance_slot();

        // Extend the chain with blocks where only honest validators agree.
        let honest_head = self
            .extend_chain(
                honest_fork_blocks,
                BlockStrategy::OnCanonicalHead,
                AttestationStrategy::SomeValidators(honest_validators.to_vec()),
            )
            .await;

        // Go back to the last block where all agreed, and build blocks upon it where only faulty nodes
        // agree.
        let faulty_head = self
            .extend_chain(
                faulty_fork_blocks,
                BlockStrategy::ForkCanonicalChainAt {
                    previous_slot: initial_head_slot,
                    // `initial_head_slot + 2` means one slot is skipped.
                    first_slot: initial_head_slot + 2,
                },
                AttestationStrategy::SomeValidators(faulty_validators.to_vec()),
            )
            .await;

        assert_ne!(honest_head, faulty_head, "forks should be distinct");

        (honest_head, faulty_head)
    }

    pub fn process_sync_contributions(
        &self,
        sync_contributions: HarnessSyncContributions<E>,
    ) -> Result<(), SyncCommitteeError> {
        let mut verified_contributions = Vec::with_capacity(sync_contributions.len());

        for (_, contribution_and_proof) in sync_contributions {
            let signed_contribution_and_proof = contribution_and_proof.unwrap();

            let verified_contribution = self
                .chain
                .verify_sync_contribution_for_gossip(signed_contribution_and_proof)?;

            verified_contributions.push(verified_contribution);
        }

        for verified_contribution in verified_contributions {
            self.chain
                .add_contribution_to_block_inclusion_pool(verified_contribution)?;
        }

        Ok(())
    }

    /// Simulate the block's custody data columns (or those in `custody_columns_opt`) being
    /// seen on gossip. Panics unless PeerDAS is enabled for the block's epoch.
    pub async fn process_gossip_columns(
        &self,
        block: &SignedBeaconBlock<E>,
        custody_columns_opt: Option<HashSet<ColumnIndex>>,
    ) {
        assert!(self.chain.spec.is_peer_das_enabled_for_epoch(block.epoch()));
        let custody_columns = custody_columns_opt.unwrap_or_else(|| {
            let epoch = block.slot().epoch(E::slots_per_epoch());
            self.chain
                .sampling_columns_for_epoch(epoch)
                .iter()
                .copied()
                .collect()
        });

        let verified_columns = generate_data_column_sidecars_from_block(block, &self.spec)
            .into_iter()
            .filter(|c| custody_columns.contains(c.index()))
            .map(|sidecar| {
                let subnet_id = DataColumnSubnetId::from_column_index(*sidecar.index(), &self.spec);
                self.chain
                    .verify_data_column_sidecar_for_gossip(sidecar, subnet_id)
            })
            .collect::<Result<Vec<_>, _>>()
            .unwrap();

        if !verified_columns.is_empty() {
            self.chain
                .process_gossip_data_columns(verified_columns, || Ok(()))
                .await
                .unwrap();
        }
    }
}

// Junk `Debug` impl to satistfy certain trait bounds during testing.
impl<T: BeaconChainTypes> fmt::Debug for BeaconChainHarness<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "BeaconChainHarness")
    }
}

pub struct MakeAttestationOptions {
    /// Produce exactly `limit` attestations.
    pub limit: Option<usize>,
    /// Fork to use for signing attestations.
    pub fork: Fork,
    /// Override post-Gloas regular attestation payload-present encoding.
    pub payload_present_override: Option<bool>,
}

pub enum NumBlobs {
    Random,
    Number(usize),
    None,
}

#[cfg(feature = "arbitrary")]
macro_rules! add_blob_transactions {
    ($message:expr, $payload_type:ty, $num_blobs:expr, $u:expr, $fork_name:expr) => {{
        let num_blobs = match $num_blobs {
            NumBlobs::Random => $u.int_in_range(DEFAULT_MIN_BLOBS..=DEFAULT_MAX_BLOBS)?,
            NumBlobs::Number(n) => n,
            NumBlobs::None => 0,
        };
        let (bundle, transactions) =
            execution_layer::test_utils::generate_blobs::<E>(num_blobs, $fork_name).unwrap();

        let payload: &mut $payload_type = &mut $message.body.execution_payload;
        payload.execution_payload.transactions = <_>::default();
        for tx in Vec::from(transactions) {
            payload.execution_payload.transactions.push(tx).unwrap();
        }
        $message.body.blob_kzg_commitments = bundle.commitments.clone();
        bundle
    }};
}

#[cfg(feature = "arbitrary")]
#[allow(clippy::type_complexity)]
pub fn generate_rand_block_and_blobs<E: EthSpec>(
    fork_name: ForkName,
    num_blobs: NumBlobs,
    u: &mut arbitrary::Unstructured,
) -> arbitrary::Result<(SignedBeaconBlock<E, FullPayload<E>>, Vec<BlobSidecar<E>>)> {
    let inner = map_fork_name!(fork_name, BeaconBlock, <_>::arbitrary(&mut *u)?);

    let mut block = SignedBeaconBlock::from_block(inner, Signature::arbitrary(&mut *u)?);
    let mut blob_sidecars = vec![];

    let bundle = match block {
        SignedBeaconBlock::Deneb(SignedBeaconBlockDeneb {
            ref mut message, ..
        }) => add_blob_transactions!(message, FullPayloadDeneb<E>, num_blobs, u, fork_name),
        SignedBeaconBlock::Electra(SignedBeaconBlockElectra {
            ref mut message, ..
        }) => add_blob_transactions!(message, FullPayloadElectra<E>, num_blobs, u, fork_name),
        SignedBeaconBlock::Fulu(SignedBeaconBlockFulu {
            ref mut message, ..
        }) => add_blob_transactions!(message, FullPayloadFulu<E>, num_blobs, u, fork_name),
        SignedBeaconBlock::Gloas(SignedBeaconBlockGloas {
            ref mut message, ..
        }) => {
            // For Gloas, commitments are in the bid, not directly in the body.
            // BlobSidecars cannot be created for Gloas because there's no merkle proof
            // from the block body to the commitments. Return early with empty blob_sidecars.
            let num_blobs = match num_blobs {
                NumBlobs::Random => u.int_in_range(DEFAULT_MIN_BLOBS..=DEFAULT_MAX_BLOBS)?,
                NumBlobs::Number(n) => n,
                NumBlobs::None => 0,
            };
            let (bundle, _transactions) =
                execution_layer::test_utils::generate_blobs::<E>(num_blobs, fork_name).unwrap();
            message
                .body
                .signed_execution_payload_bid
                .message
                .blob_kzg_commitments = bundle.commitments.clone();
            return Ok((block, blob_sidecars));
        }
        _ => return Ok((block, blob_sidecars)),
    };

    let eth2::types::BlobsBundle {
        commitments,
        proofs,
        blobs,
    } = bundle;

    for (index, ((blob, kzg_commitment), kzg_proof)) in
        blobs.into_iter().zip(commitments).zip(proofs).enumerate()
    {
        blob_sidecars.push(BlobSidecar {
            index: index as u64,
            blob: blob.clone(),
            kzg_commitment,
            kzg_proof,
            signed_block_header: block.signed_block_header(),
            kzg_commitment_inclusion_proof: block
                .message()
                .body()
                .kzg_commitment_merkle_proof(index)
                .unwrap(),
        });
    }
    Ok((block, blob_sidecars))
}

#[cfg(feature = "arbitrary")]
#[allow(clippy::type_complexity)]
pub fn generate_rand_block_and_data_columns<E: EthSpec>(
    fork_name: ForkName,
    num_blobs: NumBlobs,
    u: &mut arbitrary::Unstructured,
    spec: &ChainSpec,
) -> arbitrary::Result<(
    SignedBeaconBlock<E, FullPayload<E>>,
    DataColumnSidecarList<E>,
)> {
    let (block, _blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, u)?;
    let data_columns = generate_data_column_sidecars_from_block(&block, spec);
    Ok((block, data_columns))
}

/// Generate data column sidecars from pre-computed cells and proofs.
pub fn generate_data_column_sidecars_from_block<E: EthSpec>(
    block: &SignedBeaconBlock<E>,
    spec: &ChainSpec,
) -> DataColumnSidecarList<E> {
    // Load the precomputed column sidecar to avoid computing them for every block in the tests.
    // Then repeat the cells and proofs for every blob
    if block.fork_name_unchecked().gloas_enabled() {
        let kzg_commitments = &block
            .message()
            .body()
            .signed_execution_payload_bid()
            .expect("Gloas block should have a payload bid")
            .message
            .blob_kzg_commitments;
        if kzg_commitments.is_empty() {
            return vec![];
        }
        let num_blobs = kzg_commitments.len();
        let signed_block_header = block.signed_block_header();
        let template_data_columns =
            RuntimeVariableList::<DataColumnSidecarGloas<E>>::from_ssz_bytes(
                TEST_DATA_COLUMN_SIDECARS_GLOAS_SSZ,
                E::number_of_columns(),
            )
            .unwrap();

        let (cells, proofs) = template_data_columns
            .into_iter()
            .map(|sidecar| {
                let DataColumnSidecarGloas {
                    column, kzg_proofs, ..
                } = sidecar;
                // There's only one cell per column for a single blob
                let cell_bytes: Vec<u8> = column.into_iter().next().unwrap().into();
                let kzg_cell = cell_bytes.try_into().unwrap();
                let kzg_proof = kzg_proofs.into_iter().next().unwrap();
                (kzg_cell, kzg_proof)
            })
            .collect::<(Vec<_>, Vec<_>)>();

        let blob_cells_and_proofs_vec =
            vec![(cells.try_into().unwrap(), proofs.try_into().unwrap()); num_blobs];

        build_data_column_sidecars_gloas(
            signed_block_header.message.tree_hash_root(),
            signed_block_header.message.slot,
            blob_cells_and_proofs_vec,
            spec,
        )
        .unwrap()
    } else {
        let kzg_commitments = block.message().body().blob_kzg_commitments().unwrap();
        if kzg_commitments.is_empty() {
            return vec![];
        }

        let kzg_commitments_inclusion_proof = block
            .message()
            .body()
            .kzg_commitments_merkle_proof()
            .unwrap();
        let signed_block_header = block.signed_block_header();

        // load the precomputed column sidecar to avoid computing them for every block in the tests.
        let template_data_columns =
            RuntimeVariableList::<DataColumnSidecarFulu<E>>::from_ssz_bytes(
                TEST_DATA_COLUMN_SIDECARS_SSZ,
                E::number_of_columns(),
            )
            .unwrap();

        let (cells, proofs) = template_data_columns
            .into_iter()
            .map(|sidecar| {
                let DataColumnSidecarFulu {
                    column, kzg_proofs, ..
                } = sidecar;
                // There's only one cell per column for a single blob
                let cell_bytes: Vec<u8> = column.into_iter().next().unwrap().into();
                let kzg_cell = cell_bytes.try_into().unwrap();
                let kzg_proof = kzg_proofs.into_iter().next().unwrap();
                (kzg_cell, kzg_proof)
            })
            .collect::<(Vec<_>, Vec<_>)>();

        let blob_cells_and_proofs_vec =
            vec![(cells.try_into().unwrap(), proofs.try_into().unwrap()); kzg_commitments.len()];

        build_data_column_sidecars_fulu(
            kzg_commitments.clone(),
            kzg_commitments_inclusion_proof,
            signed_block_header,
            blob_cells_and_proofs_vec,
            spec,
        )
        .unwrap()
    }
}

pub fn generate_data_column_indices_rand_order<E: EthSpec>() -> Vec<CustodyIndex> {
    let mut indices = (0..E::number_of_columns() as u64).collect::<Vec<_>>();
    indices.shuffle(&mut StdRng::seed_from_u64(42));
    indices
}
