use crate::{
discovery::{client::ElectrumClient, utilparser::lookup_hosts},
metrics::Metrics,
signal::Waiter,
};
use anyhow::Result;
use bitcoincash::Network;
use rand::RngExt;
use rayon::prelude::IntoParallelRefIterator;
use serde_json::Value;
use std::{
collections::{HashMap, HashSet},
convert::TryInto,
net::IpAddr,
sync::{atomic::AtomicBool, Arc},
time::{Duration, Instant},
};
use tokio::sync::Mutex;
use crate::config::Config;
use crate::discovery::{
peer::{CandidatePeer, GoodKnownPeer, ServerPeer},
seed::{SEED_PEERS_CHIPNET, SEED_PEERS_MAINNET, SEED_PEERS_TESTNET, SEED_PEERS_TESTNET4},
};
use rayon::prelude::*;
const MAX_PER_OCTET_PREFIX: usize = 5;
const MAX_CANDIDATES: usize = 100;
const CANDIDATE_EVICT_DURATION: Duration = Duration::from_secs(120);
const GOOD_PEER_REVISIT_DURATION: Duration = Duration::from_secs(3600);
const ANNOUNCE_QUEUE_MAX_SIZE: usize = 200;
fn get_prefix(addr: &IpAddr) -> [u8; 2] {
match addr {
IpAddr::V4(ipv4) => ipv4.octets()[..2].try_into().unwrap(),
IpAddr::V6(ipv6) => ipv6.octets()[..2].try_into().unwrap(),
}
}
pub struct PeerDiscoverer {
good: Mutex<HashMap<u64, GoodKnownPeer>>,
candidates: Mutex<Vec<CandidatePeer>>,
last_add: Mutex<Instant>,
discovery_thread: Mutex<Option<tokio::task::JoinHandle<()>>>,
discovery_thread_kill: Arc<AtomicBool>,
announce_queue: Mutex<Vec<CandidatePeer>>,
announce_done: Mutex<HashSet< u64>>,
metric_good_now: prometheus::IntGauge,
metric_good_added: prometheus::IntCounter,
metric_good_removed: prometheus::IntCounter,
metric_candidates_now: prometheus::IntGauge,
metric_candidates_added: prometheus::IntCounter,
metric_candidates_rejected: prometheus::IntCounter,
metric_announce_now: prometheus::IntGauge,
}
impl PeerDiscoverer {
pub async fn new(config: &Config, metrics: &Metrics) -> Arc<Self> {
let d = Arc::new(Self {
good: Mutex::new(HashMap::default()),
candidates: Mutex::new(match config.network_type {
Network::Bitcoin => SEED_PEERS_MAINNET.to_vec(),
Network::Testnet => SEED_PEERS_TESTNET.to_vec(),
Network::Testnet4 => SEED_PEERS_TESTNET4.to_vec(),
Network::Chipnet => SEED_PEERS_CHIPNET.to_vec(),
Network::Regtest | Network::Scalenet => Vec::default(),
}),
last_add: Mutex::new(Instant::now()),
discovery_thread: Mutex::new(None),
discovery_thread_kill: Arc::new(AtomicBool::new(false)),
announce_done: Mutex::new(HashSet::default()),
announce_queue: Mutex::new(Vec::with_capacity(ANNOUNCE_QUEUE_MAX_SIZE)),
metric_good_now: metrics.gauge_int(prometheus::Opts::new(
"rostrum_discover_good",
"# of known good server peers",
)),
metric_good_added: metrics.counter_int(prometheus::Opts::new(
"rostrum_discover_added",
"# of known good server peers added (accumulative)",
)),
metric_good_removed: metrics.counter_int(prometheus::Opts::new(
"rostrum_discover_removed",
"# of known good server peers removed (accumulative)",
)),
metric_candidates_now: metrics.gauge_int(prometheus::Opts::new(
"rostrum_discover_candidates",
"# of known untested server peers",
)),
metric_candidates_added: metrics.counter_int(prometheus::Opts::new(
"rostrum_discover_candidates_added",
"# of server peer candidates added (accumulative)",
)),
metric_candidates_rejected: metrics.counter_int(prometheus::Opts::new(
"rostrum_discover_candidates_rejected",
"# of server peer candidates rejected (accumulative)",
)),
metric_announce_now: metrics.gauge_int(prometheus::Opts::new(
"rostrum_discover_announce_queue",
"# of server peers in queue for announcement",
)),
});
let metric_tested = metrics.counter_int(prometheus::Opts::new(
"rostrum_discover_candidates_tested",
"# of server peer test connections made (accumulative)",
));
let loop_interval = Duration::from_secs(if config.network_type == Network::Regtest {
1
} else {
10
});
let thread_d = Arc::clone(&d);
if config.announce {
info!("Peer discovery thread started.");
*d.discovery_thread.lock().await =
Some(crate::thread::spawn_task("discovery", async move {
loop {
if Waiter::wait_or_shutdown(loop_interval).await.is_err()
|| thread_d
.discovery_thread_kill
.load(std::sync::atomic::Ordering::Relaxed)
{
return Ok(());
}
let needs_revisit = thread_d
.good
.lock()
.await
.iter()
.filter_map(|(_, p)| {
if p.last_visit.elapsed() >= GOOD_PEER_REVISIT_DURATION {
Some(p.clone())
} else {
None
}
})
.take(1)
.collect::<Vec<GoodKnownPeer>>();
if let Some(peer) = needs_revisit.first() {
metric_tested.inc();
if let Some(good_peer) = evaluate_peer(peer).await {
if good_peer.id() != peer.id() {
thread_d.remove_good(&peer.id()).await;
thread_d.promote_candidate(0, good_peer).await;
} else {
thread_d.bump_last_visit(&peer.id()).await;
}
} else {
thread_d.remove_good(&peer.id()).await
}
continue;
}
let candidate = {
let lock = thread_d.candidates.lock().await;
if lock.is_empty() {
None
} else {
let index = rand::rng().random_range(0..lock.len());
lock.get(index).cloned()
}
};
if let Some(peer) = candidate {
if let Some(good_peer) = evaluate_peer(&peer).await {
thread_d.promote_candidate(peer.id(), good_peer).await;
let announce = thread_d.maybe_add_announce_queue(peer).await;
trace!("discover: Announce ourself to promoted peer: {}", announce);
} else {
thread_d.reject_candidate(&peer.id()).await;
}
}
}
}));
} else {
info!("Peer discovery thread disabled.")
}
d
}
pub async fn bump_last_visit(&self, id: &u64) {
match self.good.lock().await.get_mut(id) {
Some(p) => p.last_visit = Instant::now(),
None => {
debug_assert!(false);
warn!("Failed to bump good peer {id}");
}
};
}
pub async fn promote_candidate(&self, candidate_id: u64, good_peer: GoodKnownPeer) {
if candidate_id != 0 {
let mut lock = self.candidates.lock().await;
let candidate = lock
.iter()
.enumerate()
.find(|(_, p)| p.id() == candidate_id);
if let Some((i, _)) = candidate {
lock.remove(i);
} else {
debug_assert!(false);
warn!("Tried to promote a candidate that does not exist");
return;
}
};
self.good.lock().await.insert(good_peer.id(), good_peer);
self.metric_good_added.inc();
self.metric_good_now.inc();
}
pub async fn reject_candidate(&self, id: &u64) {
let mut lock = self.candidates.lock().await;
let candidate = lock.iter().enumerate().find(|(_, p)| &p.id() == id);
match candidate {
Some((index, _)) => {
lock.remove(index);
self.metric_candidates_rejected.inc();
self.metric_candidates_now.set(lock.len() as i64);
}
None => {
debug_assert!(false);
warn!("Tried to reject a candidate that does not exist")
}
}
}
pub async fn remove_good(&self, id: &u64) {
self.good.lock().await.remove(id);
self.metric_good_now.dec();
self.metric_good_removed.inc();
self.announce_done.lock().await.remove(id);
}
async fn len_good_peers_with_prefix(&self, prefix: &[u8; 2]) -> usize {
self.good
.lock()
.await
.par_iter()
.filter(|(_, p)| &get_prefix(&p.ip()) == prefix)
.count()
}
async fn has_good(&self, id: u64) -> bool {
self.good.lock().await.iter().any(|(_, p)| p.id() == id)
}
pub async fn add_candidate(&self, candidate: CandidatePeer) -> Result<()> {
if self
.len_good_peers_with_prefix(&get_prefix(&candidate.ip()))
.await
>= MAX_PER_OCTET_PREFIX
{
bail!("Have enough good servers in this subnet");
}
let candidate_id = candidate.id();
if self.has_good(candidate_id).await {
return Ok(());
}
let mut c = self.candidates.lock().await;
if c.iter().any(|existing| existing.id() == candidate_id) {
return Ok(());
}
if c.len() < MAX_CANDIDATES {
c.push(candidate);
*self.last_add.lock().await = Instant::now();
self.metric_candidates_added.inc();
self.metric_candidates_now.inc();
return Ok(());
}
if self.last_add.lock().await.elapsed() >= CANDIDATE_EVICT_DURATION {
trace!("Evicting random and adding candidate {:?}", candidate);
let index = rand::rng().random_range(0..c.len());
c.remove(index);
c.push(candidate);
*self.last_add.lock().await = Instant::now();
self.metric_candidates_added.inc();
return Ok(());
}
bail!("Too many peers for consideration. Try again later.");
}
pub async fn get_peers(&self) -> Vec<GoodKnownPeer> {
self.good.lock().await.values().cloned().collect()
}
pub async fn next_announce(&self) -> Option<CandidatePeer> {
let mut lock = self.announce_queue.lock().await;
if lock.is_empty() {
return None;
}
let index = rand::rng().random_range(0..lock.len());
let peer = lock.remove(index);
self.announce_done.lock().await.insert(peer.id());
self.metric_announce_now.set(lock.len() as i64);
Some(peer)
}
pub async fn maybe_add_announce_queue(&self, peer: CandidatePeer) -> bool {
if self.announce_done.lock().await.contains(&peer.id()) {
return false;
}
let mut lock = self.announce_queue.lock().await;
if lock.len() >= ANNOUNCE_QUEUE_MAX_SIZE {
trace!("discover: Announce queue full, ignoring {}", peer.ip());
return false;
}
lock.push(peer);
self.metric_announce_now.set(lock.len() as i64);
true
}
pub async fn random_good(&self) -> Option<GoodKnownPeer> {
let lock = self.good.lock().await;
if lock.is_empty() {
return None;
}
let index = rand::rng().random_range(0..lock.len());
lock.iter()
.skip(index)
.take(1)
.map(|(_, p)| p)
.collect::<Vec<_>>()
.first()
.cloned()
.cloned()
}
}
impl Drop for PeerDiscoverer {
fn drop(&mut self) {
self.discovery_thread_kill
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
async fn try_fetch_features(
conn_result: Result<ElectrumClient>,
peername: &str,
label: &str,
) -> Option<Value> {
match conn_result {
Ok(conn) => match conn.call("server.features", vec![]).await {
Ok(features) => Some(features),
Err(e) => {
trace!("discover: {label} on {peername} failed features call: {e}");
None
}
},
Err(e) => {
trace!("discover: {label} on {peername} failed to connect: {e}");
None
}
}
}
pub async fn evalute_peer_inner(
peer: &dyn ServerPeer,
allow_new_candidate: bool,
) -> (Option<GoodKnownPeer>, Option<CandidatePeer>) {
let peername = peer.pretty_hostname();
trace!("discover: Evaluating peer {peername}");
let (ssl_result, tcp_result) = ElectrumClient::connect_all(peer).await;
let features_ssl = try_fetch_features(ssl_result, &peername, "ssl").await;
let features_tcp = try_fetch_features(tcp_result, &peername, "tcp").await;
let (features_ssl, features_tcp) = match (&features_ssl, &features_tcp) {
(Some(ssl), Some(tcp)) if ssl != tcp => {
trace!(
"discover: mismatch between ssl and tcp on {}",
peer.pretty_hostname()
);
(features_ssl.clone(), None)
}
_ => (features_ssl, features_tcp),
};
let features = if let Some(features) = features_ssl.clone().or(features_tcp.clone()) {
features
} else {
trace!("discover: Did not get features from tcp or ssl from {peername}");
return (None, None);
};
let peer_version = match features
.get("protocol_max")
.and_then(|protocol| protocol.as_str())
{
Some(version) => version,
None => {
trace!("discover: peer {peername} did not provide version");
return (None, None);
}
};
let hosts = lookup_hosts(features.get("hosts"), peer.ip()).await;
let preferred = hosts.iter().find(|c| c.hostname.is_some());
let preferred = preferred.unwrap_or(hosts.first().unwrap());
if allow_new_candidate && (preferred.id() != peer.id()) {
trace!("discover: Learned new info about {peername}; recursing");
return (None, Some(preferred.clone()));
}
let tcp_port = features_tcp.as_ref().and_then(|_| peer.tcp_port());
let ssl_port = features_ssl.as_ref().and_then(|_| peer.ssl_port());
let good = GoodKnownPeer {
tcp_port,
ssl_port,
hostname: preferred.hostname(),
ip: peer.ip(),
last_visit: Instant::now(),
last_candidate_request: peer.last_candidate_request(),
version: peer_version.to_string(),
};
(Some(good), None)
}
pub async fn evaluate_peer(peer: &dyn ServerPeer) -> Option<GoodKnownPeer> {
match evalute_peer_inner(peer, true).await {
(None, None) => None,
(None, Some(candidate)) => evalute_peer_inner(&candidate, false).await.0,
(Some(good), None) => Some(good),
(Some(good), Some(_)) => {
debug_assert!(false, "should not find both good and candidate");
Some(good)
}
}
}