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