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 tokio::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<std::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 .map_or(false, |t| std::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(std::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: std::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 let _ = c.disconnect();
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 = std::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: std::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 tokio::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 = tokio::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 tokio::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 } if subscription_id == sub_id => {
753 if seen.insert(event.id) {
754 events.push(*event);
755 }
756 }
757 RelayNotification::Message { message } => match *message {
758 RelayMessage::Event { subscription_id, event } if *subscription_id == sub_id => {
759 if seen.insert(event.id) {
760 events.push(event.into_owned());
761 }
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 }
770 if status == nostr_sdk::prelude::RelayStatus::Shutdown =>
771 {
772 return Err(EoseFail::Gone);
773 }
774 _ => {}
775 }
776 }
777}
778
779pub(crate) struct UnionPlan {
783 attempted: usize,
784 successes: usize,
785 resolved: usize,
786 evidence: Evidence,
787}
788
789impl UnionPlan {
790 pub(crate) fn new(evidence: Evidence, attempted: usize) -> Self {
791 Self { attempted, successes: 0, resolved: 0, evidence }
792 }
793
794 pub(crate) fn record(&mut self, success: bool) {
795 self.resolved += 1;
796 if success {
797 self.successes += 1;
798 }
799 }
800
801 pub(crate) fn satisfied(&self) -> bool {
806 match self.evidence {
807 Evidence::Fast => self.successes >= 1,
808 Evidence::Quorum => self.successes >= (self.attempted / 2) + 1,
809 Evidence::Full => self.resolved >= self.attempted,
810 }
811 }
812
813 pub(crate) fn exhausted(&self) -> bool {
815 self.resolved >= self.attempted
816 }
817
818 pub(crate) fn successes(&self) -> usize {
819 self.successes
820 }
821
822 pub(crate) fn attempted(&self) -> usize {
823 self.attempted
824 }
825}
826
827#[cfg(feature = "tor")]
831const TOR_READY_WAIT: std::time::Duration = std::time::Duration::from_secs(30);
832
833#[allow(dead_code)]
843async fn wait_until_tor_ready<F: Fn() -> bool>(
844 is_blocked: F,
845 max_wait: std::time::Duration,
846) -> Result<(), String> {
847 if !is_blocked() {
848 return Ok(());
849 }
850 let deadline = std::time::Instant::now() + max_wait;
851 while is_blocked() {
852 if std::time::Instant::now() >= deadline {
853 return Err("Tor is still connecting. Wait a moment and try again.".to_string());
854 }
855 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
856 }
857 Ok(())
858}
859
860pub async fn prune_unneeded_community_relays(candidates: &[String]) {
868 if candidates.is_empty() {
869 return;
870 }
871 let Some(client) = crate::state::nostr_client() else { return };
872
873 let mut still_needed: std::collections::HashSet<String> = std::collections::HashSet::new();
874 if let Ok(ids) = crate::db::community::list_community_ids() {
875 for id in ids {
876 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
877 for r in &c.relays {
878 still_needed.insert(r.clone());
879 }
880 }
881 }
882 }
883
884 let pool = client;
885 let pooled = pool.relays().all().await;
887 for url in candidates {
888 if still_needed.contains(url) {
889 continue;
890 }
891 if let Ok(parsed) = nostr_sdk::prelude::RelayUrl::parse(url) {
892 if let Some(relay) = pooled.get(&parsed) {
893 if relay.capabilities().load().can_read() || relay.capabilities().load().can_write() {
894 continue; }
896 }
897 let _ = pool.remove_relay(parsed).force().await; forget_warmed_relay(url);
899 }
900 }
901}
902
903pub struct LiveTransport {
910 timeout: std::time::Duration,
911}
912
913impl Default for LiveTransport {
914 fn default() -> Self {
915 Self::with_timeout(std::time::Duration::from_secs(10))
916 }
917}
918
919impl LiveTransport {
920 pub fn new() -> Self {
921 Self::default()
922 }
923
924 pub fn with_timeout(timeout: std::time::Duration) -> Self {
930 Self { timeout: crate::relay_request_timeout(timeout) }
931 }
932
933 pub(crate) async fn warm_client(relays: &[String], connect_timeout: std::time::Duration) -> Result<Client, String> {
939 if relays.is_empty() {
940 return Err("community has no relays configured".to_string());
941 }
942 #[cfg(feature = "tor")]
948 wait_until_tor_ready(
949 || matches!(crate::tor::transport_state(), crate::tor::TorTransportState::RequiredButInactive),
950 TOR_READY_WAIT,
951 ).await?;
952 let client = crate::state::nostr_client().ok_or_else(|| "nostr client not initialized".to_string())?;
953
954 let generation = crate::db::current_session_id();
958 {
959 let warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
960 if warmed.0 == generation && relays.iter().all(|r| warmed.1.contains(r)) {
961 return Ok(client);
962 }
963 }
964
965 let mut added_new = false;
971 let mut succeeded: Vec<&String> = Vec::new();
972 for url in relays {
973 match client
974 .add_managed_relay(url.as_str())
975 .capabilities(crate::community_relay_capabilities())
976 .await
977 {
978 Ok(true) => { added_new = true; succeeded.push(url); }
979 Ok(false) => { succeeded.push(url); }
980 Err(_) => {}
981 }
982 }
983 if succeeded.is_empty() {
984 return Err("no valid community relays could be added".to_string());
985 }
986 if added_new {
987 let _ = client.try_connect().timeout(connect_timeout).await;
992 } else {
993 client.connect().await;
995 }
996
997 {
1000 let mut warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
1001 if warmed.0 != generation {
1002 warmed.0 = generation;
1003 warmed.1.clear();
1004 }
1005 for url in succeeded {
1006 warmed.1.insert(url.clone());
1007 }
1008 }
1009 Ok(client)
1010 }
1011
1012 pub async fn fetch_counted(&self, query: &Query, relays: &[String]) -> Result<(Vec<Event>, usize, usize), String> {
1017 let client = Self::warm_client(relays, self.timeout).await?;
1018 let base_timeout = self.timeout;
1019 let filter = query.to_filter();
1020
1021 let evidence = effective_evidence(query);
1022
1023 let mut targets: Vec<String> = Vec::new();
1024 for r in relays {
1025 if !targets.contains(r) {
1026 targets.push(r.clone());
1027 }
1028 }
1029 targets = drop_unrevivable_targets(&client, targets).await;
1034
1035 if evidence == Evidence::Fast && targets.len() >= 2 {
1040 let alive: Vec<String> =
1041 targets.iter().filter(|r| !breaker_tripped(r)).cloned().collect();
1042 if !alive.is_empty() {
1043 targets = alive;
1044 }
1045 }
1046
1047 if targets.len() <= 1 {
1050 let Some(url) = targets.first() else {
1051 return Err("no valid relay to fetch from".to_string());
1052 };
1053 let res = fetch_relay_eose(&client, url, filter, base_timeout)
1054 .await
1055 .map_err(|_| format!("relay did not answer the fetch: {url}"));
1056 breaker_record(url, res.is_ok(), true);
1057 return res.map(|evs| (evs, 1, 1));
1058 }
1059
1060 fn merge_events(
1061 evs: Vec<Event>,
1062 result: &mut Vec<Event>,
1063 seen: &mut std::collections::HashSet<EventId>,
1064 ) {
1065 for e in evs {
1066 if seen.insert(e.id) {
1067 result.push(e);
1068 }
1069 }
1070 }
1071
1072 let demote = evidence == Evidence::Full && demotion_allowed();
1079 use futures_util::stream::{FuturesUnordered, StreamExt};
1080 let mut fetches: FuturesUnordered<_> = targets
1081 .iter()
1082 .map(|r| {
1083 let client = client.clone();
1084 let filter = filter.clone();
1085 let r = r.clone();
1086 let timeout = if demote && breaker_tripped(&r) {
1087 TRIPPED_TIMEOUT.min(base_timeout)
1088 } else {
1089 base_timeout
1090 };
1091 let full_budget = timeout >= base_timeout;
1092 tokio::spawn(async move {
1094 let out = fetch_relay_eose(&client, &r, filter, timeout).await;
1095 (r, full_budget, out)
1096 })
1097 })
1098 .collect();
1099
1100 let mut plan = UnionPlan::new(evidence, targets.len());
1101 let mut result: Vec<Event> = Vec::new();
1102 let mut union_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1103
1104 let mut quorum_deadline: Option<tokio::time::Instant> = None;
1109 let mut quorum_window_closed = false;
1110 while !plan.satisfied() && !plan.exhausted() {
1111 let next = match quorum_deadline {
1112 Some(deadline) => match tokio::time::timeout_at(deadline, fetches.next()).await {
1113 Ok(n) => n,
1114 Err(_) => {
1115 quorum_window_closed = true;
1116 break; }
1118 },
1119 None => fetches.next().await,
1120 };
1121 let Some(joined) = next else { break };
1122 match joined {
1123 Ok((url, full_budget, Ok(evs))) => {
1124 breaker_record(&url, true, full_budget);
1125 merge_events(evs, &mut result, &mut union_ids);
1126 plan.record(true);
1127 if evidence == Evidence::Quorum && quorum_deadline.is_none() {
1128 quorum_deadline = Some(
1129 tokio::time::Instant::now()
1130 + std::time::Duration::from_millis(QUORUM_GRACE_MS),
1131 );
1132 }
1133 }
1134 Ok((url, full_budget, Err(()))) => {
1135 breaker_record(&url, false, full_budget);
1136 plan.record(false);
1137 }
1138 Err(_) => plan.record(false), }
1140 }
1141
1142 if plan.successes() == 0 {
1143 return Err(format!(
1144 "no relay answered the fetch (0/{} attempted)",
1145 plan.attempted()
1146 ));
1147 }
1148
1149 if !fetches.is_empty() && !quorum_window_closed {
1153 let grace = tokio::time::sleep(std::time::Duration::from_millis(RESIDUAL_GRACE_MS));
1154 tokio::pin!(grace);
1155 loop {
1156 tokio::select! {
1157 _ = &mut grace => break,
1158 next = fetches.next() => match next {
1159 Some(Ok((url, full_budget, Ok(evs)))) => {
1160 breaker_record(&url, true, full_budget);
1161 merge_events(evs, &mut result, &mut union_ids);
1162 }
1163 Some(Ok((url, full_budget, Err(())))) => {
1164 breaker_record(&url, false, full_budget);
1165 }
1166 Some(Err(_)) => continue,
1167 None => break,
1168 }
1169 }
1170 }
1171 }
1172
1173 if !fetches.is_empty() {
1177 let seen: std::collections::HashSet<EventId> = result.iter().map(|e| e.id).collect();
1178 crate::db::spawn_bound(async move {
1181 let mut extra: Vec<Event> = Vec::new();
1182 let mut extra_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1183 while let Some(joined) = fetches.next().await {
1184 if let Ok((url, full_budget, out)) = joined {
1185 breaker_record(&url, out.is_ok(), full_budget);
1189 if let Ok(evs) = out {
1190 for e in evs {
1191 if !seen.contains(&e.id) && extra_ids.insert(e.id) {
1192 extra.push(e);
1193 }
1194 }
1195 }
1196 }
1197 }
1198 submit_stragglers(extra);
1199 });
1200 }
1201
1202 Ok((result, plan.successes(), plan.attempted()))
1203 }
1204}
1205
1206#[async_trait::async_trait]
1207impl Transport for LiveTransport {
1208 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1209 let client = Self::warm_client(relays, self.timeout).await?;
1210 let timeout = self.timeout;
1211 let mut targets: Vec<String> = Vec::new();
1212 for r in relays { if !targets.contains(r) { targets.push(r.clone()); } }
1213 targets = drop_unrevivable_targets(&client, targets).await;
1215 use futures_util::stream::{FuturesUnordered, StreamExt};
1220 let mut sends: FuturesUnordered<_> = targets
1221 .into_iter()
1222 .map(|r| {
1223 let client = client.clone();
1224 let event = event.clone();
1225 tokio::spawn(async move {
1227 matches!(
1228 tokio::time::timeout(timeout, client.send_event(&event).to(vec![r.clone()])).await,
1229 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains_key(&u)).unwrap_or(false)
1230 )
1231 })
1232 })
1233 .collect();
1234 while let Some(joined) = sends.next().await {
1235 if matches!(joined, Ok(true)) {
1236 return Ok(()); }
1238 }
1239 Err("no relay accepted the event".to_string())
1240 }
1241
1242 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1243 self.fetch_counted(query, relays).await.map(|(events, _successes, _attempted)| events)
1244 }
1245
1246 async fn fetch_plane(&self, plane: &Keys, query: &Query, relays: &[String]) -> Result<PlaneFetch, String> {
1247 if relays.is_empty() {
1248 return Ok(PlaneFetch { events: Vec::new(), answered: 0, attempted: 0 });
1249 }
1250 #[cfg(feature = "tor")]
1251 wait_until_tor_ready(
1252 || matches!(crate::tor::transport_state(), crate::tor::TorTransportState::RequiredButInactive),
1253 TOR_READY_WAIT,
1254 ).await?;
1255 let mut targets: Vec<String> = relays.iter().filter(|r| !breaker_tripped(r)).cloned().collect();
1260 if targets.is_empty() {
1261 targets = relays.to_vec();
1262 }
1263 let filter = query.to_filter();
1264 let generation = crate::db::current_session_id();
1265 let key = plane_pool_key(&plane.public_key().to_hex(), &targets);
1266
1267 let (hit, evicted) = plane_pool_take(generation, &key);
1270 disconnect_clients(evicted);
1271 let client = if let Some(c) = hit {
1272 c
1273 } else {
1274 let client = crate::apply_tor_proxy(
1280 nostr_sdk::prelude::Client::builder().authenticator(
1281 nostr_sdk::prelude::SignerAuthenticator::new(plane.clone()),
1282 ),
1283 )
1284 .build();
1285 for r in &targets {
1290 let _ = client.add_managed_relay(r.clone()).capabilities(crate::community_relay_capabilities()).await;
1291 }
1292 client.connect().await;
1293 for r in &targets {
1296 let _ = client
1297 .fetch_events(nostr_sdk::prelude::ReqTarget::single(r.clone(), [filter.clone()]))
1298 .timeout(crate::relay_request_timeout(std::time::Duration::from_secs(5)))
1302 .await;
1303 }
1304 let ev = plane_pool_insert(generation, key, client.clone());
1305 disconnect_clients(ev);
1306 client
1307 };
1308
1309 let targets = drop_unrevivable_targets(&client, targets).await;
1313
1314 let mut result: Vec<Event> = Vec::new();
1315 let mut seen: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1316 let mut successes = 0usize;
1317 {
1322 use futures_util::stream::{FuturesUnordered, StreamExt};
1323 let mut inflight: FuturesUnordered<_> = targets
1324 .iter()
1325 .map(|r| {
1326 let client = client.clone();
1327 let filter = filter.clone();
1328 let timeout = self.timeout;
1329 async move { (r.clone(), fetch_relay_eose(&client, r, filter, timeout).await) }
1330 })
1331 .collect();
1332 while let Some((r, res)) = inflight.next().await {
1333 breaker_record(&r, res.is_ok(), true);
1336 if let Ok(events) = res {
1337 successes += 1;
1338 for e in events {
1339 if seen.insert(e.id) {
1340 result.push(e);
1341 }
1342 }
1343 }
1344 }
1345 }
1346 if successes == 0 {
1351 return Err(format!("no relay answered the plane fetch (0/{} attempted)", targets.len()));
1352 }
1353 Ok(PlaneFetch { events: result, answered: successes, attempted: targets.len() })
1355 }
1356
1357 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1358 let client = Self::warm_client(relays, self.timeout).await?;
1363 let timeout = self.timeout;
1364 let event = event.clone();
1365 let backoff = std::time::Duration::from_millis(750);
1366 let mut pending: Vec<String> = Vec::new();
1367 for r in relays { if !pending.contains(r) { pending.push(r.clone()); } }
1368 if pending.is_empty() {
1369 return Err("no relays to broadcast to".to_string());
1370 }
1371
1372 let mut acked_any = false;
1377 let _ = tokio::time::timeout(CONFIRM_WINDOW, async {
1378 loop {
1379 let round = drop_unrevivable_targets(&client, pending.clone()).await;
1383 let sends = round.iter().cloned().map(|r| {
1384 let client = &client;
1385 let event = &event;
1386 Box::pin(async move {
1387 match tokio::time::timeout(timeout, client.send_event(event).to(vec![r.clone()])).await {
1388 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains_key(&u)).unwrap_or(false) => Ok(r),
1389 _ => Err(()),
1390 }
1391 })
1392 });
1393 if let Ok((winner, _losers)) = futures_util::future::select_ok(sends).await {
1394 acked_any = true;
1395 pending.retain(|r| r != &winner);
1396 break;
1397 }
1398 tokio::time::sleep(backoff).await;
1399 }
1400 })
1401 .await;
1402
1403 if !acked_any {
1404 return Err(format!("no relay accepted the event within {}s", CONFIRM_WINDOW.as_secs()));
1405 }
1406 if pending.is_empty() {
1407 return Ok(()); }
1409
1410 tokio::spawn(async move {
1416 let client_ref = &client;
1417 let event_ref = &event;
1418 let _ = durable_broadcast(&pending, MAX_PUBLISH_ATTEMPTS, backoff, move |round| {
1419 Box::pin(async move {
1420 match tokio::time::timeout(timeout, client_ref.send_event(event_ref).to(round.clone())).await {
1421 Ok(Ok(output)) => round.into_iter().filter(|p| RelayUrl::parse(p).map(|u| output.success.contains_key(&u)).unwrap_or(false)).collect(),
1422 _ => Vec::new(),
1423 }
1424 })
1425 })
1426 .await;
1427 });
1428 Ok(())
1429 }
1430}
1431
1432#[cfg(test)]
1433pub(crate) mod memory {
1434 use super::*;
1435 use std::collections::{HashMap, HashSet};
1436 use std::sync::Mutex;
1437
1438 pub struct MemoryRelay {
1443 per_relay: Mutex<HashMap<String, Vec<Event>>>,
1444 subscribers: Mutex<Vec<(Query, tokio::sync::mpsc::UnboundedSender<Event>)>>,
1445 }
1446
1447 fn is_ephemeral(kind: u16) -> bool {
1450 (20000..30000).contains(&kind)
1451 }
1452
1453 impl MemoryRelay {
1454 pub fn new() -> Self {
1455 MemoryRelay {
1456 per_relay: Mutex::new(HashMap::new()),
1457 subscribers: Mutex::new(Vec::new()),
1458 }
1459 }
1460
1461 pub fn stored_count(&self) -> usize {
1465 self.per_relay.lock().unwrap().values().map(|v| v.len()).sum()
1466 }
1467
1468 pub fn subscribe(&self, query: Query) -> tokio::sync::mpsc::UnboundedReceiver<Event> {
1471 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1472 self.subscribers.lock().unwrap().push((query, tx));
1473 rx
1474 }
1475
1476 fn deliver(&self, event: &Event) {
1478 self.subscribers.lock().unwrap().retain(|(q, tx)| {
1479 if q.matches(event) {
1480 tx.send(event.clone()).is_ok()
1481 } else {
1482 !tx.is_closed()
1483 }
1484 });
1485 }
1486
1487 pub fn inject(&self, event: &Event, relays: &[String]) {
1490 self.deliver(event);
1491 if is_ephemeral(event.kind.as_u16()) {
1492 return; }
1494 let d_tag = |e: &Event| e.tags.iter().find_map(|t| {
1500 let s = t.as_slice();
1501 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
1502 }).unwrap_or_default();
1503 let k = event.kind.as_u16();
1504 let replaceable = (30000..40000).contains(&k) || (10000..20000).contains(&k) || k == 0 || k == 3;
1505 let coord = (event.kind.as_u16(), event.pubkey, d_tag(event));
1506 let mut map = self.per_relay.lock().unwrap();
1507 for r in relays {
1508 let v = map.entry(r.clone()).or_default();
1509 if replaceable {
1510 v.retain(|e| (e.kind.as_u16(), e.pubkey, d_tag(e)) != coord);
1511 }
1512 v.push(event.clone());
1513 }
1514 }
1515
1516 pub fn count_on(&self, relay: &str) -> usize {
1518 self.per_relay.lock().unwrap().get(relay).map_or(0, |v| v.len())
1519 }
1520
1521 fn apply_deletion(&self, deletion: &Event, relays: &[String]) {
1526 let mut id_targets: HashSet<String> = HashSet::new();
1527 let mut coord_targets: HashSet<String> = HashSet::new();
1528 for t in deletion.tags.iter() {
1529 let s = t.as_slice();
1530 if s.len() >= 2 && s[0] == "e" {
1531 id_targets.insert(s[1].clone());
1532 } else if s.len() >= 2 && s[0] == "a" {
1533 coord_targets.insert(s[1].clone());
1534 }
1535 }
1536 let mut map = self.per_relay.lock().unwrap();
1537 for r in relays {
1538 if let Some(events) = map.get_mut(r) {
1539 events.retain(|e| {
1540 if e.pubkey != deletion.pubkey {
1541 return true;
1542 }
1543 if id_targets.contains(&e.id.to_hex()) {
1544 return false;
1545 }
1546 let d = e.tags.iter().find_map(|t| {
1548 let s = t.as_slice();
1549 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
1550 }).unwrap_or_default();
1551 let coord = format!("{}:{}:{}", e.kind.as_u16(), e.pubkey.to_hex(), d);
1552 !coord_targets.contains(&coord)
1553 });
1554 }
1555 }
1556 }
1557 }
1558
1559 #[async_trait::async_trait]
1560 impl Transport for MemoryRelay {
1561 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1562 if event.kind == Kind::EventDeletion {
1564 self.apply_deletion(event, relays);
1565 self.deliver(event);
1566 } else {
1567 self.inject(event, relays);
1568 }
1569 Ok(())
1570 }
1571
1572 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1575 self.publish(event, relays).await
1576 }
1577
1578 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1579 let map = self.per_relay.lock().unwrap();
1580 let mut seen = HashSet::new();
1581 let mut out = Vec::new();
1582 for r in relays {
1583 if let Some(events) = map.get(r) {
1584 for ev in events {
1585 if is_ephemeral(ev.kind.as_u16()) {
1588 continue;
1589 }
1590 if query.matches(ev) && seen.insert(ev.id) {
1591 out.push(ev.clone());
1592 }
1593 }
1594 }
1595 }
1596 if let Some(limit) = query.limit {
1599 out.sort_by(|a, b| b.created_at.cmp(&a.created_at).then_with(|| b.id.cmp(&a.id)));
1600 out.truncate(limit);
1601 }
1602 Ok(out)
1603 }
1604
1605 async fn fetch_plane(&self, _plane: &Keys, query: &Query, relays: &[String]) -> Result<PlaneFetch, String> {
1606 Ok(PlaneFetch::whole(self.fetch(query, relays).await?))
1608 }
1609 }
1610}
1611
1612#[cfg(test)]
1623pub(crate) mod adversarial {
1624 use super::memory::MemoryRelay;
1625 use super::*;
1626 use std::sync::atomic::{AtomicU64, Ordering};
1627 use std::sync::Mutex;
1628
1629 #[derive(Clone, Debug)]
1631 pub enum Fault {
1632 FailFetch(String),
1634 ShortPage(usize),
1637 Empty,
1639 }
1640
1641 #[derive(Clone, Debug, Default)]
1644 pub struct Match {
1645 pub author: Option<String>,
1647 pub nth: Option<u64>,
1649 pub from_nth: Option<u64>,
1651 }
1652
1653 impl Match {
1654 pub fn author(hex: &str) -> Self {
1655 Match { author: Some(hex.to_string()), ..Default::default() }
1656 }
1657 pub fn any() -> Self {
1658 Match::default()
1659 }
1660 pub fn nth(mut self, n: u64) -> Self {
1661 self.nth = Some(n);
1662 self
1663 }
1664 pub fn from_nth(mut self, n: u64) -> Self {
1665 self.from_nth = Some(n);
1666 self
1667 }
1668 }
1669
1670 struct Rule {
1671 m: Match,
1672 fault: Fault,
1673 hits: AtomicU64,
1674 }
1675
1676 pub struct AdversarialRelay {
1677 inner: MemoryRelay,
1678 rules: Mutex<Vec<Rule>>,
1679 fetches: AtomicU64,
1682 }
1683
1684 impl AdversarialRelay {
1685 pub fn new() -> Self {
1686 AdversarialRelay { inner: MemoryRelay::new(), rules: Mutex::new(Vec::new()), fetches: AtomicU64::new(0) }
1687 }
1688
1689 pub fn relay(&self) -> &MemoryRelay {
1692 &self.inner
1693 }
1694
1695 pub fn on(&self, m: Match, fault: Fault) -> &Self {
1696 self.rules.lock().unwrap_or_else(|e| e.into_inner()).push(Rule { m, fault, hits: AtomicU64::new(0) });
1697 self
1698 }
1699
1700 pub fn heal(&self) {
1703 self.rules.lock().unwrap_or_else(|e| e.into_inner()).clear();
1704 }
1705
1706 pub fn fetch_count(&self) -> u64 {
1707 self.fetches.load(Ordering::Relaxed)
1708 }
1709
1710 pub fn reset_fetch_count(&self) {
1711 self.fetches.store(0, Ordering::Relaxed);
1712 }
1713
1714 fn verdict(&self, query: &Query) -> Option<Fault> {
1715 let rules = self.rules.lock().unwrap_or_else(|e| e.into_inner());
1716 for r in rules.iter() {
1717 if let Some(a) = &r.m.author {
1718 if !query.authors.iter().any(|q| q == a) {
1719 continue;
1720 }
1721 }
1722 let n = r.hits.fetch_add(1, Ordering::Relaxed) + 1;
1723 if r.m.nth.is_some_and(|want| want != n) {
1724 continue;
1725 }
1726 if r.m.from_nth.is_some_and(|first| n < first) {
1727 continue;
1728 }
1729 return Some(r.fault.clone());
1730 }
1731 None
1732 }
1733
1734 async fn governed(&self, query: &Query, relays: &[String], plane: Option<&Keys>) -> Result<Vec<Event>, String> {
1735 self.fetches.fetch_add(1, Ordering::Relaxed);
1736 match self.verdict(query) {
1737 Some(Fault::FailFetch(why)) => return Err(why),
1738 Some(Fault::Empty) => return Ok(Vec::new()),
1739 Some(Fault::ShortPage(n)) => {
1740 let mut out = match plane {
1741 Some(p) => self.inner.fetch_plane(p, query, relays).await?.events,
1742 None => self.inner.fetch(query, relays).await?,
1743 };
1744 out.truncate(n);
1745 return Ok(out);
1746 }
1747 None => {}
1748 }
1749 match plane {
1750 Some(p) => self.inner.fetch_plane(p, query, relays).await.map(|f| f.events),
1751 None => self.inner.fetch(query, relays).await,
1752 }
1753 }
1754 }
1755
1756 #[async_trait::async_trait]
1757 impl Transport for AdversarialRelay {
1758 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1759 self.inner.publish(event, relays).await
1760 }
1761 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1762 self.inner.publish_durable(event, relays).await
1763 }
1764 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1765 self.governed(query, relays, None).await
1766 }
1767 async fn fetch_plane(&self, plane: &Keys, query: &Query, relays: &[String]) -> Result<PlaneFetch, String> {
1768 Ok(PlaneFetch::whole(self.governed(query, relays, Some(plane)).await?))
1772 }
1773 }
1774}
1775
1776#[cfg(test)]
1777mod tests {
1778 use super::*;
1779
1780 #[test]
1783 fn a_dead_socket_is_skipped_but_a_connecting_or_unknown_one_is_not() {
1784 let targets: Vec<String> = ["wss://dead", "wss://connecting", "wss://unknown"].map(String::from).into();
1785 let out = drop_unrevivable(targets, |r| r == "wss://dead");
1786 assert_eq!(out, ["wss://connecting", "wss://unknown"].map(String::from).to_vec());
1787 }
1788
1789 #[test]
1790 fn an_all_dead_set_falls_back_to_the_full_list() {
1791 let targets: Vec<String> = ["wss://a", "wss://b"].map(String::from).into();
1794 let out = drop_unrevivable(targets.clone(), |_| true);
1795 assert_eq!(out, targets);
1796 }
1797
1798 #[tokio::test]
1803 async fn tor_gate_passes_immediately_when_not_blocked() {
1804 let start = std::time::Instant::now();
1805 let res = wait_until_tor_ready(|| false, std::time::Duration::from_secs(30)).await;
1806 assert!(res.is_ok());
1807 assert!(start.elapsed() < std::time::Duration::from_secs(1), "must not wait when Tor is ready");
1808 }
1809
1810 #[tokio::test]
1811 async fn tor_gate_errors_honestly_after_timeout_when_perpetually_blocked() {
1812 let res = wait_until_tor_ready(|| true, std::time::Duration::from_millis(300)).await;
1815 let err = res.expect_err("should error when Tor never activates");
1816 assert!(err.to_lowercase().contains("tor"), "error must name Tor, got: {err}");
1817 }
1818
1819 #[tokio::test]
1820 async fn tor_gate_passes_once_circuit_comes_up_mid_wait() {
1821 let calls = std::sync::atomic::AtomicUsize::new(0);
1822 let res = wait_until_tor_ready(
1824 || calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 3,
1825 std::time::Duration::from_secs(5),
1826 ).await;
1827 assert!(res.is_ok(), "should succeed once Tor activates within the window");
1828 }
1829
1830 fn evt(kind: u16, z: &str) -> Event {
1831 EventBuilder::new(Kind::Custom(kind), "x")
1832 .tags([Tag::custom(
1833 "z",
1834 [z.to_string()],
1835 )])
1836 .finalize(&Keys::generate())
1837 .unwrap()
1838 }
1839
1840 #[test]
1841 fn query_matches_kind_and_z() {
1842 let e = evt(3300, "abc");
1843 assert!(Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: None, ..Default::default() }.matches(&e));
1844 assert!(!Query { kinds: vec![3301], ..Default::default() }.matches(&e));
1845 assert!(!Query { kinds: vec![], z_tags: vec!["xyz".into()], since: None, ..Default::default() }.matches(&e));
1846 assert!(Query::default().matches(&e), "empty query matches anything");
1847 }
1848
1849 fn evt_at(kind: u16, secs: u64) -> Event {
1850 EventBuilder::new(Kind::Custom(kind), "x")
1851 .custom_created_at(Timestamp::from(secs))
1852 .finalize(&Keys::generate())
1853 .unwrap()
1854 }
1855
1856 fn evt_z_at(kind: u16, z: &str, secs: u64) -> Event {
1859 EventBuilder::new(Kind::Custom(kind), "x")
1860 .custom_created_at(Timestamp::from(secs))
1861 .tags([Tag::custom(
1862 "z",
1863 [z.to_string()],
1864 )])
1865 .finalize(&Keys::generate())
1866 .unwrap()
1867 }
1868
1869 #[tokio::test]
1870 async fn fetch_pages_with_until_and_limit_newest_first() {
1871 let relay = super::memory::MemoryRelay::new();
1873 let relays = vec!["r1".to_string()];
1874 for s in 1..=5u64 {
1875 relay.inject(&evt_z_at(3300, "pg", s), &relays);
1876 }
1877 let secs = |evs: &[Event]| evs.iter().map(|e| e.created_at.as_secs()).collect::<Vec<_>>();
1878
1879 let latest = relay
1881 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], limit: Some(2), ..Default::default() }, &relays)
1882 .await
1883 .unwrap();
1884 assert_eq!(secs(&latest), vec![5, 4]);
1885
1886 let older = relay
1888 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(3), limit: Some(2), ..Default::default() }, &relays)
1889 .await
1890 .unwrap();
1891 assert_eq!(secs(&older), vec![3, 2]);
1892
1893 let start = relay
1896 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(1), limit: Some(2), ..Default::default() }, &relays)
1897 .await
1898 .unwrap();
1899 assert_eq!(secs(&start), vec![1]);
1900 }
1901
1902 #[test]
1903 fn to_filter_translates_kinds_z_and_since() {
1904 let q = Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: Some(100), ..Default::default() };
1907 let filter = q.to_filter();
1908 let matching = EventBuilder::new(Kind::Custom(3300), "x")
1909 .custom_created_at(Timestamp::from(150))
1910 .tags([Tag::custom(
1911 "z",
1912 ["abc".to_string()],
1913 )])
1914 .finalize(&Keys::generate())
1915 .unwrap();
1916 assert!(filter.match_event(&matching, MatchEventOptions::new()), "to_filter must accept what matches() accepts");
1917 assert!(q.matches(&matching));
1918
1919 let wrong_kind = EventBuilder::new(Kind::Custom(3301), "x")
1921 .custom_created_at(Timestamp::from(150))
1922 .tags([Tag::custom(
1923 "z",
1924 ["abc".to_string()],
1925 )])
1926 .finalize(&Keys::generate())
1927 .unwrap();
1928 assert!(!filter.match_event(&wrong_kind, MatchEventOptions::new()));
1929 }
1930
1931 fn evt_sl(kind: u16, letter: SingleLetterTag, value: &str) -> Event {
1933 EventBuilder::new(Kind::Custom(kind), "x")
1934 .tags([Tag::custom(
1935 letter.as_char().to_string(),
1936 [value.to_string()],
1937 )])
1938 .finalize(&Keys::generate())
1939 .unwrap()
1940 }
1941
1942 #[test]
1943 fn to_filter_and_matches_agree_on_authors() {
1944 let keys = Keys::generate();
1945 let e = EventBuilder::new(Kind::Custom(1059), "x").finalize(&keys).unwrap();
1946 let q = Query { kinds: vec![1059], authors: vec![keys.public_key().to_hex()], ..Default::default() };
1947 assert!(q.matches(&e));
1948 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1949 let miss = Query { kinds: vec![1059], authors: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
1950 assert!(!miss.matches(&e));
1951 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1952 }
1953
1954 #[test]
1955 fn to_filter_and_matches_agree_on_p_tags() {
1956 let recipient = Keys::generate().public_key().to_hex();
1957 let e = evt_sl(1059, SingleLetterTag::LOWERCASE_P, &recipient);
1958 let q = Query { kinds: vec![1059], p_tags: vec![recipient], ..Default::default() };
1959 assert!(q.matches(&e));
1960 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1961 let miss = Query { kinds: vec![1059], p_tags: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
1962 assert!(!miss.matches(&e));
1963 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1964 }
1965
1966 #[test]
1967 fn to_filter_and_matches_agree_on_k_tags() {
1968 let e = evt_sl(1059, SingleLetterTag::LOWERCASE_K, "3311");
1969 let q = Query { kinds: vec![1059], k_tags: vec!["3311".into()], ..Default::default() };
1970 assert!(q.matches(&e));
1971 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1972 let miss = Query { kinds: vec![1059], k_tags: vec!["3300".into()], ..Default::default() };
1973 assert!(!miss.matches(&e));
1974 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1975 }
1976
1977 #[test]
1978 fn to_filter_empty_kinds_only_constrains_z_and_since() {
1979 let q = Query { kinds: vec![], z_tags: vec!["p".into()], since: None, ..Default::default() };
1981 let filter = q.to_filter();
1982 let e = evt(3300, "p");
1983 assert!(filter.match_event(&e, MatchEventOptions::new()));
1984 assert!(q.matches(&e));
1985 }
1986
1987 #[test]
1988 fn since_is_an_inclusive_lower_bound() {
1989 let below = evt_at(3300, 99);
1990 let exact = evt_at(3300, 100);
1991 let above = evt_at(3300, 101);
1992 let q = Query { kinds: vec![3300], z_tags: vec![], since: Some(100), ..Default::default() };
1993 assert!(!q.matches(&below), "below the floor is excluded");
1994 assert!(q.matches(&exact), "exactly the floor is included");
1995 assert!(q.matches(&above), "above the floor is included");
1996 }
1997
1998 #[test]
1999 fn z_tags_match_as_or_set() {
2000 let e = evt(3300, "p2");
2003 let q = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p2".into()], since: None, ..Default::default() };
2004 assert!(q.matches(&e));
2005 let miss = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p3".into()], since: None, ..Default::default() };
2006 assert!(!miss.matches(&e));
2007 }
2008
2009 #[tokio::test]
2010 async fn fetch_unions_and_dedups_across_relays() {
2011 use super::memory::MemoryRelay;
2012 let relay = MemoryRelay::new();
2013 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
2014 let e = evt(3300, "p");
2015 relay.publish(&e, &relays).await.unwrap();
2016 let got = relay
2017 .fetch(&Query { kinds: vec![3300], z_tags: vec!["p".into()], since: None, ..Default::default() }, &relays)
2018 .await
2019 .unwrap();
2020 assert_eq!(got.len(), 1, "same event on 3 relays dedups to 1");
2021 }
2022
2023 #[tokio::test]
2024 async fn durable_broadcast_retries_only_the_failing_relays_until_they_ack() {
2025 use std::cell::Cell;
2028 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
2029 let round = Cell::new(0usize);
2030 let r2_round_seen = Cell::new(0usize);
2031 let res = durable_broadcast(&relays, 30, std::time::Duration::ZERO, |pending| {
2032 let n = round.get();
2033 round.set(n + 1);
2034 if n >= 1 {
2036 assert_eq!(pending, vec!["r2".to_string()], "only the failing relay is retried");
2037 r2_round_seen.set(r2_round_seen.get() + 1);
2038 }
2039 Box::pin(async move {
2040 pending.into_iter().filter(|r| r != "r2" || n >= 4).collect()
2041 })
2042 })
2043 .await;
2044 assert!(res.is_ok(), "all relays eventually ACK → Ok");
2045 assert!(round.get() >= 5, "kept retrying r2 across rounds");
2046 }
2047
2048 #[tokio::test]
2049 async fn durable_broadcast_is_ok_if_some_ack_even_when_one_never_does() {
2050 let relays = vec!["r1".to_string(), "r2".to_string()];
2053 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |pending| {
2054 Box::pin(async move { pending.into_iter().filter(|r| r == "r1").collect() })
2055 })
2056 .await;
2057 assert!(res.is_ok(), "≥1 relay accepted → Ok despite a permanently-failing relay");
2058 }
2059
2060 #[tokio::test]
2061 async fn durable_broadcast_errs_only_if_zero_relays_ever_accept() {
2062 let relays = vec!["r1".to_string(), "r2".to_string()];
2063 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |_pending| {
2064 Box::pin(async move { Vec::new() }) })
2066 .await;
2067 assert!(res.is_err(), "zero acceptances after the retry cap → Err");
2068 }
2069
2070 #[tokio::test]
2071 async fn redundancy_self_heals_a_missing_relay() {
2072 use super::memory::MemoryRelay;
2073 let relay = MemoryRelay::new();
2074 let all = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
2075 let e = evt(3300, "p");
2076 relay.inject(&e, &["r2".to_string()]);
2078 assert_eq!(relay.count_on("r1"), 0);
2079 assert_eq!(relay.count_on("r2"), 1);
2080 let got = relay.fetch(&Query { kinds: vec![3300], ..Default::default() }, &all).await.unwrap();
2082 assert_eq!(got.len(), 1);
2083 }
2084
2085 #[tokio::test]
2086 async fn ephemeral_kind_streams_live_but_is_never_stored_or_fetched() {
2087 use super::memory::MemoryRelay;
2088 let relay = MemoryRelay::new();
2089 let relays = vec!["r1".to_string()];
2090 let mut sub = relay.subscribe(Query { kinds: vec![21059], ..Default::default() });
2091 let e = evt(21059, "p");
2092 relay.publish(&e, &relays).await.unwrap();
2093 assert_eq!(relay.count_on("r1"), 0, "ephemeral is never stored");
2094 let got = relay
2095 .fetch(&Query { kinds: vec![21059], ..Default::default() }, &relays)
2096 .await
2097 .unwrap();
2098 assert!(got.is_empty(), "a real relay never serves an ephemeral from a fetch");
2099 assert_eq!(sub.try_recv().unwrap().id, e.id, "but a live subscriber receives it");
2100 }
2101
2102 #[tokio::test]
2103 async fn stored_kind_is_fetchable_and_delivered_live() {
2104 use super::memory::MemoryRelay;
2105 let relay = MemoryRelay::new();
2106 let relays = vec!["r1".to_string()];
2107 let mut sub = relay.subscribe(Query { kinds: vec![1059], ..Default::default() });
2108 let e = evt(1059, "p");
2109 relay.publish(&e, &relays).await.unwrap();
2110 let got = relay
2111 .fetch(&Query { kinds: vec![1059], ..Default::default() }, &relays)
2112 .await
2113 .unwrap();
2114 assert_eq!(got.len(), 1, "stored kind is fetchable");
2115 assert_eq!(sub.try_recv().unwrap().id, e.id, "and delivered to the live subscriber");
2116 }
2117
2118 #[tokio::test]
2119 async fn p_tags_route_a_giftwrap_to_the_matching_subscriber() {
2120 use super::memory::MemoryRelay;
2121 let relay = MemoryRelay::new();
2122 let relays = vec!["r1".to_string()];
2123 let alice = Keys::generate().public_key().to_hex();
2124 let bob = Keys::generate().public_key().to_hex();
2125 let mut sub_alice =
2126 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![alice.clone()], ..Default::default() });
2127 let mut sub_bob =
2128 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![bob.clone()], ..Default::default() });
2129 let wrap = evt_sl(1059, SingleLetterTag::LOWERCASE_P, &alice);
2130 relay.publish(&wrap, &relays).await.unwrap();
2131 assert_eq!(sub_alice.try_recv().unwrap().id, wrap.id, "addressed recipient gets it live");
2132 assert!(sub_bob.try_recv().is_err(), "a differently-addressed subscriber does not");
2133 let for_alice = relay
2135 .fetch(&Query { kinds: vec![1059], p_tags: vec![alice], ..Default::default() }, &relays)
2136 .await
2137 .unwrap();
2138 assert_eq!(for_alice.len(), 1);
2139 let for_bob = relay
2140 .fetch(&Query { kinds: vec![1059], p_tags: vec![bob], ..Default::default() }, &relays)
2141 .await
2142 .unwrap();
2143 assert!(for_bob.is_empty());
2144 }
2145
2146 #[test]
2149 fn union_plan_fast_satisfied_on_first_success() {
2150 let mut p = UnionPlan::new(Evidence::Fast, 4);
2151 p.record(false);
2152 assert!(!p.satisfied(), "a failure is not evidence");
2153 p.record(true);
2154 assert!(p.satisfied(), "one genuine EOSE satisfies Fast");
2155 assert!(!p.exhausted());
2156 }
2157
2158 #[test]
2159 fn union_plan_quorum_majority_math() {
2160 for (n, need) in [(2usize, 2usize), (3, 2), (4, 3), (5, 3)] {
2162 let mut p = UnionPlan::new(Evidence::Quorum, n);
2163 for _ in 0..need - 1 {
2164 p.record(true);
2165 }
2166 assert!(!p.satisfied(), "{}/{} must not satisfy quorum", need - 1, n);
2167 p.record(true);
2168 assert!(p.satisfied(), "{}/{} satisfies quorum", need, n);
2169 }
2170 }
2171
2172 #[test]
2173 fn union_plan_quorum_failures_never_substitute_for_successes() {
2174 let mut p = UnionPlan::new(Evidence::Quorum, 3);
2175 p.record(true);
2176 p.record(false);
2177 p.record(false);
2178 assert!(!p.satisfied(), "1 success + 2 failures is not a majority");
2179 assert!(p.exhausted(), "all resolved — the degraded path returns best-effort");
2180 assert_eq!(p.successes(), 1);
2181 }
2182
2183 #[test]
2184 fn union_plan_full_requires_every_relay_resolved() {
2185 let mut p = UnionPlan::new(Evidence::Full, 3);
2186 p.record(true);
2187 p.record(true);
2188 assert!(!p.satisfied(), "Full waits for the last relay even after 2 EOSEs");
2189 p.record(false);
2190 assert!(p.satisfied(), "a timeout is a resolution — Full is done");
2191 assert!(p.exhausted());
2192 }
2193
2194 #[test]
2195 fn union_plan_all_dead_is_reportable_not_a_confident_empty() {
2196 let mut p = UnionPlan::new(Evidence::Quorum, 2);
2197 p.record(false);
2198 p.record(false);
2199 assert!(p.exhausted());
2200 assert_eq!(p.successes(), 0, "the caller must map this to Err, never Ok(vec![])");
2201 }
2202
2203 const BREAKER_TEST_GEN: u64 = u64::MAX;
2209
2210 #[test]
2211 fn breaker_trips_only_after_consecutive_full_budget_failures() {
2212 let url = "wss://breaker-test-full-budget.example";
2213 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2214 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "one failure is below the threshold");
2215 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2216 assert!(breaker_tripped_at(BREAKER_TEST_GEN, url), "two consecutive full-budget failures trip");
2217 }
2218
2219 #[test]
2220 fn breaker_demoted_budget_failures_never_count() {
2221 let url = "wss://breaker-test-demoted.example";
2222 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
2223 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
2224 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
2225 assert!(
2226 !breaker_tripped_at(BREAKER_TEST_GEN, url),
2227 "demoted-budget failures must not trip (anti-starvation: the post-cooldown probe must stay reachable)"
2228 );
2229 }
2230
2231 #[test]
2232 fn breaker_success_resets_the_entry() {
2233 let url = "wss://breaker-test-reset.example";
2234 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2235 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2236 assert!(breaker_tripped_at(BREAKER_TEST_GEN, url));
2237 breaker_record_at(BREAKER_TEST_GEN, url, true, false);
2239 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "any success unconditionally resets");
2240 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
2241 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "and the failure count restarted from zero");
2242 }
2243
2244 #[test]
2247 fn declared_evidence_stands_and_default_is_quorum() {
2248 assert_eq!(Query::default().evidence, Evidence::Quorum, "unclassified sites get Quorum");
2249 assert_eq!(
2250 effective_evidence(&Query { until: Some(1), evidence: Evidence::Fast, ..Default::default() }),
2251 Evidence::Fast,
2252 "chat pagination rides its declared tier — absence verdicts request Full themselves"
2253 );
2254 assert_eq!(
2255 effective_evidence(&Query { evidence: Evidence::Fast, ..Default::default() }),
2256 Evidence::Fast,
2257 "without `until` the declared tier stands"
2258 );
2259 }
2260}