use super::*;
impl Node {
pub(in crate::node) async fn update_peers(
&mut self,
new_peers: Vec<crate::config::PeerConfig>,
) -> Result<crate::node::UpdatePeersOutcome, crate::node::NodeError> {
use std::collections::{HashMap, HashSet};
let mut new_by_addr: HashMap<crate::identity::NodeAddr, crate::config::PeerConfig> =
HashMap::with_capacity(new_peers.len());
let mut new_order = Vec::with_capacity(new_peers.len());
for peer in new_peers {
let identity = match PeerIdentity::from_npub(&peer.npub) {
Ok(id) => id,
Err(e) => {
return Err(crate::node::NodeError::InvalidPeerNpub {
npub: peer.npub.clone(),
reason: e.to_string(),
});
}
};
let node_addr = *identity.node_addr();
if !new_by_addr.contains_key(&node_addr) {
new_order.push(node_addr);
}
new_by_addr.insert(node_addr, peer);
}
let current_by_addr: HashMap<crate::identity::NodeAddr, crate::config::PeerConfig> = self
.config
.peers()
.iter()
.filter_map(|pc| {
PeerIdentity::from_npub(&pc.npub)
.ok()
.map(|id| (*id.node_addr(), pc.clone()))
})
.collect();
let new_addrs: HashSet<_> = new_by_addr.keys().copied().collect();
let current_addrs: HashSet<_> = current_by_addr.keys().copied().collect();
let removed: Vec<_> = current_addrs.difference(&new_addrs).copied().collect();
let added: Vec<_> = new_addrs.difference(¤t_addrs).copied().collect();
let kept: Vec<_> = new_addrs.intersection(¤t_addrs).copied().collect();
let mut outcome = crate::node::UpdatePeersOutcome::default();
for node_addr in &removed {
if self.retry_pending.remove(node_addr).is_some() {
debug!(
peer = %self.peer_display_name(node_addr),
"Dropping retry entry for peer removed from runtime peer list"
);
}
self.peer_aliases.remove(node_addr);
self.set_discovery_fallback_transit_allowed(*node_addr, false);
outcome.removed += 1;
}
let mut auto_connect_refresh_configs = Vec::new();
for node_addr in &kept {
let new_pc = &new_by_addr[node_addr];
let current_pc = ¤t_by_addr[node_addr];
if new_pc.addresses != current_pc.addresses
|| new_pc.alias != current_pc.alias
|| new_pc.connect_policy != current_pc.connect_policy
|| new_pc.auto_reconnect != current_pc.auto_reconnect
|| new_pc.discovery_fallback_transit != current_pc.discovery_fallback_transit
{
outcome.updated += 1;
self.set_discovery_fallback_transit_allowed(
*node_addr,
new_pc.discovery_fallback_transit,
);
if let Some(state) = self.retry_pending.get_mut(node_addr) {
state.peer_config = new_pc.clone();
state.reconnect = new_pc.auto_reconnect;
state.retry_after_ms = Self::now_ms();
}
if let Some(alias) = new_pc.alias.clone() {
self.peer_aliases.insert(*node_addr, alias);
}
if new_pc.is_auto_connect() && !new_pc.addresses.is_empty() {
auto_connect_refresh_configs.push(new_pc.clone());
}
} else {
outcome.unchanged += 1;
self.set_discovery_fallback_transit_allowed(
*node_addr,
new_pc.discovery_fallback_transit,
);
if let Some(state) = self.retry_pending.get_mut(node_addr) {
state.peer_config = new_pc.clone();
state.reconnect = new_pc.auto_reconnect;
}
if new_pc.is_auto_connect() && !new_pc.addresses.is_empty() {
auto_connect_refresh_configs.push(new_pc.clone());
}
}
}
let added_configs: Vec<crate::config::PeerConfig> = new_order
.iter()
.filter(|addr| added.contains(addr))
.map(|addr| new_by_addr[addr].clone())
.collect();
self.config.peers = new_order
.iter()
.filter_map(|addr| new_by_addr.get(addr).cloned())
.collect();
self.configured_peer_send_weights = ConfiguredPeerSendWeights::from_config(&self.config);
for peer_config in added_configs {
outcome.added += 1;
let Ok(identity) = PeerIdentity::from_npub(&peer_config.npub) else {
continue;
};
let name = peer_config
.alias
.clone()
.unwrap_or_else(|| identity.short_npub());
self.peer_aliases.insert(*identity.node_addr(), name);
self.set_discovery_fallback_transit_allowed(
*identity.node_addr(),
peer_config.discovery_fallback_transit,
);
self.register_identity(*identity.node_addr(), identity.pubkey_full());
if !peer_config.is_auto_connect() {
continue;
}
match self
.try_auto_connect_graph_session(&peer_config, identity)
.await
{
Ok(true) => continue,
Ok(false) => {}
Err(err) => {
debug!(
npub = %peer_config.npub,
error = %err,
"Existing FIPS graph did not warm newly added peer"
);
}
}
if let Err(e) = self.initiate_peer_connection(&peer_config).await {
warn!(
npub = %peer_config.npub,
error = %e,
"Failed to initiate connection for newly added peer"
);
if let Ok(peer_identity) = PeerIdentity::from_npub(&peer_config.npub) {
self.schedule_retry_after_error(*peer_identity.node_addr(), Self::now_ms(), &e);
}
if matches!(e, crate::node::NodeError::NoTransportForType(_))
&& let Some(bootstrap) = self.nostr_discovery.clone()
{
bootstrap
.request_advert_stale_check(peer_config.npub.clone())
.await;
}
}
}
for peer_config in auto_connect_refresh_configs {
let Ok(peer_identity) = PeerIdentity::from_npub(&peer_config.npub) else {
continue;
};
let node_addr = *peer_identity.node_addr();
if self.peers.contains_key(&node_addr) {
match self
.initiate_active_peer_alternative_connection(&peer_config)
.await
{
Ok(attempted) => {
if attempted {
debug!(
peer = %self.peer_display_name(&node_addr),
"Started non-disruptive alternate-path handshake for active peer"
);
}
}
Err(e) => {
debug!(
npub = %peer_config.npub,
error = %e,
"Active peer alternate-path refresh did not start"
);
}
}
continue;
}
match self
.try_auto_connect_graph_session(&peer_config, peer_identity)
.await
{
Ok(true) => continue,
Ok(false) => {}
Err(err) => {
debug!(
npub = %peer_config.npub,
error = %err,
"Existing FIPS graph did not warm refreshed peer"
);
}
}
match self.initiate_peer_connection(&peer_config).await {
Ok(()) => {
let hs_timeout_ms = self.config.node.rate_limit.handshake_timeout_secs * 1000;
if let Some(state) = self.retry_pending.get_mut(&node_addr) {
state.peer_config = peer_config;
state.retry_after_ms = Self::now_ms().saturating_add(hs_timeout_ms);
}
}
Err(e) => {
debug!(
npub = %peer_config.npub,
error = %e,
"Refreshed peer addresses did not initiate a direct connection"
);
self.schedule_retry_after_error(node_addr, Self::now_ms(), &e);
}
}
}
self.warm_auto_connect_graph_sessions().await;
Ok(outcome)
}
pub(in crate::node) async fn refresh_peer_paths(
&mut self,
npubs: Vec<String>,
) -> Result<usize, crate::node::NodeError> {
let mut refreshed = 0usize;
let now_ms = Self::now_ms();
for npub in npubs {
let identity =
PeerIdentity::from_npub(&npub).map_err(|e| NodeError::InvalidPeerNpub {
npub: npub.clone(),
reason: e.to_string(),
})?;
let node_addr = *identity.node_addr();
let Some(peer_config) = self
.config
.auto_connect_peers()
.find(|pc| {
PeerIdentity::from_npub(&pc.npub)
.map(|id| *id.node_addr() == node_addr)
.unwrap_or(false)
})
.cloned()
else {
debug!(
peer = %identity.short_npub(),
"Skipping peer path refresh for peer not in auto-connect config"
);
continue;
};
if let Some(state) = self.retry_pending.get_mut(&node_addr) {
state.peer_config = peer_config.clone();
state.reconnect = peer_config.auto_reconnect;
}
let attempted = if self.peers.contains_key(&node_addr) {
self.initiate_active_peer_direct_refresh_connection(&peer_config)
.await?
} else {
match self.initiate_peer_connection(&peer_config).await {
Ok(()) => true,
Err(error) => {
self.schedule_retry_after_error(node_addr, now_ms, &error);
return Err(error);
}
}
};
if attempted {
refreshed = refreshed.saturating_add(1);
}
if peer_config.auto_reconnect {
self.schedule_link_dead_reprobe(node_addr, now_ms);
}
}
Ok(refreshed)
}
pub(in crate::node) async fn initiate_peer_connections(&mut self) {
let peer_identities: Vec<(PeerIdentity, Option<String>)> = self
.config
.peers()
.iter()
.filter_map(|pc| {
PeerIdentity::from_npub(&pc.npub)
.ok()
.map(|id| (id, pc.alias.clone()))
})
.collect();
for (identity, alias) in peer_identities {
let name = alias.unwrap_or_else(|| identity.short_npub());
self.peer_aliases.insert(*identity.node_addr(), name);
self.register_identity(*identity.node_addr(), identity.pubkey_full());
}
let peer_configs: Vec<_> = self.config.auto_connect_peers().cloned().collect();
if peer_configs.is_empty() {
debug!("No static peers configured");
return;
}
debug!(
count = peer_configs.len(),
"Initiating static peer connections"
);
for peer_config in peer_configs {
let peer_identity = match PeerIdentity::from_npub(&peer_config.npub) {
Ok(identity) => identity,
Err(_) => continue,
};
match self
.try_auto_connect_graph_session(&peer_config, peer_identity)
.await
{
Ok(true) => continue,
Ok(false) => {}
Err(err) => {
debug!(
npub = %peer_config.npub,
error = %err,
"Existing FIPS graph did not warm auto-connect peer"
);
}
}
if let Err(e) = self.initiate_peer_connection(&peer_config).await {
warn!(
npub = %peer_config.npub,
alias = ?peer_config.alias,
error = %e,
"Failed to initiate peer connection"
);
if let Ok(peer_identity) = PeerIdentity::from_npub(&peer_config.npub) {
self.schedule_retry_after_error(*peer_identity.node_addr(), Self::now_ms(), &e);
}
if matches!(e, crate::node::NodeError::NoTransportForType(_))
&& let Some(bootstrap) = self.nostr_discovery.clone()
{
bootstrap
.request_advert_stale_check(peer_config.npub.clone())
.await;
}
}
}
self.warm_auto_connect_graph_sessions().await;
}
pub(in crate::node) async fn try_auto_connect_graph_session(
&mut self,
peer_config: &PeerConfig,
peer_identity: PeerIdentity,
) -> Result<bool, NodeError> {
if !peer_config.is_auto_connect() {
return Ok(false);
}
let peer_node_addr = *peer_identity.node_addr();
if self
.peers
.get(&peer_node_addr)
.is_some_and(|peer| peer.can_send())
{
return Ok(false);
}
if self.auto_connect_should_race_direct_path(peer_config) {
return Ok(false);
}
if self
.sessions
.get(&peer_node_addr)
.is_some_and(|entry| entry.is_established() || entry.is_initiating())
{
return Ok(true);
}
if self.find_next_hop(&peer_node_addr).is_none() {
return Ok(false);
}
self.register_identity(peer_node_addr, peer_identity.pubkey_full());
match self
.initiate_session(peer_node_addr, peer_identity.pubkey_full())
.await
{
Ok(()) => {
debug!(
peer = %self.peer_display_name(&peer_node_addr),
"Warmed auto-connect peer session over existing FIPS graph"
);
Ok(true)
}
Err(NodeError::SendFailed { node_addr, reason })
if node_addr == peer_node_addr && reason == "no route to destination" =>
{
self.maybe_initiate_lookup(&peer_node_addr).await;
Ok(false)
}
Err(err) => Err(err),
}
}
pub(super) fn auto_connect_should_race_direct_path(&self, peer_config: &PeerConfig) -> bool {
!peer_config.addresses.is_empty() || self.config.node.discovery.nostr.enabled
}
pub(in crate::node) async fn initiate_peer_connection(
&mut self,
peer_config: &crate::config::PeerConfig,
) -> Result<(), NodeError> {
self.initiate_peer_connection_inner(peer_config).await
}
pub(in crate::node) async fn initiate_peer_retry_connection(
&mut self,
peer_config: &crate::config::PeerConfig,
) -> Result<(), NodeError> {
self.initiate_peer_connection_inner(peer_config).await
}
pub(in crate::node) async fn initiate_active_peer_alternative_connection(
&mut self,
peer_config: &crate::config::PeerConfig,
) -> Result<bool, NodeError> {
self.initiate_active_peer_alternative_connection_inner(peer_config, false)
.await
}
pub(in crate::node) async fn initiate_active_peer_direct_refresh_connection(
&mut self,
peer_config: &crate::config::PeerConfig,
) -> Result<bool, NodeError> {
self.initiate_active_peer_alternative_connection_inner(peer_config, true)
.await
}
pub(super) async fn initiate_active_peer_alternative_connection_inner(
&mut self,
peer_config: &crate::config::PeerConfig,
allow_same_path_refresh: bool,
) -> Result<bool, NodeError> {
let peer_identity =
PeerIdentity::from_npub(&peer_config.npub).map_err(|e| NodeError::InvalidPeerNpub {
npub: peer_config.npub.clone(),
reason: e.to_string(),
})?;
let peer_node_addr = *peer_identity.node_addr();
if !self.peers.contains_key(&peer_node_addr) {
self.initiate_peer_connection(peer_config).await?;
return Ok(true);
}
self.try_active_peer_alternative_addresses(
peer_config,
peer_identity,
allow_same_path_refresh,
)
.await
}
pub(super) async fn initiate_peer_connection_inner(
&mut self,
peer_config: &crate::config::PeerConfig,
) -> Result<(), NodeError> {
let peer_identity =
PeerIdentity::from_npub(&peer_config.npub).map_err(|e| NodeError::InvalidPeerNpub {
npub: peer_config.npub.clone(),
reason: e.to_string(),
})?;
let peer_node_addr = *peer_identity.node_addr();
if self.peers.contains_key(&peer_node_addr) {
debug!(
npub = %peer_config.npub,
"Peer already exists, skipping"
);
return Ok(());
}
self.try_peer_addresses(peer_config, peer_identity, true)
.await
}
}