zcash_client_backend: make the `SpanningTree` type usable outside of `zcash_client_sqlite`

This adds the `data_api::scanning::spanning_tree` module under
a new `unstable-spanning-tree` feature flag, making it available to
other implementations who want to be able to write their own storage
backends without having to reinvent the spanning tree logic.
This commit is contained in:
Kris Nuttycombe 2023-09-15 13:37:10 -06:00
parent 45ced4d164
commit 1575f2db88
5 changed files with 833 additions and 804 deletions

View File

@ -104,6 +104,7 @@ test-dependencies = [
]
unstable = ["byteorder"]
unstable-serialization = ["byteorder"]
unstable-spanning-tree = []
[lib]
bench = false

View File

@ -3,6 +3,9 @@ use std::ops::Range;
use zcash_primitives::consensus::BlockHeight;
#[cfg(feature = "unstable-spanning-tree")]
pub mod spanning_tree;
/// Scanning range priority levels.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ScanPriority {

View File

@ -0,0 +1,811 @@
use std::cmp::{max, Ordering};
use std::ops::{Not, Range};
use zcash_primitives::consensus::BlockHeight;
use super::{ScanPriority, ScanRange};
#[derive(Debug, Clone, Copy)]
enum InsertOn {
Left,
Right,
}
struct Insert {
on: InsertOn,
force_rescan: bool,
}
impl Insert {
fn left(force_rescan: bool) -> Self {
Insert {
on: InsertOn::Left,
force_rescan,
}
}
fn right(force_rescan: bool) -> Self {
Insert {
on: InsertOn::Right,
force_rescan,
}
}
}
impl Not for Insert {
type Output = Self;
fn not(self) -> Self::Output {
Insert {
on: match self.on {
InsertOn::Left => InsertOn::Right,
InsertOn::Right => InsertOn::Left,
},
force_rescan: self.force_rescan,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Dominance {
Left,
Right,
Equal,
}
impl From<Insert> for Dominance {
fn from(value: Insert) -> Self {
match value.on {
InsertOn::Left => Dominance::Left,
InsertOn::Right => Dominance::Right,
}
}
}
// This implements the dominance rule for range priority. If the inserted range's priority is
// `Verify`, this replaces any existing priority. Otherwise, if the current priority is
// `Scanned`, it remains as `Scanned`; and if the new priority is `Scanned`, it
// overrides any existing priority.
fn dominance(current: &ScanPriority, inserted: &ScanPriority, insert: Insert) -> Dominance {
match (current.cmp(inserted), (current, inserted)) {
(Ordering::Equal, _) => Dominance::Equal,
(_, (_, ScanPriority::Verify | ScanPriority::Scanned)) => Dominance::from(insert),
(_, (ScanPriority::Scanned, _)) if !insert.force_rescan => Dominance::from(!insert),
(Ordering::Less, _) => Dominance::from(insert),
(Ordering::Greater, _) => Dominance::from(!insert),
}
}
/// In the comments for each alternative, `()` represents the left range and `[]` represents the right range.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RangeOrdering {
/// `( ) [ ]`
LeftFirstDisjoint,
/// `( [ ) ]`
LeftFirstOverlap,
/// `[ ( ) ]`
LeftContained,
/// ```text
/// ( )
/// [ ]
/// ```
Equal,
/// `( [ ] )`
RightContained,
/// `[ ( ] )`
RightFirstOverlap,
/// `[ ] ( )`
RightFirstDisjoint,
}
impl RangeOrdering {
fn cmp<A: Ord>(a: &Range<A>, b: &Range<A>) -> Self {
use Ordering::*;
assert!(a.start <= a.end && b.start <= b.end);
match (a.start.cmp(&b.start), a.end.cmp(&b.end)) {
_ if a.end <= b.start => RangeOrdering::LeftFirstDisjoint,
_ if b.end <= a.start => RangeOrdering::RightFirstDisjoint,
(Less, Less) => RangeOrdering::LeftFirstOverlap,
(Equal, Less) | (Greater, Less) | (Greater, Equal) => RangeOrdering::LeftContained,
(Equal, Equal) => RangeOrdering::Equal,
(Equal, Greater) | (Less, Greater) | (Less, Equal) => RangeOrdering::RightContained,
(Greater, Greater) => RangeOrdering::RightFirstOverlap,
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum Joined {
One(ScanRange),
Two(ScanRange, ScanRange),
Three(ScanRange, ScanRange, ScanRange),
}
fn join_nonoverlapping(left: ScanRange, right: ScanRange) -> Joined {
assert!(left.block_range().end <= right.block_range().start);
if left.block_range().end == right.block_range().start {
if left.priority() == right.priority() {
Joined::One(ScanRange::from_parts(
left.block_range().start..right.block_range().end,
left.priority(),
))
} else {
Joined::Two(left, right)
}
} else {
// there is a gap that will need to be filled
let gap = ScanRange::from_parts(
left.block_range().end..right.block_range().start,
ScanPriority::Historic,
);
match join_nonoverlapping(left, gap) {
Joined::One(merged) => join_nonoverlapping(merged, right),
Joined::Two(left, gap) => match join_nonoverlapping(gap, right) {
Joined::One(merged) => Joined::Two(left, merged),
Joined::Two(gap, right) => Joined::Three(left, gap, right),
_ => unreachable!(),
},
_ => unreachable!(),
}
}
}
fn insert(current: ScanRange, to_insert: ScanRange, force_rescans: bool) -> Joined {
fn join_overlapping(left: ScanRange, right: ScanRange, insert: Insert) -> Joined {
assert!(
left.block_range().start <= right.block_range().start
&& left.block_range().end > right.block_range().start
);
// recompute the range dominance based upon the queue entry priorities
let dominance = match insert.on {
InsertOn::Left => dominance(&right.priority(), &left.priority(), insert),
InsertOn::Right => dominance(&left.priority(), &right.priority(), insert),
};
match dominance {
Dominance::Left => {
if let Some(right) = right.truncate_start(left.block_range().end) {
Joined::Two(left, right)
} else {
Joined::One(left)
}
}
Dominance::Equal => Joined::One(ScanRange::from_parts(
left.block_range().start..max(left.block_range().end, right.block_range().end),
left.priority(),
)),
Dominance::Right => match (
left.truncate_end(right.block_range().start),
left.truncate_start(right.block_range().end),
) {
(Some(before), Some(after)) => Joined::Three(before, right, after),
(Some(before), None) => Joined::Two(before, right),
(None, Some(after)) => Joined::Two(right, after),
(None, None) => Joined::One(right),
},
}
}
use RangeOrdering::*;
match RangeOrdering::cmp(to_insert.block_range(), current.block_range()) {
LeftFirstDisjoint => join_nonoverlapping(to_insert, current),
LeftFirstOverlap | RightContained => {
join_overlapping(to_insert, current, Insert::left(force_rescans))
}
Equal => Joined::One(ScanRange::from_parts(
to_insert.block_range().clone(),
match dominance(
&current.priority(),
&to_insert.priority(),
Insert::right(force_rescans),
) {
Dominance::Left | Dominance::Equal => current.priority(),
Dominance::Right => to_insert.priority(),
},
)),
RightFirstOverlap | LeftContained => {
join_overlapping(current, to_insert, Insert::right(force_rescans))
}
RightFirstDisjoint => join_nonoverlapping(current, to_insert),
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "unstable-spanning-tree")]
pub enum SpanningTree {
Leaf(ScanRange),
Parent {
span: Range<BlockHeight>,
left: Box<SpanningTree>,
right: Box<SpanningTree>,
},
}
#[cfg(feature = "unstable-spanning-tree")]
impl SpanningTree {
fn span(&self) -> Range<BlockHeight> {
match self {
SpanningTree::Leaf(entry) => entry.block_range().clone(),
SpanningTree::Parent { span, .. } => span.clone(),
}
}
fn from_joined(joined: Joined) -> Self {
match joined {
Joined::One(entry) => SpanningTree::Leaf(entry),
Joined::Two(left, right) => SpanningTree::Parent {
span: left.block_range().start..right.block_range().end,
left: Box::new(SpanningTree::Leaf(left)),
right: Box::new(SpanningTree::Leaf(right)),
},
Joined::Three(left, mid, right) => SpanningTree::Parent {
span: left.block_range().start..right.block_range().end,
left: Box::new(SpanningTree::Leaf(left)),
right: Box::new(SpanningTree::Parent {
span: mid.block_range().start..right.block_range().end,
left: Box::new(SpanningTree::Leaf(mid)),
right: Box::new(SpanningTree::Leaf(right)),
}),
},
}
}
fn from_insert(
left: Box<Self>,
right: Box<Self>,
to_insert: ScanRange,
insert: Insert,
) -> Self {
let (left, right) = match insert.on {
InsertOn::Left => (Box::new(left.insert(to_insert, insert.force_rescan)), right),
InsertOn::Right => (left, Box::new(right.insert(to_insert, insert.force_rescan))),
};
SpanningTree::Parent {
span: left.span().start..right.span().end,
left,
right,
}
}
fn from_split(
left: Self,
right: Self,
to_insert: ScanRange,
split_point: BlockHeight,
force_rescans: bool,
) -> Self {
let (l_insert, r_insert) = to_insert
.split_at(split_point)
.expect("Split point is within the range of to_insert");
let left = Box::new(left.insert(l_insert, force_rescans));
let right = Box::new(right.insert(r_insert, force_rescans));
SpanningTree::Parent {
span: left.span().start..right.span().end,
left,
right,
}
}
pub fn insert(self, to_insert: ScanRange, force_rescans: bool) -> Self {
match self {
SpanningTree::Leaf(cur) => Self::from_joined(insert(cur, to_insert, force_rescans)),
SpanningTree::Parent { span, left, right } => {
// This algorithm always preserves the existing partition point, and does not do
// any rebalancing or unification of ranges within the tree. This should be okay
// because `into_vec` performs such unification, and the tree being unbalanced
// should be fine given the relatively small number of ranges we should ordinarily
// be concerned with.
use RangeOrdering::*;
match RangeOrdering::cmp(&span, to_insert.block_range()) {
LeftFirstDisjoint => {
// extend the right-hand branch
Self::from_insert(left, right, to_insert, Insert::right(force_rescans))
}
LeftFirstOverlap => {
let split_point = left.span().end;
if split_point > to_insert.block_range().start {
Self::from_split(*left, *right, to_insert, split_point, force_rescans)
} else {
// to_insert is fully contained in or equals the right child
Self::from_insert(left, right, to_insert, Insert::right(force_rescans))
}
}
RightContained => {
// to_insert is fully contained within the current span, so we will insert
// into one or both sides
let split_point = left.span().end;
if to_insert.block_range().start >= split_point {
// to_insert is fully contained in the right
Self::from_insert(left, right, to_insert, Insert::right(force_rescans))
} else if to_insert.block_range().end <= split_point {
// to_insert is fully contained in the left
Self::from_insert(left, right, to_insert, Insert::left(force_rescans))
} else {
// to_insert must be split.
Self::from_split(*left, *right, to_insert, split_point, force_rescans)
}
}
Equal => {
let split_point = left.span().end;
if split_point > to_insert.block_range().start {
Self::from_split(*left, *right, to_insert, split_point, force_rescans)
} else {
// to_insert is fully contained in the right subtree
right.insert(to_insert, force_rescans)
}
}
LeftContained => {
// the current span is fully contained within to_insert, so we will extend
// or overwrite both sides
let split_point = left.span().end;
Self::from_split(*left, *right, to_insert, split_point, force_rescans)
}
RightFirstOverlap => {
let split_point = left.span().end;
if split_point < to_insert.block_range().end {
Self::from_split(*left, *right, to_insert, split_point, force_rescans)
} else {
// to_insert is fully contained in or equals the left child
Self::from_insert(left, right, to_insert, Insert::left(force_rescans))
}
}
RightFirstDisjoint => {
// extend the left-hand branch
Self::from_insert(left, right, to_insert, Insert::left(force_rescans))
}
}
}
}
}
pub fn into_vec(self) -> Vec<ScanRange> {
fn go(acc: &mut Vec<ScanRange>, tree: SpanningTree) {
match tree {
SpanningTree::Leaf(entry) => {
if !entry.is_empty() {
if let Some(top) = acc.pop() {
match join_nonoverlapping(top, entry) {
Joined::One(merged) => acc.push(merged),
Joined::Two(l, r) => {
acc.push(l);
acc.push(r);
}
_ => unreachable!(),
}
} else {
acc.push(entry);
}
}
}
SpanningTree::Parent { left, right, .. } => {
go(acc, *left);
go(acc, *right);
}
}
}
let mut acc = vec![];
go(&mut acc, self);
acc
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use std::ops::Range;
use zcash_primitives::consensus::BlockHeight;
use crate::data_api::scanning::{ScanPriority, ScanRange};
pub fn scan_range(range: Range<u32>, priority: ScanPriority) -> ScanRange {
ScanRange::from_parts(
BlockHeight::from(range.start)..BlockHeight::from(range.end),
priority,
)
}
}
#[cfg(test)]
mod tests {
use std::ops::Range;
use zcash_primitives::consensus::BlockHeight;
use super::{join_nonoverlapping, testing::scan_range, Joined, RangeOrdering, SpanningTree};
use crate::data_api::scanning::{ScanPriority, ScanRange};
#[test]
fn test_join_nonoverlapping() {
fn test_range(left: ScanRange, right: ScanRange, expected_joined: Joined) {
let joined = join_nonoverlapping(left, right);
assert_eq!(joined, expected_joined);
}
macro_rules! range {
( $start:expr, $end:expr; $priority:ident ) => {
ScanRange::from_parts(
BlockHeight::from($start)..BlockHeight::from($end),
ScanPriority::$priority,
)
};
}
macro_rules! joined {
(
($a_start:expr, $a_end:expr; $a_priority:ident)
) => {
Joined::One(
range!($a_start, $a_end; $a_priority)
)
};
(
($a_start:expr, $a_end:expr; $a_priority:ident),
($b_start:expr, $b_end:expr; $b_priority:ident)
) => {
Joined::Two(
range!($a_start, $a_end; $a_priority),
range!($b_start, $b_end; $b_priority)
)
};
(
($a_start:expr, $a_end:expr; $a_priority:ident),
($b_start:expr, $b_end:expr; $b_priority:ident),
($c_start:expr, $c_end:expr; $c_priority:ident)
) => {
Joined::Three(
range!($a_start, $a_end; $a_priority),
range!($b_start, $b_end; $b_priority),
range!($c_start, $c_end; $c_priority)
)
};
}
// Scan ranges have the same priority and
// line up.
test_range(
range!(1, 9; OpenAdjacent),
range!(9, 15; OpenAdjacent),
joined!(
(1, 15; OpenAdjacent)
),
);
// Scan ranges have different priorities,
// so we cannot merge them even though they
// line up.
test_range(
range!(1, 9; OpenAdjacent),
range!(9, 15; ChainTip),
joined!(
(1, 9; OpenAdjacent),
(9, 15; ChainTip)
),
);
// Scan ranges have the same priority but
// do not line up.
test_range(
range!(1, 9; OpenAdjacent),
range!(13, 15; OpenAdjacent),
joined!(
(1, 9; OpenAdjacent),
(9, 13; Historic),
(13, 15; OpenAdjacent)
),
);
test_range(
range!(1, 9; Historic),
range!(13, 15; OpenAdjacent),
joined!(
(1, 13; Historic),
(13, 15; OpenAdjacent)
),
);
test_range(
range!(1, 9; OpenAdjacent),
range!(13, 15; Historic),
joined!(
(1, 9; OpenAdjacent),
(9, 15; Historic)
),
);
}
#[test]
fn range_ordering() {
use super::RangeOrdering::*;
// Equal
assert_eq!(RangeOrdering::cmp(&(0..1), &(0..1)), Equal);
// Disjoint or contiguous
assert_eq!(RangeOrdering::cmp(&(0..1), &(1..2)), LeftFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(1..2), &(0..1)), RightFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(0..1), &(2..3)), LeftFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(2..3), &(0..1)), RightFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(1..2), &(2..2)), LeftFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(2..2), &(1..2)), RightFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(1..1), &(1..2)), LeftFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(1..2), &(1..1)), RightFirstDisjoint);
// Contained
assert_eq!(RangeOrdering::cmp(&(1..2), &(0..3)), LeftContained);
assert_eq!(RangeOrdering::cmp(&(0..3), &(1..2)), RightContained);
assert_eq!(RangeOrdering::cmp(&(0..1), &(0..3)), LeftContained);
assert_eq!(RangeOrdering::cmp(&(0..3), &(0..1)), RightContained);
assert_eq!(RangeOrdering::cmp(&(2..3), &(0..3)), LeftContained);
assert_eq!(RangeOrdering::cmp(&(0..3), &(2..3)), RightContained);
// Overlap
assert_eq!(RangeOrdering::cmp(&(0..2), &(1..3)), LeftFirstOverlap);
assert_eq!(RangeOrdering::cmp(&(1..3), &(0..2)), RightFirstOverlap);
}
fn spanning_tree(to_insert: &[(Range<u32>, ScanPriority)]) -> Option<SpanningTree> {
to_insert.iter().fold(None, |acc, (range, priority)| {
let scan_range = scan_range(range.clone(), *priority);
match acc {
None => Some(SpanningTree::Leaf(scan_range)),
Some(t) => Some(t.insert(scan_range, false)),
}
})
}
#[test]
fn spanning_tree_insert_adjacent() {
use ScanPriority::*;
let t = spanning_tree(&[
(0..3, Historic),
(3..6, Scanned),
(6..8, ChainTip),
(8..10, ChainTip),
])
.unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, Historic),
scan_range(3..6, Scanned),
scan_range(6..10, ChainTip),
]
);
}
#[test]
fn spanning_tree_insert_overlaps() {
use ScanPriority::*;
let t = spanning_tree(&[
(0..3, Historic),
(2..5, Scanned),
(6..8, ChainTip),
(7..10, Scanned),
])
.unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..2, Historic),
scan_range(2..5, Scanned),
scan_range(5..6, Historic),
scan_range(6..7, ChainTip),
scan_range(7..10, Scanned),
]
);
}
#[test]
fn spanning_tree_insert_empty() {
use ScanPriority::*;
let t = spanning_tree(&[
(0..3, Historic),
(3..6, Scanned),
(6..6, FoundNote),
(6..8, Scanned),
(8..10, ChainTip),
])
.unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, Historic),
scan_range(3..8, Scanned),
scan_range(8..10, ChainTip),
]
);
}
#[test]
fn spanning_tree_insert_gaps() {
use ScanPriority::*;
let t = spanning_tree(&[(0..3, Historic), (6..8, ChainTip)]).unwrap();
assert_eq!(
t.into_vec(),
vec![scan_range(0..6, Historic), scan_range(6..8, ChainTip),]
);
let t = spanning_tree(&[(0..3, Historic), (3..4, Verify), (6..8, ChainTip)]).unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, Historic),
scan_range(3..4, Verify),
scan_range(4..6, Historic),
scan_range(6..8, ChainTip),
]
);
}
#[test]
fn spanning_tree_insert_rfd_span() {
use ScanPriority::*;
// This sequence of insertions causes a RightFirstDisjoint on the last insertion,
// which originally had a bug that caused the parent's span to only cover its left
// child. The bug was otherwise unobservable as the insertion logic was able to
// heal this specific kind of bug.
let t = spanning_tree(&[
// 6..8
(6..8, Scanned),
// 6..12
// 6..8 8..12
// 8..10 10..12
(10..12, ChainTip),
// 3..12
// 3..8 8..12
// 3..6 6..8 8..10 10..12
(3..6, Historic),
])
.unwrap();
assert_eq!(t.span(), (3.into())..(12.into()));
assert_eq!(
t.into_vec(),
vec![
scan_range(3..6, Historic),
scan_range(6..8, Scanned),
scan_range(8..10, Historic),
scan_range(10..12, ChainTip),
]
);
}
#[test]
fn spanning_tree_dominance() {
use ScanPriority::*;
let t = spanning_tree(&[(0..3, Verify), (2..8, Scanned), (6..10, Verify)]).unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..2, Verify),
scan_range(2..6, Scanned),
scan_range(6..10, Verify),
]
);
let t = spanning_tree(&[(0..3, Verify), (2..8, Historic), (6..10, Verify)]).unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, Verify),
scan_range(3..6, Historic),
scan_range(6..10, Verify),
]
);
let t = spanning_tree(&[(0..3, Scanned), (2..8, Verify), (6..10, Scanned)]).unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..2, Scanned),
scan_range(2..6, Verify),
scan_range(6..10, Scanned),
]
);
let t = spanning_tree(&[(0..3, Scanned), (2..8, Historic), (6..10, Scanned)]).unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, Scanned),
scan_range(3..6, Historic),
scan_range(6..10, Scanned),
]
);
// a `ChainTip` insertion should not overwrite a scanned range.
let mut t = spanning_tree(&[(0..3, ChainTip), (3..5, Scanned), (5..7, ChainTip)]).unwrap();
t = t.insert(scan_range(0..7, ChainTip), false);
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, ChainTip),
scan_range(3..5, Scanned),
scan_range(5..7, ChainTip),
]
);
let mut t =
spanning_tree(&[(280300..280310, FoundNote), (280310..280320, Scanned)]).unwrap();
assert_eq!(
t.clone().into_vec(),
vec![
scan_range(280300..280310, FoundNote),
scan_range(280310..280320, Scanned)
]
);
t = t.insert(scan_range(280300..280340, ChainTip), false);
assert_eq!(
t.into_vec(),
vec![
scan_range(280300..280310, ChainTip),
scan_range(280310..280320, Scanned),
scan_range(280320..280340, ChainTip)
]
);
}
#[test]
fn spanning_tree_insert_coalesce_scanned() {
use ScanPriority::*;
let mut t = spanning_tree(&[
(0..3, Historic),
(2..5, Scanned),
(6..8, ChainTip),
(7..10, Scanned),
])
.unwrap();
t = t.insert(scan_range(0..3, Scanned), false);
t = t.insert(scan_range(5..8, Scanned), false);
assert_eq!(t.into_vec(), vec![scan_range(0..10, Scanned)]);
}
#[test]
fn spanning_tree_force_rescans() {
use ScanPriority::*;
let mut t = spanning_tree(&[
(0..3, Historic),
(3..5, Scanned),
(5..7, ChainTip),
(7..10, Scanned),
])
.unwrap();
t = t.insert(scan_range(4..9, OpenAdjacent), true);
let expected = vec![
scan_range(0..3, Historic),
scan_range(3..4, Scanned),
scan_range(4..5, OpenAdjacent),
scan_range(5..7, ChainTip),
scan_range(7..9, OpenAdjacent),
scan_range(9..10, Scanned),
];
assert_eq!(t.clone().into_vec(), expected);
// An insert of an ignored range should not override a scanned range; the existing
// priority should prevail, and so the expected state of the tree is unchanged.
t = t.insert(scan_range(2..5, Ignored), true);
assert_eq!(t.into_vec(), expected);
}
}

View File

@ -14,7 +14,7 @@ edition = "2021"
rust-version = "1.65"
[dependencies]
zcash_client_backend = { version = "=0.10.0-rc.2", path = "../zcash_client_backend", features = ["unstable-serialization"]}
zcash_client_backend = { version = "=0.10.0-rc.2", path = "../zcash_client_backend", features = ["unstable-serialization", "unstable-spanning-tree"]}
zcash_encoding = { version = "0.2", path = "../components/zcash_encoding" }
zcash_primitives = { version = "=0.13.0-rc.1", path = "../zcash_primitives", default-features = false }

View File

@ -1,16 +1,18 @@
use rusqlite::{self, named_params, types::Value, OptionalExtension};
use shardtree::error::ShardTreeError;
use std::cmp::{max, min, Ordering};
use std::cmp::{max, min};
use std::collections::BTreeSet;
use std::ops::{Not, Range};
use std::ops::Range;
use std::rc::Rc;
use tracing::{debug, trace};
use incrementalmerkletree::{Address, Position};
use zcash_client_backend::data_api::scanning::{ScanPriority, ScanRange};
use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade};
use zcash_client_backend::data_api::SAPLING_SHARD_HEIGHT;
use zcash_client_backend::data_api::{
scanning::{spanning_tree::SpanningTree, ScanPriority, ScanRange},
SAPLING_SHARD_HEIGHT,
};
use crate::{
error::SqliteClientError,
@ -20,60 +22,16 @@ use crate::{
use super::wallet_birthday;
#[derive(Debug, Clone, Copy)]
enum InsertOn {
Left,
Right,
}
struct Insert {
on: InsertOn,
force_rescan: bool,
}
impl Insert {
fn left(force_rescan: bool) -> Self {
Insert {
on: InsertOn::Left,
force_rescan,
}
}
fn right(force_rescan: bool) -> Self {
Insert {
on: InsertOn::Right,
force_rescan,
}
}
}
impl Not for Insert {
type Output = Self;
fn not(self) -> Self::Output {
Insert {
on: match self.on {
InsertOn::Left => InsertOn::Right,
InsertOn::Right => InsertOn::Left,
},
force_rescan: self.force_rescan,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Dominance {
Left,
Right,
Equal,
}
impl From<Insert> for Dominance {
fn from(value: Insert) -> Self {
match value.on {
InsertOn::Left => Dominance::Left,
InsertOn::Right => Dominance::Right,
}
pub(crate) fn priority_code(priority: &ScanPriority) -> i64 {
use ScanPriority::*;
match priority {
Ignored => 0,
Scanned => 10,
Historic => 20,
OpenAdjacent => 30,
FoundNote => 40,
ChainTip => 50,
Verify => 60,
}
}
@ -91,19 +49,6 @@ pub(crate) fn parse_priority_code(code: i64) -> Option<ScanPriority> {
}
}
pub(crate) fn priority_code(priority: &ScanPriority) -> i64 {
use ScanPriority::*;
match priority {
Ignored => 0,
Scanned => 10,
Historic => 20,
OpenAdjacent => 30,
FoundNote => 40,
ChainTip => 50,
Verify => 60,
}
}
pub(crate) fn suggest_scan_ranges(
conn: &rusqlite::Connection,
min_priority: ScanPriority,
@ -135,335 +80,6 @@ pub(crate) fn suggest_scan_ranges(
Ok(result)
}
// This implements the dominance rule for range priority. If the inserted range's priority is
// `Verify`, this replaces any existing priority. Otherwise, if the current priority is
// `Scanned`, it remains as `Scanned`; and if the new priority is `Scanned`, it
// overrides any existing priority.
fn dominance(current: &ScanPriority, inserted: &ScanPriority, insert: Insert) -> Dominance {
match (current.cmp(inserted), (current, inserted)) {
(Ordering::Equal, _) => Dominance::Equal,
(_, (_, ScanPriority::Verify | ScanPriority::Scanned)) => Dominance::from(insert),
(_, (ScanPriority::Scanned, _)) if !insert.force_rescan => Dominance::from(!insert),
(Ordering::Less, _) => Dominance::from(insert),
(Ordering::Greater, _) => Dominance::from(!insert),
}
}
/// In the comments for each alternative, `()` represents the left range and `[]` represents the right range.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RangeOrdering {
/// `( ) [ ]`
LeftFirstDisjoint,
/// `( [ ) ]`
LeftFirstOverlap,
/// `[ ( ) ]`
LeftContained,
/// ```text
/// ( )
/// [ ]
/// ```
Equal,
/// `( [ ] )`
RightContained,
/// `[ ( ] )`
RightFirstOverlap,
/// `[ ] ( )`
RightFirstDisjoint,
}
impl RangeOrdering {
fn cmp<A: Ord>(a: &Range<A>, b: &Range<A>) -> Self {
use Ordering::*;
assert!(a.start <= a.end && b.start <= b.end);
match (a.start.cmp(&b.start), a.end.cmp(&b.end)) {
_ if a.end <= b.start => RangeOrdering::LeftFirstDisjoint,
_ if b.end <= a.start => RangeOrdering::RightFirstDisjoint,
(Less, Less) => RangeOrdering::LeftFirstOverlap,
(Equal, Less) | (Greater, Less) | (Greater, Equal) => RangeOrdering::LeftContained,
(Equal, Equal) => RangeOrdering::Equal,
(Equal, Greater) | (Less, Greater) | (Less, Equal) => RangeOrdering::RightContained,
(Greater, Greater) => RangeOrdering::RightFirstOverlap,
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum Joined {
One(ScanRange),
Two(ScanRange, ScanRange),
Three(ScanRange, ScanRange, ScanRange),
}
fn join_nonoverlapping(left: ScanRange, right: ScanRange) -> Joined {
assert!(left.block_range().end <= right.block_range().start);
if left.block_range().end == right.block_range().start {
if left.priority() == right.priority() {
Joined::One(ScanRange::from_parts(
left.block_range().start..right.block_range().end,
left.priority(),
))
} else {
Joined::Two(left, right)
}
} else {
// there is a gap that will need to be filled
let gap = ScanRange::from_parts(
left.block_range().end..right.block_range().start,
ScanPriority::Historic,
);
match join_nonoverlapping(left, gap) {
Joined::One(merged) => join_nonoverlapping(merged, right),
Joined::Two(left, gap) => match join_nonoverlapping(gap, right) {
Joined::One(merged) => Joined::Two(left, merged),
Joined::Two(gap, right) => Joined::Three(left, gap, right),
_ => unreachable!(),
},
_ => unreachable!(),
}
}
}
fn insert(current: ScanRange, to_insert: ScanRange, force_rescans: bool) -> Joined {
fn join_overlapping(left: ScanRange, right: ScanRange, insert: Insert) -> Joined {
assert!(
left.block_range().start <= right.block_range().start
&& left.block_range().end > right.block_range().start
);
// recompute the range dominance based upon the queue entry priorities
let dominance = match insert.on {
InsertOn::Left => dominance(&right.priority(), &left.priority(), insert),
InsertOn::Right => dominance(&left.priority(), &right.priority(), insert),
};
match dominance {
Dominance::Left => {
if let Some(right) = right.truncate_start(left.block_range().end) {
Joined::Two(left, right)
} else {
Joined::One(left)
}
}
Dominance::Equal => Joined::One(ScanRange::from_parts(
left.block_range().start..max(left.block_range().end, right.block_range().end),
left.priority(),
)),
Dominance::Right => match (
left.truncate_end(right.block_range().start),
left.truncate_start(right.block_range().end),
) {
(Some(before), Some(after)) => Joined::Three(before, right, after),
(Some(before), None) => Joined::Two(before, right),
(None, Some(after)) => Joined::Two(right, after),
(None, None) => Joined::One(right),
},
}
}
use RangeOrdering::*;
match RangeOrdering::cmp(to_insert.block_range(), current.block_range()) {
LeftFirstDisjoint => join_nonoverlapping(to_insert, current),
LeftFirstOverlap | RightContained => {
join_overlapping(to_insert, current, Insert::left(force_rescans))
}
Equal => Joined::One(ScanRange::from_parts(
to_insert.block_range().clone(),
match dominance(
&current.priority(),
&to_insert.priority(),
Insert::right(force_rescans),
) {
Dominance::Left | Dominance::Equal => current.priority(),
Dominance::Right => to_insert.priority(),
},
)),
RightFirstOverlap | LeftContained => {
join_overlapping(current, to_insert, Insert::right(force_rescans))
}
RightFirstDisjoint => join_nonoverlapping(current, to_insert),
}
}
#[derive(Debug, Clone)]
enum SpanningTree {
Leaf(ScanRange),
Parent {
span: Range<BlockHeight>,
left: Box<SpanningTree>,
right: Box<SpanningTree>,
},
}
impl SpanningTree {
fn span(&self) -> Range<BlockHeight> {
match self {
SpanningTree::Leaf(entry) => entry.block_range().clone(),
SpanningTree::Parent { span, .. } => span.clone(),
}
}
fn from_joined(joined: Joined) -> Self {
match joined {
Joined::One(entry) => SpanningTree::Leaf(entry),
Joined::Two(left, right) => SpanningTree::Parent {
span: left.block_range().start..right.block_range().end,
left: Box::new(SpanningTree::Leaf(left)),
right: Box::new(SpanningTree::Leaf(right)),
},
Joined::Three(left, mid, right) => SpanningTree::Parent {
span: left.block_range().start..right.block_range().end,
left: Box::new(SpanningTree::Leaf(left)),
right: Box::new(SpanningTree::Parent {
span: mid.block_range().start..right.block_range().end,
left: Box::new(SpanningTree::Leaf(mid)),
right: Box::new(SpanningTree::Leaf(right)),
}),
},
}
}
fn from_insert(
left: Box<Self>,
right: Box<Self>,
to_insert: ScanRange,
insert: Insert,
) -> Self {
let (left, right) = match insert.on {
InsertOn::Left => (Box::new(left.insert(to_insert, insert.force_rescan)), right),
InsertOn::Right => (left, Box::new(right.insert(to_insert, insert.force_rescan))),
};
SpanningTree::Parent {
span: left.span().start..right.span().end,
left,
right,
}
}
fn from_split(
left: Self,
right: Self,
to_insert: ScanRange,
split_point: BlockHeight,
force_rescans: bool,
) -> Self {
let (l_insert, r_insert) = to_insert
.split_at(split_point)
.expect("Split point is within the range of to_insert");
let left = Box::new(left.insert(l_insert, force_rescans));
let right = Box::new(right.insert(r_insert, force_rescans));
SpanningTree::Parent {
span: left.span().start..right.span().end,
left,
right,
}
}
fn insert(self, to_insert: ScanRange, force_rescans: bool) -> Self {
match self {
SpanningTree::Leaf(cur) => Self::from_joined(insert(cur, to_insert, force_rescans)),
SpanningTree::Parent { span, left, right } => {
// This algorithm always preserves the existing partition point, and does not do
// any rebalancing or unification of ranges within the tree. This should be okay
// because `into_vec` performs such unification, and the tree being unbalanced
// should be fine given the relatively small number of ranges we should ordinarily
// be concerned with.
use RangeOrdering::*;
match RangeOrdering::cmp(&span, to_insert.block_range()) {
LeftFirstDisjoint => {
// extend the right-hand branch
Self::from_insert(left, right, to_insert, Insert::right(force_rescans))
}
LeftFirstOverlap => {
let split_point = left.span().end;
if split_point > to_insert.block_range().start {
Self::from_split(*left, *right, to_insert, split_point, force_rescans)
} else {
// to_insert is fully contained in or equals the right child
Self::from_insert(left, right, to_insert, Insert::right(force_rescans))
}
}
RightContained => {
// to_insert is fully contained within the current span, so we will insert
// into one or both sides
let split_point = left.span().end;
if to_insert.block_range().start >= split_point {
// to_insert is fully contained in the right
Self::from_insert(left, right, to_insert, Insert::right(force_rescans))
} else if to_insert.block_range().end <= split_point {
// to_insert is fully contained in the left
Self::from_insert(left, right, to_insert, Insert::left(force_rescans))
} else {
// to_insert must be split.
Self::from_split(*left, *right, to_insert, split_point, force_rescans)
}
}
Equal => {
let split_point = left.span().end;
if split_point > to_insert.block_range().start {
Self::from_split(*left, *right, to_insert, split_point, force_rescans)
} else {
// to_insert is fully contained in the right subtree
right.insert(to_insert, force_rescans)
}
}
LeftContained => {
// the current span is fully contained within to_insert, so we will extend
// or overwrite both sides
let split_point = left.span().end;
Self::from_split(*left, *right, to_insert, split_point, force_rescans)
}
RightFirstOverlap => {
let split_point = left.span().end;
if split_point < to_insert.block_range().end {
Self::from_split(*left, *right, to_insert, split_point, force_rescans)
} else {
// to_insert is fully contained in or equals the left child
Self::from_insert(left, right, to_insert, Insert::left(force_rescans))
}
}
RightFirstDisjoint => {
// extend the left-hand branch
Self::from_insert(left, right, to_insert, Insert::left(force_rescans))
}
}
}
}
}
fn into_vec(self) -> Vec<ScanRange> {
fn go(acc: &mut Vec<ScanRange>, tree: SpanningTree) {
match tree {
SpanningTree::Leaf(entry) => {
if !entry.is_empty() {
if let Some(top) = acc.pop() {
match join_nonoverlapping(top, entry) {
Joined::One(merged) => acc.push(merged),
Joined::Two(l, r) => {
acc.push(l);
acc.push(r);
}
_ => unreachable!(),
}
} else {
acc.push(entry);
}
}
}
SpanningTree::Parent { left, right, .. } => {
go(acc, *left);
go(acc, *right);
}
}
}
let mut acc = vec![];
go(&mut acc, self);
acc
}
}
pub(crate) fn insert_queue_entries<'a>(
conn: &rusqlite::Connection,
entries: impl Iterator<Item = &'a ScanRange>,
@ -883,14 +499,12 @@ pub(crate) fn update_chain_tip<P: consensus::Parameters>(
#[cfg(test)]
pub(crate) mod tests {
use std::ops::Range;
use incrementalmerkletree::{frontier::Frontier, Hashable, Level, Position};
use secrecy::SecretVec;
use zcash_client_backend::data_api::{
chain::CommitmentTreeRoot,
scanning::{ScanPriority, ScanRange},
scanning::{spanning_tree::testing::scan_range, ScanPriority},
AccountBirthday, Ratio, WalletCommitmentTrees, WalletRead, WalletWrite,
SAPLING_SHARD_HEIGHT,
};
@ -909,406 +523,6 @@ pub(crate) mod tests {
VERIFY_LOOKAHEAD,
};
use super::{join_nonoverlapping, Joined, RangeOrdering, SpanningTree};
#[test]
fn test_join_nonoverlapping() {
fn test_range(left: ScanRange, right: ScanRange, expected_joined: Joined) {
let joined = join_nonoverlapping(left, right);
assert_eq!(joined, expected_joined);
}
macro_rules! range {
( $start:expr, $end:expr; $priority:ident ) => {
ScanRange::from_parts(
BlockHeight::from($start)..BlockHeight::from($end),
ScanPriority::$priority,
)
};
}
macro_rules! joined {
(
($a_start:expr, $a_end:expr; $a_priority:ident)
) => {
Joined::One(
range!($a_start, $a_end; $a_priority)
)
};
(
($a_start:expr, $a_end:expr; $a_priority:ident),
($b_start:expr, $b_end:expr; $b_priority:ident)
) => {
Joined::Two(
range!($a_start, $a_end; $a_priority),
range!($b_start, $b_end; $b_priority)
)
};
(
($a_start:expr, $a_end:expr; $a_priority:ident),
($b_start:expr, $b_end:expr; $b_priority:ident),
($c_start:expr, $c_end:expr; $c_priority:ident)
) => {
Joined::Three(
range!($a_start, $a_end; $a_priority),
range!($b_start, $b_end; $b_priority),
range!($c_start, $c_end; $c_priority)
)
};
}
// Scan ranges have the same priority and
// line up.
test_range(
range!(1, 9; OpenAdjacent),
range!(9, 15; OpenAdjacent),
joined!(
(1, 15; OpenAdjacent)
),
);
// Scan ranges have different priorities,
// so we cannot merge them even though they
// line up.
test_range(
range!(1, 9; OpenAdjacent),
range!(9, 15; ChainTip),
joined!(
(1, 9; OpenAdjacent),
(9, 15; ChainTip)
),
);
// Scan ranges have the same priority but
// do not line up.
test_range(
range!(1, 9; OpenAdjacent),
range!(13, 15; OpenAdjacent),
joined!(
(1, 9; OpenAdjacent),
(9, 13; Historic),
(13, 15; OpenAdjacent)
),
);
test_range(
range!(1, 9; Historic),
range!(13, 15; OpenAdjacent),
joined!(
(1, 13; Historic),
(13, 15; OpenAdjacent)
),
);
test_range(
range!(1, 9; OpenAdjacent),
range!(13, 15; Historic),
joined!(
(1, 9; OpenAdjacent),
(9, 15; Historic)
),
);
}
#[test]
fn range_ordering() {
use super::RangeOrdering::*;
// Equal
assert_eq!(RangeOrdering::cmp(&(0..1), &(0..1)), Equal);
// Disjoint or contiguous
assert_eq!(RangeOrdering::cmp(&(0..1), &(1..2)), LeftFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(1..2), &(0..1)), RightFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(0..1), &(2..3)), LeftFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(2..3), &(0..1)), RightFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(1..2), &(2..2)), LeftFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(2..2), &(1..2)), RightFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(1..1), &(1..2)), LeftFirstDisjoint);
assert_eq!(RangeOrdering::cmp(&(1..2), &(1..1)), RightFirstDisjoint);
// Contained
assert_eq!(RangeOrdering::cmp(&(1..2), &(0..3)), LeftContained);
assert_eq!(RangeOrdering::cmp(&(0..3), &(1..2)), RightContained);
assert_eq!(RangeOrdering::cmp(&(0..1), &(0..3)), LeftContained);
assert_eq!(RangeOrdering::cmp(&(0..3), &(0..1)), RightContained);
assert_eq!(RangeOrdering::cmp(&(2..3), &(0..3)), LeftContained);
assert_eq!(RangeOrdering::cmp(&(0..3), &(2..3)), RightContained);
// Overlap
assert_eq!(RangeOrdering::cmp(&(0..2), &(1..3)), LeftFirstOverlap);
assert_eq!(RangeOrdering::cmp(&(1..3), &(0..2)), RightFirstOverlap);
}
fn scan_range(range: Range<u32>, priority: ScanPriority) -> ScanRange {
ScanRange::from_parts(
BlockHeight::from(range.start)..BlockHeight::from(range.end),
priority,
)
}
fn spanning_tree(to_insert: &[(Range<u32>, ScanPriority)]) -> Option<SpanningTree> {
to_insert.iter().fold(None, |acc, (range, priority)| {
let scan_range = scan_range(range.clone(), *priority);
match acc {
None => Some(SpanningTree::Leaf(scan_range)),
Some(t) => Some(t.insert(scan_range, false)),
}
})
}
#[test]
fn spanning_tree_insert_adjacent() {
use ScanPriority::*;
let t = spanning_tree(&[
(0..3, Historic),
(3..6, Scanned),
(6..8, ChainTip),
(8..10, ChainTip),
])
.unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, Historic),
scan_range(3..6, Scanned),
scan_range(6..10, ChainTip),
]
);
}
#[test]
fn spanning_tree_insert_overlaps() {
use ScanPriority::*;
let t = spanning_tree(&[
(0..3, Historic),
(2..5, Scanned),
(6..8, ChainTip),
(7..10, Scanned),
])
.unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..2, Historic),
scan_range(2..5, Scanned),
scan_range(5..6, Historic),
scan_range(6..7, ChainTip),
scan_range(7..10, Scanned),
]
);
}
#[test]
fn spanning_tree_insert_empty() {
use ScanPriority::*;
let t = spanning_tree(&[
(0..3, Historic),
(3..6, Scanned),
(6..6, FoundNote),
(6..8, Scanned),
(8..10, ChainTip),
])
.unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, Historic),
scan_range(3..8, Scanned),
scan_range(8..10, ChainTip),
]
);
}
#[test]
fn spanning_tree_insert_gaps() {
use ScanPriority::*;
let t = spanning_tree(&[(0..3, Historic), (6..8, ChainTip)]).unwrap();
assert_eq!(
t.into_vec(),
vec![scan_range(0..6, Historic), scan_range(6..8, ChainTip),]
);
let t = spanning_tree(&[(0..3, Historic), (3..4, Verify), (6..8, ChainTip)]).unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, Historic),
scan_range(3..4, Verify),
scan_range(4..6, Historic),
scan_range(6..8, ChainTip),
]
);
}
#[test]
fn spanning_tree_insert_rfd_span() {
use ScanPriority::*;
// This sequence of insertions causes a RightFirstDisjoint on the last insertion,
// which originally had a bug that caused the parent's span to only cover its left
// child. The bug was otherwise unobservable as the insertion logic was able to
// heal this specific kind of bug.
let t = spanning_tree(&[
// 6..8
(6..8, Scanned),
// 6..12
// 6..8 8..12
// 8..10 10..12
(10..12, ChainTip),
// 3..12
// 3..8 8..12
// 3..6 6..8 8..10 10..12
(3..6, Historic),
])
.unwrap();
assert_eq!(t.span(), (3.into())..(12.into()));
assert_eq!(
t.into_vec(),
vec![
scan_range(3..6, Historic),
scan_range(6..8, Scanned),
scan_range(8..10, Historic),
scan_range(10..12, ChainTip),
]
);
}
#[test]
fn spanning_tree_dominance() {
use ScanPriority::*;
let t = spanning_tree(&[(0..3, Verify), (2..8, Scanned), (6..10, Verify)]).unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..2, Verify),
scan_range(2..6, Scanned),
scan_range(6..10, Verify),
]
);
let t = spanning_tree(&[(0..3, Verify), (2..8, Historic), (6..10, Verify)]).unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, Verify),
scan_range(3..6, Historic),
scan_range(6..10, Verify),
]
);
let t = spanning_tree(&[(0..3, Scanned), (2..8, Verify), (6..10, Scanned)]).unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..2, Scanned),
scan_range(2..6, Verify),
scan_range(6..10, Scanned),
]
);
let t = spanning_tree(&[(0..3, Scanned), (2..8, Historic), (6..10, Scanned)]).unwrap();
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, Scanned),
scan_range(3..6, Historic),
scan_range(6..10, Scanned),
]
);
// a `ChainTip` insertion should not overwrite a scanned range.
let mut t = spanning_tree(&[(0..3, ChainTip), (3..5, Scanned), (5..7, ChainTip)]).unwrap();
t = t.insert(scan_range(0..7, ChainTip), false);
assert_eq!(
t.into_vec(),
vec![
scan_range(0..3, ChainTip),
scan_range(3..5, Scanned),
scan_range(5..7, ChainTip),
]
);
let mut t =
spanning_tree(&[(280300..280310, FoundNote), (280310..280320, Scanned)]).unwrap();
assert_eq!(
t.clone().into_vec(),
vec![
scan_range(280300..280310, FoundNote),
scan_range(280310..280320, Scanned)
]
);
t = t.insert(scan_range(280300..280340, ChainTip), false);
assert_eq!(
t.into_vec(),
vec![
scan_range(280300..280310, ChainTip),
scan_range(280310..280320, Scanned),
scan_range(280320..280340, ChainTip)
]
);
}
#[test]
fn spanning_tree_insert_coalesce_scanned() {
use ScanPriority::*;
let mut t = spanning_tree(&[
(0..3, Historic),
(2..5, Scanned),
(6..8, ChainTip),
(7..10, Scanned),
])
.unwrap();
t = t.insert(scan_range(0..3, Scanned), false);
t = t.insert(scan_range(5..8, Scanned), false);
assert_eq!(t.into_vec(), vec![scan_range(0..10, Scanned)]);
}
#[test]
fn spanning_tree_force_rescans() {
use ScanPriority::*;
let mut t = spanning_tree(&[
(0..3, Historic),
(3..5, Scanned),
(5..7, ChainTip),
(7..10, Scanned),
])
.unwrap();
t = t.insert(scan_range(4..9, OpenAdjacent), true);
let expected = vec![
scan_range(0..3, Historic),
scan_range(3..4, Scanned),
scan_range(4..5, OpenAdjacent),
scan_range(5..7, ChainTip),
scan_range(7..9, OpenAdjacent),
scan_range(9..10, Scanned),
];
assert_eq!(t.clone().into_vec(), expected);
// An insert of an ignored range should not override a scanned range; the existing
// priority should prevail, and so the expected state of the tree is unchanged.
t = t.insert(scan_range(2..5, Ignored), true);
assert_eq!(t.into_vec(), expected);
}
#[test]
fn scan_complete() {
use ScanPriority::*;