diff --git a/tests/beacon_chain_sim/Cargo.toml b/tests/beacon_chain_sim/Cargo.toml index e731ffe1ee..a93e659606 100644 --- a/tests/beacon_chain_sim/Cargo.toml +++ b/tests/beacon_chain_sim/Cargo.toml @@ -13,3 +13,4 @@ validator_client = { path = "../../validator_client" } parking_lot = "0.9.0" futures = "0.1.29" tokio = "0.1.22" +eth1_test_rig = { path = "../eth1_test_rig" } diff --git a/tests/beacon_chain_sim/src/checks.rs b/tests/beacon_chain_sim/src/checks.rs index 8ed3da7e09..6ff1973167 100644 --- a/tests/beacon_chain_sim/src/checks.rs +++ b/tests/beacon_chain_sim/src/checks.rs @@ -2,13 +2,26 @@ use crate::local_network::LocalNetwork; use futures::{stream, Future, IntoFuture, Stream}; use std::time::{Duration, Instant}; use tokio::timer::Delay; -use types::{Epoch, EthSpec}; +use types::{Epoch, EthSpec, Slot}; +pub fn verify_validator_onboarding( + network: LocalNetwork, + slot_duration: Duration, + expected_validator_count: usize, +) -> impl Future { + slot_delay(Slot::new(32), slot_duration) + .and_then(move |()| verify_validator_count(network, expected_validator_count)) +} + +/// Checks that the chain has made the first possible finalization. +/// +/// Intended to be run as soon as chain starts. pub fn verify_first_finalization( network: LocalNetwork, slot_duration: Duration, ) -> impl Future { - epoch_delay(Epoch::new(4), slot_duration, E::slots_per_epoch()) + // TODO: set 5 back to 4. + epoch_delay(Epoch::new(5), slot_duration, E::slots_per_epoch()) .and_then(|()| verify_all_finalized_at(network, Epoch::new(2))) } @@ -23,6 +36,13 @@ fn epoch_delay( Delay::new(Instant::now() + duration).map_err(|e| format!("Epoch delay failed: {:?}", e)) } +/// Delays for `slots`, plus half a slot extra. +fn slot_delay(slots: Slot, slot_duration: Duration) -> impl Future { + let duration = slot_duration * slots.as_u64() as u32 + slot_duration / 2; + + Delay::new(Instant::now() + duration).map_err(|e| format!("Epoch delay failed: {:?}", e)) +} + fn verify_all_finalized_at( network: LocalNetwork, epoch: Epoch, @@ -55,3 +75,43 @@ fn verify_all_finalized_at( } }) } + +fn verify_validator_count( + network: LocalNetwork, + expected_count: usize, +) -> impl Future { + network + .remote_nodes() + .into_future() + .and_then(|remote_nodes| { + stream::unfold(remote_nodes.into_iter(), |mut iter| { + iter.next().map(|remote_node| { + let beacon = remote_node.http.beacon(); + beacon + .get_head() + .map_err(|e| format!("Get head via http failed: {:?}", e)) + .and_then(move |head| { + beacon + .get_state_by_root(head.state_root) + .map(|(state, _root)| state) + .map_err(|e| format!("Get state root via http failed: {:?}", e)) + }) + .map(|state| (state.validators.len(), iter)) + }) + }) + .collect() + }) + .and_then(move |validator_counts| { + if validator_counts + .iter() + .any(|count| *count != expected_count) + { + Err(format!( + "Nodes do not all have {} validators in their state. Validator counts: {:?}", + expected_count, validator_counts + )) + } else { + Ok(()) + } + }) +} diff --git a/tests/beacon_chain_sim/src/local_network.rs b/tests/beacon_chain_sim/src/local_network.rs index 4db2239053..84f76c89bf 100644 --- a/tests/beacon_chain_sim/src/local_network.rs +++ b/tests/beacon_chain_sim/src/local_network.rs @@ -37,22 +37,35 @@ impl Deref for LocalNetwork { impl LocalNetwork { /// Creates a new network with a single `BeaconNode`. - pub fn new(context: RuntimeContext, beacon_config: ClientConfig) -> Result { - let beacon_nodes = vec![BeaconNode::production( - context.service_context("boot_node".into()), - beacon_config, - )]; - - Ok(Self { - inner: Arc::new(Inner { - context, - beacon_nodes: RwLock::new(beacon_nodes), - validator_clients: RwLock::new(vec![]), - }), - }) + pub fn new( + context: RuntimeContext, + beacon_config: ClientConfig, + ) -> impl Future { + BeaconNode::production(context.service_context("boot_node".into()), beacon_config).map( + |beacon_node| Self { + inner: Arc::new(Inner { + context, + beacon_nodes: RwLock::new(vec![beacon_node]), + validator_clients: RwLock::new(vec![]), + }), + }, + ) } - pub fn add_beacon_node(&self, mut beacon_config: ClientConfig) -> Result<(), String> { + pub fn beacon_node_count(&self) -> usize { + self.beacon_nodes.read().len() + } + + pub fn validator_client_count(&self) -> usize { + self.validator_clients.read().len() + } + + pub fn add_beacon_node( + &self, + mut beacon_config: ClientConfig, + ) -> impl Future { + let self_1 = self.clone(); + self.beacon_nodes .read() .first() @@ -64,18 +77,17 @@ impl LocalNetwork { .expect("bootnode must have a network"), ); }) - .ok_or_else(|| "No boot node".to_string())?; + .expect("should have atleast one node"); let index = self.beacon_nodes.read().len(); - let beacon_node = BeaconNode::production( + BeaconNode::production( self.context.service_context(format!("node_{}", index)), beacon_config, - ); - - self.beacon_nodes.write().push(beacon_node); - - Ok(()) + ) + .map(move |beacon_node| { + self_1.beacon_nodes.write().push(beacon_node); + }) } pub fn add_validator_client( diff --git a/tests/beacon_chain_sim/src/main.rs b/tests/beacon_chain_sim/src/main.rs index 206a22b6fb..d55d0811e2 100644 --- a/tests/beacon_chain_sim/src/main.rs +++ b/tests/beacon_chain_sim/src/main.rs @@ -1,13 +1,15 @@ mod checks; mod local_network; -use futures::{future, stream, Future, Stream}; +use eth1_test_rig::GanacheEth1Instance; +use futures::{stream, Future, Stream}; use local_network::LocalNetwork; use node_test_rig::{ environment::EnvironmentBuilder, testing_client_config, ClientGenesis, LocalBeaconNode, LocalValidatorClient, ProductionClient, ValidatorConfig, }; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, Instant}; +use tokio::timer::Interval; use types::MinimalEthSpec; pub type E = MinimalEthSpec; @@ -16,11 +18,11 @@ pub type ValidatorClient = LocalValidatorClient; fn main() { let nodes = 4; - let validators_per_node = 64 / nodes; + let validators_per_node = 20; match async_sim(nodes, validators_per_node, 4) { Ok(()) => println!("Simulation exited successfully"), - Err(e) => println!("Simulation exited with error: {}", e), + Err(e) => eprintln!("Simulation exited with error: {}", e), } } @@ -34,58 +36,122 @@ fn async_sim( .multi_threaded_tokio_runtime()? .build()?; - env.eth2_config.spec.milliseconds_per_slot = - env.eth2_config.spec.milliseconds_per_slot / speed_up_factor; + let eth1_block_time = Duration::from_millis(15_000 / speed_up_factor); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("should get system time") - .as_secs(); + let spec = &mut env.eth2_config.spec; - let mut beacon_config = testing_client_config(); - beacon_config.genesis = ClientGenesis::Interop { - genesis_time: now, - validator_count: node_count * validators_per_node, - }; + spec.milliseconds_per_slot = spec.milliseconds_per_slot / speed_up_factor; + spec.eth1_follow_distance = 16; + spec.seconds_per_day = eth1_block_time.as_secs() * spec.eth1_follow_distance * 2; + spec.min_genesis_time = 0; + spec.min_genesis_active_validator_count = 64; - let slot_duration = Duration::from_millis(env.eth2_config.spec.milliseconds_per_slot); + let slot_duration = Duration::from_millis(spec.milliseconds_per_slot); + let validator_count = validators_per_node * node_count; + let deposit_amount = env.eth2_config.spec.max_effective_balance; - let network = LocalNetwork::new(env.core_context(), beacon_config.clone())?; + let context = env.core_context(); + let executor = context.executor.clone(); - let network_1 = network.clone(); - let network_2 = network.clone(); - let network_3 = network.clone(); + let future = GanacheEth1Instance::new() + .map(move |ganache_eth1_instance| { + let deposit_contract = ganache_eth1_instance.deposit_contract; + let ganache = ganache_eth1_instance.ganache; + let eth1_endpoint = ganache.endpoint(); + let deposit_contract_address = deposit_contract.address(); - let future = future::ok(()) - .and_then(move |()| { - let network = network_1; + // Start a timer that produces eth1 blocks on an interval. + executor.spawn( + Interval::new(Instant::now(), eth1_block_time) + .map_err(|_| eprintln!("Eth1 block timer failed")) + .for_each(move |_| ganache.evm_mine().map_err(|_| ())) + .map_err(|_| eprintln!("Eth1 evm_mine failed")) + .map(|_| ()), + ); - for _ in 0..node_count - 1 { - network.add_beacon_node(beacon_config.clone())?; - } + // Submit deposits to the deposit contract. + executor.spawn( + stream::unfold(0..validator_count, move |mut iter| { + iter.next().map(|i| { + println!("Submitting deposit for validator {}...", i); + deposit_contract + .deposit_deterministic_async::(i, deposit_amount) + .map(|_| ((), iter)) + }) + }) + .collect() + .map(|_| ()) + .map_err(|e| eprintln!("Error submitting deposit: {}", e)), + ); - Ok(()) + (deposit_contract_address, eth1_endpoint) }) - .and_then(move |()| { - let network = network_2; + .map(move |(deposit_contract_address, eth1_endpoint)| { + let mut beacon_config = testing_client_config(); + + beacon_config.genesis = ClientGenesis::DepositContract; + beacon_config.eth1.endpoint = eth1_endpoint; + beacon_config.eth1.deposit_contract_address = deposit_contract_address; + beacon_config.eth1.deposit_contract_deploy_block = 0; + beacon_config.eth1.lowest_cached_block_number = 0; + beacon_config.eth1.follow_distance = 1; + beacon_config.dummy_eth1_backend = false; + beacon_config.sync_eth1_chain = true; + + beacon_config + }) + .and_then(move |beacon_config| { + LocalNetwork::new(context, beacon_config.clone()) + .map(|network| (network, beacon_config)) + }) + .and_then(move |(network, beacon_config)| { + let network_1 = network.clone(); + + stream::unfold(0..node_count - 1, move |mut iter| { + iter.next().map(|_| { + network_1 + .add_beacon_node(beacon_config.clone()) + .map(|()| ((), iter)) + }) + }) + .collect() + .map(|_| network) + }) + .and_then(move |network| { + let network_1 = network.clone(); stream::unfold(0..node_count, move |mut iter| { iter.next().map(|i| { let indices = (i * validators_per_node..(i + 1) * validators_per_node) .collect::>(); - network + network_1 .add_validator_client(ValidatorConfig::default(), i, indices) .map(|()| ((), iter)) }) }) .collect() - .map(|_| ()) + .map(|_| network) }) - .and_then(move |_| { - let network = network_3; + .and_then(move |network| { + checks::verify_first_finalization(network.clone(), slot_duration) + .join(checks::verify_validator_onboarding( + network.clone(), + slot_duration, + validator_count, + )) + .map(|_| network) + }) + .map(|network| { + println!( + "Simulation complete. Finished with {} beacon nodes and {} validator clients", + network.beacon_node_count(), + network.validator_client_count() + ); - checks::verify_first_finalization(network, slot_duration) + // Be explicit about dropping the network, as this kills all the nodes. This ensures + // all the checks have adequate time to pass. + drop(network) }); env.runtime().block_on(future) diff --git a/tests/eth1_test_rig/src/lib.rs b/tests/eth1_test_rig/src/lib.rs index 178a687329..0eaa29c3e9 100644 --- a/tests/eth1_test_rig/src/lib.rs +++ b/tests/eth1_test_rig/src/lib.rs @@ -13,7 +13,7 @@ use ganache::GanacheInstance; use std::time::{Duration, Instant}; use tokio::{runtime::Runtime, timer::Delay}; use types::DepositData; -use types::{EthSpec, Hash256, Keypair, Signature}; +use types::{test_utils::generate_deterministic_keypair, EthSpec, Hash256, Keypair, Signature}; use web3::contract::{Contract, Options}; use web3::transports::Http; use web3::types::{Address, TransactionRequest, U256}; @@ -156,6 +156,25 @@ impl DepositContract { .map_err(|e| format!("Deposit failed: {:?}", e)) } + pub fn deposit_deterministic_async( + &self, + keypair_index: usize, + amount: u64, + ) -> impl Future { + let keypair = generate_deterministic_keypair(keypair_index); + + let mut deposit = DepositData { + pubkey: keypair.pk.into(), + withdrawal_credentials: Hash256::zero(), + amount, + signature: Signature::empty_signature().into(), + }; + + deposit.signature = deposit.create_signature(&keypair.sk, &E::default_spec()); + + self.deposit_async(deposit) + } + /// Performs a non-blocking deposit. pub fn deposit_async( &self, diff --git a/tests/node_test_rig/src/lib.rs b/tests/node_test_rig/src/lib.rs index 603b0eff89..9a8d954cb1 100644 --- a/tests/node_test_rig/src/lib.rs +++ b/tests/node_test_rig/src/lib.rs @@ -29,7 +29,10 @@ impl LocalBeaconNode> { /// Starts a new, production beacon node on the tokio runtime in the given `context`. /// /// The node created is using the same types as the node we use in production. - pub fn production(context: RuntimeContext, mut client_config: ClientConfig) -> Self { + pub fn production( + context: RuntimeContext, + mut client_config: ClientConfig, + ) -> impl Future { // Creates a temporary directory that will be deleted once this `TempDir` is dropped. let datadir = TempDir::new("lighthouse_node_test_rig") .expect("should create temp directory for client datadir"); @@ -37,12 +40,10 @@ impl LocalBeaconNode> { client_config.data_dir = datadir.path().into(); client_config.network.network_dir = PathBuf::from(datadir.path()).join("network"); - let client = ProductionBeaconNode::new(context, client_config) - .wait() - .expect("should build production client") - .into_inner(); - - LocalBeaconNode { client, datadir } + ProductionBeaconNode::new(context, client_config).map(move |client| Self { + client: client.into_inner(), + datadir, + }) } }