Merge pull request #973 from nuttycom/refactor/backend_spanning_tree

zcash_client_backend: make the `SpanningTree` type usable outside of `zcash_client_sqlite`
This commit is contained in:
Kris Nuttycombe 2023-09-18 15:25:24 -06:00 committed by GitHub
commit 4a752310e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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::*;