1use crate::{
3 merkle::{Family, Location, hasher::Standard as StandardHasher},
4 qmdb::{
5 self,
6 sync::{
7 Database, Error as SyncError, Journal, Metrics, SourceFor, Target,
8 database::Config as _,
9 error::EngineError,
10 requests::{Id as RequestId, Requests},
11 source::{FeedbackTx, Request, Response, Source},
12 },
13 },
14};
15use commonware_codec::Encode;
16use commonware_cryptography::Digest;
17use commonware_macros::{boxed, select};
18use commonware_runtime::Supervisor as _;
19use commonware_utils::channel::{
20 fallible::{AsyncFallibleExt, OneshotExt as _},
21 mpsc,
22};
23use futures::future::{Aborted, Either, pending};
24use mpsc::error::TryRecvError;
25use std::{collections::BTreeMap, fmt::Debug, num::NonZeroU64, sync::Arc};
26
27type Error<DB, S> =
29 qmdb::sync::Error<<DB as Database>::Family, <S as Source>::Error, <DB as Database>::Digest>;
30
31#[derive(Debug)]
33pub(crate) enum NextStep<C, D> {
34 Continue(C),
36 Complete(D),
38}
39
40#[derive(Debug)]
42enum Event<F: Family, Op, D: Digest, E> {
43 TargetUpdate(Target<F, D>),
45 BatchReceived(Result<IndexedFetchResult<F, Op, D, E>, Aborted>),
47 UpdateChannelClosed,
49 FinishRequested,
51 FinishChannelClosed,
53}
54
55#[derive(Debug)]
57pub(super) struct IndexedFetchResult<F: Family, Op, D: Digest, E> {
58 pub id: RequestId,
60 pub result: Result<(Response<F, Op, D>, FeedbackTx), E>,
62}
63
64async fn wait_for_event<F: Family, Op: Send, D: Digest, E: Send>(
67 update_rx: &mut Option<mpsc::Receiver<Target<F, D>>>,
68 finish_rx: &mut Option<mpsc::Receiver<()>>,
69 outstanding_requests: &mut Requests<F, Op, D, E>,
70) -> Option<Event<F, Op, D, E>> {
71 if outstanding_requests.len() == 0 && update_rx.is_none() && finish_rx.is_none() {
72 return None;
73 }
74
75 let target_update_fut = update_rx.as_mut().map_or_else(
76 || Either::Right(pending()),
77 |update_rx| Either::Left(update_rx.recv()),
78 );
79 let finish_fut = finish_rx.as_mut().map_or_else(
80 || Either::Right(pending()),
81 |finish_rx| Either::Left(finish_rx.recv()),
82 );
83 let batch_result_fut = outstanding_requests.next_completed();
84
85 select! {
86 finish = finish_fut => finish.map_or_else(
87 || Some(Event::FinishChannelClosed),
88 |_| Some(Event::FinishRequested)
89 ),
90 target = target_update_fut => target.map_or_else(
91 || Some(Event::UpdateChannelClosed),
92 |target| Some(Event::TargetUpdate(target))
93 ),
94 result = batch_result_fut => Some(Event::BatchReceived(result)),
95 }
96}
97
98pub struct Config<DB, S>
100where
101 DB: Database,
102 S: SourceFor<DB>,
103 DB::Op: Encode,
104{
105 pub context: DB::Context,
107 pub source: S,
109 pub target: Target<DB::Family, DB::Digest>,
114 pub max_outstanding_requests: usize,
116 pub fetch_batch_size: NonZeroU64,
118 pub apply_batch_size: NonZeroU64,
120 pub db_config: DB::Config,
122 pub update_rx: Option<mpsc::Receiver<Target<DB::Family, DB::Digest>>>,
127 pub finish_rx: Option<mpsc::Receiver<()>>,
131 pub reached_target_tx: Option<mpsc::Sender<Target<DB::Family, DB::Digest>>>,
138 pub max_retained_roots: usize,
142}
143pub(crate) struct Engine<DB, S>
145where
146 DB: Database,
147 S: SourceFor<DB>,
148 DB::Op: Encode,
149{
150 outstanding_requests: Requests<DB::Family, DB::Op, DB::Digest, S::Error>,
152
153 fetched_operations: BTreeMap<Location<DB::Family>, Vec<DB::Op>>,
159
160 pinned_nodes: Option<Vec<DB::Digest>>,
162
163 retained_roots: BTreeMap<Location<DB::Family>, DB::Digest>,
170
171 max_retained_roots: usize,
173
174 target: Target<DB::Family, DB::Digest>,
176
177 max_outstanding_requests: usize,
179
180 fetch_batch_size: NonZeroU64,
182
183 apply_batch_size: NonZeroU64,
185
186 journal: DB::Journal,
188
189 source: Arc<S>,
191
192 hasher: StandardHasher<DB::Hasher>,
194
195 context: DB::Context,
197
198 config: DB::Config,
200
201 update_rx: Option<mpsc::Receiver<Target<DB::Family, DB::Digest>>>,
203
204 finish_requested: bool,
206
207 finish_rx: Option<mpsc::Receiver<()>>,
211
212 reached_target_tx: Option<mpsc::Sender<Target<DB::Family, DB::Digest>>>,
219
220 metrics: Metrics,
222
223 reached_current_target_reported: bool,
225}
226
227#[cfg(test)]
228impl<DB, S> Engine<DB, S>
229where
230 DB: Database,
231 S: SourceFor<DB>,
232 DB::Op: Encode,
233{
234 pub(crate) fn journal(&self) -> &DB::Journal {
235 &self.journal
236 }
237}
238
239impl<DB, S> Engine<DB, S>
240where
241 DB: Database,
242 S: SourceFor<DB>,
243 DB::Op: Encode,
244{
245 pub async fn new(config: Config<DB, S>) -> Result<Self, Error<DB, S>> {
246 if !config.target.range.end().is_valid() {
247 return Err(SyncError::Engine(EngineError::InvalidTarget {
248 lower_bound_pos: config.target.range.start(),
249 upper_bound_pos: config.target.range.end(),
250 }));
251 }
252
253 let journal = <DB::Journal as Journal<DB::Family>>::new(
255 config.context.child("journal"),
256 config.db_config.journal_config(),
257 config.target.range.clone(),
258 )
259 .await?;
260 let journal_size = journal.size();
261
262 let pinned_nodes = if journal_size == *config.target.range.end() {
267 DB::local_pinned_nodes(
268 config.context.child("local_pinned_nodes"),
269 &config.db_config,
270 &config.target,
271 &journal,
272 )
273 .await?
274 } else {
275 None
276 };
277
278 let sync_context = config.context.child("sync");
279 let metrics = Metrics::new(&sync_context);
280 let mut engine = Self {
281 outstanding_requests: Requests::new(),
282 fetched_operations: BTreeMap::new(),
283 pinned_nodes,
284 retained_roots: BTreeMap::new(),
285 max_retained_roots: config.max_retained_roots,
286 target: config.target.clone(),
287 max_outstanding_requests: config.max_outstanding_requests,
288 fetch_batch_size: config.fetch_batch_size,
289 apply_batch_size: config.apply_batch_size,
290 journal,
291 source: Arc::new(config.source),
292 hasher: qmdb::hasher::<DB::Hasher>(),
293 context: config.context,
294 config: config.db_config,
295 update_rx: config.update_rx,
296 finish_requested: false,
297 finish_rx: config.finish_rx,
298 reached_target_tx: config.reached_target_tx,
299 reached_current_target_reported: false,
300 metrics,
301 };
302 engine.schedule_requests()?;
303 engine.record_progress();
304 Ok(engine)
305 }
306
307 fn spawn_fetch(&mut self, request: Request<DB::Family>) {
309 let source = Arc::clone(&self.source);
310 self.outstanding_requests
311 .insert(request, move |id| async move {
312 let result = source.serve(request).await;
313 IndexedFetchResult { id, result }
314 });
315 }
316
317 fn schedule_requests(&mut self) -> Result<(), Error<DB, S>> {
319 let target_size = self.target.range.end();
320
321 if !self.pinned_nodes_ready()
325 && !self
326 .outstanding_requests
327 .contains(&self.target.range.start())
328 {
329 let request = Request::Boundary {
330 size: target_size,
331 start: self.target.range.start(),
332 };
333 self.spawn_fetch(request);
334 }
335
336 let num_requests = self
338 .max_outstanding_requests
339 .saturating_sub(self.outstanding_requests.len());
340
341 let log_size = self.journal.size();
342
343 for _ in 0..num_requests {
344 let operation_counts: BTreeMap<Location<DB::Family>, u64> = self
346 .fetched_operations
347 .iter()
348 .map(|(&start_loc, operations)| (start_loc, operations.len() as u64))
349 .collect();
350
351 let Some(gap_range) = crate::qmdb::sync::gaps::find_next(
353 Location::new(log_size)..self.target.range.end(),
354 &operation_counts,
355 self.outstanding_requests.ranges(),
356 ) else {
357 break; };
359
360 let gap_size = *gap_range.end.checked_sub(*gap_range.start).unwrap();
362 let gap_size: NonZeroU64 = gap_size.try_into().unwrap();
363 let batch_size = self.fetch_batch_size.min(gap_size);
364
365 let request = Request::Operations {
367 size: target_size,
368 start: gap_range.start,
369 max_ops: batch_size,
370 };
371 self.spawn_fetch(request);
372 }
373
374 Ok(())
375 }
376
377 pub async fn reset_for_target_update(
384 mut self,
385 new_target: Target<DB::Family, DB::Digest>,
386 ) -> Result<Self, Error<DB, S>> {
387 self.journal = self.journal.resize(new_target.range.start()).await?;
388 self.outstanding_requests
391 .remove_before(new_target.range.start().checked_add(1).unwrap());
392 self.fetched_operations.clear();
393 self.pinned_nodes = None;
394
395 if self.max_retained_roots > 0 {
398 self.retained_roots
399 .insert(self.target.range.end(), self.target.root);
400 while self.retained_roots.len() > self.max_retained_roots {
401 self.retained_roots.pop_first();
402 }
403 }
404
405 self.target = new_target;
406 self.reached_current_target_reported = false;
407 Ok(self)
408 }
409
410 fn drain_finish_requests(&mut self) -> Result<(), Error<DB, S>> {
417 let Some(finish_rx) = self.finish_rx.as_mut() else {
418 return Ok(());
419 };
420 match finish_rx.try_recv() {
421 Ok(()) => {
422 self.finish_rx = None;
423 self.finish_requested = true;
424 Ok(())
425 }
426 Err(TryRecvError::Empty) => Ok(()),
427 Err(TryRecvError::Disconnected) => {
428 Err(SyncError::Engine(EngineError::FinishChannelClosed))
429 }
430 }
431 }
432
433 async fn report_reached_target(&mut self) {
441 if self.reached_current_target_reported {
442 return;
443 }
444 if let Some(sender) = self.reached_target_tx.as_ref()
445 && !sender.send_lossy(self.target.clone()).await
446 {
447 self.reached_target_tx = None;
448 }
449 self.reached_current_target_reported = true;
450 }
451
452 fn record_progress(&mut self) {
454 self.metrics.record_target(*self.target.range.end());
455 self.metrics.record_synced(self.journal.size());
456 }
457
458 pub(crate) fn store_operations(
460 &mut self,
461 start_loc: Location<DB::Family>,
462 operations: Vec<DB::Op>,
463 ) {
464 if operations.is_empty() {
465 return;
466 }
467 self.fetched_operations.insert(start_loc, operations);
468 }
469
470 pub(crate) async fn apply_operations(mut self) -> Result<Self, Error<DB, S>> {
476 let mut next_loc = self.journal.size();
477
478 self.fetched_operations.retain(|&start_loc, operations| {
481 assert!(!operations.is_empty());
482 let end_loc = start_loc.checked_add(operations.len() as u64 - 1).unwrap();
483 end_loc >= next_loc
484 });
485
486 loop {
487 let range_start_loc =
490 self.fetched_operations
491 .iter()
492 .find_map(|(range_start, range_ops)| {
493 assert!(!range_ops.is_empty());
494 let range_end =
495 range_start.checked_add(range_ops.len() as u64 - 1).unwrap();
496 if *range_start <= next_loc && next_loc <= range_end {
497 Some(*range_start)
498 } else {
499 None
500 }
501 });
502
503 let Some(range_start_loc) = range_start_loc else {
504 break;
506 };
507
508 let operations = self.fetched_operations.remove(&range_start_loc).unwrap();
510 assert!(!operations.is_empty());
511 let operations = &operations[(next_loc - *range_start_loc) as usize..];
515 next_loc += operations.len() as u64;
516 self.journal = self.journal.append(operations).await?;
517 }
518
519 Ok(self)
520 }
521
522 fn is_at_target(&self) -> Result<bool, Error<DB, S>> {
524 let journal_size = self.journal.size();
525 let target_journal_size = self.target.range.end();
526
527 if journal_size >= target_journal_size {
529 if journal_size > target_journal_size {
530 return Err(SyncError::Engine(EngineError::InvalidState));
532 }
533 return Ok(true);
534 }
535
536 Ok(false)
537 }
538
539 fn needs_pinned_nodes(&self) -> bool {
541 self.target.range.start() > Location::new(0)
542 }
543
544 fn pinned_nodes_ready(&self) -> bool {
546 !self.needs_pinned_nodes() || self.pinned_nodes.is_some()
547 }
548
549 fn is_ready_to_complete(&self) -> Result<bool, Error<DB, S>> {
551 Ok(self.is_at_target()? && self.pinned_nodes_ready())
552 }
553
554 fn reject_response(feedback_tx: FeedbackTx) -> Result<(), Error<DB, S>> {
560 feedback_tx.map_or_else(
561 || Err(SyncError::Engine(EngineError::InvalidResponse)),
562 |feedback_tx| {
563 feedback_tx.send_lossy(false);
564 Ok(())
565 },
566 )
567 }
568
569 fn handle_fetch_result(
574 &mut self,
575 fetch_result: IndexedFetchResult<DB::Family, DB::Op, DB::Digest, S::Error>,
576 ) -> Result<(), Error<DB, S>> {
577 let Some(request) = self.outstanding_requests.remove(fetch_result.id) else {
580 return Ok(());
581 };
582
583 let (response, feedback_tx) = fetch_result.result.map_err(SyncError::Source)?;
584
585 let start_loc = request.start();
586 let size = request.size();
587
588 if response.proof().leaves != size {
590 return Self::reject_response(feedback_tx);
591 }
592 match (request, response) {
594 (Request::Operations { max_ops, .. }, Response::Operations { proof, operations }) => {
595 let operations_len = operations.len() as u64;
596 if operations_len == 0 || operations_len > max_ops.get() {
597 return Self::reject_response(feedback_tx);
598 }
599 let Some(root) = self.verification_root(size) else {
600 return Ok(());
601 };
602 let elements = operations.iter().map(|op| op.encode()).collect::<Vec<_>>();
603 if !proof.verify_range_inclusion(&self.hasher, &elements, start_loc, root) {
604 return Self::reject_response(feedback_tx);
605 }
606 if let Some(feedback_tx) = feedback_tx {
607 feedback_tx.send_lossy(true);
608 }
609 self.store_operations(start_loc, operations);
610 }
611 (
612 Request::Boundary { .. },
613 Response::Boundary {
614 proof,
615 op,
616 pinned_nodes,
617 },
618 ) => {
619 let need_pinned = size == self.target.range.end()
622 && self.pinned_nodes.is_none()
623 && start_loc == self.target.range.start();
624 let element = [op.encode()];
625 let valid = if need_pinned {
626 proof.verify_proof_and_pinned_nodes(
627 &self.hasher,
628 &element,
629 start_loc,
630 &pinned_nodes,
631 &self.target.root,
632 )
633 } else {
634 let Some(root) = self.verification_root(size) else {
635 return Ok(());
636 };
637 proof.verify_range_inclusion(&self.hasher, &element, start_loc, root)
638 };
639 if !valid {
640 if need_pinned {
641 tracing::warn!("boundary response failed verification");
642 }
643 return Self::reject_response(feedback_tx);
644 }
645 if let Some(feedback_tx) = feedback_tx {
646 feedback_tx.send_lossy(true);
647 }
648 if need_pinned {
649 self.pinned_nodes = Some(pinned_nodes);
650 }
651 self.store_operations(start_loc, vec![op]);
652 }
653 _ => return Self::reject_response(feedback_tx),
654 }
655
656 Ok(())
657 }
658
659 fn verification_root(&self, size: Location<DB::Family>) -> Option<&DB::Digest> {
661 if size == self.target.range.end() {
662 Some(&self.target.root)
663 } else {
664 self.retained_roots.get(&size)
665 }
666 }
667
668 async fn handle_event(
670 mut self,
671 event: Event<DB::Family, DB::Op, DB::Digest, S::Error>,
672 ) -> Result<NextStep<Self, DB>, Error<DB, S>> {
673 match event {
674 Event::TargetUpdate(new_target) => {
675 if !new_target.advances(&self.target) {
677 return Ok(NextStep::Continue(self));
678 }
679 if new_target.root == self.target.root {
682 return Err(SyncError::Engine(EngineError::SyncTargetRootUnchanged));
683 }
684
685 let mut updated_self = self.reset_for_target_update(new_target).await?;
686 updated_self.record_progress();
687 updated_self.schedule_requests()?;
688 Ok(NextStep::Continue(updated_self))
689 }
690 Event::UpdateChannelClosed => {
691 self.update_rx = None;
692 Ok(NextStep::Continue(self))
693 }
694 Event::FinishRequested => {
695 self.finish_rx = None;
696 self.finish_requested = true;
697 Ok(NextStep::Continue(self))
698 }
699 Event::FinishChannelClosed => Err(SyncError::Engine(EngineError::FinishChannelClosed)),
700 Event::BatchReceived(fetch_result) => {
701 if let Ok(fetch_result) = fetch_result {
703 self.handle_fetch_result(fetch_result)?;
704 }
705 self.schedule_requests()?;
706 let mut engine = self.apply_operations().await?;
707 engine.record_progress();
708 Ok(NextStep::Continue(engine))
709 }
710 }
711 }
712
713 #[boxed]
724 pub(crate) async fn step(mut self) -> Result<NextStep<Self, DB>, Error<DB, S>> {
725 self.drain_finish_requests()?;
726
727 if self.is_ready_to_complete()? {
729 if !self.finish_requested {
733 while let Some(update_rx) = self.update_rx.as_mut() {
734 match update_rx.try_recv() {
735 Ok(new_target) => {
736 if new_target.advances(&self.target) {
737 return self.handle_event(Event::TargetUpdate(new_target)).await;
738 }
739 }
740 Err(TryRecvError::Empty) => break,
741 Err(TryRecvError::Disconnected) => {
742 self.update_rx = None;
743 }
744 }
745 }
746 }
747
748 self.report_reached_target().await;
749
750 if self.finish_rx.is_some() {
751 let event = wait_for_event(
752 &mut self.update_rx,
753 &mut self.finish_rx,
754 &mut self.outstanding_requests,
755 )
756 .await
757 .ok_or(SyncError::Engine(EngineError::SyncStalled))?;
758 return self.handle_event(event).await;
759 }
760
761 return Ok(NextStep::Complete(self.complete().await?));
762 }
763
764 let event = wait_for_event(
766 &mut self.update_rx,
767 &mut self.finish_rx,
768 &mut self.outstanding_requests,
769 )
770 .await
771 .ok_or(SyncError::Engine(EngineError::SyncStalled))?;
772 self.handle_event(event).await
773 }
774
775 async fn complete(mut self) -> Result<DB, Error<DB, S>> {
778 self.journal = self.journal.sync().await?;
779
780 let database = DB::from_sync_result(
781 self.context,
782 self.config,
783 self.journal,
784 self.pinned_nodes,
785 self.target.range.clone(),
786 self.apply_batch_size,
787 )
788 .await?;
789
790 let got_root = database.root();
791 let expected_root = self.target.root;
792 if got_root != expected_root {
793 return Err(SyncError::Engine(EngineError::RootMismatch {
794 expected: expected_root,
795 actual: got_root,
796 }));
797 }
798
799 Ok(database.persist_sync_result().await?)
800 }
801
802 pub async fn sync(mut self) -> Result<DB, Error<DB, S>> {
807 loop {
809 match self.step().await? {
810 NextStep::Continue(new_engine) => self = new_engine,
811 NextStep::Complete(database) => return Ok(database),
812 }
813 }
814 }
815}
816
817#[cfg(test)]
818mod tests {
819 use super::*;
820 use crate::merkle::mmr::{Family as MmrFamily, Proof};
821 use commonware_cryptography::{Sha256, sha256};
822 use commonware_runtime::{Runner as _, deterministic};
823 use commonware_utils::{NZU64, non_empty_range};
824 use std::{
825 convert::Infallible,
826 sync::{
827 Arc,
828 atomic::{AtomicUsize, Ordering},
829 },
830 };
831
832 #[derive(Clone)]
833 struct TestConfig {
834 journal_size: u64,
835 pinned_node_probes: Arc<AtomicUsize>,
836 }
837
838 impl crate::qmdb::sync::DatabaseConfig for TestConfig {
839 type JournalConfig = u64;
840
841 fn journal_config(&self) -> Self::JournalConfig {
842 self.journal_size
843 }
844 }
845
846 struct TestJournal {
847 size: u64,
848 }
849
850 impl Journal<MmrFamily> for TestJournal {
851 type Config = u64;
852 type Context = deterministic::Context;
853 type Error = crate::journal::Error;
854 type Op = i32;
855
856 async fn new(
857 _context: Self::Context,
858 size: Self::Config,
859 _range: commonware_utils::range::NonEmptyRange<Location<MmrFamily>>,
860 ) -> Result<Self, Self::Error> {
861 Ok(Self { size })
862 }
863
864 async fn resize(mut self, start: Location<MmrFamily>) -> Result<Self, Self::Error> {
865 self.size = *start;
866 Ok(self)
867 }
868
869 async fn sync(self) -> Result<Self, Self::Error> {
870 Ok(self)
871 }
872
873 fn size(&self) -> u64 {
874 self.size
875 }
876
877 async fn append(mut self, ops: &[Self::Op]) -> Result<Self, Self::Error> {
878 self.size += ops.len() as u64;
879 Ok(self)
880 }
881 }
882
883 struct TestDb;
884
885 impl Database for TestDb {
886 type Config = TestConfig;
887 type Context = deterministic::Context;
888 type Digest = sha256::Digest;
889 type Family = MmrFamily;
890 type Hasher = Sha256;
891 type Journal = TestJournal;
892 type Op = i32;
893
894 async fn from_sync_result(
895 _context: Self::Context,
896 _config: Self::Config,
897 _journal: Self::Journal,
898 _pinned_nodes: Option<Vec<Self::Digest>>,
899 _range: commonware_utils::range::NonEmptyRange<Location<Self::Family>>,
900 _apply_batch_size: NonZeroU64,
901 ) -> Result<Self, qmdb::Error<Self::Family>> {
902 Ok(Self)
903 }
904
905 async fn persist_sync_result(self) -> Result<Self, qmdb::Error<Self::Family>> {
906 Ok(self)
907 }
908
909 async fn local_pinned_nodes(
910 _context: Self::Context,
911 config: &Self::Config,
912 _target: &Target<Self::Family, Self::Digest>,
913 _journal: &Self::Journal,
914 ) -> Result<Option<Vec<Self::Digest>>, qmdb::Error<Self::Family>> {
915 config.pinned_node_probes.fetch_add(1, Ordering::SeqCst);
916 Ok(Some(vec![]))
917 }
918
919 fn root(&self) -> Self::Digest {
920 sha256::Digest::from([0u8; 32])
921 }
922 }
923
924 #[derive(Clone)]
925 struct TestSource;
926
927 impl Source for TestSource {
928 type Digest = sha256::Digest;
929 type Error = Infallible;
930 type Family = MmrFamily;
931 type Op = i32;
932
933 async fn serve(
934 &self,
935 _request: Request<MmrFamily>,
936 ) -> Result<(Response<Self::Family, Self::Op, Self::Digest>, FeedbackTx), Self::Error>
937 {
938 Ok((
939 Response::Operations {
940 proof: Proof {
941 leaves: Location::new(0),
942 inactive_peaks: 0,
943 digests: vec![],
944 },
945 operations: vec![],
946 },
947 None,
948 ))
949 }
950 }
951
952 fn test_engine_config(
953 context: deterministic::Context,
954 journal_size: u64,
955 pinned_node_probes: Arc<AtomicUsize>,
956 ) -> Config<TestDb, TestSource> {
957 Config {
958 context,
959 source: TestSource,
960 target: Target {
961 root: sha256::Digest::from([1u8; 32]),
962 range: non_empty_range!(Location::new(5), Location::new(10)),
963 },
964 max_outstanding_requests: 1,
965 fetch_batch_size: NZU64!(1),
966 apply_batch_size: NZU64!(1),
967 db_config: TestConfig {
968 journal_size,
969 pinned_node_probes,
970 },
971 update_rx: None,
972 finish_rx: None,
973 reached_target_tx: None,
974 max_retained_roots: 0,
975 }
976 }
977
978 #[test]
979 fn new_probes_local_pinned_nodes_when_journal_reaches_target() {
980 deterministic::Runner::default().start(|context| async move {
981 let pinned_node_probes = Arc::new(AtomicUsize::new(0));
982 Engine::new(test_engine_config(context, 10, pinned_node_probes.clone()))
983 .await
984 .unwrap();
985
986 assert_eq!(pinned_node_probes.load(Ordering::SeqCst), 1);
987 });
988 }
989
990 #[test]
991 fn new_skips_local_pinned_nodes_when_journal_is_partial() {
992 deterministic::Runner::default().start(|context| async move {
993 let pinned_node_probes = Arc::new(AtomicUsize::new(0));
994 Engine::new(test_engine_config(context, 7, pinned_node_probes.clone()))
995 .await
996 .unwrap();
997
998 assert_eq!(pinned_node_probes.load(Ordering::SeqCst), 0);
999 });
1000 }
1001
1002 #[test]
1003 fn new_schedules_operations_after_boundary_request() {
1004 deterministic::Runner::default().start(|context| async move {
1005 let mut config = test_engine_config(context, 5, Arc::new(AtomicUsize::new(0)));
1006 config.max_outstanding_requests = 2;
1007 config.fetch_batch_size = NZU64!(5);
1008
1009 let engine = Engine::new(config).await.unwrap();
1010 let requests = &engine.outstanding_requests;
1011
1012 assert_eq!(requests.len(), 2);
1013 assert!(requests.contains(&Location::new(5)));
1014 assert!(requests.contains(&Location::new(6)));
1015 });
1016 }
1017
1018 #[test]
1019 fn step_takes_queued_update_before_completing() {
1020 deterministic::Runner::default().start(|context| async move {
1021 let (update_tx, update_rx) = mpsc::channel(2);
1022 let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1023 config.update_rx = Some(update_rx);
1024 let stale = Target {
1027 root: sha256::Digest::from([2u8; 32]),
1028 range: non_empty_range!(Location::new(5), Location::new(10)),
1029 };
1030 let advancing = Target {
1031 root: sha256::Digest::from([3u8; 32]),
1032 range: non_empty_range!(Location::new(5), Location::new(12)),
1033 };
1034 update_tx.send(stale).await.unwrap();
1035 update_tx.send(advancing.clone()).await.unwrap();
1036
1037 let engine = Engine::new(config).await.unwrap();
1038 let NextStep::Continue(engine) = engine.step().await.unwrap() else {
1039 panic!("engine should retarget instead of completing");
1040 };
1041 assert_eq!(engine.target, advancing);
1042 });
1043 }
1044
1045 #[test]
1046 fn step_completes_at_current_target_after_finish() {
1047 deterministic::Runner::default().start(|context| async move {
1048 let (update_tx, update_rx) = mpsc::channel(1);
1049 let (finish_tx, finish_rx) = mpsc::channel(1);
1050 let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1051 config.target.root = sha256::Digest::from([0u8; 32]);
1053 config.update_rx = Some(update_rx);
1054 config.finish_rx = Some(finish_rx);
1055 let advancing = Target {
1056 root: sha256::Digest::from([3u8; 32]),
1057 range: non_empty_range!(Location::new(5), Location::new(12)),
1058 };
1059 update_tx.send(advancing).await.unwrap();
1060 finish_tx.send(()).await.unwrap();
1061
1062 let engine = Engine::new(config).await.unwrap();
1063 let NextStep::Complete(_) = engine.step().await.unwrap() else {
1064 panic!("a requested finish must win over a queued update");
1065 };
1066 });
1067 }
1068
1069 fn dummy_result(id: RequestId) -> IndexedFetchResult<MmrFamily, i32, sha256::Digest, ()> {
1071 IndexedFetchResult {
1072 id,
1073 result: Ok((
1074 Response::Operations {
1075 proof: Proof {
1076 leaves: Location::new(0),
1077 inactive_peaks: 0,
1078 digests: vec![],
1079 },
1080 operations: vec![],
1081 },
1082 None,
1083 )),
1084 }
1085 }
1086
1087 fn add(requests: &mut Requests<MmrFamily, i32, sha256::Digest, ()>, loc: u64) -> RequestId {
1089 requests.insert(
1090 Request::Operations {
1091 size: Location::new(loc),
1092 start: Location::new(loc),
1093 max_ops: NZU64!(1),
1094 },
1095 |id| std::future::ready(dummy_result(id)),
1096 )
1097 }
1098
1099 #[test]
1100 fn test_add_and_remove() {
1101 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1102 assert_eq!(requests.len(), 0);
1103
1104 let id = add(&mut requests, 10);
1105 assert_eq!(requests.len(), 1);
1106 assert!(requests.contains(&Location::new(10)));
1107
1108 assert!(requests.remove(id).is_some());
1109 assert!(!requests.contains(&Location::new(10)));
1110 assert!(requests.remove(id).is_none());
1111 }
1112
1113 #[test]
1114 fn test_remove_before() {
1115 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1116
1117 add(&mut requests, 5);
1118 add(&mut requests, 10);
1119 add(&mut requests, 15);
1120 add(&mut requests, 20);
1121 assert_eq!(requests.len(), 4);
1122
1123 requests.remove_before(Location::new(10));
1124 assert_eq!(requests.len(), 3);
1125 assert!(!requests.contains(&Location::new(5)));
1126 assert!(requests.contains(&Location::new(10)));
1127 assert!(requests.contains(&Location::new(15)));
1128 assert!(requests.contains(&Location::new(20)));
1129 }
1130
1131 #[test]
1132 fn test_remove_before_all() {
1133 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1134
1135 add(&mut requests, 5);
1136 add(&mut requests, 10);
1137 assert_eq!(requests.len(), 2);
1138
1139 requests.remove_before(Location::new(100));
1140 assert_eq!(requests.len(), 0);
1141 }
1142
1143 #[test]
1144 fn test_remove_before_empty() {
1145 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1146 requests.remove_before(Location::new(10));
1147 assert_eq!(requests.len(), 0);
1148 }
1149
1150 #[test]
1151 fn test_remove_before_none() {
1152 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1153
1154 add(&mut requests, 10);
1155 add(&mut requests, 20);
1156 assert_eq!(requests.len(), 2);
1157
1158 requests.remove_before(Location::new(5));
1159 assert_eq!(requests.len(), 2);
1160 assert!(requests.contains(&Location::new(10)));
1161 assert!(requests.contains(&Location::new(20)));
1162 }
1163
1164 #[test]
1165 fn test_superseded_request() {
1166 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1167
1168 let old_id = add(&mut requests, 10);
1170 assert_eq!(requests.len(), 1);
1171
1172 let new_id = add(&mut requests, 10);
1174 assert_eq!(requests.len(), 1);
1175
1176 assert!(requests.remove(old_id).is_none());
1178
1179 assert!(requests.contains(&Location::new(10)));
1181 assert!(requests.remove(new_id).is_some());
1182 assert!(!requests.contains(&Location::new(10)));
1183 }
1184
1185 #[test]
1186 fn test_stale_id_after_remove_before() {
1187 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1188
1189 let old_id = add(&mut requests, 5);
1190 add(&mut requests, 15);
1191 requests.remove_before(Location::new(10));
1192
1193 assert!(requests.remove(old_id).is_none());
1195
1196 let new_id = add(&mut requests, 5);
1198 assert_ne!(old_id, new_id);
1199 assert!(requests.remove(new_id).is_some());
1200 }
1201
1202 #[test]
1203 fn test_remove_before_aborts_future() {
1204 deterministic::Runner::default().start(|_context| async move {
1205 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1206 requests.insert(
1207 Request::Operations {
1208 size: Location::new(5),
1209 start: Location::new(5),
1210 max_ops: NZU64!(1),
1211 },
1212 |_| std::future::pending(),
1213 );
1214 requests.remove_before(Location::new(10));
1215 assert!(matches!(requests.next_completed().await, Err(Aborted)));
1216 });
1217 }
1218}