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(Clone, Debug, Default)]
46pub struct Query {
47 pub kinds: Vec<u16>,
48 pub z_tags: Vec<String>,
50 pub d_tags: Vec<String>,
53 pub p_tags: Vec<String>,
56 pub k_tags: Vec<String>,
59 pub authors: Vec<String>,
61 pub since: Option<u64>,
63 pub until: Option<u64>,
66 pub limit: Option<usize>,
68 pub evidence: Evidence,
72}
73
74impl Query {
75 pub fn matches(&self, event: &Event) -> bool {
77 if !self.kinds.is_empty() && !self.kinds.iter().any(|k| Kind::Custom(*k) == event.kind) {
81 return false;
82 }
83 if let Some(since) = self.since {
84 if event.created_at.as_secs() < since {
85 return false;
86 }
87 }
88 if let Some(until) = self.until {
89 if event.created_at.as_secs() > until {
90 return false;
91 }
92 }
93 if !self.authors.is_empty() && !self.authors.iter().any(|a| *a == event.pubkey.to_hex()) {
94 return false;
95 }
96 if !self.z_tags.is_empty() && !self.matches_single_letter("z", &self.z_tags, event) {
97 return false;
98 }
99 if !self.d_tags.is_empty() && !self.matches_single_letter("d", &self.d_tags, event) {
100 return false;
101 }
102 if !self.p_tags.is_empty() && !self.matches_single_letter("p", &self.p_tags, event) {
103 return false;
104 }
105 if !self.k_tags.is_empty() && !self.matches_single_letter("k", &self.k_tags, event) {
106 return false;
107 }
108 true
109 }
110
111 fn matches_single_letter(&self, name: &str, wanted: &[String], event: &Event) -> bool {
112 event.tags.iter().any(|t| {
115 let s = t.as_slice();
116 s.len() >= 2 && s[0] == name && wanted.iter().any(|w| *w == s[1])
117 })
118 }
119
120 pub fn to_filter(&self) -> Filter {
122 let mut filter = Filter::new();
123 if !self.kinds.is_empty() {
124 filter = filter.kinds(self.kinds.iter().map(|k| Kind::Custom(*k)));
125 }
126 if !self.z_tags.is_empty() {
127 filter = filter
128 .custom_tags(SingleLetterTag::lowercase(Alphabet::Z), self.z_tags.clone());
129 }
130 if !self.d_tags.is_empty() {
131 filter = filter.identifiers(self.d_tags.clone());
132 }
133 if !self.p_tags.is_empty() {
134 filter = filter
135 .custom_tags(SingleLetterTag::lowercase(Alphabet::P), self.p_tags.clone());
136 }
137 if !self.k_tags.is_empty() {
138 filter = filter
139 .custom_tags(SingleLetterTag::lowercase(Alphabet::K), self.k_tags.clone());
140 }
141 if !self.authors.is_empty() {
142 let authors: Vec<PublicKey> =
143 self.authors.iter().filter_map(|a| PublicKey::from_hex(a).ok()).collect();
144 if !authors.is_empty() {
145 filter = filter.authors(authors);
146 }
147 }
148 if let Some(since) = self.since {
149 filter = filter.since(Timestamp::from_secs(since));
150 }
151 if let Some(until) = self.until {
152 filter = filter.until(Timestamp::from_secs(until));
153 }
154 if let Some(limit) = self.limit {
155 filter = filter.limit(limit);
156 }
157 filter
158 }
159}
160
161#[async_trait::async_trait]
165pub trait Transport {
166 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String>;
168 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String>;
170
171 async fn fetch_plane(&self, plane: &Keys, query: &Query, relays: &[String]) -> Result<Vec<Event>, String>;
180
181 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String>;
189}
190
191pub const MAX_PUBLISH_ATTEMPTS: usize = 30;
194
195pub const RESIDUAL_GRACE_MS: u64 = 400;
199
200pub const QUORUM_GRACE_MS: u64 = 2000;
206
207const BREAKER_TRIP_THRESHOLD: u8 = 2;
209
210const BREAKER_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(30);
213
214const TRIPPED_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(6);
218
219pub const CONFIRM_WINDOW: std::time::Duration = std::time::Duration::from_secs(30);
223
224pub async fn durable_broadcast<'a, F>(
231 relays: &[String],
232 max_attempts: usize,
233 backoff: std::time::Duration,
234 mut send_round: F,
235) -> Result<(), String>
236where
237 F: FnMut(Vec<String>) -> std::pin::Pin<Box<dyn std::future::Future<Output = Vec<String>> + Send + 'a>>,
238{
239 let mut pending: Vec<String> = Vec::new();
240 for r in relays {
241 if !pending.contains(r) {
242 pending.push(r.clone());
243 }
244 }
245 let total = pending.len();
246 if total == 0 {
247 return Err("no relays to broadcast to".to_string());
248 }
249 for attempt in 0..max_attempts {
250 if pending.is_empty() {
251 break;
252 }
253 let acked = send_round(pending.clone()).await;
254 pending.retain(|r| !acked.contains(r));
255 if pending.is_empty() || attempt + 1 == max_attempts {
256 break;
257 }
258 if !backoff.is_zero() {
259 tokio::time::sleep(backoff).await;
260 }
261 }
262 if pending.len() < total {
263 Ok(()) } else {
265 Err(format!("no relay accepted the event after {max_attempts} attempts each"))
266 }
267}
268
269pub trait CommunityIngestSink: Send + Sync + 'static {
278 fn ingest_stragglers(&self, events: Vec<Event>);
279}
280
281static INGEST_SINK: std::sync::OnceLock<Box<dyn CommunityIngestSink>> = std::sync::OnceLock::new();
282
283pub fn set_community_ingest_sink(sink: Box<dyn CommunityIngestSink>) {
285 let _ = INGEST_SINK.set(sink);
286}
287
288fn submit_stragglers(events: Vec<Event>) {
289 if events.is_empty() {
290 return;
291 }
292 if let Some(sink) = INGEST_SINK.get() {
293 sink.ingest_stragglers(events);
294 }
295}
296
297static WARMED_RELAYS: std::sync::LazyLock<std::sync::Mutex<(u64, std::collections::HashSet<String>)>> =
303 std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashSet::new())));
304
305pub fn forget_warmed_relay(url: &str) {
310 WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner()).1.remove(url);
311}
312
313#[derive(Default)]
320struct BreakerEntry {
321 consecutive_failures: u8,
322 tripped_until: Option<std::time::Instant>,
323}
324
325static RELAY_BREAKER: std::sync::LazyLock<
326 std::sync::Mutex<(u64, std::collections::HashMap<String, BreakerEntry>)>,
327> = std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashMap::new())));
328
329fn with_breaker_at<R>(
334 generation: u64,
335 f: impl FnOnce(&mut std::collections::HashMap<String, BreakerEntry>) -> R,
336) -> R {
337 let mut guard = RELAY_BREAKER.lock().unwrap_or_else(|e| e.into_inner());
338 if guard.0 != generation {
339 guard.0 = generation;
340 guard.1.clear();
341 }
342 f(&mut guard.1)
343}
344
345fn drop_unrevivable(targets: Vec<String>, is_unrevivable: impl Fn(&str) -> bool) -> Vec<String> {
359 let alive: Vec<String> = targets.iter().filter(|r| !is_unrevivable(r)).cloned().collect();
360 if alive.is_empty() {
361 targets
362 } else {
363 alive
364 }
365}
366
367async fn drop_unrevivable_targets(client: &Client, targets: Vec<String>) -> Vec<String> {
369 use nostr_sdk::prelude::RelayStatus;
370 let pool = client.relays().all().await;
371 drop_unrevivable(targets, |r| {
372 RelayUrl::parse(r)
373 .ok()
374 .and_then(|u| pool.get(&u).map(|rl| matches!(rl.status(), RelayStatus::Terminated | RelayStatus::Shutdown | RelayStatus::Banned)))
375 .unwrap_or(false)
376 })
377}
378
379fn breaker_tripped(url: &str) -> bool {
380 breaker_tripped_at(crate::state::current_session_generation(), url)
381}
382
383fn breaker_tripped_at(generation: u64, url: &str) -> bool {
384 with_breaker_at(generation, |map| {
385 map.get(url)
386 .and_then(|e| e.tripped_until)
387 .map_or(false, |t| std::time::Instant::now() < t)
388 })
389}
390
391fn breaker_record(url: &str, success: bool, full_budget: bool) {
394 breaker_record_at(crate::state::current_session_generation(), url, success, full_budget)
395}
396
397fn breaker_record_at(generation: u64, url: &str, success: bool, full_budget: bool) {
398 with_breaker_at(generation, |map| {
399 if success {
400 map.remove(url);
401 return;
402 }
403 if !full_budget {
404 return;
405 }
406 let e = map.entry(url.to_string()).or_default();
407 e.consecutive_failures = e.consecutive_failures.saturating_add(1);
408 if e.consecutive_failures >= BREAKER_TRIP_THRESHOLD {
409 e.tripped_until = Some(std::time::Instant::now() + BREAKER_COOLDOWN);
410 }
411 })
412}
413
414fn effective_evidence(query: &Query) -> Evidence {
420 query.evidence
421}
422
423struct PooledPlane {
431 client: Client,
432 last_used: std::time::Instant,
433}
434
435static PLANE_POOL: std::sync::LazyLock<std::sync::Mutex<(u64, std::collections::HashMap<String, PooledPlane>)>> =
436 std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashMap::new())));
437
438const PLANE_POOL_IDLE_TTL: std::time::Duration = std::time::Duration::from_secs(90);
441const PLANE_POOL_MAX: usize = 24;
443
444fn plane_pool_key(plane_pk: &str, relays: &[String]) -> String {
445 let mut rs: Vec<&str> = relays.iter().map(|s| s.as_str()).collect();
446 rs.sort_unstable();
447 let mut k = String::with_capacity(plane_pk.len() + 1 + rs.iter().map(|r| r.len() + 1).sum::<usize>());
448 k.push_str(plane_pk);
449 k.push('|');
450 k.push_str(&rs.join(","));
451 k
452}
453
454fn disconnect_clients(clients: Vec<Client>) {
459 if clients.is_empty() {
460 return;
461 }
462 match tokio::runtime::Handle::try_current() {
463 Ok(handle) => {
464 for c in clients {
465 handle.spawn(async move {
466 let _ = c.disconnect();
467 });
468 }
469 }
470 Err(_) => drop(clients),
471 }
472}
473
474pub fn clear_plane_pool() {
477 let drained: Vec<Client> = {
478 let mut g = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner());
479 g.0 = crate::state::current_session_generation();
484 g.1.drain().map(|(_, p)| p.client).collect()
485 };
486 disconnect_clients(drained);
487}
488
489fn plane_pool_take(generation: u64, key: &str) -> (Option<Client>, Vec<Client>) {
493 let mut g = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner());
494 let mut evicted: Vec<Client> = Vec::new();
495 if g.0 != generation {
496 evicted.extend(g.1.drain().map(|(_, p)| p.client));
497 g.0 = generation;
498 }
499 let now = std::time::Instant::now();
501 let expired: Vec<String> = g.1.iter()
502 .filter(|(_, p)| now.duration_since(p.last_used) >= PLANE_POOL_IDLE_TTL)
503 .map(|(k, _)| k.clone())
504 .collect();
505 for k in expired {
506 if let Some(p) = g.1.remove(&k) {
507 evicted.push(p.client);
508 }
509 }
510 let hit = g.1.get_mut(key).map(|p| {
511 p.last_used = now;
512 p.client.clone()
513 });
514 (hit, evicted)
515}
516
517fn plane_pool_insert(generation: u64, key: String, client: Client) -> Vec<Client> {
524 let mut g = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner());
525 if g.0 != generation || g.1.contains_key(&key) {
528 return Vec::new();
529 }
530 let mut evicted: Vec<Client> = Vec::new();
531 if g.1.len() >= PLANE_POOL_MAX {
532 if let Some(lru_key) = g.1.iter().min_by_key(|(_, p)| p.last_used).map(|(k, _)| k.clone()) {
533 if let Some(p) = g.1.remove(&lru_key) {
534 evicted.push(p.client);
535 }
536 }
537 }
538 g.1.insert(key, PooledPlane { client, last_used: std::time::Instant::now() });
539 evicted
540}
541
542fn demotion_allowed() -> bool {
546 #[cfg(feature = "tor")]
547 {
548 matches!(crate::tor::transport_state(), crate::tor::TorTransportState::Disabled)
549 }
550 #[cfg(not(feature = "tor"))]
551 {
552 true
553 }
554}
555
556pub async fn prewarm_held_communities(session: crate::state::SessionGuard) {
562 let mut relays: Vec<String> = Vec::new();
563 for id in crate::db::community::list_community_ids().unwrap_or_default() {
564 match crate::db::community::community_protocol(&id).ok().flatten() {
565 Some(crate::community::ConcordProtocol::V2) => {
566 if let Ok(Some(c)) = crate::db::community::load_community_v2(&id) {
567 relays.extend(c.relays.iter().cloned());
568 }
569 }
570 _ => {
571 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
572 relays.extend(c.relays.iter().cloned());
573 }
574 }
575 }
576 }
577 relays.sort();
578 relays.dedup();
579 if relays.is_empty() || !session.is_valid() {
580 return;
581 }
582 let Ok(client) = LiveTransport::warm_client(&relays, std::time::Duration::from_secs(4)).await
583 else {
584 return;
585 };
586 crate::community::v2::streamauth::ensure_responder(&client);
592 let probe = Keys::generate();
593 let filter = Query {
594 kinds: vec![crate::community::v2::stream::KIND_WRAP],
595 authors: vec![probe.public_key().to_hex()],
596 limit: Some(1),
597 ..Default::default()
598 }
599 .to_filter();
600 let mut elicits = futures_util::stream::FuturesUnordered::new();
601 for r in &relays {
602 let c = client.clone();
603 let f = filter.clone();
604 let r = r.clone();
605 elicits.push(async move {
606 let _ = fetch_relay_eose_filters(&c, &r, vec![f], std::time::Duration::from_secs(3)).await;
607 });
608 }
609 use futures_util::StreamExt;
610 while elicits.next().await.is_some() {}
611}
612
613#[derive(Clone, Copy, PartialEq, Eq, Debug)]
617pub enum EoseFail {
618 Closed,
620 Deadline,
622 Gone,
624}
625
626pub async fn fetch_relay_eose(
637 client: &Client,
638 url: &str,
639 filter: Filter,
640 timeout: std::time::Duration,
641) -> Result<Vec<Event>, ()> {
642 fetch_relay_eose_filters(client, url, vec![filter], timeout).await.map_err(|_| ())
643}
644
645pub async fn fetch_relay_eose_filters(
650 client: &Client,
651 url: &str,
652 filters: Vec<Filter>,
653 timeout: std::time::Duration,
654) -> Result<Vec<Event>, EoseFail> {
655 let relay = client.relay(url).await.map_err(|_| EoseFail::Gone)?.ok_or(EoseFail::Gone)?;
656 let mut notifications = relay.notifications();
658 let sub_id = SubscriptionId::generate();
659 struct CloseGuard(Relay, SubscriptionId);
666 impl Drop for CloseGuard {
667 fn drop(&mut self) {
668 let relay = self.0.clone();
669 let id = self.1.clone();
670 tokio::spawn(async move {
671 let _ = relay
672 .send_msg(nostr_sdk::prelude::ClientMessage::Close(std::borrow::Cow::Owned(id)))
673 .await;
674 });
675 }
676 }
677 let _close = CloseGuard(relay.clone(), sub_id.clone());
678 relay
679 .send_msg(nostr_sdk::prelude::ClientMessage::Req {
680 subscription_id: std::borrow::Cow::Borrowed(&sub_id),
681 filters: filters.into_iter().map(std::borrow::Cow::Owned).collect(),
682 })
683 .await
684 .map_err(|_| EoseFail::Gone)?;
685 let deadline = tokio::time::Instant::now() + timeout;
686 let mut events: Vec<Event> = Vec::new();
687 let mut seen: std::collections::HashSet<EventId> = std::collections::HashSet::new();
688 loop {
689 let notification = match tokio::time::timeout_at(deadline, notifications.next()).await {
693 Ok(Some(n)) => n,
694 Ok(None) => return Err(EoseFail::Gone),
695 Err(_) => return Err(EoseFail::Deadline), };
697 match notification {
698 RelayNotification::Event { subscription_id, event } if subscription_id == sub_id => {
699 if seen.insert(event.id) {
700 events.push(*event);
701 }
702 }
703 RelayNotification::Message { message } => match *message {
704 RelayMessage::Event { subscription_id, event } if *subscription_id == sub_id => {
705 if seen.insert(event.id) {
706 events.push(event.into_owned());
707 }
708 }
709 RelayMessage::EndOfStoredEvents(id) if *id == sub_id => return Ok(events),
710 RelayMessage::Closed { subscription_id, .. } if *subscription_id == sub_id => {
711 return Err(EoseFail::Closed); }
713 _ => {}
714 },
715 RelayNotification::RelayStatus { status }
716 if status == nostr_sdk::prelude::RelayStatus::Shutdown =>
717 {
718 return Err(EoseFail::Gone);
719 }
720 _ => {}
721 }
722 }
723}
724
725pub(crate) struct UnionPlan {
729 attempted: usize,
730 successes: usize,
731 resolved: usize,
732 evidence: Evidence,
733}
734
735impl UnionPlan {
736 pub(crate) fn new(evidence: Evidence, attempted: usize) -> Self {
737 Self { attempted, successes: 0, resolved: 0, evidence }
738 }
739
740 pub(crate) fn record(&mut self, success: bool) {
741 self.resolved += 1;
742 if success {
743 self.successes += 1;
744 }
745 }
746
747 pub(crate) fn satisfied(&self) -> bool {
752 match self.evidence {
753 Evidence::Fast => self.successes >= 1,
754 Evidence::Quorum => self.successes >= (self.attempted / 2) + 1,
755 Evidence::Full => self.resolved >= self.attempted,
756 }
757 }
758
759 pub(crate) fn exhausted(&self) -> bool {
761 self.resolved >= self.attempted
762 }
763
764 pub(crate) fn successes(&self) -> usize {
765 self.successes
766 }
767
768 pub(crate) fn attempted(&self) -> usize {
769 self.attempted
770 }
771}
772
773#[cfg(feature = "tor")]
777const TOR_READY_WAIT: std::time::Duration = std::time::Duration::from_secs(30);
778
779#[allow(dead_code)]
789async fn wait_until_tor_ready<F: Fn() -> bool>(
790 is_blocked: F,
791 max_wait: std::time::Duration,
792) -> Result<(), String> {
793 if !is_blocked() {
794 return Ok(());
795 }
796 let deadline = std::time::Instant::now() + max_wait;
797 while is_blocked() {
798 if std::time::Instant::now() >= deadline {
799 return Err("Tor is still connecting. Wait a moment and try again.".to_string());
800 }
801 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
802 }
803 Ok(())
804}
805
806pub async fn prune_unneeded_community_relays(candidates: &[String]) {
814 if candidates.is_empty() {
815 return;
816 }
817 let Some(client) = crate::state::nostr_client() else { return };
818
819 let mut still_needed: std::collections::HashSet<String> = std::collections::HashSet::new();
820 if let Ok(ids) = crate::db::community::list_community_ids() {
821 for id in ids {
822 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
823 for r in &c.relays {
824 still_needed.insert(r.clone());
825 }
826 }
827 }
828 }
829
830 let pool = client;
831 let pooled = pool.relays().all().await;
833 for url in candidates {
834 if still_needed.contains(url) {
835 continue;
836 }
837 if let Ok(parsed) = nostr_sdk::prelude::RelayUrl::parse(url) {
838 if let Some(relay) = pooled.get(&parsed) {
839 if relay.capabilities().load().can_read() || relay.capabilities().load().can_write() {
840 continue; }
842 }
843 let _ = pool.remove_relay(parsed).force().await; forget_warmed_relay(url);
845 }
846 }
847}
848
849pub struct LiveTransport {
856 timeout: std::time::Duration,
857}
858
859impl Default for LiveTransport {
860 fn default() -> Self {
861 Self::with_timeout(std::time::Duration::from_secs(10))
862 }
863}
864
865impl LiveTransport {
866 pub fn new() -> Self {
867 Self::default()
868 }
869
870 pub fn with_timeout(timeout: std::time::Duration) -> Self {
876 Self { timeout: crate::relay_request_timeout(timeout) }
877 }
878
879 pub(crate) async fn warm_client(relays: &[String], connect_timeout: std::time::Duration) -> Result<Client, String> {
885 if relays.is_empty() {
886 return Err("community has no relays configured".to_string());
887 }
888 #[cfg(feature = "tor")]
894 wait_until_tor_ready(
895 || matches!(crate::tor::transport_state(), crate::tor::TorTransportState::RequiredButInactive),
896 TOR_READY_WAIT,
897 ).await?;
898 let client = crate::state::nostr_client().ok_or_else(|| "nostr client not initialized".to_string())?;
899
900 let generation = crate::state::current_session_generation();
904 {
905 let warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
906 if warmed.0 == generation && relays.iter().all(|r| warmed.1.contains(r)) {
907 return Ok(client);
908 }
909 }
910
911 let mut added_new = false;
917 let mut succeeded: Vec<&String> = Vec::new();
918 for url in relays {
919 match client
920 .add_managed_relay(url.as_str())
921 .capabilities(crate::community_relay_capabilities())
922 .await
923 {
924 Ok(true) => { added_new = true; succeeded.push(url); }
925 Ok(false) => { succeeded.push(url); }
926 Err(_) => {}
927 }
928 }
929 if succeeded.is_empty() {
930 return Err("no valid community relays could be added".to_string());
931 }
932 if added_new {
933 let _ = client.try_connect().timeout(connect_timeout).await;
938 } else {
939 client.connect().await;
941 }
942
943 {
946 let mut warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
947 if warmed.0 != generation {
948 warmed.0 = generation;
949 warmed.1.clear();
950 }
951 for url in succeeded {
952 warmed.1.insert(url.clone());
953 }
954 }
955 Ok(client)
956 }
957
958 pub async fn fetch_counted(&self, query: &Query, relays: &[String]) -> Result<(Vec<Event>, usize, usize), String> {
963 let client = Self::warm_client(relays, self.timeout).await?;
964 let base_timeout = self.timeout;
965 let filter = query.to_filter();
966
967 let evidence = effective_evidence(query);
968
969 let mut targets: Vec<String> = Vec::new();
970 for r in relays {
971 if !targets.contains(r) {
972 targets.push(r.clone());
973 }
974 }
975 targets = drop_unrevivable_targets(&client, targets).await;
980
981 if evidence == Evidence::Fast && targets.len() >= 2 {
986 let alive: Vec<String> =
987 targets.iter().filter(|r| !breaker_tripped(r)).cloned().collect();
988 if !alive.is_empty() {
989 targets = alive;
990 }
991 }
992
993 if targets.len() <= 1 {
996 let Some(url) = targets.first() else {
997 return Err("no valid relay to fetch from".to_string());
998 };
999 let res = fetch_relay_eose(&client, url, filter, base_timeout)
1000 .await
1001 .map_err(|_| format!("relay did not answer the fetch: {url}"));
1002 breaker_record(url, res.is_ok(), true);
1003 return res.map(|evs| (evs, 1, 1));
1004 }
1005
1006 fn merge_events(
1007 evs: Vec<Event>,
1008 result: &mut Vec<Event>,
1009 seen: &mut std::collections::HashSet<EventId>,
1010 ) {
1011 for e in evs {
1012 if seen.insert(e.id) {
1013 result.push(e);
1014 }
1015 }
1016 }
1017
1018 let demote = evidence == Evidence::Full && demotion_allowed();
1025 use futures_util::stream::{FuturesUnordered, StreamExt};
1026 let mut fetches: FuturesUnordered<_> = targets
1027 .iter()
1028 .map(|r| {
1029 let client = client.clone();
1030 let filter = filter.clone();
1031 let r = r.clone();
1032 let timeout = if demote && breaker_tripped(&r) {
1033 TRIPPED_TIMEOUT.min(base_timeout)
1034 } else {
1035 base_timeout
1036 };
1037 let full_budget = timeout >= base_timeout;
1038 tokio::spawn(async move {
1039 let out = fetch_relay_eose(&client, &r, filter, timeout).await;
1040 (r, full_budget, out)
1041 })
1042 })
1043 .collect();
1044
1045 let mut plan = UnionPlan::new(evidence, targets.len());
1046 let mut result: Vec<Event> = Vec::new();
1047 let mut union_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1048
1049 let mut quorum_deadline: Option<tokio::time::Instant> = None;
1054 let mut quorum_window_closed = false;
1055 while !plan.satisfied() && !plan.exhausted() {
1056 let next = match quorum_deadline {
1057 Some(deadline) => match tokio::time::timeout_at(deadline, fetches.next()).await {
1058 Ok(n) => n,
1059 Err(_) => {
1060 quorum_window_closed = true;
1061 break; }
1063 },
1064 None => fetches.next().await,
1065 };
1066 let Some(joined) = next else { break };
1067 match joined {
1068 Ok((url, full_budget, Ok(evs))) => {
1069 breaker_record(&url, true, full_budget);
1070 merge_events(evs, &mut result, &mut union_ids);
1071 plan.record(true);
1072 if evidence == Evidence::Quorum && quorum_deadline.is_none() {
1073 quorum_deadline = Some(
1074 tokio::time::Instant::now()
1075 + std::time::Duration::from_millis(QUORUM_GRACE_MS),
1076 );
1077 }
1078 }
1079 Ok((url, full_budget, Err(()))) => {
1080 breaker_record(&url, false, full_budget);
1081 plan.record(false);
1082 }
1083 Err(_) => plan.record(false), }
1085 }
1086
1087 if plan.successes() == 0 {
1088 return Err(format!(
1089 "no relay answered the fetch (0/{} attempted)",
1090 plan.attempted()
1091 ));
1092 }
1093
1094 if !fetches.is_empty() && !quorum_window_closed {
1098 let grace = tokio::time::sleep(std::time::Duration::from_millis(RESIDUAL_GRACE_MS));
1099 tokio::pin!(grace);
1100 loop {
1101 tokio::select! {
1102 _ = &mut grace => break,
1103 next = fetches.next() => match next {
1104 Some(Ok((url, full_budget, Ok(evs)))) => {
1105 breaker_record(&url, true, full_budget);
1106 merge_events(evs, &mut result, &mut union_ids);
1107 }
1108 Some(Ok((url, full_budget, Err(())))) => {
1109 breaker_record(&url, false, full_budget);
1110 }
1111 Some(Err(_)) => continue,
1112 None => break,
1113 }
1114 }
1115 }
1116 }
1117
1118 if !fetches.is_empty() {
1122 let seen: std::collections::HashSet<EventId> = result.iter().map(|e| e.id).collect();
1123 let session = crate::state::SessionGuard::capture();
1126 tokio::spawn(async move {
1127 let mut extra: Vec<Event> = Vec::new();
1128 let mut extra_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1129 while let Some(joined) = fetches.next().await {
1130 if let Ok((url, full_budget, out)) = joined {
1131 breaker_record(&url, out.is_ok(), full_budget);
1135 if let Ok(evs) = out {
1136 for e in evs {
1137 if !seen.contains(&e.id) && extra_ids.insert(e.id) {
1138 extra.push(e);
1139 }
1140 }
1141 }
1142 }
1143 }
1144 if !session.is_valid() {
1145 return;
1146 }
1147 submit_stragglers(extra);
1148 });
1149 }
1150
1151 Ok((result, plan.successes(), plan.attempted()))
1152 }
1153}
1154
1155#[async_trait::async_trait]
1156impl Transport for LiveTransport {
1157 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1158 let client = Self::warm_client(relays, self.timeout).await?;
1159 let timeout = self.timeout;
1160 let mut targets: Vec<String> = Vec::new();
1161 for r in relays { if !targets.contains(r) { targets.push(r.clone()); } }
1162 targets = drop_unrevivable_targets(&client, targets).await;
1164 use futures_util::stream::{FuturesUnordered, StreamExt};
1170 let mut sends: FuturesUnordered<_> = targets
1171 .into_iter()
1172 .map(|r| {
1173 let client = client.clone();
1174 let event = event.clone();
1175 tokio::spawn(async move {
1176 matches!(
1177 tokio::time::timeout(timeout, client.send_event(&event).to(vec![r.clone()])).await,
1178 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains_key(&u)).unwrap_or(false)
1179 )
1180 })
1181 })
1182 .collect();
1183 while let Some(joined) = sends.next().await {
1184 if matches!(joined, Ok(true)) {
1185 return Ok(()); }
1187 }
1188 Err("no relay accepted the event".to_string())
1189 }
1190
1191 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1192 self.fetch_counted(query, relays).await.map(|(events, _successes, _attempted)| events)
1193 }
1194
1195 async fn fetch_plane(&self, plane: &Keys, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1196 if relays.is_empty() {
1197 return Ok(Vec::new());
1198 }
1199 #[cfg(feature = "tor")]
1200 wait_until_tor_ready(
1201 || matches!(crate::tor::transport_state(), crate::tor::TorTransportState::RequiredButInactive),
1202 TOR_READY_WAIT,
1203 ).await?;
1204 let mut targets: Vec<String> = relays.iter().filter(|r| !breaker_tripped(r)).cloned().collect();
1209 if targets.is_empty() {
1210 targets = relays.to_vec();
1211 }
1212 let filter = query.to_filter();
1213 let generation = crate::state::current_session_generation();
1214 let key = plane_pool_key(&plane.public_key().to_hex(), &targets);
1215
1216 let (hit, evicted) = plane_pool_take(generation, &key);
1219 disconnect_clients(evicted);
1220 let client = if let Some(c) = hit {
1221 c
1222 } else {
1223 let client = crate::apply_tor_proxy(
1229 nostr_sdk::prelude::Client::builder().authenticator(
1230 nostr_sdk::prelude::SignerAuthenticator::new(plane.clone()),
1231 ),
1232 )
1233 .build();
1234 for r in &targets {
1239 let _ = client.add_managed_relay(r.clone()).capabilities(crate::community_relay_capabilities()).await;
1240 }
1241 client.connect().await;
1242 for r in &targets {
1245 let _ = client
1246 .fetch_events(nostr_sdk::prelude::ReqTarget::single(r.clone(), [filter.clone()]))
1247 .timeout(crate::relay_request_timeout(std::time::Duration::from_secs(5)))
1251 .await;
1252 }
1253 let ev = plane_pool_insert(generation, key, client.clone());
1254 disconnect_clients(ev);
1255 client
1256 };
1257
1258 let targets = drop_unrevivable_targets(&client, targets).await;
1262
1263 let mut result: Vec<Event> = Vec::new();
1264 let mut seen: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1265 let mut successes = 0usize;
1266 for r in &targets {
1267 let res = fetch_relay_eose(&client, r, filter.clone(), self.timeout).await;
1268 breaker_record(r, res.is_ok(), true);
1271 if let Ok(events) = res {
1272 successes += 1;
1273 for e in events {
1274 if seen.insert(e.id) {
1275 result.push(e);
1276 }
1277 }
1278 }
1279 }
1280 if successes == 0 {
1285 return Err(format!("no relay answered the plane fetch (0/{} attempted)", targets.len()));
1286 }
1287 Ok(result)
1289 }
1290
1291 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1292 let client = Self::warm_client(relays, self.timeout).await?;
1297 let timeout = self.timeout;
1298 let event = event.clone();
1299 let backoff = std::time::Duration::from_millis(750);
1300 let mut pending: Vec<String> = Vec::new();
1301 for r in relays { if !pending.contains(r) { pending.push(r.clone()); } }
1302 if pending.is_empty() {
1303 return Err("no relays to broadcast to".to_string());
1304 }
1305
1306 let mut acked_any = false;
1311 let _ = tokio::time::timeout(CONFIRM_WINDOW, async {
1312 loop {
1313 let round = drop_unrevivable_targets(&client, pending.clone()).await;
1317 let sends = round.iter().cloned().map(|r| {
1318 let client = &client;
1319 let event = &event;
1320 Box::pin(async move {
1321 match tokio::time::timeout(timeout, client.send_event(event).to(vec![r.clone()])).await {
1322 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains_key(&u)).unwrap_or(false) => Ok(r),
1323 _ => Err(()),
1324 }
1325 })
1326 });
1327 if let Ok((winner, _losers)) = futures_util::future::select_ok(sends).await {
1328 acked_any = true;
1329 pending.retain(|r| r != &winner);
1330 break;
1331 }
1332 tokio::time::sleep(backoff).await;
1333 }
1334 })
1335 .await;
1336
1337 if !acked_any {
1338 return Err(format!("no relay accepted the event within {}s", CONFIRM_WINDOW.as_secs()));
1339 }
1340 if pending.is_empty() {
1341 return Ok(()); }
1343
1344 tokio::spawn(async move {
1349 let client_ref = &client;
1350 let event_ref = &event;
1351 let _ = durable_broadcast(&pending, MAX_PUBLISH_ATTEMPTS, backoff, move |round| {
1352 Box::pin(async move {
1353 match tokio::time::timeout(timeout, client_ref.send_event(event_ref).to(round.clone())).await {
1354 Ok(Ok(output)) => round.into_iter().filter(|p| RelayUrl::parse(p).map(|u| output.success.contains_key(&u)).unwrap_or(false)).collect(),
1355 _ => Vec::new(),
1356 }
1357 })
1358 })
1359 .await;
1360 });
1361 Ok(())
1362 }
1363}
1364
1365#[cfg(test)]
1366pub(crate) mod memory {
1367 use super::*;
1368 use std::collections::{HashMap, HashSet};
1369 use std::sync::Mutex;
1370
1371 pub struct MemoryRelay {
1376 per_relay: Mutex<HashMap<String, Vec<Event>>>,
1377 subscribers: Mutex<Vec<(Query, tokio::sync::mpsc::UnboundedSender<Event>)>>,
1378 }
1379
1380 fn is_ephemeral(kind: u16) -> bool {
1383 (20000..30000).contains(&kind)
1384 }
1385
1386 impl MemoryRelay {
1387 pub fn new() -> Self {
1388 MemoryRelay {
1389 per_relay: Mutex::new(HashMap::new()),
1390 subscribers: Mutex::new(Vec::new()),
1391 }
1392 }
1393
1394 pub fn stored_count(&self) -> usize {
1398 self.per_relay.lock().unwrap().values().map(|v| v.len()).sum()
1399 }
1400
1401 pub fn subscribe(&self, query: Query) -> tokio::sync::mpsc::UnboundedReceiver<Event> {
1404 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1405 self.subscribers.lock().unwrap().push((query, tx));
1406 rx
1407 }
1408
1409 fn deliver(&self, event: &Event) {
1411 self.subscribers.lock().unwrap().retain(|(q, tx)| {
1412 if q.matches(event) {
1413 tx.send(event.clone()).is_ok()
1414 } else {
1415 !tx.is_closed()
1416 }
1417 });
1418 }
1419
1420 pub fn inject(&self, event: &Event, relays: &[String]) {
1423 self.deliver(event);
1424 if is_ephemeral(event.kind.as_u16()) {
1425 return; }
1427 let d_tag = |e: &Event| e.tags.iter().find_map(|t| {
1433 let s = t.as_slice();
1434 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
1435 }).unwrap_or_default();
1436 let k = event.kind.as_u16();
1437 let replaceable = (30000..40000).contains(&k) || (10000..20000).contains(&k) || k == 0 || k == 3;
1438 let coord = (event.kind.as_u16(), event.pubkey, d_tag(event));
1439 let mut map = self.per_relay.lock().unwrap();
1440 for r in relays {
1441 let v = map.entry(r.clone()).or_default();
1442 if replaceable {
1443 v.retain(|e| (e.kind.as_u16(), e.pubkey, d_tag(e)) != coord);
1444 }
1445 v.push(event.clone());
1446 }
1447 }
1448
1449 pub fn count_on(&self, relay: &str) -> usize {
1451 self.per_relay.lock().unwrap().get(relay).map_or(0, |v| v.len())
1452 }
1453
1454 fn apply_deletion(&self, deletion: &Event, relays: &[String]) {
1459 let mut id_targets: HashSet<String> = HashSet::new();
1460 let mut coord_targets: HashSet<String> = HashSet::new();
1461 for t in deletion.tags.iter() {
1462 let s = t.as_slice();
1463 if s.len() >= 2 && s[0] == "e" {
1464 id_targets.insert(s[1].clone());
1465 } else if s.len() >= 2 && s[0] == "a" {
1466 coord_targets.insert(s[1].clone());
1467 }
1468 }
1469 let mut map = self.per_relay.lock().unwrap();
1470 for r in relays {
1471 if let Some(events) = map.get_mut(r) {
1472 events.retain(|e| {
1473 if e.pubkey != deletion.pubkey {
1474 return true;
1475 }
1476 if id_targets.contains(&e.id.to_hex()) {
1477 return false;
1478 }
1479 let d = e.tags.iter().find_map(|t| {
1481 let s = t.as_slice();
1482 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
1483 }).unwrap_or_default();
1484 let coord = format!("{}:{}:{}", e.kind.as_u16(), e.pubkey.to_hex(), d);
1485 !coord_targets.contains(&coord)
1486 });
1487 }
1488 }
1489 }
1490 }
1491
1492 #[async_trait::async_trait]
1493 impl Transport for MemoryRelay {
1494 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1495 if event.kind == Kind::EventDeletion {
1497 self.apply_deletion(event, relays);
1498 self.deliver(event);
1499 } else {
1500 self.inject(event, relays);
1501 }
1502 Ok(())
1503 }
1504
1505 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1508 self.publish(event, relays).await
1509 }
1510
1511 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1512 let map = self.per_relay.lock().unwrap();
1513 let mut seen = HashSet::new();
1514 let mut out = Vec::new();
1515 for r in relays {
1516 if let Some(events) = map.get(r) {
1517 for ev in events {
1518 if is_ephemeral(ev.kind.as_u16()) {
1521 continue;
1522 }
1523 if query.matches(ev) && seen.insert(ev.id) {
1524 out.push(ev.clone());
1525 }
1526 }
1527 }
1528 }
1529 if let Some(limit) = query.limit {
1532 out.sort_by(|a, b| b.created_at.cmp(&a.created_at).then_with(|| b.id.cmp(&a.id)));
1533 out.truncate(limit);
1534 }
1535 Ok(out)
1536 }
1537
1538 async fn fetch_plane(&self, _plane: &Keys, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1539 self.fetch(query, relays).await
1541 }
1542 }
1543}
1544
1545#[cfg(test)]
1546mod tests {
1547 use super::*;
1548
1549 #[test]
1552 fn a_dead_socket_is_skipped_but_a_connecting_or_unknown_one_is_not() {
1553 let targets: Vec<String> = ["wss://dead", "wss://connecting", "wss://unknown"].map(String::from).into();
1554 let out = drop_unrevivable(targets, |r| r == "wss://dead");
1555 assert_eq!(out, ["wss://connecting", "wss://unknown"].map(String::from).to_vec());
1556 }
1557
1558 #[test]
1559 fn an_all_dead_set_falls_back_to_the_full_list() {
1560 let targets: Vec<String> = ["wss://a", "wss://b"].map(String::from).into();
1563 let out = drop_unrevivable(targets.clone(), |_| true);
1564 assert_eq!(out, targets);
1565 }
1566
1567 #[tokio::test]
1572 async fn tor_gate_passes_immediately_when_not_blocked() {
1573 let start = std::time::Instant::now();
1574 let res = wait_until_tor_ready(|| false, std::time::Duration::from_secs(30)).await;
1575 assert!(res.is_ok());
1576 assert!(start.elapsed() < std::time::Duration::from_secs(1), "must not wait when Tor is ready");
1577 }
1578
1579 #[tokio::test]
1580 async fn tor_gate_errors_honestly_after_timeout_when_perpetually_blocked() {
1581 let res = wait_until_tor_ready(|| true, std::time::Duration::from_millis(300)).await;
1584 let err = res.expect_err("should error when Tor never activates");
1585 assert!(err.to_lowercase().contains("tor"), "error must name Tor, got: {err}");
1586 }
1587
1588 #[tokio::test]
1589 async fn tor_gate_passes_once_circuit_comes_up_mid_wait() {
1590 let calls = std::sync::atomic::AtomicUsize::new(0);
1591 let res = wait_until_tor_ready(
1593 || calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 3,
1594 std::time::Duration::from_secs(5),
1595 ).await;
1596 assert!(res.is_ok(), "should succeed once Tor activates within the window");
1597 }
1598
1599 fn evt(kind: u16, z: &str) -> Event {
1600 EventBuilder::new(Kind::Custom(kind), "x")
1601 .tags([Tag::custom(
1602 "z",
1603 [z.to_string()],
1604 )])
1605 .finalize(&Keys::generate())
1606 .unwrap()
1607 }
1608
1609 #[test]
1610 fn query_matches_kind_and_z() {
1611 let e = evt(3300, "abc");
1612 assert!(Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: None, ..Default::default() }.matches(&e));
1613 assert!(!Query { kinds: vec![3301], ..Default::default() }.matches(&e));
1614 assert!(!Query { kinds: vec![], z_tags: vec!["xyz".into()], since: None, ..Default::default() }.matches(&e));
1615 assert!(Query::default().matches(&e), "empty query matches anything");
1616 }
1617
1618 fn evt_at(kind: u16, secs: u64) -> Event {
1619 EventBuilder::new(Kind::Custom(kind), "x")
1620 .custom_created_at(Timestamp::from(secs))
1621 .finalize(&Keys::generate())
1622 .unwrap()
1623 }
1624
1625 fn evt_z_at(kind: u16, z: &str, secs: u64) -> Event {
1628 EventBuilder::new(Kind::Custom(kind), "x")
1629 .custom_created_at(Timestamp::from(secs))
1630 .tags([Tag::custom(
1631 "z",
1632 [z.to_string()],
1633 )])
1634 .finalize(&Keys::generate())
1635 .unwrap()
1636 }
1637
1638 #[tokio::test]
1639 async fn fetch_pages_with_until_and_limit_newest_first() {
1640 let relay = super::memory::MemoryRelay::new();
1642 let relays = vec!["r1".to_string()];
1643 for s in 1..=5u64 {
1644 relay.inject(&evt_z_at(3300, "pg", s), &relays);
1645 }
1646 let secs = |evs: &[Event]| evs.iter().map(|e| e.created_at.as_secs()).collect::<Vec<_>>();
1647
1648 let latest = relay
1650 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], limit: Some(2), ..Default::default() }, &relays)
1651 .await
1652 .unwrap();
1653 assert_eq!(secs(&latest), vec![5, 4]);
1654
1655 let older = relay
1657 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(3), limit: Some(2), ..Default::default() }, &relays)
1658 .await
1659 .unwrap();
1660 assert_eq!(secs(&older), vec![3, 2]);
1661
1662 let start = relay
1665 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(1), limit: Some(2), ..Default::default() }, &relays)
1666 .await
1667 .unwrap();
1668 assert_eq!(secs(&start), vec![1]);
1669 }
1670
1671 #[test]
1672 fn to_filter_translates_kinds_z_and_since() {
1673 let q = Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: Some(100), ..Default::default() };
1676 let filter = q.to_filter();
1677 let matching = EventBuilder::new(Kind::Custom(3300), "x")
1678 .custom_created_at(Timestamp::from(150))
1679 .tags([Tag::custom(
1680 "z",
1681 ["abc".to_string()],
1682 )])
1683 .finalize(&Keys::generate())
1684 .unwrap();
1685 assert!(filter.match_event(&matching, MatchEventOptions::new()), "to_filter must accept what matches() accepts");
1686 assert!(q.matches(&matching));
1687
1688 let wrong_kind = EventBuilder::new(Kind::Custom(3301), "x")
1690 .custom_created_at(Timestamp::from(150))
1691 .tags([Tag::custom(
1692 "z",
1693 ["abc".to_string()],
1694 )])
1695 .finalize(&Keys::generate())
1696 .unwrap();
1697 assert!(!filter.match_event(&wrong_kind, MatchEventOptions::new()));
1698 }
1699
1700 fn evt_sl(kind: u16, letter: Alphabet, value: &str) -> Event {
1702 EventBuilder::new(Kind::Custom(kind), "x")
1703 .tags([Tag::custom(
1704 SingleLetterTag::lowercase(letter).as_char().to_string(),
1705 [value.to_string()],
1706 )])
1707 .finalize(&Keys::generate())
1708 .unwrap()
1709 }
1710
1711 #[test]
1712 fn to_filter_and_matches_agree_on_authors() {
1713 let keys = Keys::generate();
1714 let e = EventBuilder::new(Kind::Custom(1059), "x").finalize(&keys).unwrap();
1715 let q = Query { kinds: vec![1059], authors: vec![keys.public_key().to_hex()], ..Default::default() };
1716 assert!(q.matches(&e));
1717 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1718 let miss = Query { kinds: vec![1059], authors: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
1719 assert!(!miss.matches(&e));
1720 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1721 }
1722
1723 #[test]
1724 fn to_filter_and_matches_agree_on_p_tags() {
1725 let recipient = Keys::generate().public_key().to_hex();
1726 let e = evt_sl(1059, Alphabet::P, &recipient);
1727 let q = Query { kinds: vec![1059], p_tags: vec![recipient], ..Default::default() };
1728 assert!(q.matches(&e));
1729 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1730 let miss = Query { kinds: vec![1059], p_tags: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
1731 assert!(!miss.matches(&e));
1732 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1733 }
1734
1735 #[test]
1736 fn to_filter_and_matches_agree_on_k_tags() {
1737 let e = evt_sl(1059, Alphabet::K, "3311");
1738 let q = Query { kinds: vec![1059], k_tags: vec!["3311".into()], ..Default::default() };
1739 assert!(q.matches(&e));
1740 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1741 let miss = Query { kinds: vec![1059], k_tags: vec!["3300".into()], ..Default::default() };
1742 assert!(!miss.matches(&e));
1743 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1744 }
1745
1746 #[test]
1747 fn to_filter_empty_kinds_only_constrains_z_and_since() {
1748 let q = Query { kinds: vec![], z_tags: vec!["p".into()], since: None, ..Default::default() };
1750 let filter = q.to_filter();
1751 let e = evt(3300, "p");
1752 assert!(filter.match_event(&e, MatchEventOptions::new()));
1753 assert!(q.matches(&e));
1754 }
1755
1756 #[test]
1757 fn since_is_an_inclusive_lower_bound() {
1758 let below = evt_at(3300, 99);
1759 let exact = evt_at(3300, 100);
1760 let above = evt_at(3300, 101);
1761 let q = Query { kinds: vec![3300], z_tags: vec![], since: Some(100), ..Default::default() };
1762 assert!(!q.matches(&below), "below the floor is excluded");
1763 assert!(q.matches(&exact), "exactly the floor is included");
1764 assert!(q.matches(&above), "above the floor is included");
1765 }
1766
1767 #[test]
1768 fn z_tags_match_as_or_set() {
1769 let e = evt(3300, "p2");
1772 let q = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p2".into()], since: None, ..Default::default() };
1773 assert!(q.matches(&e));
1774 let miss = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p3".into()], since: None, ..Default::default() };
1775 assert!(!miss.matches(&e));
1776 }
1777
1778 #[tokio::test]
1779 async fn fetch_unions_and_dedups_across_relays() {
1780 use super::memory::MemoryRelay;
1781 let relay = MemoryRelay::new();
1782 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
1783 let e = evt(3300, "p");
1784 relay.publish(&e, &relays).await.unwrap();
1785 let got = relay
1786 .fetch(&Query { kinds: vec![3300], z_tags: vec!["p".into()], since: None, ..Default::default() }, &relays)
1787 .await
1788 .unwrap();
1789 assert_eq!(got.len(), 1, "same event on 3 relays dedups to 1");
1790 }
1791
1792 #[tokio::test]
1793 async fn durable_broadcast_retries_only_the_failing_relays_until_they_ack() {
1794 use std::cell::Cell;
1797 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
1798 let round = Cell::new(0usize);
1799 let r2_round_seen = Cell::new(0usize);
1800 let res = durable_broadcast(&relays, 30, std::time::Duration::ZERO, |pending| {
1801 let n = round.get();
1802 round.set(n + 1);
1803 if n >= 1 {
1805 assert_eq!(pending, vec!["r2".to_string()], "only the failing relay is retried");
1806 r2_round_seen.set(r2_round_seen.get() + 1);
1807 }
1808 Box::pin(async move {
1809 pending.into_iter().filter(|r| r != "r2" || n >= 4).collect()
1810 })
1811 })
1812 .await;
1813 assert!(res.is_ok(), "all relays eventually ACK → Ok");
1814 assert!(round.get() >= 5, "kept retrying r2 across rounds");
1815 }
1816
1817 #[tokio::test]
1818 async fn durable_broadcast_is_ok_if_some_ack_even_when_one_never_does() {
1819 let relays = vec!["r1".to_string(), "r2".to_string()];
1822 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |pending| {
1823 Box::pin(async move { pending.into_iter().filter(|r| r == "r1").collect() })
1824 })
1825 .await;
1826 assert!(res.is_ok(), "≥1 relay accepted → Ok despite a permanently-failing relay");
1827 }
1828
1829 #[tokio::test]
1830 async fn durable_broadcast_errs_only_if_zero_relays_ever_accept() {
1831 let relays = vec!["r1".to_string(), "r2".to_string()];
1832 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |_pending| {
1833 Box::pin(async move { Vec::new() }) })
1835 .await;
1836 assert!(res.is_err(), "zero acceptances after the retry cap → Err");
1837 }
1838
1839 #[tokio::test]
1840 async fn redundancy_self_heals_a_missing_relay() {
1841 use super::memory::MemoryRelay;
1842 let relay = MemoryRelay::new();
1843 let all = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
1844 let e = evt(3300, "p");
1845 relay.inject(&e, &["r2".to_string()]);
1847 assert_eq!(relay.count_on("r1"), 0);
1848 assert_eq!(relay.count_on("r2"), 1);
1849 let got = relay.fetch(&Query { kinds: vec![3300], ..Default::default() }, &all).await.unwrap();
1851 assert_eq!(got.len(), 1);
1852 }
1853
1854 #[tokio::test]
1855 async fn ephemeral_kind_streams_live_but_is_never_stored_or_fetched() {
1856 use super::memory::MemoryRelay;
1857 let relay = MemoryRelay::new();
1858 let relays = vec!["r1".to_string()];
1859 let mut sub = relay.subscribe(Query { kinds: vec![21059], ..Default::default() });
1860 let e = evt(21059, "p");
1861 relay.publish(&e, &relays).await.unwrap();
1862 assert_eq!(relay.count_on("r1"), 0, "ephemeral is never stored");
1863 let got = relay
1864 .fetch(&Query { kinds: vec![21059], ..Default::default() }, &relays)
1865 .await
1866 .unwrap();
1867 assert!(got.is_empty(), "a real relay never serves an ephemeral from a fetch");
1868 assert_eq!(sub.try_recv().unwrap().id, e.id, "but a live subscriber receives it");
1869 }
1870
1871 #[tokio::test]
1872 async fn stored_kind_is_fetchable_and_delivered_live() {
1873 use super::memory::MemoryRelay;
1874 let relay = MemoryRelay::new();
1875 let relays = vec!["r1".to_string()];
1876 let mut sub = relay.subscribe(Query { kinds: vec![1059], ..Default::default() });
1877 let e = evt(1059, "p");
1878 relay.publish(&e, &relays).await.unwrap();
1879 let got = relay
1880 .fetch(&Query { kinds: vec![1059], ..Default::default() }, &relays)
1881 .await
1882 .unwrap();
1883 assert_eq!(got.len(), 1, "stored kind is fetchable");
1884 assert_eq!(sub.try_recv().unwrap().id, e.id, "and delivered to the live subscriber");
1885 }
1886
1887 #[tokio::test]
1888 async fn p_tags_route_a_giftwrap_to_the_matching_subscriber() {
1889 use super::memory::MemoryRelay;
1890 let relay = MemoryRelay::new();
1891 let relays = vec!["r1".to_string()];
1892 let alice = Keys::generate().public_key().to_hex();
1893 let bob = Keys::generate().public_key().to_hex();
1894 let mut sub_alice =
1895 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![alice.clone()], ..Default::default() });
1896 let mut sub_bob =
1897 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![bob.clone()], ..Default::default() });
1898 let wrap = evt_sl(1059, Alphabet::P, &alice);
1899 relay.publish(&wrap, &relays).await.unwrap();
1900 assert_eq!(sub_alice.try_recv().unwrap().id, wrap.id, "addressed recipient gets it live");
1901 assert!(sub_bob.try_recv().is_err(), "a differently-addressed subscriber does not");
1902 let for_alice = relay
1904 .fetch(&Query { kinds: vec![1059], p_tags: vec![alice], ..Default::default() }, &relays)
1905 .await
1906 .unwrap();
1907 assert_eq!(for_alice.len(), 1);
1908 let for_bob = relay
1909 .fetch(&Query { kinds: vec![1059], p_tags: vec![bob], ..Default::default() }, &relays)
1910 .await
1911 .unwrap();
1912 assert!(for_bob.is_empty());
1913 }
1914
1915 #[test]
1918 fn union_plan_fast_satisfied_on_first_success() {
1919 let mut p = UnionPlan::new(Evidence::Fast, 4);
1920 p.record(false);
1921 assert!(!p.satisfied(), "a failure is not evidence");
1922 p.record(true);
1923 assert!(p.satisfied(), "one genuine EOSE satisfies Fast");
1924 assert!(!p.exhausted());
1925 }
1926
1927 #[test]
1928 fn union_plan_quorum_majority_math() {
1929 for (n, need) in [(2usize, 2usize), (3, 2), (4, 3), (5, 3)] {
1931 let mut p = UnionPlan::new(Evidence::Quorum, n);
1932 for _ in 0..need - 1 {
1933 p.record(true);
1934 }
1935 assert!(!p.satisfied(), "{}/{} must not satisfy quorum", need - 1, n);
1936 p.record(true);
1937 assert!(p.satisfied(), "{}/{} satisfies quorum", need, n);
1938 }
1939 }
1940
1941 #[test]
1942 fn union_plan_quorum_failures_never_substitute_for_successes() {
1943 let mut p = UnionPlan::new(Evidence::Quorum, 3);
1944 p.record(true);
1945 p.record(false);
1946 p.record(false);
1947 assert!(!p.satisfied(), "1 success + 2 failures is not a majority");
1948 assert!(p.exhausted(), "all resolved — the degraded path returns best-effort");
1949 assert_eq!(p.successes(), 1);
1950 }
1951
1952 #[test]
1953 fn union_plan_full_requires_every_relay_resolved() {
1954 let mut p = UnionPlan::new(Evidence::Full, 3);
1955 p.record(true);
1956 p.record(true);
1957 assert!(!p.satisfied(), "Full waits for the last relay even after 2 EOSEs");
1958 p.record(false);
1959 assert!(p.satisfied(), "a timeout is a resolution — Full is done");
1960 assert!(p.exhausted());
1961 }
1962
1963 #[test]
1964 fn union_plan_all_dead_is_reportable_not_a_confident_empty() {
1965 let mut p = UnionPlan::new(Evidence::Quorum, 2);
1966 p.record(false);
1967 p.record(false);
1968 assert!(p.exhausted());
1969 assert_eq!(p.successes(), 0, "the caller must map this to Err, never Ok(vec![])");
1970 }
1971
1972 const BREAKER_TEST_GEN: u64 = u64::MAX;
1978
1979 #[test]
1980 fn breaker_trips_only_after_consecutive_full_budget_failures() {
1981 let url = "wss://breaker-test-full-budget.example";
1982 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
1983 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "one failure is below the threshold");
1984 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
1985 assert!(breaker_tripped_at(BREAKER_TEST_GEN, url), "two consecutive full-budget failures trip");
1986 }
1987
1988 #[test]
1989 fn breaker_demoted_budget_failures_never_count() {
1990 let url = "wss://breaker-test-demoted.example";
1991 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
1992 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
1993 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
1994 assert!(
1995 !breaker_tripped_at(BREAKER_TEST_GEN, url),
1996 "demoted-budget failures must not trip (anti-starvation: the post-cooldown probe must stay reachable)"
1997 );
1998 }
1999
2000 #[test]
2001 fn breaker_success_resets_the_entry() {
2002 let url = "wss://breaker-test-reset.example";
2003 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2004 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2005 assert!(breaker_tripped_at(BREAKER_TEST_GEN, url));
2006 breaker_record_at(BREAKER_TEST_GEN, url, true, false);
2008 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "any success unconditionally resets");
2009 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2010 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "and the failure count restarted from zero");
2011 }
2012
2013 #[test]
2016 fn declared_evidence_stands_and_default_is_quorum() {
2017 assert_eq!(Query::default().evidence, Evidence::Quorum, "unclassified sites get Quorum");
2018 assert_eq!(
2019 effective_evidence(&Query { until: Some(1), evidence: Evidence::Fast, ..Default::default() }),
2020 Evidence::Fast,
2021 "chat pagination rides its declared tier — absence verdicts request Full themselves"
2022 );
2023 assert_eq!(
2024 effective_evidence(&Query { evidence: Evidence::Fast, ..Default::default() }),
2025 Evidence::Fast,
2026 "without `until` the declared tier stands"
2027 );
2028 }
2029}