From 67f8405921750f6a21e5c21d54688ecf31d4e825 Mon Sep 17 00:00:00 2001 From: Mac L Date: Tue, 23 Apr 2024 01:08:36 +1000 Subject: [PATCH] Update Simulator tests (#5520) * Rewrite Simulator * Add fallback simulator * Try Sean's test fix * More fixes * Cleanup * Merge branch 'unstable' into update-simulator * Update cli.rs * Add sync sim to basic sim * Formatting * Add fixes and new block production check * Merge branch 'unstable' of https://github.com/sigp/lighthouse into update-simulator * fix compile --- .github/workflows/test-suite.yml | 58 +-- Cargo.lock | 6 +- beacon_node/client/Cargo.toml | 1 + beacon_node/client/src/builder.rs | 16 + beacon_node/client/src/config.rs | 5 + .../test_utils/execution_block_generator.rs | 34 +- .../src/test_utils/handle_rpc.rs | 6 + .../execution_layer/src/test_utils/mod.rs | 1 + testing/simulator/Cargo.toml | 5 +- .../src/{eth1_sim.rs => basic_sim.rs} | 274 +++++------- testing/simulator/src/checks.rs | 174 +++++++- testing/simulator/src/cli.rs | 163 ++++---- testing/simulator/src/fallback_sim.rs | 261 ++++++++++++ testing/simulator/src/local_network.rs | 355 ++++++++++------ testing/simulator/src/main.rs | 26 +- testing/simulator/src/no_eth1_sim.rs | 172 -------- testing/simulator/src/sync_sim.rs | 390 ------------------ validator_client/src/block_service.rs | 1 + 18 files changed, 928 insertions(+), 1020 deletions(-) rename testing/simulator/src/{eth1_sim.rs => basic_sim.rs} (51%) create mode 100644 testing/simulator/src/fallback_sim.rs delete mode 100644 testing/simulator/src/no_eth1_sim.rs delete mode 100644 testing/simulator/src/sync_sim.rs diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index a8db6fab8fd..413dd2b95dd 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -225,8 +225,8 @@ jobs: run: docker build --build-arg FEATURES=portable -t lighthouse:local . - name: Test the built image run: docker run -t lighthouse:local lighthouse --version - eth1-simulator-ubuntu: - name: eth1-simulator-ubuntu + basic-simulator-ubuntu: + name: basic-simulator-ubuntu runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -235,42 +235,10 @@ jobs: with: channel: stable cache-target: release - - name: Install Foundry (anvil) - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run the beacon chain sim that starts from an eth1 contract - run: cargo run --release --bin simulator eth1-sim - merge-transition-ubuntu: - name: merge-transition-ubuntu - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release - - name: Install Foundry (anvil) - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run the beacon chain sim and go through the merge transition - run: cargo run --release --bin simulator eth1-sim --post-merge - no-eth1-simulator-ubuntu: - name: no-eth1-simulator-ubuntu - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release - - name: Run the beacon chain sim without an eth1 connection - run: cargo run --release --bin simulator no-eth1-sim - syncing-simulator-ubuntu: - name: syncing-simulator-ubuntu + - name: Run a basic beacon chain sim that starts from Bellatrix + run: cargo run --release --bin simulator basic-sim + fallback-simulator-ubuntu: + name: fallback-simulator-ubuntu runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -279,12 +247,8 @@ jobs: with: channel: stable cache-target: release - - name: Install Foundry (anvil) - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run the syncing simulator - run: cargo run --release --bin simulator syncing-sim + - name: Run a beacon chain sim which tests VC fallback behaviour + run: cargo run --release --bin simulator fallback-sim doppelganger-protection-test: name: doppelganger-protection-test runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} @@ -442,10 +406,8 @@ jobs: 'state-transition-vectors-ubuntu', 'ef-tests-ubuntu', 'dockerfile-ubuntu', - 'eth1-simulator-ubuntu', - 'merge-transition-ubuntu', - 'no-eth1-simulator-ubuntu', - 'syncing-simulator-ubuntu', + 'basic-simulator-ubuntu', + 'fallback-simulator-ubuntu', 'doppelganger-protection-test', 'execution-engine-integration-ubuntu', 'check-code', diff --git a/Cargo.lock b/Cargo.lock index 79be0a55f18..4e3557c7efb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1420,6 +1420,7 @@ dependencies = [ "time", "timer", "tokio", + "tree_hash", "types", ] @@ -7831,13 +7832,16 @@ dependencies = [ "clap 2.34.0", "env_logger 0.9.3", "eth1", - "eth1_test_rig", + "eth2_network_config", + "ethereum-types 0.14.1", "execution_layer", "futures", "node_test_rig", "parking_lot 0.12.1", "rayon", "sensitive_url", + "serde_json", + "ssz_types", "tokio", "types", ] diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 03cbcc9ff7f..16c4a947a66 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -46,3 +46,4 @@ execution_layer = { workspace = true } beacon_processor = { workspace = true } num_cpus = { workspace = true } ethereum_ssz = { workspace = true } +tree_hash = { workspace = true } diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 6c505751542..e7f201b8521 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -26,6 +26,7 @@ use eth2::{ types::{BlockId, StateId}, BeaconNodeHttpClient, Error as ApiError, Timeouts, }; +use execution_layer::test_utils::generate_genesis_header; use execution_layer::ExecutionLayer; use futures::channel::mpsc::Receiver; use genesis::{interop_genesis_state, Eth1GenesisService, DEFAULT_ETH1_BLOCK_HASH}; @@ -267,6 +268,21 @@ where )?; builder.genesis_state(genesis_state).map(|v| (v, None))? } + ClientGenesis::InteropMerge { + validator_count, + genesis_time, + } => { + let execution_payload_header = generate_genesis_header(&spec, true); + let keypairs = generate_deterministic_keypairs(validator_count); + let genesis_state = interop_genesis_state( + &keypairs, + genesis_time, + Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), + execution_payload_header, + &spec, + )?; + builder.genesis_state(genesis_state).map(|v| (v, None))? + } ClientGenesis::GenesisState => { info!( context.log(), diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index 48ad77abc58..a441e2c186c 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -24,6 +24,11 @@ pub enum ClientGenesis { validator_count: usize, genesis_time: u64, }, + // Creates a genesis state similar to the 2019 Canada specs, but starting post-Merge. + InteropMerge { + validator_count: usize, + genesis_time: u64, + }, /// Reads the genesis state and other persisted data from the `Store`. FromStore, /// Connects to an eth1 node and waits until it can create the genesis state from the deposit diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 08f8ca68435..bac2304fa85 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -91,7 +91,14 @@ impl Block { pub fn as_execution_block_with_tx(&self) -> Option> { match self { Block::PoS(payload) => Some(payload.clone().try_into().unwrap()), - Block::PoW(_) => None, + Block::PoW(block) => Some( + ExecutionPayload::Merge(ExecutionPayloadMerge { + block_hash: block.block_hash, + ..Default::default() + }) + .try_into() + .unwrap(), + ), } } } @@ -190,6 +197,19 @@ impl ExecutionBlockGenerator { .map(|block| block.as_execution_block(self.terminal_total_difficulty)) } + pub fn genesis_block(&self) -> Option> { + if let Some(genesis_block_hash) = self.block_hashes.get(&0) { + self.blocks.get(genesis_block_hash.first()?).cloned() + } else { + None + } + } + + pub fn genesis_execution_block(&self) -> Option { + self.genesis_block() + .map(|block| block.as_execution_block(self.terminal_total_difficulty)) + } + pub fn block_by_number(&self, number: u64) -> Option> { // Get the latest canonical head block let mut latest_block = self.latest_block()?; @@ -502,13 +522,6 @@ impl ExecutionBlockGenerator { let id = match payload_attributes { None => None, Some(attributes) => { - if !self.blocks.iter().any(|(_, block)| { - block.block_hash() == self.terminal_block_hash - || block.block_number() == self.terminal_block_number - }) { - return Err("refusing to create payload id before terminal block".to_string()); - } - let parent = self .blocks .get(&head_block_hash) @@ -766,12 +779,14 @@ pub fn generate_genesis_header( generate_genesis_block(spec.terminal_total_difficulty, DEFAULT_TERMINAL_BLOCK) .ok() .map(|block| block.block_hash); + let empty_transactions_root = Transactions::::empty().tree_hash_root(); match genesis_fork { ForkName::Base | ForkName::Altair => None, ForkName::Merge => { if post_transition_merge { let mut header = ExecutionPayloadHeader::Merge(<_>::default()); *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); + *header.transactions_root_mut() = empty_transactions_root; Some(header) } else { Some(ExecutionPayloadHeader::::Merge(<_>::default())) @@ -780,16 +795,19 @@ pub fn generate_genesis_header( ForkName::Capella => { let mut header = ExecutionPayloadHeader::Capella(<_>::default()); *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); + *header.transactions_root_mut() = empty_transactions_root; Some(header) } ForkName::Deneb => { let mut header = ExecutionPayloadHeader::Deneb(<_>::default()); *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); + *header.transactions_root_mut() = empty_transactions_root; Some(header) } ForkName::Electra => { let mut header = ExecutionPayloadHeader::Electra(<_>::default()); *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); + *header.transactions_root_mut() = empty_transactions_root; Some(header) } } diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 77d972ab88e..e0ca07dcc6e 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -49,6 +49,12 @@ pub async fn handle_rpc( .latest_execution_block(), ) .unwrap()), + "0x0" => Ok(serde_json::to_value( + ctx.execution_block_generator + .read() + .genesis_execution_block(), + ) + .unwrap()), other => Err(( format!("The tag {} is not supported", other), BAD_PARAMS_ERROR_CODE, diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index eeb449b9920..43a6ee8ac22 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -58,6 +58,7 @@ mod mock_builder; mod mock_execution_layer; /// Configuration for the MockExecutionLayer. +#[derive(Clone)] pub struct MockExecutionConfig { pub server_config: Config, pub jwt_key: JwtKey, diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index eadcaf51b20..d7ff7b3dd85 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -14,8 +14,11 @@ types = { workspace = true } parking_lot = { workspace = true } futures = { workspace = true } tokio = { workspace = true } -eth1_test_rig = { workspace = true } env_logger = { workspace = true } clap = { workspace = true } rayon = { workspace = true } sensitive_url = { path = "../../common/sensitive_url" } +ssz_types = { workspace = true } +ethereum-types = { workspace = true } +eth2_network_config = { workspace = true } +serde_json = { workspace = true } diff --git a/testing/simulator/src/eth1_sim.rs b/testing/simulator/src/basic_sim.rs similarity index 51% rename from testing/simulator/src/eth1_sim.rs rename to testing/simulator/src/basic_sim.rs index 20c7c9ce9ab..755bb71b430 100644 --- a/testing/simulator/src/eth1_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -1,50 +1,48 @@ -use crate::local_network::{EXECUTION_PORT, TERMINAL_BLOCK, TERMINAL_DIFFICULTY}; +use crate::local_network::LocalNetworkParams; +use crate::local_network::TERMINAL_BLOCK; use crate::{checks, LocalNetwork}; use clap::ArgMatches; -use eth1::{Eth1Endpoint, DEFAULT_CHAIN_ID}; -use eth1_test_rig::AnvilEth1Instance; use crate::retry::with_retry; -use execution_layer::http::deposit_methods::Eth1Id; use futures::prelude::*; -use node_test_rig::environment::RuntimeContext; use node_test_rig::{ environment::{EnvironmentBuilder, LoggerConfig}, - testing_client_config, testing_validator_config, ApiTopic, ClientConfig, ClientGenesis, - ValidatorFiles, + testing_validator_config, ApiTopic, ValidatorFiles, }; use rayon::prelude::*; -use sensitive_url::SensitiveUrl; use std::cmp::max; -use std::net::Ipv4Addr; use std::time::Duration; use tokio::time::sleep; use types::{Epoch, EthSpec, MinimalEthSpec}; const END_EPOCH: u64 = 16; -const ALTAIR_FORK_EPOCH: u64 = 1; -const BELLATRIX_FORK_EPOCH: u64 = 2; +const GENESIS_DELAY: u64 = 32; +const ALTAIR_FORK_EPOCH: u64 = 0; +const BELLATRIX_FORK_EPOCH: u64 = 0; +const CAPELLA_FORK_EPOCH: u64 = 1; +const DENEB_FORK_EPOCH: u64 = 2; +//const ELECTRA_FORK_EPOCH: u64 = 3; const SUGGESTED_FEE_RECIPIENT: [u8; 20] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; -pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { - let node_count = value_t!(matches, "nodes", usize).expect("missing nodes default"); - let proposer_nodes = value_t!(matches, "proposer-nodes", usize).unwrap_or(0); - println!("PROPOSER-NODES: {}", proposer_nodes); - let validators_per_node = value_t!(matches, "validators_per_node", usize) - .expect("missing validators_per_node default"); +pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { + let node_count = value_t!(matches, "nodes", usize).expect("Missing nodes default"); + let proposer_nodes = + value_t!(matches, "proposer-nodes", usize).expect("Missing proposer-nodes default"); + let validators_per_node = value_t!(matches, "validators-per-node", usize) + .expect("Missing validators-per-node default"); let speed_up_factor = - value_t!(matches, "speed_up_factor", u64).expect("missing speed_up_factor default"); - let continue_after_checks = matches.is_present("continue_after_checks"); - let post_merge_sim = matches.is_present("post-merge"); + value_t!(matches, "speed-up-factor", u64).expect("Missing speed-up-factor default"); + let log_level = value_t!(matches, "debug-level", String).expect("Missing default log-level"); + let continue_after_checks = matches.is_present("continue-after-checks"); - println!("Beacon Chain Simulator:"); - println!(" nodes:{}, proposer_nodes: {}", node_count, proposer_nodes); - - println!(" validators_per_node:{}", validators_per_node); - println!(" post merge simulation:{}", post_merge_sim); - println!(" continue_after_checks:{}", continue_after_checks); + println!("Basic Simulator:"); + println!(" nodes: {}", node_count); + println!(" proposer-nodes: {}", proposer_nodes); + println!(" validators-per-node: {}", validators_per_node); + println!(" speed-up-factor: {}", speed_up_factor); + println!(" continue-after-checks: {}", continue_after_checks); // Generate the directories and keystores required for the validator clients. let validator_files = (0..node_count) @@ -65,8 +63,8 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { let mut env = EnvironmentBuilder::minimal() .initialize_logger(LoggerConfig { path: None, - debug_level: String::from("debug"), - logfile_debug_level: String::from("debug"), + debug_level: log_level.clone(), + logfile_debug_level: log_level, log_format: None, logfile_format: None, log_color: false, @@ -80,32 +78,29 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { .multi_threaded_tokio_runtime()? .build()?; - let eth1_block_time = Duration::from_millis(15_000 / speed_up_factor); - let spec = &mut env.eth2_config.spec; let total_validator_count = validators_per_node * node_count; - let altair_fork_version = spec.altair_fork_version; - let bellatrix_fork_version = spec.bellatrix_fork_version; + let genesis_delay = GENESIS_DELAY; + + // Convenience variables. Update these values when adding a newer fork. + let latest_fork_version = spec.deneb_fork_version; + let latest_fork_start_epoch = DENEB_FORK_EPOCH; spec.seconds_per_slot /= speed_up_factor; spec.seconds_per_slot = max(1, spec.seconds_per_slot); - spec.eth1_follow_distance = 16; - spec.genesis_delay = eth1_block_time.as_secs() * spec.eth1_follow_distance * 2; + spec.genesis_delay = genesis_delay; spec.min_genesis_time = 0; spec.min_genesis_active_validator_count = total_validator_count as u64; - spec.seconds_per_eth1_block = eth1_block_time.as_secs(); spec.altair_fork_epoch = Some(Epoch::new(ALTAIR_FORK_EPOCH)); - // Set these parameters only if we are doing a merge simulation - if post_merge_sim { - spec.terminal_total_difficulty = TERMINAL_DIFFICULTY.into(); - spec.bellatrix_fork_epoch = Some(Epoch::new(BELLATRIX_FORK_EPOCH)); - } + spec.bellatrix_fork_epoch = Some(Epoch::new(BELLATRIX_FORK_EPOCH)); + spec.capella_fork_epoch = Some(Epoch::new(CAPELLA_FORK_EPOCH)); + spec.deneb_fork_epoch = Some(Epoch::new(DENEB_FORK_EPOCH)); + //spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); - let seconds_per_slot = spec.seconds_per_slot; let slot_duration = Duration::from_secs(spec.seconds_per_slot); + let slots_per_epoch = MinimalEthSpec::slots_per_epoch(); let initial_validator_count = spec.min_genesis_active_validator_count as usize; - let deposit_amount = env.eth2_config.spec.max_effective_balance; let context = env.core_context(); @@ -114,36 +109,36 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { * Create a new `LocalNetwork` with one beacon node. */ let max_retries = 3; - let (network, beacon_config) = with_retry(max_retries, || { - Box::pin(create_local_network( + let (network, beacon_config, mock_execution_config) = with_retry(max_retries, || { + Box::pin(LocalNetwork::create_local_network( + None, + None, LocalNetworkParams { - eth1_block_time, - total_validator_count, - deposit_amount, + validator_count: total_validator_count, node_count, proposer_nodes, - post_merge_sim, + genesis_delay, }, context.clone(), )) }) .await?; - /* - * One by one, add beacon nodes to the network. - */ - for _ in 0..node_count - 1 { + // Add nodes to the network. + for _ in 0..node_count { network - .add_beacon_node(beacon_config.clone(), false) + .add_beacon_node(beacon_config.clone(), mock_execution_config.clone(), false) .await?; } /* * One by one, add proposer nodes to the network. */ - for _ in 0..proposer_nodes - 1 { + for _ in 0..proposer_nodes { println!("Adding a proposer node"); - network.add_beacon_node(beacon_config.clone(), true).await?; + network + .add_beacon_node(beacon_config.clone(), mock_execution_config.clone(), true) + .await?; } /* @@ -156,9 +151,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { executor.spawn( async move { let mut validator_config = testing_validator_config(); - if post_merge_sim { - validator_config.fee_recipient = Some(SUGGESTED_FEE_RECIPIENT.into()); - } + validator_config.fee_recipient = Some(SUGGESTED_FEE_RECIPIENT.into()); println!("Adding validator client {}", i); // Enable broadcast on every 4th node. @@ -175,7 +168,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { .await } else { network_1 - .add_validator_client(validator_config, i, files, i % 2 == 0) + .add_validator_client(validator_config, i, files) .await } .expect("should add validator"); @@ -184,25 +177,15 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { ); } + // Set all payloads as valid. This effectively assumes the EL is infalliable. + network.execution_nodes.write().iter().for_each(|node| { + node.server.all_payloads_valid(); + }); + let duration_to_genesis = network.duration_to_genesis().await; println!("Duration to genesis: {}", duration_to_genesis.as_secs()); sleep(duration_to_genesis).await; - if post_merge_sim { - let executor = executor.clone(); - let network_2 = network.clone(); - executor.spawn( - async move { - println!("Mining pow blocks"); - let mut interval = tokio::time::interval(Duration::from_secs(seconds_per_slot)); - for i in 1..=TERMINAL_BLOCK + 1 { - interval.tick().await; - let _ = network_2.mine_pow_blocks(i); - } - }, - "pow_mining", - ); - } /* * Start the checks that ensure the network performs as expected. * @@ -211,6 +194,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { * tests start at the right time. Whilst this is works well for now, it's subject to * breakage by changes to the VC. */ + let network_1 = network.clone(); let ( finalization, @@ -221,13 +205,16 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { sync_aggregate, transition, light_client_update, + blobs, + start_node_with_delay, + sync, ) = futures::join!( // Check that the chain finalizes at the first given opportunity. checks::verify_first_finalization(network.clone(), slot_duration), // Check that a block is produced at every slot. checks::verify_full_block_production_up_to( network.clone(), - Epoch::new(END_EPOCH).start_slot(MinimalEthSpec::slots_per_epoch()), + Epoch::new(END_EPOCH).start_slot(slots_per_epoch), slot_duration, ), // Check that the chain starts with the expected validator count. @@ -246,41 +233,55 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { // Check that all nodes have transitioned to the required fork. checks::verify_fork_version( network.clone(), - if post_merge_sim { - Epoch::new(BELLATRIX_FORK_EPOCH) - } else { - Epoch::new(ALTAIR_FORK_EPOCH) - }, + Epoch::new(latest_fork_start_epoch), slot_duration, - if post_merge_sim { - bellatrix_fork_version - } else { - altair_fork_version - } + latest_fork_version, ), // Check that all sync aggregates are full. checks::verify_full_sync_aggregates_up_to( network.clone(), // Start checking for sync_aggregates at `FORK_EPOCH + 1` to account for // inefficiencies in finding subnet peers at the `fork_slot`. - Epoch::new(ALTAIR_FORK_EPOCH + 1).start_slot(MinimalEthSpec::slots_per_epoch()), - Epoch::new(END_EPOCH).start_slot(MinimalEthSpec::slots_per_epoch()), + Epoch::new(ALTAIR_FORK_EPOCH + 1).start_slot(slots_per_epoch), + Epoch::new(END_EPOCH).start_slot(slots_per_epoch), slot_duration, ), // Check that the transition block is finalized. checks::verify_transition_block_finalized( network.clone(), - Epoch::new(TERMINAL_BLOCK / MinimalEthSpec::slots_per_epoch()), + Epoch::new(TERMINAL_BLOCK / slots_per_epoch), slot_duration, - post_merge_sim + true, ), checks::verify_light_client_updates( network.clone(), // Sync aggregate available from slot 1 after Altair fork transition. - Epoch::new(ALTAIR_FORK_EPOCH).start_slot(MinimalEthSpec::slots_per_epoch()) + 1, - Epoch::new(END_EPOCH).start_slot(MinimalEthSpec::slots_per_epoch()), + Epoch::new(ALTAIR_FORK_EPOCH).start_slot(slots_per_epoch) + 1, + Epoch::new(END_EPOCH).start_slot(slots_per_epoch), slot_duration - ) + ), + checks::verify_full_blob_production_up_to( + network.clone(), + // Blobs should be available immediately after the Deneb fork. + Epoch::new(DENEB_FORK_EPOCH).start_slot(slots_per_epoch), + Epoch::new(END_EPOCH).start_slot(slots_per_epoch), + slot_duration + ), + network_1.add_beacon_node_with_delay( + beacon_config.clone(), + mock_execution_config.clone(), + END_EPOCH - 1, + slot_duration, + slots_per_epoch + ), + checks::ensure_node_synced_up_to_slot( + network.clone(), + // This must be set to be the node which was just created. Should be equal to + // `node_count`. + node_count, + Epoch::new(END_EPOCH).start_slot(slots_per_epoch), + slot_duration, + ), ); block_prod?; @@ -291,6 +292,9 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { sync_aggregate?; transition?; light_client_update?; + blobs?; + start_node_with_delay?; + sync?; // The `final_future` either completes immediately or never completes, depending on the value // of `continue_after_checks`. @@ -321,89 +325,3 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { Ok(()) } - -struct LocalNetworkParams { - eth1_block_time: Duration, - total_validator_count: usize, - deposit_amount: u64, - node_count: usize, - proposer_nodes: usize, - post_merge_sim: bool, -} - -async fn create_local_network( - LocalNetworkParams { - eth1_block_time, - total_validator_count, - deposit_amount, - node_count, - proposer_nodes, - post_merge_sim, - }: LocalNetworkParams, - context: RuntimeContext, -) -> Result<(LocalNetwork, ClientConfig), String> { - /* - * Deploy the deposit contract, spawn tasks to keep creating new blocks and deposit - * validators. - */ - let anvil_eth1_instance = AnvilEth1Instance::new(DEFAULT_CHAIN_ID.into()).await?; - let deposit_contract = anvil_eth1_instance.deposit_contract; - let chain_id = anvil_eth1_instance.anvil.chain_id(); - let anvil = anvil_eth1_instance.anvil; - let eth1_endpoint = - SensitiveUrl::parse(anvil.endpoint().as_str()).expect("Unable to parse anvil endpoint."); - let deposit_contract_address = deposit_contract.address(); - - // Start a timer that produces eth1 blocks on an interval. - tokio::spawn(async move { - let mut interval = tokio::time::interval(eth1_block_time); - loop { - interval.tick().await; - let _ = anvil.evm_mine().await; - } - }); - - // Submit deposits to the deposit contract. - tokio::spawn(async move { - for i in 0..total_validator_count { - println!("Submitting deposit for validator {}...", i); - let _ = deposit_contract - .deposit_deterministic_async::(i, deposit_amount) - .await; - } - }); - - let mut beacon_config = testing_client_config(); - - beacon_config.genesis = ClientGenesis::DepositContract; - beacon_config.eth1.endpoint = Eth1Endpoint::NoAuth(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.eth1.node_far_behind_seconds = 20; - beacon_config.dummy_eth1_backend = false; - beacon_config.sync_eth1_chain = true; - beacon_config.eth1.auto_update_interval_millis = eth1_block_time.as_millis() as u64; - beacon_config.eth1.chain_id = Eth1Id::from(chain_id); - beacon_config.network.target_peers = node_count + proposer_nodes - 1; - - beacon_config.network.enr_address = (Some(Ipv4Addr::LOCALHOST), None); - beacon_config.network.enable_light_client_server = true; - beacon_config.chain.enable_light_client_server = true; - beacon_config.http_api.enable_light_client_server = true; - - if post_merge_sim { - let el_config = execution_layer::Config { - execution_endpoint: Some( - SensitiveUrl::parse(&format!("http://localhost:{}", EXECUTION_PORT)).unwrap(), - ), - ..Default::default() - }; - - beacon_config.execution_layer = Some(el_config); - } - - let network = LocalNetwork::new(context, beacon_config.clone()).await?; - Ok((network, beacon_config)) -} diff --git a/testing/simulator/src/checks.rs b/testing/simulator/src/checks.rs index d30e44a1174..03cc17fab3e 100644 --- a/testing/simulator/src/checks.rs +++ b/testing/simulator/src/checks.rs @@ -1,7 +1,7 @@ use crate::local_network::LocalNetwork; use node_test_rig::eth2::types::{BlockId, FinalityCheckpointsData, StateId}; use std::time::Duration; -use types::{Epoch, EthSpec, ExecPayload, ExecutionBlockHash, Hash256, Slot, Unsigned}; +use types::{Epoch, EthSpec, ExecPayload, ExecutionBlockHash, Slot, Unsigned}; /// Checks that all of the validators have on-boarded by the start of the second eth1 voting /// period. @@ -234,7 +234,7 @@ pub async fn verify_transition_block_finalized( } let first = block_hashes[0]; - if first.into_root() != Hash256::zero() && block_hashes.iter().all(|&item| item == first) { + if block_hashes.iter().all(|&item| item == first) { Ok(()) } else { Err(format!( @@ -333,3 +333,173 @@ pub(crate) async fn verify_light_client_updates( Ok(()) } + +/// Checks that a node is synced with the network. +/// Useful for ensuring that a node which started after genesis is able to sync to the head. +pub async fn ensure_node_synced_up_to_slot( + network: LocalNetwork, + node_index: usize, + upto_slot: Slot, + slot_duration: Duration, +) -> Result<(), String> { + slot_delay(upto_slot, slot_duration).await; + let node = &network + .remote_nodes()? + .get(node_index) + .expect("Should get node") + .clone(); + + let head = node + .get_beacon_blocks::(BlockId::Head) + .await + .ok() + .flatten() + .ok_or(format!("No head block exists on node {node_index}"))? + .data; + + // Check the head block is synced with the rest of the network. + if head.slot() >= upto_slot { + Ok(()) + } else { + Err(format!( + "Head not synced for node {node_index}. Found {}; Should be {upto_slot}", + head.slot() + )) + } +} + +/// Verifies that there's been blobs produced at every slot with a block from `blob_start_slot` up +/// to and including `upto_slot`. +pub async fn verify_full_blob_production_up_to( + network: LocalNetwork, + blob_start_slot: Slot, + upto_slot: Slot, + slot_duration: Duration, +) -> Result<(), String> { + slot_delay(upto_slot, slot_duration).await; + let remote_nodes = network.remote_nodes()?; + let remote_node = remote_nodes.first().unwrap(); + + for slot in blob_start_slot.as_u64()..=upto_slot.as_u64() { + // Ensure block exists. + let block = remote_node + .get_beacon_blocks::(BlockId::Slot(Slot::new(slot))) + .await + .ok() + .flatten(); + + // Only check blobs if the block exists. If you also want to ensure full block production, use + // the `verify_full_block_production_up_to` function. + if block.is_some() { + remote_node + .get_blobs::(BlockId::Slot(Slot::new(slot)), None) + .await + .map_err(|e| format!("Failed to get blobs at slot {slot:?}: {e:?}"))? + .ok_or_else(|| format!("No blobs available at slot {slot:?}"))?; + } + } + + Ok(()) +} + +// Causes the beacon node at `node_index` to disconnect from the execution layer. +pub async fn disconnect_from_execution_layer( + network: LocalNetwork, + node_index: usize, +) -> Result<(), String> { + eprintln!("Disabling Execution Node {node_index}"); + + // Force the execution node to return the `syncing` status. + network.execution_nodes.read()[node_index] + .server + .all_payloads_syncing(false); + Ok(()) +} + +// Causes the beacon node at `node_index` to reconnect from the execution layer. +pub async fn reconnect_to_execution_layer( + network: LocalNetwork, + node_index: usize, +) -> Result<(), String> { + network.execution_nodes.read()[node_index] + .server + .all_payloads_valid(); + + eprintln!("Enabling Execution Node {node_index}"); + Ok(()) +} + +/// Ensure all validators have attested correctly. +pub async fn check_attestation_correctness( + network: LocalNetwork, + start_epoch: u64, + upto_epoch: u64, + slot_duration: Duration, + // Select which node to query. Will use this node to determine the global network performance. + node_index: usize, + acceptable_attestation_performance: f64, +) -> Result<(), String> { + epoch_delay(Epoch::new(upto_epoch), slot_duration, E::slots_per_epoch()).await; + + let remote_node = &network.remote_nodes()?[node_index]; + + let results = remote_node + .get_lighthouse_analysis_attestation_performance( + Epoch::new(start_epoch), + Epoch::new(upto_epoch - 2), + "global".to_string(), + ) + .await + .map_err(|e| format!("Unable to get attestation performance: {e}"))?; + + let mut active_successes: f64 = 0.0; + let mut head_successes: f64 = 0.0; + let mut target_successes: f64 = 0.0; + let mut source_successes: f64 = 0.0; + + let mut total: f64 = 0.0; + + for result in results { + for epochs in result.epochs.values() { + total += 1.0; + + if epochs.active { + active_successes += 1.0; + } + if epochs.head { + head_successes += 1.0; + } + if epochs.target { + target_successes += 1.0; + } + if epochs.source { + source_successes += 1.0; + } + } + } + let active_percent = active_successes / total * 100.0; + let head_percent = head_successes / total * 100.0; + let target_percent = target_successes / total * 100.0; + let source_percent = source_successes / total * 100.0; + + eprintln!("Total Attestations: {}", total); + eprintln!("Active: {}: {}%", active_successes, active_percent); + eprintln!("Head: {}: {}%", head_successes, head_percent); + eprintln!("Target: {}: {}%", target_successes, target_percent); + eprintln!("Source: {}: {}%", source_successes, source_percent); + + if active_percent < acceptable_attestation_performance { + return Err("Active percent was below required level".to_string()); + } + if head_percent < acceptable_attestation_performance { + return Err("Head percent was below required level".to_string()); + } + if target_percent < acceptable_attestation_performance { + return Err("Target percent was below required level".to_string()); + } + if source_percent < acceptable_attestation_performance { + return Err("Source percent was below required level".to_string()); + } + + Ok(()) +} diff --git a/testing/simulator/src/cli.rs b/testing/simulator/src/cli.rs index ff80201051f..00af7e560ce 100644 --- a/testing/simulator/src/cli.rs +++ b/testing/simulator/src/cli.rs @@ -6,120 +6,121 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .author("Sigma Prime ") .about("Options for interacting with simulator") .subcommand( - SubCommand::with_name("eth1-sim") - .about( - "Lighthouse Beacon Chain Simulator creates `n` beacon node and validator clients, \ - each with `v` validators. A deposit contract is deployed at the start of the \ - simulation using a local `anvil` instance (you must have `anvil` \ - installed and avaliable on your path). All beacon nodes independently listen \ - for genesis from the deposit contract, then start operating. \ - \ + SubCommand::with_name("basic-sim") + .about( + "Runs a Beacon Chain simulation with `n` beacon node and validator clients, \ + each with `v` validators. \ + The simulation runs with a post-Merge Genesis using `mock-el`. \ As the simulation runs, there are checks made to ensure that all components \ are running correctly. If any of these checks fail, the simulation will \ exit immediately.", - ) - .arg(Arg::with_name("nodes") + ) + .arg( + Arg::with_name("nodes") .short("n") .long("nodes") .takes_value(true) - .default_value("4") - .help("Number of beacon nodes")) - .arg(Arg::with_name("proposer-nodes") + .default_value("3") + .help("Number of beacon nodes"), + ) + .arg( + Arg::with_name("proposer-nodes") .short("p") - .long("proposer_nodes") + .long("proposer-nodes") .takes_value(true) - .default_value("2") - .help("Number of proposer-only beacon nodes")) - .arg(Arg::with_name("validators_per_node") + .default_value("3") + .help("Number of proposer-only beacon nodes"), + ) + .arg( + Arg::with_name("validators-per-node") .short("v") - .long("validators_per_node") + .long("validators-per-node") .takes_value(true) .default_value("20") - .help("Number of validators")) - .arg(Arg::with_name("speed_up_factor") + .help("Number of validators"), + ) + .arg( + Arg::with_name("speed-up-factor") .short("s") - .long("speed_up_factor") + .long("speed-up-factor") .takes_value(true) .default_value("3") - .help("Speed up factor. Please use a divisor of 12.")) - .arg(Arg::with_name("post-merge") - .short("m") - .long("post-merge") - .takes_value(false) - .help("Simulate the merge transition")) - .arg(Arg::with_name("continue_after_checks") + .help("Speed up factor. Please use a divisor of 12."), + ) + .arg( + Arg::with_name("debug-level") + .short("d") + .long("debug-level") + .takes_value(true) + .default_value("debug") + .help("Set the severity level of the logs."), + ) + .arg( + Arg::with_name("continue-after-checks") .short("c") .long("continue_after_checks") .takes_value(false) - .help("Continue after checks (default false)")) + .help("Continue after checks (default false)"), + ), ) .subcommand( - SubCommand::with_name("no-eth1-sim") - .about("Runs a simulator that bypasses the eth1 chain. Useful for faster testing of - components that don't rely upon eth1") - .arg(Arg::with_name("nodes") - .short("n") - .long("nodes") + SubCommand::with_name("fallback-sim") + .about( + "Runs a Beacon Chain simulation with `c` validator clients where each VC is \ + connected to `b` beacon nodes with `v` validators. \ + During the simulation, all but the last connected BN for each VC are \ + disconnected from the execution layer, which causes the VC to fallback to the \ + single remaining BN. \ + At the end of the simulation, there are checks made to ensure that all VCs \ + efficiently performed this fallback, within a certain tolerance. \ + Otherwise, the simulation will exit and an error will be reported.", + ) + .arg( + Arg::with_name("vc-count") + .short("c") + .long("vc-count") .takes_value(true) - .default_value("4") - .help("Number of beacon nodes")) - .arg(Arg::with_name("proposer-nodes") - .short("p") - .long("proposer_nodes") + .default_value("3") + .help("Number of validator clients."), + ) + .arg( + Arg::with_name("bns-per-vc") + .short("b") + .long("bns-per-vc") .takes_value(true) .default_value("2") - .help("Number of proposer-only beacon nodes")) - .arg(Arg::with_name("validators_per_node") + .help("Number of beacon nodes per validator client."), + ) + .arg( + Arg::with_name("validators-per-vc") .short("v") - .long("validators_per_node") + .long("validators-per-vc") .takes_value(true) .default_value("20") - .help("Number of validators")) - .arg(Arg::with_name("speed_up_factor") - .short("s") - .long("speed_up_factor") - .takes_value(true) - .default_value("3") - .help("Speed up factor")) - .arg(Arg::with_name("continue_after_checks") - .short("c") - .long("continue_after_checks") - .takes_value(false) - .help("Continue after checks (default false)")) - ) - .subcommand( - SubCommand::with_name("syncing-sim") - .about("Run the syncing simulation") - .arg( - Arg::with_name("speedup") - .short("s") - .long("speedup") - .takes_value(true) - .default_value("15") - .help("Speed up factor for eth1 blocks and slot production"), + .help("Number of validators per client."), ) .arg( - Arg::with_name("initial_delay") - .short("i") - .long("initial_delay") + Arg::with_name("speed-up-factor") + .short("s") + .long("speed-up-factor") .takes_value(true) - .default_value("5") - .help("Epoch delay for new beacon node to start syncing"), + .default_value("3") + .help("Speed up factor. Please use a divisor of 12."), ) .arg( - Arg::with_name("sync_timeout") - .long("sync_timeout") + Arg::with_name("debug-level") + .short("d") + .long("debug-level") .takes_value(true) - .default_value("10") - .help("Number of epochs after which newly added beacon nodes must be synced"), + .default_value("debug") + .help("Set the severity level of the logs."), ) .arg( - Arg::with_name("strategy") - .long("strategy") - .takes_value(true) - .default_value("all") - .possible_values(&["one-node", "two-nodes", "mixed", "all"]) - .help("Sync verification strategy to run."), + Arg::with_name("continue-after-checks") + .short("c") + .long("continue_after_checks") + .takes_value(false) + .help("Continue after checks (default false)"), ), ) } diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs new file mode 100644 index 00000000000..c9deeba04d9 --- /dev/null +++ b/testing/simulator/src/fallback_sim.rs @@ -0,0 +1,261 @@ +use crate::local_network::LocalNetworkParams; +use crate::{checks, LocalNetwork}; +use clap::ArgMatches; + +use crate::retry::with_retry; +use futures::prelude::*; +use node_test_rig::{ + environment::{EnvironmentBuilder, LoggerConfig}, + testing_validator_config, ValidatorFiles, +}; +use rayon::prelude::*; +use std::cmp::max; +use std::time::Duration; +use tokio::time::sleep; +use types::{Epoch, EthSpec, MinimalEthSpec}; + +const END_EPOCH: u64 = 16; +const GENESIS_DELAY: u64 = 32; +const ALTAIR_FORK_EPOCH: u64 = 0; +const BELLATRIX_FORK_EPOCH: u64 = 0; +const CAPELLA_FORK_EPOCH: u64 = 1; +const DENEB_FORK_EPOCH: u64 = 2; +//const ELECTRA_FORK_EPOCH: u64 = 3; + +// Since simulator tests are non-deterministic and there is a non-zero chance of missed +// attestations, define an acceptable network-wide attestation performance. +// +// This has potential to block CI so it should be set conservatively enough that spurious failures +// don't become very common, but not so conservatively that regressions to the fallback mechanism +// cannot be detected. +const ACCEPTABLE_FALLBACK_ATTESTATION_HIT_PERCENTAGE: f64 = 85.0; + +const SUGGESTED_FEE_RECIPIENT: [u8; 20] = + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; + +pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { + let vc_count = value_t!(matches, "vc-count", usize).expect("Missing validator-count default"); + let validators_per_vc = + value_t!(matches, "validators-per-vc", usize).expect("Missing validators-per-vc default"); + let bns_per_vc = value_t!(matches, "bns-per-vc", usize).expect("Missing bns-per-vc default"); + assert!(bns_per_vc > 1); + let speed_up_factor = + value_t!(matches, "speed-up-factor", u64).expect("Missing speed-up-factor default"); + let log_level = value_t!(matches, "debug-level", String).expect("Missing default log-level"); + let continue_after_checks = matches.is_present("continue-after-checks"); + + println!("Fallback Simulator:"); + println!(" vc-count: {}", vc_count); + println!(" validators-per-vc: {}", validators_per_vc); + println!(" bns-per-vc: {}", bns_per_vc); + println!(" speed-up-factor: {}", speed_up_factor); + println!(" continue-after-checks: {}", continue_after_checks); + + // Generate the directories and keystores required for the validator clients. + let validator_files = (0..vc_count) + .into_par_iter() + .map(|i| { + println!( + "Generating keystores for validator {} of {}", + i + 1, + vc_count + ); + + let indices = (i * validators_per_vc..(i + 1) * validators_per_vc).collect::>(); + ValidatorFiles::with_keystores(&indices).unwrap() + }) + .collect::>(); + + let mut env = EnvironmentBuilder::minimal() + .initialize_logger(LoggerConfig { + path: None, + debug_level: log_level.clone(), + logfile_debug_level: log_level, + log_format: None, + logfile_format: None, + log_color: false, + disable_log_timestamp: false, + max_log_size: 0, + max_log_number: 0, + compression: false, + is_restricted: true, + sse_logging: false, + })? + .multi_threaded_tokio_runtime()? + .build()?; + + let spec = &mut env.eth2_config.spec; + + let total_validator_count = validators_per_vc * vc_count; + let node_count = vc_count * bns_per_vc; + + let genesis_delay = GENESIS_DELAY; + + spec.seconds_per_slot /= speed_up_factor; + spec.seconds_per_slot = max(1, spec.seconds_per_slot); + spec.genesis_delay = genesis_delay; + spec.min_genesis_time = 0; + spec.min_genesis_active_validator_count = total_validator_count as u64; + spec.altair_fork_epoch = Some(Epoch::new(ALTAIR_FORK_EPOCH)); + spec.bellatrix_fork_epoch = Some(Epoch::new(BELLATRIX_FORK_EPOCH)); + spec.capella_fork_epoch = Some(Epoch::new(CAPELLA_FORK_EPOCH)); + spec.deneb_fork_epoch = Some(Epoch::new(DENEB_FORK_EPOCH)); + //spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); + + let slot_duration = Duration::from_secs(spec.seconds_per_slot); + let slots_per_epoch = MinimalEthSpec::slots_per_epoch(); + + let disconnection_epoch = 1; + let epochs_disconnected = 14; + + let context = env.core_context(); + + let main_future = async { + /* + * Create a new `LocalNetwork` with one beacon node. + */ + let max_retries = 3; + let (network, beacon_config, mock_execution_config) = with_retry(max_retries, || { + Box::pin(LocalNetwork::create_local_network( + None, + None, + LocalNetworkParams { + validator_count: total_validator_count, + node_count, + proposer_nodes: 0, + genesis_delay, + }, + context.clone(), + )) + }) + .await?; + + // Add nodes to the network. + for _ in 0..node_count { + network + .add_beacon_node(beacon_config.clone(), mock_execution_config.clone(), false) + .await?; + } + + /* + * One by one, add validators to the network. + */ + let executor = context.executor.clone(); + for (i, files) in validator_files.into_iter().enumerate() { + let network_1 = network.clone(); + + let mut beacon_nodes = Vec::with_capacity(vc_count * bns_per_vc); + // Each VC gets a unique set of BNs which are not shared with any other VC. + for j in 0..bns_per_vc { + beacon_nodes.push(bns_per_vc * i + j) + } + + executor.spawn( + async move { + let mut validator_config = testing_validator_config(); + validator_config.fee_recipient = Some(SUGGESTED_FEE_RECIPIENT.into()); + println!("Adding validator client {}", i); + network_1 + .add_validator_client_with_fallbacks( + validator_config, + i, + beacon_nodes, + files, + ) + .await + .expect("should add validator"); + }, + "vc", + ); + } + + let duration_to_genesis = network.duration_to_genesis().await; + println!("Duration to genesis: {}", duration_to_genesis.as_secs()); + sleep(duration_to_genesis).await; + + let test_sequence = async { + checks::epoch_delay( + Epoch::new(disconnection_epoch), + slot_duration, + slots_per_epoch, + ) + .await; + // Iterate through each VC and disconnect all BNs but the last node for each VC. + for i in 0..vc_count { + for j in 0..(bns_per_vc - 1) { + let node_index = bns_per_vc * i + j; + checks::disconnect_from_execution_layer(network.clone(), node_index).await?; + } + } + checks::epoch_delay( + Epoch::new(epochs_disconnected), + slot_duration, + slots_per_epoch, + ) + .await; + // Enable all BNs. + for i in 0..node_count { + checks::reconnect_to_execution_layer(network.clone(), i).await?; + } + Ok::<(), String>(()) + }; + + /* + * Start the checks that ensure the network performs as expected. + * + * We start these checks immediately after the validators have started. This means we're + * relying on the validator futures to all return immediately after genesis so that these + * tests start at the right time. Whilst this is works well for now, it's subject to + * breakage by changes to the VC. + */ + + let (sequence, check_attestations, block_production) = futures::join!( + test_sequence, + checks::check_attestation_correctness( + network.clone(), + 0, + END_EPOCH, + slot_duration, + // Use the last node index as this will never have been disabled. + node_count - 1, + ACCEPTABLE_FALLBACK_ATTESTATION_HIT_PERCENTAGE, + ), + checks::verify_full_block_production_up_to( + network.clone(), + Epoch::new(END_EPOCH).start_slot(slots_per_epoch), + slot_duration, + ), + ); + sequence?; + block_production?; + check_attestations?; + + // The `final_future` either completes immediately or never completes, depending on the value + // of `continue_after_checks`. + + if continue_after_checks { + future::pending::<()>().await; + } + /* + * End the simulation by dropping the network. This will kill all running beacon nodes and + * validator clients. + */ + println!( + "Simulation complete. Finished with {} beacon nodes and {} validator clients", + network.beacon_node_count(), + network.validator_client_count() + ); + + // Be explicit about dropping the network, as this kills all the nodes. This ensures + // all the checks have adequate time to pass. + drop(network); + Ok::<(), String>(()) + }; + + env.runtime().block_on(main_future).unwrap(); + + env.fire_signal(); + env.shutdown_on_idle(); + + Ok(()) +} diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index 018954a5d3b..63f2ec93537 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -1,26 +1,95 @@ +use crate::checks::epoch_delay; +use eth2_network_config::TRUSTED_SETUP_BYTES; use node_test_rig::{ environment::RuntimeContext, eth2::{types::StateId, BeaconNodeHttpClient}, - ClientConfig, LocalBeaconNode, LocalExecutionNode, LocalValidatorClient, MockExecutionConfig, - MockServerConfig, ValidatorConfig, ValidatorFiles, + testing_client_config, ClientConfig, ClientGenesis, LocalBeaconNode, LocalExecutionNode, + LocalValidatorClient, MockExecutionConfig, MockServerConfig, ValidatorConfig, ValidatorFiles, }; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; use std::{ + net::Ipv4Addr, ops::Deref, - time::{SystemTime, UNIX_EPOCH}, + sync::Arc, + time::{Duration, SystemTime, UNIX_EPOCH}, }; -use std::{sync::Arc, time::Duration}; -use types::{Epoch, EthSpec}; +use types::{ChainSpec, Epoch, EthSpec}; const BOOTNODE_PORT: u16 = 42424; const QUIC_PORT: u16 = 43424; -pub const INVALID_ADDRESS: &str = "http://127.0.0.1:42423"; pub const EXECUTION_PORT: u16 = 4000; -pub const TERMINAL_DIFFICULTY: u64 = 6400; -pub const TERMINAL_BLOCK: u64 = 64; +pub const TERMINAL_BLOCK: u64 = 0; + +pub struct LocalNetworkParams { + pub validator_count: usize, + pub node_count: usize, + pub proposer_nodes: usize, + pub genesis_delay: u64, +} + +fn default_client_config(network_params: LocalNetworkParams, genesis_time: u64) -> ClientConfig { + let mut beacon_config = testing_client_config(); + + beacon_config.genesis = ClientGenesis::InteropMerge { + validator_count: network_params.validator_count, + genesis_time, + }; + beacon_config.network.target_peers = + network_params.node_count + network_params.proposer_nodes - 1; + beacon_config.network.enr_address = (Some(Ipv4Addr::LOCALHOST), None); + beacon_config.network.enable_light_client_server = true; + beacon_config.network.discv5_config.enable_packet_filter = false; + beacon_config.chain.enable_light_client_server = true; + beacon_config.http_api.enable_light_client_server = true; + beacon_config.chain.optimistic_finalized_sync = false; + beacon_config.trusted_setup = + serde_json::from_reader(TRUSTED_SETUP_BYTES).expect("Trusted setup bytes should be valid"); + + let el_config = execution_layer::Config { + execution_endpoint: Some( + SensitiveUrl::parse(&format!("http://localhost:{}", EXECUTION_PORT)).unwrap(), + ), + ..Default::default() + }; + beacon_config.execution_layer = Some(el_config); + beacon_config +} + +fn default_mock_execution_config( + spec: &ChainSpec, + genesis_time: u64, +) -> MockExecutionConfig { + let mut mock_execution_config = MockExecutionConfig { + server_config: MockServerConfig { + listen_port: EXECUTION_PORT, + ..Default::default() + }, + ..Default::default() + }; + + if let Some(capella_fork_epoch) = spec.capella_fork_epoch { + mock_execution_config.shanghai_time = Some( + genesis_time + + spec.seconds_per_slot * E::slots_per_epoch() * capella_fork_epoch.as_u64(), + ) + } + if let Some(deneb_fork_epoch) = spec.deneb_fork_epoch { + mock_execution_config.cancun_time = Some( + genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * deneb_fork_epoch.as_u64(), + ) + } + if let Some(electra_fork_epoch) = spec.electra_fork_epoch { + mock_execution_config.prague_time = Some( + genesis_time + + spec.seconds_per_slot * E::slots_per_epoch() * electra_fork_epoch.as_u64(), + ) + } + + mock_execution_config +} /// Helper struct to reduce `Arc` usage. pub struct Inner { @@ -55,56 +124,41 @@ impl Deref for LocalNetwork { } impl LocalNetwork { - /// Creates a new network with a single `BeaconNode` and a connected `ExecutionNode`. - pub async fn new( + pub async fn create_local_network( + client_config: Option, + mock_execution_config: Option, + network_params: LocalNetworkParams, context: RuntimeContext, - mut beacon_config: ClientConfig, - ) -> Result { - beacon_config.network.set_ipv4_listening_address( - std::net::Ipv4Addr::UNSPECIFIED, - BOOTNODE_PORT, - BOOTNODE_PORT, - QUIC_PORT, - ); - beacon_config.network.enr_udp4_port = Some(BOOTNODE_PORT.try_into().expect("non zero")); - beacon_config.network.enr_tcp4_port = Some(BOOTNODE_PORT.try_into().expect("non zero")); - beacon_config.network.discv5_config.table_filter = |_| true; + ) -> Result<(LocalNetwork, ClientConfig, MockExecutionConfig), String> { + let genesis_time: u64 = (SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| "should get system time")? + + Duration::from_secs(network_params.genesis_delay)) + .as_secs(); - let execution_node = if let Some(el_config) = &mut beacon_config.execution_layer { - let mock_execution_config = MockExecutionConfig { - server_config: MockServerConfig { - listen_port: EXECUTION_PORT, - ..Default::default() - }, - terminal_block: TERMINAL_BLOCK, - terminal_difficulty: TERMINAL_DIFFICULTY.into(), - ..Default::default() - }; - let execution_node = LocalExecutionNode::new( - context.service_context("boot_node_el".into()), - mock_execution_config, - ); - el_config.default_datadir = execution_node.datadir.path().to_path_buf(); - el_config.secret_file = Some(execution_node.datadir.path().join("jwt.hex")); - el_config.execution_endpoint = - Some(SensitiveUrl::parse(&execution_node.server.url()).unwrap()); - vec![execution_node] + let beacon_config = if let Some(config) = client_config { + config } else { - vec![] + default_client_config(network_params, genesis_time) }; - let beacon_node = - LocalBeaconNode::production(context.service_context("boot_node".into()), beacon_config) - .await?; - Ok(Self { + let execution_config = if let Some(config) = mock_execution_config { + config + } else { + default_mock_execution_config::(&context.eth2_config().spec, genesis_time) + }; + + let network = Self { inner: Arc::new(Inner { context, - beacon_nodes: RwLock::new(vec![beacon_node]), + beacon_nodes: RwLock::new(vec![]), proposer_nodes: RwLock::new(vec![]), - execution_nodes: RwLock::new(execution_node), + execution_nodes: RwLock::new(vec![]), validator_clients: RwLock::new(vec![]), }), - }) + }; + + Ok((network, beacon_config, execution_config)) } /// Returns the number of beacon nodes in the network. @@ -131,77 +185,151 @@ impl LocalNetwork { self.validator_clients.read().len() } - /// Adds a beacon node to the network, connecting to the 0'th beacon node via ENR. - pub async fn add_beacon_node( + async fn construct_boot_node( + &self, + mut beacon_config: ClientConfig, + mock_execution_config: MockExecutionConfig, + ) -> Result<(LocalBeaconNode, LocalExecutionNode), String> { + beacon_config.network.set_ipv4_listening_address( + std::net::Ipv4Addr::UNSPECIFIED, + BOOTNODE_PORT, + BOOTNODE_PORT, + QUIC_PORT, + ); + + beacon_config.network.enr_udp4_port = Some(BOOTNODE_PORT.try_into().expect("non zero")); + beacon_config.network.enr_tcp4_port = Some(BOOTNODE_PORT.try_into().expect("non zero")); + beacon_config.network.discv5_config.table_filter = |_| true; + + let execution_node = LocalExecutionNode::new( + self.context.service_context("boot_node_el".into()), + mock_execution_config, + ); + + beacon_config.execution_layer = Some(execution_layer::Config { + execution_endpoint: Some(SensitiveUrl::parse(&execution_node.server.url()).unwrap()), + default_datadir: execution_node.datadir.path().to_path_buf(), + secret_file: Some(execution_node.datadir.path().join("jwt.hex")), + ..Default::default() + }); + + let beacon_node = LocalBeaconNode::production( + self.context.service_context("boot_node".into()), + beacon_config, + ) + .await?; + + Ok((beacon_node, execution_node)) + } + + async fn construct_beacon_node( &self, mut beacon_config: ClientConfig, + mut mock_execution_config: MockExecutionConfig, is_proposer: bool, - ) -> Result<(), String> { - let self_1 = self.clone(); - let count = self.beacon_node_count() as u16; - println!("Adding beacon node.."); - { - let read_lock = self.beacon_nodes.read(); + ) -> Result<(LocalBeaconNode, LocalExecutionNode), String> { + let count = (self.beacon_node_count() + self.proposer_node_count()) as u16; - let boot_node = read_lock.first().expect("should have at least one node"); + // Set config. + let libp2p_tcp_port = BOOTNODE_PORT + count; + let discv5_port = BOOTNODE_PORT + count; + beacon_config.network.set_ipv4_listening_address( + std::net::Ipv4Addr::UNSPECIFIED, + libp2p_tcp_port, + discv5_port, + QUIC_PORT + count, + ); + beacon_config.network.enr_udp4_port = Some(discv5_port.try_into().unwrap()); + beacon_config.network.enr_tcp4_port = Some(libp2p_tcp_port.try_into().unwrap()); + beacon_config.network.discv5_config.table_filter = |_| true; + beacon_config.network.proposer_only = is_proposer; - beacon_config.network.boot_nodes_enr.push( - boot_node - .client - .enr() - .expect("bootnode must have a network"), - ); - let count = (self.beacon_node_count() + self.proposer_node_count()) as u16; - let libp2p_tcp_port = BOOTNODE_PORT + count; - let discv5_port = BOOTNODE_PORT + count; - beacon_config.network.set_ipv4_listening_address( - std::net::Ipv4Addr::UNSPECIFIED, - libp2p_tcp_port, - discv5_port, - QUIC_PORT + count, - ); - beacon_config.network.enr_udp4_port = Some(discv5_port.try_into().unwrap()); - beacon_config.network.enr_tcp4_port = Some(libp2p_tcp_port.try_into().unwrap()); - beacon_config.network.discv5_config.table_filter = |_| true; - beacon_config.network.proposer_only = is_proposer; - } - if let Some(el_config) = &mut beacon_config.execution_layer { - let config = MockExecutionConfig { - server_config: MockServerConfig { - listen_port: EXECUTION_PORT + count, - ..Default::default() - }, - terminal_block: TERMINAL_BLOCK, - terminal_difficulty: TERMINAL_DIFFICULTY.into(), - ..Default::default() - }; - let execution_node = LocalExecutionNode::new( - self.context.service_context(format!("node_{}_el", count)), - config, - ); - el_config.default_datadir = execution_node.datadir.path().to_path_buf(); - el_config.secret_file = Some(execution_node.datadir.path().join("jwt.hex")); - el_config.execution_endpoint = - Some(SensitiveUrl::parse(&execution_node.server.url()).unwrap()); - self.execution_nodes.write().push(execution_node); - } + mock_execution_config.server_config.listen_port = EXECUTION_PORT + count; - // We create the beacon node without holding the lock, so that the lock isn't held - // across the await. This is only correct if this function never runs in parallel - // with itself (which at the time of writing, it does not). + // Construct execution node. + let execution_node = LocalExecutionNode::new( + self.context.service_context(format!("node_{}_el", count)), + mock_execution_config, + ); + + // Pair the beacon node and execution node. + beacon_config.execution_layer = Some(execution_layer::Config { + execution_endpoint: Some(SensitiveUrl::parse(&execution_node.server.url()).unwrap()), + default_datadir: execution_node.datadir.path().to_path_buf(), + secret_file: Some(execution_node.datadir.path().join("jwt.hex")), + ..Default::default() + }); + + // Construct beacon node using the config, let beacon_node = LocalBeaconNode::production( self.context.service_context(format!("node_{}", count)), beacon_config, ) .await?; + + Ok((beacon_node, execution_node)) + } + + /// Adds a beacon node to the network, connecting to the 0'th beacon node via ENR. + pub async fn add_beacon_node( + &self, + mut beacon_config: ClientConfig, + mock_execution_config: MockExecutionConfig, + is_proposer: bool, + ) -> Result<(), String> { + let first_bn_exists: bool; + { + let read_lock = self.beacon_nodes.read(); + let boot_node = read_lock.first(); + first_bn_exists = boot_node.is_some(); + + if let Some(boot_node) = boot_node { + // Modify beacon_config to add boot node details. + beacon_config.network.boot_nodes_enr.push( + boot_node + .client + .enr() + .expect("Bootnode must have a network."), + ); + } + } + let (beacon_node, execution_node) = if first_bn_exists { + // Network already exists. We construct a new node. + self.construct_beacon_node(beacon_config, mock_execution_config, is_proposer) + .await? + } else { + // Network does not exist. We construct a boot node. + self.construct_boot_node(beacon_config, mock_execution_config) + .await? + }; + // Add nodes to the network. + self.execution_nodes.write().push(execution_node); if is_proposer { - self_1.proposer_nodes.write().push(beacon_node); + self.proposer_nodes.write().push(beacon_node); } else { - self_1.beacon_nodes.write().push(beacon_node); + self.beacon_nodes.write().push(beacon_node); } Ok(()) } + // Add a new node with a delay. This node will not have validators and is only used to test + // sync. + pub async fn add_beacon_node_with_delay( + &self, + beacon_config: ClientConfig, + mock_execution_config: MockExecutionConfig, + wait_until_epoch: u64, + slot_duration: Duration, + slots_per_epoch: u64, + ) -> Result<(), String> { + epoch_delay(Epoch::new(wait_until_epoch), slot_duration, slots_per_epoch).await; + + self.add_beacon_node(beacon_config, mock_execution_config, false) + .await?; + + Ok(()) + } + /// Adds a validator client to the network, connecting it to the beacon node with index /// `beacon_node`. pub async fn add_validator_client( @@ -209,7 +337,6 @@ impl LocalNetwork { mut validator_config: ValidatorConfig, beacon_node: usize, validator_files: ValidatorFiles, - invalid_first_beacon_node: bool, //to test beacon node fallbacks ) -> Result<(), String> { let context = self .context @@ -240,11 +367,7 @@ impl LocalNetwork { format!("http://{}:{}", socket_addr.ip(), socket_addr.port()).as_str(), ) .unwrap(); - validator_config.beacon_nodes = if invalid_first_beacon_node { - vec![SensitiveUrl::parse(INVALID_ADDRESS).unwrap(), beacon_node] - } else { - vec![beacon_node] - }; + validator_config.beacon_nodes = vec![beacon_node]; // If we have a proposer node established, use it. if let Some(proposer_socket_addr) = proposer_socket_addr { @@ -293,11 +416,11 @@ impl LocalNetwork { .http_api_listen_addr() .expect("Must have http started") }; - let beacon_node = SensitiveUrl::parse( + let beacon_node_url = SensitiveUrl::parse( format!("http://{}:{}", socket_addr.ip(), socket_addr.port()).as_str(), ) .unwrap(); - beacon_node_urls.push(beacon_node); + beacon_node_urls.push(beacon_node_url); } validator_config.beacon_nodes = beacon_node_urls; @@ -325,7 +448,7 @@ impl LocalNetwork { } /// Return current epoch of bootnode. - pub async fn bootnode_epoch(&self) -> Result { + pub async fn _bootnode_epoch(&self) -> Result { let nodes = self.remote_nodes().expect("Failed to get remote nodes"); let bootnode = nodes.first().expect("Should contain bootnode"); bootnode @@ -335,16 +458,6 @@ impl LocalNetwork { .map(|body| body.unwrap().data.finalized.epoch) } - pub fn mine_pow_blocks(&self, block_number: u64) -> Result<(), String> { - let execution_nodes = self.execution_nodes.read(); - for execution_node in execution_nodes.iter() { - let mut block_gen = execution_node.server.ctx.execution_block_generator.write(); - block_gen.insert_pow_block(block_number)?; - println!("Mined pow block {}", block_number); - } - Ok(()) - } - pub async fn duration_to_genesis(&self) -> Duration { let nodes = self.remote_nodes().expect("Failed to get remote nodes"); let bootnode = nodes.first().expect("Should contain bootnode"); diff --git a/testing/simulator/src/main.rs b/testing/simulator/src/main.rs index e8af9c18067..d1a2d0dc672 100644 --- a/testing/simulator/src/main.rs +++ b/testing/simulator/src/main.rs @@ -1,10 +1,8 @@ -//! This crate provides a simluation that creates `n` beacon node and validator clients, each with -//! `v` validators. A deposit contract is deployed at the start of the simulation using a local -//! `anvil` instance (you must have `anvil` installed and avaliable on your path). All -//! beacon nodes independently listen for genesis from the deposit contract, then start operating. +//! This crate provides various simulations that create both beacon nodes and validator clients, +//! each with `v` validators. //! -//! As the simulation runs, there are checks made to ensure that all components are running -//! correctly. If any of these checks fail, the simulation will exit immediately. +//! When a simulation runs, there are checks made to ensure that all components are operating +//! as expected. If any of these checks fail, the simulation will exit immediately. //! //! ## Future works //! @@ -16,13 +14,12 @@ #[macro_use] extern crate clap; +mod basic_sim; mod checks; mod cli; -mod eth1_sim; +mod fallback_sim; mod local_network; -mod no_eth1_sim; mod retry; -mod sync_sim; use cli::cli_app; use env_logger::{Builder, Env}; @@ -37,21 +34,14 @@ fn main() { let matches = cli_app().get_matches(); match matches.subcommand() { - ("eth1-sim", Some(matches)) => match eth1_sim::run_eth1_sim(matches) { + ("basic-sim", Some(matches)) => match basic_sim::run_basic_sim(matches) { Ok(()) => println!("Simulation exited successfully"), Err(e) => { eprintln!("Simulation exited with error: {}", e); std::process::exit(1) } }, - ("no-eth1-sim", Some(matches)) => match no_eth1_sim::run_no_eth1_sim(matches) { - Ok(()) => println!("Simulation exited successfully"), - Err(e) => { - eprintln!("Simulation exited with error: {}", e); - std::process::exit(1) - } - }, - ("syncing-sim", Some(matches)) => match sync_sim::run_syncing_sim(matches) { + ("fallback-sim", Some(matches)) => match fallback_sim::run_fallback_sim(matches) { Ok(()) => println!("Simulation exited successfully"), Err(e) => { eprintln!("Simulation exited with error: {}", e); diff --git a/testing/simulator/src/no_eth1_sim.rs b/testing/simulator/src/no_eth1_sim.rs deleted file mode 100644 index fc18b1cd489..00000000000 --- a/testing/simulator/src/no_eth1_sim.rs +++ /dev/null @@ -1,172 +0,0 @@ -use crate::{checks, LocalNetwork}; -use clap::ArgMatches; -use futures::prelude::*; -use node_test_rig::{ - environment::{EnvironmentBuilder, LoggerConfig}, - testing_client_config, testing_validator_config, ClientGenesis, ValidatorFiles, -}; -use rayon::prelude::*; -use std::cmp::max; -use std::net::Ipv4Addr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tokio::time::sleep; -use types::{Epoch, EthSpec, MainnetEthSpec}; - -pub fn run_no_eth1_sim(matches: &ArgMatches) -> Result<(), String> { - let node_count = value_t!(matches, "nodes", usize).expect("missing nodes default"); - let validators_per_node = value_t!(matches, "validators_per_node", usize) - .expect("missing validators_per_node default"); - let speed_up_factor = - value_t!(matches, "speed_up_factor", u64).expect("missing speed_up_factor default"); - let continue_after_checks = matches.is_present("continue_after_checks"); - - println!("Beacon Chain Simulator:"); - println!(" nodes:{}", node_count); - println!(" validators_per_node:{}", validators_per_node); - println!(" continue_after_checks:{}", continue_after_checks); - - // Generate the directories and keystores required for the validator clients. - let validator_files = (0..node_count) - .into_par_iter() - .map(|i| { - println!( - "Generating keystores for validator {} of {}", - i + 1, - node_count - ); - - let indices = - (i * validators_per_node..(i + 1) * validators_per_node).collect::>(); - ValidatorFiles::with_keystores(&indices).unwrap() - }) - .collect::>(); - - let mut env = EnvironmentBuilder::mainnet() - .initialize_logger(LoggerConfig { - path: None, - debug_level: String::from("debug"), - logfile_debug_level: String::from("debug"), - log_format: None, - logfile_format: None, - log_color: false, - disable_log_timestamp: false, - max_log_size: 0, - max_log_number: 0, - compression: false, - is_restricted: true, - sse_logging: false, - })? - .multi_threaded_tokio_runtime()? - .build()?; - - let eth1_block_time = Duration::from_millis(15_000 / speed_up_factor); - - let spec = &mut env.eth2_config.spec; - - let total_validator_count = validators_per_node * node_count; - - spec.seconds_per_slot /= speed_up_factor; - spec.seconds_per_slot = max(1, spec.seconds_per_slot); - spec.eth1_follow_distance = 16; - spec.genesis_delay = eth1_block_time.as_secs() * spec.eth1_follow_distance * 2; - spec.min_genesis_time = 0; - spec.min_genesis_active_validator_count = total_validator_count as u64; - spec.seconds_per_eth1_block = 1; - - let genesis_delay = Duration::from_secs(5); - let genesis_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| "should get system time")? - + genesis_delay; - - let slot_duration = Duration::from_secs(spec.seconds_per_slot); - - let context = env.core_context(); - - let mut beacon_config = testing_client_config(); - - beacon_config.genesis = ClientGenesis::Interop { - validator_count: total_validator_count, - genesis_time: genesis_time.as_secs(), - }; - beacon_config.dummy_eth1_backend = true; - beacon_config.sync_eth1_chain = true; - - beacon_config.network.enr_address = (Some(Ipv4Addr::LOCALHOST), None); - - let main_future = async { - let network = LocalNetwork::new(context.clone(), beacon_config.clone()).await?; - /* - * One by one, add beacon nodes to the network. - */ - - for _ in 0..node_count - 1 { - network - .add_beacon_node(beacon_config.clone(), false) - .await?; - } - - /* - * Create a future that will add validator clients to the network. Each validator client is - * attached to a single corresponding beacon node. Spawn each validator in a new task. - */ - let executor = context.executor.clone(); - for (i, files) in validator_files.into_iter().enumerate() { - let network_1 = network.clone(); - executor.spawn( - async move { - println!("Adding validator client {}", i); - network_1 - .add_validator_client(testing_validator_config(), i, files, i % 2 == 0) - .await - .expect("should add validator"); - }, - "vc", - ); - } - - let duration_to_genesis = network.duration_to_genesis().await; - println!("Duration to genesis: {}", duration_to_genesis.as_secs()); - sleep(duration_to_genesis).await; - - let (finalization, block_prod) = futures::join!( - // Check that the chain finalizes at the first given opportunity. - checks::verify_first_finalization(network.clone(), slot_duration), - // Check that a block is produced at every slot. - checks::verify_full_block_production_up_to( - network.clone(), - Epoch::new(4).start_slot(MainnetEthSpec::slots_per_epoch()), - slot_duration, - ), - ); - finalization?; - block_prod?; - - // The `final_future` either completes immediately or never completes, depending on the value - // of `continue_after_checks`. - - if continue_after_checks { - future::pending::<()>().await; - } - /* - * End the simulation by dropping the network. This will kill all running beacon nodes and - * validator clients. - */ - println!( - "Simulation complete. Finished with {} beacon nodes and {} validator clients", - network.beacon_node_count() + network.proposer_node_count(), - network.validator_client_count() - ); - - // Be explicit about dropping the network, as this kills all the nodes. This ensures - // all the checks have adequate time to pass. - drop(network); - Ok::<(), String>(()) - }; - - env.runtime().block_on(main_future).unwrap(); - - env.fire_signal(); - env.shutdown_on_idle(); - Ok(()) -} diff --git a/testing/simulator/src/sync_sim.rs b/testing/simulator/src/sync_sim.rs deleted file mode 100644 index ba4ea4530af..00000000000 --- a/testing/simulator/src/sync_sim.rs +++ /dev/null @@ -1,390 +0,0 @@ -use crate::checks::{epoch_delay, verify_all_finalized_at}; -use crate::local_network::LocalNetwork; -use clap::ArgMatches; -use futures::prelude::*; -use node_test_rig::{ - environment::{EnvironmentBuilder, LoggerConfig}, - testing_client_config, ClientGenesis, ValidatorFiles, -}; -use node_test_rig::{testing_validator_config, ClientConfig}; -use std::cmp::max; -use std::net::Ipv4Addr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use types::{Epoch, EthSpec}; - -pub fn run_syncing_sim(matches: &ArgMatches) -> Result<(), String> { - let initial_delay = value_t!(matches, "initial_delay", u64).unwrap(); - let sync_timeout = value_t!(matches, "sync_timeout", u64).unwrap(); - let speed_up_factor = value_t!(matches, "speedup", u64).unwrap(); - let strategy = value_t!(matches, "strategy", String).unwrap(); - - println!("Syncing Simulator:"); - println!(" initial_delay:{}", initial_delay); - println!(" sync timeout: {}", sync_timeout); - println!(" speed up factor:{}", speed_up_factor); - println!(" strategy:{}", strategy); - - let log_level = "debug"; - let log_format = None; - - syncing_sim( - speed_up_factor, - initial_delay, - sync_timeout, - strategy, - log_level, - log_format, - ) -} - -fn syncing_sim( - speed_up_factor: u64, - initial_delay: u64, - sync_timeout: u64, - strategy: String, - log_level: &str, - log_format: Option<&str>, -) -> Result<(), String> { - let mut env = EnvironmentBuilder::minimal() - .initialize_logger(LoggerConfig { - path: None, - debug_level: String::from(log_level), - logfile_debug_level: String::from("debug"), - log_format: log_format.map(String::from), - logfile_format: None, - log_color: false, - disable_log_timestamp: false, - max_log_size: 0, - max_log_number: 0, - compression: false, - is_restricted: true, - sse_logging: false, - })? - .multi_threaded_tokio_runtime()? - .build()?; - - let spec = &mut env.eth2_config.spec; - let end_after_checks = true; - let eth1_block_time = Duration::from_millis(15_000 / speed_up_factor); - - // Set fork epochs to test syncing across fork boundaries - spec.altair_fork_epoch = Some(Epoch::new(1)); - spec.bellatrix_fork_epoch = Some(Epoch::new(2)); - spec.seconds_per_slot /= speed_up_factor; - spec.seconds_per_slot = max(1, spec.seconds_per_slot); - spec.eth1_follow_distance = 16; - spec.genesis_delay = eth1_block_time.as_secs() * spec.eth1_follow_distance * 2; - spec.min_genesis_time = 0; - spec.min_genesis_active_validator_count = 64; - spec.seconds_per_eth1_block = 1; - - let num_validators = 8; - let slot_duration = Duration::from_secs(spec.seconds_per_slot); - let context = env.core_context(); - let mut beacon_config = testing_client_config(); - - let genesis_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| "should get system time")? - + Duration::from_secs(5); - beacon_config.genesis = ClientGenesis::Interop { - validator_count: num_validators, - genesis_time: genesis_time.as_secs(), - }; - beacon_config.dummy_eth1_backend = true; - beacon_config.sync_eth1_chain = true; - - beacon_config.network.enr_address = (Some(Ipv4Addr::LOCALHOST), None); - - // Generate the directories and keystores required for the validator clients. - let validator_indices = (0..num_validators).collect::>(); - let validator_files = ValidatorFiles::with_keystores(&validator_indices).unwrap(); - - let main_future = async { - /* - * Create a new `LocalNetwork` with one beacon node. - */ - let network = LocalNetwork::new(context, beacon_config.clone()).await?; - - /* - * Add a validator client which handles all validators from the genesis state. - */ - network - .add_validator_client(testing_validator_config(), 0, validator_files, true) - .await?; - - // Check all syncing strategies one after other. - pick_strategy( - &strategy, - network.clone(), - beacon_config.clone(), - slot_duration, - initial_delay, - sync_timeout, - ) - .await?; - - // The `final_future` either completes immediately or never completes, depending on the value - // of `end_after_checks`. - - if !end_after_checks { - future::pending::<()>().await; - } - - /* - * End the simulation by dropping the network. This will kill all running beacon nodes and - * validator clients. - */ - println!( - "Simulation complete. Finished with {} beacon nodes and {} validator clients", - network.beacon_node_count(), - network.validator_client_count() - ); - - // Be explicit about dropping the network, as this kills all the nodes. This ensures - // all the checks have adequate time to pass. - drop(network); - Ok::<(), String>(()) - }; - - env.runtime().block_on(main_future).unwrap(); - - env.fire_signal(); - env.shutdown_on_idle(); - - Ok(()) -} - -pub async fn pick_strategy( - strategy: &str, - network: LocalNetwork, - beacon_config: ClientConfig, - slot_duration: Duration, - initial_delay: u64, - sync_timeout: u64, -) -> Result<(), String> { - match strategy { - "one-node" => { - verify_one_node_sync( - network, - beacon_config, - slot_duration, - initial_delay, - sync_timeout, - ) - .await - } - "two-nodes" => { - verify_two_nodes_sync( - network, - beacon_config, - slot_duration, - initial_delay, - sync_timeout, - ) - .await - } - "mixed" => { - verify_in_between_sync( - network, - beacon_config, - slot_duration, - initial_delay, - sync_timeout, - ) - .await - } - "all" => { - verify_syncing( - network, - beacon_config, - slot_duration, - initial_delay, - sync_timeout, - ) - .await - } - _ => Err("Invalid strategy".into()), - } -} - -/// Verify one node added after `initial_delay` epochs is in sync -/// after `sync_timeout` epochs. -pub async fn verify_one_node_sync( - network: LocalNetwork, - beacon_config: ClientConfig, - slot_duration: Duration, - initial_delay: u64, - sync_timeout: u64, -) -> Result<(), String> { - let epoch_duration = slot_duration * (E::slots_per_epoch() as u32); - let network_c = network.clone(); - // Delay for `initial_delay` epochs before adding another node to start syncing - epoch_delay( - Epoch::new(initial_delay), - slot_duration, - E::slots_per_epoch(), - ) - .await; - // Add a beacon node - network.add_beacon_node(beacon_config, false).await?; - // Check every `epoch_duration` if nodes are synced - // limited to at most `sync_timeout` epochs - let mut interval = tokio::time::interval(epoch_duration); - let mut count = 0; - loop { - interval.tick().await; - if count >= sync_timeout || !check_still_syncing(&network_c).await? { - break; - } - count += 1; - } - let epoch = network.bootnode_epoch().await?; - verify_all_finalized_at(network, epoch) - .map_err(|e| format!("One node sync error: {}", e)) - .await -} - -/// Verify two nodes added after `initial_delay` epochs are in sync -/// after `sync_timeout` epochs. -pub async fn verify_two_nodes_sync( - network: LocalNetwork, - beacon_config: ClientConfig, - slot_duration: Duration, - initial_delay: u64, - sync_timeout: u64, -) -> Result<(), String> { - let epoch_duration = slot_duration * (E::slots_per_epoch() as u32); - let network_c = network.clone(); - // Delay for `initial_delay` epochs before adding another node to start syncing - epoch_delay( - Epoch::new(initial_delay), - slot_duration, - E::slots_per_epoch(), - ) - .await; - // Add beacon nodes - network - .add_beacon_node(beacon_config.clone(), false) - .await?; - network.add_beacon_node(beacon_config, false).await?; - // Check every `epoch_duration` if nodes are synced - // limited to at most `sync_timeout` epochs - let mut interval = tokio::time::interval(epoch_duration); - let mut count = 0; - loop { - interval.tick().await; - if count >= sync_timeout || !check_still_syncing(&network_c).await? { - break; - } - count += 1; - } - let epoch = network.bootnode_epoch().await?; - verify_all_finalized_at(network, epoch) - .map_err(|e| format!("One node sync error: {}", e)) - .await -} - -/// Add 2 syncing nodes after `initial_delay` epochs, -/// Add another node after `sync_timeout - 5` epochs and verify all are -/// in sync after `sync_timeout + 5` epochs. -pub async fn verify_in_between_sync( - network: LocalNetwork, - beacon_config: ClientConfig, - slot_duration: Duration, - initial_delay: u64, - sync_timeout: u64, -) -> Result<(), String> { - let epoch_duration = slot_duration * (E::slots_per_epoch() as u32); - let network_c = network.clone(); - // Delay for `initial_delay` epochs before adding another node to start syncing - let config1 = beacon_config.clone(); - epoch_delay( - Epoch::new(initial_delay), - slot_duration, - E::slots_per_epoch(), - ) - .await; - // Add two beacon nodes - network - .add_beacon_node(beacon_config.clone(), false) - .await?; - network.add_beacon_node(beacon_config, false).await?; - // Delay before adding additional syncing nodes. - epoch_delay( - Epoch::new(sync_timeout - 5), - slot_duration, - E::slots_per_epoch(), - ) - .await; - // Add a beacon node - network.add_beacon_node(config1.clone(), false).await?; - // Check every `epoch_duration` if nodes are synced - // limited to at most `sync_timeout` epochs - let mut interval = tokio::time::interval(epoch_duration); - let mut count = 0; - loop { - interval.tick().await; - if count >= sync_timeout || !check_still_syncing(&network_c).await? { - break; - } - count += 1; - } - let epoch = network.bootnode_epoch().await?; - verify_all_finalized_at(network, epoch) - .map_err(|e| format!("One node sync error: {}", e)) - .await -} - -/// Run syncing strategies one after other. -pub async fn verify_syncing( - network: LocalNetwork, - beacon_config: ClientConfig, - slot_duration: Duration, - initial_delay: u64, - sync_timeout: u64, -) -> Result<(), String> { - verify_one_node_sync( - network.clone(), - beacon_config.clone(), - slot_duration, - initial_delay, - sync_timeout, - ) - .await?; - println!("Completed one node sync"); - verify_two_nodes_sync( - network.clone(), - beacon_config.clone(), - slot_duration, - initial_delay, - sync_timeout, - ) - .await?; - println!("Completed two node sync"); - verify_in_between_sync( - network, - beacon_config, - slot_duration, - initial_delay, - sync_timeout, - ) - .await?; - println!("Completed in between sync"); - Ok(()) -} - -pub async fn check_still_syncing(network: &LocalNetwork) -> Result { - // get syncing status of nodes - let mut status = Vec::new(); - for remote_node in network.remote_nodes()? { - status.push( - remote_node - .get_node_syncing() - .await - .map(|body| body.data.is_syncing) - .map_err(|e| format!("Get syncing status via http failed: {:?}", e))?, - ) - } - Ok(status.iter().any(|is_syncing| *is_syncing)) -} diff --git a/validator_client/src/block_service.rs b/validator_client/src/block_service.rs index 445d4f1a5d9..06d484a52bb 100644 --- a/validator_client/src/block_service.rs +++ b/validator_client/src/block_service.rs @@ -892,6 +892,7 @@ impl UnsignedBlock { } } +#[derive(Debug)] pub enum SignedBlock { Full(PublishBlockRequest), Blinded(Arc>),