Skip to content

Commit 400a155

Browse files
committed
Add peering sim
1 parent e0ccadb commit 400a155

File tree

4 files changed

+360
-0
lines changed

4 files changed

+360
-0
lines changed

.github/workflows/test-suite.yml

+14
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,20 @@ jobs:
282282
cache-target: release
283283
- name: Run a beacon chain sim which tests VC fallback behaviour
284284
run: cargo run --release --bin simulator fallback-sim
285+
peering-simulator-ubuntu:
286+
name: peering-simulator-ubuntu
287+
needs: [check-labels]
288+
if: needs.check-labels.outputs.skip_ci != 'true'
289+
runs-on: ubuntu-latest
290+
steps:
291+
- uses: actions/checkout@v4
292+
- name: Get latest version of stable Rust
293+
uses: moonrepo/setup-rust@v1
294+
with:
295+
channel: stable
296+
cache-target: release
297+
- name: Run a beacon chain sim which tests BN peering behaviour
298+
run: cargo run --release --bin simulator peering-sim
285299
execution-engine-integration-ubuntu:
286300
name: execution-engine-integration-ubuntu
287301
needs: [check-labels]

testing/simulator/src/cli.rs

+58
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,62 @@ pub fn cli_app() -> Command {
122122
.help("Continue after checks (default false)"),
123123
),
124124
)
125+
.subcommand(
126+
Command::new("peering-sim")
127+
.about(
128+
"Runs a Beacon Chain simulation with `n` beacon node and validator clients, \
129+
each with `v` validators. \
130+
The simulation runs with a post-Merge Genesis using `mock-el`. \
131+
As the simulation runs, additional nodes are periodically added and \
132+
there are checks made to ensure that the nodes are able to sync to the \
133+
network. If a node fails to sync, the simulation will exit immediately.",
134+
)
135+
.arg(
136+
Arg::new("nodes")
137+
.short('n')
138+
.long("nodes")
139+
.action(ArgAction::Set)
140+
.default_value("2")
141+
.help("Number of beacon nodes"),
142+
)
143+
.arg(
144+
Arg::new("proposer-nodes")
145+
.short('p')
146+
.long("proposer-nodes")
147+
.action(ArgAction::Set)
148+
.default_value("0")
149+
.help("Number of proposer-only beacon nodes"),
150+
)
151+
.arg(
152+
Arg::new("validators-per-node")
153+
.short('v')
154+
.long("validators-per-node")
155+
.action(ArgAction::Set)
156+
.default_value("10")
157+
.help("Number of validators"),
158+
)
159+
.arg(
160+
Arg::new("speed-up-factor")
161+
.short('s')
162+
.long("speed-up-factor")
163+
.action(ArgAction::Set)
164+
.default_value("3")
165+
.help("Speed up factor. Please use a divisor of 12."),
166+
)
167+
.arg(
168+
Arg::new("debug-level")
169+
.short('d')
170+
.long("debug-level")
171+
.action(ArgAction::Set)
172+
.default_value("debug")
173+
.help("Set the severity level of the logs."),
174+
)
175+
.arg(
176+
Arg::new("continue-after-checks")
177+
.short('c')
178+
.long("continue_after_checks")
179+
.action(ArgAction::SetTrue)
180+
.help("Continue after checks (default false)"),
181+
),
182+
)
125183
}

testing/simulator/src/main.rs

+8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod checks;
1515
mod cli;
1616
mod fallback_sim;
1717
mod local_network;
18+
mod peering_sim;
1819
mod retry;
1920

2021
use cli::cli_app;
@@ -44,6 +45,13 @@ fn main() {
4445
std::process::exit(1)
4546
}
4647
},
48+
Some(("peering-sim", matches)) => match peering_sim::run_peering_sim(matches) {
49+
Ok(()) => println!("Simulation exited successfully"),
50+
Err(e) => {
51+
eprintln!("Simulation exited with error: {}", e);
52+
std::process::exit(1)
53+
}
54+
},
4755
_ => {
4856
eprintln!("Invalid subcommand. Use --help to see available options");
4957
std::process::exit(1)

testing/simulator/src/peering_sim.rs

+280
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
use crate::local_network::LocalNetworkParams;
2+
use crate::{checks, LocalNetwork};
3+
use clap::ArgMatches;
4+
5+
use crate::retry::with_retry;
6+
use futures::prelude::*;
7+
use node_test_rig::{
8+
environment::{EnvironmentBuilder, LoggerConfig},
9+
testing_validator_config, ApiTopic, ValidatorFiles,
10+
};
11+
use rayon::prelude::*;
12+
use std::cmp::max;
13+
use std::time::Duration;
14+
use tokio::time::sleep;
15+
use types::{Epoch, EthSpec, MinimalEthSpec};
16+
17+
const END_EPOCH: u64 = 16;
18+
const GENESIS_DELAY: u64 = 32;
19+
const ALTAIR_FORK_EPOCH: u64 = 0;
20+
const BELLATRIX_FORK_EPOCH: u64 = 0;
21+
const CAPELLA_FORK_EPOCH: u64 = 1;
22+
const DENEB_FORK_EPOCH: u64 = 2;
23+
const EIP7594_FORK_EPOCH: u64 = 3;
24+
//const ELECTRA_FORK_EPOCH: u64 = 0;
25+
26+
const SUGGESTED_FEE_RECIPIENT: [u8; 20] =
27+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
28+
29+
pub fn run_peering_sim(matches: &ArgMatches) -> Result<(), String> {
30+
let node_count = matches
31+
.get_one::<String>("nodes")
32+
.expect("missing nodes default")
33+
.parse::<usize>()
34+
.expect("missing nodes default");
35+
let proposer_nodes = matches
36+
.get_one::<String>("proposer-nodes")
37+
.unwrap_or(&String::from("0"))
38+
.parse::<usize>()
39+
.unwrap_or(0);
40+
// extra beacon node added with delay
41+
let extra_nodes: usize = 8;
42+
println!("PROPOSER-NODES: {}", proposer_nodes);
43+
let validators_per_node = matches
44+
.get_one::<String>("validators-per-node")
45+
.expect("missing validators-per-node default")
46+
.parse::<usize>()
47+
.expect("missing validators-per-node default");
48+
let speed_up_factor = matches
49+
.get_one::<String>("speed-up-factor")
50+
.expect("missing speed-up-factor default")
51+
.parse::<u64>()
52+
.expect("missing speed-up-factor default");
53+
let log_level = matches
54+
.get_one::<String>("debug-level")
55+
.expect("missing debug-level");
56+
57+
let continue_after_checks = matches.get_flag("continue-after-checks");
58+
59+
println!("Peering Simulator:");
60+
println!(" nodes: {}", node_count);
61+
println!(" proposer-nodes: {}", proposer_nodes);
62+
println!(" validators-per-node: {}", validators_per_node);
63+
println!(" speed-up-factor: {}", speed_up_factor);
64+
println!(" continue-after-checks: {}", continue_after_checks);
65+
66+
// Generate the directories and keystores required for the validator clients.
67+
let validator_files = (0..node_count)
68+
.into_par_iter()
69+
.map(|i| {
70+
println!(
71+
"Generating keystores for validator {} of {}",
72+
i + 1,
73+
node_count
74+
);
75+
76+
let indices =
77+
(i * validators_per_node..(i + 1) * validators_per_node).collect::<Vec<_>>();
78+
ValidatorFiles::with_keystores(&indices).unwrap()
79+
})
80+
.collect::<Vec<_>>();
81+
82+
let mut env = EnvironmentBuilder::minimal()
83+
.initialize_logger(LoggerConfig {
84+
path: None,
85+
debug_level: log_level.clone(),
86+
logfile_debug_level: log_level.clone(),
87+
log_format: None,
88+
logfile_format: None,
89+
log_color: false,
90+
disable_log_timestamp: false,
91+
max_log_size: 0,
92+
max_log_number: 0,
93+
compression: false,
94+
is_restricted: true,
95+
sse_logging: false,
96+
})?
97+
.multi_threaded_tokio_runtime()?
98+
.build()?;
99+
100+
let spec = &mut env.eth2_config.spec;
101+
102+
let total_validator_count = validators_per_node * node_count;
103+
let genesis_delay = GENESIS_DELAY;
104+
105+
spec.seconds_per_slot /= speed_up_factor;
106+
spec.seconds_per_slot = max(1, spec.seconds_per_slot);
107+
spec.genesis_delay = genesis_delay;
108+
spec.min_genesis_time = 0;
109+
spec.min_genesis_active_validator_count = total_validator_count as u64;
110+
spec.altair_fork_epoch = Some(Epoch::new(ALTAIR_FORK_EPOCH));
111+
spec.bellatrix_fork_epoch = Some(Epoch::new(BELLATRIX_FORK_EPOCH));
112+
spec.capella_fork_epoch = Some(Epoch::new(CAPELLA_FORK_EPOCH));
113+
spec.deneb_fork_epoch = Some(Epoch::new(DENEB_FORK_EPOCH));
114+
spec.eip7594_fork_epoch = Some(Epoch::new(EIP7594_FORK_EPOCH));
115+
//spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH));
116+
117+
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
118+
let slots_per_epoch = MinimalEthSpec::slots_per_epoch();
119+
120+
let context = env.core_context();
121+
122+
let main_future = async {
123+
/*
124+
* Create a new `LocalNetwork` with one beacon node.
125+
*/
126+
let max_retries = 3;
127+
let (network, beacon_config, mock_execution_config) = with_retry(max_retries, || {
128+
Box::pin(LocalNetwork::create_local_network(
129+
None,
130+
None,
131+
LocalNetworkParams {
132+
validator_count: total_validator_count,
133+
node_count,
134+
extra_nodes,
135+
proposer_nodes,
136+
genesis_delay,
137+
},
138+
context.clone(),
139+
))
140+
})
141+
.await?;
142+
143+
// Add nodes to the network.
144+
for _ in 0..node_count {
145+
network
146+
.add_beacon_node(beacon_config.clone(), mock_execution_config.clone(), false)
147+
.await?;
148+
}
149+
150+
/*
151+
* One by one, add proposer nodes to the network.
152+
*/
153+
for _ in 0..proposer_nodes {
154+
println!("Adding a proposer node");
155+
network
156+
.add_beacon_node(beacon_config.clone(), mock_execution_config.clone(), true)
157+
.await?;
158+
}
159+
160+
/*
161+
* One by one, add validators to the network.
162+
*/
163+
164+
let executor = context.executor.clone();
165+
for (i, files) in validator_files.into_iter().enumerate() {
166+
let network_1 = network.clone();
167+
executor.spawn(
168+
async move {
169+
let mut validator_config = testing_validator_config();
170+
validator_config.fee_recipient = Some(SUGGESTED_FEE_RECIPIENT.into());
171+
println!("Adding validator client {}", i);
172+
173+
// Enable broadcast on every 4th node.
174+
if i % 4 == 0 {
175+
validator_config.broadcast_topics = ApiTopic::all();
176+
let beacon_nodes = vec![i, (i + 1) % node_count];
177+
network_1
178+
.add_validator_client_with_fallbacks(
179+
validator_config,
180+
i,
181+
beacon_nodes,
182+
files,
183+
)
184+
.await
185+
} else {
186+
network_1
187+
.add_validator_client(validator_config, i, files)
188+
.await
189+
}
190+
.expect("should add validator");
191+
},
192+
"vc",
193+
);
194+
}
195+
196+
// Set all payloads as valid. This effectively assumes the EL is infalliable.
197+
network.execution_nodes.write().iter().for_each(|node| {
198+
node.server.all_payloads_valid();
199+
});
200+
201+
let duration_to_genesis = network.duration_to_genesis().await;
202+
println!("Duration to genesis: {}", duration_to_genesis.as_secs());
203+
sleep(duration_to_genesis).await;
204+
205+
/*
206+
* Start the checks that ensure the network performs as expected.
207+
*
208+
* We start these checks immediately after the validators have started. This means we're
209+
* relying on the validator futures to all return immediately after genesis so that these
210+
* tests start at the right time. Whilst this is works well for now, it's subject to
211+
* breakage by changes to the VC.
212+
*/
213+
214+
let mut sequence = vec![];
215+
let mut epoch_delay = extra_nodes as u64;
216+
let mut node_count = node_count;
217+
218+
for _ in 0..extra_nodes {
219+
let network_1 = network.clone();
220+
let owned_mock_execution_config = mock_execution_config.clone();
221+
let owned_beacon_config = beacon_config.clone();
222+
sequence.push(async move {
223+
network_1
224+
.add_beacon_node_with_delay(
225+
owned_beacon_config,
226+
owned_mock_execution_config,
227+
END_EPOCH - epoch_delay,
228+
slot_duration,
229+
slots_per_epoch,
230+
)
231+
.await?;
232+
checks::ensure_node_synced_up_to_slot(
233+
network_1,
234+
// This must be set to be the node which was just created. Should be equal to
235+
// `node_count`.
236+
node_count,
237+
Epoch::new(END_EPOCH).start_slot(slots_per_epoch),
238+
slot_duration,
239+
)
240+
.await?;
241+
Ok::<(), String>(())
242+
});
243+
epoch_delay -= 2;
244+
node_count += 1;
245+
}
246+
247+
let futures = futures::future::join_all(sequence).await;
248+
for res in futures {
249+
res?
250+
}
251+
252+
// The `final_future` either completes immediately or never completes, depending on the value
253+
// of `continue_after_checks`.
254+
255+
if continue_after_checks {
256+
future::pending::<()>().await;
257+
}
258+
/*
259+
* End the simulation by dropping the network. This will kill all running beacon nodes and
260+
* validator clients.
261+
*/
262+
println!(
263+
"Simulation complete. Finished with {} beacon nodes and {} validator clients",
264+
network.beacon_node_count() + network.proposer_node_count(),
265+
network.validator_client_count()
266+
);
267+
268+
// Be explicit about dropping the network, as this kills all the nodes. This ensures
269+
// all the checks have adequate time to pass.
270+
drop(network);
271+
Ok::<(), String>(())
272+
};
273+
274+
env.runtime().block_on(main_future).unwrap();
275+
276+
env.fire_signal();
277+
env.shutdown_on_idle();
278+
279+
Ok(())
280+
}

0 commit comments

Comments
 (0)