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:
parent
45ced4d164
commit
1575f2db88
|
@ -104,6 +104,7 @@ test-dependencies = [
|
|||
]
|
||||
unstable = ["byteorder"]
|
||||
unstable-serialization = ["byteorder"]
|
||||
unstable-spanning-tree = []
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
¤t.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);
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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(
|
||||
¤t.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::*;
|
||||
|
|
Loading…
Reference in New Issue