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