Add merge support to simulator (#3292)

## Issue Addressed

N/A

## Proposed Changes

Make simulator merge compatible. Adds a `--post_merge` flag to the eth1 simulator that enables a ttd and simulates the merge transition. Uses the `MockServer` in the execution layer test utils to simulate a dummy execution node.

Adds the merge transition simulation to CI.
This commit is contained in:
Pawan Dhananjay
2022-07-18 23:15:40 +00:00
parent da7b7a0f60
commit f9b9658711
17 changed files with 389 additions and 69 deletions

View File

@@ -1,7 +1,7 @@
use crate::local_network::LocalNetwork;
use node_test_rig::eth2::types::{BlockId, StateId};
use std::time::Duration;
use types::{Epoch, EthSpec, Slot, Unsigned};
use types::{Epoch, EthSpec, ExecutionBlockHash, Hash256, Slot, Unsigned};
/// Checks that all of the validators have on-boarded by the start of the second eth1 voting
/// period.
@@ -149,19 +149,19 @@ pub async fn verify_fork_version<E: EthSpec>(
network: LocalNetwork<E>,
fork_epoch: Epoch,
slot_duration: Duration,
altair_fork_version: [u8; 4],
fork_version: [u8; 4],
) -> Result<(), String> {
epoch_delay(fork_epoch, slot_duration, E::slots_per_epoch()).await;
for remote_node in network.remote_nodes()? {
let fork_version = remote_node
let remote_fork_version = remote_node
.get_beacon_states_fork(StateId::Head)
.await
.map(|resp| resp.unwrap().data.current_version)
.map_err(|e| format!("Failed to get fork from beacon node: {:?}", e))?;
if fork_version != altair_fork_version {
if fork_version != remote_fork_version {
return Err(format!(
"Fork version after FORK_EPOCH is incorrect, got: {:?}, expected: {:?}",
fork_version, altair_fork_version,
remote_fork_version, fork_version,
));
}
}
@@ -207,3 +207,39 @@ pub async fn verify_full_sync_aggregates_up_to<E: EthSpec>(
Ok(())
}
/// Verify that the first merged PoS block got finalized.
pub async fn verify_transition_block_finalized<E: EthSpec>(
network: LocalNetwork<E>,
transition_epoch: Epoch,
slot_duration: Duration,
should_verify: bool,
) -> Result<(), String> {
if !should_verify {
return Ok(());
}
epoch_delay(transition_epoch + 2, slot_duration, E::slots_per_epoch()).await;
let mut block_hashes = Vec::new();
for remote_node in network.remote_nodes()?.iter() {
let execution_block_hash: ExecutionBlockHash = remote_node
.get_beacon_blocks::<E>(BlockId::Finalized)
.await
.map(|body| body.unwrap().data)
.map_err(|e| format!("Get state root via http failed: {:?}", e))?
.message()
.execution_payload()
.map(|payload| payload.execution_payload.block_hash)
.map_err(|e| format!("Execution payload does not exist: {:?}", e))?;
block_hashes.push(execution_block_hash);
}
let first = block_hashes[0];
if first.into_root() != Hash256::zero() && block_hashes.iter().all(|&item| item == first) {
Ok(())
} else {
Err(format!(
"Terminal block not finalized on all nodes Finalized block hashes:{:?}",
block_hashes
))
}
}

View File

@@ -36,6 +36,11 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.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")
.short("c")
.long("continue_after_checks")

View File

@@ -1,4 +1,4 @@
use crate::local_network::INVALID_ADDRESS;
use crate::local_network::{EXECUTION_PORT, INVALID_ADDRESS, TERMINAL_BLOCK, TERMINAL_DIFFICULTY};
use crate::{checks, LocalNetwork, E};
use clap::ArgMatches;
use eth1::{Eth1Endpoint, DEFAULT_CHAIN_ID};
@@ -18,8 +18,12 @@ use std::time::Duration;
use tokio::time::sleep;
use types::{Epoch, EthSpec, MinimalEthSpec};
const FORK_EPOCH: u64 = 2;
const END_EPOCH: u64 = 16;
const ALTAIR_FORK_EPOCH: u64 = 1;
const BELLATRIX_FORK_EPOCH: u64 = 2;
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");
@@ -28,10 +32,12 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
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");
println!("Beacon Chain Simulator:");
println!(" nodes:{}", node_count);
println!(" validators_per_node:{}", validators_per_node);
println!(" post merge simulation:{}", post_merge_sim);
println!(" continue_after_checks:{}", continue_after_checks);
// Generate the directories and keystores required for the validator clients.
@@ -72,6 +78,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
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;
spec.seconds_per_slot /= speed_up_factor;
spec.seconds_per_slot = max(1, spec.seconds_per_slot);
@@ -80,8 +87,14 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
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(FORK_EPOCH));
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));
}
let seconds_per_slot = spec.seconds_per_slot;
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
let initial_validator_count = spec.min_genesis_active_validator_count as usize;
let deposit_amount = env.eth2_config.spec.max_effective_balance;
@@ -137,6 +150,19 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
beacon_config.network.enr_address = Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
if post_merge_sim {
let el_config = execution_layer::Config {
execution_endpoints: vec![SensitiveUrl::parse(&format!(
"http://localhost:{}",
EXECUTION_PORT
))
.unwrap()],
..Default::default()
};
beacon_config.execution_layer = Some(el_config);
}
/*
* Create a new `LocalNetwork` with one beacon node.
*/
@@ -168,9 +194,13 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
let network_1 = network.clone();
executor.spawn(
async move {
let mut validator_config = testing_validator_config();
if post_merge_sim {
validator_config.fee_recipient = Some(SUGGESTED_FEE_RECIPIENT.into());
}
println!("Adding validator client {}", i);
network_1
.add_validator_client(testing_validator_config(), i, files, i % 2 == 0)
.add_validator_client(validator_config, i, files, i % 2 == 0)
.await
.expect("should add validator");
},
@@ -182,6 +212,21 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
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.
*
@@ -190,7 +235,16 @@ 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 (finalization, block_prod, validator_count, onboarding, fork, sync_aggregate) = futures::join!(
let (
finalization,
block_prod,
validator_count,
onboarding,
fork,
sync_aggregate,
transition,
) = 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.
@@ -212,21 +266,36 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
slot_duration,
total_validator_count,
),
// Check that all nodes have transitioned to the new fork.
// Check that all nodes have transitioned to the required fork.
checks::verify_fork_version(
network.clone(),
Epoch::new(FORK_EPOCH),
if post_merge_sim {
Epoch::new(BELLATRIX_FORK_EPOCH)
} else {
Epoch::new(ALTAIR_FORK_EPOCH)
},
slot_duration,
altair_fork_version
if post_merge_sim {
bellatrix_fork_version
} else {
altair_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(FORK_EPOCH + 1).start_slot(MinimalEthSpec::slots_per_epoch()),
Epoch::new(ALTAIR_FORK_EPOCH + 1).start_slot(MinimalEthSpec::slots_per_epoch()),
Epoch::new(END_EPOCH).start_slot(MinimalEthSpec::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()),
slot_duration,
post_merge_sim
)
);
@@ -236,6 +305,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
onboarding?;
fork?;
sync_aggregate?;
transition?;
// The `final_future` either completes immediately or never completes, depending on the value
// of `continue_after_checks`.

View File

@@ -1,7 +1,8 @@
use node_test_rig::{
environment::RuntimeContext,
eth2::{types::StateId, BeaconNodeHttpClient},
ClientConfig, LocalBeaconNode, LocalValidatorClient, ValidatorConfig, ValidatorFiles,
ClientConfig, LocalBeaconNode, LocalExecutionNode, LocalValidatorClient, MockExecutionConfig,
MockServerConfig, ValidatorConfig, ValidatorFiles,
};
use parking_lot::RwLock;
use sensitive_url::SensitiveUrl;
@@ -15,11 +16,17 @@ use types::{Epoch, EthSpec};
const BOOTNODE_PORT: u16 = 42424;
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;
/// Helper struct to reduce `Arc` usage.
pub struct Inner<E: EthSpec> {
pub context: RuntimeContext<E>,
pub beacon_nodes: RwLock<Vec<LocalBeaconNode<E>>>,
pub validator_clients: RwLock<Vec<LocalValidatorClient<E>>>,
pub execution_nodes: RwLock<Vec<LocalExecutionNode<E>>>,
}
/// Represents a set of interconnected `LocalBeaconNode` and `LocalValidatorClient`.
@@ -46,7 +53,7 @@ impl<E: EthSpec> Deref for LocalNetwork<E> {
}
impl<E: EthSpec> LocalNetwork<E> {
/// Creates a new network with a single `BeaconNode`.
/// Creates a new network with a single `BeaconNode` and a connected `ExecutionNode`.
pub async fn new(
context: RuntimeContext<E>,
mut beacon_config: ClientConfig,
@@ -56,6 +63,30 @@ impl<E: EthSpec> LocalNetwork<E> {
beacon_config.network.enr_udp_port = Some(BOOTNODE_PORT);
beacon_config.network.enr_tcp_port = Some(BOOTNODE_PORT);
beacon_config.network.discv5_config.table_filter = |_| true;
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_files = vec![execution_node.datadir.path().join("jwt.hex")];
el_config.execution_endpoints =
vec![SensitiveUrl::parse(&execution_node.server.url()).unwrap()];
vec![execution_node]
} else {
vec![]
};
let beacon_node =
LocalBeaconNode::production(context.service_context("boot_node".into()), beacon_config)
.await?;
@@ -63,6 +94,7 @@ impl<E: EthSpec> LocalNetwork<E> {
inner: Arc::new(Inner {
context,
beacon_nodes: RwLock::new(vec![beacon_node]),
execution_nodes: RwLock::new(execution_node),
validator_clients: RwLock::new(vec![]),
}),
})
@@ -87,6 +119,7 @@ impl<E: EthSpec> LocalNetwork<E> {
/// 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) -> 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();
@@ -99,20 +132,38 @@ impl<E: EthSpec> LocalNetwork<E> {
.enr()
.expect("bootnode must have a network"),
);
let count = self.beacon_node_count() as u16;
beacon_config.network.discovery_port = BOOTNODE_PORT + count;
beacon_config.network.libp2p_port = BOOTNODE_PORT + count;
beacon_config.network.enr_udp_port = Some(BOOTNODE_PORT + count);
beacon_config.network.enr_tcp_port = Some(BOOTNODE_PORT + count);
beacon_config.network.discv5_config.table_filter = |_| true;
}
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_files = vec![execution_node.datadir.path().join("jwt.hex")];
el_config.execution_endpoints =
vec![SensitiveUrl::parse(&execution_node.server.url()).unwrap()];
self.execution_nodes.write().push(execution_node);
}
// 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).
let index = self_1.beacon_nodes.read().len();
let beacon_node = LocalBeaconNode::production(
self.context.service_context(format!("node_{}", index)),
self.context.service_context(format!("node_{}", count)),
beacon_config,
)
.await?;
@@ -184,6 +235,16 @@ 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");