use super::*;
impl NostrDiscovery {
pub async fn learn_public_udp_addr(
&self,
transport_id_key: u32,
advertise_port: u16,
) -> Option<SocketAddr> {
if let Some(entry) = self
.public_udp_addr_cache
.read()
.await
.get(&transport_id_key)
{
let ttl = if entry.addr.is_some() {
Duration::from_secs(self.config.advert_refresh_secs.max(60))
} else {
PUBLIC_UDP_ADDR_FAILURE_TTL
};
if entry.fetched_at.elapsed() < ttl {
return entry.addr;
}
}
let resolved = self.stun_observe_public_ip(advertise_port).await;
let mut cache = self.public_udp_addr_cache.write().await;
cache.insert(
transport_id_key,
CachedPublicUdpAddr {
addr: resolved,
fetched_at: Instant::now(),
},
);
resolved
}
async fn stun_observe_public_ip(&self, advertise_port: u16) -> Option<SocketAddr> {
if self.config.stun_servers.is_empty() {
return None;
}
let socket = match bind_traversal_udp_socket() {
Ok(s) => s,
Err(err) => {
debug!(error = %err, "public-udp-addr: ephemeral bind failed");
return None;
}
};
let observed = match observe_traversal_addresses(
&socket,
&self.config.stun_servers,
false,
ADVERT_STUN_TIMEOUT,
)
.await
{
Ok((reflexive, _local, stun_server)) => {
debug!(
stun = %stun_server.as_deref().unwrap_or("-"),
reflexive = %reflexive
.as_ref()
.map(|a| format!("{}:{}", a.ip, a.port))
.unwrap_or_else(|| "-".into()),
"public-udp-addr: STUN observation"
);
reflexive
}
Err(err) => {
debug!(error = %err, "public-udp-addr: STUN failed");
return None;
}
};
observed.and_then(|addr| {
let parsed_ip: std::net::IpAddr = addr.ip.parse().ok()?;
Some(SocketAddr::new(parsed_ip, advertise_port))
})
}
pub async fn refetch_advert_for_stale_check(&self, peer_npub: &str) -> NostrRefetchOutcome {
let target_pubkey = match PublicKey::parse(peer_npub) {
Ok(p) => p,
Err(_) => return NostrRefetchOutcome::Skipped,
};
let peer_key = NostrPeerKey::from_public_key_ref(&target_pubkey);
let relay_config = self.relay_config.read().await.clone();
if relay_config.advert_relays.is_empty() {
return NostrRefetchOutcome::Skipped;
}
let cached_created_at = self
.advert_cache
.read()
.await
.get(&peer_key)
.map(|c| c.created_at);
let events = match self
.client
.fetch_events_from(
relay_config.advert_relays.clone(),
Filter::new()
.author(target_pubkey)
.kind(Kind::Custom(ADVERT_KIND))
.identifier(advert_d_tag(&self.config.app)),
Duration::from_secs(2),
)
.await
{
Ok(e) => e,
Err(_) => return NostrRefetchOutcome::Skipped,
};
let mut newest: Option<(u64, &Event)> = None;
for ev in events.iter() {
let ts = ev.created_at.as_secs();
match newest {
Some((cur, _)) if ts <= cur => {}
_ => newest = Some((ts, ev)),
}
}
let Some((relay_created_at, ev)) = newest else {
self.advert_cache.write().await.remove(&peer_key);
self.failure_state.reset_streak_after_refresh(peer_key);
return NostrRefetchOutcome::Evicted;
};
match cached_created_at {
Some(cached) if relay_created_at <= cached => NostrRefetchOutcome::SameAdvert,
_ => {
let Some(valid_until_ms) = self.event_valid_until_ms(ev) else {
return NostrRefetchOutcome::Skipped;
};
let Ok(verified_event) = VerifiedEvent::try_from(ev) else {
return NostrRefetchOutcome::Skipped;
};
let Ok(advert) = Self::parse_overlay_advert_event(verified_event, &self.config.app)
else {
return NostrRefetchOutcome::Skipped;
};
let updated = CachedOverlayAdvert {
author_npub: peer_npub.to_string(),
advert,
created_at: relay_created_at,
valid_until_ms,
};
self.advert_cache.write().await.insert(peer_key, updated);
self.failure_state.reset_streak_after_refresh(peer_key);
NostrRefetchOutcome::Refreshed
}
}
}
pub async fn request_advert_stale_check(self: &Arc<Self>, peer_npub: String) -> bool {
let Ok(peer_key) = NostrPeerKey::parse(&peer_npub) else {
return false;
};
let relay_config = self.relay_config.read().await.clone();
if relay_config.advert_relays.is_empty() {
return false;
}
{
let mut active = self.active_refetches.lock().await;
if !active.insert(peer_key) {
return false;
}
}
let runtime = Arc::clone(self);
tokio::spawn(async move {
let outcome = runtime.refetch_advert_for_stale_check(&peer_npub).await;
match outcome {
NostrRefetchOutcome::Evicted => info!(
npub = %peer_npub,
"stale-advert sweep: peer evicted from advert cache"
),
NostrRefetchOutcome::Refreshed => info!(
npub = %peer_npub,
"stale-advert sweep: peer republished, cache refreshed and streak reset"
),
NostrRefetchOutcome::SameAdvert => debug!(
npub = %peer_npub,
"stale-advert sweep: relay still has same advert"
),
NostrRefetchOutcome::Skipped => debug!(
npub = %peer_npub,
"stale-advert sweep: skipped"
),
}
runtime.active_refetches.lock().await.remove(&peer_key);
});
true
}
pub async fn update_local_advert(
self: &Arc<Self>,
advert: Option<OverlayAdvert>,
) -> Result<(), BootstrapError> {
let changed = {
let mut slot = self.local_advert.write().await;
if *slot == advert {
false
} else {
*slot = advert;
true
}
};
if !changed {
return Ok(());
}
self.request_publish_advert();
Ok(())
}
pub async fn advert_endpoints_for_peer(
&self,
peer_npub: &str,
) -> Result<Vec<OverlayEndpointAdvert>, BootstrapError> {
let target_pubkey =
PublicKey::parse(peer_npub).map_err(|e| BootstrapError::InvalidPeerNpub {
npub: peer_npub.to_string(),
reason: e.to_string(),
})?;
let advert = self.fetch_advert(peer_npub, target_pubkey).await?;
Ok(advert.endpoints)
}
pub async fn cached_advert_endpoints_for_peer(
&self,
peer_npub: &str,
) -> Option<Vec<OverlayEndpointAdvert>> {
let peer_key = NostrPeerKey::parse(peer_npub).ok()?;
self.prune_advert_cache().await;
let now = now_ms();
self.advert_cache
.read()
.await
.get(&peer_key)
.filter(|cached| cached.valid_until_ms > now)
.map(|cached| cached.advert.endpoints.clone())
}
pub async fn cached_open_discovery_candidates(
&self,
max: usize,
) -> Vec<(String, Vec<OverlayEndpointAdvert>, u64)> {
self.prune_advert_cache().await;
let now = now_ms();
let self_key = self.self_peer_key();
let cache = self.advert_cache.read().await;
cache
.iter()
.filter(|(peer_key, _)| **peer_key != self_key)
.map(|(_, entry)| entry)
.filter(|entry| entry.valid_until_ms > now)
.map(|entry| {
(
entry.author_npub.clone(),
entry.advert.endpoints.clone(),
entry.created_at,
)
})
.take(max)
.collect()
}
pub(super) async fn publish_advert(&self) -> Result<(), BootstrapError> {
let previous_event_id = self.current_advert_event_id.read().await.to_owned();
if !self.config.advertise {
if let Some(event_id) = previous_event_id {
let relay_config = self.relay_config.read().await.clone();
self.publish_delete(&relay_config.advert_relays, [event_id])
.await?;
*self.current_advert_event_id.write().await = None;
}
return Ok(());
}
let mut advert = match self.local_advert.read().await.clone() {
Some(advert) => advert,
None => return Ok(()),
};
advert.identifier = ADVERT_IDENTIFIER.to_string();
advert.version = ADVERT_VERSION;
advert.endpoints.retain(endpoint_advert_is_publicly_usable);
if advert.endpoints.is_empty() {
return Ok(());
}
if advert.has_udp_nat_endpoint() {
if advert
.signal_relays
.as_ref()
.is_none_or(|relays| relays.is_empty())
{
return Err(BootstrapError::InvalidAdvert(
"udp:nat endpoint requires non-empty signalRelays".to_string(),
));
}
if advert
.stun_servers
.as_ref()
.is_none_or(|servers| servers.is_empty())
{
return Err(BootstrapError::InvalidAdvert(
"udp:nat endpoint requires non-empty stunServers".to_string(),
));
}
} else {
advert.signal_relays = None;
advert.stun_servers = None;
}
let expires_at = now_ms() + self.config.advert_ttl_secs * 1000;
let tags = vec![
Tag::identifier(advert_d_tag(&self.config.app)),
Tag::custom(TagKind::custom("protocol"), [self.config.app.clone()]),
Tag::custom(TagKind::custom("version"), [PROTOCOL_VERSION.to_string()]),
Tag::expiration(Timestamp::from((expires_at / 1000).max(1))),
];
let event = EventBuilder::new(Kind::Custom(ADVERT_KIND), serde_json::to_string(&advert)?)
.tags(tags)
.sign_with_keys(&self.keys)
.map_err(|e| BootstrapError::Nostr(e.to_string()))?;
let relay_config = self.relay_config.read().await.clone();
self.client
.send_event_to(relay_config.advert_relays.clone(), &event)
.await
.map_err(|e| BootstrapError::Nostr(e.to_string()))?;
debug!(
event = %short_id(&event.id.to_string()),
relays = relay_config.advert_relays.len(),
endpoints = %endpoint_summary(&advert.endpoints),
ttl_secs = self.config.advert_ttl_secs,
"advert: published"
);
*self.current_advert_event_id.write().await = Some(event.id);
Ok(())
}
pub(super) async fn fetch_advert(
&self,
peer_npub: &str,
target_pubkey: PublicKey,
) -> Result<OverlayAdvert, BootstrapError> {
let peer_key = NostrPeerKey::from_public_key_ref(&target_pubkey);
self.prune_advert_cache().await;
if let Some(cached) = self.advert_cache.read().await.get(&peer_key).cloned() {
debug!(
peer = %short_npub(peer_npub),
source = "cache",
endpoints = %endpoint_summary(&cached.advert.endpoints),
"advert: resolved"
);
return Ok(cached.advert);
}
let relay_config = self.relay_config.read().await.clone();
let events = self
.client
.fetch_events_from(
relay_config.advert_relays.clone(),
Filter::new()
.author(target_pubkey)
.kind(Kind::Custom(ADVERT_KIND))
.identifier(advert_d_tag(&self.config.app)),
Duration::from_secs(2),
)
.await
.map_err(|e| BootstrapError::Nostr(e.to_string()))?;
let mut best: Option<CachedOverlayAdvert> = None;
for event in events.iter() {
let Some(valid_until_ms) = self.event_valid_until_ms(event) else {
continue;
};
let Ok(verified_event) = VerifiedEvent::try_from(event) else {
continue;
};
if *verified_event.pubkey() != target_pubkey {
continue;
}
let Ok(advert) = Self::parse_overlay_advert_event(verified_event, &self.config.app)
else {
continue;
};
let replace = best
.as_ref()
.map(|current| event.created_at.as_secs() >= current.created_at)
.unwrap_or(true);
if replace {
best = Some(CachedOverlayAdvert {
author_npub: peer_npub.to_string(),
advert,
created_at: event.created_at.as_secs(),
valid_until_ms,
});
}
}
let cached = best.ok_or_else(|| BootstrapError::MissingAdvert(peer_npub.to_string()))?;
debug!(
peer = %short_npub(peer_npub),
source = "relay-fetch",
endpoints = %endpoint_summary(&cached.advert.endpoints),
"advert: resolved"
);
self.advert_cache
.write()
.await
.insert(peer_key, cached.clone());
self.prune_advert_cache().await;
Ok(cached.advert)
}
pub(in crate::discovery::nostr) fn parse_overlay_advert_event(
event: VerifiedEvent<'_>,
expected_app: &str,
) -> Result<OverlayAdvert, BootstrapError> {
let event = event.as_event();
if event.kind != Kind::Custom(ADVERT_KIND) {
return Err(BootstrapError::InvalidAdvert(
"unexpected advert event kind".to_string(),
));
}
let advertised_app = event
.tags
.find(TagKind::custom("protocol"))
.and_then(|tag| tag.content())
.ok_or_else(|| {
BootstrapError::InvalidAdvert("missing required protocol tag".to_string())
})?;
if advertised_app != expected_app {
return Err(BootstrapError::InvalidAdvert(format!(
"unsupported protocol '{}'",
advertised_app
)));
}
let advert: OverlayAdvert = serde_json::from_str(&event.content)?;
Self::validate_overlay_advert(advert)
}
pub(in crate::discovery::nostr) fn validate_overlay_advert(
mut advert: OverlayAdvert,
) -> Result<OverlayAdvert, BootstrapError> {
if advert.identifier != ADVERT_IDENTIFIER {
return Err(BootstrapError::InvalidAdvert(format!(
"unsupported identifier '{}'",
advert.identifier
)));
}
if advert.version != ADVERT_VERSION {
return Err(BootstrapError::InvalidAdvert(format!(
"unsupported version '{}'",
advert.version
)));
}
if advert.endpoints.is_empty() {
return Err(BootstrapError::InvalidAdvert(
"missing required endpoints".to_string(),
));
}
advert.endpoints.retain(endpoint_advert_is_publicly_usable);
if advert.endpoints.is_empty() {
return Err(BootstrapError::InvalidAdvert(
"missing publicly routable endpoints".to_string(),
));
}
let has_nat = advert.has_udp_nat_endpoint();
let has_webrtc = advert
.endpoints
.iter()
.any(|endpoint| endpoint.transport == OverlayTransportKind::WebRtc);
if has_nat {
if advert
.signal_relays
.as_ref()
.is_none_or(|relays| relays.is_empty())
{
return Err(BootstrapError::InvalidAdvert(
"udp:nat endpoint requires signalRelays".to_string(),
));
}
if advert
.stun_servers
.as_ref()
.is_none_or(|servers| servers.is_empty())
{
return Err(BootstrapError::InvalidAdvert(
"udp:nat endpoint requires stunServers".to_string(),
));
}
} else if has_webrtc {
if advert
.signal_relays
.as_ref()
.is_none_or(|relays| relays.is_empty())
{
return Err(BootstrapError::InvalidAdvert(
"webrtc endpoint requires signalRelays".to_string(),
));
}
} else {
advert.signal_relays = None;
advert.stun_servers = None;
}
Ok(advert)
}
pub(super) async fn prune_advert_cache(&self) {
let now = now_ms();
let mut cache = self.advert_cache.write().await;
cache.retain(|_, entry| entry.valid_until_ms > now);
if cache.len() <= self.config.advert_cache_max_entries {
return;
}
let mut oldest = cache
.iter()
.map(|(peer_key, entry)| (*peer_key, entry.valid_until_ms))
.collect::<Vec<_>>();
oldest.sort_by_key(|(_, ts)| *ts);
let overflow = cache
.len()
.saturating_sub(self.config.advert_cache_max_entries);
for (peer_key, _) in oldest.into_iter().take(overflow) {
cache.remove(&peer_key);
}
debug!(
evicted = overflow,
retained = cache.len(),
cap = self.config.advert_cache_max_entries,
"advert cache overflow; evicted oldest entries"
);
}
fn advert_max_age_ms(&self) -> u64 {
self.config.advert_ttl_secs * 1000 * ADVERT_CACHE_STALE_GRACE_MULTIPLIER
}
pub(super) fn event_valid_until_ms(&self, event: &Event) -> Option<u64> {
Self::compute_advert_valid_until_ms(event, self.advert_max_age_ms(), now_ms())
}
pub(in crate::discovery::nostr) fn compute_advert_valid_until_ms(
event: &Event,
advert_max_age_ms: u64,
now_ms: u64,
) -> Option<u64> {
if event.is_expired() {
return None;
}
let created_ms = event.created_at.as_secs().saturating_mul(1000);
let created_window_until = created_ms.saturating_add(advert_max_age_ms);
if created_window_until <= now_ms {
return None;
}
let expires_ms = event
.tags
.expiration()
.map(|timestamp| timestamp.as_secs().saturating_mul(1000));
let valid_until_ms = expires_ms
.map(|expires| expires.min(created_window_until))
.unwrap_or(created_window_until);
(valid_until_ms > now_ms).then_some(valid_until_ms)
}
}