Skip to main content

commonware_storage/qmdb/sync/
engine.rs

1//! Core sync engine components that are shared across sync clients.
2use crate::{
3    merkle::{Family, Location, hasher::Standard as StandardHasher},
4    qmdb::{
5        self,
6        sync::{
7            Database, Error as SyncError, Journal, Metrics, SourceFor, Target,
8            database::Config as _,
9            error::EngineError,
10            requests::{Id as RequestId, Requests},
11            source::{FeedbackTx, Request, Response, Source},
12        },
13    },
14};
15use commonware_codec::Encode;
16use commonware_cryptography::Digest;
17use commonware_macros::{boxed, select};
18use commonware_runtime::Supervisor as _;
19use commonware_utils::channel::{
20    fallible::{AsyncFallibleExt, OneshotExt as _},
21    mpsc,
22};
23use futures::future::{Aborted, Either, pending};
24use mpsc::error::TryRecvError;
25use std::{collections::BTreeMap, fmt::Debug, num::NonZeroU64, sync::Arc};
26
27/// Type alias for sync engine errors
28type Error<DB, S> =
29    qmdb::sync::Error<<DB as Database>::Family, <S as Source>::Error, <DB as Database>::Digest>;
30
31/// Whether sync should continue or complete
32#[derive(Debug)]
33pub(crate) enum NextStep<C, D> {
34    /// Sync should continue with the updated client
35    Continue(C),
36    /// Sync is complete with the final database
37    Complete(D),
38}
39
40/// Events that can occur during synchronization
41#[derive(Debug)]
42enum Event<F: Family, Op, D: Digest, E> {
43    /// A target update was received
44    TargetUpdate(Target<F, D>),
45    /// A batch of operations was received, or its request was aborted by a target update
46    BatchReceived(Result<IndexedFetchResult<F, Op, D, E>, Aborted>),
47    /// The target update channel was closed
48    UpdateChannelClosed,
49    /// A finish signal was received
50    FinishRequested,
51    /// The finish signal channel was closed
52    FinishChannelClosed,
53}
54
55/// Result from a fetch operation, tagged with its request ID.
56#[derive(Debug)]
57pub(super) struct IndexedFetchResult<F: Family, Op, D: Digest, E> {
58    /// Unique ID assigned when the request was scheduled.
59    pub id: RequestId,
60    /// The result of the fetch operation.
61    pub result: Result<(Response<F, Op, D>, FeedbackTx), E>,
62}
63
64/// Wait for the next synchronization event.
65/// Returns `None` when there are no outstanding requests and no channels to wait on.
66async fn wait_for_event<F: Family, Op: Send, D: Digest, E: Send>(
67    update_rx: &mut Option<mpsc::Receiver<Target<F, D>>>,
68    finish_rx: &mut Option<mpsc::Receiver<()>>,
69    outstanding_requests: &mut Requests<F, Op, D, E>,
70) -> Option<Event<F, Op, D, E>> {
71    if outstanding_requests.len() == 0 && update_rx.is_none() && finish_rx.is_none() {
72        return None;
73    }
74
75    let target_update_fut = update_rx.as_mut().map_or_else(
76        || Either::Right(pending()),
77        |update_rx| Either::Left(update_rx.recv()),
78    );
79    let finish_fut = finish_rx.as_mut().map_or_else(
80        || Either::Right(pending()),
81        |finish_rx| Either::Left(finish_rx.recv()),
82    );
83    let batch_result_fut = outstanding_requests.next_completed();
84
85    select! {
86        finish = finish_fut => finish.map_or_else(
87            || Some(Event::FinishChannelClosed),
88            |_| Some(Event::FinishRequested)
89        ),
90        target = target_update_fut => target.map_or_else(
91            || Some(Event::UpdateChannelClosed),
92            |target| Some(Event::TargetUpdate(target))
93        ),
94        result = batch_result_fut => Some(Event::BatchReceived(result)),
95    }
96}
97
98/// Configuration for creating a new Engine
99pub struct Config<DB, S>
100where
101    DB: Database,
102    S: SourceFor<DB>,
103    DB::Op: Encode,
104{
105    /// Runtime context for creating database components
106    pub context: DB::Context,
107    /// Source of operations and proofs
108    pub source: S,
109    /// Trusted sync target (root digest and operation bounds).
110    ///
111    /// The engine only verifies source data against this commitment and does not select or
112    /// authenticate the target.
113    pub target: Target<DB::Family, DB::Digest>,
114    /// Maximum number of outstanding requests for operation batches
115    pub max_outstanding_requests: usize,
116    /// Maximum operations to fetch per batch
117    pub fetch_batch_size: NonZeroU64,
118    /// Number of operations to apply in a single batch
119    pub apply_batch_size: NonZeroU64,
120    /// Database-specific configuration
121    pub db_config: DB::Config,
122    /// Channel for receiving sync target updates.
123    ///
124    /// The caller selects targets before sending updates. The engine adopts only strictly
125    /// advancing targets and discards the rest.
126    pub update_rx: Option<mpsc::Receiver<Target<DB::Family, DB::Digest>>>,
127    /// Channel that requests sync completion once the current target is reached.
128    ///
129    /// When `None`, sync completes as soon as the target is reached.
130    pub finish_rx: Option<mpsc::Receiver<()>>,
131    /// Channel used to notify an observer once the current target is reached.
132    /// The engine sends at most one notification for each target.
133    ///
134    /// When `reached_target_tx` is `Some(...)`, this receiver must be actively
135    /// drained by the observer. The engine awaits send capacity on this channel before
136    /// proceeding, so backpressure can pause progress at target.
137    pub reached_target_tx: Option<mpsc::Sender<Target<DB::Family, DB::Digest>>>,
138    /// Maximum number of previous roots to retain for verifying in-flight
139    /// requests after target updates. Set to 0 to disable (all retained
140    /// requests will be re-fetched).
141    pub max_retained_roots: usize,
142}
143/// A shared sync engine that manages the core synchronization state and operations.
144pub(crate) struct Engine<DB, S>
145where
146    DB: Database,
147    S: SourceFor<DB>,
148    DB::Op: Encode,
149{
150    /// Tracks outstanding fetch requests and their futures
151    outstanding_requests: Requests<DB::Family, DB::Op, DB::Digest, S::Error>,
152
153    /// Operations that have been fetched but not yet applied to the log.
154    ///
155    /// # Invariant
156    ///
157    /// The vectors in the map are non-empty.
158    fetched_operations: BTreeMap<Location<DB::Family>, Vec<DB::Op>>,
159
160    /// Pinned merkle nodes extracted from proofs, used for database construction
161    pinned_nodes: Option<Vec<DB::Digest>>,
162
163    /// Historical roots from superseded sync targets, keyed by database size
164    /// (target.range.end()). Keys strictly increase across target updates
165    /// (non-advancing updates are discarded), so each size maps to a unique
166    /// root and the smallest key is the oldest. Eviction drops it first.
167    /// When a retained request completes, its requested size selects the
168    /// historical root to verify against.
169    retained_roots: BTreeMap<Location<DB::Family>, DB::Digest>,
170
171    /// Maximum number of historical roots to retain
172    max_retained_roots: usize,
173
174    /// The current sync target (root digest and operation bounds)
175    target: Target<DB::Family, DB::Digest>,
176
177    /// Maximum number of parallel outstanding requests
178    max_outstanding_requests: usize,
179
180    /// Maximum operations to fetch in a single batch
181    fetch_batch_size: NonZeroU64,
182
183    /// Number of operations to apply in a single batch
184    apply_batch_size: NonZeroU64,
185
186    /// Journal that operations are applied to during sync
187    journal: DB::Journal,
188
189    /// Source of operations and proofs, shared with in-flight requests
190    source: Arc<S>,
191
192    /// Hasher used for proof verification
193    hasher: StandardHasher<DB::Hasher>,
194
195    /// Runtime context for database operations
196    context: DB::Context,
197
198    /// Configuration for building the final database
199    config: DB::Config,
200
201    /// Optional receiver for target updates during sync
202    update_rx: Option<mpsc::Receiver<Target<DB::Family, DB::Digest>>>,
203
204    /// Whether the caller has asked the sync to finish at the current target.
205    finish_requested: bool,
206
207    /// Channel that requests sync completion once the current target is reached.
208    ///
209    /// When `None`, sync completes as soon as the target is reached.
210    finish_rx: Option<mpsc::Receiver<()>>,
211
212    /// Channel used to notify an observer once the current target is reached.
213    /// The engine sends at most one notification for each target.
214    ///
215    /// When `reached_target_tx` is `Some(...)`, this receiver must be actively
216    /// drained by the observer. The engine awaits send capacity on this channel before
217    /// proceeding, so backpressure can pause progress at target.
218    reached_target_tx: Option<mpsc::Sender<Target<DB::Family, DB::Digest>>>,
219
220    /// Progress gauges updated after target updates and batch application.
221    metrics: Metrics,
222
223    /// Tracks whether the current target has already been reported as reached.
224    reached_current_target_reported: bool,
225}
226
227#[cfg(test)]
228impl<DB, S> Engine<DB, S>
229where
230    DB: Database,
231    S: SourceFor<DB>,
232    DB::Op: Encode,
233{
234    pub(crate) fn journal(&self) -> &DB::Journal {
235        &self.journal
236    }
237}
238
239impl<DB, S> Engine<DB, S>
240where
241    DB: Database,
242    S: SourceFor<DB>,
243    DB::Op: Encode,
244{
245    pub async fn new(config: Config<DB, S>) -> Result<Self, Error<DB, S>> {
246        if !config.target.range.end().is_valid() {
247            return Err(SyncError::Engine(EngineError::InvalidTarget {
248                lower_bound_pos: config.target.range.start(),
249                upper_bound_pos: config.target.range.end(),
250            }));
251        }
252
253        // Create journal and verifier using the database's factory methods
254        let journal = <DB::Journal as Journal<DB::Family>>::new(
255            config.context.child("journal"),
256            config.db_config.journal_config(),
257            config.target.range.clone(),
258        )
259        .await?;
260        let journal_size = journal.size();
261
262        // The sync journal is the source of truth for resume. If it already
263        // reaches the target, try to recover the target's pinned nodes from local
264        // Merkle state before asking peers for them. Partial journals resume without
265        // probing completed database state.
266        let pinned_nodes = if journal_size == *config.target.range.end() {
267            DB::local_pinned_nodes(
268                config.context.child("local_pinned_nodes"),
269                &config.db_config,
270                &config.target,
271                &journal,
272            )
273            .await?
274        } else {
275            None
276        };
277
278        let sync_context = config.context.child("sync");
279        let metrics = Metrics::new(&sync_context);
280        let mut engine = Self {
281            outstanding_requests: Requests::new(),
282            fetched_operations: BTreeMap::new(),
283            pinned_nodes,
284            retained_roots: BTreeMap::new(),
285            max_retained_roots: config.max_retained_roots,
286            target: config.target.clone(),
287            max_outstanding_requests: config.max_outstanding_requests,
288            fetch_batch_size: config.fetch_batch_size,
289            apply_batch_size: config.apply_batch_size,
290            journal,
291            source: Arc::new(config.source),
292            hasher: qmdb::hasher::<DB::Hasher>(),
293            context: config.context,
294            config: config.db_config,
295            update_rx: config.update_rx,
296            finish_requested: false,
297            finish_rx: config.finish_rx,
298            reached_target_tx: config.reached_target_tx,
299            reached_current_target_reported: false,
300            metrics,
301        };
302        engine.schedule_requests()?;
303        engine.record_progress();
304        Ok(engine)
305    }
306
307    /// Track `request` and spawn its fetch against the shared source.
308    fn spawn_fetch(&mut self, request: Request<DB::Family>) {
309        let source = Arc::clone(&self.source);
310        self.outstanding_requests
311            .insert(request, move |id| async move {
312                let result = source.serve(request).await;
313                IndexedFetchResult { id, result }
314            });
315    }
316
317    /// Schedule new fetch requests for operations in the sync range that we haven't yet fetched.
318    fn schedule_requests(&mut self) -> Result<(), Error<DB, S>> {
319        let target_size = self.target.range.end();
320
321        // Schedule a boundary request at the lower sync bound if pinned nodes are still
322        // needed and one isn't already in flight. The pinned nodes it returns are what let
323        // us rebuild the pruned prefix.
324        if !self.pinned_nodes_ready()
325            && !self
326                .outstanding_requests
327                .contains(&self.target.range.start())
328        {
329            let request = Request::Boundary {
330                size: target_size,
331                start: self.target.range.start(),
332            };
333            self.spawn_fetch(request);
334        }
335
336        // Calculate the maximum number of requests to make
337        let num_requests = self
338            .max_outstanding_requests
339            .saturating_sub(self.outstanding_requests.len());
340
341        let log_size = self.journal.size();
342
343        for _ in 0..num_requests {
344            // Convert fetched operations to operation counts for shared gap detection
345            let operation_counts: BTreeMap<Location<DB::Family>, u64> = self
346                .fetched_operations
347                .iter()
348                .map(|(&start_loc, operations)| (start_loc, operations.len() as u64))
349                .collect();
350
351            // Find the next gap in the sync range that needs to be fetched.
352            let Some(gap_range) = crate::qmdb::sync::gaps::find_next(
353                Location::new(log_size)..self.target.range.end(),
354                &operation_counts,
355                self.outstanding_requests.ranges(),
356            ) else {
357                break; // No more gaps to fill
358            };
359
360            // Calculate batch size for this gap
361            let gap_size = *gap_range.end.checked_sub(*gap_range.start).unwrap();
362            let gap_size: NonZeroU64 = gap_size.try_into().unwrap();
363            let batch_size = self.fetch_batch_size.min(gap_size);
364
365            // Schedule the request
366            let request = Request::Operations {
367                size: target_size,
368                start: gap_range.start,
369                max_ops: batch_size,
370            };
371            self.spawn_fetch(request);
372        }
373
374        Ok(())
375    }
376
377    /// Reset sync state for a target update.
378    ///
379    /// Only cancels requests that cover ranges before the new target range
380    /// start. Requests at or after the new start are retained; their proofs
381    /// will be verified against the saved historical root (see
382    /// `retained_roots`) so the fetched operations can still be used.
383    pub async fn reset_for_target_update(
384        mut self,
385        new_target: Target<DB::Family, DB::Digest>,
386    ) -> Result<Self, Error<DB, S>> {
387        self.journal = self.journal.resize(new_target.range.start()).await?;
388        // Remove requests at or before the new start. The request at start
389        // must be re-issued as a boundary request with the new target size.
390        self.outstanding_requests
391            .remove_before(new_target.range.start().checked_add(1).unwrap());
392        self.fetched_operations.clear();
393        self.pinned_nodes = None;
394
395        // Save the current root keyed by its database size for verifying
396        // retained requests that were issued against this target.
397        if self.max_retained_roots > 0 {
398            self.retained_roots
399                .insert(self.target.range.end(), self.target.root);
400            while self.retained_roots.len() > self.max_retained_roots {
401                self.retained_roots.pop_first();
402            }
403        }
404
405        self.target = new_target;
406        self.reached_current_target_reported = false;
407        Ok(self)
408    }
409
410    /// Drain a pending explicit-finish signal without blocking.
411    ///
412    /// If a finish signal is present, the finish channel is dropped and the engine
413    /// may complete as soon as it is at a target. If the finish channel is
414    /// disconnected before a finish request is observed, this returns
415    /// [`EngineError::FinishChannelClosed`].
416    fn drain_finish_requests(&mut self) -> Result<(), Error<DB, S>> {
417        let Some(finish_rx) = self.finish_rx.as_mut() else {
418            return Ok(());
419        };
420        match finish_rx.try_recv() {
421            Ok(()) => {
422                self.finish_rx = None;
423                self.finish_requested = true;
424                Ok(())
425            }
426            Err(TryRecvError::Empty) => Ok(()),
427            Err(TryRecvError::Disconnected) => {
428                Err(SyncError::Engine(EngineError::FinishChannelClosed))
429            }
430        }
431    }
432
433    /// Notify an observer that the current target has been reached. The notification is sent
434    /// at most once per target, guarded by `reached_current_target_reported`.
435    ///
436    /// This send awaits backpressure. When `reached_target_tx` is `Some(...)`,
437    /// the receiver is expected to consume notifications promptly so the engine
438    /// can keep making progress. If the receiver side is closed, we drop the
439    /// sender and continue syncing without further reached-target notifications.
440    async fn report_reached_target(&mut self) {
441        if self.reached_current_target_reported {
442            return;
443        }
444        if let Some(sender) = self.reached_target_tx.as_ref()
445            && !sender.send_lossy(self.target.clone()).await
446        {
447            self.reached_target_tx = None;
448        }
449        self.reached_current_target_reported = true;
450    }
451
452    /// Record a progress snapshot in metrics.
453    fn record_progress(&mut self) {
454        self.metrics.record_target(*self.target.range.end());
455        self.metrics.record_synced(self.journal.size());
456    }
457
458    /// Store a batch of fetched operations. If the input list is empty, this is a no-op.
459    pub(crate) fn store_operations(
460        &mut self,
461        start_loc: Location<DB::Family>,
462        operations: Vec<DB::Op>,
463    ) {
464        if operations.is_empty() {
465            return;
466        }
467        self.fetched_operations.insert(start_loc, operations);
468    }
469
470    /// Apply fetched operations to the journal if we have them.
471    ///
472    /// This method finds operations that are contiguous with the current journal tip
473    /// and applies them in order. It removes stale batches and handles partial
474    /// application of batches when needed.
475    pub(crate) async fn apply_operations(mut self) -> Result<Self, Error<DB, S>> {
476        let mut next_loc = self.journal.size();
477
478        // Remove any batches of operations with stale data.
479        // That is, those whose last operation is before `next_loc`.
480        self.fetched_operations.retain(|&start_loc, operations| {
481            assert!(!operations.is_empty());
482            let end_loc = start_loc.checked_add(operations.len() as u64 - 1).unwrap();
483            end_loc >= next_loc
484        });
485
486        loop {
487            // See if we have the next operation to apply (i.e. at the journal tip).
488            // Find the index of the range that contains the next location.
489            let range_start_loc =
490                self.fetched_operations
491                    .iter()
492                    .find_map(|(range_start, range_ops)| {
493                        assert!(!range_ops.is_empty());
494                        let range_end =
495                            range_start.checked_add(range_ops.len() as u64 - 1).unwrap();
496                        if *range_start <= next_loc && next_loc <= range_end {
497                            Some(*range_start)
498                        } else {
499                            None
500                        }
501                    });
502
503            let Some(range_start_loc) = range_start_loc else {
504                // We don't have the next operation to apply (i.e. at the journal tip)
505                break;
506            };
507
508            // Remove the batch of operations that contains the next operation to apply.
509            let operations = self.fetched_operations.remove(&range_start_loc).unwrap();
510            assert!(!operations.is_empty());
511            // Skip operations that are before the next location. The containment check when
512            // selecting the range (`next_loc <= range_end`) guarantees at least one operation
513            // at or after it, so the batch is never empty.
514            let operations = &operations[(next_loc - *range_start_loc) as usize..];
515            next_loc += operations.len() as u64;
516            self.journal = self.journal.append(operations).await?;
517        }
518
519        Ok(self)
520    }
521
522    /// Check if sync is complete based on the current journal size and target
523    fn is_at_target(&self) -> Result<bool, Error<DB, S>> {
524        let journal_size = self.journal.size();
525        let target_journal_size = self.target.range.end();
526
527        // Check if we've completed sync
528        if journal_size >= target_journal_size {
529            if journal_size > target_journal_size {
530                // This shouldn't happen in normal operation - indicates a bug
531                return Err(SyncError::Engine(EngineError::InvalidState));
532            }
533            return Ok(true);
534        }
535
536        Ok(false)
537    }
538
539    /// Returns whether this target needs pinned nodes to reconstruct pruned state.
540    fn needs_pinned_nodes(&self) -> bool {
541        self.target.range.start() > Location::new(0)
542    }
543
544    /// Returns whether pinned nodes are present or not needed by this target.
545    fn pinned_nodes_ready(&self) -> bool {
546        !self.needs_pinned_nodes() || self.pinned_nodes.is_some()
547    }
548
549    /// Returns whether the journal and pinned nodes are both ready for completion.
550    fn is_ready_to_complete(&self) -> Result<bool, Error<DB, S>> {
551        Ok(self.is_at_target()? && self.pinned_nodes_ready())
552    }
553
554    /// Handle a response that failed validation.
555    ///
556    /// A source that accepts feedback is told the response failed, and the request is
557    /// retried. A source that is not listening cannot change its answer, so the failure
558    /// is terminal.
559    fn reject_response(feedback_tx: FeedbackTx) -> Result<(), Error<DB, S>> {
560        feedback_tx.map_or_else(
561            || Err(SyncError::Engine(EngineError::InvalidResponse)),
562            |feedback_tx| {
563                feedback_tx.send_lossy(false);
564                Ok(())
565            },
566        )
567    }
568
569    /// Handle the result of a fetch operation.
570    ///
571    /// Verifies the proof against the current root first, then falls back
572    /// to a matching historical root from `retained_roots` if available.
573    fn handle_fetch_result(
574        &mut self,
575        fetch_result: IndexedFetchResult<DB::Family, DB::Op, DB::Digest, S::Error>,
576    ) -> Result<(), Error<DB, S>> {
577        // Removal aborts a request's future, so a result for an untracked ID should
578        // be unreachable.
579        let Some(request) = self.outstanding_requests.remove(fetch_result.id) else {
580            return Ok(());
581        };
582
583        let (response, feedback_tx) = fetch_result.result.map_err(SyncError::Source)?;
584
585        let start_loc = request.start();
586        let size = request.size();
587
588        // The proof must cover exactly the requested size.
589        if response.proof().leaves != size {
590            return Self::reject_response(feedback_tx);
591        }
592        // A response must match the shape of its request.
593        match (request, response) {
594            (Request::Operations { max_ops, .. }, Response::Operations { proof, operations }) => {
595                let operations_len = operations.len() as u64;
596                if operations_len == 0 || operations_len > max_ops.get() {
597                    return Self::reject_response(feedback_tx);
598                }
599                let Some(root) = self.verification_root(size) else {
600                    return Ok(());
601                };
602                let elements = operations.iter().map(|op| op.encode()).collect::<Vec<_>>();
603                if !proof.verify_range_inclusion(&self.hasher, &elements, start_loc, root) {
604                    return Self::reject_response(feedback_tx);
605                }
606                if let Some(feedback_tx) = feedback_tx {
607                    feedback_tx.send_lossy(true);
608                }
609                self.store_operations(start_loc, operations);
610            }
611            (
612                Request::Boundary { .. },
613                Response::Boundary {
614                    proof,
615                    op,
616                    pinned_nodes,
617                },
618            ) => {
619                // Use the pinned nodes only if the current target still needs them. Otherwise
620                // keep the operation and drop the pinned nodes.
621                let need_pinned = size == self.target.range.end()
622                    && self.pinned_nodes.is_none()
623                    && start_loc == self.target.range.start();
624                let element = [op.encode()];
625                let valid = if need_pinned {
626                    proof.verify_proof_and_pinned_nodes(
627                        &self.hasher,
628                        &element,
629                        start_loc,
630                        &pinned_nodes,
631                        &self.target.root,
632                    )
633                } else {
634                    let Some(root) = self.verification_root(size) else {
635                        return Ok(());
636                    };
637                    proof.verify_range_inclusion(&self.hasher, &element, start_loc, root)
638                };
639                if !valid {
640                    if need_pinned {
641                        tracing::warn!("boundary response failed verification");
642                    }
643                    return Self::reject_response(feedback_tx);
644                }
645                if let Some(feedback_tx) = feedback_tx {
646                    feedback_tx.send_lossy(true);
647                }
648                if need_pinned {
649                    self.pinned_nodes = Some(pinned_nodes);
650                }
651                self.store_operations(start_loc, vec![op]);
652            }
653            _ => return Self::reject_response(feedback_tx),
654        }
655
656        Ok(())
657    }
658
659    /// The root to verify a response against at a given size.
660    fn verification_root(&self, size: Location<DB::Family>) -> Option<&DB::Digest> {
661        if size == self.target.range.end() {
662            Some(&self.target.root)
663        } else {
664            self.retained_roots.get(&size)
665        }
666    }
667
668    /// Handle a sync event and return the next engine state.
669    async fn handle_event(
670        mut self,
671        event: Event<DB::Family, DB::Op, DB::Digest, S::Error>,
672    ) -> Result<NextStep<Self, DB>, Error<DB, S>> {
673        match event {
674            Event::TargetUpdate(new_target) => {
675                // A non-advancing update is discarded.
676                if !new_target.advances(&self.target) {
677                    return Ok(NextStep::Continue(self));
678                }
679                // A same-root update that advances is impossible for an append-only log and
680                // indicates a caller bug.
681                if new_target.root == self.target.root {
682                    return Err(SyncError::Engine(EngineError::SyncTargetRootUnchanged));
683                }
684
685                let mut updated_self = self.reset_for_target_update(new_target).await?;
686                updated_self.record_progress();
687                updated_self.schedule_requests()?;
688                Ok(NextStep::Continue(updated_self))
689            }
690            Event::UpdateChannelClosed => {
691                self.update_rx = None;
692                Ok(NextStep::Continue(self))
693            }
694            Event::FinishRequested => {
695                self.finish_rx = None;
696                self.finish_requested = true;
697                Ok(NextStep::Continue(self))
698            }
699            Event::FinishChannelClosed => Err(SyncError::Engine(EngineError::FinishChannelClosed)),
700            Event::BatchReceived(fetch_result) => {
701                // An aborted request carries no result, but still wakes the loop to reschedule.
702                if let Ok(fetch_result) = fetch_result {
703                    self.handle_fetch_result(fetch_result)?;
704                }
705                self.schedule_requests()?;
706                let mut engine = self.apply_operations().await?;
707                engine.record_progress();
708                Ok(NextStep::Continue(engine))
709            }
710        }
711    }
712
713    /// Execute one step of the synchronization process.
714    ///
715    /// This is the main coordination method that:
716    /// 1. Checks if sync is complete
717    /// 2. Waits for the next synchronization event
718    /// 3. Handles different event types (target updates, fetch results)
719    /// 4. Coordinates request scheduling and operation application
720    ///
721    /// Returns `NextStep::Complete(database)` when sync is finished, or
722    /// `NextStep::Continue(self)` when more work remains.
723    #[boxed]
724    pub(crate) async fn step(mut self) -> Result<NextStep<Self, DB>, Error<DB, S>> {
725        self.drain_finish_requests()?;
726
727        // Check if sync is complete
728        if self.is_ready_to_complete()? {
729            // Take a queued target update before completing at the old target, unless the
730            // caller already asked to finish. Updates that do not advance the target are
731            // discarded.
732            if !self.finish_requested {
733                while let Some(update_rx) = self.update_rx.as_mut() {
734                    match update_rx.try_recv() {
735                        Ok(new_target) => {
736                            if new_target.advances(&self.target) {
737                                return self.handle_event(Event::TargetUpdate(new_target)).await;
738                            }
739                        }
740                        Err(TryRecvError::Empty) => break,
741                        Err(TryRecvError::Disconnected) => {
742                            self.update_rx = None;
743                        }
744                    }
745                }
746            }
747
748            self.report_reached_target().await;
749
750            if self.finish_rx.is_some() {
751                let event = wait_for_event(
752                    &mut self.update_rx,
753                    &mut self.finish_rx,
754                    &mut self.outstanding_requests,
755                )
756                .await
757                .ok_or(SyncError::Engine(EngineError::SyncStalled))?;
758                return self.handle_event(event).await;
759            }
760
761            return Ok(NextStep::Complete(self.complete().await?));
762        }
763
764        // Wait for the next synchronization event
765        let event = wait_for_event(
766            &mut self.update_rx,
767            &mut self.finish_rx,
768            &mut self.outstanding_requests,
769        )
770        .await
771        .ok_or(SyncError::Engine(EngineError::SyncStalled))?;
772        self.handle_event(event).await
773    }
774
775    /// Build the final database from the completed sync and verify its root against the
776    /// target.
777    async fn complete(mut self) -> Result<DB, Error<DB, S>> {
778        self.journal = self.journal.sync().await?;
779
780        let database = DB::from_sync_result(
781            self.context,
782            self.config,
783            self.journal,
784            self.pinned_nodes,
785            self.target.range.clone(),
786            self.apply_batch_size,
787        )
788        .await?;
789
790        let got_root = database.root();
791        let expected_root = self.target.root;
792        if got_root != expected_root {
793            return Err(SyncError::Engine(EngineError::RootMismatch {
794                expected: expected_root,
795                actual: got_root,
796            }));
797        }
798
799        Ok(database.persist_sync_result().await?)
800    }
801
802    /// Run sync to completion, returning the final database when done.
803    ///
804    /// This method repeatedly calls `step()` until sync is complete. The `step()` method
805    /// handles building the final database and verifying the root digest.
806    pub async fn sync(mut self) -> Result<DB, Error<DB, S>> {
807        // Run sync loop until completion
808        loop {
809            match self.step().await? {
810                NextStep::Continue(new_engine) => self = new_engine,
811                NextStep::Complete(database) => return Ok(database),
812            }
813        }
814    }
815}
816
817#[cfg(test)]
818mod tests {
819    use super::*;
820    use crate::merkle::mmr::{Family as MmrFamily, Proof};
821    use commonware_cryptography::{Sha256, sha256};
822    use commonware_runtime::{Runner as _, deterministic};
823    use commonware_utils::{NZU64, non_empty_range};
824    use std::{
825        convert::Infallible,
826        sync::{
827            Arc,
828            atomic::{AtomicUsize, Ordering},
829        },
830    };
831
832    #[derive(Clone)]
833    struct TestConfig {
834        journal_size: u64,
835        pinned_node_probes: Arc<AtomicUsize>,
836    }
837
838    impl crate::qmdb::sync::DatabaseConfig for TestConfig {
839        type JournalConfig = u64;
840
841        fn journal_config(&self) -> Self::JournalConfig {
842            self.journal_size
843        }
844    }
845
846    struct TestJournal {
847        size: u64,
848    }
849
850    impl Journal<MmrFamily> for TestJournal {
851        type Config = u64;
852        type Context = deterministic::Context;
853        type Error = crate::journal::Error;
854        type Op = i32;
855
856        async fn new(
857            _context: Self::Context,
858            size: Self::Config,
859            _range: commonware_utils::range::NonEmptyRange<Location<MmrFamily>>,
860        ) -> Result<Self, Self::Error> {
861            Ok(Self { size })
862        }
863
864        async fn resize(mut self, start: Location<MmrFamily>) -> Result<Self, Self::Error> {
865            self.size = *start;
866            Ok(self)
867        }
868
869        async fn sync(self) -> Result<Self, Self::Error> {
870            Ok(self)
871        }
872
873        fn size(&self) -> u64 {
874            self.size
875        }
876
877        async fn append(mut self, ops: &[Self::Op]) -> Result<Self, Self::Error> {
878            self.size += ops.len() as u64;
879            Ok(self)
880        }
881    }
882
883    struct TestDb;
884
885    impl Database for TestDb {
886        type Config = TestConfig;
887        type Context = deterministic::Context;
888        type Digest = sha256::Digest;
889        type Family = MmrFamily;
890        type Hasher = Sha256;
891        type Journal = TestJournal;
892        type Op = i32;
893
894        async fn from_sync_result(
895            _context: Self::Context,
896            _config: Self::Config,
897            _journal: Self::Journal,
898            _pinned_nodes: Option<Vec<Self::Digest>>,
899            _range: commonware_utils::range::NonEmptyRange<Location<Self::Family>>,
900            _apply_batch_size: NonZeroU64,
901        ) -> Result<Self, qmdb::Error<Self::Family>> {
902            Ok(Self)
903        }
904
905        async fn persist_sync_result(self) -> Result<Self, qmdb::Error<Self::Family>> {
906            Ok(self)
907        }
908
909        async fn local_pinned_nodes(
910            _context: Self::Context,
911            config: &Self::Config,
912            _target: &Target<Self::Family, Self::Digest>,
913            _journal: &Self::Journal,
914        ) -> Result<Option<Vec<Self::Digest>>, qmdb::Error<Self::Family>> {
915            config.pinned_node_probes.fetch_add(1, Ordering::SeqCst);
916            Ok(Some(vec![]))
917        }
918
919        fn root(&self) -> Self::Digest {
920            sha256::Digest::from([0u8; 32])
921        }
922    }
923
924    #[derive(Clone)]
925    struct TestSource;
926
927    impl Source for TestSource {
928        type Digest = sha256::Digest;
929        type Error = Infallible;
930        type Family = MmrFamily;
931        type Op = i32;
932
933        async fn serve(
934            &self,
935            _request: Request<MmrFamily>,
936        ) -> Result<(Response<Self::Family, Self::Op, Self::Digest>, FeedbackTx), Self::Error>
937        {
938            Ok((
939                Response::Operations {
940                    proof: Proof {
941                        leaves: Location::new(0),
942                        inactive_peaks: 0,
943                        digests: vec![],
944                    },
945                    operations: vec![],
946                },
947                None,
948            ))
949        }
950    }
951
952    fn test_engine_config(
953        context: deterministic::Context,
954        journal_size: u64,
955        pinned_node_probes: Arc<AtomicUsize>,
956    ) -> Config<TestDb, TestSource> {
957        Config {
958            context,
959            source: TestSource,
960            target: Target {
961                root: sha256::Digest::from([1u8; 32]),
962                range: non_empty_range!(Location::new(5), Location::new(10)),
963            },
964            max_outstanding_requests: 1,
965            fetch_batch_size: NZU64!(1),
966            apply_batch_size: NZU64!(1),
967            db_config: TestConfig {
968                journal_size,
969                pinned_node_probes,
970            },
971            update_rx: None,
972            finish_rx: None,
973            reached_target_tx: None,
974            max_retained_roots: 0,
975        }
976    }
977
978    #[test]
979    fn new_probes_local_pinned_nodes_when_journal_reaches_target() {
980        deterministic::Runner::default().start(|context| async move {
981            let pinned_node_probes = Arc::new(AtomicUsize::new(0));
982            Engine::new(test_engine_config(context, 10, pinned_node_probes.clone()))
983                .await
984                .unwrap();
985
986            assert_eq!(pinned_node_probes.load(Ordering::SeqCst), 1);
987        });
988    }
989
990    #[test]
991    fn new_skips_local_pinned_nodes_when_journal_is_partial() {
992        deterministic::Runner::default().start(|context| async move {
993            let pinned_node_probes = Arc::new(AtomicUsize::new(0));
994            Engine::new(test_engine_config(context, 7, pinned_node_probes.clone()))
995                .await
996                .unwrap();
997
998            assert_eq!(pinned_node_probes.load(Ordering::SeqCst), 0);
999        });
1000    }
1001
1002    #[test]
1003    fn new_schedules_operations_after_boundary_request() {
1004        deterministic::Runner::default().start(|context| async move {
1005            let mut config = test_engine_config(context, 5, Arc::new(AtomicUsize::new(0)));
1006            config.max_outstanding_requests = 2;
1007            config.fetch_batch_size = NZU64!(5);
1008
1009            let engine = Engine::new(config).await.unwrap();
1010            let requests = &engine.outstanding_requests;
1011
1012            assert_eq!(requests.len(), 2);
1013            assert!(requests.contains(&Location::new(5)));
1014            assert!(requests.contains(&Location::new(6)));
1015        });
1016    }
1017
1018    #[test]
1019    fn step_takes_queued_update_before_completing() {
1020        deterministic::Runner::default().start(|context| async move {
1021            let (update_tx, update_rx) = mpsc::channel(2);
1022            let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1023            config.update_rx = Some(update_rx);
1024            // Queue a stale update and an advancing one. The stale one is discarded and
1025            // the advancing one retargets the engine instead of completing.
1026            let stale = Target {
1027                root: sha256::Digest::from([2u8; 32]),
1028                range: non_empty_range!(Location::new(5), Location::new(10)),
1029            };
1030            let advancing = Target {
1031                root: sha256::Digest::from([3u8; 32]),
1032                range: non_empty_range!(Location::new(5), Location::new(12)),
1033            };
1034            update_tx.send(stale).await.unwrap();
1035            update_tx.send(advancing.clone()).await.unwrap();
1036
1037            let engine = Engine::new(config).await.unwrap();
1038            let NextStep::Continue(engine) = engine.step().await.unwrap() else {
1039                panic!("engine should retarget instead of completing");
1040            };
1041            assert_eq!(engine.target, advancing);
1042        });
1043    }
1044
1045    #[test]
1046    fn step_completes_at_current_target_after_finish() {
1047        deterministic::Runner::default().start(|context| async move {
1048            let (update_tx, update_rx) = mpsc::channel(1);
1049            let (finish_tx, finish_rx) = mpsc::channel(1);
1050            let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1051            // TestDb's root, so completion's final check passes.
1052            config.target.root = sha256::Digest::from([0u8; 32]);
1053            config.update_rx = Some(update_rx);
1054            config.finish_rx = Some(finish_rx);
1055            let advancing = Target {
1056                root: sha256::Digest::from([3u8; 32]),
1057                range: non_empty_range!(Location::new(5), Location::new(12)),
1058            };
1059            update_tx.send(advancing).await.unwrap();
1060            finish_tx.send(()).await.unwrap();
1061
1062            let engine = Engine::new(config).await.unwrap();
1063            let NextStep::Complete(_) = engine.step().await.unwrap() else {
1064                panic!("a requested finish must win over a queued update");
1065            };
1066        });
1067    }
1068
1069    /// A no-op fetch result for testing request tracking.
1070    fn dummy_result(id: RequestId) -> IndexedFetchResult<MmrFamily, i32, sha256::Digest, ()> {
1071        IndexedFetchResult {
1072            id,
1073            result: Ok((
1074                Response::Operations {
1075                    proof: Proof {
1076                        leaves: Location::new(0),
1077                        inactive_peaks: 0,
1078                        digests: vec![],
1079                    },
1080                    operations: vec![],
1081                },
1082                None,
1083            )),
1084        }
1085    }
1086
1087    /// Helper to add a request at a given location.
1088    fn add(requests: &mut Requests<MmrFamily, i32, sha256::Digest, ()>, loc: u64) -> RequestId {
1089        requests.insert(
1090            Request::Operations {
1091                size: Location::new(loc),
1092                start: Location::new(loc),
1093                max_ops: NZU64!(1),
1094            },
1095            |id| std::future::ready(dummy_result(id)),
1096        )
1097    }
1098
1099    #[test]
1100    fn test_add_and_remove() {
1101        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1102        assert_eq!(requests.len(), 0);
1103
1104        let id = add(&mut requests, 10);
1105        assert_eq!(requests.len(), 1);
1106        assert!(requests.contains(&Location::new(10)));
1107
1108        assert!(requests.remove(id).is_some());
1109        assert!(!requests.contains(&Location::new(10)));
1110        assert!(requests.remove(id).is_none());
1111    }
1112
1113    #[test]
1114    fn test_remove_before() {
1115        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1116
1117        add(&mut requests, 5);
1118        add(&mut requests, 10);
1119        add(&mut requests, 15);
1120        add(&mut requests, 20);
1121        assert_eq!(requests.len(), 4);
1122
1123        requests.remove_before(Location::new(10));
1124        assert_eq!(requests.len(), 3);
1125        assert!(!requests.contains(&Location::new(5)));
1126        assert!(requests.contains(&Location::new(10)));
1127        assert!(requests.contains(&Location::new(15)));
1128        assert!(requests.contains(&Location::new(20)));
1129    }
1130
1131    #[test]
1132    fn test_remove_before_all() {
1133        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1134
1135        add(&mut requests, 5);
1136        add(&mut requests, 10);
1137        assert_eq!(requests.len(), 2);
1138
1139        requests.remove_before(Location::new(100));
1140        assert_eq!(requests.len(), 0);
1141    }
1142
1143    #[test]
1144    fn test_remove_before_empty() {
1145        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1146        requests.remove_before(Location::new(10));
1147        assert_eq!(requests.len(), 0);
1148    }
1149
1150    #[test]
1151    fn test_remove_before_none() {
1152        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1153
1154        add(&mut requests, 10);
1155        add(&mut requests, 20);
1156        assert_eq!(requests.len(), 2);
1157
1158        requests.remove_before(Location::new(5));
1159        assert_eq!(requests.len(), 2);
1160        assert!(requests.contains(&Location::new(10)));
1161        assert!(requests.contains(&Location::new(20)));
1162    }
1163
1164    #[test]
1165    fn test_superseded_request() {
1166        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1167
1168        // Old request at location 10
1169        let old_id = add(&mut requests, 10);
1170        assert_eq!(requests.len(), 1);
1171
1172        // New request supersedes at same location
1173        let new_id = add(&mut requests, 10);
1174        assert_eq!(requests.len(), 1);
1175
1176        // Old ID is no longer tracked (superseded by insert)
1177        assert!(requests.remove(old_id).is_none());
1178
1179        // New ID is still tracked and by_location is intact
1180        assert!(requests.contains(&Location::new(10)));
1181        assert!(requests.remove(new_id).is_some());
1182        assert!(!requests.contains(&Location::new(10)));
1183    }
1184
1185    #[test]
1186    fn test_stale_id_after_remove_before() {
1187        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1188
1189        let old_id = add(&mut requests, 5);
1190        add(&mut requests, 15);
1191        requests.remove_before(Location::new(10));
1192
1193        // Old ID at location 5 was discarded by remove_before
1194        assert!(requests.remove(old_id).is_none());
1195
1196        // New request at the same location gets a different ID
1197        let new_id = add(&mut requests, 5);
1198        assert_ne!(old_id, new_id);
1199        assert!(requests.remove(new_id).is_some());
1200    }
1201
1202    #[test]
1203    fn test_remove_before_aborts_future() {
1204        deterministic::Runner::default().start(|_context| async move {
1205            let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1206            requests.insert(
1207                Request::Operations {
1208                    size: Location::new(5),
1209                    start: Location::new(5),
1210                    max_ops: NZU64!(1),
1211                },
1212                |_| std::future::pending(),
1213            );
1214            requests.remove_before(Location::new(10));
1215            assert!(matches!(requests.next_completed().await, Err(Aborted)));
1216        });
1217    }
1218}