hashtree_cli/fips_transport/
retained_provider.rs1use super::DaemonNostrCache;
2use crate::{Config, NostrRootResolver};
3use anyhow::{Context, Result};
4use nostr::nips::nip19::ToBech32;
5use nostr::JsonUtil;
6use nostr_pubsub::{
7 EventBus, EventSource, Filter, PublishReport, PubsubProvider, PubsubProviderMode, QueryOptions,
8 QueryReport, VerifiedEvent,
9};
10use nostr_pubsub_fips::FipsPubsubClient;
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::task::JoinHandle;
14
15pub struct DaemonNostrProvider {
19 mode: PubsubProviderMode,
20 state: tokio::sync::RwLock<Option<ProviderState>>,
21}
22
23struct ProviderState {
24 inner: Arc<dyn PubsubProvider>,
25 cache: Option<Arc<DaemonNostrCache>>,
26 client: Option<Arc<FipsPubsubClient>>,
27 intake: Vec<JoinHandle<()>>,
28}
29
30impl Drop for ProviderState {
31 fn drop(&mut self) {
32 for task in &self.intake {
33 task.abort();
34 }
35 }
36}
37
38impl DaemonNostrProvider {
39 pub(super) fn new(
40 inner: Arc<dyn PubsubProvider>,
41 cache: Option<Arc<DaemonNostrCache>>,
42 client: Option<Arc<FipsPubsubClient>>,
43 intake: Vec<JoinHandle<()>>,
44 ) -> Self {
45 Self {
46 mode: inner.mode(),
47 state: tokio::sync::RwLock::new(Some(ProviderState {
48 inner,
49 cache,
50 client,
51 intake,
52 })),
53 }
54 }
55
56 pub async fn shutdown(&self) {
57 let mut state = self.state.write().await;
58 if let Some(mut owned) = state.take() {
59 if let Some(client) = &owned.client {
60 if let Err(error) = client.set_replay_source(None) {
61 tracing::warn!(%error, "Retained head replay source could not be detached");
62 }
63 }
64 if let Some(cache) = &owned.cache {
65 close_intake(std::mem::take(&mut owned.intake), cache).await;
66 }
67 }
68 }
69}
70
71#[async_trait::async_trait]
72impl EventBus for DaemonNostrProvider {
73 async fn publish(
74 &self,
75 event: VerifiedEvent,
76 source: EventSource,
77 ) -> nostr_pubsub::Result<PublishReport> {
78 let state = self.state.read().await;
81 let state = state.as_ref().ok_or_else(closed)?;
82 if let Some(cache) = state.cache.as_ref().filter(|_| is_public_head(&event)) {
83 if event.as_event().as_json().len()
84 > nostr_pubsub_fips::FIPS_NOSTR_PUBSUB_MAX_FRAME_BYTES
85 {
86 return Err(nostr_pubsub::PubsubError::Validation(
87 "Signed root exceeds the retained event size limit".into(),
88 ));
89 }
90 let report = cache.publish(event.clone(), source.clone()).await?;
91 if !report.accepted {
92 return Ok(report);
93 }
94 }
95 state.inner.publish(event, source).await
96 }
97
98 async fn query(
99 &self,
100 filters: Vec<Filter>,
101 options: QueryOptions,
102 ) -> nostr_pubsub::Result<QueryReport> {
103 let state = self.state.read().await;
104 state
105 .as_ref()
106 .ok_or_else(closed)?
107 .inner
108 .query(filters, options)
109 .await
110 }
111}
112
113fn closed() -> nostr_pubsub::PubsubError {
114 nostr_pubsub::PubsubError::Storage("Daemon Nostr provider is closed".into())
115}
116
117impl PubsubProvider for DaemonNostrProvider {
118 fn mode(&self) -> PubsubProviderMode {
119 self.mode
121 }
122}
123
124pub(super) fn is_public_head(event: &VerifiedEvent) -> bool {
125 let event = event.as_event();
126 let Some(tree) = event.tags.identifier() else {
127 return false;
128 };
129 let author = event
130 .pubkey
131 .to_bech32()
132 .expect("public key bech32 encoding is infallible");
133 NostrRootResolver::event_matches_key(&format!("{author}/{tree}"), event).unwrap_or(false)
134}
135
136type IntakeEvent = (nostr_pubsub::QueryEvent, bool);
137
138pub(super) async fn start_intake(
139 config: &Config,
140 cache: Arc<DaemonNostrCache>,
141 client: Option<Arc<FipsPubsubClient>>,
142) -> Result<Vec<JoinHandle<()>>> {
143 if config.nostr.retained_roots.len() > 64 {
144 anyhow::bail!("nostr.retained_roots is limited to 64 author/tree keys");
145 }
146 let filters = config
147 .nostr
148 .retained_roots
149 .iter()
150 .map(|key| NostrRootResolver::filter_for_key(key).map(|filter| filter.limit(1)))
151 .collect::<std::result::Result<Vec<_>, _>>()?;
152 if filters.is_empty() {
153 return Ok(Vec::new());
154 }
155 if let Some(client) = &client {
156 let options = client.options();
157 if filters.len().div_ceil(options.max_filters_per_subscription)
159 >= options.max_active_subscriptions
160 {
161 anyhow::bail!("Retained roots exceed available FIPS subscription capacity");
162 }
163 }
164 let relay = if config.nostr.relays.is_empty() {
166 None
167 } else {
168 Some(Arc::new(
169 nostr_pubsub_relay::RelayEventBus::new(
170 config.nostr.relays.clone(),
171 Duration::from_secs(5),
172 )
173 .await
174 .context("Retained root relay provider")?,
175 ))
176 };
177 let (sender, receiver) = tokio::sync::mpsc::channel(128);
178 let mut tasks = vec![tokio::spawn(consume_intake(
179 cache,
180 client.clone(),
181 receiver,
182 ))];
183 if let Some(relay) = relay {
184 tasks.push(reconcile(
185 relay.clone(),
186 filters.clone(),
187 sender.clone(),
188 true,
189 ));
190 let filters = filters.clone();
191 let sender = sender.clone();
192 tasks.push(tokio::spawn(async move {
193 loop {
194 let sender = sender.clone();
195 let subscription = relay
196 .subscribe_with_admission(
197 filters.clone(),
198 Arc::new(move |event| {
199 if !is_public_head(&event.event) {
200 return true;
201 }
202 !matches!(
203 sender.try_send((event, true)),
204 Err(tokio::sync::mpsc::error::TrySendError::Full(_))
205 )
206 }),
207 )
208 .await;
209 if let Ok(subscription) = subscription {
210 std::future::pending::<()>().await;
213 let _ = subscription.close().await;
214 }
215 tokio::time::sleep(Duration::from_secs(5)).await;
216 }
217 }));
218 }
219 if let Some(client) = client {
220 tasks.push(reconcile_fips(
221 client.clone(),
222 filters.clone(),
223 sender.clone(),
224 ));
225 for chunk in filters.chunks(client.options().max_filters_per_subscription) {
226 let filters = chunk.to_vec();
227 let client = client.clone();
228 let sender = sender.clone();
229 tasks.push(tokio::spawn(async move {
230 loop {
231 if let Ok(mut subscription) = client.subscribe(filters.clone()).await {
232 while let Some(event) = subscription.recv().await {
233 if sender.send((event, false)).await.is_err() {
234 return;
235 }
236 }
237 }
238 tokio::time::sleep(Duration::from_secs(5)).await;
239 }
240 }));
241 }
242 }
243 Ok(tasks)
244}
245
246fn reconcile_fips(
247 client: Arc<FipsPubsubClient>,
248 filters: Vec<Filter>,
249 sender: tokio::sync::mpsc::Sender<IntakeEvent>,
250) -> JoinHandle<()> {
251 tokio::spawn(async move {
252 let mut tick = tokio::time::interval(Duration::from_secs(60) / filters.len() as u32);
253 tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
254 for filter in filters.into_iter().cycle() {
255 tick.tick().await;
256 if let Ok(Some(event)) = query_fresh_head(&client, filter).await {
257 if sender.send((event, false)).await.is_err() {
258 return;
259 }
260 }
261 }
262 })
263}
264
265async fn query_fresh_head(
266 client: &Arc<FipsPubsubClient>,
267 filter: Filter,
268) -> nostr_pubsub::Result<Option<nostr_pubsub::QueryEvent>> {
269 use nostr_pubsub::NostrEventSubscriber;
270 let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
271 let subscription = client
272 .fresh_subscriber()
273 .subscribe(
274 vec![filter],
275 Arc::new(move |event| {
276 let _ = sender.try_send(event);
277 }),
278 )
279 .await?;
280 let event = tokio::time::timeout(client.options().query_timeout, receiver.recv())
281 .await
282 .ok()
283 .flatten();
284 subscription.close().await?;
285 Ok(event)
286}
287
288fn reconcile(
289 source: Arc<dyn EventBus>,
290 filters: Vec<Filter>,
291 sender: tokio::sync::mpsc::Sender<IntakeEvent>,
292 forward: bool,
293) -> JoinHandle<()> {
294 tokio::spawn(async move {
295 let mut tick = tokio::time::interval(Duration::from_secs(60) / filters.len() as u32);
298 tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
299 for filter in filters.into_iter().cycle() {
300 tick.tick().await;
301 if let Ok(report) = source
303 .query(vec![filter], QueryOptions { limit: Some(1) })
304 .await
305 {
306 for event in report.events {
307 if sender.send((event, forward)).await.is_err() {
308 return;
309 }
310 }
311 }
312 }
313 })
314}
315
316async fn consume_intake(
317 cache: Arc<DaemonNostrCache>,
318 client: Option<Arc<FipsPubsubClient>>,
319 mut receiver: tokio::sync::mpsc::Receiver<IntakeEvent>,
320) {
321 while let Some((event, forward)) = receiver.recv().await {
322 forward_retained(
323 &cache,
324 if forward { client.as_deref() } else { None },
325 event,
326 )
327 .await;
328 }
329}
330
331async fn close_intake(tasks: Vec<JoinHandle<()>>, cache: &DaemonNostrCache) {
332 for task in &tasks {
333 task.abort();
334 }
335 for task in tasks {
336 let _ = task.await;
337 }
338 let _ = cache.root_cid().await;
341}
342
343async fn forward_retained(
344 cache: &DaemonNostrCache,
345 client: Option<&FipsPubsubClient>,
346 event: nostr_pubsub::QueryEvent,
347) {
348 if persist(cache, &event).await {
349 if let Some(client) = client {
350 let _ = client.publish(event.event, event.source).await;
353 }
354 }
355}
356
357async fn persist(cache: &DaemonNostrCache, event: &nostr_pubsub::QueryEvent) -> bool {
358 if !is_public_head(&event.event) {
359 return false;
360 }
361 if event.event.as_event().as_json().len() > nostr_pubsub_fips::FIPS_NOSTR_PUBSUB_MAX_FRAME_BYTES
362 {
363 return false;
364 }
365 match cache
366 .publish(event.event.clone(), event.source.clone())
367 .await
368 {
369 Ok(report) => report.accepted,
370 Err(error) => {
371 tracing::warn!(%error, "Retained signed head could not be committed");
372 false
373 }
374 }
375}
376
377#[cfg(test)]
378#[path = "../../tests/common/mod.rs"]
379mod test_common;
380#[cfg(test)]
381mod tests;