mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-03 00:31:50 +00:00
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:
@@ -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 }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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)"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
261
testing/simulator/src/fallback_sim.rs
Normal file
261
testing/simulator/src/fallback_sim.rs
Normal 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(())
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user