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},
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::{Request, Response, Source},
12        },
13    },
14};
15use commonware_codec::Encode;
16use commonware_cryptography::{Digest, Hasher};
17use commonware_macros::{boxed, select};
18use commonware_runtime::Supervisor as _;
19use commonware_utils::channel::{mpsc, ring};
20use futures::future::{Aborted, Either, pending};
21use mpsc::error::TryRecvError;
22use std::{
23    collections::BTreeMap,
24    fmt::Debug,
25    num::{NonZeroU64, NonZeroUsize},
26    sync::Arc,
27};
28
29/// Type alias for sync engine errors
30type Error<DB, S> =
31    qmdb::sync::Error<<DB as Database>::Family, <S as Source>::Error, <DB as Database>::Digest>;
32
33/// Whether sync should continue or complete
34#[derive(Debug)]
35pub(crate) enum NextStep<C, D> {
36    /// Sync should continue with the updated client
37    Continue(C),
38    /// Sync is complete with the final database
39    Complete(D),
40}
41
42/// Events that can occur during synchronization
43#[derive(Debug)]
44enum Event<F: Family, Op, D: Digest, E> {
45    /// A target update was received
46    TargetUpdate(Target<F, D>),
47    /// A batch of operations was received, or its request was aborted by a target update
48    BatchReceived(Result<IndexedFetchResult<F, Op, D, E>, Aborted>),
49    /// The target update channel was closed
50    UpdateChannelClosed,
51    /// A finish signal was received
52    FinishRequested,
53    /// The finish signal channel was closed
54    FinishChannelClosed,
55}
56
57/// Result from a fetch operation, tagged with its request ID.
58#[derive(Debug)]
59pub(super) struct IndexedFetchResult<F: Family, Op, D: Digest, E> {
60    /// Unique ID assigned when the request was scheduled.
61    pub id: RequestId,
62    /// The result of the fetch operation.
63    pub result: Result<Option<Response<F, Op, D>>, E>,
64}
65
66/// Verifies one response against the exact request and trusted root that scheduled it.
67fn verify_response<F, Op, H>(
68    request: Request<F>,
69    root: &H::Digest,
70    response: &Response<F, Op, H::Digest>,
71) -> bool
72where
73    F: Family,
74    Op: Encode,
75    H: Hasher,
76{
77    if response.proof().leaves != request.size() {
78        return false;
79    }
80
81    let hasher = qmdb::hasher::<H>();
82    match (&request, response) {
83        (
84            Request::Operations { start, max_ops, .. },
85            Response::Operations { proof, operations },
86        ) => {
87            let operations_len = operations.len() as u64;
88            if operations_len == 0 || operations_len > max_ops.get() {
89                false
90            } else {
91                let elements = operations.iter().map(Encode::encode).collect::<Vec<_>>();
92                proof.verify_range_inclusion(&hasher, &elements, *start, root)
93            }
94        }
95        (
96            Request::Boundary { start, .. },
97            Response::Boundary {
98                proof,
99                op,
100                pinned_nodes,
101            },
102        ) => {
103            proof.verify_proof_and_pinned_nodes(&hasher, &[op.encode()], *start, pinned_nodes, root)
104        }
105        _ => false,
106    }
107}
108
109/// Wait for the next synchronization event.
110/// Returns `None` when there are no outstanding requests and no channels to wait on.
111async fn wait_for_event<F: Family, Op: Send, D: Digest, E: Send>(
112    update_rx: &mut Option<ring::Receiver<Target<F, D>>>,
113    finish_rx: &mut Option<mpsc::Receiver<()>>,
114    outstanding_requests: &mut Requests<F, Op, D, E>,
115) -> Option<Event<F, Op, D, E>> {
116    if outstanding_requests.len() == 0 && update_rx.is_none() && finish_rx.is_none() {
117        return None;
118    }
119
120    let target_update_fut = update_rx.as_mut().map_or_else(
121        || Either::Right(pending()),
122        |update_rx| Either::Left(update_rx.recv()),
123    );
124    let finish_fut = finish_rx.as_mut().map_or_else(
125        || Either::Right(pending()),
126        |finish_rx| Either::Left(finish_rx.recv()),
127    );
128    let batch_result_fut = outstanding_requests.next_completed();
129
130    select! {
131        finish = finish_fut => finish.map_or_else(
132            || Some(Event::FinishChannelClosed),
133            |_| Some(Event::FinishRequested)
134        ),
135        target = target_update_fut => target.map_or_else(
136            || Some(Event::UpdateChannelClosed),
137            |target| Some(Event::TargetUpdate(target))
138        ),
139        result = batch_result_fut => Some(Event::BatchReceived(result)),
140    }
141}
142
143/// Configuration for creating a new Engine
144pub struct Config<DB, S>
145where
146    DB: Database,
147{
148    /// Runtime context for creating database components
149    pub context: DB::Context,
150    /// Source of operations and proofs
151    pub source: S,
152    /// Trusted sync target (root digest and operation bounds).
153    ///
154    /// The engine only verifies source data against this commitment and does not select or
155    /// authenticate the target.
156    pub target: Target<DB::Family, DB::Digest>,
157    /// Maximum number of outstanding requests. The boundary request for the pinned nodes counts
158    /// toward it.
159    ///
160    /// Requests cover the next unfetched gaps anywhere in the sync range, so operations fetched
161    /// ahead of an unanswered request wait in memory until it is answered. The buffered amount
162    /// grows with the fetch rate and the time a request stays unanswered, which the source's
163    /// timeout and retry policy bounds.
164    pub max_outstanding_requests: NonZeroUsize,
165    /// Maximum operations to fetch per batch
166    pub fetch_batch_size: NonZeroU64,
167    /// Number of operations to apply in a single batch
168    pub apply_batch_size: NonZeroU64,
169    /// Database-specific configuration
170    pub db_config: DB::Config,
171    /// Channel for receiving sync target updates.
172    ///
173    /// The caller selects targets before sending updates and sends valid targets of the same
174    /// database in advancing order; an update may be superseded by a newer one before the engine
175    /// reads it. The engine adopts only strictly advancing targets and discards the rest.
176    pub update_rx: Option<ring::Receiver<Target<DB::Family, DB::Digest>>>,
177    /// Channel that requests sync completion once the current target is reached.
178    ///
179    /// When `None`, sync completes as soon as the target is reached.
180    pub finish_rx: Option<mpsc::Receiver<()>>,
181    /// Channel used to notify an observer once the current target is reached.
182    ///
183    /// The engine reports each reached target at most once, in the order reached, and never
184    /// waits for the observer, so an observer that falls behind skips intermediate reports.
185    pub reached_target_tx: Option<ring::Sender<Target<DB::Family, DB::Digest>>>,
186}
187/// A shared sync engine that manages the core synchronization state and operations.
188pub(crate) struct Engine<DB, S>
189where
190    DB: Database,
191    S: SourceFor<DB>,
192{
193    /// Tracks outstanding fetch requests and their futures
194    outstanding_requests: Requests<DB::Family, DB::Op, DB::Digest, S::Error>,
195
196    /// Operations that have been fetched but not yet applied to the log.
197    ///
198    /// # Invariant
199    ///
200    /// The vectors in the map are non-empty.
201    fetched_operations: BTreeMap<Location<DB::Family>, Vec<DB::Op>>,
202
203    /// Pinned merkle nodes at `target.range.start()`, used for database construction.
204    pinned_nodes: Option<Vec<DB::Digest>>,
205
206    /// The current sync target (root digest and operation bounds)
207    target: Target<DB::Family, DB::Digest>,
208
209    /// Maximum number of parallel outstanding requests
210    max_outstanding_requests: NonZeroUsize,
211
212    /// Maximum operations to fetch in a single batch
213    fetch_batch_size: NonZeroU64,
214
215    /// Number of operations to apply in a single batch
216    apply_batch_size: NonZeroU64,
217
218    /// Journal that operations are applied to during sync
219    journal: DB::Journal,
220
221    /// Source of operations and proofs, shared with in-flight requests
222    source: Arc<S>,
223
224    /// Runtime context for database operations
225    context: DB::Context,
226
227    /// Configuration for building the final database
228    config: DB::Config,
229
230    /// Optional receiver for target updates during sync
231    update_rx: Option<ring::Receiver<Target<DB::Family, DB::Digest>>>,
232
233    /// Whether the caller has asked the sync to finish at the current target.
234    finish_requested: bool,
235
236    /// Channel that requests sync completion once the current target is reached.
237    ///
238    /// When `None`, sync completes as soon as the target is reached.
239    finish_rx: Option<mpsc::Receiver<()>>,
240
241    /// Channel used to notify an observer once the current target is reached.
242    reached_target_tx: Option<ring::Sender<Target<DB::Family, DB::Digest>>>,
243
244    /// Progress gauges updated after target updates and batch application.
245    metrics: Metrics,
246
247    /// Tracks whether the current target has already been reported as reached.
248    reached_current_target_reported: bool,
249}
250
251#[cfg(test)]
252impl<DB, S> Engine<DB, S>
253where
254    DB: Database,
255    S: SourceFor<DB>,
256{
257    pub(crate) fn journal(&self) -> &DB::Journal {
258        &self.journal
259    }
260}
261
262impl<DB, S> Engine<DB, S>
263where
264    DB: Database,
265    S: SourceFor<DB>,
266{
267    pub async fn new(config: Config<DB, S>) -> Result<Self, Error<DB, S>> {
268        if !config.target.range.end().is_valid() {
269            return Err(SyncError::Engine(EngineError::InvalidTarget {
270                lower_bound_pos: config.target.range.start(),
271                upper_bound_pos: config.target.range.end(),
272            }));
273        }
274
275        // Recover the operation prefix that can resume this target.
276        let journal = <DB::Journal as Journal<DB::Family>>::new(
277            config.context.child("journal"),
278            config.db_config.journal_config(),
279            config.target.range.clone(),
280        )
281        .await?;
282        let journal_size = journal.size();
283
284        // The sync journal is the source of truth for resume. If it already
285        // reaches the target, try to recover the target's pinned nodes from local
286        // Merkle state before asking peers for them. Partial journals resume without
287        // probing completed database state.
288        let pinned_nodes = if journal_size == *config.target.range.end() {
289            DB::local_pinned_nodes(
290                config.context.child("local_pinned_nodes"),
291                &config.db_config,
292                &config.target,
293                &journal,
294            )
295            .await?
296        } else {
297            None
298        };
299
300        let sync_context = config.context.child("sync");
301        let metrics = Metrics::new(&sync_context);
302        let mut engine = Self {
303            outstanding_requests: Requests::new(),
304            fetched_operations: BTreeMap::new(),
305            pinned_nodes,
306            target: config.target.clone(),
307            max_outstanding_requests: config.max_outstanding_requests,
308            fetch_batch_size: config.fetch_batch_size,
309            apply_batch_size: config.apply_batch_size,
310            journal,
311            source: Arc::new(config.source),
312            context: config.context,
313            config: config.db_config,
314            update_rx: config.update_rx,
315            finish_requested: false,
316            finish_rx: config.finish_rx,
317            reached_target_tx: config.reached_target_tx,
318            reached_current_target_reported: false,
319            metrics,
320        };
321        engine.schedule_requests();
322        engine.record_progress();
323        Ok(engine)
324    }
325
326    /// Track `request` and spawn its fetch against the shared source.
327    fn spawn_fetch(&mut self, request: Request<DB::Family>) {
328        let source = Arc::clone(&self.source);
329        let root = self.target.root;
330        self.outstanding_requests
331            .insert(request, move |id| async move {
332                let result: Result<_, S::Error> = async {
333                    let (mut response, mut feedback) = source.serve(request).await?;
334                    loop {
335                        if verify_response::<DB::Family, DB::Op, DB::Hasher>(
336                            request, &root, &response,
337                        ) {
338                            if let Some(feedback) = feedback {
339                                feedback.accept();
340                            }
341                            return Ok(Some(response));
342                        }
343
344                        let Some(current) = feedback else {
345                            return Ok(None);
346                        };
347                        let Some((next_response, next_feedback)) = current.reject().await else {
348                            return Ok(None);
349                        };
350                        response = next_response;
351                        feedback = Some(next_feedback);
352                    }
353                }
354                .await;
355                IndexedFetchResult { id, result }
356            });
357    }
358
359    /// Schedule new fetch requests for operations in the sync range that we haven't yet fetched.
360    fn schedule_requests(&mut self) {
361        let target_size = self.target.range.end();
362
363        // Schedule a boundary request at the lower sync bound if pinned nodes are still
364        // needed and one isn't already in flight. The pinned nodes it returns are what let
365        // us rebuild the pruned prefix.
366        if !self.pinned_nodes_ready()
367            && !self
368                .outstanding_requests
369                .contains(&self.target.range.start())
370        {
371            let request = Request::Boundary {
372                size: target_size,
373                start: self.target.range.start(),
374            };
375            self.spawn_fetch(request);
376        }
377
378        // Calculate the maximum number of requests to make
379        let num_requests = self
380            .max_outstanding_requests
381            .get()
382            .saturating_sub(self.outstanding_requests.len());
383
384        let log_size = self.journal.size();
385
386        for _ in 0..num_requests {
387            // Find the next gap in the sync range that needs to be fetched.
388            let Some(gap_range) = crate::qmdb::sync::gaps::find_next(
389                Location::new(log_size)..self.target.range.end(),
390                self.fetched_operations.iter().map(|(&start, operations)| {
391                    start..start.checked_add(operations.len() as u64).unwrap()
392                }),
393                self.outstanding_requests.ranges(),
394            ) else {
395                break; // No more gaps to fill
396            };
397
398            // Calculate batch size for this gap
399            let gap_size = *gap_range.end.checked_sub(*gap_range.start).unwrap();
400            let gap_size: NonZeroU64 = gap_size.try_into().unwrap();
401            let batch_size = self.fetch_batch_size.min(gap_size);
402
403            // Schedule the request
404            let request = Request::Operations {
405                size: target_size,
406                start: gap_range.start,
407                max_ops: batch_size,
408            };
409            self.spawn_fetch(request);
410        }
411    }
412
413    /// Reset sync state for a target update.
414    ///
415    /// Keeps fetched operations. Keeps pinned nodes only while the lower bound is unchanged.
416    /// Keeps outstanding requests, whatever target size they were issued for, except those at or
417    /// below a moved lower bound. Each request verifies against the root it was issued with.
418    pub async fn reset_for_target_update(
419        mut self,
420        new_target: Target<DB::Family, DB::Digest>,
421    ) -> Result<Self, Error<DB, S>> {
422        let start_moved = self.target.range.start() != new_target.range.start();
423        self.journal = self.journal.resize(new_target.range.start()).await?;
424        if start_moved {
425            self.pinned_nodes = None;
426
427            // A source may prune up to the new lower bound, so a request below a moved bound
428            // may never be answered. Requests are also tracked by start location, so one kept at the
429            // new bound would block the boundary request there.
430            let new_start = new_target.range.start();
431            self.outstanding_requests
432                .retain(|request| request.start() > new_start);
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 finish channel is dropped and the engine
443    /// may complete as soon as it is at a target. If the finish channel is
444    /// disconnected before a finish request is observed, this returns
445    /// [`EngineError::FinishChannelClosed`].
446    fn drain_finish_requests(&mut self) -> Result<(), Error<DB, S>> {
447        let Some(finish_rx) = self.finish_rx.as_mut() else {
448            return Ok(());
449        };
450        match finish_rx.try_recv() {
451            Ok(()) => {
452                self.finish_rx = None;
453                self.finish_requested = true;
454                Ok(())
455            }
456            Err(TryRecvError::Empty) => Ok(()),
457            Err(TryRecvError::Disconnected) => {
458                Err(SyncError::Engine(EngineError::FinishChannelClosed))
459            }
460        }
461    }
462
463    /// Notify an observer that the current target has been reached. The notification is sent
464    /// at most once per target, guarded by `reached_current_target_reported`.
465    ///
466    /// The send never waits. If the observer has dropped its receiver, the engine stops reporting
467    /// and keeps syncing.
468    fn report_reached_target(&mut self) {
469        if self.reached_current_target_reported {
470            return;
471        }
472        if let Some(sender) = self.reached_target_tx.as_ref()
473            && !sender.send_lossy(self.target.clone())
474        {
475            self.reached_target_tx = None;
476        }
477        self.reached_current_target_reported = true;
478    }
479
480    /// Record a progress snapshot in metrics.
481    fn record_progress(&mut self) {
482        self.metrics.record_target(*self.target.range.end());
483        self.metrics.record_synced(self.journal.size());
484    }
485
486    /// Store a batch of fetched operations. If the input list is empty, this is a no-op.
487    ///
488    /// Each start has at most one outstanding request, and gaps skip stored batches, so a batch
489    /// never replaces another.
490    pub(crate) fn store_operations(
491        &mut self,
492        start_loc: Location<DB::Family>,
493        operations: Vec<DB::Op>,
494    ) {
495        if operations.is_empty() {
496            return;
497        }
498        self.fetched_operations.insert(start_loc, operations);
499    }
500
501    /// Apply fetched operations to the journal if we have them.
502    ///
503    /// This method finds operations that are contiguous with the current journal tip
504    /// and applies them in order. It removes stale batches and handles partial
505    /// application of batches when needed.
506    pub(crate) async fn apply_operations(mut self) -> Result<Self, Error<DB, S>> {
507        let mut next_loc = self.journal.size();
508
509        // Remove any batches of operations with stale data.
510        // That is, those whose last operation is before `next_loc`.
511        self.fetched_operations.retain(|&start_loc, operations| {
512            assert!(!operations.is_empty());
513            let end_loc = start_loc.checked_add(operations.len() as u64 - 1).unwrap();
514            end_loc >= next_loc
515        });
516
517        loop {
518            // See if we have the next operation to apply (i.e. at the journal tip).
519            // Find the index of the range that contains the next location.
520            let range_start_loc =
521                self.fetched_operations
522                    .iter()
523                    .find_map(|(range_start, range_ops)| {
524                        assert!(!range_ops.is_empty());
525                        let range_end =
526                            range_start.checked_add(range_ops.len() as u64 - 1).unwrap();
527                        if *range_start <= next_loc && next_loc <= range_end {
528                            Some(*range_start)
529                        } else {
530                            None
531                        }
532                    });
533
534            let Some(range_start_loc) = range_start_loc else {
535                // We don't have the next operation to apply (i.e. at the journal tip)
536                break;
537            };
538
539            // Remove the batch of operations that contains the next operation to apply.
540            let mut operations = self.fetched_operations.remove(&range_start_loc).unwrap();
541            assert!(!operations.is_empty());
542            // Skip operations that are before the next location. The containment check when
543            // selecting the range (`next_loc <= range_end`) guarantees at least one operation
544            // at or after it, so the batch is never empty.
545            operations.drain(..(next_loc - *range_start_loc) as usize);
546            next_loc += operations.len() as u64;
547            self.journal = self.journal.append(operations).await?;
548        }
549
550        Ok(self)
551    }
552
553    /// Check if sync is complete based on the current journal size and target
554    fn is_at_target(&self) -> Result<bool, Error<DB, S>> {
555        let journal_size = self.journal.size();
556        let target_journal_size = self.target.range.end();
557
558        // Check if we've completed sync
559        if journal_size >= target_journal_size {
560            if journal_size > target_journal_size {
561                // This shouldn't happen in normal operation - indicates a bug
562                return Err(SyncError::Engine(EngineError::InvalidState));
563            }
564            return Ok(true);
565        }
566
567        Ok(false)
568    }
569
570    /// Returns whether this target needs pinned nodes to reconstruct pruned state.
571    fn needs_pinned_nodes(&self) -> bool {
572        self.target.range.start() > Location::new(0)
573    }
574
575    /// Returns whether pinned nodes are present or not needed by this target.
576    fn pinned_nodes_ready(&self) -> bool {
577        !self.needs_pinned_nodes() || self.pinned_nodes.is_some()
578    }
579
580    /// Returns whether the journal and pinned nodes are both ready for completion.
581    fn is_ready_to_complete(&self) -> Result<bool, Error<DB, S>> {
582        Ok(self.is_at_target()? && self.pinned_nodes_ready())
583    }
584
585    /// Handle the result of a fetch operation.
586    fn handle_fetch_result(
587        &mut self,
588        fetch_result: IndexedFetchResult<DB::Family, DB::Op, DB::Digest, S::Error>,
589    ) -> Result<(), Error<DB, S>> {
590        // A target update can retire a request before its completed result is handled.
591        let Some(request) = self.outstanding_requests.remove(fetch_result.id) else {
592            return Ok(());
593        };
594
595        let response = fetch_result
596            .result
597            .map_err(SyncError::Source)?
598            .ok_or(SyncError::Engine(EngineError::InvalidResponse))?;
599
600        let start_loc = request.start();
601        match response {
602            Response::Operations { operations, .. } => {
603                self.store_operations(start_loc, operations);
604            }
605            Response::Boundary {
606                op, pinned_nodes, ..
607            } => {
608                // A tracked boundary request is at the current lower bound. A fetched batch
609                // there already holds its operation.
610                self.pinned_nodes = Some(pinned_nodes);
611                self.fetched_operations
612                    .entry(start_loc)
613                    .or_insert_with(|| vec![op]);
614            }
615        }
616
617        Ok(())
618    }
619
620    /// Handle a sync event and return the next engine state.
621    async fn handle_event(
622        mut self,
623        event: Event<DB::Family, DB::Op, DB::Digest, S::Error>,
624    ) -> Result<NextStep<Self, DB>, Error<DB, S>> {
625        match event {
626            Event::TargetUpdate(new_target) => {
627                // A non-advancing update is discarded.
628                if !new_target.advances(&self.target) {
629                    return Ok(NextStep::Continue(self));
630                }
631                // A same-root update that advances is impossible for an append-only log and
632                // indicates a caller bug.
633                if new_target.root == self.target.root {
634                    return Err(SyncError::Engine(EngineError::SyncTargetRootUnchanged));
635                }
636
637                let mut updated_self = self.reset_for_target_update(new_target).await?;
638                updated_self.record_progress();
639                updated_self.schedule_requests();
640                Ok(NextStep::Continue(updated_self))
641            }
642            Event::UpdateChannelClosed => {
643                self.update_rx = None;
644                Ok(NextStep::Continue(self))
645            }
646            Event::FinishRequested => {
647                self.finish_rx = None;
648                self.finish_requested = true;
649                Ok(NextStep::Continue(self))
650            }
651            Event::FinishChannelClosed => Err(SyncError::Engine(EngineError::FinishChannelClosed)),
652            Event::BatchReceived(fetch_result) => {
653                // An aborted request carries no result, but still wakes the loop to reschedule.
654                if let Ok(fetch_result) = fetch_result {
655                    self.handle_fetch_result(fetch_result)?;
656                }
657                self.schedule_requests();
658                let mut engine = self.apply_operations().await?;
659                engine.record_progress();
660                Ok(NextStep::Continue(engine))
661            }
662        }
663    }
664
665    /// Execute one step of the synchronization process.
666    ///
667    /// This is the main coordination method that:
668    /// 1. Checks if sync is complete
669    /// 2. Waits for the next synchronization event
670    /// 3. Handles different event types (target updates, fetch results)
671    /// 4. Coordinates request scheduling and operation application
672    ///
673    /// Returns `NextStep::Complete(database)` when sync is finished, or
674    /// `NextStep::Continue(self)` when more work remains.
675    #[boxed]
676    pub(crate) async fn step(mut self) -> Result<NextStep<Self, DB>, Error<DB, S>> {
677        self.drain_finish_requests()?;
678
679        // Check if sync is complete
680        if self.is_ready_to_complete()? {
681            // Report the reached target before a queued update can replace it.
682            self.report_reached_target();
683
684            // Take a queued target update before completing at the old target, unless the
685            // caller already asked to finish. Updates that do not advance the target are
686            // discarded.
687            if !self.finish_requested {
688                while let Some(update_rx) = self.update_rx.as_mut() {
689                    match update_rx.try_recv() {
690                        Ok(new_target) => {
691                            if new_target.advances(&self.target) {
692                                return self.handle_event(Event::TargetUpdate(new_target)).await;
693                            }
694                        }
695                        Err(ring::TryRecvError::Empty) => break,
696                        Err(ring::TryRecvError::Disconnected) => {
697                            self.update_rx = None;
698                        }
699                    }
700                }
701            }
702
703            if self.finish_rx.is_some() {
704                let event = wait_for_event(
705                    &mut self.update_rx,
706                    &mut self.finish_rx,
707                    &mut self.outstanding_requests,
708                )
709                .await
710                .ok_or(SyncError::Engine(EngineError::SyncStalled))?;
711                return self.handle_event(event).await;
712            }
713
714            return Ok(NextStep::Complete(self.complete().await?));
715        }
716
717        // Wait for the next synchronization event
718        let event = wait_for_event(
719            &mut self.update_rx,
720            &mut self.finish_rx,
721            &mut self.outstanding_requests,
722        )
723        .await
724        .ok_or(SyncError::Engine(EngineError::SyncStalled))?;
725        self.handle_event(event).await
726    }
727
728    /// Build the final database from the completed sync and verify its root against the
729    /// target.
730    async fn complete(mut self) -> Result<DB, Error<DB, S>> {
731        self.journal = self.journal.sync().await?;
732
733        let database = DB::from_sync_result(
734            self.context,
735            self.config,
736            self.journal,
737            self.pinned_nodes,
738            self.target.range.clone(),
739            self.apply_batch_size,
740        )
741        .await?;
742
743        let got_root = database.root();
744        let expected_root = self.target.root;
745        if got_root != expected_root {
746            return Err(SyncError::Engine(EngineError::RootMismatch {
747                expected: expected_root,
748                actual: got_root,
749            }));
750        }
751
752        Ok(database.persist_sync_result().await?)
753    }
754
755    /// Run sync to completion, returning the final database when done.
756    ///
757    /// This method repeatedly calls `step()` until sync is complete. The `step()` method
758    /// handles building the final database and verifying the root digest.
759    pub async fn sync(mut self) -> Result<DB, Error<DB, S>> {
760        // Run sync loop until completion
761        loop {
762            match self.step().await? {
763                NextStep::Continue(new_engine) => self = new_engine,
764                NextStep::Complete(database) => return Ok(database),
765            }
766        }
767    }
768}
769
770#[cfg(test)]
771pub(crate) mod tests {
772    use super::*;
773    use crate::{
774        merkle::{Proof, mmr::Family as MmrFamily},
775        qmdb::sync::{
776            harness::{DbOf, OpOf, SyncTestHarness},
777            source::{self, tests::FailSource},
778        },
779    };
780    use commonware_cryptography::{Sha256, sha256};
781    use commonware_runtime::{Runner as _, deterministic};
782    use commonware_utils::{NZU64, NZUsize, non_empty_range};
783    use std::{
784        convert::Infallible,
785        sync::{
786            Arc,
787            atomic::{AtomicUsize, Ordering},
788        },
789    };
790
791    #[derive(Clone)]
792    struct TestConfig {
793        journal_size: u64,
794        pinned_node_probes: Arc<AtomicUsize>,
795    }
796
797    impl crate::qmdb::sync::DatabaseConfig for TestConfig {
798        type JournalConfig = u64;
799
800        fn journal_config(&self) -> Self::JournalConfig {
801            self.journal_size
802        }
803    }
804
805    struct TestJournal {
806        size: u64,
807    }
808
809    impl Journal<MmrFamily> for TestJournal {
810        type Config = u64;
811        type Context = deterministic::Context;
812        type Error = crate::journal::Error;
813        type Op = i32;
814
815        async fn new(
816            _context: Self::Context,
817            size: Self::Config,
818            _range: commonware_utils::range::NonEmptyRange<Location<MmrFamily>>,
819        ) -> Result<Self, Self::Error> {
820            Ok(Self { size })
821        }
822
823        async fn resize(mut self, start: Location<MmrFamily>) -> Result<Self, Self::Error> {
824            self.size = *start;
825            Ok(self)
826        }
827
828        async fn sync(self) -> Result<Self, Self::Error> {
829            Ok(self)
830        }
831
832        fn size(&self) -> u64 {
833            self.size
834        }
835
836        async fn append(mut self, ops: Vec<Self::Op>) -> Result<Self, Self::Error> {
837            self.size += ops.len() as u64;
838            Ok(self)
839        }
840    }
841
842    struct TestDb;
843
844    impl Database for TestDb {
845        type Config = TestConfig;
846        type Context = deterministic::Context;
847        type Digest = sha256::Digest;
848        type Family = MmrFamily;
849        type Hasher = Sha256;
850        type Journal = TestJournal;
851        type Op = i32;
852
853        async fn from_sync_result(
854            _context: Self::Context,
855            _config: Self::Config,
856            _journal: Self::Journal,
857            _pinned_nodes: Option<Vec<Self::Digest>>,
858            _range: commonware_utils::range::NonEmptyRange<Location<Self::Family>>,
859            _apply_batch_size: NonZeroU64,
860        ) -> Result<Self, qmdb::Error<Self::Family>> {
861            Ok(Self)
862        }
863
864        async fn persist_sync_result(self) -> Result<Self, qmdb::Error<Self::Family>> {
865            Ok(self)
866        }
867
868        async fn local_pinned_nodes(
869            _context: Self::Context,
870            config: &Self::Config,
871            _target: &Target<Self::Family, Self::Digest>,
872            _journal: &Self::Journal,
873        ) -> Result<Option<Vec<Self::Digest>>, qmdb::Error<Self::Family>> {
874            config.pinned_node_probes.fetch_add(1, Ordering::SeqCst);
875            Ok(Some(vec![]))
876        }
877
878        fn root(&self) -> Self::Digest {
879            sha256::Digest::from([0u8; 32])
880        }
881    }
882
883    #[derive(Clone)]
884    struct TestSource;
885
886    impl Source for TestSource {
887        type Digest = sha256::Digest;
888        type Error = Infallible;
889        type Family = MmrFamily;
890        type Op = i32;
891
892        async fn serve(&self, _request: Request<MmrFamily>) -> source::Result<Self> {
893            Ok((
894                Response::Operations {
895                    proof: Proof {
896                        leaves: Location::new(0),
897                        inactive_peaks: 0,
898                        digests: vec![],
899                    },
900                    operations: vec![],
901                },
902                None,
903            ))
904        }
905    }
906
907    fn test_engine_config(
908        context: deterministic::Context,
909        journal_size: u64,
910        pinned_node_probes: Arc<AtomicUsize>,
911    ) -> Config<TestDb, TestSource> {
912        Config {
913            context,
914            source: TestSource,
915            target: Target {
916                root: sha256::Digest::from([1u8; 32]),
917                range: non_empty_range!(Location::new(5), Location::new(10)),
918            },
919            max_outstanding_requests: NZUsize!(1),
920            fetch_batch_size: NZU64!(1),
921            apply_batch_size: NZU64!(1),
922            db_config: TestConfig {
923                journal_size,
924                pinned_node_probes,
925            },
926            update_rx: None,
927            finish_rx: None,
928            reached_target_tx: None,
929        }
930    }
931
932    async fn retention_engine<H: SyncTestHarness>(
933        context: deterministic::Context,
934    ) -> Engine<DbOf<H>, FailSource<H::Family, OpOf<H>, sha256::Digest>>
935    where
936        OpOf<H>: 'static,
937    {
938        // Source requests stay unpolled; completed fetch results are injected directly.
939        let config = Config {
940            db_config: H::config("retention"),
941            context,
942            source: FailSource::new(),
943            target: Target {
944                root: sha256::Digest::from([1u8; 32]),
945                range: non_empty_range!(Location::new(5), Location::new(10)),
946            },
947            max_outstanding_requests: NZUsize!(1),
948            fetch_batch_size: NZU64!(1),
949            apply_batch_size: NZU64!(1),
950            update_rx: None,
951            finish_rx: None,
952            reached_target_tx: None,
953        };
954        Engine::new(config).await.unwrap()
955    }
956
957    fn insert_pending_request<DB, S>(
958        engine: &mut Engine<DB, S>,
959        request: Request<DB::Family>,
960    ) -> RequestId
961    where
962        DB: Database,
963        S: SourceFor<DB>,
964        DB::Op: 'static,
965    {
966        engine
967            .outstanding_requests
968            .insert(request, |_| std::future::pending())
969    }
970
971    fn late_fetch_result<F: Family, Op>(
972        id: RequestId,
973        operation: Op,
974    ) -> IndexedFetchResult<F, Op, sha256::Digest, qmdb::Error<F>> {
975        IndexedFetchResult {
976            id,
977            result: Ok(Some(Response::Operations {
978                proof: Proof {
979                    leaves: Location::new(10),
980                    inactive_peaks: 0,
981                    digests: vec![],
982                },
983                operations: vec![operation],
984            })),
985        }
986    }
987
988    /// A boundary response keeps a fetched batch that starts at the lower bound.
989    pub(crate) fn test_boundary_response_keeps_fetched_batch_at_lower_bound<H: SyncTestHarness>()
990    where
991        OpOf<H>: Clone + PartialEq + Debug + 'static,
992    {
993        deterministic::Runner::default().start(|context| async move {
994            let mut engine = retention_engine::<H>(context).await;
995            let operations = H::create_ops(3);
996            engine
997                .fetched_operations
998                .insert(Location::new(6), operations.clone());
999
1000            // Move the lower bound to the start of the fetched batch.
1001            let next = Target {
1002                root: sha256::Digest::from([2; 32]),
1003                range: non_empty_range!(Location::new(6), Location::new(12)),
1004            };
1005            let mut engine = engine.reset_for_target_update(next).await.unwrap();
1006            assert!(engine.pinned_nodes.is_none());
1007
1008            // The boundary response sets pinned nodes without replacing the batch.
1009            let id = insert_pending_request(
1010                &mut engine,
1011                Request::Boundary {
1012                    size: Location::new(12),
1013                    start: Location::new(6),
1014                },
1015            );
1016            let pinned = vec![sha256::Digest::from([7; 32])];
1017            engine
1018                .handle_fetch_result(IndexedFetchResult {
1019                    id,
1020                    result: Ok(Some(Response::Boundary {
1021                        proof: Proof {
1022                            leaves: Location::new(12),
1023                            inactive_peaks: 0,
1024                            digests: vec![],
1025                        },
1026                        op: operations[0].clone(),
1027                        pinned_nodes: pinned.clone(),
1028                    })),
1029                })
1030                .unwrap();
1031            assert_eq!(engine.pinned_nodes, Some(pinned));
1032            assert_eq!(
1033                engine.fetched_operations.get(&Location::new(6)),
1034                Some(&operations)
1035            );
1036
1037            // Applying moves the journal past the whole batch.
1038            let engine = engine.apply_operations().await.unwrap();
1039            assert_eq!(engine.journal.size(), 9);
1040        });
1041    }
1042
1043    /// Moving the lower bound past an operation request cancels it with the old boundary and
1044    /// drops its queued result.
1045    pub(crate) fn test_target_update_drops_queued_result_of_cancelled_request<H: SyncTestHarness>()
1046    where
1047        OpOf<H>: 'static,
1048    {
1049        deterministic::Runner::default().start(|context| async move {
1050            let mut engine = retention_engine::<H>(context).await;
1051
1052            // Track the boundary request and an operation request below the next lower bound.
1053            assert!(engine.outstanding_requests.contains(&Location::new(5)));
1054            let request = Request::Operations {
1055                size: Location::new(10),
1056                start: Location::new(6),
1057                max_ops: NZU64!(1),
1058            };
1059            let old_id = insert_pending_request(&mut engine, request);
1060            let queued_result = late_fetch_result(old_id, H::create_ops(1).pop().unwrap());
1061            assert_eq!(engine.outstanding_requests.len(), 2);
1062
1063            // Moving the lower bound beyond both starts cancels both requests.
1064            let new_target = Target {
1065                root: sha256::Digest::from([2u8; 32]),
1066                range: non_empty_range!(Location::new(7), Location::new(12)),
1067            };
1068            let mut engine = engine.reset_for_target_update(new_target).await.unwrap();
1069            assert_eq!(engine.outstanding_requests.len(), 0);
1070            assert!(engine.outstanding_requests.remove(old_id).is_none());
1071
1072            // The queued result of the cancelled request is ignored.
1073            engine.handle_fetch_result(queued_result).unwrap();
1074            assert!(engine.fetched_operations.is_empty());
1075            assert!(engine.pinned_nodes.is_none());
1076            assert!(matches!(
1077                engine.outstanding_requests.next_completed().await,
1078                Err(Aborted)
1079            ));
1080            assert!(matches!(
1081                engine.outstanding_requests.next_completed().await,
1082                Err(Aborted)
1083            ));
1084            assert_eq!(engine.outstanding_requests.len(), 0);
1085        });
1086    }
1087
1088    /// An operation request at an old size survives any number of target updates while its
1089    /// start is beyond the lower bound, and its late response is stored. The boundary request
1090    /// survives updates that keep the lower bound.
1091    pub(crate) fn test_target_updates_keep_old_size_request_beyond_floor<H: SyncTestHarness>()
1092    where
1093        OpOf<H>: Clone + PartialEq + Debug + 'static,
1094    {
1095        deterministic::Runner::default().start(|context| async move {
1096            let mut engine = retention_engine::<H>(context).await;
1097
1098            // Track a pending boundary request and an operation request at the first target
1099            // size.
1100            assert!(engine.outstanding_requests.contains(&Location::new(5)));
1101            insert_pending_request(
1102                &mut engine,
1103                Request::Boundary {
1104                    size: Location::new(10),
1105                    start: Location::new(5),
1106                },
1107            );
1108            let request = Request::Operations {
1109                size: Location::new(10),
1110                start: Location::new(8),
1111                max_ops: NZU64!(1),
1112            };
1113            let id = insert_pending_request(&mut engine, request);
1114            assert_eq!(engine.outstanding_requests.len(), 2);
1115
1116            // Updates with an unchanged lower bound keep both requests.
1117            let mut root = 2u8;
1118            for end in 11..21 {
1119                let next = Target {
1120                    root: sha256::Digest::from([root; 32]),
1121                    range: non_empty_range!(Location::new(5), Location::new(end)),
1122                };
1123                root += 1;
1124                engine = engine.reset_for_target_update(next).await.unwrap();
1125                assert!(engine.outstanding_requests.contains(&Location::new(5)));
1126                assert!(engine.outstanding_requests.contains(&Location::new(8)));
1127                assert_eq!(engine.outstanding_requests.len(), 2);
1128            }
1129
1130            // Updates that move the lower bound below the request start cancel the boundary
1131            // and keep the operation request.
1132            for (start, end) in [(6, 22), (7, 24)] {
1133                let next = Target {
1134                    root: sha256::Digest::from([root; 32]),
1135                    range: non_empty_range!(Location::new(start), Location::new(end)),
1136                };
1137                root += 1;
1138                engine = engine.reset_for_target_update(next).await.unwrap();
1139                assert!(engine.outstanding_requests.contains(&Location::new(8)));
1140                assert_eq!(engine.outstanding_requests.len(), 1);
1141            }
1142
1143            // The late response at the first target size is stored at its start.
1144            let operation = H::create_ops(1).pop().unwrap();
1145            engine
1146                .handle_fetch_result(late_fetch_result(id, operation.clone()))
1147                .unwrap();
1148            assert_eq!(engine.outstanding_requests.len(), 0);
1149            assert_eq!(
1150                engine.fetched_operations.get(&Location::new(8)),
1151                Some(&vec![operation])
1152            );
1153        });
1154    }
1155
1156    /// Moving the lower bound cancels an operation request whose range ends at the new bound and
1157    /// keeps one beyond it.
1158    pub(crate) fn test_target_update_floor_move_cancels_operations_below_bound<H: SyncTestHarness>()
1159    where
1160        OpOf<H>: 'static,
1161    {
1162        deterministic::Runner::default().start(|context| async move {
1163            let mut engine = retention_engine::<H>(context).await;
1164
1165            // Track operation requests below and beyond the next lower bound.
1166            let below = Request::Operations {
1167                size: Location::new(10),
1168                start: Location::new(6),
1169                max_ops: NZU64!(2),
1170            };
1171            insert_pending_request(&mut engine, below);
1172            let beyond = Request::Operations {
1173                size: Location::new(10),
1174                start: Location::new(9),
1175                max_ops: NZU64!(1),
1176            };
1177            insert_pending_request(&mut engine, beyond);
1178            assert_eq!(engine.outstanding_requests.len(), 3);
1179
1180            // Moving the lower bound to the end of the first request cancels it and the old
1181            // boundary. The request beyond the bound survives.
1182            let next = Target {
1183                root: sha256::Digest::from([2; 32]),
1184                range: non_empty_range!(Location::new(8), Location::new(14)),
1185            };
1186            let engine = engine.reset_for_target_update(next).await.unwrap();
1187            assert!(!engine.outstanding_requests.contains(&Location::new(5)));
1188            assert!(!engine.outstanding_requests.contains(&Location::new(6)));
1189            assert!(engine.outstanding_requests.contains(&Location::new(9)));
1190            assert_eq!(engine.outstanding_requests.len(), 1);
1191        });
1192    }
1193
1194    /// Moving the lower bound to the start of an operation request cancels it and the old
1195    /// boundary request, and scheduling issues the boundary request at the new lower bound.
1196    pub(crate) fn test_moved_floor_schedules_boundary_without_waiting_for_old_operation<
1197        H: SyncTestHarness,
1198    >()
1199    where
1200        OpOf<H>: 'static,
1201    {
1202        deterministic::Runner::default().start(|context| async move {
1203            let mut engine = retention_engine::<H>(context).await;
1204
1205            // Track the boundary request and an operation request at the next lower bound.
1206            let operation_id = insert_pending_request(
1207                &mut engine,
1208                Request::Operations {
1209                    size: Location::new(10),
1210                    start: Location::new(6),
1211                    max_ops: NZU64!(1),
1212                },
1213            );
1214            assert!(engine.outstanding_requests.contains(&Location::new(5)));
1215
1216            // Move the lower bound to the start of the operation request.
1217            let next = Target {
1218                root: sha256::Digest::from([2; 32]),
1219                range: non_empty_range!(Location::new(6), Location::new(12)),
1220            };
1221            let mut engine = engine.reset_for_target_update(next).await.unwrap();
1222            engine.schedule_requests();
1223
1224            // Both requests are cancelled and the boundary request at the new lower bound is
1225            // the only one outstanding.
1226            assert!(engine.outstanding_requests.remove(operation_id).is_none());
1227            assert!(!engine.outstanding_requests.contains(&Location::new(5)));
1228            assert!(engine.outstanding_requests.contains(&Location::new(6)));
1229            assert_eq!(engine.outstanding_requests.len(), 1);
1230        });
1231    }
1232
1233    /// Target updates keep fetched operations and keep pinned nodes only while the lower bound is
1234    /// unchanged. Once the lower bound moves, applying drops batches below it and trims a batch
1235    /// that straddles it.
1236    #[test]
1237    fn target_updates_keep_operations_and_reset_pins_only_when_floor_moves() {
1238        deterministic::Runner::default().start(|context| async move {
1239            let mut engine = Engine::new(test_engine_config(
1240                context,
1241                5,
1242                Arc::new(AtomicUsize::new(0)),
1243            ))
1244            .await
1245            .unwrap();
1246
1247            // Hold pinned nodes and two batches fetched ahead of the journal tip.
1248            let fetched = BTreeMap::from([
1249                (Location::new(6), vec![1, 2]),
1250                (Location::new(8), vec![3, 4]),
1251            ]);
1252            engine.fetched_operations = fetched.clone();
1253            let pinned = vec![sha256::Digest::from([7; 32])];
1254            engine.pinned_nodes = Some(pinned.clone());
1255
1256            // An update with an unchanged lower bound keeps operations and pinned nodes.
1257            let next = Target {
1258                root: sha256::Digest::from([2; 32]),
1259                range: non_empty_range!(Location::new(5), Location::new(12)),
1260            };
1261            let engine = engine.reset_for_target_update(next).await.unwrap();
1262            assert_eq!(engine.fetched_operations, fetched);
1263            assert_eq!(engine.pinned_nodes, Some(pinned));
1264
1265            // An update that moves the lower bound keeps operations and clears pinned nodes.
1266            let next = Target {
1267                root: sha256::Digest::from([3; 32]),
1268                range: non_empty_range!(Location::new(9), Location::new(14)),
1269            };
1270            let engine = engine.reset_for_target_update(next).await.unwrap();
1271            assert_eq!(engine.fetched_operations, fetched);
1272            assert!(engine.pinned_nodes.is_none());
1273
1274            // Applying drops the batch below the new lower bound and appends the operation of
1275            // the straddling batch at it.
1276            let engine = engine.apply_operations().await.unwrap();
1277            assert_eq!(engine.journal.size(), 10);
1278            assert!(engine.fetched_operations.is_empty());
1279        });
1280    }
1281
1282    #[test]
1283    fn new_probes_local_pinned_nodes_when_journal_reaches_target() {
1284        deterministic::Runner::default().start(|context| async move {
1285            let pinned_node_probes = Arc::new(AtomicUsize::new(0));
1286            Engine::new(test_engine_config(context, 10, pinned_node_probes.clone()))
1287                .await
1288                .unwrap();
1289
1290            assert_eq!(pinned_node_probes.load(Ordering::SeqCst), 1);
1291        });
1292    }
1293
1294    #[test]
1295    fn new_skips_local_pinned_nodes_when_journal_is_partial() {
1296        deterministic::Runner::default().start(|context| async move {
1297            let pinned_node_probes = Arc::new(AtomicUsize::new(0));
1298            Engine::new(test_engine_config(context, 7, pinned_node_probes.clone()))
1299                .await
1300                .unwrap();
1301
1302            assert_eq!(pinned_node_probes.load(Ordering::SeqCst), 0);
1303        });
1304    }
1305
1306    #[test]
1307    fn new_schedules_operations_after_boundary_request() {
1308        deterministic::Runner::default().start(|context| async move {
1309            let mut config = test_engine_config(context, 5, Arc::new(AtomicUsize::new(0)));
1310            config.max_outstanding_requests = NZUsize!(2);
1311            config.fetch_batch_size = NZU64!(5);
1312
1313            let engine = Engine::new(config).await.unwrap();
1314            let requests = &engine.outstanding_requests;
1315
1316            assert_eq!(requests.len(), 2);
1317            assert!(requests.contains(&Location::new(5)));
1318            assert!(requests.contains(&Location::new(6)));
1319        });
1320    }
1321
1322    #[test]
1323    fn step_takes_queued_update_before_completing() {
1324        deterministic::Runner::default().start(|context| async move {
1325            let (update_tx, update_rx) = ring::channel(NZUsize!(2));
1326            let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1327            config.update_rx = Some(update_rx);
1328            // Queue a stale update and an advancing one. The stale one is discarded and
1329            // the advancing one retargets the engine instead of completing.
1330            let stale = Target {
1331                root: sha256::Digest::from([2u8; 32]),
1332                range: non_empty_range!(Location::new(5), Location::new(10)),
1333            };
1334            let advancing = Target {
1335                root: sha256::Digest::from([3u8; 32]),
1336                range: non_empty_range!(Location::new(5), Location::new(12)),
1337            };
1338            assert!(update_tx.send_lossy(stale));
1339            assert!(update_tx.send_lossy(advancing.clone()));
1340
1341            let engine = Engine::new(config).await.unwrap();
1342            let NextStep::Continue(engine) = engine.step().await.unwrap() else {
1343                panic!("engine should retarget instead of completing");
1344            };
1345            assert_eq!(engine.target, advancing);
1346        });
1347    }
1348
1349    #[test]
1350    fn step_completes_at_current_target_after_finish() {
1351        deterministic::Runner::default().start(|context| async move {
1352            let (update_tx, update_rx) = ring::channel(NZUsize!(1));
1353            let (finish_tx, finish_rx) = mpsc::channel(1);
1354            let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1355            // TestDb's root, so completion's final check passes.
1356            config.target.root = sha256::Digest::from([0u8; 32]);
1357            config.update_rx = Some(update_rx);
1358            config.finish_rx = Some(finish_rx);
1359            let advancing = Target {
1360                root: sha256::Digest::from([3u8; 32]),
1361                range: non_empty_range!(Location::new(5), Location::new(12)),
1362            };
1363            assert!(update_tx.send_lossy(advancing));
1364            finish_tx.send(()).await.unwrap();
1365
1366            let engine = Engine::new(config).await.unwrap();
1367            let NextStep::Complete(_) = engine.step().await.unwrap() else {
1368                panic!("a requested finish must win over a queued update");
1369            };
1370        });
1371    }
1372
1373    /// A no-op fetch result for testing request tracking.
1374    fn dummy_result(id: RequestId) -> IndexedFetchResult<MmrFamily, i32, sha256::Digest, ()> {
1375        IndexedFetchResult {
1376            id,
1377            result: Ok(Some(Response::Operations {
1378                proof: Proof {
1379                    leaves: Location::new(0),
1380                    inactive_peaks: 0,
1381                    digests: vec![],
1382                },
1383                operations: vec![],
1384            })),
1385        }
1386    }
1387
1388    /// Helper to add a request at a given location.
1389    fn add(requests: &mut Requests<MmrFamily, i32, sha256::Digest, ()>, loc: u64) -> RequestId {
1390        requests.insert(
1391            Request::Operations {
1392                size: Location::new(loc),
1393                start: Location::new(loc),
1394                max_ops: NZU64!(1),
1395            },
1396            |id| std::future::ready(dummy_result(id)),
1397        )
1398    }
1399
1400    #[test]
1401    fn test_add_and_remove() {
1402        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1403        assert_eq!(requests.len(), 0);
1404
1405        let id = add(&mut requests, 10);
1406        assert_eq!(requests.len(), 1);
1407        assert!(requests.contains(&Location::new(10)));
1408
1409        assert!(requests.remove(id).is_some());
1410        assert!(!requests.contains(&Location::new(10)));
1411        assert!(requests.remove(id).is_none());
1412    }
1413
1414    #[test]
1415    fn test_retain_matching() {
1416        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1417
1418        add(&mut requests, 5);
1419        add(&mut requests, 10);
1420        add(&mut requests, 15);
1421        add(&mut requests, 20);
1422        assert_eq!(requests.len(), 4);
1423
1424        requests.retain(|request| request.start() >= Location::new(10));
1425        assert_eq!(requests.len(), 3);
1426        assert!(!requests.contains(&Location::new(5)));
1427        assert!(requests.contains(&Location::new(10)));
1428        assert!(requests.contains(&Location::new(15)));
1429        assert!(requests.contains(&Location::new(20)));
1430    }
1431
1432    #[test]
1433    fn test_retain_none() {
1434        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1435
1436        add(&mut requests, 5);
1437        add(&mut requests, 10);
1438        assert_eq!(requests.len(), 2);
1439
1440        requests.retain(|_| false);
1441        assert_eq!(requests.len(), 0);
1442    }
1443
1444    #[test]
1445    fn test_retain_empty() {
1446        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1447        requests.retain(|_| true);
1448        assert_eq!(requests.len(), 0);
1449    }
1450
1451    #[test]
1452    fn test_retain_all() {
1453        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1454
1455        add(&mut requests, 10);
1456        add(&mut requests, 20);
1457        assert_eq!(requests.len(), 2);
1458
1459        requests.retain(|_| true);
1460        assert_eq!(requests.len(), 2);
1461        assert!(requests.contains(&Location::new(10)));
1462        assert!(requests.contains(&Location::new(20)));
1463    }
1464
1465    #[test]
1466    fn test_superseded_request() {
1467        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1468
1469        // Old request at location 10
1470        let old_id = add(&mut requests, 10);
1471        assert_eq!(requests.len(), 1);
1472
1473        // New request supersedes at same location
1474        let new_id = add(&mut requests, 10);
1475        assert_eq!(requests.len(), 1);
1476
1477        // Old ID is no longer tracked (superseded by insert)
1478        assert!(requests.remove(old_id).is_none());
1479
1480        // New ID is still tracked and by_location is intact
1481        assert!(requests.contains(&Location::new(10)));
1482        assert!(requests.remove(new_id).is_some());
1483        assert!(!requests.contains(&Location::new(10)));
1484    }
1485
1486    #[test]
1487    fn test_stale_completion_after_retain() {
1488        let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1489
1490        let old_id = add(&mut requests, 5);
1491        let queued_result = dummy_result(old_id);
1492        add(&mut requests, 15);
1493        requests.retain(|request| request.start() >= Location::new(10));
1494
1495        // The queued completion no longer resolves to a tracked request.
1496        assert!(requests.remove(queued_result.id).is_none());
1497
1498        // New request at the same location gets a different ID
1499        let new_id = add(&mut requests, 5);
1500        assert_ne!(old_id, new_id);
1501        assert!(requests.remove(new_id).is_some());
1502    }
1503
1504    #[test]
1505    fn test_retain_aborts_future() {
1506        deterministic::Runner::default().start(|_context| async move {
1507            let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1508            requests.insert(
1509                Request::Operations {
1510                    size: Location::new(5),
1511                    start: Location::new(5),
1512                    max_ops: NZU64!(1),
1513                },
1514                |_| std::future::pending(),
1515            );
1516            requests.retain(|_| false);
1517            assert!(matches!(requests.next_completed().await, Err(Aborted)));
1518        });
1519    }
1520
1521    #[test]
1522    fn reports_reached_target_before_queued_update() {
1523        deterministic::Runner::default().start(|context| async move {
1524            let (update_tx, update_rx) = ring::channel(NZUsize!(1));
1525            let (_finish_tx, finish_rx) = mpsc::channel(1);
1526            let (reached_tx, mut reached_rx) = ring::channel(NZUsize!(1));
1527            let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1528            config.update_rx = Some(update_rx);
1529            config.finish_rx = Some(finish_rx);
1530            config.reached_target_tx = Some(reached_tx);
1531            let original = config.target.clone();
1532
1533            // Queue an advancing target before the engine starts.
1534            let next = Target {
1535                root: sha256::Digest::from([2; 32]),
1536                range: non_empty_range!(Location::new(5), Location::new(12)),
1537            };
1538            assert!(update_tx.send_lossy(next.clone()));
1539
1540            // The journal already reaches the original target, so the first step reports it
1541            // before adopting the queued update.
1542            let engine = Engine::new(config).await.unwrap();
1543            let NextStep::Continue(engine) = engine.step().await.unwrap() else {
1544                panic!("the coordinator has not requested completion");
1545            };
1546            assert_eq!(
1547                reached_rx.try_recv().ok(),
1548                Some(original),
1549                "a queued update hid a reached target from the coordinator"
1550            );
1551            assert_eq!(engine.target, next);
1552        });
1553    }
1554
1555    #[test]
1556    fn reports_never_wait_for_the_observer() {
1557        deterministic::Runner::default().start(|context| async move {
1558            let (finish_tx, finish_rx) = mpsc::channel(1);
1559            let (reached_tx, mut reached_rx) = ring::channel(NZUsize!(1));
1560            let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1561            config.finish_rx = Some(finish_rx);
1562
1563            // Use TestDb's root so completion's final check passes.
1564            config.target.root = sha256::Digest::from([0; 32]);
1565            let original = config.target.clone();
1566
1567            // Fill the progress channel with a stale report that the observer never reads.
1568            let stale = Target {
1569                root: sha256::Digest::from([9; 32]),
1570                range: non_empty_range!(Location::new(5), Location::new(8)),
1571            };
1572            assert!(reached_tx.send_lossy(stale));
1573            config.reached_target_tx = Some(reached_tx);
1574
1575            // The step reports and completes without waiting, and its report replaces the stale
1576            // one.
1577            finish_tx.send(()).await.unwrap();
1578            let engine = Engine::new(config).await.unwrap();
1579            let NextStep::Complete(_) = engine.step().await.unwrap() else {
1580                panic!("a requested finish completes at the reached target");
1581            };
1582            assert_eq!(reached_rx.try_recv(), Ok(original));
1583        });
1584    }
1585
1586    #[test]
1587    fn step_adopts_newest_target_after_overflow() {
1588        deterministic::Runner::default().start(|context| async move {
1589            let (update_tx, update_rx) = ring::channel(NZUsize!(1));
1590            let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1591            config.update_rx = Some(update_rx);
1592
1593            // Send three advancing targets into a one-slot channel, which keeps only the newest.
1594            let targets = [12u64, 14, 16].map(|end| Target {
1595                root: sha256::Digest::from([end as u8; 32]),
1596                range: non_empty_range!(Location::new(5), Location::new(end)),
1597            });
1598            for target in targets.iter().cloned() {
1599                assert!(update_tx.send_lossy(target));
1600            }
1601
1602            // The journal already reaches the original target, so the step adopts the newest one.
1603            let engine = Engine::new(config).await.unwrap();
1604            let NextStep::Continue(engine) = engine.step().await.unwrap() else {
1605                panic!("engine should retarget instead of completing");
1606            };
1607            assert_eq!(engine.target, targets[2]);
1608        });
1609    }
1610}