1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::path::PathBuf;
3use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
4use std::sync::Arc;
5use std::sync::Mutex;
6use std::time::Duration;
7use std::time::Instant;
8
9use anyhow::{Context, Result};
10use hashtree_core::{nhash_encode_full, NHashData};
11use hashtree_nostr::{
12 CrawlConfig, CrawlReport, ListEventsOptions, NostrBridge, NostrEventStore, RelayFetchMode,
13 HASHTREE_ROOT_KIND,
14};
15use nostr::{
16 Alphabet, Event, EventBuilder, Filter, Kind, PublicKey, SingleLetterTag, Tag, TagKind,
17 Timestamp,
18};
19use nostr_sdk::{
20 pool::RelayLimits, prelude::RelayPoolNotification, Client, ClientOptions, Keys, RelayStatus,
21};
22use tokio::sync::{watch, Mutex as AsyncMutex};
23use tracing::{debug, info, warn};
24
25use crate::blossom_push::background_blossom_push_incremental_with_store;
26use crate::socialgraph::crawler::SOCIALGRAPH_RELAY_EVENT_MAX_SIZE;
27use crate::socialgraph::{self, SocialGraphBackend, SocialGraphStore};
28use crate::HashtreeStore;
29
30#[cfg(not(test))]
31const MIRROR_STARTUP_DELAY: Duration = Duration::from_secs(8);
32#[cfg(test)]
33const MIRROR_STARTUP_DELAY: Duration = Duration::from_millis(50);
34
35#[cfg(not(test))]
36const MIRROR_CONNECT_SETTLE_DELAY: Duration = Duration::from_secs(1);
37#[cfg(test)]
38const MIRROR_CONNECT_SETTLE_DELAY: Duration = Duration::from_millis(250);
39
40#[cfg(not(test))]
41const MIRROR_AUTHOR_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
42#[cfg(test)]
43const MIRROR_AUTHOR_REFRESH_INTERVAL: Duration = Duration::from_millis(100);
44
45#[cfg(not(test))]
46const MIRROR_RECONNECT_HISTORY_SYNC_COOLDOWN: Duration = Duration::from_secs(30);
47#[cfg(test)]
48const MIRROR_RECONNECT_HISTORY_SYNC_COOLDOWN: Duration = Duration::from_millis(100);
49
50const KIND_LONG_FORM_CONTENT: u16 = 30_023;
51const KIND_PICTURE_FIRST: u16 = 20;
52const KIND_COMMENT: u16 = 1_111;
53const FULL_ARCHIVE_HISTORY_KINDS: [u16; 9] = [
54 1,
55 5,
56 6,
57 7,
58 16,
59 KIND_PICTURE_FIRST,
60 KIND_COMMENT,
61 9_735,
62 KIND_LONG_FORM_CONTENT,
63];
64const LEGACY_TEXT_HISTORY_KINDS: [u16; 2] = [1, KIND_LONG_FORM_CONTENT];
65const DEFAULT_HISTORY_KINDS: [u16; 13] = [
66 0,
67 1,
68 3,
69 5,
70 6,
71 7,
72 16,
73 KIND_PICTURE_FIRST,
74 KIND_COMMENT,
75 9_735,
76 10_000,
77 30_000,
78 KIND_LONG_FORM_CONTENT,
79];
80const DEFAULT_EVENT_TREE_NAME: &str = "nostr-event-index";
81const DEFAULT_PROFILE_SEARCH_TREE_NAME: &str = "profile-search";
82const DEFAULT_PROFILES_BY_PUBKEY_TREE_NAME: &str = "profiles-by-pubkey";
83const MIRROR_UPLOAD_STATE_DIR: &str = "nostr-mirror";
84const MIRROR_UPLOADED_ROOT_SUFFIX: &str = ".uploaded-root";
85const NOSTR_INDEX_STATE_DIR: &str = "nostr-index";
86const NOSTR_INDEX_LATEST_ROOT_FILE: &str = "latest-root.txt";
87const METADATA_HISTORY_SYNC_PER_AUTHOR_EVENT_LIMIT: usize = 1;
88const METADATA_HISTORY_SYNC_AUTHOR_BATCH_SIZE: usize = 64;
89const DEFAULT_FULL_TEXT_NOTE_HISTORY_FOLLOW_DISTANCE: u32 = 2;
90const DEFAULT_FULL_TEXT_NOTE_HISTORY_MAX_RELAY_PAGES: usize = 0;
91const DEFAULT_ARCHIVE_HISTORY_FOLLOW_DISTANCE: u32 = 2;
92const DEFAULT_ARCHIVE_HISTORY_MAX_RELAY_PAGES: usize = 0;
93const ARCHIVE_HISTORY_PRIORITY_MAX_DISTANCE: u32 = 1;
94const ARCHIVE_HISTORY_PRIORITY_SAMPLE_LIMIT: usize = 32;
95const FULL_ARCHIVE_FETCH_TIMEOUT_MAX_MULTIPLIER: usize = 4;
96
97#[cfg(not(test))]
98const MIRROR_MISSING_PROFILE_BACKFILL_INTERVAL: Duration = Duration::from_secs(300);
99#[cfg(test)]
100const MIRROR_MISSING_PROFILE_BACKFILL_INTERVAL: Duration = Duration::from_millis(100);
101
102#[cfg(not(test))]
103const MIRROR_ROOT_PUBLISH_DEBOUNCE: Duration = Duration::from_secs(5);
104#[cfg(test)]
105const MIRROR_ROOT_PUBLISH_DEBOUNCE: Duration = Duration::from_millis(20);
106
107#[cfg(not(test))]
108const MIRROR_ROOT_PUBLISH_MAX_STALENESS: Duration = Duration::from_secs(30);
109#[cfg(test)]
110const MIRROR_ROOT_PUBLISH_MAX_STALENESS: Duration = Duration::from_millis(100);
111
112#[cfg(not(test))]
113const MIRROR_ROOT_UPLOAD_RETRY_INTERVAL: Duration = Duration::from_secs(60);
114#[cfg(test)]
115const MIRROR_ROOT_UPLOAD_RETRY_INTERVAL: Duration = Duration::from_millis(100);
116
117#[cfg(not(test))]
118const MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT: Duration = Duration::from_secs(2);
119#[cfg(test)]
120const MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT: Duration = Duration::from_millis(250);
121
122#[cfg(not(test))]
123const MIRROR_ROOT_PUBLISH_RETRY_TIMEOUT: Duration = Duration::from_secs(5);
124#[cfg(test)]
125const MIRROR_ROOT_PUBLISH_RETRY_TIMEOUT: Duration = Duration::from_secs(2);
126
127const MISSING_LOCAL_BLOB_PUSH_ERROR: &str = "missing local blob";
128
129fn trim_transient_allocations() {
130 #[cfg(all(target_os = "linux", target_env = "gnu"))]
131 unsafe {
132 libc::malloc_trim(0);
136 }
137}
138
139struct RootPublicationDeferral<'a> {
140 count: &'a AtomicUsize,
141}
142
143impl<'a> RootPublicationDeferral<'a> {
144 fn new(count: &'a AtomicUsize) -> Self {
145 count.fetch_add(1, Ordering::AcqRel);
146 Self { count }
147 }
148}
149
150impl Drop for RootPublicationDeferral<'_> {
151 fn drop(&mut self) {
152 self.count.fetch_sub(1, Ordering::AcqRel);
153 }
154}
155
156fn decode_hex_pubkey(value: &str) -> Option<[u8; 32]> {
157 let bytes = hex::decode(value).ok()?;
158 <[u8; 32]>::try_from(bytes.as_slice()).ok()
159}
160
161#[derive(Debug, Clone)]
162pub struct NostrMirrorConfig {
163 pub relays: Vec<String>,
164 pub publish_relays: Vec<String>,
165 pub blossom_write_servers: Vec<String>,
166 pub max_follow_distance: u32,
167 pub overmute_threshold: f64,
168 pub author_batch_size: usize,
169 pub history_sync_author_chunk_size: usize,
170 pub history_sync_per_author_event_limit: usize,
171 pub missing_profile_backfill_batch_size: usize,
172 pub fetch_timeout: Duration,
173 pub relay_event_max_size: Option<u32>,
174 pub require_negentropy: bool,
175 pub kinds: Vec<u16>,
176 pub history_sync_on_start: bool,
177 pub history_sync_on_reconnect: bool,
178 pub full_text_note_history_follow_distance: Option<u32>,
179 pub full_text_note_history_max_relay_pages: usize,
180 pub archive_history_follow_distance: Option<u32>,
181 pub archive_history_max_relay_pages: usize,
182 pub published_event_tree_name: Option<String>,
183 pub published_profile_search_tree_name: Option<String>,
184 pub published_profiles_by_pubkey_tree_name: Option<String>,
185}
186
187impl Default for NostrMirrorConfig {
188 fn default() -> Self {
189 Self {
190 relays: Vec::new(),
191 publish_relays: Vec::new(),
192 blossom_write_servers: Vec::new(),
193 max_follow_distance: 2,
194 overmute_threshold: 1.0,
195 author_batch_size: 256,
196 history_sync_author_chunk_size: 5_000,
197 history_sync_per_author_event_limit: 256,
198 missing_profile_backfill_batch_size: 5_000,
199 fetch_timeout: Duration::from_secs(15),
200 relay_event_max_size: Some(SOCIALGRAPH_RELAY_EVENT_MAX_SIZE),
201 require_negentropy: false,
202 kinds: DEFAULT_HISTORY_KINDS.to_vec(),
203 history_sync_on_start: true,
204 history_sync_on_reconnect: true,
205 full_text_note_history_follow_distance: Some(
206 DEFAULT_FULL_TEXT_NOTE_HISTORY_FOLLOW_DISTANCE,
207 ),
208 full_text_note_history_max_relay_pages: DEFAULT_FULL_TEXT_NOTE_HISTORY_MAX_RELAY_PAGES,
209 archive_history_follow_distance: Some(DEFAULT_ARCHIVE_HISTORY_FOLLOW_DISTANCE),
210 archive_history_max_relay_pages: DEFAULT_ARCHIVE_HISTORY_MAX_RELAY_PAGES,
211 published_event_tree_name: Some(DEFAULT_EVENT_TREE_NAME.to_string()),
212 published_profile_search_tree_name: Some(DEFAULT_PROFILE_SEARCH_TREE_NAME.to_string()),
213 published_profiles_by_pubkey_tree_name: Some(
214 DEFAULT_PROFILES_BY_PUBKEY_TREE_NAME.to_string(),
215 ),
216 }
217 }
218}
219
220#[derive(Debug, Default)]
221struct RootPublishState {
222 pending_root: Option<hashtree_core::Cid>,
223 last_changed_at: Option<Instant>,
224 dirty_since: Option<Instant>,
225 last_published_root: Option<hashtree_core::Cid>,
226 last_published_at: Option<Instant>,
227 last_published_created_at: Option<Timestamp>,
228 last_uploaded_root: Option<hashtree_core::Cid>,
229 last_uploaded_at: Option<Instant>,
230 upload_in_progress_root: Option<hashtree_core::Cid>,
231 last_upload_failed_at: Option<Instant>,
232 last_upload_error: Option<String>,
233 missing_blob_rebuild_required: bool,
234}
235
236struct RootUploadTask {
237 cancel: watch::Sender<bool>,
238 join: tokio::task::JoinHandle<()>,
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242struct HistorySyncPlan {
243 relay_fetch_mode: RelayFetchMode,
244 author_batch_size: usize,
245 per_author_event_limit: usize,
246 relay_page_size: usize,
247 max_relay_pages: usize,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq)]
251struct ArchiveHistorySettings {
252 follow_distance: u32,
253 max_relay_pages: usize,
254 kinds: Vec<u16>,
255}
256
257pub struct BackgroundNostrMirror {
258 config: NostrMirrorConfig,
259 store: Arc<HashtreeStore>,
260 graph_store: Arc<SocialGraphStore>,
261 client: Client,
262 publish_client: Option<Client>,
263 event_publish_state: Arc<Mutex<RootPublishState>>,
264 profile_search_publish_state: Arc<Mutex<RootPublishState>>,
265 profiles_by_pubkey_publish_state: Arc<Mutex<RootPublishState>>,
266 pending_live_events: Mutex<BTreeMap<String, Event>>,
267 missing_profile_cursor: Mutex<usize>,
268 history_sync_lock: AsyncMutex<()>,
269 root_publication_deferrals: AtomicUsize,
270 root_upload_tasks: Mutex<Vec<RootUploadTask>>,
271 shutting_down: AtomicBool,
272 shutdown_tx: watch::Sender<bool>,
273 shutdown_rx: watch::Receiver<bool>,
274}
275
276impl BackgroundNostrMirror {
277 pub async fn new(
278 config: NostrMirrorConfig,
279 store: Arc<HashtreeStore>,
280 graph_store: Arc<SocialGraphStore>,
281 publish_keys: Option<Keys>,
282 ) -> Result<Self> {
283 let client = if let Some(max_size) = config.relay_event_max_size {
284 let mut limits = RelayLimits::default();
285 limits.events.max_size = Some(max_size);
286 Client::builder()
287 .signer(Keys::generate())
288 .opts(ClientOptions::new().relay_limits(limits))
289 .build()
290 } else {
291 Client::new(Keys::generate())
292 };
293 for relay in &config.relays {
294 client
295 .add_relay(relay)
296 .await
297 .with_context(|| format!("add mirror relay {relay}"))?;
298 }
299 client.connect().await;
300
301 let publish_client = if let Some(keys) = publish_keys {
302 if config.publish_relays.is_empty() {
303 None
304 } else {
305 let client = Client::new(keys);
306 for relay in &config.publish_relays {
307 client
308 .add_relay(relay)
309 .await
310 .with_context(|| format!("add mirror publish relay {relay}"))?;
311 }
312 client.connect().await;
313 Some(client)
314 }
315 } else {
316 None
317 };
318
319 let (shutdown_tx, shutdown_rx) = watch::channel(false);
320 let event_publish_state = Self::root_publish_state_from_disk(
321 store.base_path(),
322 config.published_event_tree_name.as_deref(),
323 "event root",
324 );
325 let profile_search_publish_state = Self::root_publish_state_from_disk(
326 store.base_path(),
327 config.published_profile_search_tree_name.as_deref(),
328 "profile-search root",
329 );
330 let profiles_by_pubkey_publish_state = Self::root_publish_state_from_disk(
331 store.base_path(),
332 config.published_profiles_by_pubkey_tree_name.as_deref(),
333 "profiles-by-pubkey root",
334 );
335 Ok(Self {
336 config,
337 store,
338 graph_store,
339 client,
340 publish_client,
341 event_publish_state: Arc::new(Mutex::new(event_publish_state)),
342 profile_search_publish_state: Arc::new(Mutex::new(profile_search_publish_state)),
343 profiles_by_pubkey_publish_state: Arc::new(Mutex::new(
344 profiles_by_pubkey_publish_state,
345 )),
346 pending_live_events: Mutex::new(BTreeMap::new()),
347 missing_profile_cursor: Mutex::new(0),
348 history_sync_lock: AsyncMutex::new(()),
349 root_publication_deferrals: AtomicUsize::new(0),
350 root_upload_tasks: Mutex::new(Vec::new()),
351 shutting_down: AtomicBool::new(false),
352 shutdown_tx,
353 shutdown_rx,
354 })
355 }
356
357 fn root_publish_state_from_disk(
358 base_path: &std::path::Path,
359 tree_name: Option<&str>,
360 log_label: &str,
361 ) -> RootPublishState {
362 let Some(tree_name) = tree_name else {
363 return RootPublishState::default();
364 };
365 let path = Self::uploaded_root_state_path(base_path, tree_name);
366 let Some(root) = Self::read_uploaded_root_state(&path, log_label) else {
367 return RootPublishState::default();
368 };
369
370 RootPublishState {
371 last_uploaded_root: Some(root),
372 ..RootPublishState::default()
373 }
374 }
375
376 fn uploaded_root_state_path(base_path: &std::path::Path, tree_name: &str) -> PathBuf {
377 let safe_tree_name = tree_name
378 .chars()
379 .map(|ch| {
380 if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
381 ch
382 } else {
383 '_'
384 }
385 })
386 .collect::<String>();
387 base_path
388 .join(MIRROR_UPLOAD_STATE_DIR)
389 .join(format!("{safe_tree_name}{MIRROR_UPLOADED_ROOT_SUFFIX}"))
390 }
391
392 fn read_uploaded_root_state(
393 path: &std::path::Path,
394 log_label: &str,
395 ) -> Option<hashtree_core::Cid> {
396 let raw = match std::fs::read_to_string(path) {
397 Ok(raw) => raw,
398 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None,
399 Err(err) => {
400 warn!(
401 "Nostr mirror failed to read uploaded {} state {}: {}",
402 log_label,
403 path.display(),
404 err
405 );
406 return None;
407 }
408 };
409 let trimmed = raw.trim();
410 if trimmed.is_empty() {
411 return None;
412 }
413 match hashtree_core::Cid::parse(trimmed) {
414 Ok(root) => Some(root),
415 Err(err) => {
416 warn!(
417 "Nostr mirror ignored invalid uploaded {} state {}: {}",
418 log_label,
419 path.display(),
420 err
421 );
422 None
423 }
424 }
425 }
426
427 fn write_uploaded_root_state(
428 path: &std::path::Path,
429 root: &hashtree_core::Cid,
430 log_label: &str,
431 ) -> Result<()> {
432 if let Some(parent) = path.parent() {
433 std::fs::create_dir_all(parent)
434 .with_context(|| format!("create {}", parent.display()))?;
435 }
436 std::fs::write(path, format!("{root}\n"))
437 .with_context(|| format!("write uploaded {log_label} state {}", path.display()))
438 }
439
440 fn write_latest_event_root_state(
441 base_path: &std::path::Path,
442 root: Option<&hashtree_core::Cid>,
443 ) -> Result<()> {
444 let path = base_path
445 .join(NOSTR_INDEX_STATE_DIR)
446 .join(NOSTR_INDEX_LATEST_ROOT_FILE);
447 let Some(root) = root else {
448 if path.exists() {
449 std::fs::remove_file(&path)
450 .with_context(|| format!("remove {}", path.display()))?;
451 }
452 return Ok(());
453 };
454 if let Some(parent) = path.parent() {
455 std::fs::create_dir_all(parent)
456 .with_context(|| format!("create {}", parent.display()))?;
457 }
458 let encoded = nhash_encode_full(&NHashData {
459 hash: root.hash,
460 decrypt_key: root.key,
461 })
462 .context("encode mirrored Nostr index root")?;
463 let tmp_path = path.with_extension("tmp");
464 std::fs::write(&tmp_path, format!("{encoded}\n"))
465 .with_context(|| format!("write {}", tmp_path.display()))?;
466 #[cfg(windows)]
467 if path.exists() {
468 std::fs::remove_file(&path)
469 .with_context(|| format!("remove stale {}", path.display()))?;
470 }
471 std::fs::rename(&tmp_path, &path).with_context(|| format!("replace {}", path.display()))?;
472 Ok(())
473 }
474
475 pub fn shutdown(&self) {
476 self.shutting_down.store(true, Ordering::Release);
477 let tasks = self.root_upload_tasks.lock().expect("root upload tasks");
478 for task in tasks.iter() {
479 let _ = task.cancel.send(true);
480 }
481 let _ = self.shutdown_tx.send(true);
482 }
483
484 async fn finish_root_upload_tasks(&self) {
485 let tasks = {
486 let mut tasks = self.root_upload_tasks.lock().expect("root upload tasks");
487 tasks.drain(..).collect::<Vec<_>>()
488 };
489 for mut task in tasks {
490 let _ = task.cancel.send(true);
491 if tokio::time::timeout(Duration::from_secs(3), &mut task.join)
492 .await
493 .is_err()
494 {
495 warn!("Timed out waiting for Nostr mirror root upload shutdown");
496 task.join.abort();
497 }
498 }
499 }
500
501 fn sync_publish_roots_from_store(&self) -> Result<()> {
502 self.note_public_events_root_change()?;
503 self.note_profile_search_root_change()?;
504 self.note_profiles_by_pubkey_root_change()?;
505 Ok(())
506 }
507
508 async fn publish_pending_roots(
509 &self,
510 force_event: bool,
511 force_profile_search: bool,
512 force_profiles_by_pubkey: bool,
513 ) -> (Result<()>, Result<()>, Result<()>) {
514 tokio::join!(
515 self.maybe_publish_event_root(force_event),
516 self.maybe_publish_profile_search_root(force_profile_search),
517 self.maybe_publish_profiles_by_pubkey_root(force_profiles_by_pubkey),
518 )
519 }
520
521 async fn publish_priority_roots(
522 &self,
523 force_event: bool,
524 force_profile_search: bool,
525 force_profiles_by_pubkey: bool,
526 ) -> (Result<()>, Result<()>, Result<()>) {
527 let (profile_search_result, profiles_by_pubkey_result) = tokio::join!(
528 async {
529 if force_profile_search {
530 self.maybe_publish_profile_search_root(true).await
531 } else {
532 Ok(())
533 }
534 },
535 async {
536 if force_profiles_by_pubkey {
537 self.maybe_publish_profiles_by_pubkey_root(true).await
538 } else {
539 Ok(())
540 }
541 },
542 );
543 let event_result = if force_event {
544 self.maybe_publish_event_root(true).await
545 } else {
546 Ok(())
547 };
548 (
549 event_result,
550 profile_search_result,
551 profiles_by_pubkey_result,
552 )
553 }
554
555 pub async fn run(self: Arc<Self>) -> Result<()> {
556 if self.config.relays.is_empty() || self.config.max_follow_distance == 0 {
557 return Ok(());
558 }
559
560 info!(
561 "Nostr mirror starting: relays={} max_follow_distance={} negentropy_only={} kinds={:?} history_sync_author_chunk_size={} history_sync_on_start={} history_sync_on_reconnect={}",
562 self.config.relays.len(),
563 self.config.max_follow_distance,
564 self.config.require_negentropy,
565 self.config.kinds,
566 self.config.history_sync_author_chunk_size.max(1),
567 self.config.history_sync_on_start,
568 self.config.history_sync_on_reconnect
569 );
570
571 tokio::time::sleep(MIRROR_STARTUP_DELAY).await;
572 tokio::time::sleep(MIRROR_CONNECT_SETTLE_DELAY).await;
573 let live_since = Timestamp::now();
574 self.sync_publish_roots_from_store()?;
575 let (event_result, profile_search_result, profiles_by_pubkey_result) =
576 self.publish_priority_roots(true, true, true).await;
577 if let Err(err) = event_result {
578 warn!(
579 "Nostr mirror event-root publish failed on startup: {:#}",
580 err
581 );
582 }
583 if let Err(err) = profile_search_result {
584 warn!(
585 "Nostr mirror profile-search publish failed on startup: {:#}",
586 err
587 );
588 }
589 if let Err(err) = profiles_by_pubkey_result {
590 warn!(
591 "Nostr mirror profiles-by-pubkey publish failed on startup: {:#}",
592 err
593 );
594 }
595
596 let initial_authors = self.collect_authors()?;
597 if initial_authors.is_empty() {
598 info!("Nostr mirror: no social-graph authors to mirror yet");
599 }
600
601 let mut subscribed_authors = HashSet::new();
602 self.subscribe_authors_since(&initial_authors, live_since, &mut subscribed_authors)
603 .await?;
604
605 if !initial_authors.is_empty() && self.config.history_sync_on_start {
606 self.spawn_startup_history_sync(initial_authors.clone());
607 }
608
609 let mut relay_statuses = self.capture_relay_statuses().await;
610 let mut last_reconnect_history_sync_at: Option<Instant> = None;
611 let mut last_missing_profile_backfill_at: Option<Instant> = None;
612 let mut notifications = self.client.notifications();
613 let mut shutdown_rx = self.shutdown_rx.clone();
614 let mut refresh_interval = tokio::time::interval(MIRROR_AUTHOR_REFRESH_INTERVAL);
615 let mut publish_interval = tokio::time::interval(MIRROR_ROOT_PUBLISH_DEBOUNCE);
616
617 loop {
618 tokio::select! {
619 _ = shutdown_rx.changed() => {
620 if *shutdown_rx.borrow() {
621 break;
622 }
623 }
624 _ = refresh_interval.tick() => {
625 let authors = self.collect_authors()?;
626 let mut reconnected_relay = None;
627 for (relay_url, status) in self.capture_relay_statuses().await {
628 let previous = relay_statuses.insert(relay_url.clone(), status);
629 if reconnected_relay.is_none()
630 && Self::should_history_sync_on_reconnect(
631 self.config.history_sync_on_reconnect,
632 previous,
633 status,
634 )
635 {
636 reconnected_relay = Some(relay_url);
637 }
638 }
639 if let Some(relay_url) = reconnected_relay {
640 if Self::should_run_reconnect_history_sync(
641 last_reconnect_history_sync_at.as_ref(),
642 ) && !authors.is_empty()
643 {
644 info!(
645 "Nostr mirror relay reconnected; running catch-up history sync: relay={} authors={} negentropy_only={}",
646 relay_url,
647 authors.len(),
648 self.config.require_negentropy
649 );
650 self.spawn_author_history_sync(
651 "relay reconnect catch-up",
652 authors.clone(),
653 false,
654 false,
655 );
656 last_reconnect_history_sync_at = Some(Instant::now());
657 }
658 }
659 let new_authors = authors
660 .iter()
661 .filter(|author| !subscribed_authors.contains(*author))
662 .cloned()
663 .collect::<Vec<_>>();
664 if !new_authors.is_empty() {
665 debug!(
666 "Nostr mirror discovered {} newly reachable author(s)",
667 new_authors.len()
668 );
669 self.subscribe_authors_since(
670 &new_authors,
671 Timestamp::now(),
672 &mut subscribed_authors,
673 )
674 .await?;
675 self.spawn_author_history_sync(
676 "new-author catch-up",
677 new_authors.clone(),
678 true,
679 true,
680 );
681 }
682 if self.should_backfill_missing_profiles(last_missing_profile_backfill_at) {
683 let missing_profile_authors = self.collect_missing_profile_authors(
684 self.config.missing_profile_backfill_batch_size,
685 )?;
686 if !missing_profile_authors.is_empty() {
687 info!(
688 "Nostr mirror missing-profile backfill starting: authors={}",
689 missing_profile_authors.len()
690 );
691 self.spawn_missing_profile_backfill(missing_profile_authors);
692 last_missing_profile_backfill_at = Some(Instant::now());
693 }
694 }
695 }
696 _ = publish_interval.tick() => {
697 self.sync_publish_roots_from_store()?;
698 if let Err(err) = self.flush_live_events().await {
699 warn!("Nostr mirror live event flush failed: {:#}", err);
700 }
701 let (event_result, profile_search_result, profiles_by_pubkey_result) = self
702 .publish_pending_roots(false, false, false)
703 .await;
704 if let Err(err) = event_result {
705 warn!("Nostr mirror event-root publish failed: {:#}", err);
706 }
707 if let Err(err) = profile_search_result {
708 warn!("Nostr mirror profile-search publish failed: {:#}", err);
709 }
710 if let Err(err) = profiles_by_pubkey_result {
711 warn!("Nostr mirror profiles-by-pubkey publish failed: {:#}", err);
712 }
713 }
714 notification = notifications.recv() => {
715 match notification {
716 Ok(RelayPoolNotification::Event { event, .. }) => {
717 self.ingest_live_event(&event)?;
718 }
719 Ok(RelayPoolNotification::Shutdown) => break,
720 Ok(_) => {}
721 Err(err) => {
722 warn!("Nostr mirror notification error: {}", err);
723 break;
724 }
725 }
726 }
727 }
728 }
729
730 if let Err(err) = self.flush_live_events().await {
731 warn!(
732 "Nostr mirror live event flush failed during shutdown: {:#}",
733 err
734 );
735 }
736 if let Err(err) = self.sync_publish_roots_from_store() {
737 warn!(
738 "Nostr mirror root-state refresh failed during shutdown: {:#}",
739 err
740 );
741 }
742 let (event_result, profile_search_result, profiles_by_pubkey_result) =
743 self.publish_pending_roots(true, true, true).await;
744 if let Err(err) = event_result {
745 warn!(
746 "Nostr mirror event-root publish failed during shutdown: {:#}",
747 err
748 );
749 }
750 if let Err(err) = profile_search_result {
751 warn!(
752 "Nostr mirror profile-search publish failed during shutdown: {:#}",
753 err
754 );
755 }
756 if let Err(err) = profiles_by_pubkey_result {
757 warn!(
758 "Nostr mirror profiles-by-pubkey publish failed during shutdown: {:#}",
759 err
760 );
761 }
762 self.finish_root_upload_tasks().await;
763 let _ = self.client.disconnect().await;
764 if let Some(client) = self.publish_client.as_ref() {
765 let _ = client.disconnect().await;
766 }
767 Ok(())
768 }
769
770 fn spawn_startup_history_sync(self: &Arc<Self>, initial_authors: Vec<String>) {
771 let mirror = Arc::clone(self);
772 tokio::task::spawn_blocking(move || {
773 let runtime = tokio::runtime::Builder::new_current_thread()
774 .enable_all()
775 .build()
776 .expect("build nostr mirror startup history sync runtime");
777 runtime.block_on(async move {
778 let _guard = mirror.history_sync_lock.lock().await;
779 if let Err(err) = mirror.run_startup_history_sync(initial_authors).await {
780 warn!("Nostr mirror startup history sync failed: {:#}", err);
781 }
782 });
783 });
784 }
785
786 async fn run_startup_history_sync(&self, initial_authors: Vec<String>) -> Result<()> {
787 self.history_sync_authors(initial_authors).await?;
788 self.history_sync_archive_for_reachable_authors().await?;
789 if self.should_backfill_missing_profiles(None) {
790 let missing_profile_authors = self
791 .collect_missing_profile_authors(self.config.missing_profile_backfill_batch_size)?;
792 if !missing_profile_authors.is_empty() {
793 info!(
794 "Nostr mirror missing-profile backfill starting: authors={}",
795 missing_profile_authors.len()
796 );
797 self.history_sync_authors_with_kinds(
798 missing_profile_authors,
799 &[Kind::Metadata.as_u16()],
800 )
801 .await?;
802 }
803 }
804 Ok(())
805 }
806
807 fn spawn_author_history_sync(
808 self: &Arc<Self>,
809 label: &'static str,
810 authors: Vec<String>,
811 include_archive_history: bool,
812 wait_for_existing_sync: bool,
813 ) {
814 let mirror = Arc::clone(self);
815 tokio::task::spawn_blocking(move || {
816 let runtime = tokio::runtime::Builder::new_current_thread()
817 .enable_all()
818 .build()
819 .expect("build nostr mirror author history sync runtime");
820 runtime.block_on(async move {
821 if wait_for_existing_sync {
822 let _guard = mirror.history_sync_lock.lock().await;
823 if let Err(err) = mirror
824 .run_author_history_sync(authors, include_archive_history)
825 .await
826 {
827 warn!("Nostr mirror {label} failed: {:#}", err);
828 }
829 return;
830 }
831
832 let Ok(_guard) = mirror.history_sync_lock.try_lock() else {
833 info!("Nostr mirror {label} skipped; another history sync is running");
834 return;
835 };
836 if let Err(err) = mirror
837 .run_author_history_sync(authors, include_archive_history)
838 .await
839 {
840 warn!("Nostr mirror {label} failed: {:#}", err);
841 }
842 });
843 });
844 }
845
846 async fn run_author_history_sync(
847 &self,
848 authors: Vec<String>,
849 include_archive_history: bool,
850 ) -> Result<()> {
851 self.history_sync_authors(authors.clone()).await?;
852 if include_archive_history {
853 self.history_sync_archive_for_authors(authors).await?;
854 }
855 Ok(())
856 }
857
858 fn spawn_missing_profile_backfill(self: &Arc<Self>, authors: Vec<String>) {
859 let mirror = Arc::clone(self);
860 tokio::task::spawn_blocking(move || {
861 let runtime = tokio::runtime::Builder::new_current_thread()
862 .enable_all()
863 .build()
864 .expect("build nostr mirror missing profile runtime");
865 runtime.block_on(async move {
866 let Ok(_guard) = mirror.history_sync_lock.try_lock() else {
867 info!(
868 "Nostr mirror missing-profile backfill skipped; another history sync is running"
869 );
870 return;
871 };
872 if let Err(err) = mirror
873 .history_sync_authors_with_kinds(authors, &[Kind::Metadata.as_u16()])
874 .await
875 {
876 warn!("Nostr mirror missing-profile backfill failed: {:#}", err);
877 }
878 });
879 });
880 }
881
882 async fn capture_relay_statuses(&self) -> HashMap<String, RelayStatus> {
883 let mut statuses = HashMap::new();
884 for (relay_url, relay) in self.client.relays().await {
885 statuses.insert(relay_url.to_string(), relay.status());
886 }
887 statuses
888 }
889
890 async fn has_connected_publish_relay(&self) -> bool {
891 let Some(client) = self.publish_client.as_ref() else {
892 return false;
893 };
894 Self::client_has_connected_relay(client).await
895 }
896
897 async fn client_has_connected_relay(client: &Client) -> bool {
898 for (_relay_url, relay) in client.relays().await {
899 if relay.status() == RelayStatus::Connected {
900 return true;
901 }
902 }
903 false
904 }
905
906 fn collect_authors(&self) -> Result<Vec<String>> {
907 self.collect_authors_with_max_distance(self.config.max_follow_distance)
908 }
909
910 fn collect_authors_with_max_distance(&self, max_distance: u32) -> Result<Vec<String>> {
911 let mut authors = Vec::new();
912 let mut seen = HashSet::new();
913 for distance in 0..=max_distance {
914 for pubkey in socialgraph::SocialGraphBackend::users_by_follow_distance(
915 self.graph_store.as_ref(),
916 distance,
917 )
918 .with_context(|| format!("load social-graph distance {distance}"))?
919 {
920 if self
921 .graph_store
922 .is_overmuted_user(&pubkey, self.config.overmute_threshold)?
923 {
924 continue;
925 }
926 let hex = hex::encode(pubkey);
927 if seen.insert(hex.clone()) {
928 authors.push(hex);
929 }
930 }
931 }
932 Ok(authors)
933 }
934
935 async fn prioritize_archive_history_authors(
936 &self,
937 authors: Vec<String>,
938 ) -> Result<Vec<String>> {
939 let Some(root) = self.graph_store.public_events_root()? else {
940 return Ok(authors);
941 };
942
943 let event_store = NostrEventStore::new(self.store.store_arc());
944 let mut prioritized = Vec::with_capacity(authors.len());
945 let mut sampled = 0usize;
946 for (index, author) in authors.into_iter().enumerate() {
947 let distance = match decode_hex_pubkey(&author) {
948 Some(pubkey) => self
949 .graph_store
950 .follow_distance(&pubkey)?
951 .unwrap_or(u32::MAX),
952 None => u32::MAX,
953 };
954 let indexed_text_sample = if distance <= ARCHIVE_HISTORY_PRIORITY_MAX_DISTANCE {
955 sampled = sampled.saturating_add(1);
956 event_store
957 .list_by_author_and_kind(
958 Some(&root),
959 &author,
960 Kind::TextNote.as_u16() as u32,
961 ListEventsOptions {
962 limit: Some(ARCHIVE_HISTORY_PRIORITY_SAMPLE_LIMIT),
963 ..ListEventsOptions::default()
964 },
965 )
966 .await
967 .with_context(|| {
968 format!("sample indexed text-note history for author {author}")
969 })?
970 .len()
971 } else {
972 ARCHIVE_HISTORY_PRIORITY_SAMPLE_LIMIT
973 };
974 prioritized.push((distance, indexed_text_sample, index, author));
975 }
976
977 prioritized.sort_by(|left, right| {
978 left.0
979 .cmp(&right.0)
980 .then_with(|| left.1.cmp(&right.1))
981 .then_with(|| left.2.cmp(&right.2))
982 });
983 info!(
984 "Nostr mirror configured archive history prioritized authors: authors={} sampled_distance_le_{}={}",
985 prioritized.len(),
986 ARCHIVE_HISTORY_PRIORITY_MAX_DISTANCE,
987 sampled
988 );
989 Ok(prioritized
990 .into_iter()
991 .map(|(_, _, _, author)| author)
992 .collect())
993 }
994
995 fn full_archive_history_kinds_for_config(config: &NostrMirrorConfig) -> Vec<u16> {
996 FULL_ARCHIVE_HISTORY_KINDS
997 .into_iter()
998 .filter(|kind| config.kinds.contains(kind))
999 .collect()
1000 }
1001
1002 fn legacy_text_history_kinds_for_config(config: &NostrMirrorConfig) -> Vec<u16> {
1003 LEGACY_TEXT_HISTORY_KINDS
1004 .into_iter()
1005 .filter(|kind| config.kinds.contains(kind))
1006 .collect()
1007 }
1008
1009 fn archive_history_settings_for_config(
1010 config: &NostrMirrorConfig,
1011 ) -> Option<ArchiveHistorySettings> {
1012 let (follow_distance, max_relay_pages, kinds) =
1013 if config.archive_history_max_relay_pages > 0 {
1014 (
1015 config.archive_history_follow_distance?,
1016 config.archive_history_max_relay_pages,
1017 Self::full_archive_history_kinds_for_config(config),
1018 )
1019 } else {
1020 (
1021 config.full_text_note_history_follow_distance?,
1022 config.full_text_note_history_max_relay_pages,
1023 Self::legacy_text_history_kinds_for_config(config),
1024 )
1025 };
1026 if max_relay_pages == 0 || kinds.is_empty() {
1027 return None;
1028 }
1029 Some(ArchiveHistorySettings {
1030 follow_distance: follow_distance.min(config.max_follow_distance),
1031 max_relay_pages,
1032 kinds,
1033 })
1034 }
1035
1036 fn archive_history_settings(&self) -> Option<ArchiveHistorySettings> {
1037 Self::archive_history_settings_for_config(&self.config)
1038 }
1039
1040 fn history_sync_kinds_for_config(config: &NostrMirrorConfig) -> Vec<u16> {
1041 config.kinds.clone()
1042 }
1043
1044 fn collect_missing_profile_authors(&self, limit: usize) -> Result<Vec<String>> {
1045 if limit == 0 {
1046 return Ok(Vec::new());
1047 }
1048
1049 let authors = self.collect_authors()?;
1050 if authors.is_empty() {
1051 return Ok(Vec::new());
1052 }
1053
1054 let mut cursor = self
1055 .missing_profile_cursor
1056 .lock()
1057 .expect("missing profile cursor");
1058 let mut index = (*cursor).min(authors.len());
1059 let mut scanned = 0usize;
1060 let mut missing = Vec::new();
1061
1062 while scanned < authors.len() && missing.len() < limit {
1063 let author = &authors[index];
1064 if self.graph_store.latest_profile_event(author)?.is_none() {
1065 missing.push(author.clone());
1066 }
1067 index += 1;
1068 if index == authors.len() {
1069 index = 0;
1070 }
1071 scanned += 1;
1072 }
1073
1074 *cursor = index;
1075 Ok(missing)
1076 }
1077
1078 fn should_backfill_missing_profiles(&self, last_run: Option<Instant>) -> bool {
1079 if self.config.missing_profile_backfill_batch_size == 0
1080 || !self.config.kinds.contains(&Kind::Metadata.as_u16())
1081 {
1082 return false;
1083 }
1084 match last_run {
1085 Some(last_run) => last_run.elapsed() >= MIRROR_MISSING_PROFILE_BACKFILL_INTERVAL,
1086 None => true,
1087 }
1088 }
1089
1090 fn should_history_sync_on_reconnect(
1091 history_sync_on_reconnect: bool,
1092 previous: Option<RelayStatus>,
1093 status: RelayStatus,
1094 ) -> bool {
1095 history_sync_on_reconnect
1096 && status == RelayStatus::Connected
1097 && matches!(
1098 previous,
1099 Some(
1100 RelayStatus::Initialized
1101 | RelayStatus::Pending
1102 | RelayStatus::Connecting
1103 | RelayStatus::Disconnected
1104 | RelayStatus::Terminated
1105 )
1106 )
1107 }
1108
1109 fn should_run_reconnect_history_sync(last_run: Option<&Instant>) -> bool {
1110 match last_run {
1111 None => true,
1112 Some(last_run) => last_run.elapsed() >= MIRROR_RECONNECT_HISTORY_SYNC_COOLDOWN,
1113 }
1114 }
1115
1116 fn is_metadata_only_history_sync(kinds: &[u16]) -> bool {
1117 !kinds.is_empty() && kinds.iter().all(|kind| *kind == Kind::Metadata.as_u16())
1118 }
1119
1120 fn history_sync_kinds_affect_profile_or_graph(kinds: &[u16]) -> bool {
1121 kinds.is_empty()
1122 || kinds.iter().any(|kind| {
1123 *kind == Kind::Metadata.as_u16()
1124 || *kind == Kind::ContactList.as_u16()
1125 || *kind == Kind::MuteList.as_u16()
1126 })
1127 }
1128
1129 fn history_sync_plan_for(
1130 config: &NostrMirrorConfig,
1131 _authors: usize,
1132 kinds: &[u16],
1133 ) -> HistorySyncPlan {
1134 let author_batch_size = config.author_batch_size.max(1);
1135 let per_author_event_limit = config.history_sync_per_author_event_limit.max(1);
1136 let relay_page_size = 1_000;
1137 let max_relay_pages = 10;
1138
1139 if Self::is_metadata_only_history_sync(kinds) {
1140 return HistorySyncPlan {
1141 relay_fetch_mode: RelayFetchMode::AuthorBatches,
1142 author_batch_size: author_batch_size.min(METADATA_HISTORY_SYNC_AUTHOR_BATCH_SIZE),
1143 per_author_event_limit: METADATA_HISTORY_SYNC_PER_AUTHOR_EVENT_LIMIT,
1144 relay_page_size,
1145 max_relay_pages,
1146 };
1147 }
1148
1149 HistorySyncPlan {
1150 relay_fetch_mode: RelayFetchMode::AuthorBatches,
1151 author_batch_size,
1152 per_author_event_limit,
1153 relay_page_size,
1154 max_relay_pages,
1155 }
1156 }
1157
1158 fn history_sync_plan(&self, authors: usize, kinds: &[u16]) -> HistorySyncPlan {
1159 Self::history_sync_plan_for(&self.config, authors, kinds)
1160 }
1161
1162 fn full_archive_history_plan(
1163 mut plan: HistorySyncPlan,
1164 max_relay_pages: usize,
1165 ) -> HistorySyncPlan {
1166 plan.relay_fetch_mode = RelayFetchMode::AuthorBatches;
1167 plan.max_relay_pages = max_relay_pages.max(1);
1168 plan.per_author_event_limit = plan
1169 .per_author_event_limit
1170 .max(plan.relay_page_size.saturating_mul(plan.max_relay_pages));
1171 plan
1172 }
1173
1174 fn full_archive_fetch_timeout(base: Duration, max_relay_pages: usize) -> Duration {
1175 let multiplier = max_relay_pages.clamp(1, FULL_ARCHIVE_FETCH_TIMEOUT_MAX_MULTIPLIER) as u32;
1176 base.checked_mul(multiplier).unwrap_or(Duration::MAX)
1177 }
1178
1179 fn history_sync_chunk_size_for_config(
1180 config: &NostrMirrorConfig,
1181 _authors: usize,
1182 _kinds: &[u16],
1183 full_author_history: bool,
1184 chunk_size_override: Option<usize>,
1185 ) -> usize {
1186 let configured = chunk_size_override
1187 .unwrap_or(config.history_sync_author_chunk_size)
1188 .max(1);
1189 if full_author_history {
1190 configured.min(config.author_batch_size.max(1))
1191 } else {
1192 configured
1193 }
1194 }
1195
1196 async fn history_sync_authors(&self, authors: Vec<String>) -> Result<()> {
1197 let kinds = Self::history_sync_kinds_for_config(&self.config);
1198 if kinds.is_empty() {
1199 info!("Nostr mirror history sync skipped: no enabled history kinds");
1200 return Ok(());
1201 }
1202 self.history_sync_authors_with_kinds(authors, &kinds).await
1203 }
1204
1205 async fn history_sync_authors_with_kinds(
1206 &self,
1207 authors: Vec<String>,
1208 kinds: &[u16],
1209 ) -> Result<()> {
1210 self.history_sync_authors_with_kinds_and_mode(authors, kinds, false, None)
1211 .await
1212 }
1213
1214 async fn history_sync_archive_for_reachable_authors(&self) -> Result<()> {
1215 let Some(settings) = self.archive_history_settings() else {
1216 info!("Nostr mirror configured archive history sync skipped: disabled settings");
1217 return Ok(());
1218 };
1219 let distance = settings.follow_distance;
1220 info!(
1221 "Nostr mirror configured archive history author collection starting: max_follow_distance={distance}"
1222 );
1223 let authors = self
1224 .prioritize_archive_history_authors(self.collect_authors_with_max_distance(distance)?)
1225 .await?;
1226 self.history_sync_distance_filtered_archive(authors, settings)
1227 .await
1228 }
1229
1230 async fn history_sync_archive_for_authors(&self, authors: Vec<String>) -> Result<()> {
1231 let Some(settings) = self.archive_history_settings() else {
1232 info!("Nostr mirror configured archive history sync skipped: disabled settings");
1233 return Ok(());
1234 };
1235 let distance = settings.follow_distance;
1236 let mut close_authors = Vec::new();
1237 for author in authors {
1238 let Some(pubkey) = decode_hex_pubkey(&author) else {
1239 continue;
1240 };
1241 if self
1242 .graph_store
1243 .follow_distance(&pubkey)?
1244 .is_some_and(|actual_distance| actual_distance <= distance)
1245 {
1246 close_authors.push(author);
1247 }
1248 }
1249 if close_authors.is_empty() {
1250 return Ok(());
1251 }
1252 let close_authors = self
1253 .prioritize_archive_history_authors(close_authors)
1254 .await?;
1255
1256 self.history_sync_distance_filtered_archive(close_authors, settings)
1257 .await
1258 }
1259
1260 async fn history_sync_distance_filtered_archive(
1261 &self,
1262 close_authors: Vec<String>,
1263 settings: ArchiveHistorySettings,
1264 ) -> Result<()> {
1265 if close_authors.is_empty() {
1266 return Ok(());
1267 }
1268
1269 info!(
1270 "Nostr mirror configured archive history sync starting: authors={} max_follow_distance={} max_relay_pages={}",
1271 close_authors.len(),
1272 settings.follow_distance,
1273 settings.max_relay_pages
1274 );
1275 info!(
1276 "Nostr mirror configured archive per-kind sync starting: kinds={:?} authors={}",
1277 settings.kinds,
1278 close_authors.len()
1279 );
1280 self.history_sync_authors_with_kinds_and_mode(
1281 close_authors,
1282 &settings.kinds,
1283 true,
1284 Some(settings.max_relay_pages),
1285 )
1286 .await
1287 }
1288
1289 async fn history_sync_authors_with_kinds_and_mode(
1290 &self,
1291 authors: Vec<String>,
1292 kinds: &[u16],
1293 full_author_history: bool,
1294 max_relay_pages: Option<usize>,
1295 ) -> Result<()> {
1296 let update_profile_and_graph = Self::history_sync_kinds_affect_profile_or_graph(kinds);
1297 let chunk_size = Self::history_sync_chunk_size_for_config(
1298 &self.config,
1299 authors.len(),
1300 kinds,
1301 full_author_history,
1302 None,
1303 );
1304 self.history_sync_authors_chunked(
1305 authors,
1306 |current_root, author_chunk| async move {
1307 self.history_sync_author_chunk(
1308 current_root,
1309 author_chunk,
1310 kinds,
1311 full_author_history,
1312 max_relay_pages,
1313 )
1314 .await
1315 },
1316 update_profile_and_graph,
1317 Some(chunk_size),
1318 )
1319 .await
1320 }
1321
1322 async fn history_sync_authors_chunked<F, Fut>(
1323 &self,
1324 authors: Vec<String>,
1325 mut run_chunk: F,
1326 update_profile_and_graph: bool,
1327 chunk_size_override: Option<usize>,
1328 ) -> Result<()>
1329 where
1330 F: FnMut(Option<hashtree_core::Cid>, Vec<String>) -> Fut,
1331 Fut: std::future::Future<Output = Result<CrawlReport>>,
1332 {
1333 if authors.is_empty() {
1334 return Ok(());
1335 }
1336 let _publication_deferral = RootPublicationDeferral::new(&self.root_publication_deferrals);
1337
1338 info!(
1339 "Nostr mirror history sync starting: authors={} relays={} negentropy_only={}",
1340 authors.len(),
1341 self.config.relays.len(),
1342 self.config.require_negentropy
1343 );
1344
1345 let mut current_root = self.graph_store.public_events_root_for_write()?;
1346 let mut last_error = None;
1347 let mut applied_chunks = 0usize;
1348 let mut failed_chunks = 0usize;
1349 let mut authors_since_publish = 0usize;
1350 let publish_checkpoint_authors = self.config.history_sync_author_chunk_size.max(1);
1351 let chunk_size = chunk_size_override
1352 .unwrap_or(self.config.history_sync_author_chunk_size)
1353 .max(1);
1354 let total_chunks = authors.len().div_ceil(chunk_size);
1355
1356 for (chunk_index, author_chunk) in authors.chunks(chunk_size).enumerate() {
1357 let author_chunk = author_chunk.to_vec();
1358 let author_count = author_chunk.len();
1359 info!(
1360 "Nostr mirror history sync chunk starting: chunk={}/{} authors={}",
1361 chunk_index + 1,
1362 total_chunks,
1363 author_count
1364 );
1365 let mut report = match run_chunk(current_root.clone(), author_chunk.clone()).await {
1366 Ok(report) => report,
1367 Err(err) => {
1368 failed_chunks = failed_chunks.saturating_add(1);
1369 warn!(
1370 "Nostr mirror history sync chunk failed: chunk={}/{} authors={} error={:#}",
1371 chunk_index + 1,
1372 total_chunks,
1373 author_count,
1374 err
1375 );
1376 last_error = Some(err);
1377 trim_transient_allocations();
1378 continue;
1379 }
1380 };
1381
1382 let latest_root = self.graph_store.public_events_root_for_write()?;
1383 if latest_root != current_root {
1384 info!(
1385 "Nostr mirror history sync root advanced while chunk was fetching; merging chunk into latest root: chunk={}/{} authors={} events_applied={}",
1386 chunk_index + 1,
1387 total_chunks,
1388 author_count,
1389 report.applied_events.len()
1390 );
1391 if report.applied_events.is_empty() {
1392 report.root = latest_root.clone();
1393 } else {
1394 let event_store = NostrEventStore::new(self.store.store_arc());
1395 report.root = event_store
1396 .build(latest_root.as_ref(), report.applied_events.clone())
1397 .await
1398 .context("merge history chunk into latest mirrored event root")?;
1399 }
1400 current_root = latest_root;
1401 }
1402
1403 if report.root != current_root {
1404 self.apply_history_root_with_options(
1405 report.root.as_ref(),
1406 update_profile_and_graph,
1407 false,
1408 Some(&report.applied_events),
1409 )
1410 .await?;
1411 current_root = report.root.clone();
1412 info!(
1413 "Nostr mirror history sync updated trusted root: chunk={}/{} authors_processed={} events_selected={} events_seen={}",
1414 chunk_index + 1,
1415 total_chunks,
1416 report.authors_processed,
1417 report.events_selected,
1418 report.events_seen
1419 );
1420 }
1421 applied_chunks = applied_chunks.saturating_add(1);
1422 authors_since_publish = authors_since_publish.saturating_add(author_count);
1423 if authors_since_publish >= publish_checkpoint_authors {
1424 self.publish_history_roots(update_profile_and_graph).await;
1425 authors_since_publish = 0;
1426 }
1427 trim_transient_allocations();
1428 }
1429
1430 if applied_chunks == 0 {
1431 return Err(last_error
1432 .unwrap_or_else(|| anyhow::anyhow!("mirror history sync made no progress"))
1433 .context(format!(
1434 "mirror history sync failed: applied_chunks=0 failed_chunks={failed_chunks}"
1435 )));
1436 }
1437 if failed_chunks > 0 {
1438 warn!(
1439 "Nostr mirror history sync completed with skipped chunks: applied_chunks={} failed_chunks={}",
1440 applied_chunks, failed_chunks
1441 );
1442 }
1443 self.publish_history_roots(update_profile_and_graph).await;
1447 if failed_chunks > 0 {
1448 return Err(last_error
1449 .unwrap_or_else(|| anyhow::anyhow!("mirror history sync skipped chunks"))
1450 .context(format!(
1451 "mirror history sync incomplete: applied_chunks={applied_chunks} failed_chunks={failed_chunks}"
1452 )));
1453 }
1454 Ok(())
1455 }
1456
1457 async fn history_sync_author_chunk(
1458 &self,
1459 current_root: Option<hashtree_core::Cid>,
1460 authors: Vec<String>,
1461 kinds: &[u16],
1462 full_author_history: bool,
1463 max_relay_pages: Option<usize>,
1464 ) -> Result<CrawlReport> {
1465 let mut last_error = None;
1466 let mut report = None;
1467 let mut plan = self.history_sync_plan(authors.len(), kinds);
1468 if full_author_history {
1469 plan = Self::full_archive_history_plan(
1470 plan,
1471 max_relay_pages.unwrap_or(plan.max_relay_pages),
1472 );
1473 }
1474 let fetch_timeout = if full_author_history {
1475 Self::full_archive_fetch_timeout(self.config.fetch_timeout, plan.max_relay_pages)
1476 } else {
1477 self.config.fetch_timeout
1478 };
1479 for attempt in 0..3 {
1480 let mut last_logged_authors = 0usize;
1481 let bridge = NostrBridge::new(
1482 self.store.store_arc(),
1483 CrawlConfig {
1484 relays: self.config.relays.clone(),
1485 author_allowlist: Some(authors.clone()),
1486 max_live_bytes: None,
1487 max_events_seen: None,
1488 max_authors: None,
1489 max_follow_distance: None,
1490 author_batch_size: plan.author_batch_size,
1491 per_author_event_limit: plan.per_author_event_limit,
1492 per_author_kind_event_limit: Some(plan.per_author_event_limit),
1493 per_author_live_bytes: None,
1494 fetch_timeout,
1495 kinds: Some(kinds.to_vec()),
1496 relay_fetch_mode: plan.relay_fetch_mode,
1497 require_negentropy: self.config.require_negentropy,
1498 relay_event_max_size: self.config.relay_event_max_size,
1499 relay_page_size: plan.relay_page_size,
1500 max_relay_pages: plan.max_relay_pages,
1501 full_author_history,
1502 },
1503 );
1504
1505 let crawl = bridge.crawl_with_progress(
1506 self.graph_store.as_ref(),
1507 current_root.as_ref(),
1508 |progress| {
1509 let log_interval = self.config.author_batch_size.saturating_mul(8).max(2_048);
1510 let should_log = progress.authors_processed == progress.authors_considered
1511 || progress.authors_processed == 0
1512 || progress
1513 .authors_processed
1514 .saturating_sub(last_logged_authors)
1515 >= log_interval;
1516 if should_log {
1517 last_logged_authors = progress.authors_processed;
1518 info!(
1519 "Nostr mirror history sync progress: authors_processed={}/{} events_selected={} events_seen={}",
1520 progress.authors_processed,
1521 progress.authors_considered,
1522 progress.events_selected,
1523 progress.events_seen
1524 );
1525 }
1526 },
1527 );
1528 let crawl_result: Result<CrawlReport> = crawl.await.map_err(Into::into);
1532
1533 match crawl_result {
1534 Ok(next_report) => {
1535 report = Some(next_report);
1536 break;
1537 }
1538 Err(err) => {
1539 last_error = Some(err);
1540 if attempt < 2 {
1541 tokio::time::sleep(Duration::from_millis(500)).await;
1542 }
1543 }
1544 }
1545 }
1546 report
1547 .ok_or_else(|| last_error.expect("history sync retry captured error"))
1548 .context("run mirror history sync")
1549 }
1550
1551 #[cfg(test)]
1552 async fn apply_history_root(&self, root: Option<&hashtree_core::Cid>) -> Result<()> {
1553 self.apply_history_root_with_options(root, true, true, None)
1554 .await
1555 }
1556
1557 async fn apply_history_root_with_options(
1558 &self,
1559 root: Option<&hashtree_core::Cid>,
1560 update_profile_and_graph: bool,
1561 publish_roots: bool,
1562 applied_events: Option<&[hashtree_nostr::StoredNostrEvent]>,
1563 ) -> Result<()> {
1564 self.graph_store.write_public_events_root(root)?;
1565 let Some(root) = root else {
1566 return Ok(());
1567 };
1568
1569 self.note_public_events_root_change()?;
1570 if update_profile_and_graph {
1571 let events = match applied_events {
1572 Some(events) => events
1573 .iter()
1574 .cloned()
1575 .map(socialgraph::stored_event_to_nostr_event)
1576 .collect::<Result<Vec<_>>>()?,
1577 None => {
1578 let event_store = NostrEventStore::new(self.store.store_arc());
1579 event_store
1580 .list_recent_lossy(Some(root), ListEventsOptions::default())
1581 .await
1582 .context("list trusted mirrored events")?
1583 .into_iter()
1584 .map(socialgraph::stored_event_to_nostr_event)
1585 .collect::<Result<Vec<_>>>()?
1586 }
1587 };
1588
1589 socialgraph::ingest_graph_parsed_events(self.graph_store.as_ref(), &events)
1590 .context("sync mirrored social graph state")?;
1591 if applied_events.is_some() {
1592 self.graph_store
1593 .sync_profile_index_for_events(&events)
1594 .context("update mirrored profile search index")?;
1595 } else {
1596 self.graph_store
1597 .rebuild_profile_index_for_events(&events)
1598 .context("rebuild mirrored profile search index")?;
1599 }
1600 self.note_profile_search_root_change()?;
1601 self.note_profiles_by_pubkey_root_change()?;
1602 }
1603 if !publish_roots {
1604 return Ok(());
1605 }
1606 self.publish_history_roots(update_profile_and_graph).await;
1607 Ok(())
1608 }
1609
1610 async fn publish_history_roots(&self, update_profile_and_graph: bool) {
1611 let (event_result, profile_search_result, profiles_by_pubkey_result) = self
1612 .publish_priority_roots(true, update_profile_and_graph, update_profile_and_graph)
1613 .await;
1614 if let Err(err) = event_result {
1615 warn!(
1616 "Nostr mirror event-root publish failed after root update: {:#}",
1617 err
1618 );
1619 }
1620 if let Err(err) = profile_search_result {
1621 warn!(
1622 "Nostr mirror profile-search publish failed after root update: {:#}",
1623 err
1624 );
1625 }
1626 if let Err(err) = profiles_by_pubkey_result {
1627 warn!(
1628 "Nostr mirror profiles-by-pubkey publish failed after root update: {:#}",
1629 err
1630 );
1631 }
1632 }
1633
1634 async fn subscribe_authors_since(
1635 &self,
1636 authors: &[String],
1637 since: Timestamp,
1638 subscribed_authors: &mut HashSet<String>,
1639 ) -> Result<()> {
1640 let new_authors = authors
1641 .iter()
1642 .filter(|author| !subscribed_authors.contains(*author))
1643 .cloned()
1644 .collect::<Vec<_>>();
1645 if new_authors.is_empty() {
1646 return Ok(());
1647 }
1648
1649 for chunk in new_authors.chunks(self.config.author_batch_size.max(1)) {
1650 let pubkeys = chunk
1651 .iter()
1652 .filter_map(|author| PublicKey::from_hex(author).ok())
1653 .collect::<Vec<_>>();
1654 if pubkeys.is_empty() {
1655 continue;
1656 }
1657
1658 let filter = Filter::new()
1659 .authors(pubkeys)
1660 .kinds(self.config.kinds.iter().copied().map(Kind::from))
1661 .since(since);
1662
1663 if let Err(err) = self.client.subscribe(filter, None).await {
1664 warn!(
1665 "Nostr mirror author subscription failed: authors={} error={:#}",
1666 chunk.len(),
1667 err
1668 );
1669 continue;
1670 }
1671 subscribed_authors.extend(chunk.iter().cloned());
1672 }
1673 Ok(())
1674 }
1675
1676 fn ingest_live_event(&self, event: &Event) -> Result<()> {
1677 self.pending_live_events
1678 .lock()
1679 .expect("pending live events")
1680 .insert(event.id.to_hex(), event.clone());
1681 Ok(())
1682 }
1683
1684 async fn flush_live_events(&self) -> Result<()> {
1685 let pending = {
1686 let mut pending = self
1687 .pending_live_events
1688 .lock()
1689 .expect("pending live events");
1690 if pending.is_empty() {
1691 return Ok(());
1692 }
1693 std::mem::take(&mut *pending)
1694 };
1695 let events = pending.into_values().collect::<Vec<_>>();
1696 let event_count = events.len();
1697 let previous_event_root = self.graph_store.public_events_root()?;
1698 let previous_profile_search_root = self.graph_store.profile_search_root()?;
1699 let previous_profiles_by_pubkey_root = self.graph_store.profiles_by_pubkey_root()?;
1700
1701 socialgraph::ingest_parsed_events_with_storage_class(
1702 self.graph_store.as_ref(),
1703 &events,
1704 socialgraph::EventStorageClass::Public,
1705 )
1706 .context("ingest live mirrored event batch")?;
1707
1708 let next_event_root = self.graph_store.public_events_root()?;
1709 let next_profile_search_root = self.graph_store.profile_search_root()?;
1710 let next_profiles_by_pubkey_root = self.graph_store.profiles_by_pubkey_root()?;
1711 let event_root_changed = next_event_root != previous_event_root;
1712 let profile_search_root_changed = next_profile_search_root != previous_profile_search_root;
1713 let profiles_by_pubkey_root_changed =
1714 next_profiles_by_pubkey_root != previous_profiles_by_pubkey_root;
1715
1716 if event_root_changed {
1717 self.note_public_events_root_change()?;
1718 }
1719 if profile_search_root_changed {
1720 self.note_profile_search_root_change()?;
1721 }
1722 if profiles_by_pubkey_root_changed {
1723 self.note_profiles_by_pubkey_root_change()?;
1724 }
1725 if profile_search_root_changed {
1726 self.maybe_publish_profile_search_root(false).await?;
1727 }
1728 if profiles_by_pubkey_root_changed {
1729 self.maybe_publish_profiles_by_pubkey_root(false).await?;
1730 }
1731 if event_root_changed {
1732 self.maybe_publish_event_root(false).await?;
1733 }
1734 info!(
1735 "Nostr mirror flushed live events: events={} event_root_changed={} profile_search_root_changed={} profiles_by_pubkey_root_changed={}",
1736 event_count,
1737 event_root_changed,
1738 profile_search_root_changed,
1739 profiles_by_pubkey_root_changed
1740 );
1741 Ok(())
1742 }
1743
1744 fn note_public_events_root_change(&self) -> Result<()> {
1745 let root = self.graph_store.public_events_root()?;
1746 if let Err(err) = Self::write_latest_event_root_state(self.store.base_path(), root.as_ref())
1747 {
1748 warn!("Nostr mirror failed to persist queryable event-root state: {err:#}");
1749 }
1750 Self::note_root_change(
1751 self.config.published_event_tree_name.as_deref(),
1752 &self.event_publish_state,
1753 root,
1754 )
1755 }
1756
1757 fn note_profile_search_root_change(&self) -> Result<()> {
1758 let root = self.graph_store.profile_search_root()?;
1759 Self::note_root_change(
1760 self.config.published_profile_search_tree_name.as_deref(),
1761 &self.profile_search_publish_state,
1762 root,
1763 )
1764 }
1765
1766 fn note_profiles_by_pubkey_root_change(&self) -> Result<()> {
1767 let root = self.graph_store.profiles_by_pubkey_root()?;
1768 Self::note_root_change(
1769 self.config
1770 .published_profiles_by_pubkey_tree_name
1771 .as_deref(),
1772 &self.profiles_by_pubkey_publish_state,
1773 root,
1774 )
1775 }
1776
1777 fn note_root_change(
1778 tree_name: Option<&str>,
1779 publish_state: &Arc<Mutex<RootPublishState>>,
1780 root: Option<hashtree_core::Cid>,
1781 ) -> Result<()> {
1782 let Some(_tree_name) = tree_name else {
1783 return Ok(());
1784 };
1785
1786 let mut state = publish_state.lock().expect("root publish state");
1787 let now = Instant::now();
1788
1789 if state.pending_root == root {
1790 return Ok(());
1791 }
1792
1793 state.pending_root = root;
1794 state.last_upload_failed_at = None;
1795 state.last_upload_error = None;
1796 state.last_changed_at = Some(now);
1797 if state.dirty_since.is_none() {
1798 state.dirty_since = Some(now);
1799 }
1800 Ok(())
1801 }
1802
1803 async fn maybe_publish_event_root(&self, force: bool) -> Result<()> {
1804 if self.take_missing_blob_event_upload_error() {
1805 return self.rebuild_event_indexes_after_missing_blobs(force).await;
1806 }
1807 let result = self
1808 .maybe_publish_root(
1809 self.config.published_event_tree_name.as_deref(),
1810 &self.event_publish_state,
1811 "event root",
1812 force,
1813 false,
1814 )
1815 .await;
1816 let Err(error) = result else {
1817 if self.take_missing_blob_event_upload_error() {
1818 return self.rebuild_event_indexes_after_missing_blobs(force).await;
1819 }
1820 return Ok(());
1821 };
1822 if !is_missing_local_blob_push_error(&error) {
1823 return Err(error);
1824 }
1825
1826 self.rebuild_event_indexes_after_missing_blobs(force).await
1827 }
1828
1829 fn take_missing_blob_event_upload_error(&self) -> bool {
1830 let mut state = self
1831 .event_publish_state
1832 .lock()
1833 .expect("event root publish state");
1834 if state.upload_in_progress_root.is_some() {
1835 return false;
1836 }
1837 if !state.missing_blob_rebuild_required {
1838 return false;
1839 }
1840 state.missing_blob_rebuild_required = false;
1841 state.last_upload_error = None;
1842 state.last_upload_failed_at = None;
1843 true
1844 }
1845
1846 async fn rebuild_event_indexes_after_missing_blobs(&self, force: bool) -> Result<()> {
1847 warn!(
1848 "Nostr mirror event root DAG references missing local blobs; rebuilding event indexes from stored events"
1849 );
1850 let (public_count, ambient_count) = self
1851 .graph_store
1852 .rebuild_event_indexes_from_stored_events_async()
1853 .await
1854 .context("rebuild event indexes after missing event blobs")?;
1855 info!(
1856 "Nostr mirror rebuilt event indexes after missing blobs: public={} ambient={}",
1857 public_count, ambient_count
1858 );
1859 self.sync_publish_roots_from_store()?;
1860
1861 self.maybe_publish_root(
1862 self.config.published_event_tree_name.as_deref(),
1863 &self.event_publish_state,
1864 "event root",
1865 force,
1866 false,
1867 )
1868 .await
1869 }
1870
1871 async fn maybe_publish_profile_search_root(&self, force: bool) -> Result<()> {
1872 self.maybe_publish_root(
1873 self.config.published_profile_search_tree_name.as_deref(),
1874 &self.profile_search_publish_state,
1875 "profile search root",
1876 force,
1877 true,
1878 )
1879 .await
1880 }
1881
1882 async fn maybe_publish_profiles_by_pubkey_root(&self, force: bool) -> Result<()> {
1883 self.maybe_publish_root(
1884 self.config
1885 .published_profiles_by_pubkey_tree_name
1886 .as_deref(),
1887 &self.profiles_by_pubkey_publish_state,
1888 "profiles-by-pubkey root",
1889 force,
1890 true,
1891 )
1892 .await
1893 }
1894
1895 async fn maybe_publish_root(
1896 &self,
1897 tree_name: Option<&str>,
1898 publish_state: &Arc<Mutex<RootPublishState>>,
1899 log_label: &str,
1900 force: bool,
1901 publish_before_upload_ready_on_force: bool,
1902 ) -> Result<()> {
1903 if !force && self.root_publication_deferrals.load(Ordering::Acquire) > 0 {
1904 return Ok(());
1905 }
1906 let Some(tree_name) = tree_name else {
1907 return Ok(());
1908 };
1909
1910 let pending_root = {
1911 let state = publish_state.lock().expect("root publish state");
1912 let Some(pending_root) = state.pending_root.clone() else {
1913 return Ok(());
1914 };
1915
1916 let now = Instant::now();
1917 let debounce_ready = state.last_changed_at.is_some_and(|changed_at| {
1918 now.duration_since(changed_at) >= MIRROR_ROOT_PUBLISH_DEBOUNCE
1919 });
1920 let stale_ready = state.dirty_since.is_some_and(|dirty_since| {
1921 now.duration_since(dirty_since) >= MIRROR_ROOT_PUBLISH_MAX_STALENESS
1922 });
1923 if !force && !debounce_ready && !stale_ready {
1924 return Ok(());
1925 }
1926
1927 pending_root
1928 };
1929
1930 let upload_started = self.maybe_start_background_root_upload(
1931 tree_name,
1932 &pending_root,
1933 publish_state,
1934 log_label,
1935 );
1936 let upload_required = !self.config.blossom_write_servers.is_empty();
1937 let (upload_ready, publish_root) = {
1938 let state = publish_state.lock().expect("root publish state");
1939 let upload_ready =
1940 !upload_required || state.last_uploaded_root.as_ref() == Some(&pending_root);
1941 let publish_root = if upload_ready {
1942 Some(pending_root.clone())
1943 } else {
1944 state.last_uploaded_root.clone().filter(|uploaded_root| {
1945 state.last_published_root.as_ref() != Some(uploaded_root)
1946 })
1947 };
1948 (upload_ready, publish_root)
1949 };
1950 let publish_before_upload_ready =
1951 force && upload_required && !upload_ready && publish_before_upload_ready_on_force;
1952 let publish_root = if let Some(publish_root) = publish_root {
1953 publish_root
1954 } else if publish_before_upload_ready {
1955 pending_root.clone()
1956 } else {
1957 if upload_started {
1958 info!(
1959 "Nostr mirror uploading {} DAG before publish: tree={} hash={}",
1960 log_label,
1961 tree_name,
1962 hex::encode(pending_root.hash),
1963 );
1964 }
1965 return Ok(());
1966 };
1967
1968 let mut successful_relays = Vec::new();
1969 let mut failed_relays = Vec::new();
1970 let mut published_now = false;
1971 let publish_required =
1972 self.publish_client.is_some() && !self.config.publish_relays.is_empty();
1973 if publish_required {
1974 let Some(publish_client) = self.publish_client.as_ref() else {
1975 unreachable!("publish_required implies publish_client");
1976 };
1977 if !self.has_connected_publish_relay().await {
1978 return Ok(());
1979 }
1980 if publish_before_upload_ready {
1981 info!(
1982 "Nostr mirror publishing {} before Blossom upload completes: tree={} hash={}",
1983 log_label,
1984 tree_name,
1985 hex::encode(pending_root.hash),
1986 );
1987 } else if !upload_ready {
1988 info!(
1989 "Nostr mirror publishing uploaded {} while newer root is still uploading: tree={} published_hash={} pending_hash={}",
1990 log_label,
1991 tree_name,
1992 hex::encode(publish_root.hash),
1993 hex::encode(pending_root.hash),
1994 );
1995 }
1996
1997 let already_published = {
1998 let state = publish_state.lock().expect("root publish state");
1999 state.last_published_root.as_ref() == Some(&publish_root)
2000 };
2001 if !already_published {
2002 let publish_relays = self.config.publish_relays.clone();
2003 let latest_known_created_at = {
2004 let state = publish_state.lock().expect("root publish state");
2005 state.last_published_created_at
2006 };
2007 let publish_created_at =
2008 next_replaceable_created_at(Timestamp::now(), latest_known_created_at);
2009 let event = publish_client
2010 .sign_event_builder(Self::build_public_root_event(
2011 tree_name,
2012 &publish_root,
2013 publish_created_at,
2014 ))
2015 .await
2016 .with_context(|| format!("sign {log_label} event"))?;
2017 let publish_result = self
2018 .publish_root_event_to_relays(publish_client, &publish_relays, &event)
2019 .await
2020 .with_context(|| format!("publish {log_label} event"))?;
2021 successful_relays = publish_result.0;
2022 failed_relays = publish_result.1;
2023 if successful_relays.is_empty() {
2024 let failure_summary = if failed_relays.is_empty() {
2025 "no publish relays accepted the event".to_string()
2026 } else {
2027 failed_relays.join("; ")
2028 };
2029 anyhow::bail!("no publish relays accepted the event ({failure_summary})");
2030 }
2031
2032 let mut state = publish_state.lock().expect("root publish state");
2033 if state.pending_root.as_ref() == Some(&pending_root)
2034 || state.last_uploaded_root.as_ref() == Some(&publish_root)
2035 || publish_before_upload_ready
2036 {
2037 state.last_published_root = Some(publish_root.clone());
2038 state.last_published_at = Some(Instant::now());
2039 state.last_published_created_at = Some(event.created_at);
2040 }
2041 published_now = true;
2042 }
2043 }
2044
2045 {
2046 let mut state = publish_state.lock().expect("root publish state");
2047 if state.pending_root.as_ref() == Some(&pending_root) {
2048 let upload_satisfied = self.config.blossom_write_servers.is_empty()
2049 || state.last_uploaded_root.as_ref() == Some(&pending_root);
2050 let publish_satisfied =
2051 !publish_required || state.last_published_root.as_ref() == Some(&pending_root);
2052 if upload_satisfied && publish_satisfied {
2053 state.dirty_since = None;
2054 }
2055 }
2056 }
2057
2058 if published_now {
2059 info!(
2060 "Nostr mirror published {}: tree={} hash={} relays={:?}",
2061 log_label,
2062 tree_name,
2063 hex::encode(publish_root.hash),
2064 successful_relays,
2065 );
2066 }
2067 if !failed_relays.is_empty() {
2068 warn!(
2069 "Nostr mirror publish had relay failures: tree={} failures={:?}",
2070 tree_name, failed_relays
2071 );
2072 }
2073 Ok(())
2074 }
2075
2076 fn maybe_start_background_root_upload(
2077 &self,
2078 tree_name: &str,
2079 pending_root: &hashtree_core::Cid,
2080 publish_state: &Arc<Mutex<RootPublishState>>,
2081 log_label: &str,
2082 ) -> bool {
2083 if self.config.blossom_write_servers.is_empty()
2084 || self.shutting_down.load(Ordering::Acquire)
2085 {
2086 return false;
2087 }
2088
2089 let previous_uploaded_root = {
2090 let mut state = publish_state.lock().expect("root publish state");
2091 if state.last_uploaded_root.as_ref() == Some(pending_root)
2092 || state.upload_in_progress_root.is_some()
2093 {
2094 return false;
2095 }
2096 if state
2097 .last_upload_failed_at
2098 .is_some_and(|failed_at| failed_at.elapsed() < MIRROR_ROOT_UPLOAD_RETRY_INTERVAL)
2099 {
2100 return false;
2101 }
2102 state.upload_in_progress_root = Some(pending_root.clone());
2103 state.last_uploaded_root.clone()
2104 };
2105
2106 let store = Arc::clone(&self.store);
2107 let upload_state_path = Self::uploaded_root_state_path(store.base_path(), tree_name);
2108 let servers = self.config.blossom_write_servers.clone();
2109 let root = pending_root.clone();
2110 let publish_state = Arc::clone(publish_state);
2111 let log_label = log_label.to_string();
2112 let (cancel, mut cancelled) = watch::channel(false);
2113 let task = tokio::task::spawn_blocking(move || {
2114 let runtime = tokio::runtime::Builder::new_current_thread()
2115 .enable_all()
2116 .build()
2117 .expect("build nostr mirror root upload runtime");
2118 let result = runtime.block_on(async {
2119 tokio::select! {
2120 result = background_blossom_push_incremental_with_store(
2121 store,
2122 root.clone(),
2123 previous_uploaded_root,
2124 &servers,
2125 ) => Some(result),
2126 _ = cancelled.changed() => None,
2127 }
2128 });
2129 let mut state = publish_state.lock().expect("root publish state");
2130 if state.upload_in_progress_root.as_ref() == Some(&root) {
2131 state.upload_in_progress_root = None;
2132 }
2133 if let Some(result) = result {
2134 match result {
2135 Ok(()) => {
2136 if let Err(err) =
2137 Self::write_uploaded_root_state(&upload_state_path, &root, &log_label)
2138 {
2139 warn!("Nostr mirror failed to persist uploaded root state: {err:#}");
2140 }
2141 state.last_uploaded_root = Some(root.clone());
2142 state.last_uploaded_at = Some(Instant::now());
2143 if state.pending_root.as_ref() == Some(&root) {
2144 state.last_upload_failed_at = None;
2145 state.last_upload_error = None;
2146 state.missing_blob_rebuild_required = false;
2147 }
2148 info!(
2149 "Nostr mirror uploaded {} DAG to Blossom: hash={}",
2150 log_label,
2151 hex::encode(root.hash)
2152 );
2153 }
2154 Err(err) => {
2155 if state.pending_root.as_ref() == Some(&root) {
2156 state.last_upload_failed_at = Some(Instant::now());
2157 state.last_upload_error = Some(format!("{err:#}"));
2158 }
2159 if is_missing_local_blob_message(&format!("{err:#}")) {
2160 state.missing_blob_rebuild_required = true;
2161 }
2162 warn!(
2163 "Nostr mirror {} DAG upload failed: hash={} error={:#}",
2164 log_label,
2165 hex::encode(root.hash),
2166 err
2167 );
2168 }
2169 }
2170 }
2171 drop(state);
2172 trim_transient_allocations();
2173 });
2174 let mut tasks = self.root_upload_tasks.lock().expect("root upload tasks");
2175 tasks.retain(|task| !task.join.is_finished());
2176 let task = RootUploadTask { cancel, join: task };
2177 if self.shutting_down.load(Ordering::Acquire) {
2178 let _ = task.cancel.send(true);
2179 }
2180 tasks.push(task);
2181
2182 true
2183 }
2184
2185 async fn publish_root_event_to_relays(
2186 &self,
2187 publish_client: &Client,
2188 relays: &[String],
2189 event: &Event,
2190 ) -> Result<(Vec<String>, Vec<String>)> {
2191 let primary_send = Self::send_root_event_to_relays(publish_client, relays, event);
2192 let (mut successful_relays, mut failed_relays) =
2193 match tokio::time::timeout(MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT, primary_send).await {
2194 Ok(result) => result,
2195 Err(_) => (
2196 Vec::new(),
2197 vec![format!(
2198 "publish relays: primary publish timed out after {:?}",
2199 MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT
2200 )],
2201 ),
2202 };
2203
2204 if successful_relays.is_empty() {
2205 let (retry_successes, retry_failures) = self
2206 .publish_root_event_with_fresh_client(relays, event)
2207 .await;
2208 successful_relays.extend(retry_successes);
2209 failed_relays.extend(retry_failures);
2210 }
2211
2212 Ok((successful_relays, failed_relays))
2213 }
2214
2215 async fn send_root_event_to_relays(
2216 publish_client: &Client,
2217 relays: &[String],
2218 event: &Event,
2219 ) -> (Vec<String>, Vec<String>) {
2220 let mut successful_relays = Vec::new();
2221 let mut failed_relays = Vec::new();
2222
2223 match publish_client
2224 .send_event_to(relays.iter().map(|relay| relay.as_str()), event)
2225 .await
2226 {
2227 Ok(output) => {
2228 for relay in relays {
2229 let relay_url = relay.trim_end_matches('/');
2230 if output
2231 .success
2232 .iter()
2233 .any(|url| url.as_str().trim_end_matches('/') == relay_url)
2234 {
2235 successful_relays.push(relay.clone());
2236 }
2237 }
2238 failed_relays.extend(
2239 output
2240 .failed
2241 .into_iter()
2242 .map(|(url, reason)| format!("{url}: {reason}")),
2243 );
2244 }
2245 Err(err) => {
2246 failed_relays.push(format!("publish relays: {err}"));
2247 }
2248 }
2249
2250 (successful_relays, failed_relays)
2251 }
2252
2253 async fn publish_root_event_with_fresh_client(
2254 &self,
2255 relays: &[String],
2256 event: &Event,
2257 ) -> (Vec<String>, Vec<String>) {
2258 let client = Client::new(Keys::generate());
2259 let mut setup_failures = Vec::new();
2260 for relay in relays {
2261 if let Err(err) = client.add_relay(relay).await {
2262 setup_failures.push(format!("{relay}: add relay failed: {err}"));
2263 }
2264 }
2265
2266 client.connect().await;
2267 let publish = Self::send_root_event_to_relays(&client, relays, event);
2268 let retry_timeout = self
2269 .config
2270 .fetch_timeout
2271 .min(MIRROR_ROOT_PUBLISH_RETRY_TIMEOUT);
2272 let result = tokio::time::timeout(retry_timeout, publish).await;
2273 let _ = client.disconnect().await;
2274
2275 match result {
2276 Ok((successful_relays, mut failed_relays)) => {
2277 failed_relays.extend(setup_failures);
2278 (successful_relays, failed_relays)
2279 }
2280 Err(_) => {
2281 setup_failures.push(format!(
2282 "fresh publish client timed out after {:?}",
2283 retry_timeout
2284 ));
2285 (Vec::new(), setup_failures)
2286 }
2287 }
2288 }
2289
2290 fn build_public_root_event(
2291 tree_name: &str,
2292 cid: &hashtree_core::Cid,
2293 created_at: Timestamp,
2294 ) -> EventBuilder {
2295 let mut tags = vec![
2296 Tag::identifier(tree_name.to_string()),
2297 Tag::custom(
2298 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)),
2299 vec!["hashtree"],
2300 ),
2301 Tag::custom(TagKind::Custom("hash".into()), vec![hex::encode(cid.hash)]),
2302 ];
2303 if let Some(key) = cid.key {
2304 tags.push(Tag::custom(
2305 TagKind::Custom("key".into()),
2306 vec![hex::encode(key)],
2307 ));
2308 }
2309
2310 EventBuilder::new(Kind::Custom(HASHTREE_ROOT_KIND as u16), "")
2311 .tags(tags)
2312 .custom_created_at(created_at)
2313 }
2314}
2315
2316fn is_missing_local_blob_push_error(error: &anyhow::Error) -> bool {
2317 error
2318 .chain()
2319 .any(|cause| cause.to_string().contains(MISSING_LOCAL_BLOB_PUSH_ERROR))
2320}
2321
2322fn is_missing_local_blob_message(message: &str) -> bool {
2323 message.contains(MISSING_LOCAL_BLOB_PUSH_ERROR)
2324}
2325
2326fn next_replaceable_created_at(now: Timestamp, latest_existing: Option<Timestamp>) -> Timestamp {
2327 match latest_existing {
2328 Some(latest) if latest >= now => Timestamp::from_secs(latest.as_secs().saturating_add(1)),
2329 _ => now,
2330 }
2331}
2332
2333#[cfg(test)]
2334mod tests;