diff --git a/Cargo.lock b/Cargo.lock index 968175b72484..f325dd969ad2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3254,6 +3254,7 @@ dependencies = [ "glob", "group", "hex", + "hickory-resolver", "http 1.1.0", "http-range-header", "human-repr", diff --git a/Cargo.toml b/Cargo.toml index 509bd4fecb05..15e76f051868 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ gethostname = "0.5" git-version = "0.3" group = "0.13" hex = { version = "0.4", features = ["serde"] } +hickory-resolver = { version = "0.24", default-features = false, features = ["system-config"] } http = "1" human-repr = "1" human_bytes = "0.4" diff --git a/build/bootstrap/butterflynet b/build/bootstrap/butterflynet index 1d483036fb61..8eef6793e1a4 100644 --- a/build/bootstrap/butterflynet +++ b/build/bootstrap/butterflynet @@ -1,2 +1 @@ -/dns4/bootstrap-0.butterfly.fildev.network/tcp/1347/p2p/12D3KooWHSojyKBpM7phx5jj9myYpmbVQ9n1MjQZmpSa7NL2RxwX -/dns4/bootstrap-1.butterfly.fildev.network/tcp/1347/p2p/12D3KooWKzxVysUt8FNEihBhWoN86BYnXh22GpEoYRKUAhXJtApt +/dnsaddr/bootstrap.butterfly.fildev.network diff --git a/src/libp2p/behaviour.rs b/src/libp2p/behaviour.rs index b0a3763862af..082869a3fca8 100644 --- a/src/libp2p/behaviour.rs +++ b/src/libp2p/behaviour.rs @@ -65,7 +65,7 @@ impl Recorder for Metrics { } impl ForestBehaviour { - pub fn new( + pub async fn new( local_key: &Keypair, config: &Libp2pConfig, network_name: &str, @@ -122,7 +122,8 @@ impl ForestBehaviour { let discovery = DiscoveryConfig::new(local_key.public(), network_name) .with_mdns(config.mdns) .with_kademlia(config.kademlia) - .with_user_defined(config.bootstrap_peers.clone())? + .with_user_defined(config.bootstrap_peers.clone()) + .await? .target_peer_count(config.target_peer_count as u64) .finish()?; diff --git a/src/libp2p/discovery.rs b/src/libp2p/discovery.rs index 291189b73fd8..e4331affbf1c 100644 --- a/src/libp2p/discovery.rs +++ b/src/libp2p/discovery.rs @@ -1,9 +1,11 @@ // Copyright 2019-2024 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use core::str; use std::{ cmp, collections::VecDeque, + io, task::{Context, Poll}, time::Duration, }; @@ -94,12 +96,20 @@ impl<'a> DiscoveryConfig<'a> { } /// Set custom nodes which never expire, e.g. bootstrap or reserved nodes. - pub fn with_user_defined( + pub async fn with_user_defined( mut self, user_defined: impl IntoIterator, ) -> anyhow::Result { for mut addr in user_defined.into_iter() { - if let Some(Protocol::P2p(peer_id)) = addr.pop() { + if let Some((_, Protocol::Dnsaddr(addr))) = addr + .iter() + .enumerate() + .find(|(_, p)| matches!(p, Protocol::Dnsaddr(_))) + { + for pair in resolve_libp2p_dnsaddr(&addr).await? { + self.user_defined.push(pair) + } + } else if let Some(Protocol::P2p(peer_id)) = addr.pop() { self.user_defined.push((peer_id, addr)) } else { anyhow::bail!("Failed to parse peer id from {addr}") @@ -527,16 +537,78 @@ impl NetworkBehaviour for DiscoveryBehaviour { } } +async fn resolve_libp2p_dnsaddr(name: &str) -> anyhow::Result> { + use hickory_resolver::{system_conf, TokioAsyncResolver}; + + let (cfg, opts) = system_conf::read_system_conf()?; + let resolver = TokioAsyncResolver::tokio(cfg, opts); + + let name = ["_dnsaddr.", name].concat(); + let txts = resolver.txt_lookup(name).await?; + + let mut pairs = vec![]; + for txt in txts { + if let Some(chars) = txt.txt_data().first() { + match parse_dnsaddr_txt(chars) { + Err(e) => { + // Skip over seemingly invalid entries. + tracing::debug!("Invalid TXT record: {:?}", e); + } + Ok(mut addr) => { + if let Some(Protocol::P2p(peer_id)) = addr.pop() { + pairs.push((peer_id, addr)) + } else { + tracing::debug!("Failed to parse peer id from {addr}") + } + } + } + } + } + Ok(pairs) +} + +/// Parses a `` of a `dnsaddr` `TXT` record. +fn parse_dnsaddr_txt(txt: &[u8]) -> io::Result { + let s = str::from_utf8(txt).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + match s.strip_prefix("dnsaddr=") { + None => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Missing `dnsaddr=` prefix.", + )), + Some(a) => Ok( + Multiaddr::try_from(a).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? + ), + } +} + #[cfg(test)] mod tests { - use libp2p::{identity::Keypair, swarm::SwarmEvent, Swarm}; + use super::*; + use libp2p::{ + core::transport::MemoryTransport, identity::Keypair, swarm::SwarmEvent, Swarm, + Transport as _, + }; use libp2p_swarm_test::SwarmExt as _; + use std::str::FromStr as _; - use super::*; + #[tokio::test] + async fn resolve_libp2p_dnsaddr_test() { + let addr = Multiaddr::from_str("/dnsaddr/bootstrap.butterfly.fildev.network").unwrap(); + let p = addr + .iter() + .find(|p| matches!(p, Protocol::Dnsaddr(_))) + .unwrap(); + if let Protocol::Dnsaddr(name) = p { + let pairs = resolve_libp2p_dnsaddr(&name).await.unwrap(); + assert!(!pairs.is_empty()); + } else { + panic!("No dnsaddr protocol found"); + } + } #[tokio::test] async fn kademlia_test() { - fn new_discovery( + async fn new_discovery( keypair: Keypair, seed_peers: impl IntoIterator, ) -> DiscoveryBehaviour { @@ -544,13 +616,33 @@ mod tests { .with_mdns(false) .with_kademlia(true) .with_user_defined(seed_peers) + .await .unwrap() .target_peer_count(128) .finish() .unwrap() } - let mut b = Swarm::new_ephemeral(|k| new_discovery(k, vec![])); + async fn new_ephemeral(seed_peers: Vec) -> Swarm { + let identity = Keypair::generate_ed25519(); + let peer_id = PeerId::from(identity.public()); + let transport = MemoryTransport::default() + .or_transport(libp2p::tcp::tokio::Transport::default()) + .upgrade(libp2p::core::upgrade::Version::V1) + .authenticate(libp2p::noise::Config::new(&identity).unwrap()) + .multiplex(libp2p::yamux::Config::default()) + .timeout(Duration::from_secs(20)) + .boxed(); + Swarm::new( + transport, + new_discovery(identity, seed_peers).await, + peer_id, + libp2p::swarm::Config::with_tokio_executor() + .with_idle_connection_timeout(Duration::from_secs(5)), + ) + } + + let mut b = new_ephemeral(vec![]).await; b.listen().with_memory_addr_external().await; let b_peer_id = *b.local_peer_id(); let b_addresses: Vec<_> = b @@ -562,7 +654,7 @@ mod tests { }) .collect(); - let mut c = Swarm::new_ephemeral(|k| new_discovery(k, vec![])); + let mut c = new_ephemeral(vec![]).await; c.listen().with_memory_addr_external().await; let c_peer_id = *c.local_peer_id(); if let Some(c_kad) = c.behaviour_mut().discovery.kademlia.as_mut() { @@ -571,7 +663,7 @@ mod tests { } } - let mut a = Swarm::new_ephemeral(|k| new_discovery(k, b_addresses)); + let mut a = new_ephemeral(b_addresses).await; // Bootstrap `a` and `c` a.behaviour_mut().bootstrap().unwrap(); diff --git a/src/libp2p/service.rs b/src/libp2p/service.rs index 919dfc67bf0c..50ae3bd8aabd 100644 --- a/src/libp2p/service.rs +++ b/src/libp2p/service.rs @@ -189,7 +189,7 @@ where genesis_cid: Cid, ) -> anyhow::Result { let behaviour = - ForestBehaviour::new(&net_keypair, &config, network_name, peer_manager.clone())?; + ForestBehaviour::new(&net_keypair, &config, network_name, peer_manager.clone()).await?; let mut swarm = SwarmBuilder::with_existing_identity(net_keypair) .with_tokio() .with_tcp( diff --git a/src/networks/butterflynet/mod.rs b/src/networks/butterflynet/mod.rs index 32a25e815134..408d333079f0 100644 --- a/src/networks/butterflynet/mod.rs +++ b/src/networks/butterflynet/mod.rs @@ -40,22 +40,22 @@ pub async fn fetch_genesis(db: &DB) -> anyhow::Result /// Genesis CID pub static GENESIS_CID: Lazy = Lazy::new(|| { - Cid::from_str("bafy2bzacecqfnzdjcmjisrj6qvdaohweaxdvgwfej2sb4eklw3ksatbg7xj4k").unwrap() + Cid::from_str("bafy2bzacedgcrrsfkdi5dcdfuj6b6zsuenzd3bzeeirvffhoiecddco4ahoni").unwrap() }); /// Compressed genesis file. It is compressed with zstd and cuts the download size by 80% (from 10 MB to 2 MB). static GENESIS_URL: Lazy = Lazy::new(|| { - "https://forest-snapshots.fra1.cdn.digitaloceanspaces.com/genesis/butterflynet.car.zst" + "https://forest-snapshots.fra1.cdn.digitaloceanspaces.com/genesis/butterflynet-bafy2bzacedgcrrsfkdi5dcdfuj6b6zsuenzd3bzeeirvffhoiecddco4ahoni.car.zst" .parse() .expect("hard-coded URL must parse") }); /// Alternative URL for the genesis file. This is hosted on the `lotus` repository and is not /// compressed. -/// The genesis file does not live on the `master` branch, currently on a draft PR. -/// `` +/// The genesis file does not live on the `master` branch, currently on `butterfly/v24` branch. +/// `` static GENESIS_URL_ALT: Lazy = Lazy::new(|| { - "https://github.com/filecoin-project/lotus/raw/4dfe16f58e55b3bbb87c5ff95fbe80bb41d44b80/build/genesis/butterflynet.car".parse().expect("hard-coded URL must parse") + "https://github.com/filecoin-project/lotus/raw/36e6a639fd8411dd69048c95ac478468f2755b8d/build/genesis/butterflynet.car".parse().expect("hard-coded URL must parse") }); pub(crate) const MINIMUM_CONSENSUS_POWER: i64 = 2 << 30;