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::spawn::node_spawner::NodeSpawner;
10use crate::RunningNode;
11use ant_evm::{EvmNetwork, RewardsAddress};
12use libp2p::Multiaddr;
13use std::net::{IpAddr, Ipv4Addr, SocketAddr};
14use std::path::PathBuf;
15
16pub struct NetworkSpawner {
17    /// The EVM network to which the spawned nodes will connect.
18    evm_network: EvmNetwork,
19    /// The address that will receive rewards from the spawned nodes.
20    rewards_address: RewardsAddress,
21    /// Specifies whether the network will operate in local mode and sets the listen address.
22    /// - `true`: Nodes listen on the local loopback address (`127.0.0.1`).
23    /// - `false`: Nodes listen on all available interfaces (`0.0.0.0`).
24    local: bool,
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    /// - `evm_network`: `EvmNetwork::default()`
38    /// - `rewards_address`: `RewardsAddress::default()`
39    /// - `local`: `false`
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            local: false,
48            no_upnp: false,
49            root_dir: None,
50            size: 5,
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    /// Configures the local mode for the network.
75    ///
76    /// # Arguments
77    ///
78    /// * `value` - If set to `true`, nodes will operate in local mode and listen only on `127.0.0.1`.
79    ///   Otherwise, they listen on all interfaces (`0.0.0.0`).
80    pub fn with_local(mut self, value: bool) -> Self {
81        self.local = value;
82        self
83    }
84
85    /// Disabled UPnP for the nodes.
86    ///
87    /// # Arguments
88    ///
89    /// * `value` - If `false`, nodes will attempt automatic port forwarding using UPnP.
90    pub fn with_no_upnp(mut self, value: bool) -> Self {
91        self.no_upnp = value;
92        self
93    }
94
95    /// Sets the root directory for the nodes.
96    ///
97    /// # Arguments
98    ///
99    /// * `root_dir` - An optional file path where nodes will store their data.
100    pub fn with_root_dir(mut self, root_dir: Option<PathBuf>) -> Self {
101        self.root_dir = root_dir;
102        self
103    }
104
105    /// Specifies the number of nodes to spawn in the network.
106    ///
107    /// # Arguments
108    ///
109    /// * `size` - The number of nodes to create. Default is 5.
110    pub fn with_size(mut self, size: usize) -> Self {
111        self.size = size;
112        self
113    }
114
115    /// Spawns the network with the configured parameters.
116    ///
117    /// # Returns
118    ///
119    /// A future resolving to a `SpawnedNetwork` containing the running nodes,
120    /// or an error if the spawning process fails.
121    pub async fn spawn(self) -> eyre::Result<RunningNetwork> {
122        spawn_network(
123            self.evm_network,
124            self.rewards_address,
125            self.local,
126            self.no_upnp,
127            self.root_dir,
128            self.size,
129        )
130        .await
131    }
132}
133
134impl Default for NetworkSpawner {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140pub struct RunningNetwork {
141    running_nodes: Vec<RunningNode>,
142}
143
144impl RunningNetwork {
145    /// Returns a bootstrap peer from this network.
146    pub async fn bootstrap_peer(&self) -> Multiaddr {
147        self.running_nodes()
148            .first()
149            .expect("No nodes running, cannot get bootstrap peer")
150            .get_listen_addrs_with_peer_id()
151            .await
152            .expect("Could not get listen addresses for bootstrap peer")
153            .last()
154            .expect("Bootstrap peer has no listen addresses")
155            .clone()
156    }
157
158    /// Return all running nodes.
159    pub fn running_nodes(&self) -> &Vec<RunningNode> {
160        &self.running_nodes
161    }
162
163    /// Shutdown all running nodes.
164    pub fn shutdown(self) {
165        for node in self.running_nodes.into_iter() {
166            node.shutdown();
167        }
168    }
169}
170
171async fn spawn_network(
172    evm_network: EvmNetwork,
173    rewards_address: RewardsAddress,
174    local: bool,
175    no_upnp: bool,
176    root_dir: Option<PathBuf>,
177    size: usize,
178) -> eyre::Result<RunningNetwork> {
179    let mut running_nodes: Vec<RunningNode> = vec![];
180
181    for i in 0..size {
182        let ip = match local {
183            true => IpAddr::V4(Ipv4Addr::LOCALHOST),
184            false => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
185        };
186
187        let socket_addr = SocketAddr::new(ip, 0);
188
189        // Get the initial peers from the previously spawned nodes
190        let mut initial_peers: Vec<Multiaddr> = vec![];
191
192        for peer in running_nodes.iter() {
193            if let Ok(listen_addrs_with_peer_id) = peer.get_listen_addrs_with_peer_id().await {
194                initial_peers.extend(listen_addrs_with_peer_id);
195            }
196        }
197
198        let node = NodeSpawner::new()
199            .with_socket_addr(socket_addr)
200            .with_evm_network(evm_network.clone())
201            .with_rewards_address(rewards_address)
202            .with_initial_peers(initial_peers)
203            .with_local(local)
204            .with_no_upnp(no_upnp)
205            .with_root_dir(root_dir.clone())
206            .spawn()
207            .await?;
208
209        let listen_addrs = node.get_listen_addrs().await;
210
211        info!(
212            "Spawned node #{} with listen addresses: {:?}",
213            i + 1,
214            listen_addrs
215        );
216
217        running_nodes.push(node);
218    }
219
220    Ok(RunningNetwork { running_nodes })
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use std::time::Duration;
227    use tokio::time::sleep;
228
229    #[tokio::test(flavor = "multi_thread")]
230    async fn test_spawn_network() {
231        let network_size = 20;
232
233        let running_network = NetworkSpawner::new()
234            .with_evm_network(Default::default())
235            .with_local(true)
236            .with_no_upnp(true)
237            .with_size(network_size)
238            .spawn()
239            .await
240            .unwrap();
241
242        assert_eq!(running_network.running_nodes().len(), network_size);
243
244        // Wait for nodes to fill up their RT
245        sleep(Duration::from_secs(15)).await;
246
247        // Validate that all nodes know each other
248        for node in running_network.running_nodes() {
249            let peers_in_routing_table = node
250                .get_swarm_local_state()
251                .await
252                .unwrap()
253                .peers_in_routing_table;
254
255            assert!(
256                peers_in_routing_table >= network_size - 2 && peers_in_routing_table < network_size
257            );
258        }
259
260        running_network.shutdown();
261    }
262}