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