Skip to main content

zcash_client_backend/data_api/scanning/
spanning_tree.rs

1use std::cmp::{Ordering, max};
2use std::ops::{Not, Range};
3
4use zcash_protocol::consensus::BlockHeight;
5
6use super::{ScanPriority, ScanRange};
7use RangeOrdering::*;
8
9#[derive(Debug, Clone, Copy)]
10enum InsertOn {
11    Left,
12    Right,
13}
14
15struct Insert {
16    on: InsertOn,
17    force_rescan: bool,
18}
19
20impl Insert {
21    fn left(force_rescan: bool) -> Self {
22        Insert {
23            on: InsertOn::Left,
24            force_rescan,
25        }
26    }
27
28    fn right(force_rescan: bool) -> Self {
29        Insert {
30            on: InsertOn::Right,
31            force_rescan,
32        }
33    }
34}
35
36impl Not for Insert {
37    type Output = Self;
38
39    fn not(self) -> Self::Output {
40        Insert {
41            on: match self.on {
42                InsertOn::Left => InsertOn::Right,
43                InsertOn::Right => InsertOn::Left,
44            },
45            force_rescan: self.force_rescan,
46        }
47    }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51enum Dominance {
52    Left,
53    Right,
54    Equal,
55}
56
57impl From<Insert> for Dominance {
58    fn from(value: Insert) -> Self {
59        match value.on {
60            InsertOn::Left => Dominance::Left,
61            InsertOn::Right => Dominance::Right,
62        }
63    }
64}
65
66// This implements the dominance rule for range priority. If the inserted range's priority is
67// `Verify`, this replaces any existing priority. Otherwise, if the current priority is
68// `Scanned`, it remains as `Scanned`; and if the new priority is `Scanned`, it
69// overrides any existing priority.
70fn dominance(current: &ScanPriority, inserted: &ScanPriority, insert: Insert) -> Dominance {
71    match (current.cmp(inserted), (current, inserted)) {
72        (Ordering::Equal, _) => Dominance::Equal,
73        (_, (_, ScanPriority::Verify | ScanPriority::Scanned)) => Dominance::from(insert),
74        (_, (ScanPriority::Scanned, _)) if !insert.force_rescan => Dominance::from(!insert),
75        (Ordering::Less, _) => Dominance::from(insert),
76        (Ordering::Greater, _) => Dominance::from(!insert),
77    }
78}
79
80/// In the comments for each alternative, `()` represents the left range and `[]` represents the right range.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82enum RangeOrdering {
83    /// `(   ) [   ]`
84    LeftFirstDisjoint,
85    /// `( [   ) ]`
86    LeftFirstOverlap,
87    /// `[ (   ) ]`
88    LeftContained,
89    /// ```text
90    /// (   )
91    /// [   ]
92    /// ```
93    Equal,
94    /// `( [   ] )`
95    RightContained,
96    /// `[ (   ] )`
97    RightFirstOverlap,
98    /// `[   ] (   )`
99    RightFirstDisjoint,
100}
101
102impl RangeOrdering {
103    fn cmp<A: Ord>(a: &Range<A>, b: &Range<A>) -> Self {
104        assert!(a.start <= a.end && b.start <= b.end);
105        match (a.start.cmp(&b.start), a.end.cmp(&b.end)) {
106            _ if a.end <= b.start => RangeOrdering::LeftFirstDisjoint,
107            _ if b.end <= a.start => RangeOrdering::RightFirstDisjoint,
108            (Ordering::Less, Ordering::Less) => RangeOrdering::LeftFirstOverlap,
109            (Ordering::Equal, Ordering::Less)
110            | (Ordering::Greater, Ordering::Less)
111            | (Ordering::Greater, Ordering::Equal) => RangeOrdering::LeftContained,
112            (Ordering::Equal, Ordering::Equal) => RangeOrdering::Equal,
113            (Ordering::Equal, Ordering::Greater)
114            | (Ordering::Less, Ordering::Greater)
115            | (Ordering::Less, Ordering::Equal) => RangeOrdering::RightContained,
116            (Ordering::Greater, Ordering::Greater) => RangeOrdering::RightFirstOverlap,
117        }
118    }
119}
120
121#[derive(Debug, PartialEq, Eq)]
122enum Joined {
123    One(ScanRange),
124    Two(ScanRange, ScanRange),
125    Three(ScanRange, ScanRange, ScanRange),
126}
127
128fn join_nonoverlapping(left: ScanRange, right: ScanRange) -> Joined {
129    assert!(left.block_range().end <= right.block_range().start);
130
131    if left.block_range().end == right.block_range().start {
132        if left.priority() == right.priority() {
133            Joined::One(ScanRange::from_parts(
134                left.block_range().start..right.block_range().end,
135                left.priority(),
136            ))
137        } else {
138            Joined::Two(left, right)
139        }
140    } else {
141        // there is a gap that will need to be filled
142        let gap = ScanRange::from_parts(
143            left.block_range().end..right.block_range().start,
144            ScanPriority::Historic,
145        );
146
147        match join_nonoverlapping(left, gap) {
148            Joined::One(merged) => join_nonoverlapping(merged, right),
149            Joined::Two(left, gap) => match join_nonoverlapping(gap, right) {
150                Joined::One(merged) => Joined::Two(left, merged),
151                Joined::Two(gap, right) => Joined::Three(left, gap, right),
152                _ => unreachable!(),
153            },
154            _ => unreachable!(),
155        }
156    }
157}
158
159fn insert(current: ScanRange, to_insert: ScanRange, force_rescans: bool) -> Joined {
160    fn join_overlapping(left: ScanRange, right: ScanRange, insert: Insert) -> Joined {
161        assert!(
162            left.block_range().start <= right.block_range().start
163                && left.block_range().end > right.block_range().start
164        );
165
166        // recompute the range dominance based upon the queue entry priorities
167        let dominance = match insert.on {
168            InsertOn::Left => dominance(&right.priority(), &left.priority(), insert),
169            InsertOn::Right => dominance(&left.priority(), &right.priority(), insert),
170        };
171
172        match dominance {
173            Dominance::Left => {
174                if let Some(right) = right.truncate_start(left.block_range().end) {
175                    Joined::Two(left, right)
176                } else {
177                    Joined::One(left)
178                }
179            }
180            Dominance::Equal => Joined::One(ScanRange::from_parts(
181                left.block_range().start..max(left.block_range().end, right.block_range().end),
182                left.priority(),
183            )),
184            Dominance::Right => match (
185                left.truncate_end(right.block_range().start),
186                left.truncate_start(right.block_range().end),
187            ) {
188                (Some(before), Some(after)) => Joined::Three(before, right, after),
189                (Some(before), None) => Joined::Two(before, right),
190                (None, Some(after)) => Joined::Two(right, after),
191                (None, None) => Joined::One(right),
192            },
193        }
194    }
195
196    match RangeOrdering::cmp(to_insert.block_range(), current.block_range()) {
197        LeftFirstDisjoint => join_nonoverlapping(to_insert, current),
198        LeftFirstOverlap | RightContained => {
199            join_overlapping(to_insert, current, Insert::left(force_rescans))
200        }
201        Equal => Joined::One(ScanRange::from_parts(
202            to_insert.block_range().clone(),
203            match dominance(
204                &current.priority(),
205                &to_insert.priority(),
206                Insert::right(force_rescans),
207            ) {
208                Dominance::Left | Dominance::Equal => current.priority(),
209                Dominance::Right => to_insert.priority(),
210            },
211        )),
212        RightFirstOverlap | LeftContained => {
213            join_overlapping(current, to_insert, Insert::right(force_rescans))
214        }
215        RightFirstDisjoint => join_nonoverlapping(current, to_insert),
216    }
217}
218
219#[derive(Debug, Clone)]
220#[cfg(feature = "unstable-spanning-tree")]
221pub enum SpanningTree {
222    Leaf(ScanRange),
223    Parent {
224        span: Range<BlockHeight>,
225        left: Box<SpanningTree>,
226        right: Box<SpanningTree>,
227    },
228}
229
230#[cfg(feature = "unstable-spanning-tree")]
231impl SpanningTree {
232    fn span(&self) -> Range<BlockHeight> {
233        match self {
234            SpanningTree::Leaf(entry) => entry.block_range().clone(),
235            SpanningTree::Parent { span, .. } => span.clone(),
236        }
237    }
238
239    fn from_joined(joined: Joined) -> Self {
240        match joined {
241            Joined::One(entry) => SpanningTree::Leaf(entry),
242            Joined::Two(left, right) => SpanningTree::Parent {
243                span: left.block_range().start..right.block_range().end,
244                left: Box::new(SpanningTree::Leaf(left)),
245                right: Box::new(SpanningTree::Leaf(right)),
246            },
247            Joined::Three(left, mid, right) => SpanningTree::Parent {
248                span: left.block_range().start..right.block_range().end,
249                left: Box::new(SpanningTree::Leaf(left)),
250                right: Box::new(SpanningTree::Parent {
251                    span: mid.block_range().start..right.block_range().end,
252                    left: Box::new(SpanningTree::Leaf(mid)),
253                    right: Box::new(SpanningTree::Leaf(right)),
254                }),
255            },
256        }
257    }
258
259    fn from_insert(
260        left: Box<Self>,
261        right: Box<Self>,
262        to_insert: ScanRange,
263        insert: Insert,
264    ) -> Self {
265        let (left, right) = match insert.on {
266            InsertOn::Left => (Box::new(left.insert(to_insert, insert.force_rescan)), right),
267            InsertOn::Right => (left, Box::new(right.insert(to_insert, insert.force_rescan))),
268        };
269        SpanningTree::Parent {
270            span: left.span().start..right.span().end,
271            left,
272            right,
273        }
274    }
275
276    fn from_split(
277        left: Self,
278        right: Self,
279        to_insert: ScanRange,
280        split_point: BlockHeight,
281        force_rescans: bool,
282    ) -> Self {
283        let (l_insert, r_insert) = to_insert
284            .split_at(split_point)
285            .expect("Split point is within the range of to_insert");
286        let left = Box::new(left.insert(l_insert, force_rescans));
287        let right = Box::new(right.insert(r_insert, force_rescans));
288        SpanningTree::Parent {
289            span: left.span().start..right.span().end,
290            left,
291            right,
292        }
293    }
294
295    pub fn insert(self, to_insert: ScanRange, force_rescans: bool) -> Self {
296        match self {
297            SpanningTree::Leaf(cur) => Self::from_joined(insert(cur, to_insert, force_rescans)),
298            SpanningTree::Parent { span, left, right } => {
299                // This algorithm always preserves the existing partition point, and does not do
300                // any rebalancing or unification of ranges within the tree. This should be okay
301                // because `into_vec` performs such unification, and the tree being unbalanced
302                // should be fine given the relatively small number of ranges we should ordinarily
303                // be concerned with.
304                match RangeOrdering::cmp(&span, to_insert.block_range()) {
305                    LeftFirstDisjoint => {
306                        // extend the right-hand branch
307                        Self::from_insert(left, right, to_insert, Insert::right(force_rescans))
308                    }
309                    LeftFirstOverlap => {
310                        let split_point = left.span().end;
311                        if split_point > to_insert.block_range().start {
312                            Self::from_split(*left, *right, to_insert, split_point, force_rescans)
313                        } else {
314                            // to_insert is fully contained in or equals the right child
315                            Self::from_insert(left, right, to_insert, Insert::right(force_rescans))
316                        }
317                    }
318                    RightContained => {
319                        // to_insert is fully contained within the current span, so we will insert
320                        // into one or both sides
321                        let split_point = left.span().end;
322                        if to_insert.block_range().start >= split_point {
323                            // to_insert is fully contained in the right
324                            Self::from_insert(left, right, to_insert, Insert::right(force_rescans))
325                        } else if to_insert.block_range().end <= split_point {
326                            // to_insert is fully contained in the left
327                            Self::from_insert(left, right, to_insert, Insert::left(force_rescans))
328                        } else {
329                            // to_insert must be split.
330                            Self::from_split(*left, *right, to_insert, split_point, force_rescans)
331                        }
332                    }
333                    Equal => {
334                        let split_point = left.span().end;
335                        if split_point > to_insert.block_range().start {
336                            Self::from_split(*left, *right, to_insert, split_point, force_rescans)
337                        } else {
338                            // to_insert is fully contained in the right subtree
339                            right.insert(to_insert, force_rescans)
340                        }
341                    }
342                    LeftContained => {
343                        // the current span is fully contained within to_insert, so we will extend
344                        // or overwrite both sides
345                        let split_point = left.span().end;
346                        Self::from_split(*left, *right, to_insert, split_point, force_rescans)
347                    }
348                    RightFirstOverlap => {
349                        let split_point = left.span().end;
350                        if split_point < to_insert.block_range().end {
351                            Self::from_split(*left, *right, to_insert, split_point, force_rescans)
352                        } else {
353                            // to_insert is fully contained in or equals the left child
354                            Self::from_insert(left, right, to_insert, Insert::left(force_rescans))
355                        }
356                    }
357                    RightFirstDisjoint => {
358                        // extend the left-hand branch
359                        Self::from_insert(left, right, to_insert, Insert::left(force_rescans))
360                    }
361                }
362            }
363        }
364    }
365
366    pub fn into_vec(self) -> Vec<ScanRange> {
367        fn go(acc: &mut Vec<ScanRange>, tree: SpanningTree) {
368            match tree {
369                SpanningTree::Leaf(entry) => {
370                    if !entry.is_empty() {
371                        if let Some(top) = acc.pop() {
372                            match join_nonoverlapping(top, entry) {
373                                Joined::One(merged) => acc.push(merged),
374                                Joined::Two(l, r) => {
375                                    acc.push(l);
376                                    acc.push(r);
377                                }
378                                _ => unreachable!(),
379                            }
380                        } else {
381                            acc.push(entry);
382                        }
383                    }
384                }
385                SpanningTree::Parent { left, right, .. } => {
386                    go(acc, *left);
387                    go(acc, *right);
388                }
389            }
390        }
391
392        let mut acc = vec![];
393        go(&mut acc, self);
394        acc
395    }
396}
397
398#[cfg(any(test, feature = "test-dependencies"))]
399pub mod testing {
400    use std::ops::Range;
401
402    use zcash_protocol::consensus::BlockHeight;
403
404    use crate::data_api::scanning::{ScanPriority, ScanRange};
405
406    pub fn scan_range(range: Range<u32>, priority: ScanPriority) -> ScanRange {
407        ScanRange::from_parts(
408            BlockHeight::from(range.start)..BlockHeight::from(range.end),
409            priority,
410        )
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use std::ops::Range;
417
418    use zcash_protocol::consensus::BlockHeight;
419
420    use super::RangeOrdering::*;
421    use super::{Joined, RangeOrdering, SpanningTree, join_nonoverlapping, testing::scan_range};
422    use crate::data_api::scanning::ScanPriority::*;
423    use crate::data_api::scanning::{ScanPriority, ScanRange};
424
425    #[test]
426    fn test_join_nonoverlapping() {
427        fn test_range(left: ScanRange, right: ScanRange, expected_joined: Joined) {
428            let joined = join_nonoverlapping(left, right);
429
430            assert_eq!(joined, expected_joined);
431        }
432
433        macro_rules! range {
434            ( $start:expr, $end:expr; $priority:ident ) => {
435                ScanRange::from_parts(
436                    BlockHeight::from($start)..BlockHeight::from($end),
437                    ScanPriority::$priority,
438                )
439            };
440        }
441
442        macro_rules! joined {
443            (
444                ($a_start:expr, $a_end:expr; $a_priority:ident)
445            ) => {
446                Joined::One(
447                    range!($a_start, $a_end; $a_priority)
448                )
449            };
450            (
451                ($a_start:expr, $a_end:expr; $a_priority:ident),
452                ($b_start:expr, $b_end:expr; $b_priority:ident)
453            ) => {
454                Joined::Two(
455                    range!($a_start, $a_end; $a_priority),
456                    range!($b_start, $b_end; $b_priority)
457                )
458            };
459            (
460                ($a_start:expr, $a_end:expr; $a_priority:ident),
461                ($b_start:expr, $b_end:expr; $b_priority:ident),
462                ($c_start:expr, $c_end:expr; $c_priority:ident)
463
464            ) => {
465                Joined::Three(
466                    range!($a_start, $a_end; $a_priority),
467                    range!($b_start, $b_end; $b_priority),
468                    range!($c_start, $c_end; $c_priority)
469                )
470            };
471        }
472
473        // Scan ranges have the same priority and
474        // line up.
475        test_range(
476            range!(1, 9; OpenAdjacent),
477            range!(9, 15; OpenAdjacent),
478            joined!(
479                (1, 15; OpenAdjacent)
480            ),
481        );
482
483        // Scan ranges have different priorities,
484        // so we cannot merge them even though they
485        // line up.
486        test_range(
487            range!(1, 9; OpenAdjacent),
488            range!(9, 15; ChainTip),
489            joined!(
490                (1, 9; OpenAdjacent),
491                (9, 15; ChainTip)
492            ),
493        );
494
495        // Scan ranges have the same priority but
496        // do not line up.
497        test_range(
498            range!(1, 9; OpenAdjacent),
499            range!(13, 15; OpenAdjacent),
500            joined!(
501                (1, 9; OpenAdjacent),
502                (9, 13; Historic),
503                (13, 15; OpenAdjacent)
504            ),
505        );
506
507        test_range(
508            range!(1, 9; Historic),
509            range!(13, 15; OpenAdjacent),
510            joined!(
511                (1, 13; Historic),
512                (13, 15; OpenAdjacent)
513            ),
514        );
515
516        test_range(
517            range!(1, 9; OpenAdjacent),
518            range!(13, 15; Historic),
519            joined!(
520                (1, 9; OpenAdjacent),
521                (9, 15; Historic)
522            ),
523        );
524    }
525
526    #[test]
527    fn range_ordering() {
528        // Equal
529        assert_eq!(RangeOrdering::cmp(&(0..1), &(0..1)), Equal);
530
531        // Disjoint or contiguous
532        assert_eq!(RangeOrdering::cmp(&(0..1), &(1..2)), LeftFirstDisjoint);
533        assert_eq!(RangeOrdering::cmp(&(1..2), &(0..1)), RightFirstDisjoint);
534        assert_eq!(RangeOrdering::cmp(&(0..1), &(2..3)), LeftFirstDisjoint);
535        assert_eq!(RangeOrdering::cmp(&(2..3), &(0..1)), RightFirstDisjoint);
536        assert_eq!(RangeOrdering::cmp(&(1..2), &(2..2)), LeftFirstDisjoint);
537        assert_eq!(RangeOrdering::cmp(&(2..2), &(1..2)), RightFirstDisjoint);
538        assert_eq!(RangeOrdering::cmp(&(1..1), &(1..2)), LeftFirstDisjoint);
539        assert_eq!(RangeOrdering::cmp(&(1..2), &(1..1)), RightFirstDisjoint);
540
541        // Contained
542        assert_eq!(RangeOrdering::cmp(&(1..2), &(0..3)), LeftContained);
543        assert_eq!(RangeOrdering::cmp(&(0..3), &(1..2)), RightContained);
544        assert_eq!(RangeOrdering::cmp(&(0..1), &(0..3)), LeftContained);
545        assert_eq!(RangeOrdering::cmp(&(0..3), &(0..1)), RightContained);
546        assert_eq!(RangeOrdering::cmp(&(2..3), &(0..3)), LeftContained);
547        assert_eq!(RangeOrdering::cmp(&(0..3), &(2..3)), RightContained);
548
549        // Overlap
550        assert_eq!(RangeOrdering::cmp(&(0..2), &(1..3)), LeftFirstOverlap);
551        assert_eq!(RangeOrdering::cmp(&(1..3), &(0..2)), RightFirstOverlap);
552    }
553
554    fn spanning_tree(to_insert: &[(Range<u32>, ScanPriority)]) -> Option<SpanningTree> {
555        to_insert.iter().fold(None, |acc, (range, priority)| {
556            let scan_range = scan_range(range.clone(), *priority);
557            match acc {
558                None => Some(SpanningTree::Leaf(scan_range)),
559                Some(t) => Some(t.insert(scan_range, false)),
560            }
561        })
562    }
563
564    #[test]
565    fn spanning_tree_insert_adjacent() {
566        let t = spanning_tree(&[
567            (0..3, Historic),
568            (3..6, Scanned),
569            (6..8, ChainTip),
570            (8..10, ChainTip),
571        ])
572        .unwrap();
573
574        assert_eq!(
575            t.into_vec(),
576            vec![
577                scan_range(0..3, Historic),
578                scan_range(3..6, Scanned),
579                scan_range(6..10, ChainTip),
580            ]
581        );
582    }
583
584    #[test]
585    fn spanning_tree_insert_overlaps() {
586        let t = spanning_tree(&[
587            (0..3, Historic),
588            (2..5, Scanned),
589            (6..8, ChainTip),
590            (7..10, Scanned),
591        ])
592        .unwrap();
593
594        assert_eq!(
595            t.into_vec(),
596            vec![
597                scan_range(0..2, Historic),
598                scan_range(2..5, Scanned),
599                scan_range(5..6, Historic),
600                scan_range(6..7, ChainTip),
601                scan_range(7..10, Scanned),
602            ]
603        );
604    }
605
606    #[test]
607    fn spanning_tree_insert_empty() {
608        let t = spanning_tree(&[
609            (0..3, Historic),
610            (3..6, Scanned),
611            (6..6, FoundNote),
612            (6..8, Scanned),
613            (8..10, ChainTip),
614        ])
615        .unwrap();
616
617        assert_eq!(
618            t.into_vec(),
619            vec![
620                scan_range(0..3, Historic),
621                scan_range(3..8, Scanned),
622                scan_range(8..10, ChainTip),
623            ]
624        );
625    }
626
627    #[test]
628    fn spanning_tree_insert_gaps() {
629        let t = spanning_tree(&[(0..3, Historic), (6..8, ChainTip)]).unwrap();
630
631        assert_eq!(
632            t.into_vec(),
633            vec![scan_range(0..6, Historic), scan_range(6..8, ChainTip),]
634        );
635
636        let t = spanning_tree(&[(0..3, Historic), (3..4, Verify), (6..8, ChainTip)]).unwrap();
637
638        assert_eq!(
639            t.into_vec(),
640            vec![
641                scan_range(0..3, Historic),
642                scan_range(3..4, Verify),
643                scan_range(4..6, Historic),
644                scan_range(6..8, ChainTip),
645            ]
646        );
647    }
648
649    #[test]
650    fn spanning_tree_insert_rfd_span() {
651        // This sequence of insertions causes a RightFirstDisjoint on the last insertion,
652        // which originally had a bug that caused the parent's span to only cover its left
653        // child. The bug was otherwise unobservable as the insertion logic was able to
654        // heal this specific kind of bug.
655        let t = spanning_tree(&[
656            // 6..8
657            (6..8, Scanned),
658            //       6..12
659            // 6..8        8..12
660            //         8..10  10..12
661            (10..12, ChainTip),
662            //          3..12
663            //    3..8        8..12
664            // 3..6  6..8  8..10  10..12
665            (3..6, Historic),
666        ])
667        .unwrap();
668
669        assert_eq!(t.span(), (3.into())..(12.into()));
670        assert_eq!(
671            t.into_vec(),
672            vec![
673                scan_range(3..6, Historic),
674                scan_range(6..8, Scanned),
675                scan_range(8..10, Historic),
676                scan_range(10..12, ChainTip),
677            ]
678        );
679    }
680
681    #[test]
682    fn spanning_tree_dominance() {
683        let t = spanning_tree(&[(0..3, Verify), (2..8, Scanned), (6..10, Verify)]).unwrap();
684        assert_eq!(
685            t.into_vec(),
686            vec![
687                scan_range(0..2, Verify),
688                scan_range(2..6, Scanned),
689                scan_range(6..10, Verify),
690            ]
691        );
692
693        let t = spanning_tree(&[(0..3, Verify), (2..8, Historic), (6..10, Verify)]).unwrap();
694        assert_eq!(
695            t.into_vec(),
696            vec![
697                scan_range(0..3, Verify),
698                scan_range(3..6, Historic),
699                scan_range(6..10, Verify),
700            ]
701        );
702
703        let t = spanning_tree(&[(0..3, Scanned), (2..8, Verify), (6..10, Scanned)]).unwrap();
704        assert_eq!(
705            t.into_vec(),
706            vec![
707                scan_range(0..2, Scanned),
708                scan_range(2..6, Verify),
709                scan_range(6..10, Scanned),
710            ]
711        );
712
713        let t = spanning_tree(&[(0..3, Scanned), (2..8, Historic), (6..10, Scanned)]).unwrap();
714        assert_eq!(
715            t.into_vec(),
716            vec![
717                scan_range(0..3, Scanned),
718                scan_range(3..6, Historic),
719                scan_range(6..10, Scanned),
720            ]
721        );
722
723        // a `ChainTip` insertion should not overwrite a scanned range.
724        let mut t = spanning_tree(&[(0..3, ChainTip), (3..5, Scanned), (5..7, ChainTip)]).unwrap();
725        t = t.insert(scan_range(0..7, ChainTip), false);
726        assert_eq!(
727            t.into_vec(),
728            vec![
729                scan_range(0..3, ChainTip),
730                scan_range(3..5, Scanned),
731                scan_range(5..7, ChainTip),
732            ]
733        );
734
735        let mut t =
736            spanning_tree(&[(280300..280310, FoundNote), (280310..280320, Scanned)]).unwrap();
737        assert_eq!(
738            t.clone().into_vec(),
739            vec![
740                scan_range(280300..280310, FoundNote),
741                scan_range(280310..280320, Scanned)
742            ]
743        );
744        t = t.insert(scan_range(280300..280340, ChainTip), false);
745        assert_eq!(
746            t.into_vec(),
747            vec![
748                scan_range(280300..280310, ChainTip),
749                scan_range(280310..280320, Scanned),
750                scan_range(280320..280340, ChainTip)
751            ]
752        );
753    }
754
755    #[test]
756    fn spanning_tree_insert_coalesce_scanned() {
757        let mut t = spanning_tree(&[
758            (0..3, Historic),
759            (2..5, Scanned),
760            (6..8, ChainTip),
761            (7..10, Scanned),
762        ])
763        .unwrap();
764
765        t = t.insert(scan_range(0..3, Scanned), false);
766        t = t.insert(scan_range(5..8, Scanned), false);
767
768        assert_eq!(t.into_vec(), vec![scan_range(0..10, Scanned)]);
769    }
770
771    #[test]
772    fn spanning_tree_force_rescans() {
773        let mut t = spanning_tree(&[
774            (0..3, Historic),
775            (3..5, Scanned),
776            (5..7, ChainTip),
777            (7..10, Scanned),
778        ])
779        .unwrap();
780
781        t = t.insert(scan_range(4..9, OpenAdjacent), true);
782
783        let expected = vec![
784            scan_range(0..3, Historic),
785            scan_range(3..4, Scanned),
786            scan_range(4..5, OpenAdjacent),
787            scan_range(5..7, ChainTip),
788            scan_range(7..9, OpenAdjacent),
789            scan_range(9..10, Scanned),
790        ];
791        assert_eq!(t.clone().into_vec(), expected);
792
793        // An insert of an ignored range should not override a scanned range; the existing
794        // priority should prevail, and so the expected state of the tree is unchanged.
795        t = t.insert(scan_range(2..5, Ignored), true);
796        assert_eq!(t.into_vec(), expected);
797    }
798}