//! 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 build script in this crate which obtains the latest version of Web3Signer and makes //! it available via the `OUT_DIR`. #[cfg(all(test, unix, not(debug_assertions)))] mod tests { use account_utils::validator_definitions::{ SigningDefinition, ValidatorDefinition, ValidatorDefinitions, }; use eth2_keystore::KeystoreBuilder; use eth2_network_config::Eth2NetworkConfig; use reqwest::Client; use serde::Serialize; use slot_clock::{SlotClock, TestingSlotClock}; 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; use std::time::{Duration, Instant}; use task_executor::TaskExecutor; use tempfile::TempDir; use tokio::time::sleep; use types::*; use url::Url; use validator_client::{ initialized_validators::{load_pem_certificate, InitializedValidators}, validator_store::ValidatorStore, SlashingDatabase, SLASHING_PROTECTION_FILENAME, }; /// 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(20); /// Set to `false` to send the Web3Signer logs to the console during tests. Logs are useful when /// debugging. const SUPPRESS_WEB3SIGNER_LOGS: bool = true; 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 {} impl SignedObject for SignedBeaconBlock {} impl SignedObject for SignedAggregateAndProof {} impl SignedObject for SelectionProof {} impl SignedObject for SyncSelectionProof {} impl SignedObject for SyncCommitteeMessage {} impl SignedObject for SignedContributionAndProof {} /// 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 { PathBuf::from(env::var("OUT_DIR").unwrap()) .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("cert.pem") } /// 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(); } } impl Web3SignerRig { pub async fn new(network: &str, listen_address: &str, listen_port: u16) -> Self { 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(); serde_yaml::to_writer(key_config_file, &key_config).unwrap(); let tls_keystore_file = tls_dir().join("key.p12"); let tls_keystore_password_file = tls_dir().join("password.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("--tls-allow-any-client=true") .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)) .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 http_client = Client::builder() .add_root_certificate(certificate) .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>, _validator_dir: TempDir, runtime: Arc, _runtime_shutdown: exit_future::Signal, } impl ValidatorStoreRig { pub async fn new(validator_definitions: Vec, spec: ChainSpec) -> Self { let log = environment::null_logger().unwrap(); let validator_dir = TempDir::new().unwrap(); let validator_definitions = ValidatorDefinitions::from(validator_definitions); let initialized_validators = InitializedValidators::from_definitions( validator_definitions, validator_dir.path().into(), log.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) = exit_future::signal(); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let executor = TaskExecutor::new(Arc::downgrade(&runtime), exit, log.clone(), 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 validator_store = ValidatorStore::<_, E>::new( initialized_validators, slashing_protection, Hash256::repeat_byte(42), spec, None, slot_clock, executor, log.clone(), ); Self { validator_store: Arc::new(validator_store), _validator_dir: validator_dir, runtime, _runtime_shutdown: runtime_shutdown, } } 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, 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, spec: 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, 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], 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, description: String::default(), signing_definition: SigningDefinition::Web3Signer { url: signer_rig.url.to_string(), root_certificate_path: Some(root_certificate_path()), request_timeout_ms: None, }, }; ValidatorStoreRig::new(vec![validator_definition], 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( self, case_name: &str, generate_sig: F, ) -> Self where F: Fn(PublicKeyBytes, Arc>) -> R, R: Future, // 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 } } /// Get a generic, arbitrary attestation for signing. fn get_attestation() -> Attestation { Attestation { 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(), } } /// 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 = &network_config.chain_spec::().unwrap(); TestingRig::new(network, 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| async move { let block = BeaconBlock::Base(BeaconBlockBase::empty(spec)); let block_slot = block.slot(); validator_store .sign_block(pubkey, block, block_slot) .await .unwrap() }) .await .assert_signatures_match("attestation", |pubkey, validator_store| async move { let mut attestation = get_attestation(); validator_store .sign_attestation(pubkey, 0, &mut attestation, Epoch::new(0)) .await .unwrap(); attestation }) .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; } /// 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 = &network_config.chain_spec::().unwrap(); let altair_fork_slot = spec .altair_fork_epoch .unwrap() .start_slot(E::slots_per_epoch()); TestingRig::new(network, spec.clone(), listen_port) .await .assert_signatures_match( "beacon_block_altair", |pubkey, validator_store| async move { let mut altair_block = BeaconBlockAltair::empty(spec); altair_block.slot = altair_fork_slot; validator_store .sign_block(pubkey, BeaconBlock::Altair(altair_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; } #[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 prater_base_types() { test_base_types("prater", 4246).await } #[tokio::test] async fn prater_altair_types() { test_altair_types("prater", 4247).await } }