eidetica/sync/ops.rs
1//! Core sync operations for the sync system.
2
3use std::future::Future;
4use std::time::Duration;
5
6use tokio::sync::oneshot;
7use tracing::{debug, info, warn};
8
9use super::{
10 Address, DatabaseTicket, PeerId, Sync, SyncError,
11 background::SyncCommand,
12 peer_manager::PeerManager,
13 peer_types,
14 protocol::{self, SyncRequest, SyncRequestAuth, SyncResponse, SyncTreeRequest},
15 user_sync_manager::UserSyncManager,
16};
17use crate::{
18 Database, Entry, Result,
19 auth::Permission,
20 auth::crypto::{PrivateKey, PublicKey},
21 crdt::Doc,
22 entry::ID,
23 store::DocStore,
24};
25
26use super::utils::collect_ancestors_to_send;
27
28impl Sync {
29 // === Core Sync Methods ===
30
31 /// Synchronize a specific tree with a peer using bidirectional sync.
32 ///
33 /// This is the main synchronization method that implements tip exchange
34 /// and bidirectional entry transfer to keep trees in sync between peers.
35 /// It performs both pull (fetch missing entries) and push (send our entries).
36 ///
37 /// # Arguments
38 /// * `peer_pubkey` - The public key of the peer to sync with
39 /// * `tree_id` - The ID of the tree to synchronize
40 ///
41 /// # Returns
42 /// A Result indicating success or failure of the sync operation.
43 pub async fn sync_tree_with_peer(&self, peer_pubkey: &PublicKey, tree_id: &ID) -> Result<()> {
44 self.sync_tree_with_peer_as(peer_pubkey, tree_id, None)
45 .await
46 }
47
48 /// Synchronize a tree with a peer, signing the request with `signing_key`.
49 ///
50 /// The peer authorizes the pull against the key that signed it, so this must
51 /// be a key with read access on `tree_id`. `None` uses this instance's device
52 /// key, which is right when the device itself holds the access (directly or
53 /// through a delegated tree). A database that instead granted a *user* key —
54 /// the usual outcome of bootstrapping with one — needs that key here.
55 pub async fn sync_tree_with_peer_as(
56 &self,
57 peer_pubkey: &PublicKey,
58 tree_id: &ID,
59 signing_key: Option<&PrivateKey>,
60 ) -> Result<()> {
61 // Get peer information and address
62 let peer_info = self
63 .get_peer_info(peer_pubkey)
64 .await?
65 .ok_or_else(|| SyncError::PeerNotFound(peer_pubkey.to_string()))?;
66
67 let address = peer_info
68 .addresses
69 .first()
70 .ok_or_else(|| SyncError::Network("No addresses found for peer".to_string()))?;
71
72 // Get our current tips for this tree (empty if tree doesn't exist)
73 let backend = self.backend()?;
74 let our_tips = backend
75 .snapshot(tree_id)
76 .await
77 .map_err(|e| SyncError::BackendError(format!("Failed to get local tips: {e}")))?;
78
79 // Get our device public key for automatic peer tracking
80 let our_device_pubkey = self.get_device_pubkey().ok();
81
82 // Send unified sync request, signed so the peer can authorize the pull
83 let instance = self.instance()?;
84 let auth = SyncRequestAuth::sign(
85 signing_key.unwrap_or(instance.signing_key()?),
86 peer_pubkey,
87 tree_id,
88 &our_tips,
89 instance.clock().now_millis(),
90 );
91 let request = SyncRequest::SyncTree(SyncTreeRequest {
92 tree_id: tree_id.clone(),
93 our_tips,
94 peer_pubkey: our_device_pubkey,
95 requesting_key: None,
96 requesting_key_name: None,
97 requested_permission: None,
98 metadata: None,
99 auth: Some(auth),
100 });
101
102 // Send request via background sync command
103 let (tx, rx) = oneshot::channel();
104 self.background_tx
105 .get()
106 .ok_or(SyncError::NoTransportEnabled)?
107 .send(SyncCommand::SendRequest {
108 address: address.clone(),
109 request: Box::new(request),
110 response: tx,
111 })
112 .await
113 .map_err(|e| SyncError::CommandSendError(e.to_string()))?;
114
115 let response = rx
116 .await
117 .map_err(|e| SyncError::Network(format!("Response channel error: {e}")))?
118 .map_err(|e| SyncError::Network(format!("Request failed: {e}")))?;
119
120 match response {
121 SyncResponse::Bootstrap(bootstrap_response) => {
122 self.handle_bootstrap_response(bootstrap_response).await?;
123 }
124 SyncResponse::Incremental(incremental_response) => {
125 self.handle_incremental_response(incremental_response, address)
126 .await?;
127 }
128 SyncResponse::Error(msg) => {
129 return Err(SyncError::SyncProtocolError(format!("Sync error: {msg}")).into());
130 }
131 _ => {
132 return Err(SyncError::UnexpectedResponse {
133 expected: "Bootstrap or Incremental",
134 actual: format!("{response:?}"),
135 }
136 .into());
137 }
138 }
139
140 // Track tree/peer relationship for sync_on_commit to work
141 // This allows on_local_write() to find this peer when queueing entries
142 self.add_tree_sync(peer_pubkey, tree_id).await?;
143
144 Ok(())
145 }
146
147 /// Handle bootstrap response by storing root and all entries
148 pub(super) async fn handle_bootstrap_response(
149 &self,
150 response: protocol::BootstrapResponse,
151 ) -> Result<()> {
152 tracing::info!(tree_id = %response.tree_id, "Processing bootstrap response");
153
154 // Integrity check: the root entry's content must hash to the declared
155 // tree_id. Rejects peers serving substituted content. A mismatch here
156 // also covers the cross-algorithm bootstrap case (e.g. a SHA-256 tree
157 // advertised to a BLAKE3-default node) — those are unsupported until
158 // the backend gains multi-CID-per-entry storage, so failing loudly is
159 // better than silently re-keying the DAG under the wrong algorithm.
160 let derived = response.root_entry.id();
161 if derived != response.tree_id {
162 return Err(SyncError::InvalidEntry(format!(
163 "root entry content hashes to {} but bootstrap response declares tree_id {}",
164 derived, response.tree_id
165 ))
166 .into());
167 }
168
169 // Combine root entry with all other entries into a single batch
170 let mut all_entries = Vec::with_capacity(1 + response.all_entries.len());
171 all_entries.push(response.root_entry);
172 all_entries.extend(response.all_entries);
173
174 // Store all entries and fire callbacks once
175 self.store_received_entries(&response.tree_id, all_entries)
176 .await?;
177
178 tracing::info!(tree_id = %response.tree_id, "Bootstrap completed successfully");
179 Ok(())
180 }
181
182 /// Handle incremental response by storing missing entries and sending back what server is missing
183 pub(super) async fn handle_incremental_response(
184 &self,
185 response: protocol::IncrementalResponse,
186 peer_address: &peer_types::Address,
187 ) -> Result<()> {
188 tracing::debug!(tree_id = %response.tree_id, "Processing incremental response");
189
190 // Step 1: Store missing entries
191 self.store_received_entries(&response.tree_id, response.missing_entries)
192 .await?;
193
194 // Step 2: Check if server is missing entries from us
195 let backend = self.backend()?;
196 let our_snapshot = backend.snapshot(&response.tree_id).await?;
197 let their_tips = &response.their_tips;
198
199 // Find tips they don't have
200 let missing_tip_ids: Vec<_> = our_snapshot
201 .tips()
202 .iter()
203 .filter(|tip_id| !their_tips.contains(tip_id))
204 .cloned()
205 .collect();
206
207 if !missing_tip_ids.is_empty() {
208 tracing::debug!(
209 tree_id = %response.tree_id,
210 missing_tips = missing_tip_ids.len(),
211 "Server is missing some of our entries, sending them back"
212 );
213
214 // Collect entries server is missing
215 let engine = self
216 .backend()?
217 .local_engine()
218 .expect("sync requires local backend");
219 let entries_for_server =
220 collect_ancestors_to_send(engine.as_ref(), &missing_tip_ids, their_tips).await?;
221
222 if !entries_for_server.is_empty() {
223 // Send these entries back to server
224 self.send_missing_entries_to_peer(
225 peer_address,
226 &response.tree_id,
227 entries_for_server,
228 )
229 .await?;
230 }
231 }
232
233 tracing::debug!(tree_id = %response.tree_id, "Incremental sync completed");
234 Ok(())
235 }
236
237 /// Send entries that the server is missing back to complete bidirectional sync
238 async fn send_missing_entries_to_peer(
239 &self,
240 peer_address: &peer_types::Address,
241 tree_id: &ID,
242 entries: Vec<Entry>,
243 ) -> Result<()> {
244 if entries.is_empty() {
245 return Ok(());
246 }
247
248 tracing::debug!(
249 tree_id = %tree_id,
250 entry_count = entries.len(),
251 "Sending missing entries back to peer for bidirectional sync"
252 );
253
254 let request = protocol::SyncRequest::SendEntries(entries);
255
256 // Send via command channel
257 let (tx, rx) = tokio::sync::oneshot::channel();
258 self.background_tx
259 .get()
260 .ok_or(SyncError::NoTransportEnabled)?
261 .send(SyncCommand::SendRequest {
262 address: peer_address.clone(),
263 request: Box::new(request),
264 response: tx,
265 })
266 .await
267 .map_err(|e| SyncError::CommandSendError(e.to_string()))?;
268
269 // Wait for acknowledgment
270 let response = rx
271 .await
272 .map_err(|e| SyncError::Network(format!("Response channel error: {e}")))?
273 .map_err(|e| SyncError::Network(format!("Request failed: {e}")))?;
274
275 match response {
276 protocol::SyncResponse::Ack | protocol::SyncResponse::Count(_) => {
277 tracing::debug!(tree_id = %tree_id, "Server acknowledged receipt of missing entries");
278 Ok(())
279 }
280 protocol::SyncResponse::Error(e) => {
281 Err(SyncError::Network(format!("Server error receiving entries: {e}")).into())
282 }
283 _ => Err(SyncError::UnexpectedResponse {
284 expected: "Ack or Count",
285 actual: format!("{response:?}"),
286 }
287 .into()),
288 }
289 }
290
291 /// Validate and store received entries from a peer, firing remote write callbacks.
292 pub(super) async fn store_received_entries(
293 &self,
294 tree_id: &ID,
295 entries: Vec<Entry>,
296 ) -> Result<()> {
297 // These entries arrive without per-entry declared IDs — they were batched
298 // under a single tree_id by the sender. Content is stored under whatever
299 // ID our local `entry.id()` derives, so substitution attacks on individual
300 // entries would fail DAG connectivity checks via parent pointers rather
301 // than a per-entry hash check here. Root-level integrity is verified by
302 // the bootstrap handler against the declared tree_id.
303 //
304 // TODO: Add signature verification and parent-existence / DAG-connectivity
305 // checks before marking entries as verified.
306
307 // Store entries and fire callbacks via Instance::put_remote_entries.
308 // Stored Unverified: these arrived from a peer and have not been
309 // verified by this node.
310 let instance = self.instance()?;
311 instance
312 .put_remote_entries(tree_id, entries)
313 .await
314 .map_err(|e| SyncError::BackendError(format!("Failed to store entries: {e}")))?;
315
316 Ok(())
317 }
318
319 /// Send a batch of entries to a sync peer (async version).
320 ///
321 /// # Arguments
322 /// * `entries` - The entries to send
323 /// * `address` - The address of the peer to send to
324 ///
325 /// # Returns
326 /// A Result indicating whether the entries were successfully acknowledged.
327 pub async fn send_entries(
328 &self,
329 entries: impl AsRef<[Entry]>,
330 address: &Address,
331 ) -> Result<()> {
332 let entries_vec = entries.as_ref().to_vec();
333 let request = SyncRequest::SendEntries(entries_vec);
334 let response = self.send_request(&request, address).await?;
335
336 match response {
337 SyncResponse::Ack | SyncResponse::Count(_) => Ok(()),
338 SyncResponse::Error(msg) => Err(SyncError::SyncProtocolError(format!(
339 "Peer {} returned error: {}",
340 address.address, msg
341 ))
342 .into()),
343 _ => Err(SyncError::UnexpectedResponse {
344 expected: "Ack or Count",
345 actual: format!("{response:?}"),
346 }
347 .into()),
348 }
349 }
350
351 /// Send specific entries to a peer via the background sync engine.
352 ///
353 /// This method queues entries for direct transmission without duplicate filtering.
354 /// The caller is responsible for determining which entries should be sent.
355 ///
356 /// # Duplicate Prevention Architecture
357 ///
358 /// Eidetica uses **smart duplicate prevention** in the background sync engine:
359 /// - **Database sync** (`SyncWithPeer` command): Uses tip comparison for semantic filtering
360 /// - **Direct send** (this method): Trusts caller to provide appropriate entries
361 ///
362 /// For automatic duplicate prevention, use tree-based sync relationships instead
363 /// of calling this method directly.
364 ///
365 /// # Arguments
366 /// * `peer_id` - The peer ID to send to
367 /// * `entries` - The specific entries to send (no filtering applied)
368 ///
369 /// # Returns
370 /// A Result indicating whether the command was successfully queued for background processing.
371 pub async fn send_entries_to_peer(&self, peer_id: &PeerId, entries: Vec<Entry>) -> Result<()> {
372 self.background_tx
373 .get()
374 .ok_or(SyncError::NoTransportEnabled)?
375 .send(SyncCommand::SendEntries {
376 peer: peer_id.clone(),
377 entries,
378 })
379 .await
380 .map_err(|e| SyncError::CommandSendError(e.to_string()))?;
381 Ok(())
382 }
383
384 /// Queue an entry for sync to a peer (non-blocking, for use in callbacks).
385 ///
386 /// This method is designed for use in write callbacks where async operations
387 /// are not possible. It uses try_send to avoid blocking, and logs errors
388 /// rather than failing the callback.
389 ///
390 /// # Arguments
391 /// * `peer_pubkey` - The public key of the peer to sync with
392 /// * `entry_id` - The ID of the entry to queue
393 /// * `tree_id` - The tree ID where the entry belongs
394 ///
395 /// # Returns
396 /// Ok(()) if the entry was successfully queued.
397 /// Only returns Err if transport is not enabled.
398 pub fn queue_entry_for_sync(
399 &self,
400 peer_id: &PeerId,
401 entry_id: &ID,
402 tree_id: &ID,
403 ) -> Result<()> {
404 // Ensure background sync is running
405 if self.background_tx.get().is_none() {
406 return Err(SyncError::NoTransportEnabled.into());
407 }
408
409 // Add to queue - BackgroundSync will process and send
410 self.queue
411 .enqueue(peer_id, entry_id.clone(), tree_id.clone());
412
413 Ok(())
414 }
415
416 /// Handle local write events for automatic sync.
417 ///
418 /// This method is called by the Instance write callback system when entries
419 /// are committed locally. It looks up the combined sync settings for the database
420 /// and queues the entry for sync with all configured peers if sync is enabled.
421 ///
422 /// This is the core method that implements automatic sync-on-commit behavior.
423 ///
424 /// # Arguments
425 /// * `event` - The write event containing the newly committed entries
426 /// * `database` - The database where the entries were committed
427 ///
428 /// # Returns
429 /// Ok(()) on success, or an error if settings lookup fails
430 pub(crate) async fn on_local_write(
431 &self,
432 event: &crate::instance::WriteEvent,
433 database: &Database,
434 ) -> Result<()> {
435 // Early return if background sync not running
436 if self.background_tx.get().is_none() {
437 return Ok(());
438 }
439
440 // Look up combined settings for this database
441 let tx = self.sync_tree.new_transaction().await?;
442 let user_mgr = UserSyncManager::new(&tx);
443 let peer_mgr = PeerManager::new(&tx);
444
445 let combined_settings = match user_mgr.get_combined_settings(database.root_id()).await? {
446 Some(settings) => settings,
447 None => {
448 // No settings configured for this database - no sync needed
449 debug!(database_id = %database.root_id(), "No sync settings for database, skipping");
450 return Ok(());
451 }
452 };
453
454 // Check if sync is enabled and sync_on_commit is true
455 if !combined_settings.sync_enabled || !combined_settings.sync_on_commit {
456 debug!(
457 database_id = %database.root_id(),
458 sync_enabled = combined_settings.sync_enabled,
459 sync_on_commit = combined_settings.sync_on_commit,
460 "Sync not enabled for database"
461 );
462 return Ok(());
463 }
464
465 // Get list of peers for this database
466 let peers = peer_mgr.get_tree_peers(database.root_id()).await?;
467
468 if peers.is_empty() {
469 debug!(database_id = %database.root_id(), "No peers configured for database");
470 return Ok(());
471 }
472
473 // Queue each entry for sync with each peer. The event no longer
474 // carries entry payloads — expand the cursor advance back into a
475 // concrete set of IDs by walking the DAG diff. Cost is bounded
476 // by the cursor delta, not the full tree.
477 let tree_id = database.root_id();
478
479 let new_ids = database
480 .ids_added(event.previous_tips(), event.post_tips())
481 .await?;
482
483 for entry_id in &new_ids {
484 debug!(
485 database_id = %tree_id,
486 entry_id = %entry_id,
487 peer_count = peers.len(),
488 "Queueing entry for automatic sync"
489 );
490
491 for peer_id in &peers {
492 self.queue_entry_for_sync(peer_id, entry_id, tree_id)?;
493 }
494 }
495
496 Ok(())
497 }
498
499 /// Initialize combined settings for all users.
500 ///
501 /// This is called during Sync initialization. For new sync trees (just created),
502 /// it scans the _users database to register all existing users. For existing
503 /// sync trees (loaded), it updates combined settings for already-tracked users.
504 pub(super) async fn initialize_user_settings(&self) -> Result<()> {
505 use crate::store::Table;
506 use crate::user::types::UserInfo;
507
508 // Check if sync tree is freshly created (no users tracked yet)
509 let user_tracking = self
510 .sync_tree
511 .get_store_viewer::<DocStore>(super::user_sync_manager::USER_TRACKING_SUBTREE)
512 .await?;
513 let all_tracked = user_tracking.get_all().await?;
514
515 if all_tracked.keys().count() == 0 {
516 // New sync tree - register all users from _users database
517 let instance = self.instance.upgrade().ok_or(SyncError::InstanceDropped)?;
518 let users_db = instance.users_db().await?;
519 let users_table = users_db
520 .get_store_viewer::<Table<UserInfo>>("users")
521 .await?;
522 let all_users = users_table.search(|_| true).await?;
523
524 for (user_uuid, user_info) in all_users {
525 self.sync_user(&user_uuid, &user_info.user_database_id)
526 .await?;
527 }
528 } else {
529 // Existing sync tree - update settings for tracked users if changed
530 let tx = self.sync_tree.new_transaction().await?;
531 let user_mgr = UserSyncManager::new(&tx);
532
533 for user_uuid in all_tracked.keys() {
534 if let Some((prefs_db_id, _tips)) =
535 user_mgr.get_tracked_user_state(user_uuid).await?
536 {
537 self.sync_user(user_uuid, &prefs_db_id).await?;
538 }
539 }
540 }
541
542 Ok(())
543 }
544
545 /// Send a sync request to a peer and get a response (async version).
546 ///
547 /// # Arguments
548 /// * `request` - The sync request to send
549 /// * `address` - The address of the peer
550 ///
551 /// # Returns
552 /// The sync response from the peer.
553 pub(super) async fn send_request(
554 &self,
555 request: &SyncRequest,
556 address: &Address,
557 ) -> Result<SyncResponse> {
558 let (tx, rx) = oneshot::channel();
559
560 self.background_tx
561 .get()
562 .ok_or(SyncError::NoTransportEnabled)?
563 .send(SyncCommand::SendRequest {
564 address: address.clone(),
565 request: Box::new(request.clone()),
566 response: tx,
567 })
568 .await
569 .map_err(|e| SyncError::CommandSendError(e.to_string()))?;
570
571 rx.await
572 .map_err(|e| SyncError::Network(format!("Response channel error: {e}")))?
573 }
574
575 /// Discover available trees from a peer (simplified API).
576 ///
577 /// This method connects to a peer and retrieves the list of trees they're willing to sync.
578 /// This is useful for discovering what can be synced before setting up sync relationships.
579 ///
580 /// # Arguments
581 /// * `address` - The transport address of the peer.
582 ///
583 /// # Returns
584 /// A vector of TreeInfo describing available trees, or an error.
585 pub async fn discover_peer_trees(&self, address: &Address) -> Result<Vec<protocol::TreeInfo>> {
586 // Connect and get handshake info
587 let _peer_pubkey = self.connect_to_peer(address).await?;
588
589 // The handshake already contains the tree list, but we need to get it again
590 // since connect_to_peer doesn't return it. For now, return empty list
591 // TODO: Enhance this to actually return the tree list from handshake
592
593 tracing::warn!(
594 "discover_peer_trees not fully implemented - handshake contains tree info but API needs enhancement"
595 );
596 Ok(vec![])
597 }
598
599 /// Sync with a peer at a given address.
600 ///
601 /// This is a blocking convenience method that:
602 /// 1. Connects to discover the peer's public key
603 /// 2. Registers the peer and performs immediate sync
604 /// 3. Returns after sync completes
605 ///
606 /// For new code, prefer using [`register_sync_peer()`](Self::register_sync_peer)
607 /// directly, which registers intent and lets background sync handle it.
608 ///
609 /// # Arguments
610 /// * `address` - The transport address of the peer.
611 /// * `tree_id` - Optional tree ID to sync (None = discover available trees)
612 ///
613 /// # Returns
614 /// Result indicating success or failure.
615 pub async fn sync_with_peer(&self, address: &Address, tree_id: Option<&ID>) -> Result<()> {
616 self.sync_with_peer_as(address, tree_id, None).await
617 }
618
619 /// Sync with a peer, signing requests with `signing_key`.
620 ///
621 /// See [`sync_tree_with_peer_as`](Self::sync_tree_with_peer_as) for which
622 /// key to pass.
623 pub async fn sync_with_peer_as(
624 &self,
625 address: &Address,
626 tree_id: Option<&ID>,
627 signing_key: Option<&PrivateKey>,
628 ) -> Result<()> {
629 // Connect to peer if not already connected
630 let peer_pubkey = self.connect_to_peer(address).await?;
631
632 // Store the address for this peer (needed for sync_tree_with_peer)
633 self.add_peer_address(&peer_pubkey, address.clone()).await?;
634
635 if let Some(tree_id) = tree_id {
636 // Sync specific tree
637 self.sync_tree_with_peer_as(&peer_pubkey, tree_id, signing_key)
638 .await?;
639 } else {
640 // TODO: Sync all available trees
641 tracing::warn!(
642 "Syncing all trees not yet implemented - need to enhance discover_peer_trees first"
643 );
644 }
645
646 Ok(())
647 }
648
649 /// Sync with a peer using a [`DatabaseTicket`].
650 ///
651 /// Attempts [`sync_with_peer`](Self::sync_with_peer) for every address
652 /// hint in the ticket concurrently. Each address may point to a different
653 /// peer, so connections are independent. Succeeds if at least one address
654 /// syncs successfully; returns the last error if all fail.
655 ///
656 /// # Arguments
657 /// * `ticket` - A ticket containing the database ID and address hints.
658 ///
659 /// # Errors
660 /// Returns [`SyncError::InvalidAddress`] if the ticket has no address hints.
661 /// Returns the last sync error if no address succeeded.
662 pub async fn sync_with_ticket(&self, ticket: &DatabaseTicket) -> Result<()> {
663 let database_id = ticket.database_id().clone();
664 self.try_addresses_concurrently(ticket.addresses(), |sync, addr| {
665 let db_id = database_id.clone();
666 async move { sync.sync_with_peer(&addr, Some(&db_id)).await }
667 })
668 .await
669 }
670
671 /// Sync a specific tree with a peer, with optional authentication for bootstrap.
672 ///
673 /// This is a lower-level method that allows specifying authentication parameters
674 /// for bootstrap scenarios where access needs to be requested.
675 ///
676 /// # Arguments
677 /// * `peer_pubkey` - The public key of the peer to sync with
678 /// * `tree_id` - The ID of the tree to sync
679 /// * `requesting_key` - Optional private key to sign with and request access for
680 /// * `requesting_key_name` - Optional name/ID of the requesting key
681 /// * `requested_permission` - Optional permission level being requested
682 ///
683 /// # Returns
684 /// A Result indicating success or failure.
685 pub async fn sync_tree_with_peer_auth(
686 &self,
687 peer_pubkey: &PublicKey,
688 tree_id: &ID,
689 requesting_key: Option<&PrivateKey>,
690 requesting_key_name: Option<&str>,
691 requested_permission: Option<Permission>,
692 metadata: Option<Doc>,
693 ) -> Result<()> {
694 // Get peer information and address
695 let peer_info = self
696 .get_peer_info(peer_pubkey)
697 .await?
698 .ok_or_else(|| SyncError::PeerNotFound(peer_pubkey.to_string()))?;
699
700 let address = peer_info
701 .addresses
702 .first()
703 .ok_or_else(|| SyncError::Network("No addresses found for peer".to_string()))?;
704
705 // Get our current tips for this tree (empty if tree doesn't exist)
706 let backend = self.backend()?;
707 let our_tips = backend
708 .snapshot(tree_id)
709 .await
710 .map_err(|e| SyncError::BackendError(format!("Failed to get local tips: {e}")))?;
711
712 // Get our device public key for automatic peer tracking
713 let our_device_pubkey = self.get_device_pubkey().ok();
714
715 // Send unified sync request with auth parameters. The signature proves
716 // we hold our device key; a bootstrap that asks for a *different* key
717 // cannot be proven and stays on the manual approval path.
718 let instance = self.instance()?;
719 let signing_key = requesting_key.unwrap_or(instance.signing_key()?);
720 let auth = SyncRequestAuth::sign(
721 signing_key,
722 peer_pubkey,
723 tree_id,
724 &our_tips,
725 instance.clock().now_millis(),
726 );
727 let request = SyncRequest::SyncTree(SyncTreeRequest {
728 tree_id: tree_id.clone(),
729 our_tips,
730 peer_pubkey: our_device_pubkey,
731 requesting_key: requesting_key.map(|k| k.public_key()),
732 requesting_key_name: requesting_key_name.map(|k| k.to_string()),
733 requested_permission,
734 metadata,
735 auth: Some(auth),
736 });
737
738 // Send request via background sync command
739 let (tx, rx) = oneshot::channel();
740 self.background_tx
741 .get()
742 .ok_or(SyncError::NoTransportEnabled)?
743 .send(SyncCommand::SendRequest {
744 address: address.clone(),
745 request: Box::new(request),
746 response: tx,
747 })
748 .await
749 .map_err(|_| {
750 SyncError::CommandSendError("Background sync command channel closed".to_string())
751 })?;
752
753 // Wait for response
754 let response = rx
755 .await
756 .map_err(|_| {
757 SyncError::CommandSendError("Background sync response channel closed".to_string())
758 })?
759 .map_err(|e| SyncError::Network(format!("Sync request failed: {e}")))?;
760
761 // Handle the response (same logic as existing sync_tree_with_peer)
762 match response {
763 SyncResponse::Bootstrap(bootstrap_response) => {
764 info!(peer = %peer_pubkey, tree = %tree_id, entry_count = bootstrap_response.all_entries.len() + 1, "Received bootstrap response");
765
766 // Store root + all entries as a single batch with callback dispatch
767 let mut all_entries = Vec::with_capacity(1 + bootstrap_response.all_entries.len());
768 all_entries.push(bootstrap_response.root_entry);
769 all_entries.extend(bootstrap_response.all_entries);
770
771 // Bootstrap entries come from a peer; stored Unverified.
772 let instance = self.instance()?;
773 instance.put_remote_entries(tree_id, all_entries).await?;
774
775 info!(peer = %peer_pubkey, tree = %tree_id, "Bootstrap sync completed successfully");
776 }
777 SyncResponse::Incremental(incremental_response) => {
778 info!(peer = %peer_pubkey, tree = %tree_id, missing_count = incremental_response.missing_entries.len(), "Received incremental sync response");
779
780 // Use the enhanced handler that supports bidirectional sync
781 self.handle_incremental_response(incremental_response, address)
782 .await?;
783
784 debug!(peer = %peer_pubkey, tree = %tree_id, "Incremental sync completed");
785 }
786 SyncResponse::BootstrapPending {
787 request_id,
788 message,
789 } => {
790 info!(peer = %peer_pubkey, tree = %tree_id, request_id = %request_id, "Bootstrap request pending manual approval");
791 return Err(SyncError::BootstrapPending {
792 request_id,
793 message,
794 }
795 .into());
796 }
797 SyncResponse::Error(err) => {
798 return Err(SyncError::Network(format!("Peer returned error: {err}")).into());
799 }
800 _ => {
801 return Err(SyncError::SyncProtocolError(
802 "Unexpected response type for sync tree request".to_string(),
803 )
804 .into());
805 }
806 }
807
808 // Track tree/peer relationship for sync_on_commit to work
809 // This allows on_local_write() to find this peer when queueing entries
810 self.add_tree_sync(peer_pubkey, tree_id).await?;
811
812 Ok(())
813 }
814
815 // === Flush Operations ===
816
817 /// Process all queued entries and retry any failed sends.
818 ///
819 /// This method:
820 /// 1. Retries all entries in the retry queue (ignoring backoff timers)
821 /// 2. Processes all entries in the sync queue (batched by peer)
822 ///
823 /// When this method returns, all pending sync work has been attempted.
824 /// This is useful to eensuree that all pending pushes have completed.
825 ///
826 /// # Returns
827 /// `Ok(())` if all operations completed successfully, or an error
828 /// if the background sync engine is not running or sends failed.
829 pub async fn flush(&self) -> Result<()> {
830 let (tx, rx) = oneshot::channel();
831
832 self.background_tx
833 .get()
834 .ok_or(SyncError::NoTransportEnabled)?
835 .send(SyncCommand::Flush { response: tx })
836 .await
837 .map_err(|e| SyncError::CommandSendError(e.to_string()))?;
838
839 rx.await
840 .map_err(|e| SyncError::Network(format!("Response channel error: {e}")))?
841 }
842
843 /// Timeout applied to each address attempt in
844 /// [`try_addresses_concurrently`](Self::try_addresses_concurrently).
845 const ADDRESS_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(30);
846
847 /// Try an operation against multiple addresses concurrently, returning on
848 /// the first success.
849 ///
850 /// Spawns one detached task per address via [`tokio::spawn`]. Returns as
851 /// soon as any task succeeds. Remaining tasks are **not** cancelled — they
852 /// continue running in the background so that additional peer connections
853 /// can be established and registered for future syncs. Each task is subject
854 /// to [`ADDRESS_ATTEMPT_TIMEOUT`](Self::ADDRESS_ATTEMPT_TIMEOUT).
855 ///
856 /// If all tasks fail the last error is returned. If `addresses` is empty
857 /// an [`SyncError::InvalidAddress`] error is returned.
858 pub(super) async fn try_addresses_concurrently<F, Fut>(
859 &self,
860 addresses: &[Address],
861 f: F,
862 ) -> Result<()>
863 where
864 F: Fn(Sync, Address) -> Fut,
865 Fut: Future<Output = Result<()>> + Send + 'static,
866 {
867 if addresses.is_empty() {
868 return Err(SyncError::InvalidAddress("Ticket has no address hints".into()).into());
869 }
870
871 let (tx, mut rx) = tokio::sync::mpsc::channel(addresses.len());
872
873 for addr in addresses {
874 let tx = tx.clone();
875 let fut = f(self.clone(), addr.clone());
876 let addr_info = addr.clone();
877 // Detached spawn: the task keeps running even after we return.
878 tokio::spawn(async move {
879 let result = tokio::time::timeout(Self::ADDRESS_ATTEMPT_TIMEOUT, fut).await;
880 let result = match result {
881 Ok(inner) => inner,
882 Err(_) => {
883 warn!(
884 address = ?addr_info,
885 "Address attempt timed out after {:?}",
886 Self::ADDRESS_ATTEMPT_TIMEOUT,
887 );
888 Err(SyncError::Network(format!(
889 "Address attempt timed out after {:?}",
890 Self::ADDRESS_ATTEMPT_TIMEOUT,
891 ))
892 .into())
893 }
894 };
895 match &result {
896 Ok(()) => debug!(address = ?addr_info, "Address attempt succeeded"),
897 Err(e) => debug!(address = ?addr_info, error = %e, "Address attempt failed"),
898 }
899 // Ignore send errors — the receiver is dropped on early success,
900 // but the task still completes its work (peer registration, etc.).
901 let _ = tx.send(result).await;
902 });
903 }
904 // Drop our sender so the channel closes when all tasks finish.
905 drop(tx);
906
907 let mut last_err = None;
908 while let Some(result) = rx.recv().await {
909 match result {
910 Ok(()) => return Ok(()),
911 Err(e) => last_err = Some(e),
912 }
913 }
914
915 Err(last_err.expect("at least one task was spawned"))
916 }
917}