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
This commit is contained in:
Mac L
2024-04-23 01:08:36 +10:00
committed by GitHub
parent 9b5895ca89
commit 67f8405921
18 changed files with 944 additions and 1036 deletions

View File

@@ -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 }

View File

@@ -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<E: EthSpec>(
LocalNetworkParams {
eth1_block_time,
total_validator_count,
deposit_amount,
node_count,
proposer_nodes,
post_merge_sim,
}: LocalNetworkParams,
context: RuntimeContext<E>,
) -> Result<(LocalNetwork<E>, 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::<E>(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))
}

View File

@@ -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<E: EthSpec>(
}
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<E: EthSpec>(
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<E: EthSpec>(
network: LocalNetwork<E>,
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::<E>(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<E: EthSpec>(
network: LocalNetwork<E>,
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::<E>(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::<E>(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<E: EthSpec>(
network: LocalNetwork<E>,
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<E: EthSpec>(
network: LocalNetwork<E>,
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<E: EthSpec>(
network: LocalNetwork<E>,
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(())
}

View File

@@ -6,120 +6,121 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.author("Sigma Prime <contact@sigmaprime.io>")
.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")
.takes_value(true)
.default_value("2")
.help("Number of proposer-only beacon nodes"))
.arg(Arg::with_name("validators_per_node")
.short("v")
.long("validators_per_node")
.takes_value(true)
.default_value("20")
.help("Number of validators"))
.arg(Arg::with_name("speed_up_factor")
.short("s")
.long("speed_up_factor")
.long("proposer-nodes")
.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("Number of proposer-only beacon nodes"),
)
.arg(
Arg::with_name("validators-per-node")
.short("v")
.long("validators-per-node")
.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. 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")
.takes_value(true)
.default_value("4")
.help("Number of beacon nodes"))
.arg(Arg::with_name("proposer-nodes")
.short("p")
.long("proposer_nodes")
.takes_value(true)
.default_value("2")
.help("Number of proposer-only beacon nodes"))
.arg(Arg::with_name("validators_per_node")
.short("v")
.long("validators_per_node")
.takes_value(true)
.default_value("20")
.help("Number of validators"))
.arg(Arg::with_name("speed_up_factor")
.short("s")
.long("speed_up_factor")
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("3")
.help("Speed up factor"))
.arg(Arg::with_name("continue_after_checks")
.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 beacon nodes per validator client."),
)
.arg(
Arg::with_name("validators-per-vc")
.short("v")
.long("validators-per-vc")
.takes_value(true)
.default_value("20")
.help("Number of validators per client."),
)
.arg(
Arg::with_name("speed-up-factor")
.short("s")
.long("speed-up-factor")
.takes_value(true)
.default_value("3")
.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)"))
)
.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"),
)
.arg(
Arg::with_name("initial_delay")
.short("i")
.long("initial_delay")
.takes_value(true)
.default_value("5")
.help("Epoch delay for new beacon node to start syncing"),
)
.arg(
Arg::with_name("sync_timeout")
.long("sync_timeout")
.takes_value(true)
.default_value("10")
.help("Number of epochs after which newly added beacon nodes must be synced"),
)
.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."),
.help("Continue after checks (default false)"),
),
)
}

View File

@@ -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::<Vec<_>>();
ValidatorFiles::with_keystores(&indices).unwrap()
})
.collect::<Vec<_>>();
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(())
}

View File

@@ -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<E: EthSpec>(
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<E: EthSpec> {
@@ -55,56 +124,41 @@ impl<E: EthSpec> Deref for LocalNetwork<E> {
}
impl<E: EthSpec> LocalNetwork<E> {
/// 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<ClientConfig>,
mock_execution_config: Option<MockExecutionConfig>,
network_params: LocalNetworkParams,
context: RuntimeContext<E>,
mut beacon_config: ClientConfig,
) -> Result<Self, 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;
) -> Result<(LocalNetwork<E>, 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::<E>(&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,74 +185,148 @@ impl<E: EthSpec> LocalNetwork<E> {
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<E>, LocalExecutionNode<E>), 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<E>, LocalExecutionNode<E>), 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?;
if is_proposer {
self_1.proposer_nodes.write().push(beacon_node);
} else {
self_1.beacon_nodes.write().push(beacon_node);
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.proposer_nodes.write().push(beacon_node);
} else {
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(())
}
@@ -209,7 +337,6 @@ impl<E: EthSpec> LocalNetwork<E> {
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<E: EthSpec> LocalNetwork<E> {
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<E: EthSpec> LocalNetwork<E> {
.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<E: EthSpec> LocalNetwork<E> {
}
/// Return current epoch of bootnode.
pub async fn bootnode_epoch(&self) -> Result<Epoch, String> {
pub async fn _bootnode_epoch(&self) -> Result<Epoch, String> {
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<E: EthSpec> LocalNetwork<E> {
.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");

View File

@@ -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);

View File

@@ -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::<Vec<_>>();
ValidatorFiles::with_keystores(&indices).unwrap()
})
.collect::<Vec<_>>();
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(())
}

View File

@@ -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::<Vec<_>>();
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<E: EthSpec>(
strategy: &str,
network: LocalNetwork<E>,
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<E: EthSpec>(
network: LocalNetwork<E>,
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<E: EthSpec>(
network: LocalNetwork<E>,
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<E: EthSpec>(
network: LocalNetwork<E>,
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<E: EthSpec>(
network: LocalNetwork<E>,
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<E: EthSpec>(network: &LocalNetwork<E>) -> Result<bool, String> {
// 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))
}