diff options
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | src/config/mod.rs | 51 | ||||
| -rw-r--r-- | src/main.rs | 59 |
3 files changed, 97 insertions, 15 deletions
@@ -5,6 +5,8 @@ edition = "2024" [features] tokio-console = ["dep:console-subscriber"] +# The libp2p transport (`--transports libp2p`). +libp2p = [] [dependencies] anyhow = "1.0" diff --git a/src/config/mod.rs b/src/config/mod.rs index 811ad57..21a8733 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -236,6 +236,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ "Chunk size when serving large transfers to other nodes (e.g. an epoch's snapshot to a rejoining node), 1KiB to 4MiB.", ), option( + "transports", + Kind::List, + "NAME,[NAME...]", + Some("mycelium"), + "The transports this node uses, in order of preference: mycelium, libp2p (in builds with the libp2p feature). A peer is reached on the first one both have; nodes on several carry the traffic between nodes that share none.", + ), + option( "mycelium-rest-url", Kind::Text, "SCHEME://HOSTNAME:PORT", @@ -342,6 +349,8 @@ pub(crate) struct Settings { /// In bytes. #[serde(default = "default_blob_chunk", with = "crate::units::size_option")] pub(crate) blob_chunk: u64, + #[serde(default = "default_transports")] + pub(crate) transports: Vec<String>, pub(crate) mycelium_rest_url: Option<String>, pub(crate) mycelium_key_file: Option<PathBuf>, pub(crate) mycelium_network_key_file: Option<PathBuf>, @@ -373,6 +382,12 @@ fn default_electorate_window() -> Duration { fn default_blob_chunk() -> u64 { 64 * 1024 } +fn default_transports() -> Vec<String> { + vec!["mycelium".to_owned()] +} +/// The transports `transports` may name, and whether this build has each. +pub(crate) const TRANSPORTS: &[(&str, bool)] = + &[("mycelium", true), ("libp2p", cfg!(feature = "libp2p"))]; fn default_mycelium_peers() -> Vec<String> { vec!["tls://127.0.0.1:9651".to_owned()] } @@ -434,6 +449,28 @@ impl Settings { if self.genesis && self.recover_from.is_some() { bail!("recover-from starts the swarm from a backup; it can't be combined with genesis"); } + if self.transports.is_empty() { + bail!("transports: a node needs at least one"); + } + for (index, name) in self.transports.iter().enumerate() { + match TRANSPORTS.iter().find(|(known, _)| known == name) { + None => bail!( + "transports: unknown transport {name:?} (known: {})", + TRANSPORTS + .iter() + .map(|(known, _)| *known) + .collect::<Vec<_>>() + .join(", ") + ), + Some((_, false)) => { + bail!("transports: this build has no {name} (built without the {name} feature)") + } + Some(_) if self.transports[..index].contains(name) => { + bail!("transports: {name} is named twice") + } + Some(_) => {} + } + } if self.debug_epoch.is_some() && !self.debug_api { bail!("debug-epoch needs debug-api"); } @@ -1354,6 +1391,20 @@ mod tests { ) .is_err() ); + let transports = |names: JsonValue| { + resolve( + layer(json!({ "htpasswd": "u", "node-key-file": "node.key", "transports": names })), + Vec::new(), + ) + }; + assert!(transports(json!([])).is_err()); + assert!(transports(json!(["pigeon"])).is_err()); + assert!(transports(json!(["mycelium", "mycelium"])).is_err()); + assert!(transports(json!(["mycelium"])).is_ok()); + assert_eq!( + transports(json!(["libp2p", "mycelium"])).is_ok(), + cfg!(feature = "libp2p") + ); } #[test] diff --git a/src/main.rs b/src/main.rs index b5e7fc6..435f83a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,7 +69,8 @@ async fn main() -> Result<()> { // A broken htpasswd will abort startup. let auth_state = AuthState::new(&args.htpasswd)?; - let mycelium_setup = { + let uses = |name: &str| args.transports.iter().any(|transport| transport == name); + let mut mycelium_setup = if uses("mycelium") { let spawn_config = MyceliumSpawnConfig { rest_url_opt: args.mycelium_rest_url(), key_file_opt: args.mycelium_key_file.clone(), @@ -78,22 +79,24 @@ async fn main() -> Result<()> { tcp_port: args.mycelium_tcp_listen_port, peers: args.mycelium_peers.clone(), }; - tokio::select! { + let setup = tokio::select! { setup = MyceliumSetup::bootstrap(spawn_config) => setup?, _ = stopped(stop.clone()) => return Ok(()), - } + }; + // Without a password it may carry (the option is secret). + let mut shown_url = setup.rest_url.clone(); + let _ = shown_url.set_password(None); + debug!(url = shown_url.to_string(), "Using Mycelium REST API"); + Some(setup) + } else { + None }; - let mycelium_rest_url = mycelium_setup.rest_url.clone(); - info!( bind_addr = args.bind_addr.to_string(), + transports = ?args.transports, "Starting replication service" ); - // Without a password it may carry (the option is secret). - let mut shown_url = mycelium_rest_url.clone(); - let _ = shown_url.set_password(None); - debug!(url = shown_url.to_string(), "Using Mycelium REST API"); let force = force::Force::parse(args.force.iter().map(String::as_str), config::FORCE) .map_err(anyhow::Error::msg)?; @@ -160,9 +163,35 @@ async fn main() -> Result<()> { }; debug!(epoch = %initial_epoch, "Local epoch"); - let mycelium_transport = Arc::new(mycelium::MyceliumTransport::connect(mycelium_setup).await?); - debug!(address = mycelium_transport.local_id(), "Transport ready"); - let delivery_limit = mycelium_transport.delivery_limit(); + // In the order of preference (`--transports`, checked when loading). + let mut transports: Vec<(&'static str, Arc<dyn Transport>)> = Vec::new(); + for name in &args.transports { + let transport: (&'static str, Arc<dyn Transport>) = match name.as_str() { + "mycelium" => { + let setup = mycelium_setup + .take() + .context("Mycelium named twice in transports")?; + ( + "mycelium", + Arc::new(mycelium::MyceliumTransport::connect(setup).await?), + ) + } + other => anyhow::bail!("transports: no {other} in this build"), + }; + debug!( + transport = transport.0, + address = transport.1.local_id(), + "Transport ready" + ); + transports.push(transport); + } + let delivery_limit = transports + .iter() + .map(|(_, transport)| transport.delivery_limit()) + .max() + .unwrap_or_default(); + let mut transports = transports.into_iter(); + let (scheme, transport) = transports.next().context("No transports")?; let document_file = args.document_file.clone().map(|path| { Arc::new(document::file::DocumentFile::new( @@ -173,11 +202,11 @@ async fn main() -> Result<()> { )) }); let node = node::Node::new(node::NodeConfig { - transport: mycelium_transport, + transport, identity: Some(transport::identity::IdentityConfig { key: Arc::new(node_key), - scheme: "mycelium", - more: Vec::new(), + scheme, + more: transports.collect(), }), initial_epoch, initial_doc, |
