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