From fad0b936a9ff8baf0e5bbd231dc0becde0882622 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Thu, 4 Jan 2024 16:12:01 +0000 Subject: [PATCH] feat: provide `--first` argument for `safenode` In the previous setup, if the `network-contacts` feature was enabled, running `safenode` with no `--peer` args caused the peers to be retrieved from the default network contacts file. In the situation where we're launching a genesis node, we don't want this behaviour, and hence we introduce a `--first` flag to make a distinction. The `safenode` binary was changed to remove the use of `unwrap_or` when calling `get_peers_from_args` so that an error will actually occur when peers are not obtainable, rather than swallowing it by returning an empty peer list. The `testnet` binary was also updated to use the `--first` argument when a new network was being created. This necessitated a change to the memcheck workflow, which starts a testnet in a slightly unusual fashion. The `--first` argument had to be applied to the initial node that is launched, and then the subsequent local testnet launch had to be changed to use a join rather than starting a new network. This is because the first peer launched by `testnet`, using `--first`, did not have the initial node launched outside of `testnet` in its peer list. For the test to work correctly, an environment variable was introduced to control the starting port deployment inventory, because using a join network means the port range starts one digit higher. We will be able to remove this when we switch over to using the node manager. BREAKING CHANGE: the `parse_peer_args` function was renamed `get_peers_from_args` and the error handling was changed. The new function name is semantically more accurate, and because `sn_peers_acquisition` is a library crate, we should prefer an `Error` type rather than using `eyre`. Tried to add some unit tests for the `get_peers_from_args` function, but Tokio being an optional dependency proved to be problematic. --- .github/workflows/memcheck.yml | 15 ++++- Cargo.lock | 2 +- sn_cli/src/main.rs | 8 +-- sn_faucet/src/main.rs | 4 +- sn_node/src/bin/safenode/main.rs | 5 +- sn_node/tests/common/client.rs | 16 +++-- sn_node/tests/data_with_churn.rs | 2 +- sn_node/tests/msgs_over_gossipsub.rs | 2 +- sn_node/tests/nodes_rewards.rs | 2 +- sn_node/tests/verify_data_location.rs | 2 +- sn_node/tests/verify_routing_table.rs | 2 +- sn_node_rpc_client/src/main.rs | 4 +- sn_peers_acquisition/Cargo.toml | 2 +- sn_peers_acquisition/src/error.rs | 22 +++++++ sn_peers_acquisition/src/lib.rs | 95 ++++++++++++++++----------- sn_testnet/src/lib.rs | 3 + 16 files changed, 122 insertions(+), 64 deletions(-) create mode 100644 sn_peers_acquisition/src/error.rs diff --git a/.github/workflows/memcheck.yml b/.github/workflows/memcheck.yml index 3b00711473..19ed865d8a 100644 --- a/.github/workflows/memcheck.yml +++ b/.github/workflows/memcheck.yml @@ -43,7 +43,7 @@ jobs: - name: Start a node instance that does not undergo churn run: | mkdir -p $BOOTSTRAP_NODE_DATA_PATH - ./target/release/safenode \ + ./target/release/safenode --first \ --root-dir $BOOTSTRAP_NODE_DATA_PATH --log-output-dest $BOOTSTRAP_NODE_DATA_PATH --local & sleep 10 env: @@ -55,6 +55,10 @@ jobs: rg '/ip4.*$' -m1 -o | rg '"' -r '') echo "SAFE_PEERS=$safe_peers" >> $GITHUB_ENV + - name: Set CHURN_TEST_START_PORT + run: | + echo "CHURN_TEST_START_PORT=12001" >> $GITHUB_ENV + - name: Check SAFE_PEERS was set shell: bash run: echo "The SAFE_PEERS variable has been set to $SAFE_PEERS" @@ -71,6 +75,7 @@ jobs: platform: ubuntu-latest set-safe-peers: false build: true + join: true # In this case we did *not* want SAFE_PEERS to be set to another value by starting the testnet - name: Check SAFE_PEERS was not changed @@ -79,7 +84,13 @@ jobs: - name: Create and fund a wallet to pay for files storage run: | - cargo run --bin faucet --release -- --log-output-dest=data-dir send 5000000 $(cargo run --bin safe --release -- --log-output-dest=data-dir wallet address | tail -n 1) > initial_balance_from_faucet.txt + echo "Obtaining address for use with the faucet..." + address=$(cargo run \ + --bin safe --release -- --log-output-dest=data-dir wallet address | tail -n 1) + echo "Sending tokens to the faucet at $address" + cargo run \ + --bin faucet --release -- \ + --log-output-dest=data-dir send 5000000 $address > initial_balance_from_faucet.txt cat initial_balance_from_faucet.txt cat initial_balance_from_faucet.txt | tail -n 1 > transfer_hex cat transfer_hex diff --git a/Cargo.lock b/Cargo.lock index 0dc5746b5a..19af84450c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4737,10 +4737,10 @@ name = "sn_peers_acquisition" version = "0.1.14" dependencies = [ "clap 4.4.10", - "color-eyre", "libp2p 0.53.1", "rand", "reqwest", + "thiserror", "tokio", "tracing", "url", diff --git a/sn_cli/src/main.rs b/sn_cli/src/main.rs index c642609f4a..400d504471 100644 --- a/sn_cli/src/main.rs +++ b/sn_cli/src/main.rs @@ -28,7 +28,7 @@ use color_eyre::Result; use sn_client::Client; #[cfg(feature = "metrics")] use sn_logging::{metrics::init_metrics, LogBuilder, LogFormat}; -use sn_peers_acquisition::parse_peers_args; +use sn_peers_acquisition::get_peers_from_args; use sn_transfers::bls_secret_from_hex; use std::path::PathBuf; use tracing::Level; @@ -85,11 +85,11 @@ async fn main() -> Result<()> { println!("Instantiating a SAFE client..."); let secret_key = get_client_secret_key(&client_data_dir_path)?; - let bootstrap_peers = parse_peers_args(opt.peers).await?; + let bootstrap_peers = get_peers_from_args(opt.peers).await?; println!( - "Connecting to the network with {} peers:", - bootstrap_peers.len() + "Connecting to the network with {} peers", + bootstrap_peers.len(), ); let bootstrap_peers = if bootstrap_peers.is_empty() { diff --git a/sn_faucet/src/main.rs b/sn_faucet/src/main.rs index 446900dc84..c9312e0584 100644 --- a/sn_faucet/src/main.rs +++ b/sn_faucet/src/main.rs @@ -13,7 +13,7 @@ use color_eyre::eyre::{bail, eyre, Result}; use faucet_server::{restart_faucet_server, run_faucet_server}; use sn_client::{get_tokens_from_faucet, load_faucet_wallet_from_genesis_wallet, Client}; use sn_logging::{LogBuilder, LogOutputDest}; -use sn_peers_acquisition::{parse_peers_args, PeersArgs}; +use sn_peers_acquisition::{get_peers_from_args, PeersArgs}; use sn_transfers::{MainPubkey, NanoTokens, Transfer}; use std::path::PathBuf; use tracing::{error, info}; @@ -23,7 +23,7 @@ use tracing_core::Level; async fn main() -> Result<()> { let opt = Opt::parse(); - let bootstrap_peers = parse_peers_args(opt.peers).await?; + let bootstrap_peers = get_peers_from_args(opt.peers).await?; let bootstrap_peers = if bootstrap_peers.is_empty() { // empty vec is returned if `local-discovery` flag is provided None diff --git a/sn_node/src/bin/safenode/main.rs b/sn_node/src/bin/safenode/main.rs index 8fb93298a5..5f6ae96f7c 100644 --- a/sn_node/src/bin/safenode/main.rs +++ b/sn_node/src/bin/safenode/main.rs @@ -18,7 +18,7 @@ use libp2p::{identity::Keypair, PeerId}; use sn_logging::metrics::init_metrics; use sn_logging::{LogFormat, LogOutputDest}; use sn_node::{Marker, NodeBuilder, NodeEvent, NodeEventsReceiver}; -use sn_peers_acquisition::{parse_peers_args, PeersArgs}; +use sn_peers_acquisition::{get_peers_from_args, PeersArgs}; use sn_protocol::node_rpc::NodeCtrl; use std::{ env, @@ -162,8 +162,7 @@ fn main() -> Result<()> { let (log_output_dest, _log_appender_guard) = init_logging(&opt, keypair.public().to_peer_id())?; let rt = Runtime::new()?; - // bootstrap peers can be empty for the genesis node. - let bootstrap_peers = rt.block_on(parse_peers_args(opt.peers)).unwrap_or(vec![]); + let bootstrap_peers = rt.block_on(get_peers_from_args(opt.peers))?; let msg = format!( "Running {} v{}", env!("CARGO_BIN_NAME"), diff --git a/sn_node/tests/common/client.rs b/sn_node/tests/common/client.rs index 692a44106f..ffbd81c30f 100644 --- a/sn_node/tests/common/client.rs +++ b/sn_node/tests/common/client.rs @@ -49,17 +49,23 @@ pub fn get_node_count() -> usize { /// Get the list of all RPC addresses /// If SN_INVENTORY flag is passed, the RPC addresses of all the droplet nodes are returned /// else generate local addresses for NODE_COUNT nodes -pub fn get_all_rpc_addresses() -> Vec { +pub fn get_all_rpc_addresses() -> Result> { match DeploymentInventory::load() { - Ok(inventory) => inventory.rpc_endpoints, + Ok(inventory) => Ok(inventory.rpc_endpoints), Err(_) => { + let starting_port = match std::env::var("CHURN_TEST_START_PORT") { + Ok(val) => val.parse()?, + Err(_) => 12000, + }; let mut addresses = Vec::new(); for i in 1..LOCAL_NODE_COUNT + 1 { - let addr = - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12000 + i as u16); + let addr = SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + starting_port + i as u16, + ); addresses.push(addr); } - addresses + Ok(addresses) } } } diff --git a/sn_node/tests/data_with_churn.rs b/sn_node/tests/data_with_churn.rs index 42e5bdc9aa..96f1482e79 100644 --- a/sn_node/tests/data_with_churn.rs +++ b/sn_node/tests/data_with_churn.rs @@ -494,7 +494,7 @@ fn churn_nodes_task( ) { let start = Instant::now(); let _handle = tokio::spawn(async move { - let node_rpc_addresses = get_all_rpc_addresses(); + let node_rpc_addresses = get_all_rpc_addresses().expect("Failed to obtain rpc addresses"); 'main: loop { for rpc_address in node_rpc_addresses.iter() { sleep(churn_period).await; diff --git a/sn_node/tests/msgs_over_gossipsub.rs b/sn_node/tests/msgs_over_gossipsub.rs index fcc01f5eb4..ebc587a0be 100644 --- a/sn_node/tests/msgs_over_gossipsub.rs +++ b/sn_node/tests/msgs_over_gossipsub.rs @@ -33,7 +33,7 @@ async fn msgs_over_gossipsub() -> Result<()> { let node_count = get_node_count(); let nodes_subscribed = node_count / 2; // 12 out of 25 nodes will be subscribers - let node_rpc_addresses = get_all_rpc_addresses() + let node_rpc_addresses = get_all_rpc_addresses()? .into_iter() .enumerate() .collect::>(); diff --git a/sn_node/tests/nodes_rewards.rs b/sn_node/tests/nodes_rewards.rs index 5edc6f7a81..febff5f621 100644 --- a/sn_node/tests/nodes_rewards.rs +++ b/sn_node/tests/nodes_rewards.rs @@ -187,7 +187,7 @@ async fn nodes_rewards_transfer_notifs_filter() -> Result<()> { let (files_api, _content_bytes, _content_addr, chunks) = random_content(&client, paying_wallet_dir.to_path_buf(), chunks_dir.path())?; - let node_rpc_addresses = get_all_rpc_addresses(); + let node_rpc_addresses = get_all_rpc_addresses()?; // this node shall receive the notifications since we set the correct royalties pk as filter let royalties_pk = NETWORK_ROYALTIES_PK.public_key(); diff --git a/sn_node/tests/verify_data_location.rs b/sn_node/tests/verify_data_location.rs index 533e79c2a3..93c5fe2665 100644 --- a/sn_node/tests/verify_data_location.rs +++ b/sn_node/tests/verify_data_location.rs @@ -91,7 +91,7 @@ async fn verify_data_location() -> Result<()> { "Performing data location verification with a churn count of {churn_count} and n_chunks {chunk_count}\nIt will take approx {:?}", VERIFICATION_DELAY*churn_count as u32 ); - let node_rpc_address = get_all_rpc_addresses(); + let node_rpc_address = get_all_rpc_addresses()?; let mut all_peers = get_all_peer_ids(&node_rpc_address).await?; // Store chunks diff --git a/sn_node/tests/verify_routing_table.rs b/sn_node/tests/verify_routing_table.rs index 9b936a2554..96c92e6182 100644 --- a/sn_node/tests/verify_routing_table.rs +++ b/sn_node/tests/verify_routing_table.rs @@ -42,7 +42,7 @@ async fn verify_routing_table() -> Result<()> { println!("Sleeping for {sleep_duration:?} before verification"); tokio::time::sleep(sleep_duration).await; - let node_rpc_address = get_all_rpc_addresses(); + let node_rpc_address = get_all_rpc_addresses()?; let all_peers = get_all_peer_ids(&node_rpc_address).await?; let mut all_failed_list = BTreeMap::new(); diff --git a/sn_node_rpc_client/src/main.rs b/sn_node_rpc_client/src/main.rs index a2d75e21ed..62ad3f28a0 100644 --- a/sn_node_rpc_client/src/main.rs +++ b/sn_node_rpc_client/src/main.rs @@ -17,7 +17,7 @@ use libp2p::Multiaddr; use sn_client::Client; use sn_logging::LogBuilder; use sn_node::{NodeEvent, ROYALTY_TRANSFER_NOTIF_TOPIC}; -use sn_peers_acquisition::{parse_peers_args, PeersArgs}; +use sn_peers_acquisition::{get_peers_from_args, PeersArgs}; use sn_protocol::safenode_proto::{ safe_node_client::SafeNodeClient, GossipsubSubscribeRequest, NodeEventsRequest, TransferNotifsFilterRequest, @@ -134,7 +134,7 @@ async fn main() -> Result<()> { log_cash_notes, peers, } => { - let bootstrap_peers = parse_peers_args(peers).await?; + let bootstrap_peers = get_peers_from_args(peers).await?; let bootstrap_peers = if bootstrap_peers.is_empty() { // empty vec is returned if `local-discovery` flag is provided None diff --git a/sn_peers_acquisition/Cargo.toml b/sn_peers_acquisition/Cargo.toml index 57a22b686d..f8359f3c29 100644 --- a/sn_peers_acquisition/Cargo.toml +++ b/sn_peers_acquisition/Cargo.toml @@ -17,10 +17,10 @@ network-contacts = ["reqwest", "tokio", "url"] [dependencies] clap = { version = "4.2.1", features = ["derive", "env"] } -color-eyre = "~0.6" libp2p = { version="0.53", features = [] } rand = "0.8.5" reqwest = { version="0.11.18", default-features=false, features = ["rustls-tls"], optional = true } +thiserror = "1.0.23" tokio = { version = "1.32.0", optional = true} tracing = { version = "~0.1.26" } url = { version = "2.4.0", optional = true } diff --git a/sn_peers_acquisition/src/error.rs b/sn_peers_acquisition/src/error.rs new file mode 100644 index 0000000000..9f3fec99eb --- /dev/null +++ b/sn_peers_acquisition/src/error.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Could not parse the supplied multiaddr or socket address")] + InvalidPeerAddr, + #[error("Could not obtain network contacts from {0} after {1} retries")] + NetworkContactsUnretrievable(String, usize), + #[error("No valid multaddr was present in the contacts file at {0}")] + NoMultiAddrObtainedFromNetworkContacts(String), + #[error("Could not obtain peers through any available options")] + PeersNotObtained, + #[cfg(feature = "network-contacts")] + #[error(transparent)] + ReqwestError(#[from] reqwest::Error), + #[cfg(feature = "network-contacts")] + #[error(transparent)] + UrlParseError(#[from] url::ParseError), +} diff --git a/sn_peers_acquisition/src/lib.rs b/sn_peers_acquisition/src/lib.rs index 9eca019c07..674c84fe9e 100644 --- a/sn_peers_acquisition/src/lib.rs +++ b/sn_peers_acquisition/src/lib.rs @@ -6,10 +6,10 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. +pub mod error; + +use crate::error::{Error, Result}; use clap::Args; -#[cfg(feature = "network-contacts")] -use color_eyre::eyre::Context; -use color_eyre::{eyre::eyre, Result}; use libp2p::{multiaddr::Protocol, Multiaddr}; use rand::{seq::SliceRandom, thread_rng}; use tracing::*; @@ -29,47 +29,65 @@ pub const SAFE_PEERS_ENV: &str = "SAFE_PEERS"; #[derive(Args, Debug)] pub struct PeersArgs { + /// Set to indicate this is the first node in a new network + /// + /// If this argument is used, any others will be ignored because they do not apply to the first + /// node. + #[clap(long)] + first: bool, /// Peer(s) to use for bootstrap, in a 'multiaddr' format containing the peer ID. /// - /// A multiaddr looks like '/ip4/1.2.3.4/tcp/1200/tcp/p2p/12D3KooWRi6wF7yxWLuPSNskXc6kQ5cJ6eaymeMbCRdTnMesPgFx' - /// where `1.2.3.4` is the IP, `1200` is the port and the (optional) last part is the peer ID. + /// A multiaddr looks like + /// '/ip4/1.2.3.4/tcp/1200/tcp/p2p/12D3KooWRi6wF7yxWLuPSNskXc6kQ5cJ6eaymeMbCRdTnMesPgFx' where + /// `1.2.3.4` is the IP, `1200` is the port and the (optional) last part is the peer ID. /// /// This argument can be provided multiple times to connect to multiple peers. /// - /// Peers can also be provided by an environment variable (see below), but the - /// command-line argument (`--peer`) takes precedence. To pass multiple peers with the - /// environment variable, separate them with commas. - #[clap(long = "peer", value_name = "multiaddr", value_delimiter = ',', value_parser = parse_peer_addr)] + /// Alternatively, the `SAFE_PEERS` environment variable can provide a comma-separated peer + /// list. + /// + /// If both the `--peer` argument and `SAFE_PEERS` environment variables are used, the + /// specified peers will be combined. + #[clap(long = "peer", value_name = "multiaddr", value_delimiter = ',', value_parser = parse_peer_addr, conflicts_with = "first")] pub peers: Vec, /// Specify the URL to fetch the network contacts from. /// - /// This argument will be overridden if the "peers" argument is set or if the `local-discovery` feature flag is - /// enabled. + /// This argument will be overridden if the "peers" argument is set or if the `local-discovery` + /// feature flag is enabled. #[cfg(feature = "network-contacts")] - #[clap(long)] + #[clap(long, conflicts_with = "first")] pub network_contacts_url: Option, } -/// Parses PeersArgs +/// Gets the peers based on the arguments provided. +/// +/// If the `--first` flag is used, no peers will be provided. /// -/// The order of precedence for the bootstrap peers are `--peer` arg, `SAFE_PEERS` env variable, `local-discovery` flag -/// and `network-contacts` flag respectively. The later ones are ignored if one of the prior option is used. -pub async fn parse_peers_args(args: PeersArgs) -> Result> { +/// Otherwise, peers are obtained in the following order of precedence: +/// * The `--peer` argument. +/// * The `SAFE_PEERS` environment variable. +/// * Using the `local-discovery` feature, which will return an empty peer list. +/// * Using the `network-contacts` feature, which will download the peer list from a file on S3. +/// +/// Note: the current behaviour is that `--peer` and `SAFE_PEERS` will be combined. Some tests +/// currently rely on this. We will change it soon. +pub async fn get_peers_from_args(args: PeersArgs) -> Result> { + if args.first { + return Ok(vec![]); + } + let mut peers = if !args.peers.is_empty() { - info!("Using passed peers or SAFE_PEERS env variable"); + info!("Using peers supplied with the --peer argument(s)"); args.peers } else if cfg!(feature = "local-discovery") { - info!("No peers given. As `local-discovery` feature is enabled, we will be attempt to connect to the network using mDNS."); + info!("No peers given"); + info!( + "The `local-discovery` feature is enabled, so peers will be discovered through mDNS." + ); return Ok(vec![]); } else if cfg!(feature = "network-contacts") { - match get_network_contacts(&args).await { - Ok(peers) => peers, - Err(err) => { - println!("Error {err:?} while fetching bootstrap peers from Network contacts URL"); - vec![] - } - } + get_network_contacts(&args).await? } else { vec![] }; @@ -85,9 +103,8 @@ pub async fn parse_peers_args(args: PeersArgs) -> Result> { } if peers.is_empty() { - let err_str = "No peers given, 'local-discovery' and 'network-contacts' feature flags are disabled. We cannot connect to the network."; - error!("{err_str}"); - return Err(color_eyre::eyre::eyre!("{err_str}")); + error!("Peers not obtained through any available options"); + return Err(Error::PeersNotObtained); }; // Randomly sort peers before we return them to avoid overly hitting any one peer @@ -113,9 +130,7 @@ async fn get_network_contacts(args: &PeersArgs) -> Result> { .network_contacts_url .clone() .unwrap_or(Url::parse(NETWORK_CONTACTS_URL)?); - get_bootstrap_peers_from_url(url) - .await - .wrap_err("Error while fetching bootstrap peers from Network contacts URL") + get_bootstrap_peers_from_url(url).await } /// Parse strings like `1.2.3.4:1234` and `/ip4/1.2.3.4/tcp/1234` into a (TCP) multiaddr. @@ -138,7 +153,7 @@ pub fn parse_peer_addr(addr: &str) -> Result { return Ok(addr); } - Err(eyre!("invalid multiaddr or socket address")) + Err(Error::InvalidPeerAddr) } #[cfg(feature = "network-contacts")] @@ -171,24 +186,26 @@ async fn get_bootstrap_peers_from_url(url: Url) -> Result> { trace!("Successfully got bootstrap peers from URL {multi_addresses:?}"); return Ok(multi_addresses); } else { - return Err(color_eyre::eyre::eyre!( - "Could not obtain a single valid multi-addr from URL {NETWORK_CONTACTS_URL}" + return Err(Error::NoMultiAddrObtainedFromNetworkContacts( + NETWORK_CONTACTS_URL.to_string(), )); } } else { retries += 1; if retries >= MAX_NETWORK_CONTACTS_GET_RETRIES { - return Err(color_eyre::eyre::eyre!( - "Could not GET network contacts from {NETWORK_CONTACTS_URL} after {MAX_NETWORK_CONTACTS_GET_RETRIES} retries", + return Err(Error::NetworkContactsUnretrievable( + NETWORK_CONTACTS_URL.to_string(), + MAX_NETWORK_CONTACTS_GET_RETRIES, )); } } } - Err(err) => { + Err(_) => { retries += 1; if retries >= MAX_NETWORK_CONTACTS_GET_RETRIES { - return Err(color_eyre::eyre::eyre!( - "Failed to perform request to {NETWORK_CONTACTS_URL} after {MAX_NETWORK_CONTACTS_GET_RETRIES} retries due to: {err:?}" + return Err(Error::NetworkContactsUnretrievable( + NETWORK_CONTACTS_URL.to_string(), + MAX_NETWORK_CONTACTS_GET_RETRIES, )); } } diff --git a/sn_testnet/src/lib.rs b/sn_testnet/src/lib.rs index 95919aebde..9481162d05 100644 --- a/sn_testnet/src/lib.rs +++ b/sn_testnet/src/lib.rs @@ -233,6 +233,7 @@ impl Testnet { let rpc_address = "127.0.0.1:12001".parse()?; let mut launch_args = self.get_launch_args("safenode-1", Some(rpc_address), node_args)?; + launch_args.push("--first".to_string()); let genesis_port: u16 = 11101; launch_args.push("--port".to_string()); @@ -443,6 +444,7 @@ mod test { rpc_address.to_string(), "--log-format".to_string(), "json".to_string(), + "--first".to_string(), "--port".to_string(), "11101".to_string(), ]), @@ -516,6 +518,7 @@ mod test { rpc_address.to_string(), "--log-format".to_string(), "json".to_string(), + "--first".to_string(), "--port".to_string(), "11101".to_string(), ]),