From bbd7e2d549346d4ce3bf59e49e1e68e36188bdf8 Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Fri, 17 Jan 2025 19:19:17 +0100 Subject: [PATCH] feat: add spawnable nodes and networks --- ant-evm/src/lib.rs | 1 + ant-node/src/bin/antnode/main.rs | 81 +------------ ant-node/src/lib.rs | 15 ++- ant-node/src/node.rs | 3 +- ant-node/src/spawn/mod.rs | 2 + ant-node/src/spawn/network.rs | 173 ++++++++++++++++++++++++++++ ant-node/src/spawn/node.rs | 192 +++++++++++++++++++++++++++++++ ant-node/src/utils.rs | 80 +++++++++++++ 8 files changed, 464 insertions(+), 83 deletions(-) create mode 100644 ant-node/src/spawn/mod.rs create mode 100644 ant-node/src/spawn/network.rs create mode 100644 ant-node/src/spawn/node.rs create mode 100644 ant-node/src/utils.rs diff --git a/ant-evm/src/lib.rs b/ant-evm/src/lib.rs index e8d5e92784..2f9be27bf2 100644 --- a/ant-evm/src/lib.rs +++ b/ant-evm/src/lib.rs @@ -18,6 +18,7 @@ pub use evmlib::contract::payment_vault; pub use evmlib::cryptography; #[cfg(feature = "external-signer")] pub use evmlib::external_signer; +pub use evmlib::testnet::Testnet as EvmTestnet; pub use evmlib::utils; pub use evmlib::utils::get_evm_network; pub use evmlib::utils::{DATA_PAYMENTS_ADDRESS, PAYMENT_TOKEN_ADDRESS, RPC_URL}; diff --git a/ant-node/src/bin/antnode/main.rs b/ant-node/src/bin/antnode/main.rs index 2be7543dae..60cebf6751 100644 --- a/ant-node/src/bin/antnode/main.rs +++ b/ant-node/src/bin/antnode/main.rs @@ -17,6 +17,7 @@ use ant_bootstrap::{BootstrapCacheConfig, BootstrapCacheStore, PeersArgs}; use ant_evm::{get_evm_network, EvmNetwork, RewardsAddress}; use ant_logging::metrics::init_metrics; use ant_logging::{Level, LogFormat, LogOutputDest, ReloadHandle}; +use ant_node::utils::get_root_dir_and_keypair; use ant_node::{Marker, NodeBuilder, NodeEvent, NodeEventsReceiver}; use ant_protocol::{ node::get_antnode_root_dir, @@ -26,12 +27,11 @@ use ant_protocol::{ use clap::{command, Parser}; use color_eyre::{eyre::eyre, Result}; use const_hex::traits::FromHex; -use libp2p::{identity::Keypair, PeerId}; +use libp2p::PeerId; use std::{ env, - io::Write, net::{IpAddr, Ipv4Addr, SocketAddr}, - path::{Path, PathBuf}, + path::PathBuf, process::Command, time::Duration, }; @@ -623,81 +623,6 @@ fn init_logging(opt: &Opt, peer_id: PeerId) -> Result<(String, ReloadHandle, Opt Ok((output_dest.to_string(), reload_handle, log_appender_guard)) } -fn create_secret_key_file(path: impl AsRef) -> Result { - let mut opt = std::fs::OpenOptions::new(); - opt.write(true).create_new(true); - - // On Unix systems, make sure only the current user can read/write. - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - opt.mode(0o600); - } - - opt.open(path) -} - -fn keypair_from_path(path: impl AsRef) -> Result { - let keypair = match std::fs::read(&path) { - // If the file is opened successfully, read the key from it - Ok(key) => { - let keypair = Keypair::ed25519_from_bytes(key) - .map_err(|err| eyre!("could not read ed25519 key from file: {err}"))?; - - info!("loaded secret key from file: {:?}", path.as_ref()); - - keypair - } - // In case the file is not found, generate a new keypair and write it to the file - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - let secret_key = libp2p::identity::ed25519::SecretKey::generate(); - let mut file = create_secret_key_file(&path) - .map_err(|err| eyre!("could not create secret key file: {err}"))?; - file.write_all(secret_key.as_ref())?; - - info!("generated new key and stored to file: {:?}", path.as_ref()); - - libp2p::identity::ed25519::Keypair::from(secret_key).into() - } - // Else the file can't be opened, for whatever reason (e.g. permissions). - Err(err) => { - return Err(eyre!("failed to read secret key file: {err}")); - } - }; - - Ok(keypair) -} - -/// The keypair is located inside the root directory. At the same time, when no dir is specified, -/// the dir name is derived from the keypair used in the application: the peer ID is used as the directory name. -fn get_root_dir_and_keypair(root_dir: &Option) -> Result<(PathBuf, Keypair)> { - match root_dir { - Some(dir) => { - std::fs::create_dir_all(dir)?; - - let secret_key_path = dir.join("secret-key"); - Ok((dir.clone(), keypair_from_path(secret_key_path)?)) - } - None => { - let secret_key = libp2p::identity::ed25519::SecretKey::generate(); - let keypair: Keypair = - libp2p::identity::ed25519::Keypair::from(secret_key.clone()).into(); - let peer_id = keypair.public().to_peer_id(); - - let dir = get_antnode_root_dir(peer_id)?; - std::fs::create_dir_all(&dir)?; - - let secret_key_path = dir.join("secret-key"); - - let mut file = create_secret_key_file(secret_key_path) - .map_err(|err| eyre!("could not create secret key file: {err}"))?; - file.write_all(secret_key.as_ref())?; - - Ok((dir, keypair)) - } - } -} - /// Starts a new process running the binary with the same args as /// the current process /// Optionally provide the node's root dir and listen port to retain it's PeerId diff --git a/ant-node/src/lib.rs b/ant-node/src/lib.rs index 3599e14a7a..d9073dea83 100644 --- a/ant-node/src/lib.rs +++ b/ant-node/src/lib.rs @@ -32,6 +32,10 @@ mod put_validation; mod python; mod quote; mod replication; +#[allow(missing_docs)] +pub mod spawn; +#[allow(missing_docs)] +pub mod utils; pub use self::{ event::{NodeEvent, NodeEventsChannel, NodeEventsReceiver}, @@ -41,16 +45,15 @@ pub use self::{ use crate::error::{Error, Result}; +use ant_evm::RewardsAddress; use ant_networking::{Network, SwarmLocalState}; use ant_protocol::{get_port_from_multiaddr, NetworkAddress}; -use libp2p::PeerId; +use libp2p::{Multiaddr, PeerId}; use std::{ collections::{BTreeMap, HashSet}, path::PathBuf, }; -use ant_evm::RewardsAddress; - /// Once a node is started and running, the user obtains /// a `NodeRunning` object which can be used to interact with it. #[derive(Clone)] @@ -85,6 +88,12 @@ impl RunningNode { Ok(state) } + /// Return the node's listening addresses. + pub async fn get_listen_addrs(&self) -> Result> { + let listeners = self.network.get_swarm_local_state().await?.listeners; + Ok(listeners) + } + /// Return the node's listening port pub async fn get_node_listening_port(&self) -> Result { let listen_addrs = self.network.get_swarm_local_state().await?.listeners; diff --git a/ant-node/src/node.rs b/ant-node/src/node.rs index ba83779c84..16e3c9593f 100644 --- a/ant-node/src/node.rs +++ b/ant-node/src/node.rs @@ -14,6 +14,7 @@ use crate::metrics::NodeMetricsRecorder; use crate::RunningNode; use ant_bootstrap::BootstrapCacheStore; use ant_evm::RewardsAddress; +use ant_evm::{EvmNetwork, U256}; #[cfg(feature = "open-metrics")] use ant_networking::MetricsRegistries; use ant_networking::{ @@ -49,8 +50,6 @@ use tokio::{ task::{spawn, JoinSet}, }; -use ant_evm::{EvmNetwork, U256}; - /// Interval to trigger replication of all records to all peers. /// This is the max time it should take. Minimum interval at any node will be half this pub const PERIODIC_REPLICATION_INTERVAL_MAX_S: u64 = 180; diff --git a/ant-node/src/spawn/mod.rs b/ant-node/src/spawn/mod.rs new file mode 100644 index 0000000000..4c47460324 --- /dev/null +++ b/ant-node/src/spawn/mod.rs @@ -0,0 +1,2 @@ +pub mod network; +pub mod node; diff --git a/ant-node/src/spawn/network.rs b/ant-node/src/spawn/network.rs new file mode 100644 index 0000000000..d991b693fb --- /dev/null +++ b/ant-node/src/spawn/network.rs @@ -0,0 +1,173 @@ +use crate::spawn::node::NodeSpawner; +use crate::RunningNode; +use ant_evm::{EvmNetwork, RewardsAddress}; +use libp2p::Multiaddr; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::PathBuf; + +pub struct NetworkSpawner { + evm_network: EvmNetwork, + rewards_address: RewardsAddress, + local: bool, + upnp: bool, + root_dir: Option, + size: usize, +} + +impl NetworkSpawner { + pub fn new() -> Self { + Self { + evm_network: Default::default(), + rewards_address: Default::default(), + local: false, + upnp: false, + root_dir: None, + size: 5, + } + } + + /// Sets the EVM network. + pub fn with_evm_network(mut self, evm_network: EvmNetwork) -> Self { + self.evm_network = evm_network; + self + } + + /// Sets the rewards address. + pub fn with_rewards_address(mut self, rewards_address: RewardsAddress) -> Self { + self.rewards_address = rewards_address; + self + } + + /// Sets the local mode value. + pub fn with_local(mut self, value: bool) -> Self { + self.local = value; + self + } + + /// Sets the UPnP value (automatic port forwarding). + pub fn with_upnp(mut self, value: bool) -> Self { + self.upnp = value; + self + } + + /// Sets the root directory for the nodes. + pub fn with_root_dir(mut self, root_dir: Option) -> Self { + self.root_dir = root_dir; + self + } + + /// Sets the amount of nodes spawned in the network. + pub fn with_size(mut self, size: usize) -> Self { + self.size = size; + self + } + + pub async fn spawn(self) -> eyre::Result { + spawn_network( + self.evm_network, + self.rewards_address, + self.local, + self.upnp, + self.root_dir, + self.size, + ) + .await + } +} + +impl Default for NetworkSpawner { + fn default() -> Self { + Self::new() + } +} + +pub struct SpawnedNetwork { + running_nodes: Vec, +} + +impl SpawnedNetwork { + pub fn running_nodes(&self) -> &Vec { + &self.running_nodes + } +} + +async fn spawn_network( + evm_network: EvmNetwork, + rewards_address: RewardsAddress, + local: bool, + upnp: bool, + root_dir: Option, + size: usize, +) -> eyre::Result { + let mut running_nodes: Vec = vec![]; + + for i in 0..size { + let ip = match local { + true => IpAddr::V4(Ipv4Addr::LOCALHOST), + false => IpAddr::V4(Ipv4Addr::UNSPECIFIED), + }; + + let socket_addr = SocketAddr::new(ip, 0); + + let mut initial_peers: Vec = vec![]; + + for peer in running_nodes.iter() { + if let Ok(listen_addrs) = peer.get_listen_addrs().await { + initial_peers.extend(listen_addrs); + } + } + + let node = NodeSpawner::new() + .with_socket_addr(socket_addr) + .with_evm_network(evm_network.clone()) + .with_rewards_address(rewards_address) + .with_initial_peers(initial_peers) + .with_local(local) + .with_upnp(upnp) + .with_root_dir(root_dir.clone()) + .spawn() + .await?; + + let listen_addrs = node.get_listen_addrs().await; + + info!( + "Spawned node #{} with listen addresses: {:?}", + i + 1, + listen_addrs + ); + + running_nodes.push(node); + } + + Ok(SpawnedNetwork { running_nodes }) +} + +#[cfg(test)] +mod tests { + use super::*; + use ant_evm::EvmTestnet; + + #[tokio::test] + async fn test_spawn_network() { + // start local Ethereum node + let evm_testnet = EvmTestnet::new().await; + let evm_network = evm_testnet.to_network(); + let network_size = 20; + + let spawned_network = NetworkSpawner::new() + .with_evm_network(evm_network) + .with_size(network_size) + .spawn() + .await + .unwrap(); + + assert_eq!(spawned_network.running_nodes().len(), network_size); + + // Validate each node's listen addresses are not empty + for node in spawned_network.running_nodes() { + let listen_addrs = node.get_listen_addrs().await.unwrap(); + + assert!(!listen_addrs.is_empty()); + } + } +} diff --git a/ant-node/src/spawn/node.rs b/ant-node/src/spawn/node.rs new file mode 100644 index 0000000000..81bb01a4c9 --- /dev/null +++ b/ant-node/src/spawn/node.rs @@ -0,0 +1,192 @@ +use crate::utils::get_root_dir_and_keypair; +use crate::{NodeBuilder, RunningNode}; +use ant_evm::{EvmNetwork, RewardsAddress}; +use libp2p::Multiaddr; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::PathBuf; + +pub struct NodeSpawner { + socket_addr: SocketAddr, + evm_network: EvmNetwork, + rewards_address: RewardsAddress, + initial_peers: Vec, + local: bool, + upnp: bool, + root_dir: Option, +} + +impl NodeSpawner { + pub fn new() -> Self { + Self { + socket_addr: SocketAddr::new(IpAddr::from(Ipv4Addr::UNSPECIFIED), 0), + evm_network: Default::default(), + rewards_address: Default::default(), + initial_peers: vec![], + local: false, + upnp: false, + root_dir: None, + } + } + + /// Set the socket address for the node. + /// + /// # Arguments + /// + /// * `socket_addr` - The `SocketAddr` where the node will listen. + pub fn with_socket_addr(mut self, socket_addr: SocketAddr) -> Self { + self.socket_addr = socket_addr; + self + } + + /// Set the EVM network for the node. + /// + /// # Arguments + /// + /// * `evm_network` - The `EvmNetwork` the node will connect to. + pub fn with_evm_network(mut self, evm_network: EvmNetwork) -> Self { + self.evm_network = evm_network; + self + } + + /// Set the rewards address for the node. + /// + /// # Arguments + /// + /// * `rewards_address` - The `RewardsAddress` used for distributing rewards. + pub fn with_rewards_address(mut self, rewards_address: RewardsAddress) -> Self { + self.rewards_address = rewards_address; + self + } + + /// Set the initial peers for the node. + /// + /// # Arguments + /// + /// * `initial_peers` - A vector of `Multiaddr` representing the initial peers. + pub fn with_initial_peers(mut self, initial_peers: Vec) -> Self { + self.initial_peers = initial_peers; + self + } + + /// Set the local mode flag for the node. + /// + /// # Arguments + /// + /// * `local` - A boolean indicating whether the node should run in local mode. + pub fn with_local(mut self, local: bool) -> Self { + self.local = local; + self + } + + /// Set the UPnP flag for the node. + /// + /// # Arguments + /// + /// * `upnp` - A boolean indicating whether UPnP should be enabled. + pub fn with_upnp(mut self, upnp: bool) -> Self { + self.upnp = upnp; + self + } + + /// Set the root directory for the node. + /// + /// # Arguments + /// + /// * `root_dir` - An optional `PathBuf` representing the root directory for the node. + pub fn with_root_dir(mut self, root_dir: Option) -> Self { + self.root_dir = root_dir; + self + } + + pub async fn spawn(self) -> eyre::Result { + spawn_node( + self.socket_addr, + self.evm_network, + self.rewards_address, + self.initial_peers, + self.local, + self.upnp, + &self.root_dir, + ) + .await + } +} + +impl Default for NodeSpawner { + fn default() -> Self { + Self::new() + } +} + +async fn spawn_node( + socket_addr: SocketAddr, + evm_network: EvmNetwork, + rewards_address: RewardsAddress, + initial_peers: Vec, + local: bool, + upnp: bool, + root_dir: &Option, +) -> eyre::Result { + let (root_dir, keypair) = get_root_dir_and_keypair(root_dir)?; + + let mut node_builder = NodeBuilder::new( + keypair, + rewards_address, + evm_network, + socket_addr, + local, + root_dir, + upnp, + ); + + if !initial_peers.is_empty() { + node_builder.initial_peers(initial_peers); + } + + let running_node = node_builder.build_and_run()?; + + // Verify that node is running + let mut retries: u8 = 0; + + let listen_addrs: Vec = loop { + if let Ok(listen_addrs) = running_node.get_listen_addrs().await { + break Ok(listen_addrs); + } + + if retries >= 3 { + break Err(eyre::eyre!( + "Failed to get listen addresses after {} retries", + retries + )); + } + + retries += 1; + + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + }?; + + info!("Node listening on addresses: {:?}", listen_addrs); + + Ok(running_node) +} + +#[cfg(test)] +mod tests { + use super::*; + use ant_evm::EvmNetwork; + + #[tokio::test] + async fn test_launch_node() { + let evm_network = EvmNetwork::ArbitrumSepolia; + + let node = NodeSpawner::new() + .with_evm_network(evm_network) + .spawn() + .await + .unwrap(); + + let listen_addrs = node.get_listen_addrs().await.unwrap(); + + assert!(!listen_addrs.is_empty()); + } +} diff --git a/ant-node/src/utils.rs b/ant-node/src/utils.rs new file mode 100644 index 0000000000..595d075393 --- /dev/null +++ b/ant-node/src/utils.rs @@ -0,0 +1,80 @@ +use ant_protocol::node::get_antnode_root_dir; +use eyre::eyre; +use libp2p::identity::Keypair; +use std::io::Write; +use std::path::{Path, PathBuf}; + +/// The keypair is located inside the root directory. At the same time, when no dir is specified, +/// the dir name is derived from the keypair used in the application: the peer ID is used as the directory name. +pub fn get_root_dir_and_keypair(root_dir: &Option) -> eyre::Result<(PathBuf, Keypair)> { + match root_dir { + Some(dir) => { + std::fs::create_dir_all(dir)?; + + let secret_key_path = dir.join("secret-key"); + Ok((dir.clone(), keypair_from_path(secret_key_path)?)) + } + None => { + let secret_key = libp2p::identity::ed25519::SecretKey::generate(); + let keypair: Keypair = + libp2p::identity::ed25519::Keypair::from(secret_key.clone()).into(); + let peer_id = keypair.public().to_peer_id(); + + let dir = get_antnode_root_dir(peer_id)?; + std::fs::create_dir_all(&dir)?; + + let secret_key_path = dir.join("secret-key"); + + let mut file = create_secret_key_file(secret_key_path) + .map_err(|err| eyre!("could not create secret key file: {err}"))?; + file.write_all(secret_key.as_ref())?; + + Ok((dir, keypair)) + } + } +} + +fn keypair_from_path(path: impl AsRef) -> eyre::Result { + let keypair = match std::fs::read(&path) { + // If the file is opened successfully, read the key from it + Ok(key) => { + let keypair = Keypair::ed25519_from_bytes(key) + .map_err(|err| eyre!("could not read ed25519 key from file: {err}"))?; + + info!("loaded secret key from file: {:?}", path.as_ref()); + + keypair + } + // In case the file is not found, generate a new keypair and write it to the file + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + let secret_key = libp2p::identity::ed25519::SecretKey::generate(); + let mut file = create_secret_key_file(&path) + .map_err(|err| eyre!("could not create secret key file: {err}"))?; + file.write_all(secret_key.as_ref())?; + + info!("generated new key and stored to file: {:?}", path.as_ref()); + + libp2p::identity::ed25519::Keypair::from(secret_key).into() + } + // Else the file can't be opened, for whatever reason (e.g. permissions). + Err(err) => { + return Err(eyre!("failed to read secret key file: {err}")); + } + }; + + Ok(keypair) +} + +fn create_secret_key_file(path: impl AsRef) -> eyre::Result { + let mut opt = std::fs::OpenOptions::new(); + let _ = opt.write(true).create_new(true); + + // On Unix systems, make sure only the current user can read/write. + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + let _ = opt.mode(0o600); + } + + opt.open(path) +}