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
/// or [`Retention::Checkpoint`] retention with [`Marking::Marked`] marking.
pub fn is_marked(&self) -> bool {
match self {
matches!(
self,
Retention::Marked
| Retention::Checkpoint {
marking: Marking::Marked,
..
} => true,
_ => false,
}
| Retention::Checkpoint {
marking: Marking::Marked,
..
}
)
}
/// 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
### 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::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

View File

@ -234,16 +234,17 @@ impl<
let (append_result, position, checkpoint_id) =
if let Some(subtree) = self.store.last_shard().map_err(ShardTreeError::Storage)? {
if subtree.root.is_complete() {
let addr = subtree.root_addr;
if addr.index() < Self::max_subtree_index() {
LocatedTree::empty(addr.next_at_level()).append(value, retention)?
} else {
return Err(InsertionError::TreeFull.into());
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;
if subtree.root_addr.index() < Self::max_subtree_index() {
LocatedTree::empty(addr.next_at_level()).append(value, retention)?
} else {
return Err(InsertionError::TreeFull.into());
}
}
} else {
subtree.append(value, retention)?
_ => subtree.append(value, retention)?,
}
} else {
let root_addr = Address::from_parts(Self::subtree_level(), 0);
@ -412,8 +413,8 @@ impl<
root_addr: Address,
root: &PrunableTree<H>,
) -> Option<(PrunableTree<H>, Position)> {
match root {
Tree(Node::Parent { ann, left, right }) => {
match &root.0 {
Node::Parent { ann, left, right } => {
let (l_addr, r_addr) = root_addr.children().unwrap();
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 {
value: (h.clone(), *r | RetentionFlags::CHECKPOINT),
}),
root_addr.max_position(),
)),
Tree(Node::Nil) => None,
Node::Nil | Node::Pruned => None,
}
}
@ -576,12 +577,19 @@ impl<
// Prune each affected subtree
for (subtree_addr, positions) in clear_positions.into_iter() {
let cleared = self
let to_clear = self
.store
.get_shard(subtree_addr)
.map_err(ShardTreeError::Storage)?
.map(|subtree| subtree.clear_flags(positions));
if let Some(cleared) = cleared {
.map_err(ShardTreeError::Storage)?;
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
.put_shard(cleared)
.map_err(ShardTreeError::Storage)?;
@ -757,8 +765,8 @@ impl<
// roots.
truncate_at: Position,
) -> Result<(H, Option<PrunableTree<H>>), ShardTreeError<S::Error>> {
match &cap.root {
Tree(Node::Parent { ann, left, right }) => {
match &cap.root.0 {
Node::Parent { ann, left, right } => {
match ann {
Some(cached_root) if target_addr.contains(&cap.root_addr) => {
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()
&& target_addr.contains(&cap.root_addr)
{
@ -857,7 +865,7 @@ impl<
))
}
}
Tree(Node::Nil) => {
Node::Nil | Node::Pruned => {
if cap.root_addr == target_addr
|| cap.root_addr.level() == ShardTree::<S, DEPTH, SHARD_HEIGHT>::subtree_level()
{
@ -867,7 +875,7 @@ impl<
Ok((
root.clone(),
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
Some(Tree::leaf((root, RetentionFlags::EPHEMERAL)))
} else {
@ -1301,21 +1309,24 @@ impl<
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use assert_matches::assert_matches;
use proptest::prelude::*;
use incrementalmerkletree::{
frontier::NonEmptyFrontier,
frontier::{Frontier, NonEmptyFrontier},
testing::{
arb_operation, check_append, check_checkpoint_rewind, check_operations,
check_remove_mark, check_rewind_remove_mark, check_root_hashes,
check_witness_consistency, check_witnesses, complete_tree::CompleteTree, CombinedTree,
SipHashable,
},
Address, Hashable, Level, Marking, Position, Retention,
Address, Hashable, Level, Marking, MerklePath, Position, Retention,
};
use crate::{
error::{QueryError, ShardTreeError},
store::memory::MemoryShardStore,
testing::{
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
#[allow(clippy::type_complexity)]
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())
}
/// 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.
pub fn contains_marked(&self) -> bool {
match &self.0 {
Node::Parent { left, right, .. } => left.contains_marked() || right.contains_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
Ok(H::empty_root(root_addr.level()))
} else {
match self {
Tree(Node::Parent { ann, left, right }) => ann
match &self.0 {
Node::Parent { ann, left, right } => ann
.as_ref()
.filter(|_| truncate_at >= root_addr.position_range_end())
.map_or_else(
@ -145,7 +158,7 @@ impl<H: Hashable + Clone + PartialEq> PrunableTree<H> {
Ok(rc.as_ref().clone())
},
),
Tree(Node::Leaf { value }) => {
Node::Leaf { value } => {
if truncate_at >= root_addr.position_range_end() {
// no truncation of this leaf is necessary, just use it
Ok(value.0.clone())
@ -157,7 +170,7 @@ impl<H: Hashable + Clone + PartialEq> PrunableTree<H> {
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
}
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();
match (t0, t1) {
(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 })) => {
if vl.0 == vr.0 {
// 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 {
match (left, right) {
(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 }))
// 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
@ -518,7 +534,7 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
None
}
}
Node::Nil => None,
Node::Nil | Node::Pruned => None,
}
}
@ -555,7 +571,6 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
root_addr: Address,
into: &PrunableTree<H>,
subtree: LocatedPrunableTree<H>,
is_complete: bool,
contains_marked: bool,
) -> Result<(PrunableTree<H>, Vec<IncompleteAt>), InsertionError> {
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
// subtree up to the level of the node being replaced, adding Nil siblings
// 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
let mut incomplete = vec![];
while node.root_addr.level() < root_addr.level() {
@ -579,19 +596,21 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
address: node.root_addr.sibling(),
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 {
root_addr: node.root_addr.parent(),
root: if node.root_addr.is_right_child() {
Tree(Node::Parent {
ann: None,
left: Arc::new(Tree(Node::Nil)),
right: Arc::new(node.root),
left: empty,
right: full,
})
} else {
Tree(Node::Parent {
ann: None,
left: Arc::new(node.root),
right: Arc::new(Tree(Node::Nil)),
left: full,
right: empty,
})
},
};
@ -600,7 +619,8 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
};
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 {
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
// discarding the existing leaf value.
if is_complete {
if subtree.root.has_computable_root() {
Ok((
if subtree.root.is_leaf() {
// When replacing a leaf with a leaf, `REFERENCE` retention
@ -635,7 +655,7 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
)?
} else {
// 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
},
vec![],
@ -655,7 +675,22 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
Err(InsertionError::Conflict(root_addr))
}
} 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 => {
@ -673,7 +708,7 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
let (l_addr, r_addr) = root_addr.children().unwrap();
if l_addr.contains(&subtree.root_addr) {
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((
Tree::unite(
root_addr.level() - 1,
@ -684,13 +719,8 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
incomplete,
))
} else {
let (new_right, incomplete) = go(
r_addr,
right.as_ref(),
subtree,
is_complete,
contains_marked,
)?;
let (new_right, incomplete) =
go(r_addr, right.as_ref(), subtree, contains_marked)?;
Ok((
Tree::unite(
root_addr.level() - 1,
@ -710,22 +740,16 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
trace!(
max_position = ?max_position,
tree = ?self,
subtree_complete = subtree.root.is_complete(),
to_insert = ?subtree,
"Current shard"
);
let LocatedTree { root_addr, root } = self;
if root_addr.contains(&subtree.root_addr) {
let complete = subtree.root.is_complete();
go(*root_addr, root, subtree, complete, contains_marked).map(|(root, incomplete)| {
go(*root_addr, root, subtree, contains_marked).map(|(root, incomplete)| {
let new_tree = LocatedTree {
root_addr: *root_addr,
root,
};
trace!(
max_position = ?new_tree.max_position(),
"Replacement shard"
);
assert!(new_tree.max_position() >= max_position);
(new_tree, incomplete)
})
@ -786,9 +810,7 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
split_at: Level,
) -> (Self, Option<Self>) {
let mut addr = Address::from(position);
let mut subtree = Tree(Node::Leaf {
value: (leaf, leaf_retention.into()),
});
let mut subtree = Tree::leaf((leaf, leaf_retention.into()));
while addr.level() < split_at {
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
root.clone()
} else {
match root {
Tree(Node::Parent { ann, left, right }) => {
match &root.0 {
Node::Parent { ann, left, right } => {
let (l_addr, r_addr) = root_addr.children().unwrap();
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),
)
}
Tree(Node::Leaf { value: (h, r) }) => {
Node::Leaf { value: (h, r) } => {
trace!("In {:?}, clearing {:?}", root_addr, to_clear);
// 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
@ -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
// be a level-0 leaf.
match to_clear {
[(pos, flags)] => {
assert_eq!(*pos, root_addr.max_position());
Tree(Node::Leaf {
value: (h.clone(), *r & !*flags),
})
}
[(_, flags)] => Tree(Node::Leaf {
value: (h.clone(), *r & !*flags),
}),
_ => {
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]
fn located_insert_subtree_leaf_overwrites() {
fn located_insert_subtree_prevents_leaf_overwrite_conflict() {
let t: LocatedPrunableTree<String> = LocatedTree {
root_addr: Address::from_parts(2.into(), 1),
root: parent(leaf(("a".to_string(), RetentionFlags::MARKED)), nil()),

View File

@ -19,6 +19,9 @@ pub enum Node<C, A, V> {
Leaf { value: V },
/// The empty tree; a subtree or leaf for which no information is available.
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> {
@ -35,7 +38,8 @@ impl<C, A, V> Node<C, A, V> {
match self {
Node::Parent { .. } => None,
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::Leaf { .. } => 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(),
},
Node::Nil => Node::Nil,
Node::Pruned => Node::Pruned,
}
}
}
@ -92,6 +98,11 @@ impl<A, V> Tree<A, V> {
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.
pub fn leaf(value: V) -> Self {
Tree(Node::Leaf { value })
@ -122,18 +133,8 @@ impl<A, V> Tree<A, V> {
matches!(&self.0, Node::Leaf { .. })
}
/// Returns `true` if no [`Node::Nil`] nodes are present in the tree, `false` otherwise.
pub fn is_complete(&self) -> bool {
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.
/// Returns a vector of the addresses of [`Node::Nil`] and [`Node::Pruned`] subtree roots
/// within this tree.
///
/// The given address must correspond to the root of this tree, or this method will
/// yield incorrect results or may panic.
@ -154,7 +155,7 @@ impl<A, V> Tree<A, V> {
left_incomplete
}
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
A: Clone,
{
match &self.0 {
Node::Parent { ann, left, right } => Tree(Node::Parent {
Tree(match &self.0 {
Node::Parent { ann, left, right } => Node::Parent {
ann: ann.clone(),
left: Arc::new(left.map(f)),
right: Arc::new(right.map(f)),
}),
Node::Leaf { value } => Tree(Node::Leaf { value: f(value) }),
Node::Nil => Tree(Node::Nil),
}
},
Node::Leaf { value } => Node::Leaf { value: f(value) },
Node::Nil => Node::Nil,
Node::Pruned => Node::Pruned,
})
}
/// 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::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> {
match &root.0 {
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, .. } => {
let (l_addr, r_addr) = addr.children().unwrap();
Self::max_position_internal(r_addr, right.as_ref())