//! NOTE: These tests will fail without a java runtime environment (such as openjdk) installed and
//! available on `$PATH`.
//!
//! This crate provides a series of integration tests between the Lighthouse `ValidatorStore` and
//! Web3Signer by Consensys.
//!
//! These tests aim to ensure that:
//!
//! - Lighthouse can issue valid requests to Web3Signer.
//! - The signatures generated by Web3Signer are identical to those which Lighthouse generates.
//!
//! There is a `download_binary` function in the `get_web3signer` module which obtains the latest version of Web3Signer and makes
//! it available via the `TEMP_DIR`.
#![cfg(all(test, unix, not(debug_assertions)))]

mod get_web3signer;

mod tests {
    use crate::get_web3signer::download_binary;
    use account_utils::validator_definitions::{
        SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition,
    };
    use bls::{AggregateSignature, Keypair, PublicKeyBytes, SecretKey, Signature};
    use eth2::types::FullBlockContents;
    use eth2_keystore::KeystoreBuilder;
    use eth2_network_config::Eth2NetworkConfig;
    use fixed_bytes::FixedBytesExtended;
    use futures::StreamExt;
    use initialized_validators::{
        InitializedValidators, load_pem_certificate, load_pkcs12_identity,
    };
    use lighthouse_validator_store::LighthouseValidatorStore;
    use parking_lot::Mutex;
    use reqwest::Client;
    use serde::Serialize;
    use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase};
    use slot_clock::{SlotClock, TestingSlotClock};
    use ssz_types::BitList;
    use std::env;
    use std::fmt::Debug;
    use std::fs::{self, File};
    use std::future::Future;
    use std::path::PathBuf;
    use std::process::{Child, Command, Stdio};
    use std::sync::{Arc, LazyLock};
    use std::time::{Duration, Instant};
    use task_executor::TaskExecutor;
    use tempfile::{TempDir, tempdir};
    use tokio::sync::OnceCell;
    use tokio::time::sleep;
    use types::{attestation::AttestationBase, *};
    use url::Url;
    use validator_store::{
        AttestationToSign, Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore,
    };

    /// If the we are unable to reach the Web3Signer HTTP API within this time out then we will
    /// assume it failed to start.
    const UPCHECK_TIMEOUT: Duration = Duration::from_secs(30);

    /// Set to `false` to send the Web3Signer logs to the console during tests. Logs are useful when
    /// debugging.
    const SUPPRESS_WEB3SIGNER_LOGS: bool = true;

    static TEMP_DIR: LazyLock<Arc<Mutex<TempDir>>> = LazyLock::new(|| {
        Arc::new(Mutex::new(
            tempdir().expect("Failed to create temporary directory"),
        ))
    });

    static GET_WEB3SIGNER_BIN: OnceCell<()> = OnceCell::const_new();

    type E = MainnetEthSpec;

    /// This marker trait is implemented for objects that we wish to compare to ensure Web3Signer
    /// and Lighthouse agree on signatures.
    ///
    /// The purpose of this trait is to prevent accidentally comparing useless values like `()`.
    trait SignedObject: PartialEq + Debug {}

    impl SignedObject for Signature {}
    impl SignedObject for Attestation<E> {}
    impl SignedObject for SignedBeaconBlock<E> {}
    impl SignedObject for SignedBlock<E> {}
    impl SignedObject for SignedAggregateAndProof<E> {}
    impl SignedObject for SelectionProof {}
    impl SignedObject for SyncSelectionProof {}
    impl SignedObject for SyncCommitteeMessage {}
    impl SignedObject for SignedContributionAndProof<E> {}
    impl SignedObject for SignedValidatorRegistrationData {}

    /// A file format used by Web3Signer to discover and unlock keystores.
    #[derive(Serialize)]
    struct Web3SignerKeyConfig {
        #[serde(rename = "type")]
        config_type: String,
        #[serde(rename = "keyType")]
        key_type: String,
        #[serde(rename = "keystoreFile")]
        keystore_file: String,
        #[serde(rename = "keystorePasswordFile")]
        keystore_password_file: String,
    }

    const KEYSTORE_PASSWORD: &str = "hi mum";
    const WEB3SIGNER_LISTEN_ADDRESS: &str = "127.0.0.1";

    /// A deterministic, arbitrary keypair.
    fn testing_keypair() -> Keypair {
        // Just an arbitrary secret key.
        let sk = SecretKey::deserialize(&[
            85, 40, 245, 17, 84, 193, 234, 155, 24, 234, 181, 58, 171, 193, 209, 164, 120, 147, 10,
            174, 189, 228, 119, 48, 181, 19, 117, 223, 2, 240, 7, 108,
        ])
        .unwrap();
        let pk = sk.public_key();
        Keypair::from_components(pk, sk)
    }

    /// The location of the Web3Signer binary generated by the build script.
    fn web3signer_binary() -> PathBuf {
        TEMP_DIR
            .lock()
            .path()
            .to_path_buf()
            .join("web3signer")
            .join("bin")
            .join("web3signer")
    }

    /// The location of a directory where we keep some files for testing TLS.
    fn tls_dir() -> PathBuf {
        PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("tls")
    }

    fn root_certificate_path() -> PathBuf {
        tls_dir().join("lighthouse").join("web3signer.pem")
    }

    fn client_identity_path() -> PathBuf {
        tls_dir().join("lighthouse").join("key.p12")
    }

    fn client_identity_password() -> String {
        fs::read_to_string(tls_dir().join("lighthouse").join("password.txt"))
            .unwrap()
            .trim()
            .to_string()
    }

    /// A testing rig which holds a live Web3Signer process.
    struct Web3SignerRig {
        keypair: Keypair,
        _keystore_dir: TempDir,
        keystore_path: PathBuf,
        web3signer_child: Child,
        http_client: Client,
        url: Url,
    }

    impl Drop for Web3SignerRig {
        fn drop(&mut self) {
            self.web3signer_child.kill().unwrap();
        }
    }

    #[derive(Debug, Clone, Copy)]
    struct SlashingProtectionConfig {
        /// Whether to enable slashing protection for web3signer keys locally within Lighthouse.
        local: bool,
    }

    impl Default for SlashingProtectionConfig {
        fn default() -> Self {
            SlashingProtectionConfig { local: true }
        }
    }

    impl Web3SignerRig {
        // We need to hold that lock as we want to get the binary only once
        #[allow(clippy::await_holding_lock)]
        pub async fn new(network: &str, listen_address: &str, listen_port: u16) -> Self {
            GET_WEB3SIGNER_BIN
                .get_or_init(|| async {
                    download_binary(TEMP_DIR.lock().path().to_path_buf()).await;
                })
                .await;

            let keystore_dir = TempDir::new().unwrap();
            let keypair = testing_keypair();
            let keystore =
                KeystoreBuilder::new(&keypair, KEYSTORE_PASSWORD.as_bytes(), "".to_string())
                    .unwrap()
                    .build()
                    .unwrap();
            let keystore_filename = "keystore.json";
            let keystore_path = keystore_dir.path().join(keystore_filename);
            let keystore_file = File::create(&keystore_path).unwrap();
            keystore.to_json_writer(&keystore_file).unwrap();

            let keystore_password_filename = "password.txt";
            let keystore_password_path = keystore_dir.path().join(keystore_password_filename);
            fs::write(&keystore_password_path, KEYSTORE_PASSWORD.as_bytes()).unwrap();

            let key_config = Web3SignerKeyConfig {
                config_type: "file-keystore".to_string(),
                key_type: "BLS".to_string(),
                keystore_file: keystore_filename.to_string(),
                keystore_password_file: keystore_password_filename.to_string(),
            };
            let key_config_file =
                File::create(keystore_dir.path().join("key-config.yaml")).unwrap();
            yaml_serde::to_writer(key_config_file, &key_config).unwrap();

            let tls_keystore_file = tls_dir().join("web3signer").join("key.p12");
            let tls_keystore_password_file = tls_dir().join("web3signer").join("password.txt");
            let tls_known_clients_file = tls_dir().join("web3signer").join("known_clients.txt");

            let stdio = || {
                if SUPPRESS_WEB3SIGNER_LOGS {
                    Stdio::null()
                } else {
                    Stdio::inherit()
                }
            };

            let web3signer_child = Command::new(web3signer_binary())
                .arg(format!(
                    "--key-store-path={}",
                    keystore_dir.path().to_str().unwrap()
                ))
                .arg(format!("--http-listen-host={}", listen_address))
                .arg(format!("--http-listen-port={}", listen_port))
                .arg(format!(
                    "--tls-known-clients-file={}",
                    tls_known_clients_file.to_str().unwrap()
                ))
                .arg(format!(
                    "--tls-keystore-file={}",
                    tls_keystore_file.to_str().unwrap()
                ))
                .arg(format!(
                    "--tls-keystore-password-file={}",
                    tls_keystore_password_file.to_str().unwrap()
                ))
                .arg("eth2")
                .arg(format!("--network={}", network))
                // Can't *easily* test `--slashing-protection-enabled=true` because web3signer
                // requires a Postgres instance.
                .arg("--slashing-protection-enabled=false")
                .stdout(stdio())
                .stderr(stdio())
                .spawn()
                .unwrap();

            let url = Url::parse(&format!("https://{}:{}", listen_address, listen_port)).unwrap();

            let certificate = load_pem_certificate(root_certificate_path()).unwrap();
            let identity =
                load_pkcs12_identity(client_identity_path(), &client_identity_password()).unwrap();
            let http_client = Client::builder()
                .add_root_certificate(certificate)
                .identity(identity)
                .build()
                .unwrap();

            let s = Self {
                keypair,
                _keystore_dir: keystore_dir,
                keystore_path,
                web3signer_child,
                http_client,
                url,
            };

            s.wait_until_up(UPCHECK_TIMEOUT).await;

            s
        }

        pub async fn wait_until_up(&self, timeout: Duration) {
            let start = Instant::now();
            loop {
                if self.upcheck().await.is_ok() {
                    return;
                } else if Instant::now().duration_since(start) > timeout {
                    panic!("upcheck failed with timeout {:?}", timeout)
                } else {
                    sleep(Duration::from_secs(1)).await;
                }
            }
        }

        pub async fn upcheck(&self) -> Result<(), ()> {
            let url = self.url.join("upcheck").unwrap();
            self.http_client
                .get(url)
                .send()
                .await
                .map_err(|_| ())?
                .error_for_status()
                .map(|_| ())
                .map_err(|_| ())
        }
    }

    /// A testing rig which holds a `ValidatorStore`.
    struct ValidatorStoreRig {
        validator_store: Arc<LighthouseValidatorStore<TestingSlotClock, E>>,
        _validator_dir: TempDir,
        runtime: Arc<tokio::runtime::Runtime>,
        _runtime_shutdown: async_channel::Sender<()>,
        using_web3signer: bool,
    }

    impl ValidatorStoreRig {
        pub async fn new(
            validator_definitions: Vec<ValidatorDefinition>,
            slashing_protection_config: SlashingProtectionConfig,
            using_web3signer: bool,
            spec: Arc<ChainSpec>,
        ) -> Self {
            let validator_dir = TempDir::new().unwrap();

            let config = initialized_validators::Config::default();
            let validator_definitions = ValidatorDefinitions::from(validator_definitions);
            let initialized_validators = InitializedValidators::from_definitions(
                validator_definitions,
                validator_dir.path().into(),
                config.clone(),
            )
            .await
            .unwrap();

            let voting_pubkeys: Vec<_> = initialized_validators.iter_voting_pubkeys().collect();

            let runtime = Arc::new(
                tokio::runtime::Builder::new_multi_thread()
                    .enable_all()
                    .build()
                    .unwrap(),
            );
            let (runtime_shutdown, exit) = async_channel::bounded(1);
            let (shutdown_tx, _) = futures::channel::mpsc::channel(1);
            let executor = TaskExecutor::new(Arc::downgrade(&runtime), exit, shutdown_tx);

            let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME);
            let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap();
            slashing_protection
                .register_validators(voting_pubkeys.iter().copied())
                .unwrap();

            let slot_clock =
                TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1));
            let config = lighthouse_validator_store::Config {
                enable_web3signer_slashing_protection: slashing_protection_config.local,
                ..Default::default()
            };

            let validator_store = LighthouseValidatorStore::<_, E>::new(
                initialized_validators,
                slashing_protection,
                Hash256::repeat_byte(42),
                spec,
                None,
                slot_clock,
                &config,
                executor,
            );

            Self {
                validator_store: Arc::new(validator_store),
                _validator_dir: validator_dir,
                runtime,
                _runtime_shutdown: runtime_shutdown,
                using_web3signer,
            }
        }

        pub fn shutdown(self) {
            Arc::try_unwrap(self.runtime).unwrap().shutdown_background()
        }
    }

    /// A testing rig which holds multiple `ValidatorStore` rigs and one `Web3Signer` rig.
    ///
    /// The intent of this rig is to allow testing a `ValidatorStore` using `Web3Signer` against
    /// another `ValidatorStore` using a local keystore and ensure that both `ValidatorStore`s
    /// behave identically.
    struct TestingRig {
        _signer_rig: Web3SignerRig,
        validator_rigs: Vec<ValidatorStoreRig>,
        validator_pubkey: PublicKeyBytes,
    }

    impl Drop for TestingRig {
        fn drop(&mut self) {
            for rig in std::mem::take(&mut self.validator_rigs) {
                rig.shutdown();
            }
        }
    }

    impl TestingRig {
        pub async fn new(
            network: &str,
            slashing_protection_config: SlashingProtectionConfig,
            spec: Arc<ChainSpec>,
            listen_port: u16,
        ) -> Self {
            let signer_rig =
                Web3SignerRig::new(network, WEB3SIGNER_LISTEN_ADDRESS, listen_port).await;
            let validator_pubkey = signer_rig.keypair.pk.clone();

            let local_signer_validator_store = {
                let validator_definition = ValidatorDefinition {
                    enabled: true,
                    voting_public_key: validator_pubkey.clone(),
                    graffiti: None,
                    suggested_fee_recipient: None,
                    gas_limit: None,
                    builder_proposals: None,
                    builder_boost_factor: None,
                    prefer_builder_proposals: None,
                    description: String::default(),
                    signing_definition: SigningDefinition::LocalKeystore {
                        voting_keystore_path: signer_rig.keystore_path.clone(),
                        voting_keystore_password_path: None,
                        voting_keystore_password: Some(KEYSTORE_PASSWORD.to_string().into()),
                    },
                };
                ValidatorStoreRig::new(
                    vec![validator_definition],
                    slashing_protection_config,
                    false,
                    spec.clone(),
                )
                .await
            };

            let remote_signer_validator_store = {
                let validator_definition = ValidatorDefinition {
                    enabled: true,
                    voting_public_key: validator_pubkey.clone(),
                    graffiti: None,
                    suggested_fee_recipient: None,
                    gas_limit: None,
                    builder_proposals: None,
                    builder_boost_factor: None,
                    prefer_builder_proposals: None,
                    description: String::default(),
                    signing_definition: SigningDefinition::Web3Signer(Web3SignerDefinition {
                        url: signer_rig.url.to_string(),
                        root_certificate_path: Some(root_certificate_path()),
                        request_timeout_ms: None,
                        client_identity_path: Some(client_identity_path()),
                        client_identity_password: Some(client_identity_password()),
                    }),
                };
                ValidatorStoreRig::new(
                    vec![validator_definition],
                    slashing_protection_config,
                    true,
                    spec,
                )
                .await
            };

            Self {
                _signer_rig: signer_rig,
                validator_rigs: vec![local_signer_validator_store, remote_signer_validator_store],
                validator_pubkey: PublicKeyBytes::from(&validator_pubkey),
            }
        }

        /// Run the `generate_sig` function across all validator stores on `self` and assert that
        /// they all return the same value.
        pub async fn assert_signatures_match<F, R, S>(
            self,
            case_name: &str,
            generate_sig: F,
        ) -> Self
        where
            F: Fn(PublicKeyBytes, Arc<LighthouseValidatorStore<TestingSlotClock, E>>) -> R,
            R: Future<Output = S>,
            // We use the `SignedObject` trait to white-list objects for comparison. This avoids
            // accidentally comparing something meaningless like a `()`.
            S: SignedObject,
        {
            let mut prev_signature = None;
            for (i, validator_rig) in self.validator_rigs.iter().enumerate() {
                let signature =
                    generate_sig(self.validator_pubkey, validator_rig.validator_store.clone())
                        .await;

                if let Some(prev_signature) = &prev_signature {
                    assert_eq!(
                        prev_signature, &signature,
                        "signature mismatch at index {} for case {}",
                        i, case_name
                    );
                }

                prev_signature = Some(signature)
            }
            assert!(prev_signature.is_some(), "sanity check");
            self
        }

        /// Assert that a slashable message fails to be signed locally and is either signed or not
        /// by the web3signer rig depending on the value of `web3signer_should_sign`.
        pub async fn assert_slashable_message_should_sign<F, R>(
            self,
            case_name: &str,
            generate_sig: F,
            web3signer_should_sign: bool,
        ) -> Self
        where
            F: Fn(PublicKeyBytes, Arc<LighthouseValidatorStore<TestingSlotClock, E>>) -> R,
            R: Future<Output = Result<(), lighthouse_validator_store::Error>>,
        {
            for validator_rig in &self.validator_rigs {
                let result =
                    generate_sig(self.validator_pubkey, validator_rig.validator_store.clone())
                        .await;

                if !validator_rig.using_web3signer || !web3signer_should_sign {
                    let err = result.unwrap_err();
                    assert!(
                        matches!(err, ValidatorStoreError::Slashable(_)),
                        "should not sign slashable {case_name}"
                    );
                } else {
                    assert_eq!(result, Ok(()), "should sign slashable {case_name}");
                }
            }
            self
        }

        /// Assert that a slashable attestation fails to be signed locally (empty result) and is
        /// either signed or not by the web3signer rig depending on the value of
        /// `web3signer_should_sign`.
        ///
        /// The batch attestation signing API returns an empty result instead of an error for
        /// slashable attestations.
        pub async fn assert_slashable_attestation_should_sign<F, R>(
            self,
            case_name: &str,
            generate_sig: F,
            web3signer_should_sign: bool,
        ) -> Self
        where
            F: Fn(PublicKeyBytes, Arc<LighthouseValidatorStore<TestingSlotClock, E>>) -> R,
            R: Future<
                Output = Result<Vec<(u64, Attestation<E>)>, lighthouse_validator_store::Error>,
            >,
        {
            for validator_rig in &self.validator_rigs {
                let result =
                    generate_sig(self.validator_pubkey, validator_rig.validator_store.clone())
                        .await;

                if !validator_rig.using_web3signer || !web3signer_should_sign {
                    // For local validators, slashable attestations should return an empty result
                    // or an error.
                    match result {
                        Ok(attestations) => {
                            assert!(
                                attestations.is_empty(),
                                "should not sign slashable {case_name}: expected empty result"
                            );
                        }
                        Err(ValidatorStoreError::Slashable(_)) => {
                            // Also acceptable - error indicates slashable
                        }
                        Err(e) => {
                            panic!("unexpected error for slashable {case_name}: {e:?}");
                        }
                    }
                } else {
                    // Web3signer should sign (has its own slashing protection)
                    let attestations = result.expect("should sign slashable {case_name}");
                    assert!(
                        !attestations.is_empty(),
                        "web3signer should sign slashable {case_name}"
                    );
                }
            }
            self
        }
    }

    /// Get a generic, arbitrary attestation for signing.
    fn get_attestation() -> Attestation<E> {
        Attestation::Base(AttestationBase {
            aggregation_bits: BitList::with_capacity(1).unwrap(),
            data: AttestationData {
                slot: <_>::default(),
                index: <_>::default(),
                beacon_block_root: <_>::default(),
                source: Checkpoint {
                    epoch: <_>::default(),
                    root: <_>::default(),
                },
                target: Checkpoint {
                    epoch: <_>::default(),
                    root: <_>::default(),
                },
            },
            signature: AggregateSignature::empty(),
        })
    }

    fn get_validator_registration(pubkey: PublicKeyBytes) -> ValidatorRegistrationData {
        let fee_recipient = Address::repeat_byte(42);
        ValidatorRegistrationData {
            fee_recipient,
            gas_limit: 30_000_000,
            timestamp: 100,
            pubkey,
        }
    }

    /// Test all the "base" (phase 0) types.
    async fn test_base_types(network: &str, listen_port: u16) {
        let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap();
        let spec = Arc::new(network_config.chain_spec::<E>().unwrap());

        TestingRig::new(
            network,
            SlashingProtectionConfig::default(),
            spec.clone(),
            listen_port,
        )
        .await
        .assert_signatures_match("randao_reveal", |pubkey, validator_store| async move {
            validator_store
                .randao_reveal(pubkey, Epoch::new(0))
                .await
                .unwrap()
        })
        .await
        .assert_signatures_match("beacon_block_base", |pubkey, validator_store| {
            let spec = spec.clone();
            async move {
                let block = BeaconBlock::<E>::Base(BeaconBlockBase::empty(&spec));
                let block_slot = block.slot();
                let unsigned_block = UnsignedBlock::Full(FullBlockContents::Block(block));
                validator_store
                    .sign_block(pubkey, unsigned_block, block_slot)
                    .await
                    .unwrap()
            }
        })
        .await
        .assert_signatures_match("attestation", |pubkey, validator_store| async move {
            let attestation = get_attestation();
            let stream = validator_store.sign_attestations(vec![AttestationToSign {
                validator_index: 0,
                pubkey,
                validator_committee_index: 0,
                attestation,
            }]);
            tokio::pin!(stream);
            stream.next().await.unwrap().unwrap().pop().unwrap().1
        })
        .await
        .assert_signatures_match("signed_aggregate", |pubkey, validator_store| async move {
            let attestation = get_attestation();
            validator_store
                .produce_signed_aggregate_and_proof(
                    pubkey,
                    0,
                    attestation,
                    SelectionProof::from(Signature::empty()),
                )
                .await
                .unwrap()
        })
        .await
        .assert_signatures_match("selection_proof", |pubkey, validator_store| async move {
            validator_store
                .produce_selection_proof(pubkey, Slot::new(0))
                .await
                .unwrap()
        })
        .await
        .assert_signatures_match(
            "validator_registration",
            |pubkey, validator_store| async move {
                let val_reg_data = get_validator_registration(pubkey);
                validator_store
                    .sign_validator_registration_data(val_reg_data)
                    .await
                    .unwrap()
            },
        )
        .await;
    }

    /// Test all the Altair types.
    async fn test_altair_types(network: &str, listen_port: u16) {
        let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap();
        let spec = Arc::new(network_config.chain_spec::<E>().unwrap());
        let altair_fork_slot = spec
            .altair_fork_epoch
            .unwrap()
            .start_slot(E::slots_per_epoch());

        TestingRig::new(
            network,
            SlashingProtectionConfig::default(),
            spec.clone(),
            listen_port,
        )
        .await
        .assert_signatures_match("beacon_block_altair", |pubkey, validator_store| {
            let spec = spec.clone();
            async move {
                let mut altair_block = BeaconBlockAltair::empty(&spec);
                altair_block.slot = altair_fork_slot;
                let unsigned_block =
                    UnsignedBlock::Full(FullBlockContents::Block(altair_block.into()));
                validator_store
                    .sign_block(pubkey, unsigned_block, altair_fork_slot)
                    .await
                    .unwrap()
            }
        })
        .await
        .assert_signatures_match(
            "sync_selection_proof",
            |pubkey, validator_store| async move {
                validator_store
                    .produce_sync_selection_proof(&pubkey, altair_fork_slot, SyncSubnetId::from(0))
                    .await
                    .unwrap()
            },
        )
        .await
        .assert_signatures_match(
            "sync_committee_signature",
            |pubkey, validator_store| async move {
                validator_store
                    .produce_sync_committee_signature(altair_fork_slot, Hash256::zero(), 0, &pubkey)
                    .await
                    .unwrap()
            },
        )
        .await
        .assert_signatures_match(
            "signed_contribution_and_proof",
            |pubkey, validator_store| async move {
                let contribution = SyncCommitteeContribution {
                    slot: altair_fork_slot,
                    beacon_block_root: <_>::default(),
                    subcommittee_index: <_>::default(),
                    aggregation_bits: <_>::default(),
                    signature: AggregateSignature::empty(),
                };
                validator_store
                    .produce_signed_contribution_and_proof(
                        0,
                        pubkey,
                        contribution,
                        SyncSelectionProof::from(Signature::empty()),
                    )
                    .await
                    .unwrap()
            },
        )
        .await
        .assert_signatures_match(
            "validator_registration",
            |pubkey, validator_store| async move {
                let val_reg_data = get_validator_registration(pubkey);
                validator_store
                    .sign_validator_registration_data(val_reg_data)
                    .await
                    .unwrap()
            },
        )
        .await;
    }

    /// Test all the Bellatrix types.
    async fn test_bellatrix_types(network: &str, listen_port: u16) {
        let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap();
        let spec = Arc::new(network_config.chain_spec::<E>().unwrap());
        let bellatrix_fork_slot = spec
            .bellatrix_fork_epoch
            .unwrap()
            .start_slot(E::slots_per_epoch());

        TestingRig::new(
            network,
            SlashingProtectionConfig::default(),
            spec.clone(),
            listen_port,
        )
        .await
        .assert_signatures_match("beacon_block_bellatrix", |pubkey, validator_store| {
            let spec = spec.clone();
            async move {
                let mut bellatrix_block = BeaconBlockBellatrix::empty(&spec);
                bellatrix_block.slot = bellatrix_fork_slot;
                let unsigned_block =
                    UnsignedBlock::Full(FullBlockContents::Block(bellatrix_block.into()));
                validator_store
                    .sign_block(pubkey, unsigned_block, bellatrix_fork_slot)
                    .await
                    .unwrap()
            }
        })
        .await;
    }

    async fn test_lighthouse_slashing_protection(
        slashing_protection_config: SlashingProtectionConfig,
        listen_port: u16,
    ) {
        // Run these tests on mainnet.
        let network = "mainnet";

        let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap();
        let spec = Arc::new(network_config.chain_spec::<E>().unwrap());
        let bellatrix_fork_slot = spec
            .bellatrix_fork_epoch
            .unwrap()
            .start_slot(E::slots_per_epoch());

        // The slashable message should only be signed by the web3signer validator if slashing
        // protection is disabled in Lighthouse.
        let slashable_message_should_sign = !slashing_protection_config.local;

        let first_attestation = || {
            let mut attestation = get_attestation();
            attestation.data_mut().source.epoch = Epoch::new(1);
            attestation.data_mut().target.epoch = Epoch::new(4);
            attestation
        };

        let double_vote_attestation = || {
            let mut attestation = first_attestation();
            attestation.data_mut().beacon_block_root = Hash256::from_low_u64_be(1);
            attestation
        };

        let surrounding_attestation = || {
            let mut attestation = first_attestation();
            attestation.data_mut().source.epoch = Epoch::new(0);
            attestation.data_mut().target.epoch = Epoch::new(5);
            attestation
        };

        let surrounded_attestation = || {
            let mut attestation = first_attestation();
            attestation.data_mut().source.epoch = Epoch::new(2);
            attestation.data_mut().target.epoch = Epoch::new(3);
            attestation
        };

        let first_block = || {
            let mut bellatrix_block = BeaconBlockBellatrix::<E>::empty(&spec);
            bellatrix_block.slot = bellatrix_fork_slot;
            BeaconBlock::Bellatrix(bellatrix_block)
        };

        let double_vote_block = || {
            let mut block = first_block();
            *block.state_root_mut() = Hash256::repeat_byte(0xff);
            block
        };

        TestingRig::new(
            network,
            slashing_protection_config,
            spec.clone(),
            listen_port,
        )
        .await
        .assert_signatures_match("first_attestation", |pubkey, validator_store| async move {
            let attestation = first_attestation();
            let stream = validator_store.sign_attestations(vec![AttestationToSign {
                validator_index: 0,
                pubkey,
                validator_committee_index: 0,
                attestation,
            }]);
            tokio::pin!(stream);
            stream.next().await.unwrap().unwrap().pop().unwrap().1
        })
        .await
        .assert_slashable_attestation_should_sign(
            "double_vote_attestation",
            move |pubkey, validator_store| async move {
                let attestation = double_vote_attestation();
                let stream = validator_store.sign_attestations(vec![AttestationToSign {
                    validator_index: 0,
                    pubkey,
                    validator_committee_index: 0,
                    attestation,
                }]);
                tokio::pin!(stream);
                stream.next().await.unwrap()
            },
            slashable_message_should_sign,
        )
        .await
        .assert_slashable_attestation_should_sign(
            "surrounding_attestation",
            move |pubkey, validator_store| async move {
                let attestation = surrounding_attestation();
                let stream = validator_store.sign_attestations(vec![AttestationToSign {
                    validator_index: 0,
                    pubkey,
                    validator_committee_index: 0,
                    attestation,
                }]);
                tokio::pin!(stream);
                stream.next().await.unwrap()
            },
            slashable_message_should_sign,
        )
        .await
        .assert_slashable_attestation_should_sign(
            "surrounded_attestation",
            move |pubkey, validator_store| async move {
                let attestation = surrounded_attestation();
                let stream = validator_store.sign_attestations(vec![AttestationToSign {
                    validator_index: 0,
                    pubkey,
                    validator_committee_index: 0,
                    attestation,
                }]);
                tokio::pin!(stream);
                stream.next().await.unwrap()
            },
            slashable_message_should_sign,
        )
        .await
        .assert_signatures_match("first_block", |pubkey, validator_store| async move {
            let block = first_block();
            let slot = block.slot();
            let unsigned_block = UnsignedBlock::Full(FullBlockContents::Block(block));
            validator_store
                .sign_block(pubkey, unsigned_block, slot)
                .await
                .unwrap()
        })
        .await
        .assert_slashable_message_should_sign(
            "double_vote_block",
            move |pubkey, validator_store| async move {
                let block = double_vote_block();
                let slot = block.slot();
                let unsigned_block = UnsignedBlock::Full(FullBlockContents::Block(block));
                validator_store
                    .sign_block(pubkey, unsigned_block, slot)
                    .await
                    .map(|_| ())
            },
            slashable_message_should_sign,
        )
        .await;
    }

    #[tokio::test]
    async fn mainnet_base_types() {
        test_base_types("mainnet", 4242).await
    }

    #[tokio::test]
    async fn mainnet_altair_types() {
        test_altair_types("mainnet", 4243).await
    }

    #[tokio::test]
    async fn mainnet_bellatrix_types() {
        test_bellatrix_types("mainnet", 4244).await
    }

    #[tokio::test]
    async fn holesky_bellatrix_types() {
        // web3signer does not support forks prior to Bellatrix on Holesky
        test_bellatrix_types("holesky", 4247).await
    }

    #[tokio::test]
    async fn sepolia_base_types() {
        test_base_types("sepolia", 4250).await
    }

    #[tokio::test]
    async fn sepolia_altair_types() {
        test_altair_types("sepolia", 4251).await
    }

    #[tokio::test]
    async fn sepolia_bellatrix_types() {
        test_bellatrix_types("sepolia", 4252).await
    }

    #[tokio::test]
    async fn slashing_protection_disabled_locally() {
        test_lighthouse_slashing_protection(SlashingProtectionConfig { local: false }, 4253).await
    }

    #[tokio::test]
    async fn slashing_protection_enabled_locally() {
        test_lighthouse_slashing_protection(SlashingProtectionConfig { local: true }, 4254).await
    }
}
