diff --git a/bin/florestad/src/cli.rs b/bin/florestad/src/cli.rs index 855e4dc6f..2e778f38b 100644 --- a/bin/florestad/src/cli.rs +++ b/bin/florestad/src/cli.rs @@ -98,11 +98,11 @@ pub struct Cli { pub zmq_address: Option, #[arg(long, value_name = "address[:]")] - /// A node to connect to + /// A node to connect to. May be specified multiple times. /// - /// If this option is provided, we'll connect **only** to this node. It should be an ipv4 - /// address in the format `
[:]`. - pub connect: Option, + /// If this option is provided, we'll connect **only** to the listed nodes. Each value + /// should be an ipv4/ipv6/hostname address in the format `
[:]`. + pub connect: Vec, #[arg(long, value_name = "address[:]")] /// The address where our json-rpc server should listen to, in the format `
[:]` diff --git a/bin/florestad/src/main.rs b/bin/florestad/src/main.rs index fbe779cfe..66280b3c4 100644 --- a/bin/florestad/src/main.rs +++ b/bin/florestad/src/main.rs @@ -60,7 +60,7 @@ fn main() { let config = Config { datadir, - disable_dns_seeds: params.connect.is_some() || params.disable_dns_seeds, + disable_dns_seeds: !params.connect.is_empty() || params.disable_dns_seeds, network: params.network, debug: params.debug, cfilters: !params.no_cfilters, diff --git a/crates/floresta-node/src/florestad.rs b/crates/floresta-node/src/florestad.rs index ba0fd989d..8d96677c9 100644 --- a/crates/floresta-node/src/florestad.rs +++ b/crates/floresta-node/src/florestad.rs @@ -157,10 +157,10 @@ pub struct Config { /// is the address that we'll listen for incoming connections. pub zmq_address: Option, - /// A node to connect to + /// Nodes to connect to /// - /// If this option is provided, we'll connect **only** to this node. - pub connect: Option, + /// If non-empty, we'll connect **only** to these nodes. + pub connect: Vec, #[cfg(feature = "json-rpc")] /// The address our json-rpc should listen to @@ -238,7 +238,7 @@ impl Config { filters_start_height: None, #[cfg(feature = "zmq-server")] zmq_address: None, - connect: None, + connect: Vec::new(), #[cfg(feature = "json-rpc")] json_rpc_address: None, log_to_stdout: false, @@ -412,7 +412,7 @@ impl Florestad { pow_fraud_proofs: false, proxy, datadir: datadir.into(), - fixed_peer: self.config.connect.clone(), + fixed_peers: self.config.connect.clone(), compact_filters: self.config.cfilters, assume_utreexo: self.config.assumeutreexo_value.clone().or(assume_utreexo), backfill: self.config.backfill, diff --git a/crates/floresta-wire/src/p2p_wire/mod.rs b/crates/floresta-wire/src/p2p_wire/mod.rs index d622ec617..747ccf5a7 100644 --- a/crates/floresta-wire/src/p2p_wire/mod.rs +++ b/crates/floresta-wire/src/p2p_wire/mod.rs @@ -27,11 +27,11 @@ pub struct UtreexoNodeConfig { /// needing to download the whole chain. It will download ~1GB of filters, and then /// download the blocks that match the filters. pub compact_filters: bool, - /// Fixed peers to connect to. Defaults to None. + /// Fixed peers to connect to. Defaults to an empty list. /// - /// If you want to connect to a specific peer, you can set this to a string with the - /// format `ip:port`. For example, `localhost:8333`. - pub fixed_peer: Option, + /// Each entry is `host[:port]`, where `host` is an IPv4 address, a bracketed IPv6 address (`[::1]`), or a hostname; + /// `port` is optional and defaults to the network's default port (for example, `"localhost"` or `"127.0.0.1:8333"`). + pub fixed_peers: Vec, /// Maximum ban score. Defaults to 100. /// /// If a peer misbehaves, we increase its ban score. If the ban score reaches this value, @@ -75,7 +75,7 @@ impl Default for UtreexoNodeConfig { network: Network::Bitcoin, pow_fraud_proofs: false, compact_filters: false, - fixed_peer: None, + fixed_peers: Vec::new(), max_banscore: 100, datadir: ".floresta-node".into(), proxy: None, diff --git a/crates/floresta-wire/src/p2p_wire/node/chain_selector_ctx.rs b/crates/floresta-wire/src/p2p_wire/node/chain_selector_ctx.rs index 6387012a8..b5d4bd86c 100644 --- a/crates/floresta-wire/src/p2p_wire/node/chain_selector_ctx.rs +++ b/crates/floresta-wire/src/p2p_wire/node/chain_selector_ctx.rs @@ -823,8 +823,8 @@ where return true; } - if self.fixed_peer.is_some() && connected_peers >= 1 { - return true; + if self.has_fixed_peers() { + return connected_peers >= 1; } connected_peers >= ChainSelector::MAX_OUTGOING_PEERS diff --git a/crates/floresta-wire/src/p2p_wire/node/conn.rs b/crates/floresta-wire/src/p2p_wire/node/conn.rs index 99ba5cca6..a7eac780d 100644 --- a/crates/floresta-wire/src/p2p_wire/node/conn.rs +++ b/crates/floresta-wire/src/p2p_wire/node/conn.rs @@ -62,10 +62,11 @@ where /// Create a new outgoing connection, selecting an appropriate peer address. /// - /// If a fixed peer is set via the `--connect` CLI argument, its connection - /// kind will always be coerced to [`ConnectionKind::Manual`]. Otherwise, - /// an address is selected from the [`AddressMan`] based on the required - /// [`ServiceFlags`] for the given `connection_kind`. + /// If fixed peers are set via the `--connect` CLI argument, their connection + /// kind will always be coerced to [`ConnectionKind::Manual`] and the first + /// not-yet-connected fixed peer is selected. Otherwise, an address is + /// selected from the [`AddressMan`] based on the required [`ServiceFlags`] + /// for the given `connection_kind`. /// /// If no address is available and the kind is not [`ConnectionKind::Manual`], /// hardcoded addresses are loaded into the [`AddressMan`] as a fallback. @@ -74,28 +75,36 @@ where mut conn_kind: ConnectionKind, ) -> Result<(), WireError> { // Set the fixed peer's connection kind to manual, if set. - if self.fixed_peer.is_some() { + if self.has_fixed_peers() { conn_kind = ConnectionKind::Manual; } // Get the peer's `ServiceFlags`. let required_services = match conn_kind { ConnectionKind::Regular(services) => services, - _ => ServiceFlags::NONE, + ConnectionKind::Feeler | ConnectionKind::Extra | ConnectionKind::Manual => { + ServiceFlags::NONE + } }; - // Get the fixed peer's `peer_id` and `LocalAddress` if set, - // or fetch a new address from the address manager. - let candidate_peer = self - .fixed_peer - .as_ref() - .map(|addr| (0, addr.clone())) - .or_else(|| { - self.address_man.get_address_to_connect( - required_services, - matches!(conn_kind, ConnectionKind::Feeler), - ) - }); + // Pick the first fixed peer that we are not already connected to, or + // fall back to fetching a new address from the address manager when no + // fixed peers were configured. + let candidate_peer = if self.has_fixed_peers() { + self.fixed_peers + .iter() + .find(|addr| { + self.peers + .values() + .all(|p| p.address != addr.get_net_address() || p.port != addr.get_port()) + }) + .map(|addr| (0, addr.clone())) + } else { + self.address_man.get_address_to_connect( + required_services, + matches!(conn_kind, ConnectionKind::Feeler), + ) + }; // Load hardcoded addresses to the address manager if no fixed or manual peers exist. let Some((peer_id, peer_address)) = candidate_peer else { @@ -142,7 +151,7 @@ where pub(crate) fn open_feeler_connection(&mut self) -> Result<(), WireError> { // No feeler if `--connect` is set - if self.fixed_peer.is_some() { + if self.has_fixed_peers() { return Ok(()); } @@ -552,7 +561,7 @@ where /// can't find a Utreexo peer in a context we need them. This function /// won't do anything if `--connect` was used fn maybe_use_hardcoded_addresses(&mut self) { - if self.fixed_peer.is_some() { + if self.has_fixed_peers() { return; } @@ -612,11 +621,14 @@ where let connection_kind = ConnectionKind::Regular(required_service); - // If the user passes in a `--connect` cli argument, we only connect with - // that particular peer. - if self.fixed_peer.is_some() { - if self.peers.is_empty() { - self.create_connection(connection_kind)?; + // If the user passes in `--connect` cli arguments, we only connect with + // those peers. Try to (re)connect as many as we are missing. + if self.has_fixed_peers() { + let missing = self.fixed_peers.len().saturating_sub(self.peers.len()); + for _ in 0..missing { + if let Err(e) = self.create_connection(connection_kind) { + debug!("Failed to connect to fixed peer: {e:?}"); + } } return Ok(()); } diff --git a/crates/floresta-wire/src/p2p_wire/node/mod.rs b/crates/floresta-wire/src/p2p_wire/node/mod.rs index ec5b7fc4c..0933d3097 100644 --- a/crates/floresta-wire/src/p2p_wire/node/mod.rs +++ b/crates/floresta-wire/src/p2p_wire/node/mod.rs @@ -15,6 +15,7 @@ mod user_req; use core::fmt::Debug; use core::net::IpAddr; use std::collections::HashMap; +use std::collections::HashSet; use std::ops::Deref; use std::ops::DerefMut; use std::path::PathBuf; @@ -270,7 +271,7 @@ pub struct NodeCommon { // 4. Networking Configuration pub(crate) socks5: Option, - pub(crate) fixed_peer: Option, + pub(crate) fixed_peers: Vec, // 5. Time and Event Tracking pub(crate) inflight: HashMap, @@ -348,11 +349,15 @@ where let (node_tx, node_rx) = unbounded_channel(); let socks5 = config.proxy.map(Socks5StreamBuilder::new); - let fixed_peer = config - .fixed_peer - .as_ref() - .map(|address| Self::resolve_connect_host(address, Self::get_port(config.network))) - .transpose()?; + // Dedup the resolved fixed peers so we don't open multiple connections to the same host. + let mut seen = HashSet::new(); + let mut fixed_peers = Vec::with_capacity(config.fixed_peers.len()); + for address in &config.fixed_peers { + let resolved = Self::resolve_connect_host(address, Self::get_port(config.network))?; + if seen.insert((resolved.get_net_address(), resolved.get_port())) { + fixed_peers.push(resolved); + } + } Ok(UtreexoNode { common: NodeCommon { @@ -386,7 +391,7 @@ where datadir: config.datadir.clone(), max_banscore: config.max_banscore, socks5, - fixed_peer, + fixed_peers, config, kill_signal, added_peers: Vec::new(), diff --git a/crates/floresta-wire/src/p2p_wire/node/peer_man.rs b/crates/floresta-wire/src/p2p_wire/node/peer_man.rs index 88c0c7dfd..5def200eb 100644 --- a/crates/floresta-wire/src/p2p_wire/node/peer_man.rs +++ b/crates/floresta-wire/src/p2p_wire/node/peer_man.rs @@ -101,6 +101,14 @@ where Some((id, peer)) } + /// Whether the node was configured with a fixed peer list (via `--connect`). + /// + /// When this is true, we connect *only* to those peers and skip the usual + /// peer-discovery paths (address manager, feelers, hardcoded fallback). + pub(crate) fn has_fixed_peers(&self) -> bool { + !self.fixed_peers.is_empty() + } + /// Returns how many connected peers we have. /// /// This function will only count peers that completed handshake and are ready diff --git a/crates/floresta-wire/src/p2p_wire/node/sync_ctx.rs b/crates/floresta-wire/src/p2p_wire/node/sync_ctx.rs index 58bf242c0..753c22a1f 100644 --- a/crates/floresta-wire/src/p2p_wire/node/sync_ctx.rs +++ b/crates/floresta-wire/src/p2p_wire/node/sync_ctx.rs @@ -141,6 +141,13 @@ where /// - we have enough peers to download blocks from (at most `MAX_OUTGOING_PEERS`) /// - if some of peers are too slow, and potentially stalling our block download (TODO) fn check_connections(&mut self) -> Result<(), WireError> { + // With `--connect`, the user pins us to a specific peer set; don't search + // for extra utreexo or network peers. `maybe_open_connection` still + // reconnects any fixed peer that has dropped. + if self.has_fixed_peers() { + return self.maybe_open_connection(service_flags::UTREEXO.into()); + } + let total_peers = self.connected_peers(); let utreexo_peers = self .peer_by_service diff --git a/crates/floresta-wire/src/p2p_wire/tests/utils.rs b/crates/floresta-wire/src/p2p_wire/tests/utils.rs index 4027ce6be..528730c8b 100644 --- a/crates/floresta-wire/src/p2p_wire/tests/utils.rs +++ b/crates/floresta-wire/src/p2p_wire/tests/utils.rs @@ -345,7 +345,7 @@ pub async fn setup_node( // Add a fixed peer to avoid opening real P2P connections if i == 0 { - node.fixed_peer = Some(to_addr_v2(peer.address).into()); + node.fixed_peers = vec![to_addr_v2(peer.address).into()]; } node.peers.insert(peer_id, peer);