shardtree: Add `Pruned` node type

In circumstances where insertion into a subtree results in pruning, and
then a subsequent insertion within the same range contains leaves that
must be retained, it is necessary to be able to distinguish the maximum
position among notes that have been observed but later pruned. This
fixes a bug wherein an insertion into an already-pruned tree could cause
the maximum position reported for the subtree to regress.
This commit is contained in:
Kris Nuttycombe 2024-05-22 13:48:35 -06:00
parent ffc087424d
commit 7e48886fd3
5 changed files with 236 additions and 99 deletions

View File

@ -106,14 +106,14 @@ impl<C> Retention<C> {
/// Returns whether the associated node has [`Retention::Marked`] retention /// Returns whether the associated node has [`Retention::Marked`] retention
/// or [`Retention::Checkpoint`] retention with [`Marking::Marked`] marking. /// or [`Retention::Checkpoint`] retention with [`Marking::Marked`] marking.
pub fn is_marked(&self) -> bool { pub fn is_marked(&self) -> bool {
match self { matches!(
self,
Retention::Marked Retention::Marked
| Retention::Checkpoint { | Retention::Checkpoint {
marking: Marking::Marked, marking: Marking::Marked,
.. ..
} => true,
_ => false,
} }
)
} }
/// Applies the provided function to the checkpoint identifier, if any, and returns a new /// Applies the provided function to the checkpoint identifier, if any, and returns a new

View File

@ -8,8 +8,22 @@ and this project adheres to Rust's notion of
## Unreleased ## Unreleased
### Added ### Added
- `shardtree::tree::Tree::{is_leaf, map, try_map}` - `shardtree::tree::Tree::{is_leaf, map, try_map, empty_pruned}`
- `shardtree::tree::LocatedTree::{map, try_map}` - `shardtree::tree::LocatedTree::{map, try_map}`
- `shardtree::prunable::PrunableTree::{has_computable_root}`
### Changed
- `shardtree::tree::Node` has additional variant `Node::Pruned`.
### Removed
- `shardtree::tree::Tree::is_complete` as it is no longer well-defined in the
presence of `Pruned` nodes. Use `PrunableTree::has_computable_root` to
determine whether it is possible to compute the root of a tree.
### Fixed
- Fixes an error that could occur if an inserted `Frontier` node was
interpreted as a node that had actually had its value observed as though it
had been inserted using the ordinary tree insertion methods.
## [0.3.1] - 2024-04-03 ## [0.3.1] - 2024-04-03

View File

@ -234,16 +234,17 @@ impl<
let (append_result, position, checkpoint_id) = let (append_result, position, checkpoint_id) =
if let Some(subtree) = self.store.last_shard().map_err(ShardTreeError::Storage)? { if let Some(subtree) = self.store.last_shard().map_err(ShardTreeError::Storage)? {
if subtree.root.is_complete() { match subtree.max_position() {
// If the subtree is full, then construct a successor tree.
Some(pos) if pos == subtree.root_addr.max_position() => {
let addr = subtree.root_addr; let addr = subtree.root_addr;
if subtree.root_addr.index() < Self::max_subtree_index() {
if addr.index() < Self::max_subtree_index() {
LocatedTree::empty(addr.next_at_level()).append(value, retention)? LocatedTree::empty(addr.next_at_level()).append(value, retention)?
} else { } else {
return Err(InsertionError::TreeFull.into()); return Err(InsertionError::TreeFull.into());
} }
} else { }
subtree.append(value, retention)? _ => subtree.append(value, retention)?,
} }
} else { } else {
let root_addr = Address::from_parts(Self::subtree_level(), 0); let root_addr = Address::from_parts(Self::subtree_level(), 0);
@ -412,8 +413,8 @@ impl<
root_addr: Address, root_addr: Address,
root: &PrunableTree<H>, root: &PrunableTree<H>,
) -> Option<(PrunableTree<H>, Position)> { ) -> Option<(PrunableTree<H>, Position)> {
match root { match &root.0 {
Tree(Node::Parent { ann, left, right }) => { Node::Parent { ann, left, right } => {
let (l_addr, r_addr) = root_addr.children().unwrap(); let (l_addr, r_addr) = root_addr.children().unwrap();
go(r_addr, right).map_or_else( go(r_addr, right).map_or_else(
|| { || {
@ -442,13 +443,13 @@ impl<
}, },
) )
} }
Tree(Node::Leaf { value: (h, r) }) => Some(( Node::Leaf { value: (h, r) } => Some((
Tree(Node::Leaf { Tree(Node::Leaf {
value: (h.clone(), *r | RetentionFlags::CHECKPOINT), value: (h.clone(), *r | RetentionFlags::CHECKPOINT),
}), }),
root_addr.max_position(), root_addr.max_position(),
)), )),
Tree(Node::Nil) => None, Node::Nil | Node::Pruned => None,
} }
} }
@ -576,12 +577,19 @@ impl<
// Prune each affected subtree // Prune each affected subtree
for (subtree_addr, positions) in clear_positions.into_iter() { for (subtree_addr, positions) in clear_positions.into_iter() {
let cleared = self let to_clear = self
.store .store
.get_shard(subtree_addr) .get_shard(subtree_addr)
.map_err(ShardTreeError::Storage)? .map_err(ShardTreeError::Storage)?;
.map(|subtree| subtree.clear_flags(positions));
if let Some(cleared) = cleared { if let Some(to_clear) = to_clear {
let pre_clearing_max_position = to_clear.max_position();
let cleared = to_clear.clear_flags(positions);
// Clearing flags should not modify the max position of leaves represented
// in the shard.
assert!(cleared.max_position() == pre_clearing_max_position);
self.store self.store
.put_shard(cleared) .put_shard(cleared)
.map_err(ShardTreeError::Storage)?; .map_err(ShardTreeError::Storage)?;
@ -757,8 +765,8 @@ impl<
// roots. // roots.
truncate_at: Position, truncate_at: Position,
) -> Result<(H, Option<PrunableTree<H>>), ShardTreeError<S::Error>> { ) -> Result<(H, Option<PrunableTree<H>>), ShardTreeError<S::Error>> {
match &cap.root { match &cap.root.0 {
Tree(Node::Parent { ann, left, right }) => { Node::Parent { ann, left, right } => {
match ann { match ann {
Some(cached_root) if target_addr.contains(&cap.root_addr) => { Some(cached_root) if target_addr.contains(&cap.root_addr) => {
Ok((cached_root.as_ref().clone(), None)) Ok((cached_root.as_ref().clone(), None))
@ -832,7 +840,7 @@ impl<
} }
} }
} }
Tree(Node::Leaf { value }) => { Node::Leaf { value } => {
if truncate_at >= cap.root_addr.position_range_end() if truncate_at >= cap.root_addr.position_range_end()
&& target_addr.contains(&cap.root_addr) && target_addr.contains(&cap.root_addr)
{ {
@ -857,7 +865,7 @@ impl<
)) ))
} }
} }
Tree(Node::Nil) => { Node::Nil | Node::Pruned => {
if cap.root_addr == target_addr if cap.root_addr == target_addr
|| cap.root_addr.level() == ShardTree::<S, DEPTH, SHARD_HEIGHT>::subtree_level() || cap.root_addr.level() == ShardTree::<S, DEPTH, SHARD_HEIGHT>::subtree_level()
{ {
@ -867,7 +875,7 @@ impl<
Ok(( Ok((
root.clone(), root.clone(),
if truncate_at >= cap.root_addr.position_range_end() { if truncate_at >= cap.root_addr.position_range_end() {
// return the compute root as a new leaf to be cached if it contains no // return the computed root as a new leaf to be cached if it contains no
// empty hashes due to truncation // empty hashes due to truncation
Some(Tree::leaf((root, RetentionFlags::EPHEMERAL))) Some(Tree::leaf((root, RetentionFlags::EPHEMERAL)))
} else { } else {
@ -1301,21 +1309,24 @@ impl<
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::convert::Infallible;
use assert_matches::assert_matches; use assert_matches::assert_matches;
use proptest::prelude::*; use proptest::prelude::*;
use incrementalmerkletree::{ use incrementalmerkletree::{
frontier::NonEmptyFrontier, frontier::{Frontier, NonEmptyFrontier},
testing::{ testing::{
arb_operation, check_append, check_checkpoint_rewind, check_operations, arb_operation, check_append, check_checkpoint_rewind, check_operations,
check_remove_mark, check_rewind_remove_mark, check_root_hashes, check_remove_mark, check_rewind_remove_mark, check_root_hashes,
check_witness_consistency, check_witnesses, complete_tree::CompleteTree, CombinedTree, check_witness_consistency, check_witnesses, complete_tree::CompleteTree, CombinedTree,
SipHashable, SipHashable,
}, },
Address, Hashable, Level, Marking, Position, Retention, Address, Hashable, Level, Marking, MerklePath, Position, Retention,
}; };
use crate::{ use crate::{
error::{QueryError, ShardTreeError},
store::memory::MemoryShardStore, store::memory::MemoryShardStore,
testing::{ testing::{
arb_char_str, arb_shardtree, check_shard_sizes, check_shardtree_insertion, arb_char_str, arb_shardtree, check_shard_sizes, check_shardtree_insertion,
@ -1420,6 +1431,95 @@ mod tests {
); );
} }
#[test]
fn avoid_pruning_reference() {
fn test_with_marking(
frontier_marking: Marking,
) -> Result<MerklePath<String, 6>, ShardTreeError<Infallible>> {
let mut tree = ShardTree::<MemoryShardStore<String, usize>, 6, 3>::new(
MemoryShardStore::empty(),
5,
);
let frontier_end = Position::from((1 << 3) - 3);
let mut f0 = Frontier::<String, 6>::empty();
for c in 'a'..='f' {
f0.append(c.to_string());
}
let frontier = Frontier::from_parts(
frontier_end,
"f".to_owned(),
vec!["e".to_owned(), "abcd".to_owned()],
)
.unwrap();
// Insert a frontier two leaves from the end of the first shard, checkpointed,
// with the specified marking.
tree.insert_frontier(
frontier,
Retention::Checkpoint {
id: 1,
marking: frontier_marking,
},
)?;
// Insert a few leaves beginning at the subsequent position, so as to cross the shard
// boundary.
tree.batch_insert(
frontier_end + 1,
('g'..='j')
.into_iter()
.map(|c| (c.to_string(), Retention::Ephemeral)),
)?;
// Trigger pruning by adding 5 more checkpoints
for i in 2..7 {
tree.checkpoint(i).unwrap();
}
// Insert nodes that require the pruned nodes for witnessing
tree.batch_insert(
frontier_end - 1,
('e'..='f')
.into_iter()
.map(|c| (c.to_string(), Retention::Marked)),
)?;
// Compute the witness
tree.witness_at_checkpoint_id(frontier_end, &6)
}
// If we insert the frontier with Marking::None, the frontier nodes are treated
// as ephemeral nodes and are pruned, leaving an incomplete tree.
assert_matches!(
test_with_marking(Marking::None),
Err(ShardTreeError::Query(QueryError::TreeIncomplete(_)))
);
// If we insert the frontier with Marking::Reference, the frontier nodes will
// not be pruned on completion of the subtree, and thus we'll be able to compute
// the witness.
let expected_witness = MerklePath::from_parts(
[
"e",
"gh",
"abcd",
"ij______",
"________________",
"________________________________",
]
.iter()
.map(|s| s.to_string())
.collect(),
Position::from(5),
)
.unwrap();
let witness = test_with_marking(Marking::Reference).unwrap();
assert_eq!(witness, expected_witness);
}
// Combined tree tests // Combined tree tests
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn new_combined_tree<H: Hashable + Ord + Clone + core::fmt::Debug>( fn new_combined_tree<H: Hashable + Ord + Clone + core::fmt::Debug>(

View File

@ -100,12 +100,25 @@ impl<H: Hashable + Clone + PartialEq> PrunableTree<H> {
.map_or(false, |(_, retention)| retention.is_marked()) .map_or(false, |(_, retention)| retention.is_marked())
} }
/// Returns `true` if it is possible to compute or retrieve the Merkle root of this
/// tree.
pub fn has_computable_root(&self) -> bool {
match &self.0 {
Node::Parent { ann, left, right } => {
ann.is_some()
|| (left.as_ref().has_computable_root() && right.as_ref().has_computable_root())
}
Node::Leaf { .. } => true,
Node::Nil | Node::Pruned => false,
}
}
/// Determines whether a tree has any [`Retention::Marked`] nodes. /// Determines whether a tree has any [`Retention::Marked`] nodes.
pub fn contains_marked(&self) -> bool { pub fn contains_marked(&self) -> bool {
match &self.0 { match &self.0 {
Node::Parent { left, right, .. } => left.contains_marked() || right.contains_marked(), Node::Parent { left, right, .. } => left.contains_marked() || right.contains_marked(),
Node::Leaf { value: (_, r) } => r.is_marked(), Node::Leaf { value: (_, r) } => r.is_marked(),
Node::Nil => false, Node::Nil | Node::Pruned => false,
} }
} }
@ -122,8 +135,8 @@ impl<H: Hashable + Clone + PartialEq> PrunableTree<H> {
// so no need to inspect the tree // so no need to inspect the tree
Ok(H::empty_root(root_addr.level())) Ok(H::empty_root(root_addr.level()))
} else { } else {
match self { match &self.0 {
Tree(Node::Parent { ann, left, right }) => ann Node::Parent { ann, left, right } => ann
.as_ref() .as_ref()
.filter(|_| truncate_at >= root_addr.position_range_end()) .filter(|_| truncate_at >= root_addr.position_range_end())
.map_or_else( .map_or_else(
@ -145,7 +158,7 @@ impl<H: Hashable + Clone + PartialEq> PrunableTree<H> {
Ok(rc.as_ref().clone()) Ok(rc.as_ref().clone())
}, },
), ),
Tree(Node::Leaf { value }) => { Node::Leaf { value } => {
if truncate_at >= root_addr.position_range_end() { if truncate_at >= root_addr.position_range_end() {
// no truncation of this leaf is necessary, just use it // no truncation of this leaf is necessary, just use it
Ok(value.0.clone()) Ok(value.0.clone())
@ -157,7 +170,7 @@ impl<H: Hashable + Clone + PartialEq> PrunableTree<H> {
Err(vec![root_addr]) Err(vec![root_addr])
} }
} }
Tree(Node::Nil) => Err(vec![root_addr]), Node::Nil | Node::Pruned => Err(vec![root_addr]),
} }
} }
} }
@ -192,7 +205,7 @@ impl<H: Hashable + Clone + PartialEq> PrunableTree<H> {
} }
result result
} }
Node::Nil => BTreeSet::new(), Node::Nil | Node::Pruned => BTreeSet::new(),
} }
} }
@ -229,6 +242,7 @@ impl<H: Hashable + Clone + PartialEq> PrunableTree<H> {
let no_default_fill = addr.position_range_end(); let no_default_fill = addr.position_range_end();
match (t0, t1) { match (t0, t1) {
(Tree(Node::Nil), other) | (other, Tree(Node::Nil)) => Ok(other), (Tree(Node::Nil), other) | (other, Tree(Node::Nil)) => Ok(other),
(Tree(Node::Pruned), other) | (other, Tree(Node::Pruned)) => Ok(other),
(Tree(Node::Leaf { value: vl }), Tree(Node::Leaf { value: vr })) => { (Tree(Node::Leaf { value: vl }), Tree(Node::Leaf { value: vr })) => {
if vl.0 == vr.0 { if vl.0 == vr.0 {
// Merge the flags together. // Merge the flags together.
@ -301,6 +315,8 @@ impl<H: Hashable + Clone + PartialEq> PrunableTree<H> {
pub(crate) fn unite(level: Level, ann: Option<Arc<H>>, left: Self, right: Self) -> Self { pub(crate) fn unite(level: Level, ann: Option<Arc<H>>, left: Self, right: Self) -> Self {
match (left, right) { match (left, right) {
(Tree(Node::Nil), Tree(Node::Nil)) => Tree(Node::Nil), (Tree(Node::Nil), Tree(Node::Nil)) => Tree(Node::Nil),
(Tree(Node::Nil | Node::Pruned), Tree(Node::Pruned)) => Tree(Node::Pruned),
(Tree(Node::Pruned), Tree(Node::Nil)) => Tree(Node::Pruned),
(Tree(Node::Leaf { value: lv }), Tree(Node::Leaf { value: rv })) (Tree(Node::Leaf { value: lv }), Tree(Node::Leaf { value: rv }))
// we can prune right-hand leaves that are not marked or reference leaves; if a // we can prune right-hand leaves that are not marked or reference leaves; if a
// leaf is a checkpoint then that information will be propagated to the replacement // leaf is a checkpoint then that information will be propagated to the replacement
@ -518,7 +534,7 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
None None
} }
} }
Node::Nil => None, Node::Nil | Node::Pruned => None,
} }
} }
@ -555,7 +571,6 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
root_addr: Address, root_addr: Address,
into: &PrunableTree<H>, into: &PrunableTree<H>,
subtree: LocatedPrunableTree<H>, subtree: LocatedPrunableTree<H>,
is_complete: bool,
contains_marked: bool, contains_marked: bool,
) -> Result<(PrunableTree<H>, Vec<IncompleteAt>), InsertionError> { ) -> Result<(PrunableTree<H>, Vec<IncompleteAt>), InsertionError> {
trace!( trace!(
@ -571,7 +586,9 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
// In the case that we are replacing a node entirely, we need to extend the // In the case that we are replacing a node entirely, we need to extend the
// subtree up to the level of the node being replaced, adding Nil siblings // subtree up to the level of the node being replaced, adding Nil siblings
// and recording the presence of those incomplete nodes when necessary // and recording the presence of those incomplete nodes when necessary
let replacement = |ann: Option<Arc<H>>, mut node: LocatedPrunableTree<H>| { let replacement = |ann: Option<Arc<H>>,
mut node: LocatedPrunableTree<H>,
pruned: bool| {
// construct the replacement node bottom-up // construct the replacement node bottom-up
let mut incomplete = vec![]; let mut incomplete = vec![];
while node.root_addr.level() < root_addr.level() { while node.root_addr.level() < root_addr.level() {
@ -579,19 +596,21 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
address: node.root_addr.sibling(), address: node.root_addr.sibling(),
required_for_witness: contains_marked, required_for_witness: contains_marked,
}); });
let empty = Arc::new(Tree(if pruned { Node::Pruned } else { Node::Nil }));
let full = Arc::new(node.root);
node = LocatedTree { node = LocatedTree {
root_addr: node.root_addr.parent(), root_addr: node.root_addr.parent(),
root: if node.root_addr.is_right_child() { root: if node.root_addr.is_right_child() {
Tree(Node::Parent { Tree(Node::Parent {
ann: None, ann: None,
left: Arc::new(Tree(Node::Nil)), left: empty,
right: Arc::new(node.root), right: full,
}) })
} else { } else {
Tree(Node::Parent { Tree(Node::Parent {
ann: None, ann: None,
left: Arc::new(node.root), left: full,
right: Arc::new(Tree(Node::Nil)), right: empty,
}) })
}, },
}; };
@ -600,7 +619,8 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
}; };
match into { match into {
Tree(Node::Nil) => Ok(replacement(None, subtree)), Tree(Node::Nil) => Ok(replacement(None, subtree, false)),
Tree(Node::Pruned) => Ok(replacement(None, subtree, true)),
Tree(Node::Leaf { Tree(Node::Leaf {
value: (value, retention), value: (value, retention),
}) => { }) => {
@ -610,7 +630,7 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
// entirely with the subtree, or reannotate the root so as to avoid // entirely with the subtree, or reannotate the root so as to avoid
// discarding the existing leaf value. // discarding the existing leaf value.
if is_complete { if subtree.root.has_computable_root() {
Ok(( Ok((
if subtree.root.is_leaf() { if subtree.root.is_leaf() {
// When replacing a leaf with a leaf, `REFERENCE` retention // When replacing a leaf with a leaf, `REFERENCE` retention
@ -635,7 +655,7 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
)? )?
} else { } else {
// It is safe to replace the existing root unannotated, because we // It is safe to replace the existing root unannotated, because we
// can always recompute the root from a complete subtree. // can always recompute the root from the subtree.
subtree.root subtree.root
}, },
vec![], vec![],
@ -655,7 +675,22 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
Err(InsertionError::Conflict(root_addr)) Err(InsertionError::Conflict(root_addr))
} }
} else { } else {
Ok(replacement(Some(Arc::new(value.clone())), subtree)) Ok(replacement(
Some(Arc::new(value.clone())),
subtree,
// The subtree being inserted may have its root at some level lower
// than the next level down. The siblings of nodes that will be
// generated while descending to the subtree root level will be
// `Nil` nodes (indicating that the value of these nodes have never
// been observed) if the leaf being replaced has `REFERENCE`
// retention. Any other leaf without `REFERENCE` retention will
// have been produced by pruning of previously observed node
// values, so in those cases we use `Pruned` nodes for the absent
// siblings. This allows us to retain the distinction between what
// parts of the tree have been directly observed and what parts
// have not.
!retention.contains(RetentionFlags::REFERENCE),
))
} }
} }
parent if root_addr == subtree.root_addr => { parent if root_addr == subtree.root_addr => {
@ -673,7 +708,7 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
let (l_addr, r_addr) = root_addr.children().unwrap(); let (l_addr, r_addr) = root_addr.children().unwrap();
if l_addr.contains(&subtree.root_addr) { if l_addr.contains(&subtree.root_addr) {
let (new_left, incomplete) = let (new_left, incomplete) =
go(l_addr, left.as_ref(), subtree, is_complete, contains_marked)?; go(l_addr, left.as_ref(), subtree, contains_marked)?;
Ok(( Ok((
Tree::unite( Tree::unite(
root_addr.level() - 1, root_addr.level() - 1,
@ -684,13 +719,8 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
incomplete, incomplete,
)) ))
} else { } else {
let (new_right, incomplete) = go( let (new_right, incomplete) =
r_addr, go(r_addr, right.as_ref(), subtree, contains_marked)?;
right.as_ref(),
subtree,
is_complete,
contains_marked,
)?;
Ok(( Ok((
Tree::unite( Tree::unite(
root_addr.level() - 1, root_addr.level() - 1,
@ -710,22 +740,16 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
trace!( trace!(
max_position = ?max_position, max_position = ?max_position,
tree = ?self, tree = ?self,
subtree_complete = subtree.root.is_complete(),
to_insert = ?subtree, to_insert = ?subtree,
"Current shard" "Current shard"
); );
let LocatedTree { root_addr, root } = self; let LocatedTree { root_addr, root } = self;
if root_addr.contains(&subtree.root_addr) { if root_addr.contains(&subtree.root_addr) {
let complete = subtree.root.is_complete(); go(*root_addr, root, subtree, contains_marked).map(|(root, incomplete)| {
go(*root_addr, root, subtree, complete, contains_marked).map(|(root, incomplete)| {
let new_tree = LocatedTree { let new_tree = LocatedTree {
root_addr: *root_addr, root_addr: *root_addr,
root, root,
}; };
trace!(
max_position = ?new_tree.max_position(),
"Replacement shard"
);
assert!(new_tree.max_position() >= max_position); assert!(new_tree.max_position() >= max_position);
(new_tree, incomplete) (new_tree, incomplete)
}) })
@ -786,9 +810,7 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
split_at: Level, split_at: Level,
) -> (Self, Option<Self>) { ) -> (Self, Option<Self>) {
let mut addr = Address::from(position); let mut addr = Address::from(position);
let mut subtree = Tree(Node::Leaf { let mut subtree = Tree::leaf((leaf, leaf_retention.into()));
value: (leaf, leaf_retention.into()),
});
while addr.level() < split_at { while addr.level() < split_at {
if addr.is_left_child() { if addr.is_left_child() {
@ -890,8 +912,8 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
// nothing to do, so we just return the root // nothing to do, so we just return the root
root.clone() root.clone()
} else { } else {
match root { match &root.0 {
Tree(Node::Parent { ann, left, right }) => { Node::Parent { ann, left, right } => {
let (l_addr, r_addr) = root_addr.children().unwrap(); let (l_addr, r_addr) = root_addr.children().unwrap();
let p = to_clear.partition_point(|(p, _)| p < &l_addr.position_range_end()); let p = to_clear.partition_point(|(p, _)| p < &l_addr.position_range_end());
@ -908,7 +930,7 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
go(&to_clear[p..], r_addr, right), go(&to_clear[p..], r_addr, right),
) )
} }
Tree(Node::Leaf { value: (h, r) }) => { Node::Leaf { value: (h, r) } => {
trace!("In {:?}, clearing {:?}", root_addr, to_clear); trace!("In {:?}, clearing {:?}", root_addr, to_clear);
// When we reach a leaf, we should be down to just a single position // When we reach a leaf, we should be down to just a single position
// which should correspond to the last level-0 child of the address's // which should correspond to the last level-0 child of the address's
@ -916,18 +938,16 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
// a partially-pruned branch, and if it's a marked node then it will // a partially-pruned branch, and if it's a marked node then it will
// be a level-0 leaf. // be a level-0 leaf.
match to_clear { match to_clear {
[(pos, flags)] => { [(_, flags)] => Tree(Node::Leaf {
assert_eq!(*pos, root_addr.max_position());
Tree(Node::Leaf {
value: (h.clone(), *r & !*flags), value: (h.clone(), *r & !*flags),
}) }),
}
_ => { _ => {
panic!("Tree state inconsistent with checkpoints."); panic!("Tree state inconsistent with checkpoints.");
} }
} }
} }
Tree(Node::Nil) => Tree(Node::Nil), Node::Nil => Tree(Node::Nil),
Node::Pruned => Tree(Node::Pruned),
} }
} }
} }
@ -1129,7 +1149,7 @@ mod tests {
} }
#[test] #[test]
fn located_insert_subtree_leaf_overwrites() { fn located_insert_subtree_prevents_leaf_overwrite_conflict() {
let t: LocatedPrunableTree<String> = LocatedTree { let t: LocatedPrunableTree<String> = LocatedTree {
root_addr: Address::from_parts(2.into(), 1), root_addr: Address::from_parts(2.into(), 1),
root: parent(leaf(("a".to_string(), RetentionFlags::MARKED)), nil()), root: parent(leaf(("a".to_string(), RetentionFlags::MARKED)), nil()),

View File

@ -19,6 +19,9 @@ pub enum Node<C, A, V> {
Leaf { value: V }, Leaf { value: V },
/// The empty tree; a subtree or leaf for which no information is available. /// The empty tree; a subtree or leaf for which no information is available.
Nil, Nil,
/// An empty node in the tree created as a consequence of partial reinserion of data into a
/// subtree after the subtree was previously pruned.
Pruned,
} }
impl<C, A, V> Node<C, A, V> { impl<C, A, V> Node<C, A, V> {
@ -35,7 +38,8 @@ impl<C, A, V> Node<C, A, V> {
match self { match self {
Node::Parent { .. } => None, Node::Parent { .. } => None,
Node::Leaf { value } => Some(value), Node::Leaf { value } => Some(value),
Node::Nil { .. } => None, Node::Nil => None,
Node::Pruned => None,
} }
} }
@ -45,6 +49,7 @@ impl<C, A, V> Node<C, A, V> {
Node::Parent { ann, .. } => Some(ann), Node::Parent { ann, .. } => Some(ann),
Node::Leaf { .. } => None, Node::Leaf { .. } => None,
Node::Nil => None, Node::Nil => None,
Node::Pruned => None,
} }
} }
@ -71,6 +76,7 @@ impl<'a, C: Clone, A: Clone, V: Clone> Node<C, &'a A, &'a V> {
value: (*value).clone(), value: (*value).clone(),
}, },
Node::Nil => Node::Nil, Node::Nil => Node::Nil,
Node::Pruned => Node::Pruned,
} }
} }
} }
@ -92,6 +98,11 @@ impl<A, V> Tree<A, V> {
Tree(Node::Nil) Tree(Node::Nil)
} }
/// Constructs the empty tree consisting of a single pruned node.
pub fn empty_pruned() -> Self {
Tree(Node::Pruned)
}
/// Constructs a tree containing a single leaf. /// Constructs a tree containing a single leaf.
pub fn leaf(value: V) -> Self { pub fn leaf(value: V) -> Self {
Tree(Node::Leaf { value }) Tree(Node::Leaf { value })
@ -122,18 +133,8 @@ impl<A, V> Tree<A, V> {
matches!(&self.0, Node::Leaf { .. }) matches!(&self.0, Node::Leaf { .. })
} }
/// Returns `true` if no [`Node::Nil`] nodes are present in the tree, `false` otherwise. /// Returns a vector of the addresses of [`Node::Nil`] and [`Node::Pruned`] subtree roots
pub fn is_complete(&self) -> bool { /// within this tree.
match &self.0 {
Node::Parent { left, right, .. } => {
left.as_ref().is_complete() && right.as_ref().is_complete()
}
Node::Leaf { .. } => true,
Node::Nil { .. } => false,
}
}
/// Returns a vector of the addresses of [`Node::Nil`] subtree roots within this tree.
/// ///
/// The given address must correspond to the root of this tree, or this method will /// The given address must correspond to the root of this tree, or this method will
/// yield incorrect results or may panic. /// yield incorrect results or may panic.
@ -154,7 +155,7 @@ impl<A, V> Tree<A, V> {
left_incomplete left_incomplete
} }
Node::Leaf { .. } => vec![], Node::Leaf { .. } => vec![],
Node::Nil => vec![root_addr], Node::Nil | Node::Pruned => vec![root_addr],
} }
} }
@ -164,15 +165,16 @@ impl<A, V> Tree<A, V> {
where where
A: Clone, A: Clone,
{ {
match &self.0 { Tree(match &self.0 {
Node::Parent { ann, left, right } => Tree(Node::Parent { Node::Parent { ann, left, right } => Node::Parent {
ann: ann.clone(), ann: ann.clone(),
left: Arc::new(left.map(f)), left: Arc::new(left.map(f)),
right: Arc::new(right.map(f)), right: Arc::new(right.map(f)),
}), },
Node::Leaf { value } => Tree(Node::Leaf { value: f(value) }), Node::Leaf { value } => Node::Leaf { value: f(value) },
Node::Nil => Tree(Node::Nil), Node::Nil => Node::Nil,
} Node::Pruned => Node::Pruned,
})
} }
/// Applies the provided function to each leaf of the tree and returns /// Applies the provided function to each leaf of the tree and returns
@ -190,6 +192,7 @@ impl<A, V> Tree<A, V> {
}, },
Node::Leaf { value } => Node::Leaf { value: f(value)? }, Node::Leaf { value } => Node::Leaf { value: f(value)? },
Node::Nil => Node::Nil, Node::Nil => Node::Nil,
Node::Pruned => Node::Pruned,
})) }))
} }
} }
@ -248,7 +251,7 @@ impl<A, V> LocatedTree<A, V> {
pub(crate) fn max_position_internal(addr: Address, root: &Tree<A, V>) -> Option<Position> { pub(crate) fn max_position_internal(addr: Address, root: &Tree<A, V>) -> Option<Position> {
match &root.0 { match &root.0 {
Node::Nil => None, Node::Nil => None,
Node::Leaf { .. } => Some(addr.position_range_end() - 1), Node::Leaf { .. } | Node::Pruned => Some(addr.position_range_end() - 1),
Node::Parent { left, right, .. } => { Node::Parent { left, right, .. } => {
let (l_addr, r_addr) = addr.children().unwrap(); let (l_addr, r_addr) = addr.children().unwrap();
Self::max_position_internal(r_addr, right.as_ref()) Self::max_position_internal(r_addr, right.as_ref())