fips_core/
nostr_relay_adapter.rs1use std::sync::Arc;
8use std::time::Duration;
9
10use nostr_sdk::Event;
11use nostr_sdk::prelude::{Client, Filter, Keys, Kind, PublicKey, RelayPoolNotification};
12use tokio::sync::{Semaphore, oneshot};
13use tokio::task::JoinHandle;
14
15use crate::transport::nostr_relay::NOSTR_RELAY_DATAGRAM_KIND;
16use crate::{FipsEndpoint, FipsEndpointError, NostrRelayIo};
17
18const NOSTR_RELAY_ACTIVE_OUTBOUND_POLL_INTERVAL: Duration = Duration::from_millis(10);
19const NOSTR_RELAY_IDLE_OUTBOUND_POLL_INTERVAL: Duration = Duration::from_millis(250);
20
21const fn outbound_poll_delay(drained_events: usize) -> Duration {
22 if drained_events == 0 {
23 NOSTR_RELAY_IDLE_OUTBOUND_POLL_INTERVAL
24 } else {
25 NOSTR_RELAY_ACTIVE_OUTBOUND_POLL_INTERVAL
26 }
27}
28
29pub struct NostrRelayAdapter {
32 client: Client,
33 shutdown: Option<oneshot::Sender<()>>,
34 task: JoinHandle<()>,
35}
36
37#[derive(Clone)]
38enum RelayEventIo {
39 Endpoint(Arc<FipsEndpoint>),
40 Node(NostrRelayIo),
41}
42
43impl RelayEventIo {
44 fn npub(&self) -> &str {
45 match self {
46 Self::Endpoint(endpoint) => endpoint.npub(),
47 Self::Node(io) => io.npub(),
48 }
49 }
50
51 async fn drain_events(&self, limit: usize) -> Result<Vec<Event>, FipsEndpointError> {
52 match self {
53 Self::Endpoint(endpoint) => endpoint.drain_nostr_relay_events(limit).await,
54 Self::Node(io) => io.drain_events(limit).await,
55 }
56 }
57
58 async fn ingest_event(&self, event: Event) -> Result<bool, FipsEndpointError> {
59 match self {
60 Self::Endpoint(endpoint) => endpoint.ingest_nostr_event(event).await,
61 Self::Node(io) => io.ingest_event(event).await,
62 }
63 }
64}
65
66impl NostrRelayAdapter {
67 pub async fn start(
69 endpoint: Arc<FipsEndpoint>,
70 relays: &[String],
71 ) -> Result<Option<Self>, String> {
72 Self::start_with_io(RelayEventIo::Endpoint(endpoint), relays).await
73 }
74
75 pub async fn start_for_node(
77 io: NostrRelayIo,
78 relays: &[String],
79 ) -> Result<Option<Self>, String> {
80 Self::start_with_io(RelayEventIo::Node(io), relays).await
81 }
82
83 async fn start_with_io(io: RelayEventIo, relays: &[String]) -> Result<Option<Self>, String> {
84 if relays.is_empty() {
85 return Ok(None);
86 }
87
88 let local_pubkey = PublicKey::parse(io.npub())
89 .map_err(|error| format!("invalid FIPS endpoint identity: {error}"))?;
90 let client = Client::new(Keys::generate());
91 for relay in relays {
92 client
93 .add_relay(relay)
94 .await
95 .map_err(|error| format!("failed to add Nostr relay {relay}: {error}"))?;
96 }
97 let mut notifications = client.notifications();
98 client.connect().await;
99 client
100 .subscribe(
101 Filter::new()
102 .kind(Kind::Custom(NOSTR_RELAY_DATAGRAM_KIND))
103 .pubkey(local_pubkey),
104 None,
105 )
106 .await
107 .map_err(|error| format!("failed to subscribe to FIPS relay datagrams: {error}"))?;
108
109 let task_client = client.clone();
110 let publish_slots = Arc::new(Semaphore::new(32));
111 let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
112 let task = tokio::spawn(async move {
113 let outbound_sleep = tokio::time::sleep(Duration::ZERO);
114 tokio::pin!(outbound_sleep);
115 loop {
116 tokio::select! {
117 _ = &mut shutdown_rx => break,
118 () = &mut outbound_sleep => {
119 let next_delay = match io.drain_events(64).await {
120 Ok(events) => {
121 let delay = outbound_poll_delay(events.len());
122 for event in events {
123 let Ok(permit) = Arc::clone(&publish_slots).try_acquire_owned() else {
124 tracing::debug!(event_id = %event.id, "FIPS relay publish queue is saturated");
125 continue;
126 };
127 let client = task_client.clone();
128 tokio::spawn(async move {
129 let _permit = permit;
130 if let Err(error) = client.send_event(&event).await {
131 tracing::debug!(%error, event_id = %event.id, "failed to publish FIPS relay datagram");
132 }
133 });
134 }
135 delay
136 }
137 Err(error) => {
138 tracing::debug!(%error, "failed to drain FIPS relay datagrams");
139 NOSTR_RELAY_IDLE_OUTBOUND_POLL_INTERVAL
140 }
141 };
142 outbound_sleep
143 .as_mut()
144 .reset(tokio::time::Instant::now() + next_delay);
145 }
146 notification = notifications.recv() => {
147 match notification {
148 Ok(RelayPoolNotification::Event { event, .. })
149 if event.kind == Kind::Custom(NOSTR_RELAY_DATAGRAM_KIND) =>
150 {
151 if let Err(error) = io.ingest_event((*event).clone()).await {
152 tracing::debug!(%error, "failed to ingest FIPS relay datagram");
153 }
154 }
155 Ok(_) => {}
156 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
157 tracing::debug!(skipped, "FIPS relay notification receiver lagged");
158 }
159 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
160 }
161 }
162 }
163 }
164 });
165
166 Ok(Some(Self {
167 client,
168 shutdown: Some(shutdown_tx),
169 task,
170 }))
171 }
172
173 pub async fn stop(mut self) {
175 if let Some(shutdown) = self.shutdown.take() {
176 let _ = shutdown.send(());
177 }
178 let _ = (&mut self.task).await;
179 self.client.shutdown().await;
180 }
181}
182
183impl Drop for NostrRelayAdapter {
184 fn drop(&mut self) {
185 if let Some(shutdown) = self.shutdown.take() {
186 let _ = shutdown.send(());
187 }
188 self.task.abort();
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn nostr_relay_outbound_poll_delay_is_adaptive() {
198 assert_eq!(
199 outbound_poll_delay(1),
200 NOSTR_RELAY_ACTIVE_OUTBOUND_POLL_INTERVAL
201 );
202 assert_eq!(
203 outbound_poll_delay(0),
204 NOSTR_RELAY_IDLE_OUTBOUND_POLL_INTERVAL
205 );
206 assert!(NOSTR_RELAY_ACTIVE_OUTBOUND_POLL_INTERVAL <= Duration::from_millis(10));
207 assert!(NOSTR_RELAY_IDLE_OUTBOUND_POLL_INTERVAL >= Duration::from_millis(250));
208 }
209}