Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 37 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,18 @@ impl std::cmp::Ord for IndexedFilter {
}
}

#[derive(Debug, Clone)]
enum TrustedPeerInner {
Addr(AddrV2),
Hostname(String),
}

impl From<AddrV2> for TrustedPeerInner {
fn from(addr: AddrV2) -> Self {
Self::Addr(addr)
}
}

/// A peer on the Bitcoin P2P network
///
/// # Building peers
Expand All @@ -205,22 +217,25 @@ impl std::cmp::Ord for IndexedFilter {
/// // Or implicitly with `into`
/// let local_host = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
/// let trusted: TrustedPeer = (local_host, None).into();
///
/// // Or from a hostname — resolution happens at connection time.
/// let trusted = TrustedPeer::from_hostname("bitcoind.svc.local", 8333);
/// ```
#[derive(Debug, Clone)]
pub struct TrustedPeer {
/// The IP address of the remote node to connect to.
pub address: AddrV2,
/// The port to establish a TCP connection. If none is provided, the typical Bitcoin Core port is used as the default.
pub port: Option<u16>,
/// The services this peer is known to offer before starting the node.
pub known_services: ServiceFlags,
// The address or hostname of the remote node to connect to.
address: TrustedPeerInner,
// The port to establish a TCP connection. If none is provided, the typical Bitcoin Core port is used as the default.
port: Option<u16>,
// The services this peer is known to offer before starting the node.
known_services: ServiceFlags,
}

impl TrustedPeer {
/// Create a new trusted peer.
pub fn new(address: AddrV2, port: Option<u16>, services: ServiceFlags) -> Self {
pub fn new(address: impl Into<AddrV2>, port: Option<u16>, services: ServiceFlags) -> Self {
Self {
address,
address: TrustedPeerInner::Addr(address.into()),
port,
known_services: services,
}
Expand All @@ -233,7 +248,7 @@ impl TrustedPeer {
IpAddr::V6(ip) => AddrV2::Ipv6(ip),
};
Self {
address,
address: TrustedPeerInner::Addr(address),
port: None,
known_services: ServiceFlags::NONE,
}
Expand All @@ -247,15 +262,24 @@ impl TrustedPeer {
SocketAddr::V6(ip) => AddrV2::Ipv6(*ip.ip()),
};
Self {
address,
address: TrustedPeerInner::Addr(address),
port: Some(socket_addr.port()),
known_services: ServiceFlags::NONE,
}
}

/// The IP address of the trusted peer.
pub fn address(&self) -> AddrV2 {
self.address.clone()
/// Create a new trusted peer from a DNS hostname.
///
/// The hostname is stored as-is and resolved to an IP address at the
/// time a connection is attempted, via [`tokio::net::lookup_host`]. If
/// resolution fails or yields no addresses, the peer is skipped and
/// the next configured peer is tried.
pub fn from_hostname(hostname: impl Into<String>, port: u16) -> Self {
Self {
address: TrustedPeerInner::Hostname(hostname.into()),
port: Some(port),
known_services: ServiceFlags::NONE,
}
}

/// A recommended port to connect to, if there is one.
Expand Down Expand Up @@ -284,12 +308,6 @@ impl From<(IpAddr, Option<u16>)> for TrustedPeer {
}
}

impl From<TrustedPeer> for (AddrV2, Option<u16>) {
fn from(value: TrustedPeer) -> Self {
(value.address(), value.port())
}
}

impl From<IpAddr> for TrustedPeer {
fn from(value: IpAddr) -> Self {
TrustedPeer::from_ip(value)
Expand Down
50 changes: 45 additions & 5 deletions src/network/peer_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
broadcaster::BroadcastQueue,
default_port_from_network,
network::{dns::bootstrap_dns, error::PeerError, peer::Peer, PeerId, PeerTimeoutConfig},
BlockType, Dialog, TrustedPeer,
BlockType, Dialog, TrustedPeer, TrustedPeerInner,
};

use super::{AddressBook, ConnectionType, MainThreadMessage, PeerThreadMessage};
Expand Down Expand Up @@ -217,13 +217,53 @@ impl PeerMap {
// as long as it is not from the same netgroup. If there are no peers in the database, try DNS.
// When `whitelist_only` is set, only whitelist peers are used.
pub async fn next_peer(&mut self) -> Option<Record> {
if let Some(peer) = self.whitelist.pop() {
crate::debug!("Using a configured peer");
while let Some(peer) = self.whitelist.pop() {
let port = peer
.port
.unwrap_or(default_port_from_network(&self.network));
let record = Record::new(peer.address(), port, peer.known_services, &LOCAL_HOST);
return Some(record);
let addr = match peer.address {
TrustedPeerInner::Addr(addr) => addr,
TrustedPeerInner::Hostname(host) => {
crate::debug!(format!("Resolving hostname {host}:{port}"));
match tokio::net::lookup_host((host.as_str(), port)).await {
Ok(iter) => {
let resolved: Vec<AddrV2> = iter
.map(|sa| match sa.ip() {
IpAddr::V4(ip) => AddrV2::Ipv4(ip),
IpAddr::V6(ip) => AddrV2::Ipv6(ip),
})
.collect();
if resolved.is_empty() {
crate::debug!(format!(
"Hostname {host} resolved to no addresses, skipping"
));
continue;
}
crate::debug!(format!(
"Resolved {host} to {} address(es)",
resolved.len()
));
// Push every resolved address onto the whitelist so each is tried on
// a subsequent call. Reversed so the resolver's preferred order is
// preserved under LIFO pop.
for resolved_addr in resolved.into_iter().rev() {
self.whitelist.push(TrustedPeer {
address: TrustedPeerInner::Addr(resolved_addr),
port: Some(port),
known_services: peer.known_services,
});
}
continue;
}
Err(_) => {
crate::debug!(format!("Failed to resolve hostname {host}"));
continue;
}
}
}
};
crate::debug!("Using a configured peer");
return Some(Record::new(addr, port, peer.known_services, &LOCAL_HOST));
}
if self.whitelist_only {
return None;
Expand Down
29 changes: 29 additions & 0 deletions tests/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@ async fn whitelist_only_sync() {
assert_eq!(cp.hash, best);
requester.shutdown().unwrap();
rpc.stop().unwrap();
// No peer available, white list only.
let builder = bip157::builder::Builder::new(bitcoin::Network::Regtest)
.chain_state(ChainState::Checkpoint(HeaderCheckpoint::from_genesis(
bitcoin::Network::Regtest,
Expand All @@ -694,6 +695,34 @@ async fn whitelist_only_sync() {
let (node, _client) = builder.build();
let result = node.run().await;
assert!(result.is_err());
// Peer resolved from hostname.
let (bitcoind, socket_addr) = start_bitcoind(true).unwrap();
let rpc = &bitcoind.client;
let miner = rpc.new_address().unwrap();
mine_blocks(rpc, &miner, 10, 2).await;
let best = best_hash(rpc);
let peer = TrustedPeer::from_hostname(socket_addr.ip().to_string(), socket_addr.port());
let builder = bip157::builder::Builder::new(bitcoin::Network::Regtest)
.chain_state(ChainState::Checkpoint(HeaderCheckpoint::from_genesis(
bitcoin::Network::Regtest,
)))
.add_peer(peer)
.whitelist_only()
.data_dir(&tempdir);
let (node, client) = builder.build();
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
let cp = requester.chain_tip().await.unwrap();
assert_eq!(cp.hash, best);
requester.shutdown().unwrap();
rpc.stop().unwrap();
}

#[tokio::test]
Expand Down
Loading