1use super::Node;
5use anyhow::Result;
6use futures::future::try_join_all;
7use rand::rngs::OsRng;
8use std::collections::HashMap;
9use std::net::SocketAddr;
10use std::num::NonZeroUsize;
11use std::time::Duration;
12use std::{
13 ops,
14 path::{Path, PathBuf},
15};
16
17#[cfg(msim)]
18use sui_config::node::ExecutionTimeObserverConfig;
19use sui_config::node::{
20 AuthorityOverloadConfig, ConsensusTransactionPoolConfig, DBCheckpointConfig, RunWithRange,
21};
22use sui_config::{ExecutionCacheConfig, NodeConfig};
23use sui_macros::nondeterministic;
24use sui_node::SuiNodeHandle;
25use sui_protocol_config::{Chain, ProtocolVersion};
26use sui_swarm_config::genesis_config::{AccountConfig, GenesisConfig, ValidatorGenesisConfig};
27use sui_swarm_config::network_config::NetworkConfig;
28use sui_swarm_config::network_config_builder::{
29 CommitteeConfig, ConfigBuilder, FundsWithdrawSchedulerTypeConfig,
30 GlobalStateHashV2EnabledConfig, ProtocolVersionsConfig, SupportedProtocolVersionsCallback,
31 ValidatorObserverConfigCallback,
32};
33use sui_swarm_config::node_config_builder::FullnodeConfigBuilder;
34use sui_types::base_types::AuthorityName;
35use sui_types::object::Object;
36use sui_types::supported_protocol_versions::SupportedProtocolVersions;
37use tempfile::TempDir;
38use tracing::info;
39
40pub struct SwarmBuilder<R = OsRng> {
41 rng: R,
42 dir: Option<PathBuf>,
44 committee: CommitteeConfig,
45 genesis_config: Option<GenesisConfig>,
46 network_config: Option<NetworkConfig>,
47 chain_override: Option<Chain>,
48 additional_objects: Vec<Object>,
49 fullnode_count: usize,
50 fullnode_rpc_port: Option<u16>,
51 fullnode_rpc_addr: Option<SocketAddr>,
52 fullnode_rpc_config: Option<sui_config::RpcConfig>,
53 fullnode_config: Option<NodeConfig>,
54 supported_protocol_versions_config: ProtocolVersionsConfig,
55 fullnode_supported_protocol_versions_config: Option<ProtocolVersionsConfig>,
57 db_checkpoint_config: DBCheckpointConfig,
58 jwk_fetch_interval: Option<Duration>,
59 num_unpruned_validators: Option<usize>,
60 authority_overload_config: Option<AuthorityOverloadConfig>,
61 consensus_transaction_pool_config: Option<ConsensusTransactionPoolConfig>,
62 execution_cache_config: Option<ExecutionCacheConfig>,
63 data_ingestion_dir: Option<PathBuf>,
64 fullnode_run_with_range: Option<RunWithRange>,
65 global_state_hash_v2_enabled_config: GlobalStateHashV2EnabledConfig,
66 funds_withdraw_scheduler_type_config: Option<FundsWithdrawSchedulerTypeConfig>,
67 disable_fullnode_pruning: bool,
68 state_sync_config: Option<sui_config::p2p::StateSyncConfig>,
69 peer_deny_sync_config:
70 Option<sui_swarm_config::network_config_builder::PeerDenySyncConfigCallback>,
71 #[cfg(msim)]
72 execution_time_observer_config: Option<ExecutionTimeObserverConfig>,
73 validator_observer_config: Option<ValidatorObserverConfigCallback>,
74}
75
76impl SwarmBuilder {
77 #[allow(clippy::new_without_default)]
78 pub fn new() -> Self {
79 Self {
80 rng: OsRng,
81 dir: None,
82 committee: CommitteeConfig::Size(NonZeroUsize::new(1).unwrap()),
83 genesis_config: None,
84 network_config: None,
85 chain_override: None,
86 additional_objects: vec![],
87 fullnode_count: 0,
88 fullnode_rpc_port: None,
89 fullnode_rpc_addr: None,
90 fullnode_rpc_config: None,
91 fullnode_config: None,
92 supported_protocol_versions_config: ProtocolVersionsConfig::Default,
93 fullnode_supported_protocol_versions_config: None,
94 db_checkpoint_config: DBCheckpointConfig::default(),
95 jwk_fetch_interval: None,
96 num_unpruned_validators: None,
97 authority_overload_config: None,
98 consensus_transaction_pool_config: None,
99 execution_cache_config: None,
100 data_ingestion_dir: None,
101 fullnode_run_with_range: None,
102 global_state_hash_v2_enabled_config: GlobalStateHashV2EnabledConfig::Global(true),
103 funds_withdraw_scheduler_type_config: None,
104 disable_fullnode_pruning: false,
105 state_sync_config: None,
106 peer_deny_sync_config: None,
107 #[cfg(msim)]
108 execution_time_observer_config: None,
109 validator_observer_config: None,
110 }
111 }
112}
113
114impl<R> SwarmBuilder<R> {
115 pub fn rng<N: rand::RngCore + rand::CryptoRng>(self, rng: N) -> SwarmBuilder<N> {
116 SwarmBuilder {
117 rng,
118 dir: self.dir,
119 committee: self.committee,
120 genesis_config: self.genesis_config,
121 network_config: self.network_config,
122 chain_override: self.chain_override,
123 additional_objects: self.additional_objects,
124 fullnode_count: self.fullnode_count,
125 fullnode_rpc_port: self.fullnode_rpc_port,
126 fullnode_rpc_addr: self.fullnode_rpc_addr,
127 fullnode_rpc_config: self.fullnode_rpc_config.clone(),
128 fullnode_config: self.fullnode_config,
129 supported_protocol_versions_config: self.supported_protocol_versions_config,
130 fullnode_supported_protocol_versions_config: self
131 .fullnode_supported_protocol_versions_config,
132 db_checkpoint_config: self.db_checkpoint_config,
133 jwk_fetch_interval: self.jwk_fetch_interval,
134 num_unpruned_validators: self.num_unpruned_validators,
135 authority_overload_config: self.authority_overload_config,
136 consensus_transaction_pool_config: self.consensus_transaction_pool_config,
137 execution_cache_config: self.execution_cache_config,
138 data_ingestion_dir: self.data_ingestion_dir,
139 fullnode_run_with_range: self.fullnode_run_with_range,
140 global_state_hash_v2_enabled_config: self.global_state_hash_v2_enabled_config,
141 funds_withdraw_scheduler_type_config: self.funds_withdraw_scheduler_type_config,
142 disable_fullnode_pruning: self.disable_fullnode_pruning,
143 state_sync_config: self.state_sync_config,
144 peer_deny_sync_config: self.peer_deny_sync_config,
145 #[cfg(msim)]
146 execution_time_observer_config: self.execution_time_observer_config,
147 validator_observer_config: self.validator_observer_config,
148 }
149 }
150
151 pub fn dir<P: Into<PathBuf>>(mut self, dir: P) -> Self {
157 self.dir = Some(dir.into());
158 self
159 }
160
161 pub fn committee_size(mut self, committee_size: NonZeroUsize) -> Self {
165 self.committee = CommitteeConfig::Size(committee_size);
166 self
167 }
168
169 pub fn with_validators(mut self, validators: Vec<ValidatorGenesisConfig>) -> Self {
170 self.committee = CommitteeConfig::Validators(validators);
171 self
172 }
173
174 pub fn with_genesis_config(mut self, genesis_config: GenesisConfig) -> Self {
175 assert!(self.network_config.is_none() && self.genesis_config.is_none());
176 self.genesis_config = Some(genesis_config);
177 self
178 }
179
180 pub fn with_chain_override(mut self, chain: Chain) -> Self {
181 assert!(self.chain_override.is_none());
182 self.chain_override = Some(chain);
183 self
184 }
185
186 pub fn with_num_unpruned_validators(mut self, n: usize) -> Self {
187 assert!(self.network_config.is_none());
188 self.num_unpruned_validators = Some(n);
189 self
190 }
191
192 pub fn with_jwk_fetch_interval(mut self, i: Duration) -> Self {
193 self.jwk_fetch_interval = Some(i);
194 self
195 }
196
197 pub fn with_network_config(mut self, network_config: NetworkConfig) -> Self {
198 assert!(self.network_config.is_none() && self.genesis_config.is_none());
199 self.network_config = Some(network_config);
200 self
201 }
202
203 pub fn with_accounts(mut self, accounts: Vec<AccountConfig>) -> Self {
204 self.get_or_init_genesis_config().accounts = accounts;
205 self
206 }
207
208 pub fn with_objects<I: IntoIterator<Item = Object>>(mut self, objects: I) -> Self {
209 self.additional_objects.extend(objects);
210 self
211 }
212
213 pub fn with_fullnode_count(mut self, fullnode_count: usize) -> Self {
214 self.fullnode_count = fullnode_count;
215 self
216 }
217
218 pub fn with_fullnode_rpc_port(mut self, fullnode_rpc_port: u16) -> Self {
219 assert!(self.fullnode_rpc_addr.is_none());
220 self.fullnode_rpc_port = Some(fullnode_rpc_port);
221 self
222 }
223
224 pub fn with_fullnode_rpc_addr(mut self, fullnode_rpc_addr: SocketAddr) -> Self {
225 assert!(self.fullnode_rpc_port.is_none());
226 self.fullnode_rpc_addr = Some(fullnode_rpc_addr);
227 self
228 }
229
230 pub fn with_fullnode_rpc_config(mut self, fullnode_rpc_config: sui_config::RpcConfig) -> Self {
231 self.fullnode_rpc_config = Some(fullnode_rpc_config);
232 self
233 }
234
235 pub fn with_fullnode_config(mut self, fullnode_config: NodeConfig) -> Self {
236 self.fullnode_config = Some(fullnode_config);
237 self
238 }
239
240 pub fn with_epoch_duration_ms(mut self, epoch_duration_ms: u64) -> Self {
241 assert!(
242 epoch_duration_ms >= 10000,
243 "Epoch duration must be at least 10s (10000ms) to avoid flaky tests. Got {epoch_duration_ms}ms."
244 );
245 self.get_or_init_genesis_config()
246 .parameters
247 .epoch_duration_ms = epoch_duration_ms;
248 self
249 }
250
251 pub fn with_protocol_version(mut self, v: ProtocolVersion) -> Self {
252 self.get_or_init_genesis_config()
253 .parameters
254 .protocol_version = v;
255 self
256 }
257
258 pub fn with_supported_protocol_versions(mut self, c: SupportedProtocolVersions) -> Self {
259 self.supported_protocol_versions_config = ProtocolVersionsConfig::Global(c);
260 self
261 }
262
263 pub fn with_supported_protocol_version_callback(
264 mut self,
265 func: SupportedProtocolVersionsCallback,
266 ) -> Self {
267 self.supported_protocol_versions_config = ProtocolVersionsConfig::PerValidator(func);
268 self
269 }
270
271 pub fn with_supported_protocol_versions_config(mut self, c: ProtocolVersionsConfig) -> Self {
272 self.supported_protocol_versions_config = c;
273 self
274 }
275
276 pub fn with_global_state_hash_v2_enabled_config(
277 mut self,
278 c: GlobalStateHashV2EnabledConfig,
279 ) -> Self {
280 self.global_state_hash_v2_enabled_config = c;
281 self
282 }
283
284 pub fn with_funds_withdraw_scheduler_type_config(
285 mut self,
286 c: FundsWithdrawSchedulerTypeConfig,
287 ) -> Self {
288 self.funds_withdraw_scheduler_type_config = Some(c);
289 self
290 }
291
292 #[cfg(msim)]
293 pub fn with_execution_time_observer_config(mut self, c: ExecutionTimeObserverConfig) -> Self {
294 self.execution_time_observer_config = Some(c);
295 self
296 }
297
298 pub fn with_validator_observer_config(mut self, c: ValidatorObserverConfigCallback) -> Self {
299 self.validator_observer_config = Some(c);
300 self
301 }
302
303 pub fn with_fullnode_supported_protocol_versions_config(
304 mut self,
305 c: ProtocolVersionsConfig,
306 ) -> Self {
307 self.fullnode_supported_protocol_versions_config = Some(c);
308 self
309 }
310
311 pub fn with_db_checkpoint_config(mut self, db_checkpoint_config: DBCheckpointConfig) -> Self {
312 self.db_checkpoint_config = db_checkpoint_config;
313 self
314 }
315
316 pub fn with_authority_overload_config(
317 mut self,
318 authority_overload_config: AuthorityOverloadConfig,
319 ) -> Self {
320 assert!(self.network_config.is_none());
321 self.authority_overload_config = Some(authority_overload_config);
322 self
323 }
324
325 pub fn with_consensus_transaction_pool_config(
326 mut self,
327 consensus_transaction_pool_config: ConsensusTransactionPoolConfig,
328 ) -> Self {
329 assert!(self.network_config.is_none());
330 self.consensus_transaction_pool_config = Some(consensus_transaction_pool_config);
331 self
332 }
333
334 pub fn with_execution_cache_config(
335 mut self,
336 execution_cache_config: ExecutionCacheConfig,
337 ) -> Self {
338 self.execution_cache_config = Some(execution_cache_config);
339 self
340 }
341
342 pub fn with_data_ingestion_dir(mut self, path: PathBuf) -> Self {
343 self.data_ingestion_dir = Some(path);
344 self
345 }
346
347 pub fn with_state_sync_config(mut self, config: sui_config::p2p::StateSyncConfig) -> Self {
348 self.state_sync_config = Some(config);
349 self
350 }
351
352 pub fn with_peer_deny_sync_config_per_validator(
353 mut self,
354 f: sui_swarm_config::network_config_builder::PeerDenySyncConfigCallback,
355 ) -> Self {
356 self.peer_deny_sync_config = Some(f);
357 self
358 }
359
360 pub fn with_fullnode_run_with_range(mut self, run_with_range: Option<RunWithRange>) -> Self {
361 if let Some(run_with_range) = run_with_range {
362 self.fullnode_run_with_range = Some(run_with_range);
363 }
364 self
365 }
366
367 fn get_or_init_genesis_config(&mut self) -> &mut GenesisConfig {
368 if self.genesis_config.is_none() {
369 assert!(self.network_config.is_none());
370 self.genesis_config = Some(GenesisConfig::for_local_testing());
371 }
372 self.genesis_config.as_mut().unwrap()
373 }
374
375 pub fn with_disable_fullnode_pruning(mut self) -> Self {
376 self.disable_fullnode_pruning = true;
377 self
378 }
379}
380
381impl<R: rand::RngCore + rand::CryptoRng> SwarmBuilder<R> {
382 pub fn build(self) -> Swarm {
384 let dir = if let Some(dir) = self.dir {
385 SwarmDirectory::Persistent(dir)
386 } else {
387 SwarmDirectory::new_temporary()
388 };
389
390 let ingest_data = self.data_ingestion_dir.clone();
391
392 let network_config = self.network_config.unwrap_or_else(|| {
393 let mut config_builder = ConfigBuilder::new(dir.as_ref());
394
395 if let Some(genesis_config) = self.genesis_config {
396 config_builder = config_builder.with_genesis_config(genesis_config);
397 }
398
399 if let Some(chain_override) = self.chain_override {
400 config_builder = config_builder.with_chain_override(chain_override);
401 }
402
403 if let Some(num_unpruned_validators) = self.num_unpruned_validators {
404 config_builder =
405 config_builder.with_num_unpruned_validators(num_unpruned_validators);
406 }
407
408 if let Some(jwk_fetch_interval) = self.jwk_fetch_interval {
409 config_builder = config_builder.with_jwk_fetch_interval(jwk_fetch_interval);
410 }
411
412 if let Some(authority_overload_config) = self.authority_overload_config {
413 config_builder =
414 config_builder.with_authority_overload_config(authority_overload_config);
415 }
416
417 if let Some(config) = self.consensus_transaction_pool_config {
418 config_builder = config_builder.with_consensus_transaction_pool_config(config);
419 }
420
421 if let Some(execution_cache_config) = self.execution_cache_config {
422 config_builder = config_builder.with_execution_cache_config(execution_cache_config);
423 }
424
425 if let Some(path) = self.data_ingestion_dir {
426 config_builder = config_builder.with_data_ingestion_dir(path);
427 }
428
429 #[allow(unused_mut)]
430 let mut final_builder = config_builder
431 .committee(self.committee)
432 .rng(self.rng)
433 .with_objects(self.additional_objects)
434 .with_supported_protocol_versions_config(
435 self.supported_protocol_versions_config.clone(),
436 )
437 .with_global_state_hash_v2_enabled_config(
438 self.global_state_hash_v2_enabled_config.clone(),
439 );
440
441 if let Some(funds_withdraw_scheduler_type_config) =
442 self.funds_withdraw_scheduler_type_config.clone()
443 {
444 final_builder = final_builder.with_funds_withdraw_scheduler_type_config(
445 funds_withdraw_scheduler_type_config,
446 );
447 }
448
449 if let Some(state_sync_config) = self.state_sync_config.clone() {
450 final_builder = final_builder.with_state_sync_config(state_sync_config);
451 }
452
453 if let Some(cb) = self.peer_deny_sync_config.clone() {
454 final_builder = final_builder.with_peer_deny_sync_config_per_validator(cb);
455 }
456
457 #[cfg(msim)]
458 if let Some(execution_time_observer_config) = self.execution_time_observer_config {
459 final_builder = final_builder
460 .with_execution_time_observer_config(execution_time_observer_config);
461 }
462
463 if let Some(validator_observer_config) = self.validator_observer_config {
464 final_builder =
465 final_builder.with_validator_observer_config(validator_observer_config);
466 }
467
468 final_builder.build()
469 });
470
471 let mut nodes: HashMap<_, _> = network_config
472 .validator_configs()
473 .iter()
474 .map(|config| {
475 info!(
476 "SwarmBuilder configuring validator with name {}",
477 config.protocol_public_key()
478 );
479 (config.protocol_public_key(), Node::new(config.to_owned()))
480 })
481 .collect();
482
483 let mut fullnode_config_builder = FullnodeConfigBuilder::new()
484 .with_config_directory(dir.as_ref().into())
485 .with_db_checkpoint_config(self.db_checkpoint_config.clone())
486 .with_run_with_range(self.fullnode_run_with_range)
487 .with_data_ingestion_dir(ingest_data)
488 .with_disable_pruning(self.disable_fullnode_pruning);
489
490 if let Some(state_sync_config) = self.state_sync_config.clone() {
491 fullnode_config_builder =
492 fullnode_config_builder.with_state_sync_config(state_sync_config);
493 }
494
495 if let Some(chain) = self.chain_override {
496 fullnode_config_builder = fullnode_config_builder.with_chain_override(chain);
497 }
498
499 if let Some(spvc) = &self.fullnode_supported_protocol_versions_config {
500 let supported_versions = match spvc {
501 ProtocolVersionsConfig::Default => SupportedProtocolVersions::SYSTEM_DEFAULT,
502 ProtocolVersionsConfig::Global(v) => *v,
503 ProtocolVersionsConfig::PerValidator(func) => func(0, None),
504 };
505 fullnode_config_builder =
506 fullnode_config_builder.with_supported_protocol_versions(supported_versions);
507 }
508
509 if self.fullnode_count > 0 {
510 let mut prebuilt_fullnode_config = self.fullnode_config;
511 (0..self.fullnode_count).for_each(|idx| {
512 let config = if idx == 0 && prebuilt_fullnode_config.is_some() {
513 prebuilt_fullnode_config.take().unwrap()
514 } else {
515 let mut builder = fullnode_config_builder.clone();
516 if idx == 0 {
517 if let Some(rpc_addr) = self.fullnode_rpc_addr {
520 builder = builder.with_rpc_addr(rpc_addr);
521 }
522 if let Some(rpc_port) = self.fullnode_rpc_port {
523 builder = builder.with_rpc_port(rpc_port);
524 }
525 if let Some(rpc_config) = &self.fullnode_rpc_config {
526 builder = builder.with_rpc_config(rpc_config.clone());
527 }
528 }
529 builder.build(&mut OsRng, &network_config)
530 };
531 info!(
532 "SwarmBuilder configuring full node with name {}",
533 config.protocol_public_key()
534 );
535 nodes.insert(config.protocol_public_key(), Node::new(config));
536 });
537 }
538 Swarm {
539 dir,
540 network_config,
541 nodes,
542 fullnode_config_builder,
543 }
544 }
545}
546
547#[derive(Debug)]
549pub struct Swarm {
550 dir: SwarmDirectory,
551 network_config: NetworkConfig,
552 nodes: HashMap<AuthorityName, Node>,
553 fullnode_config_builder: FullnodeConfigBuilder,
555}
556
557impl Drop for Swarm {
558 fn drop(&mut self) {
559 self.nodes_iter_mut().for_each(|node| node.stop());
560 }
561}
562
563impl Swarm {
564 fn nodes_iter_mut(&mut self) -> impl Iterator<Item = &mut Node> {
565 self.nodes.values_mut()
566 }
567
568 pub fn builder() -> SwarmBuilder {
570 SwarmBuilder::new()
571 }
572
573 pub async fn launch(&mut self) -> Result<()> {
575 try_join_all(self.nodes_iter_mut().map(|node| node.start())).await?;
576 tracing::info!("Successfully launched Swarm");
577 Ok(())
578 }
579
580 pub fn dir(&self) -> &Path {
582 self.dir.as_ref()
583 }
584
585 pub fn config(&self) -> &NetworkConfig {
587 &self.network_config
588 }
589
590 pub fn config_mut(&mut self) -> &mut NetworkConfig {
593 &mut self.network_config
594 }
595
596 pub fn all_nodes(&self) -> impl Iterator<Item = &Node> {
597 self.nodes.values()
598 }
599
600 pub fn node(&self, name: &AuthorityName) -> Option<&Node> {
601 self.nodes.get(name)
602 }
603
604 pub fn node_mut(&mut self, name: &AuthorityName) -> Option<&mut Node> {
605 self.nodes.get_mut(name)
606 }
607
608 pub fn validator_nodes(&self) -> impl Iterator<Item = &Node> {
613 self.nodes
614 .values()
615 .filter(|node| node.config().intended_node_role().is_validator())
616 }
617
618 pub fn validator_node_handles(&self) -> Vec<SuiNodeHandle> {
619 self.validator_nodes()
620 .map(|node| node.get_node_handle().unwrap())
621 .collect()
622 }
623
624 pub fn active_validators(&self) -> impl Iterator<Item = &Node> {
626 self.validator_nodes().filter(|node| {
627 node.get_node_handle().is_some_and(|handle| {
628 let state = handle.state();
629 state.is_validator(&state.epoch_store_for_testing())
630 })
631 })
632 }
633
634 pub fn fullnodes(&self) -> impl Iterator<Item = &Node> {
636 self.nodes
637 .values()
638 .filter(|node| node.config().intended_node_role().is_fullnode())
639 }
640
641 pub fn observer_nodes(&self) -> impl Iterator<Item = &Node> {
644 use sui_types::node_role::{FullNodeSyncMode, NodeRole};
645 self.nodes.values().filter(|node| {
646 node.config().intended_node_role()
647 == NodeRole::FullNode(FullNodeSyncMode::ConsensusObserver)
648 })
649 }
650
651 pub async fn spawn_new_node(&mut self, config: NodeConfig) -> SuiNodeHandle {
652 let name = config.protocol_public_key();
653 let node = Node::new(config);
654 node.start().await.unwrap();
655 let handle = node.get_node_handle().unwrap();
656 self.nodes.insert(name, node);
657 handle
658 }
659
660 pub fn get_fullnode_config_builder(&self) -> FullnodeConfigBuilder {
661 self.fullnode_config_builder.clone()
662 }
663}
664
665#[derive(Debug)]
666enum SwarmDirectory {
667 Persistent(PathBuf),
668 Temporary(TempDir),
669}
670
671impl SwarmDirectory {
672 fn new_temporary() -> Self {
673 SwarmDirectory::Temporary(nondeterministic!(TempDir::new().unwrap()))
674 }
675}
676
677impl ops::Deref for SwarmDirectory {
678 type Target = Path;
679
680 fn deref(&self) -> &Self::Target {
681 match self {
682 SwarmDirectory::Persistent(dir) => dir.deref(),
683 SwarmDirectory::Temporary(dir) => dir.path(),
684 }
685 }
686}
687
688impl AsRef<Path> for SwarmDirectory {
689 fn as_ref(&self) -> &Path {
690 match self {
691 SwarmDirectory::Persistent(dir) => dir.as_ref(),
692 SwarmDirectory::Temporary(dir) => dir.as_ref(),
693 }
694 }
695}
696
697#[cfg(test)]
698mod test {
699 use super::Swarm;
700 use std::num::NonZeroUsize;
701
702 #[tokio::test]
703 async fn launch() {
704 telemetry_subscribers::init_for_testing();
705 let mut swarm = Swarm::builder()
706 .committee_size(NonZeroUsize::new(4).unwrap())
707 .with_fullnode_count(1)
708 .build();
709
710 swarm.launch().await.unwrap();
711
712 for validator in swarm.validator_nodes() {
713 validator.health_check(true).await.unwrap();
714 }
715
716 for fullnode in swarm.fullnodes() {
717 fullnode.health_check(false).await.unwrap();
718 }
719
720 println!("hello");
721 }
722}