1use nostr_sdk::prelude::*;
9use crate::ClientRelayExt;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub enum Evidence {
23 Fast,
27 #[default]
34 Quorum,
35 Full,
40}
41
42#[derive(Debug, Clone)]
58pub struct PlaneFetch {
59 pub events: Vec<Event>,
60 pub answered: usize,
62 pub attempted: usize,
64}
65
66impl PlaneFetch {
67 pub fn is_complete(&self) -> bool {
69 self.answered > 0 && self.answered == self.attempted
70 }
71
72 pub fn whole(events: Vec<Event>) -> Self {
75 PlaneFetch { events, answered: 1, attempted: 1 }
76 }
77}
78
79#[derive(Clone, Debug, Default)]
83pub struct Query {
84 pub kinds: Vec<u16>,
85 pub z_tags: Vec<String>,
87 pub d_tags: Vec<String>,
90 pub p_tags: Vec<String>,
93 pub k_tags: Vec<String>,
96 pub authors: Vec<String>,
98 pub ids: Vec<String>,
101 pub since: Option<u64>,
103 pub until: Option<u64>,
106 pub limit: Option<usize>,
108 pub evidence: Evidence,
112}
113
114impl Query {
115 pub fn matches(&self, event: &Event) -> bool {
117 if !self.kinds.is_empty() && !self.kinds.iter().any(|k| Kind::Custom(*k) == event.kind) {
121 return false;
122 }
123 if let Some(since) = self.since {
124 if event.created_at.as_secs() < since {
125 return false;
126 }
127 }
128 if let Some(until) = self.until {
129 if event.created_at.as_secs() > until {
130 return false;
131 }
132 }
133 if !self.authors.is_empty() && !self.authors.iter().any(|a| *a == event.pubkey.to_hex()) {
134 return false;
135 }
136 if !self.ids.is_empty() && !self.ids.iter().any(|i| *i == event.id.to_hex()) {
137 return false;
138 }
139 if !self.z_tags.is_empty() && !self.matches_single_letter("z", &self.z_tags, event) {
140 return false;
141 }
142 if !self.d_tags.is_empty() && !self.matches_single_letter("d", &self.d_tags, event) {
143 return false;
144 }
145 if !self.p_tags.is_empty() && !self.matches_single_letter("p", &self.p_tags, event) {
146 return false;
147 }
148 if !self.k_tags.is_empty() && !self.matches_single_letter("k", &self.k_tags, event) {
149 return false;
150 }
151 true
152 }
153
154 fn matches_single_letter(&self, name: &str, wanted: &[String], event: &Event) -> bool {
155 event.tags.iter().any(|t| {
158 let s = t.as_slice();
159 s.len() >= 2 && s[0] == name && wanted.iter().any(|w| *w == s[1])
160 })
161 }
162
163 pub fn to_filter(&self) -> Filter {
165 let mut filter = Filter::new();
166 if !self.kinds.is_empty() {
167 filter = filter.kinds(self.kinds.iter().map(|k| Kind::Custom(*k)));
168 }
169 if !self.z_tags.is_empty() {
170 filter = filter
171 .custom_tags(SingleLetterTag::LOWERCASE_Z, self.z_tags.clone());
172 }
173 if !self.d_tags.is_empty() {
174 filter = filter.identifiers(self.d_tags.clone());
175 }
176 if !self.p_tags.is_empty() {
177 filter = filter
178 .custom_tags(SingleLetterTag::LOWERCASE_P, self.p_tags.clone());
179 }
180 if !self.k_tags.is_empty() {
181 filter = filter
182 .custom_tags(SingleLetterTag::LOWERCASE_K, self.k_tags.clone());
183 }
184 if !self.authors.is_empty() {
185 let authors: Vec<PublicKey> =
186 self.authors.iter().filter_map(|a| PublicKey::from_hex(a).ok()).collect();
187 if !authors.is_empty() {
188 filter = filter.authors(authors);
189 }
190 }
191 if !self.ids.is_empty() {
192 let ids: Vec<EventId> =
193 self.ids.iter().filter_map(|i| EventId::from_hex(i).ok()).collect();
194 if !ids.is_empty() {
195 filter = filter.ids(ids);
196 }
197 }
198 if let Some(since) = self.since {
199 filter = filter.since(Timestamp::from_secs(since));
200 }
201 if let Some(until) = self.until {
202 filter = filter.until(Timestamp::from_secs(until));
203 }
204 if let Some(limit) = self.limit {
205 filter = filter.limit(limit);
206 }
207 filter
208 }
209}
210
211#[async_trait::async_trait]
215pub trait Transport {
216 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String>;
218 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String>;
220
221 async fn fetch_plane(&self, plane: &Keys, query: &Query, relays: &[String]) -> Result<PlaneFetch, String>;
230
231 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String>;
239}
240
241pub const MAX_PUBLISH_ATTEMPTS: usize = 30;
244
245pub const RESIDUAL_GRACE_MS: u64 = 400;
249
250pub const QUORUM_GRACE_MS: u64 = 2000;
256
257const BREAKER_TRIP_THRESHOLD: u8 = 2;
259
260const BREAKER_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(30);
263
264const TRIPPED_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(6);
268
269pub const CONFIRM_WINDOW: std::time::Duration = std::time::Duration::from_secs(30);
273
274pub async fn durable_broadcast<'a, F>(
281 relays: &[String],
282 max_attempts: usize,
283 backoff: std::time::Duration,
284 mut send_round: F,
285) -> Result<(), String>
286where
287 F: FnMut(Vec<String>) -> std::pin::Pin<Box<dyn std::future::Future<Output = Vec<String>> + Send + 'a>>,
288{
289 let mut pending: Vec<String> = Vec::new();
290 for r in relays {
291 if !pending.contains(r) {
292 pending.push(r.clone());
293 }
294 }
295 let total = pending.len();
296 if total == 0 {
297 return Err("no relays to broadcast to".to_string());
298 }
299 for attempt in 0..max_attempts {
300 if pending.is_empty() {
301 break;
302 }
303 let acked = send_round(pending.clone()).await;
304 pending.retain(|r| !acked.contains(r));
305 if pending.is_empty() || attempt + 1 == max_attempts {
306 break;
307 }
308 if !backoff.is_zero() {
309 crate::rt::time::sleep(backoff).await;
310 }
311 }
312 if pending.len() < total {
313 Ok(()) } else {
315 Err(format!("no relay accepted the event after {max_attempts} attempts each"))
316 }
317}
318
319pub trait CommunityIngestSink: Send + Sync + 'static {
328 fn ingest_stragglers(&self, events: Vec<Event>);
329}
330
331static INGEST_SINK: std::sync::OnceLock<Box<dyn CommunityIngestSink>> = std::sync::OnceLock::new();
332
333pub fn set_community_ingest_sink(sink: Box<dyn CommunityIngestSink>) {
335 let _ = INGEST_SINK.set(sink);
336}
337
338fn submit_stragglers(events: Vec<Event>) {
339 if events.is_empty() {
340 return;
341 }
342 if let Some(sink) = INGEST_SINK.get() {
343 sink.ingest_stragglers(events);
344 }
345}
346
347static WARMED_RELAYS: std::sync::LazyLock<std::sync::Mutex<(u64, std::collections::HashSet<String>)>> =
353 std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashSet::new())));
354
355pub fn forget_warmed_relay(url: &str) {
360 WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner()).1.remove(url);
361}
362
363#[derive(Default)]
370struct BreakerEntry {
371 consecutive_failures: u8,
372 tripped_until: Option<web_time::Instant>,
373}
374
375static RELAY_BREAKER: std::sync::LazyLock<
376 std::sync::Mutex<(u64, std::collections::HashMap<String, BreakerEntry>)>,
377> = std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashMap::new())));
378
379fn with_breaker_at<R>(
384 generation: u64,
385 f: impl FnOnce(&mut std::collections::HashMap<String, BreakerEntry>) -> R,
386) -> R {
387 let mut guard = RELAY_BREAKER.lock().unwrap_or_else(|e| e.into_inner());
388 if guard.0 != generation {
389 guard.0 = generation;
390 guard.1.clear();
391 }
392 f(&mut guard.1)
393}
394
395fn drop_unrevivable(targets: Vec<String>, is_unrevivable: impl Fn(&str) -> bool) -> Vec<String> {
409 let alive: Vec<String> = targets.iter().filter(|r| !is_unrevivable(r)).cloned().collect();
410 if alive.is_empty() {
411 targets
412 } else {
413 alive
414 }
415}
416
417async fn drop_unrevivable_targets(client: &Client, targets: Vec<String>) -> Vec<String> {
419 use nostr_sdk::prelude::RelayStatus;
420 let pool = client.relays().all().await;
421 drop_unrevivable(targets, |r| {
422 RelayUrl::parse(r)
423 .ok()
424 .and_then(|u| pool.get(&u).map(|rl| matches!(rl.status(), RelayStatus::Terminated | RelayStatus::Shutdown | RelayStatus::Banned)))
425 .unwrap_or(false)
426 })
427}
428
429fn breaker_tripped(url: &str) -> bool {
430 breaker_tripped_at(crate::db::current_session_id(), url)
431}
432
433fn breaker_tripped_at(generation: u64, url: &str) -> bool {
434 with_breaker_at(generation, |map| {
435 map.get(url)
436 .and_then(|e| e.tripped_until)
437 .is_some_and(|t| web_time::Instant::now() < t)
438 })
439}
440
441fn breaker_record(url: &str, success: bool, full_budget: bool) {
444 breaker_record_at(crate::db::current_session_id(), url, success, full_budget)
445}
446
447fn breaker_record_at(generation: u64, url: &str, success: bool, full_budget: bool) {
448 with_breaker_at(generation, |map| {
449 if success {
450 map.remove(url);
451 return;
452 }
453 if !full_budget {
454 return;
455 }
456 let e = map.entry(url.to_string()).or_default();
457 e.consecutive_failures = e.consecutive_failures.saturating_add(1);
458 if e.consecutive_failures >= BREAKER_TRIP_THRESHOLD {
459 e.tripped_until = Some(web_time::Instant::now() + BREAKER_COOLDOWN);
460 }
461 })
462}
463
464fn effective_evidence(query: &Query) -> Evidence {
470 query.evidence
471}
472
473struct PooledPlane {
481 client: Client,
482 last_used: web_time::Instant,
483}
484
485static PLANE_POOL: std::sync::LazyLock<std::sync::Mutex<(u64, std::collections::HashMap<String, PooledPlane>)>> =
486 std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashMap::new())));
487
488const PLANE_POOL_IDLE_TTL: std::time::Duration = std::time::Duration::from_secs(90);
491const PLANE_POOL_MAX: usize = 24;
493
494fn plane_pool_key(plane_pk: &str, relays: &[String]) -> String {
495 let mut rs: Vec<&str> = relays.iter().map(|s| s.as_str()).collect();
496 rs.sort_unstable();
497 let mut k = String::with_capacity(plane_pk.len() + 1 + rs.iter().map(|r| r.len() + 1).sum::<usize>());
498 k.push_str(plane_pk);
499 k.push('|');
500 k.push_str(&rs.join(","));
501 k
502}
503
504fn disconnect_clients(clients: Vec<Client>) {
509 if clients.is_empty() {
510 return;
511 }
512 match tokio::runtime::Handle::try_current() {
513 Ok(handle) => {
514 for c in clients {
515 handle.spawn(async move {
516 c.disconnect().await;
517 });
518 }
519 }
520 Err(_) => drop(clients),
521 }
522}
523
524pub fn clear_plane_pool() {
527 let drained: Vec<Client> = {
528 let mut g = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner());
529 g.0 = crate::db::current_session_id();
534 g.1.drain().map(|(_, p)| p.client).collect()
535 };
536 disconnect_clients(drained);
537}
538
539fn plane_pool_take(generation: u64, key: &str) -> (Option<Client>, Vec<Client>) {
543 let mut g = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner());
544 let mut evicted: Vec<Client> = Vec::new();
545 if g.0 != generation {
546 evicted.extend(g.1.drain().map(|(_, p)| p.client));
547 g.0 = generation;
548 }
549 let now = web_time::Instant::now();
551 let expired: Vec<String> = g.1.iter()
552 .filter(|(_, p)| now.duration_since(p.last_used) >= PLANE_POOL_IDLE_TTL)
553 .map(|(k, _)| k.clone())
554 .collect();
555 for k in expired {
556 if let Some(p) = g.1.remove(&k) {
557 evicted.push(p.client);
558 }
559 }
560 let hit = g.1.get_mut(key).map(|p| {
561 p.last_used = now;
562 p.client.clone()
563 });
564 (hit, evicted)
565}
566
567fn plane_pool_insert(generation: u64, key: String, client: Client) -> Vec<Client> {
574 let mut g = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner());
575 if g.0 != generation || g.1.contains_key(&key) {
578 return Vec::new();
579 }
580 let mut evicted: Vec<Client> = Vec::new();
581 if g.1.len() >= PLANE_POOL_MAX {
582 if let Some(lru_key) = g.1.iter().min_by_key(|(_, p)| p.last_used).map(|(k, _)| k.clone()) {
583 if let Some(p) = g.1.remove(&lru_key) {
584 evicted.push(p.client);
585 }
586 }
587 }
588 g.1.insert(key, PooledPlane { client, last_used: web_time::Instant::now() });
589 evicted
590}
591
592fn demotion_allowed() -> bool {
596 #[cfg(feature = "tor")]
597 {
598 matches!(crate::tor::transport_state(), crate::tor::TorTransportState::Disabled)
599 }
600 #[cfg(not(feature = "tor"))]
601 {
602 true
603 }
604}
605
606pub async fn prewarm_held_communities() {
612 crate::db::scoped(async move {
613 let mut relays: Vec<String> = Vec::new();
614 for id in crate::db::community::list_community_ids().unwrap_or_default() {
615 match crate::db::community::community_protocol(&id).ok().flatten() {
616 Some(crate::community::ConcordProtocol::V2) => {
617 if let Ok(Some(c)) = crate::db::community::load_community_v2(&id) {
618 relays.extend(c.relays.iter().cloned());
619 }
620 }
621 _ => {
622 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
623 relays.extend(c.relays.iter().cloned());
624 }
625 }
626 }
627 }
628 relays.sort();
629 relays.dedup();
630 if relays.is_empty() {
631 return;
632 }
633 let Ok(client) = LiveTransport::warm_client(&relays, std::time::Duration::from_secs(4)).await
634 else {
635 return;
636 };
637 crate::community::v2::streamauth::ensure_responder(&client);
643 let probe = Keys::generate();
644 let filter = Query {
645 kinds: vec![crate::community::v2::stream::KIND_WRAP],
646 authors: vec![probe.public_key().to_hex()],
647 limit: Some(1),
648 ..Default::default()
649 }
650 .to_filter();
651 let mut elicits = futures_util::stream::FuturesUnordered::new();
652 for r in &relays {
653 let c = client.clone();
654 let f = filter.clone();
655 let r = r.clone();
656 elicits.push(async move {
657 let _ = fetch_relay_eose_filters(&c, &r, vec![f], std::time::Duration::from_secs(3)).await;
658 });
659 }
660 use futures_util::StreamExt;
661 while elicits.next().await.is_some() {}
662 })
663 .await
664}
665
666#[derive(Clone, Copy, PartialEq, Eq, Debug)]
670pub enum EoseFail {
671 Closed,
673 Deadline,
675 Gone,
677}
678
679pub async fn fetch_relay_eose(
690 client: &Client,
691 url: &str,
692 filter: Filter,
693 timeout: std::time::Duration,
694) -> Result<Vec<Event>, ()> {
695 fetch_relay_eose_filters(client, url, vec![filter], timeout).await.map_err(|_| ())
696}
697
698pub async fn fetch_relay_eose_filters(
703 client: &Client,
704 url: &str,
705 filters: Vec<Filter>,
706 timeout: std::time::Duration,
707) -> Result<Vec<Event>, EoseFail> {
708 let relay = client.relay(url).await.map_err(|_| EoseFail::Gone)?.ok_or(EoseFail::Gone)?;
709 let mut notifications = relay.notifications();
711 let sub_id = SubscriptionId::generate();
712 struct CloseGuard(Relay, SubscriptionId);
719 impl Drop for CloseGuard {
720 fn drop(&mut self) {
721 let relay = self.0.clone();
722 let id = self.1.clone();
723 crate::rt::spawn(async move {
725 let _ = relay
726 .send_msg(nostr_sdk::prelude::ClientMessage::Close(std::borrow::Cow::Owned(id)))
727 .await;
728 });
729 }
730 }
731 let _close = CloseGuard(relay.clone(), sub_id.clone());
732 relay
733 .send_msg(nostr_sdk::prelude::ClientMessage::Req {
734 subscription_id: std::borrow::Cow::Borrowed(&sub_id),
735 filters: filters.into_iter().map(std::borrow::Cow::Owned).collect(),
736 })
737 .await
738 .map_err(|_| EoseFail::Gone)?;
739 let deadline = crate::rt::time::Instant::now() + timeout;
740 let mut events: Vec<Event> = Vec::new();
741 let mut seen: std::collections::HashSet<EventId> = std::collections::HashSet::new();
742 loop {
743 let notification = match crate::rt::time::timeout_at(deadline, notifications.next()).await {
747 Ok(Some(n)) => n,
748 Ok(None) => return Err(EoseFail::Gone),
749 Err(_) => return Err(EoseFail::Deadline), };
751 match notification {
752 RelayNotification::Event { subscription_id, event }
753 if subscription_id == sub_id && seen.insert(event.id) =>
754 {
755 events.push(*event);
756 }
757 RelayNotification::Message { message } => match *message {
758 RelayMessage::Event { subscription_id, event }
759 if *subscription_id == sub_id && seen.insert(event.id) =>
760 {
761 events.push(event.into_owned());
762 }
763 RelayMessage::EndOfStoredEvents(id) if *id == sub_id => return Ok(events),
764 RelayMessage::Closed { subscription_id, .. } if *subscription_id == sub_id => {
765 return Err(EoseFail::Closed); }
767 _ => {}
768 },
769 RelayNotification::RelayStatus { status: nostr_sdk::prelude::RelayStatus::Shutdown } => {
770 return Err(EoseFail::Gone);
771 }
772 _ => {}
773 }
774 }
775}
776
777pub(crate) struct UnionPlan {
781 attempted: usize,
782 successes: usize,
783 resolved: usize,
784 evidence: Evidence,
785}
786
787impl UnionPlan {
788 pub(crate) fn new(evidence: Evidence, attempted: usize) -> Self {
789 Self { attempted, successes: 0, resolved: 0, evidence }
790 }
791
792 pub(crate) fn record(&mut self, success: bool) {
793 self.resolved += 1;
794 if success {
795 self.successes += 1;
796 }
797 }
798
799 pub(crate) fn satisfied(&self) -> bool {
804 match self.evidence {
805 Evidence::Fast => self.successes >= 1,
806 Evidence::Quorum => self.successes > (self.attempted / 2),
807 Evidence::Full => self.resolved >= self.attempted,
808 }
809 }
810
811 pub(crate) fn exhausted(&self) -> bool {
813 self.resolved >= self.attempted
814 }
815
816 pub(crate) fn successes(&self) -> usize {
817 self.successes
818 }
819
820 pub(crate) fn attempted(&self) -> usize {
821 self.attempted
822 }
823}
824
825#[cfg(feature = "tor")]
829const TOR_READY_WAIT: std::time::Duration = std::time::Duration::from_secs(30);
830
831#[allow(dead_code)]
841async fn wait_until_tor_ready<F: Fn() -> bool>(
842 is_blocked: F,
843 max_wait: std::time::Duration,
844) -> Result<(), String> {
845 if !is_blocked() {
846 return Ok(());
847 }
848 let deadline = web_time::Instant::now() + max_wait;
849 while is_blocked() {
850 if web_time::Instant::now() >= deadline {
851 return Err("Tor is still connecting. Wait a moment and try again.".to_string());
852 }
853 crate::rt::time::sleep(std::time::Duration::from_millis(250)).await;
854 }
855 Ok(())
856}
857
858pub async fn prune_unneeded_community_relays(candidates: &[String]) {
866 if candidates.is_empty() {
867 return;
868 }
869 let Some(client) = crate::state::nostr_client() else { return };
870
871 let mut still_needed: std::collections::HashSet<String> = std::collections::HashSet::new();
872 if let Ok(ids) = crate::db::community::list_community_ids() {
873 for id in ids {
874 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
875 for r in &c.relays {
876 still_needed.insert(r.clone());
877 }
878 }
879 }
880 }
881
882 let pool = client;
883 let pooled = pool.relays().all().await;
885 for url in candidates {
886 if still_needed.contains(url) {
887 continue;
888 }
889 if let Ok(parsed) = nostr_sdk::prelude::RelayUrl::parse(url) {
890 if let Some(relay) = pooled.get(&parsed) {
891 if relay.capabilities().load().can_read() || relay.capabilities().load().can_write() {
892 continue; }
894 }
895 let _ = pool.remove_relay(parsed).force().await; forget_warmed_relay(url);
897 }
898 }
899}
900
901pub struct LiveTransport {
908 timeout: std::time::Duration,
909}
910
911impl Default for LiveTransport {
912 fn default() -> Self {
913 Self::with_timeout(std::time::Duration::from_secs(10))
914 }
915}
916
917impl LiveTransport {
918 pub fn new() -> Self {
919 Self::default()
920 }
921
922 pub fn with_timeout(timeout: std::time::Duration) -> Self {
928 Self { timeout: crate::relay_request_timeout(timeout) }
929 }
930
931 pub(crate) async fn warm_client(relays: &[String], connect_timeout: std::time::Duration) -> Result<Client, String> {
937 if relays.is_empty() {
938 return Err("community has no relays configured".to_string());
939 }
940 #[cfg(feature = "tor")]
946 wait_until_tor_ready(
947 || matches!(crate::tor::transport_state(), crate::tor::TorTransportState::RequiredButInactive),
948 TOR_READY_WAIT,
949 ).await?;
950 let client = crate::state::nostr_client().ok_or_else(|| "nostr client not initialized".to_string())?;
951
952 let generation = crate::db::current_session_id();
956 {
957 let warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
958 if warmed.0 == generation && relays.iter().all(|r| warmed.1.contains(r)) {
959 return Ok(client);
960 }
961 }
962
963 let mut added_new = false;
969 let mut succeeded: Vec<&String> = Vec::new();
970 for url in relays {
971 match client
972 .add_managed_relay(url.as_str())
973 .capabilities(crate::community_relay_capabilities())
974 .await
975 {
976 Ok(true) => { added_new = true; succeeded.push(url); }
977 Ok(false) => { succeeded.push(url); }
978 Err(_) => {}
979 }
980 }
981 if succeeded.is_empty() {
982 return Err("no valid community relays could be added".to_string());
983 }
984 if added_new {
985 let _ = client.try_connect().timeout(connect_timeout).await;
990 } else {
991 client.connect().await;
993 }
994
995 {
998 let mut warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
999 if warmed.0 != generation {
1000 warmed.0 = generation;
1001 warmed.1.clear();
1002 }
1003 for url in succeeded {
1004 warmed.1.insert(url.clone());
1005 }
1006 }
1007 Ok(client)
1008 }
1009
1010 pub async fn fetch_counted(&self, query: &Query, relays: &[String]) -> Result<(Vec<Event>, usize, usize), String> {
1015 let client = Self::warm_client(relays, self.timeout).await?;
1016 let base_timeout = self.timeout;
1017 let filter = query.to_filter();
1018
1019 let evidence = effective_evidence(query);
1020
1021 let mut targets: Vec<String> = Vec::new();
1022 for r in relays {
1023 if !targets.contains(r) {
1024 targets.push(r.clone());
1025 }
1026 }
1027 targets = drop_unrevivable_targets(&client, targets).await;
1032
1033 if evidence == Evidence::Fast && targets.len() >= 2 {
1038 let alive: Vec<String> =
1039 targets.iter().filter(|r| !breaker_tripped(r)).cloned().collect();
1040 if !alive.is_empty() {
1041 targets = alive;
1042 }
1043 }
1044
1045 if targets.len() <= 1 {
1048 let Some(url) = targets.first() else {
1049 return Err("no valid relay to fetch from".to_string());
1050 };
1051 let res = fetch_relay_eose(&client, url, filter, base_timeout)
1052 .await
1053 .map_err(|_| format!("relay did not answer the fetch: {url}"));
1054 breaker_record(url, res.is_ok(), true);
1055 return res.map(|evs| (evs, 1, 1));
1056 }
1057
1058 fn merge_events(
1059 evs: Vec<Event>,
1060 result: &mut Vec<Event>,
1061 seen: &mut std::collections::HashSet<EventId>,
1062 ) {
1063 for e in evs {
1064 if seen.insert(e.id) {
1065 result.push(e);
1066 }
1067 }
1068 }
1069
1070 let demote = evidence == Evidence::Full && demotion_allowed();
1077 use futures_util::stream::{FuturesUnordered, StreamExt};
1078 let mut fetches: FuturesUnordered<_> = targets
1079 .iter()
1080 .map(|r| {
1081 let client = client.clone();
1082 let filter = filter.clone();
1083 let r = r.clone();
1084 let timeout = if demote && breaker_tripped(&r) {
1085 TRIPPED_TIMEOUT.min(base_timeout)
1086 } else {
1087 base_timeout
1088 };
1089 let full_budget = timeout >= base_timeout;
1090 crate::rt::spawn(async move {
1092 let out = fetch_relay_eose(&client, &r, filter, timeout).await;
1093 (r, full_budget, out)
1094 })
1095 })
1096 .collect();
1097
1098 let mut plan = UnionPlan::new(evidence, targets.len());
1099 let mut result: Vec<Event> = Vec::new();
1100 let mut union_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1101
1102 let mut quorum_deadline: Option<crate::rt::time::Instant> = None;
1107 let mut quorum_window_closed = false;
1108 while !plan.satisfied() && !plan.exhausted() {
1109 let next = match quorum_deadline {
1110 Some(deadline) => match crate::rt::time::timeout_at(deadline, fetches.next()).await {
1111 Ok(n) => n,
1112 Err(_) => {
1113 quorum_window_closed = true;
1114 break; }
1116 },
1117 None => fetches.next().await,
1118 };
1119 let Some(joined) = next else { break };
1120 match joined {
1121 Ok((url, full_budget, Ok(evs))) => {
1122 breaker_record(&url, true, full_budget);
1123 merge_events(evs, &mut result, &mut union_ids);
1124 plan.record(true);
1125 if evidence == Evidence::Quorum && quorum_deadline.is_none() {
1126 quorum_deadline = Some(
1127 crate::rt::time::Instant::now()
1128 + std::time::Duration::from_millis(QUORUM_GRACE_MS),
1129 );
1130 }
1131 }
1132 Ok((url, full_budget, Err(()))) => {
1133 breaker_record(&url, false, full_budget);
1134 plan.record(false);
1135 }
1136 Err(_) => plan.record(false), }
1138 }
1139
1140 if plan.successes() == 0 {
1141 return Err(format!(
1142 "no relay answered the fetch (0/{} attempted)",
1143 plan.attempted()
1144 ));
1145 }
1146
1147 if !fetches.is_empty() && !quorum_window_closed {
1151 let grace = crate::rt::time::sleep(std::time::Duration::from_millis(RESIDUAL_GRACE_MS));
1152 tokio::pin!(grace);
1153 loop {
1154 tokio::select! {
1155 _ = &mut grace => break,
1156 next = fetches.next() => match next {
1157 Some(Ok((url, full_budget, Ok(evs)))) => {
1158 breaker_record(&url, true, full_budget);
1159 merge_events(evs, &mut result, &mut union_ids);
1160 }
1161 Some(Ok((url, full_budget, Err(())))) => {
1162 breaker_record(&url, false, full_budget);
1163 }
1164 Some(Err(_)) => continue,
1165 None => break,
1166 }
1167 }
1168 }
1169 }
1170
1171 if !fetches.is_empty() {
1175 let seen: std::collections::HashSet<EventId> = result.iter().map(|e| e.id).collect();
1176 crate::db::spawn_bound(async move {
1179 let mut extra: Vec<Event> = Vec::new();
1180 let mut extra_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1181 while let Some(joined) = fetches.next().await {
1182 if let Ok((url, full_budget, out)) = joined {
1183 breaker_record(&url, out.is_ok(), full_budget);
1187 if let Ok(evs) = out {
1188 for e in evs {
1189 if !seen.contains(&e.id) && extra_ids.insert(e.id) {
1190 extra.push(e);
1191 }
1192 }
1193 }
1194 }
1195 }
1196 submit_stragglers(extra);
1197 });
1198 }
1199
1200 Ok((result, plan.successes(), plan.attempted()))
1201 }
1202}
1203
1204#[async_trait::async_trait]
1205impl Transport for LiveTransport {
1206 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1207 let client = Self::warm_client(relays, self.timeout).await?;
1208 let timeout = self.timeout;
1209 let mut targets: Vec<String> = Vec::new();
1210 for r in relays { if !targets.contains(r) { targets.push(r.clone()); } }
1211 targets = drop_unrevivable_targets(&client, targets).await;
1213 use futures_util::stream::{FuturesUnordered, StreamExt};
1218 let mut sends: FuturesUnordered<_> = targets
1219 .into_iter()
1220 .map(|r| {
1221 let client = client.clone();
1222 let event = event.clone();
1223 crate::rt::spawn(async move {
1225 matches!(
1226 crate::rt::time::timeout(timeout, client.send_event(&event).to(vec![r.clone()])).await,
1227 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains_key(&u)).unwrap_or(false)
1228 )
1229 })
1230 })
1231 .collect();
1232 while let Some(joined) = sends.next().await {
1233 if matches!(joined, Ok(true)) {
1234 return Ok(()); }
1236 }
1237 Err("no relay accepted the event".to_string())
1238 }
1239
1240 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1241 self.fetch_counted(query, relays).await.map(|(events, _successes, _attempted)| events)
1242 }
1243
1244 async fn fetch_plane(&self, plane: &Keys, query: &Query, relays: &[String]) -> Result<PlaneFetch, String> {
1245 if relays.is_empty() {
1246 return Ok(PlaneFetch { events: Vec::new(), answered: 0, attempted: 0 });
1247 }
1248 #[cfg(feature = "tor")]
1249 wait_until_tor_ready(
1250 || matches!(crate::tor::transport_state(), crate::tor::TorTransportState::RequiredButInactive),
1251 TOR_READY_WAIT,
1252 ).await?;
1253 let mut targets: Vec<String> = relays.iter().filter(|r| !breaker_tripped(r)).cloned().collect();
1258 if targets.is_empty() {
1259 targets = relays.to_vec();
1260 }
1261 let filter = query.to_filter();
1262 let generation = crate::db::current_session_id();
1263 let key = plane_pool_key(&plane.public_key().to_hex(), &targets);
1264
1265 let (hit, evicted) = plane_pool_take(generation, &key);
1268 disconnect_clients(evicted);
1269 let client = if let Some(c) = hit {
1270 c
1271 } else {
1272 let client = crate::apply_tor_proxy(
1278 nostr_sdk::prelude::Client::builder().authenticator(
1279 nostr_sdk::prelude::SignerAuthenticator::new(plane.clone()),
1280 ),
1281 )
1282 .build();
1283 for r in &targets {
1288 let _ = client.add_managed_relay(r.clone()).capabilities(crate::community_relay_capabilities()).await;
1289 }
1290 client.connect().await;
1291 for r in &targets {
1294 let _ = client
1295 .fetch_events(nostr_sdk::prelude::ReqTarget::single(r.clone(), [filter.clone()]))
1296 .timeout(crate::relay_request_timeout(std::time::Duration::from_secs(5)))
1300 .await;
1301 }
1302 let ev = plane_pool_insert(generation, key, client.clone());
1303 disconnect_clients(ev);
1304 client
1305 };
1306
1307 let targets = drop_unrevivable_targets(&client, targets).await;
1311
1312 let mut result: Vec<Event> = Vec::new();
1313 let mut seen: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1314 let mut successes = 0usize;
1315 {
1320 use futures_util::stream::{FuturesUnordered, StreamExt};
1321 let mut inflight: FuturesUnordered<_> = targets
1322 .iter()
1323 .map(|r| {
1324 let client = client.clone();
1325 let filter = filter.clone();
1326 let timeout = self.timeout;
1327 async move { (r.clone(), fetch_relay_eose(&client, r, filter, timeout).await) }
1328 })
1329 .collect();
1330 while let Some((r, res)) = inflight.next().await {
1331 breaker_record(&r, res.is_ok(), true);
1334 if let Ok(events) = res {
1335 successes += 1;
1336 for e in events {
1337 if seen.insert(e.id) {
1338 result.push(e);
1339 }
1340 }
1341 }
1342 }
1343 }
1344 if successes == 0 {
1349 return Err(format!("no relay answered the plane fetch (0/{} attempted)", targets.len()));
1350 }
1351 Ok(PlaneFetch { events: result, answered: successes, attempted: targets.len() })
1353 }
1354
1355 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1356 let client = Self::warm_client(relays, self.timeout).await?;
1361 let timeout = self.timeout;
1362 let event = event.clone();
1363 let backoff = std::time::Duration::from_millis(750);
1364 let mut pending: Vec<String> = Vec::new();
1365 for r in relays { if !pending.contains(r) { pending.push(r.clone()); } }
1366 if pending.is_empty() {
1367 return Err("no relays to broadcast to".to_string());
1368 }
1369
1370 let mut acked_any = false;
1375 let _ = crate::rt::time::timeout(CONFIRM_WINDOW, async {
1376 loop {
1377 let round = drop_unrevivable_targets(&client, pending.clone()).await;
1381 let sends = round.iter().cloned().map(|r| {
1382 let client = &client;
1383 let event = &event;
1384 Box::pin(async move {
1385 match crate::rt::time::timeout(timeout, client.send_event(event).to(vec![r.clone()])).await {
1386 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains_key(&u)).unwrap_or(false) => Ok(r),
1387 _ => Err(()),
1388 }
1389 })
1390 });
1391 if let Ok((winner, _losers)) = futures_util::future::select_ok(sends).await {
1392 acked_any = true;
1393 pending.retain(|r| r != &winner);
1394 break;
1395 }
1396 crate::rt::time::sleep(backoff).await;
1397 }
1398 })
1399 .await;
1400
1401 if !acked_any {
1402 return Err(format!("no relay accepted the event within {}s", CONFIRM_WINDOW.as_secs()));
1403 }
1404 if pending.is_empty() {
1405 return Ok(()); }
1407
1408 crate::rt::spawn(async move {
1414 let client_ref = &client;
1415 let event_ref = &event;
1416 let _ = durable_broadcast(&pending, MAX_PUBLISH_ATTEMPTS, backoff, move |round| {
1417 Box::pin(async move {
1418 match crate::rt::time::timeout(timeout, client_ref.send_event(event_ref).to(round.clone())).await {
1419 Ok(Ok(output)) => round.into_iter().filter(|p| RelayUrl::parse(p).map(|u| output.success.contains_key(&u)).unwrap_or(false)).collect(),
1420 _ => Vec::new(),
1421 }
1422 })
1423 })
1424 .await;
1425 });
1426 Ok(())
1427 }
1428}
1429
1430#[cfg(test)]
1431pub(crate) mod memory {
1432 use super::*;
1433 use std::collections::{HashMap, HashSet};
1434 use std::sync::Mutex;
1435
1436 pub struct MemoryRelay {
1441 per_relay: Mutex<HashMap<String, Vec<Event>>>,
1442 subscribers: Mutex<Vec<(Query, tokio::sync::mpsc::UnboundedSender<Event>)>>,
1443 }
1444
1445 fn is_ephemeral(kind: u16) -> bool {
1448 (20000..30000).contains(&kind)
1449 }
1450
1451 impl MemoryRelay {
1452 pub fn new() -> Self {
1453 MemoryRelay {
1454 per_relay: Mutex::new(HashMap::new()),
1455 subscribers: Mutex::new(Vec::new()),
1456 }
1457 }
1458
1459 pub fn stored_count(&self) -> usize {
1463 self.per_relay.lock().unwrap().values().map(|v| v.len()).sum()
1464 }
1465
1466 pub fn subscribe(&self, query: Query) -> tokio::sync::mpsc::UnboundedReceiver<Event> {
1469 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1470 self.subscribers.lock().unwrap().push((query, tx));
1471 rx
1472 }
1473
1474 fn deliver(&self, event: &Event) {
1476 self.subscribers.lock().unwrap().retain(|(q, tx)| {
1477 if q.matches(event) {
1478 tx.send(event.clone()).is_ok()
1479 } else {
1480 !tx.is_closed()
1481 }
1482 });
1483 }
1484
1485 pub fn inject(&self, event: &Event, relays: &[String]) {
1488 self.deliver(event);
1489 if is_ephemeral(event.kind.as_u16()) {
1490 return; }
1492 let d_tag = |e: &Event| e.tags.iter().find_map(|t| {
1498 let s = t.as_slice();
1499 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
1500 }).unwrap_or_default();
1501 let k = event.kind.as_u16();
1502 let replaceable = (30000..40000).contains(&k) || (10000..20000).contains(&k) || k == 0 || k == 3;
1503 let coord = (event.kind.as_u16(), event.pubkey, d_tag(event));
1504 let mut map = self.per_relay.lock().unwrap();
1505 for r in relays {
1506 let v = map.entry(r.clone()).or_default();
1507 if replaceable {
1508 v.retain(|e| (e.kind.as_u16(), e.pubkey, d_tag(e)) != coord);
1509 }
1510 v.push(event.clone());
1511 }
1512 }
1513
1514 pub fn count_on(&self, relay: &str) -> usize {
1516 self.per_relay.lock().unwrap().get(relay).map_or(0, |v| v.len())
1517 }
1518
1519 fn apply_deletion(&self, deletion: &Event, relays: &[String]) {
1524 let mut id_targets: HashSet<String> = HashSet::new();
1525 let mut coord_targets: HashSet<String> = HashSet::new();
1526 for t in deletion.tags.iter() {
1527 let s = t.as_slice();
1528 if s.len() >= 2 && s[0] == "e" {
1529 id_targets.insert(s[1].clone());
1530 } else if s.len() >= 2 && s[0] == "a" {
1531 coord_targets.insert(s[1].clone());
1532 }
1533 }
1534 let mut map = self.per_relay.lock().unwrap();
1535 for r in relays {
1536 if let Some(events) = map.get_mut(r) {
1537 events.retain(|e| {
1538 if e.pubkey != deletion.pubkey {
1539 return true;
1540 }
1541 if id_targets.contains(&e.id.to_hex()) {
1542 return false;
1543 }
1544 let d = e.tags.iter().find_map(|t| {
1546 let s = t.as_slice();
1547 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
1548 }).unwrap_or_default();
1549 let coord = format!("{}:{}:{}", e.kind.as_u16(), e.pubkey.to_hex(), d);
1550 !coord_targets.contains(&coord)
1551 });
1552 }
1553 }
1554 }
1555 }
1556
1557 #[async_trait::async_trait]
1558 impl Transport for MemoryRelay {
1559 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1560 if event.kind == Kind::EventDeletion {
1562 self.apply_deletion(event, relays);
1563 self.deliver(event);
1564 } else {
1565 self.inject(event, relays);
1566 }
1567 Ok(())
1568 }
1569
1570 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1573 self.publish(event, relays).await
1574 }
1575
1576 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1577 let map = self.per_relay.lock().unwrap();
1578 let mut seen = HashSet::new();
1579 let mut out = Vec::new();
1580 for r in relays {
1581 if let Some(events) = map.get(r) {
1582 for ev in events {
1583 if is_ephemeral(ev.kind.as_u16()) {
1586 continue;
1587 }
1588 if query.matches(ev) && seen.insert(ev.id) {
1589 out.push(ev.clone());
1590 }
1591 }
1592 }
1593 }
1594 if let Some(limit) = query.limit {
1597 out.sort_by(|a, b| b.created_at.cmp(&a.created_at).then_with(|| b.id.cmp(&a.id)));
1598 out.truncate(limit);
1599 }
1600 Ok(out)
1601 }
1602
1603 async fn fetch_plane(&self, _plane: &Keys, query: &Query, relays: &[String]) -> Result<PlaneFetch, String> {
1604 Ok(PlaneFetch::whole(self.fetch(query, relays).await?))
1606 }
1607 }
1608}
1609
1610#[cfg(test)]
1621pub(crate) mod adversarial {
1622 #![allow(dead_code)]
1625
1626 use super::memory::MemoryRelay;
1627 use super::*;
1628 use std::sync::atomic::{AtomicU64, Ordering};
1629 use std::sync::Mutex;
1630
1631 #[derive(Clone, Debug)]
1633 pub enum Fault {
1634 FailFetch(String),
1636 ShortPage(usize),
1639 Empty,
1641 }
1642
1643 #[derive(Clone, Debug, Default)]
1646 pub struct Match {
1647 pub author: Option<String>,
1649 pub nth: Option<u64>,
1651 pub from_nth: Option<u64>,
1653 }
1654
1655 impl Match {
1656 pub fn author(hex: &str) -> Self {
1657 Match { author: Some(hex.to_string()), ..Default::default() }
1658 }
1659 pub fn any() -> Self {
1660 Match::default()
1661 }
1662 pub fn nth(mut self, n: u64) -> Self {
1663 self.nth = Some(n);
1664 self
1665 }
1666 pub fn from_nth(mut self, n: u64) -> Self {
1667 self.from_nth = Some(n);
1668 self
1669 }
1670 }
1671
1672 struct Rule {
1673 m: Match,
1674 fault: Fault,
1675 hits: AtomicU64,
1676 }
1677
1678 pub struct AdversarialRelay {
1679 inner: MemoryRelay,
1680 rules: Mutex<Vec<Rule>>,
1681 fetches: AtomicU64,
1684 }
1685
1686 impl AdversarialRelay {
1687 pub fn new() -> Self {
1688 AdversarialRelay { inner: MemoryRelay::new(), rules: Mutex::new(Vec::new()), fetches: AtomicU64::new(0) }
1689 }
1690
1691 pub fn relay(&self) -> &MemoryRelay {
1694 &self.inner
1695 }
1696
1697 pub fn on(&self, m: Match, fault: Fault) -> &Self {
1698 self.rules.lock().unwrap_or_else(|e| e.into_inner()).push(Rule { m, fault, hits: AtomicU64::new(0) });
1699 self
1700 }
1701
1702 pub fn heal(&self) {
1705 self.rules.lock().unwrap_or_else(|e| e.into_inner()).clear();
1706 }
1707
1708 pub fn fetch_count(&self) -> u64 {
1709 self.fetches.load(Ordering::Relaxed)
1710 }
1711
1712 pub fn reset_fetch_count(&self) {
1713 self.fetches.store(0, Ordering::Relaxed);
1714 }
1715
1716 fn verdict(&self, query: &Query) -> Option<Fault> {
1717 let rules = self.rules.lock().unwrap_or_else(|e| e.into_inner());
1718 for r in rules.iter() {
1719 if let Some(a) = &r.m.author {
1720 if !query.authors.iter().any(|q| q == a) {
1721 continue;
1722 }
1723 }
1724 let n = r.hits.fetch_add(1, Ordering::Relaxed) + 1;
1725 if r.m.nth.is_some_and(|want| want != n) {
1726 continue;
1727 }
1728 if r.m.from_nth.is_some_and(|first| n < first) {
1729 continue;
1730 }
1731 return Some(r.fault.clone());
1732 }
1733 None
1734 }
1735
1736 async fn governed(&self, query: &Query, relays: &[String], plane: Option<&Keys>) -> Result<Vec<Event>, String> {
1737 self.fetches.fetch_add(1, Ordering::Relaxed);
1738 match self.verdict(query) {
1739 Some(Fault::FailFetch(why)) => return Err(why),
1740 Some(Fault::Empty) => return Ok(Vec::new()),
1741 Some(Fault::ShortPage(n)) => {
1742 let mut out = match plane {
1743 Some(p) => self.inner.fetch_plane(p, query, relays).await?.events,
1744 None => self.inner.fetch(query, relays).await?,
1745 };
1746 out.truncate(n);
1747 return Ok(out);
1748 }
1749 None => {}
1750 }
1751 match plane {
1752 Some(p) => self.inner.fetch_plane(p, query, relays).await.map(|f| f.events),
1753 None => self.inner.fetch(query, relays).await,
1754 }
1755 }
1756 }
1757
1758 #[async_trait::async_trait]
1759 impl Transport for AdversarialRelay {
1760 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1761 self.inner.publish(event, relays).await
1762 }
1763 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1764 self.inner.publish_durable(event, relays).await
1765 }
1766 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1767 self.governed(query, relays, None).await
1768 }
1769 async fn fetch_plane(&self, plane: &Keys, query: &Query, relays: &[String]) -> Result<PlaneFetch, String> {
1770 Ok(PlaneFetch::whole(self.governed(query, relays, Some(plane)).await?))
1774 }
1775 }
1776}
1777
1778#[cfg(test)]
1779mod tests {
1780 use super::*;
1781
1782 #[test]
1785 fn a_dead_socket_is_skipped_but_a_connecting_or_unknown_one_is_not() {
1786 let targets: Vec<String> = ["wss://dead", "wss://connecting", "wss://unknown"].map(String::from).into();
1787 let out = drop_unrevivable(targets, |r| r == "wss://dead");
1788 assert_eq!(out, ["wss://connecting", "wss://unknown"].map(String::from).to_vec());
1789 }
1790
1791 #[test]
1792 fn an_all_dead_set_falls_back_to_the_full_list() {
1793 let targets: Vec<String> = ["wss://a", "wss://b"].map(String::from).into();
1796 let out = drop_unrevivable(targets.clone(), |_| true);
1797 assert_eq!(out, targets);
1798 }
1799
1800 #[tokio::test]
1805 async fn tor_gate_passes_immediately_when_not_blocked() {
1806 let start = web_time::Instant::now();
1807 let res = wait_until_tor_ready(|| false, std::time::Duration::from_secs(30)).await;
1808 assert!(res.is_ok());
1809 assert!(start.elapsed() < std::time::Duration::from_secs(1), "must not wait when Tor is ready");
1810 }
1811
1812 #[tokio::test]
1813 async fn tor_gate_errors_honestly_after_timeout_when_perpetually_blocked() {
1814 let res = wait_until_tor_ready(|| true, std::time::Duration::from_millis(300)).await;
1817 let err = res.expect_err("should error when Tor never activates");
1818 assert!(err.to_lowercase().contains("tor"), "error must name Tor, got: {err}");
1819 }
1820
1821 #[tokio::test]
1822 async fn tor_gate_passes_once_circuit_comes_up_mid_wait() {
1823 let calls = std::sync::atomic::AtomicUsize::new(0);
1824 let res = wait_until_tor_ready(
1826 || calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 3,
1827 std::time::Duration::from_secs(5),
1828 ).await;
1829 assert!(res.is_ok(), "should succeed once Tor activates within the window");
1830 }
1831
1832 fn evt(kind: u16, z: &str) -> Event {
1833 EventBuilder::new(Kind::Custom(kind), "x")
1834 .tags([Tag::custom(
1835 "z",
1836 [z.to_string()],
1837 )])
1838 .finalize(&Keys::generate())
1839 .unwrap()
1840 }
1841
1842 #[test]
1843 fn query_matches_kind_and_z() {
1844 let e = evt(3300, "abc");
1845 assert!(Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: None, ..Default::default() }.matches(&e));
1846 assert!(!Query { kinds: vec![3301], ..Default::default() }.matches(&e));
1847 assert!(!Query { kinds: vec![], z_tags: vec!["xyz".into()], since: None, ..Default::default() }.matches(&e));
1848 assert!(Query::default().matches(&e), "empty query matches anything");
1849 }
1850
1851 fn evt_at(kind: u16, secs: u64) -> Event {
1852 EventBuilder::new(Kind::Custom(kind), "x")
1853 .custom_created_at(Timestamp::from(secs))
1854 .finalize(&Keys::generate())
1855 .unwrap()
1856 }
1857
1858 fn evt_z_at(kind: u16, z: &str, secs: u64) -> Event {
1861 EventBuilder::new(Kind::Custom(kind), "x")
1862 .custom_created_at(Timestamp::from(secs))
1863 .tags([Tag::custom(
1864 "z",
1865 [z.to_string()],
1866 )])
1867 .finalize(&Keys::generate())
1868 .unwrap()
1869 }
1870
1871 #[tokio::test]
1872 async fn fetch_pages_with_until_and_limit_newest_first() {
1873 let relay = super::memory::MemoryRelay::new();
1875 let relays = vec!["r1".to_string()];
1876 for s in 1..=5u64 {
1877 relay.inject(&evt_z_at(3300, "pg", s), &relays);
1878 }
1879 let secs = |evs: &[Event]| evs.iter().map(|e| e.created_at.as_secs()).collect::<Vec<_>>();
1880
1881 let latest = relay
1883 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], limit: Some(2), ..Default::default() }, &relays)
1884 .await
1885 .unwrap();
1886 assert_eq!(secs(&latest), vec![5, 4]);
1887
1888 let older = relay
1890 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(3), limit: Some(2), ..Default::default() }, &relays)
1891 .await
1892 .unwrap();
1893 assert_eq!(secs(&older), vec![3, 2]);
1894
1895 let start = relay
1898 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(1), limit: Some(2), ..Default::default() }, &relays)
1899 .await
1900 .unwrap();
1901 assert_eq!(secs(&start), vec![1]);
1902 }
1903
1904 #[test]
1905 fn to_filter_translates_kinds_z_and_since() {
1906 let q = Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: Some(100), ..Default::default() };
1909 let filter = q.to_filter();
1910 let matching = EventBuilder::new(Kind::Custom(3300), "x")
1911 .custom_created_at(Timestamp::from(150))
1912 .tags([Tag::custom(
1913 "z",
1914 ["abc".to_string()],
1915 )])
1916 .finalize(&Keys::generate())
1917 .unwrap();
1918 assert!(filter.match_event(&matching, MatchEventOptions::new()), "to_filter must accept what matches() accepts");
1919 assert!(q.matches(&matching));
1920
1921 let wrong_kind = EventBuilder::new(Kind::Custom(3301), "x")
1923 .custom_created_at(Timestamp::from(150))
1924 .tags([Tag::custom(
1925 "z",
1926 ["abc".to_string()],
1927 )])
1928 .finalize(&Keys::generate())
1929 .unwrap();
1930 assert!(!filter.match_event(&wrong_kind, MatchEventOptions::new()));
1931 }
1932
1933 fn evt_sl(kind: u16, letter: SingleLetterTag, value: &str) -> Event {
1935 EventBuilder::new(Kind::Custom(kind), "x")
1936 .tags([Tag::custom(
1937 letter.as_char().to_string(),
1938 [value.to_string()],
1939 )])
1940 .finalize(&Keys::generate())
1941 .unwrap()
1942 }
1943
1944 #[test]
1945 fn to_filter_and_matches_agree_on_authors() {
1946 let keys = Keys::generate();
1947 let e = EventBuilder::new(Kind::Custom(1059), "x").finalize(&keys).unwrap();
1948 let q = Query { kinds: vec![1059], authors: vec![keys.public_key().to_hex()], ..Default::default() };
1949 assert!(q.matches(&e));
1950 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1951 let miss = Query { kinds: vec![1059], authors: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
1952 assert!(!miss.matches(&e));
1953 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1954 }
1955
1956 #[test]
1957 fn to_filter_and_matches_agree_on_p_tags() {
1958 let recipient = Keys::generate().public_key().to_hex();
1959 let e = evt_sl(1059, SingleLetterTag::LOWERCASE_P, &recipient);
1960 let q = Query { kinds: vec![1059], p_tags: vec![recipient], ..Default::default() };
1961 assert!(q.matches(&e));
1962 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1963 let miss = Query { kinds: vec![1059], p_tags: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
1964 assert!(!miss.matches(&e));
1965 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1966 }
1967
1968 #[test]
1969 fn to_filter_and_matches_agree_on_k_tags() {
1970 let e = evt_sl(1059, SingleLetterTag::LOWERCASE_K, "3311");
1971 let q = Query { kinds: vec![1059], k_tags: vec!["3311".into()], ..Default::default() };
1972 assert!(q.matches(&e));
1973 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1974 let miss = Query { kinds: vec![1059], k_tags: vec!["3300".into()], ..Default::default() };
1975 assert!(!miss.matches(&e));
1976 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1977 }
1978
1979 #[test]
1980 fn to_filter_empty_kinds_only_constrains_z_and_since() {
1981 let q = Query { kinds: vec![], z_tags: vec!["p".into()], since: None, ..Default::default() };
1983 let filter = q.to_filter();
1984 let e = evt(3300, "p");
1985 assert!(filter.match_event(&e, MatchEventOptions::new()));
1986 assert!(q.matches(&e));
1987 }
1988
1989 #[test]
1990 fn since_is_an_inclusive_lower_bound() {
1991 let below = evt_at(3300, 99);
1992 let exact = evt_at(3300, 100);
1993 let above = evt_at(3300, 101);
1994 let q = Query { kinds: vec![3300], z_tags: vec![], since: Some(100), ..Default::default() };
1995 assert!(!q.matches(&below), "below the floor is excluded");
1996 assert!(q.matches(&exact), "exactly the floor is included");
1997 assert!(q.matches(&above), "above the floor is included");
1998 }
1999
2000 #[test]
2001 fn z_tags_match_as_or_set() {
2002 let e = evt(3300, "p2");
2005 let q = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p2".into()], since: None, ..Default::default() };
2006 assert!(q.matches(&e));
2007 let miss = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p3".into()], since: None, ..Default::default() };
2008 assert!(!miss.matches(&e));
2009 }
2010
2011 #[tokio::test]
2012 async fn fetch_unions_and_dedups_across_relays() {
2013 use super::memory::MemoryRelay;
2014 let relay = MemoryRelay::new();
2015 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
2016 let e = evt(3300, "p");
2017 relay.publish(&e, &relays).await.unwrap();
2018 let got = relay
2019 .fetch(&Query { kinds: vec![3300], z_tags: vec!["p".into()], since: None, ..Default::default() }, &relays)
2020 .await
2021 .unwrap();
2022 assert_eq!(got.len(), 1, "same event on 3 relays dedups to 1");
2023 }
2024
2025 #[tokio::test]
2026 async fn durable_broadcast_retries_only_the_failing_relays_until_they_ack() {
2027 use std::cell::Cell;
2030 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
2031 let round = Cell::new(0usize);
2032 let r2_round_seen = Cell::new(0usize);
2033 let res = durable_broadcast(&relays, 30, std::time::Duration::ZERO, |pending| {
2034 let n = round.get();
2035 round.set(n + 1);
2036 if n >= 1 {
2038 assert_eq!(pending, vec!["r2".to_string()], "only the failing relay is retried");
2039 r2_round_seen.set(r2_round_seen.get() + 1);
2040 }
2041 Box::pin(async move {
2042 pending.into_iter().filter(|r| r != "r2" || n >= 4).collect()
2043 })
2044 })
2045 .await;
2046 assert!(res.is_ok(), "all relays eventually ACK → Ok");
2047 assert!(round.get() >= 5, "kept retrying r2 across rounds");
2048 }
2049
2050 #[tokio::test]
2051 async fn durable_broadcast_is_ok_if_some_ack_even_when_one_never_does() {
2052 let relays = vec!["r1".to_string(), "r2".to_string()];
2055 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |pending| {
2056 Box::pin(async move { pending.into_iter().filter(|r| r == "r1").collect() })
2057 })
2058 .await;
2059 assert!(res.is_ok(), "≥1 relay accepted → Ok despite a permanently-failing relay");
2060 }
2061
2062 #[tokio::test]
2063 async fn durable_broadcast_errs_only_if_zero_relays_ever_accept() {
2064 let relays = vec!["r1".to_string(), "r2".to_string()];
2065 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |_pending| {
2066 Box::pin(async move { Vec::new() }) })
2068 .await;
2069 assert!(res.is_err(), "zero acceptances after the retry cap → Err");
2070 }
2071
2072 #[tokio::test]
2073 async fn redundancy_self_heals_a_missing_relay() {
2074 use super::memory::MemoryRelay;
2075 let relay = MemoryRelay::new();
2076 let all = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
2077 let e = evt(3300, "p");
2078 relay.inject(&e, &["r2".to_string()]);
2080 assert_eq!(relay.count_on("r1"), 0);
2081 assert_eq!(relay.count_on("r2"), 1);
2082 let got = relay.fetch(&Query { kinds: vec![3300], ..Default::default() }, &all).await.unwrap();
2084 assert_eq!(got.len(), 1);
2085 }
2086
2087 #[tokio::test]
2088 async fn ephemeral_kind_streams_live_but_is_never_stored_or_fetched() {
2089 use super::memory::MemoryRelay;
2090 let relay = MemoryRelay::new();
2091 let relays = vec!["r1".to_string()];
2092 let mut sub = relay.subscribe(Query { kinds: vec![21059], ..Default::default() });
2093 let e = evt(21059, "p");
2094 relay.publish(&e, &relays).await.unwrap();
2095 assert_eq!(relay.count_on("r1"), 0, "ephemeral is never stored");
2096 let got = relay
2097 .fetch(&Query { kinds: vec![21059], ..Default::default() }, &relays)
2098 .await
2099 .unwrap();
2100 assert!(got.is_empty(), "a real relay never serves an ephemeral from a fetch");
2101 assert_eq!(sub.try_recv().unwrap().id, e.id, "but a live subscriber receives it");
2102 }
2103
2104 #[tokio::test]
2105 async fn stored_kind_is_fetchable_and_delivered_live() {
2106 use super::memory::MemoryRelay;
2107 let relay = MemoryRelay::new();
2108 let relays = vec!["r1".to_string()];
2109 let mut sub = relay.subscribe(Query { kinds: vec![1059], ..Default::default() });
2110 let e = evt(1059, "p");
2111 relay.publish(&e, &relays).await.unwrap();
2112 let got = relay
2113 .fetch(&Query { kinds: vec![1059], ..Default::default() }, &relays)
2114 .await
2115 .unwrap();
2116 assert_eq!(got.len(), 1, "stored kind is fetchable");
2117 assert_eq!(sub.try_recv().unwrap().id, e.id, "and delivered to the live subscriber");
2118 }
2119
2120 #[tokio::test]
2121 async fn p_tags_route_a_giftwrap_to_the_matching_subscriber() {
2122 use super::memory::MemoryRelay;
2123 let relay = MemoryRelay::new();
2124 let relays = vec!["r1".to_string()];
2125 let alice = Keys::generate().public_key().to_hex();
2126 let bob = Keys::generate().public_key().to_hex();
2127 let mut sub_alice =
2128 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![alice.clone()], ..Default::default() });
2129 let mut sub_bob =
2130 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![bob.clone()], ..Default::default() });
2131 let wrap = evt_sl(1059, SingleLetterTag::LOWERCASE_P, &alice);
2132 relay.publish(&wrap, &relays).await.unwrap();
2133 assert_eq!(sub_alice.try_recv().unwrap().id, wrap.id, "addressed recipient gets it live");
2134 assert!(sub_bob.try_recv().is_err(), "a differently-addressed subscriber does not");
2135 let for_alice = relay
2137 .fetch(&Query { kinds: vec![1059], p_tags: vec![alice], ..Default::default() }, &relays)
2138 .await
2139 .unwrap();
2140 assert_eq!(for_alice.len(), 1);
2141 let for_bob = relay
2142 .fetch(&Query { kinds: vec![1059], p_tags: vec![bob], ..Default::default() }, &relays)
2143 .await
2144 .unwrap();
2145 assert!(for_bob.is_empty());
2146 }
2147
2148 #[test]
2151 fn union_plan_fast_satisfied_on_first_success() {
2152 let mut p = UnionPlan::new(Evidence::Fast, 4);
2153 p.record(false);
2154 assert!(!p.satisfied(), "a failure is not evidence");
2155 p.record(true);
2156 assert!(p.satisfied(), "one genuine EOSE satisfies Fast");
2157 assert!(!p.exhausted());
2158 }
2159
2160 #[test]
2161 fn union_plan_quorum_majority_math() {
2162 for (n, need) in [(2usize, 2usize), (3, 2), (4, 3), (5, 3)] {
2164 let mut p = UnionPlan::new(Evidence::Quorum, n);
2165 for _ in 0..need - 1 {
2166 p.record(true);
2167 }
2168 assert!(!p.satisfied(), "{}/{} must not satisfy quorum", need - 1, n);
2169 p.record(true);
2170 assert!(p.satisfied(), "{}/{} satisfies quorum", need, n);
2171 }
2172 }
2173
2174 #[test]
2175 fn union_plan_quorum_failures_never_substitute_for_successes() {
2176 let mut p = UnionPlan::new(Evidence::Quorum, 3);
2177 p.record(true);
2178 p.record(false);
2179 p.record(false);
2180 assert!(!p.satisfied(), "1 success + 2 failures is not a majority");
2181 assert!(p.exhausted(), "all resolved — the degraded path returns best-effort");
2182 assert_eq!(p.successes(), 1);
2183 }
2184
2185 #[test]
2186 fn union_plan_full_requires_every_relay_resolved() {
2187 let mut p = UnionPlan::new(Evidence::Full, 3);
2188 p.record(true);
2189 p.record(true);
2190 assert!(!p.satisfied(), "Full waits for the last relay even after 2 EOSEs");
2191 p.record(false);
2192 assert!(p.satisfied(), "a timeout is a resolution — Full is done");
2193 assert!(p.exhausted());
2194 }
2195
2196 #[test]
2197 fn union_plan_all_dead_is_reportable_not_a_confident_empty() {
2198 let mut p = UnionPlan::new(Evidence::Quorum, 2);
2199 p.record(false);
2200 p.record(false);
2201 assert!(p.exhausted());
2202 assert_eq!(p.successes(), 0, "the caller must map this to Err, never Ok(vec![])");
2203 }
2204
2205 const BREAKER_TEST_GEN: u64 = u64::MAX;
2211
2212 #[test]
2213 fn breaker_trips_only_after_consecutive_full_budget_failures() {
2214 let url = "wss://breaker-test-full-budget.example";
2215 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2216 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "one failure is below the threshold");
2217 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2218 assert!(breaker_tripped_at(BREAKER_TEST_GEN, url), "two consecutive full-budget failures trip");
2219 }
2220
2221 #[test]
2222 fn breaker_demoted_budget_failures_never_count() {
2223 let url = "wss://breaker-test-demoted.example";
2224 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
2225 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
2226 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
2227 assert!(
2228 !breaker_tripped_at(BREAKER_TEST_GEN, url),
2229 "demoted-budget failures must not trip (anti-starvation: the post-cooldown probe must stay reachable)"
2230 );
2231 }
2232
2233 #[test]
2234 fn breaker_success_resets_the_entry() {
2235 let url = "wss://breaker-test-reset.example";
2236 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2237 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2238 assert!(breaker_tripped_at(BREAKER_TEST_GEN, url));
2239 breaker_record_at(BREAKER_TEST_GEN, url, true, false);
2241 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "any success unconditionally resets");
2242 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2243 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "and the failure count restarted from zero");
2244 }
2245
2246 #[test]
2249 fn declared_evidence_stands_and_default_is_quorum() {
2250 assert_eq!(Query::default().evidence, Evidence::Quorum, "unclassified sites get Quorum");
2251 assert_eq!(
2252 effective_evidence(&Query { until: Some(1), evidence: Evidence::Fast, ..Default::default() }),
2253 Evidence::Fast,
2254 "chat pagination rides its declared tier — absence verdicts request Full themselves"
2255 );
2256 assert_eq!(
2257 effective_evidence(&Query { evidence: Evidence::Fast, ..Default::default() }),
2258 Evidence::Fast,
2259 "without `until` the declared tier stands"
2260 );
2261 }
2262}