ant_node/spawn/
network_spawner.rs

1// Copyright 2025 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use crate::RunningNode;
10use crate::spawn::node_spawner::NodeSpawner;
11use ant_bootstrap::BootstrapConfig;
12use ant_evm::{EvmNetwork, RewardsAddress};
13use libp2p::Multiaddr;
14use std::net::{IpAddr, Ipv4Addr, SocketAddr};
15use std::path::PathBuf;
16
17#[derive(Debug, Clone)]
18pub struct NetworkSpawner {
19    /// Bootstrap configuration for all nodes in the network.
20    bootstrap_config: Option<BootstrapConfig>,
21    /// The EVM network to which the spawned nodes will connect.
22    evm_network: EvmNetwork,
23    /// The address that will receive rewards from the spawned nodes.
24    rewards_address: RewardsAddress,
25    /// Disables UPnP on the node (automatic port forwarding).
26    no_upnp: bool,
27    /// Optional root directory to store node data and configurations.
28    root_dir: Option<PathBuf>,
29    /// Number of nodes to spawn in the network.
30    size: usize,
31}
32
33impl NetworkSpawner {
34    /// Creates a new `NetworkSpawner` with default configurations.
35    ///
36    /// Default values:
37    /// - `bootstrap_config`: `None`
38    /// - `evm_network`: `EvmNetwork::default()`
39    /// - `rewards_address`: `RewardsAddress::default()`
40    /// - `no_upnp`: `false`
41    /// - `root_dir`: `None`
42    /// - `size`: `5`
43    pub fn new() -> Self {
44        Self {
45            evm_network: Default::default(),
46            rewards_address: Default::default(),
47            no_upnp: false,
48            root_dir: None,
49            size: 5,
50            bootstrap_config: None,
51        }
52    }
53
54    /// Sets the EVM network to be used by the nodes.
55    ///
56    /// # Arguments
57    ///
58    /// * `evm_network` - The target `EvmNetwork` for the nodes.
59    pub fn with_evm_network(mut self, evm_network: EvmNetwork) -> Self {
60        self.evm_network = evm_network;
61        self
62    }
63
64    /// Sets the rewards address for the nodes.
65    ///
66    /// # Arguments
67    ///
68    /// * `rewards_address` - A valid `RewardsAddress` to collect rewards.
69    pub fn with_rewards_address(mut self, rewards_address: RewardsAddress) -> Self {
70        self.rewards_address = rewards_address;
71        self
72    }
73
74    /// Sets the bootstrap configuration for all nodes in the network.
75    ///
76    /// # Arguments
77    ///
78    /// * `bootstrap_config` - Bootstrap configuration including peer addresses, cache settings, etc.
79    pub fn with_bootstrap_config(mut self, bootstrap_config: BootstrapConfig) -> Self {
80        self.bootstrap_config = Some(bootstrap_config);
81        self
82    }
83
84    /// Disabled UPnP for the nodes.
85    ///
86    /// # Arguments
87    ///
88    /// * `value` - If `false`, nodes will attempt automatic port forwarding using UPnP.
89    pub fn with_no_upnp(mut self, value: bool) -> Self {
90        self.no_upnp = value;
91        self
92    }
93
94    /// Sets the root directory for the nodes.
95    ///
96    /// # Arguments
97    ///
98    /// * `root_dir` - An optional file path where nodes will store their data.
99    pub fn with_root_dir(mut self, root_dir: Option<PathBuf>) -> Self {
100        self.root_dir = root_dir;
101        self
102    }
103
104    /// Specifies the number of nodes to spawn in the network.
105    ///
106    /// # Arguments
107    ///
108    /// * `size` - The number of nodes to create. Default is 5.
109    pub fn with_size(mut self, size: usize) -> Self {
110        self.size = size;
111        self
112    }
113
114    /// Spawns the network with the configured parameters.
115    ///
116    /// # Returns
117    ///
118    /// A future resolving to a `SpawnedNetwork` containing the running nodes,
119    /// or an error if the spawning process fails.
120    pub async fn spawn(self) -> eyre::Result<RunningNetwork> {
121        spawn_network(
122            self.evm_network,
123            self.rewards_address,
124            self.no_upnp,
125            self.root_dir,
126            self.size,
127            self.bootstrap_config,
128        )
129        .await
130    }
131}
132
133impl Default for NetworkSpawner {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139pub struct RunningNetwork {
140    running_nodes: Vec<RunningNode>,
141}
142
143impl RunningNetwork {
144    /// Returns a bootstrap peer from this network.
145    pub async fn bootstrap_peer(&self) -> Multiaddr {
146        self.running_nodes()
147            .first()
148            .expect("No nodes running, cannot get bootstrap peer")
149            .get_listen_addrs_with_peer_id()
150            .await
151            .expect("Could not get listen addresses for bootstrap peer")
152            .last()
153            .expect("Bootstrap peer has no listen addresses")
154            .clone()
155    }
156
157    /// Return all running nodes.
158    pub fn running_nodes(&self) -> &Vec<RunningNode> {
159        &self.running_nodes
160    }
161
162    /// Shutdown all running nodes.
163    pub fn shutdown(self) {
164        for node in self.running_nodes.into_iter() {
165            node.shutdown();
166        }
167    }
168}
169
170async fn spawn_network(
171    evm_network: EvmNetwork,
172    rewards_address: RewardsAddress,
173    no_upnp: bool,
174    root_dir: Option<PathBuf>,
175    size: usize,
176    bootstrap_config: Option<BootstrapConfig>,
177) -> eyre::Result<RunningNetwork> {
178    let mut running_nodes: Vec<RunningNode> = vec![];
179
180    // Extract local flag from bootstrap_config, default to false
181    let local = bootstrap_config.as_ref().map(|c| c.local).unwrap_or(false);
182
183    for i in 0..size {
184        let ip = match local {
185            true => IpAddr::V4(Ipv4Addr::LOCALHOST),
186            false => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
187        };
188
189        let socket_addr = SocketAddr::new(ip, 0);
190
191        // Get the initial peers from the previously spawned nodes
192        let mut initial_peers: Vec<Multiaddr> = vec![];
193
194        for peer in running_nodes.iter() {
195            if let Ok(listen_addrs_with_peer_id) = peer.get_listen_addrs_with_peer_id().await {
196                initial_peers.extend(listen_addrs_with_peer_id);
197            }
198        }
199
200        // Merge bootstrap_config with node-specific config
201        let mut node_bootstrap_config = bootstrap_config.clone().unwrap_or_default();
202        node_bootstrap_config.initial_peers.extend(initial_peers);
203        node_bootstrap_config.first = running_nodes.is_empty();
204        node_bootstrap_config.local = local;
205
206        let node = NodeSpawner::new()
207            .with_socket_addr(socket_addr)
208            .with_evm_network(evm_network.clone())
209            .with_rewards_address(rewards_address)
210            .with_bootstrap_config(node_bootstrap_config)
211            .with_no_upnp(no_upnp)
212            .with_root_dir(root_dir.clone())
213            .spawn()
214            .await?;
215
216        let listen_addrs = node.get_listen_addrs().await;
217
218        info!(
219            "Spawned node #{} with listen addresses: {:?}",
220            i + 1,
221            listen_addrs
222        );
223
224        running_nodes.push(node);
225    }
226
227    Ok(RunningNetwork { running_nodes })
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use std::time::Duration;
234    use tokio::time::sleep;
235
236    #[tokio::test(flavor = "multi_thread")]
237    async fn test_spawn_network() {
238        let network_size = 20;
239
240        let bootstrap_config = BootstrapConfig::new(true)
241            .with_disable_cache_reading(true)
242            .with_disable_env_peers(true);
243
244        let running_network = NetworkSpawner::new()
245            .with_evm_network(Default::default())
246            .with_bootstrap_config(bootstrap_config)
247            .with_no_upnp(true)
248            .with_size(network_size)
249            .spawn()
250            .await
251            .unwrap();
252
253        assert_eq!(running_network.running_nodes().len(), network_size);
254
255        // Wait for nodes to fill up their RT
256        sleep(Duration::from_secs(15)).await;
257
258        // Validate that all nodes know each other
259        for node in running_network.running_nodes() {
260            let peers_in_routing_table = node
261                .get_swarm_local_state()
262                .await
263                .unwrap()
264                .peers_in_routing_table;
265
266            assert!(
267                peers_in_routing_table >= network_size - 2 && peers_in_routing_table < network_size,
268                "Node with PeerId {} has {} peers in its routing table, expected between {} and {}",
269                node.peer_id(),
270                peers_in_routing_table,
271                network_size - 2,
272                network_size - 1
273            );
274        }
275
276        running_network.shutdown();
277    }
278}