shardtree: Discard `REFERENCE` retention in leaf overwrites.

Also, disallow value-conflicted overwrites at the leaf level.
This commit is contained in:
Kris Nuttycombe 2024-05-21 17:24:04 -06:00
parent e55ff2d7f2
commit ffc087424d
3 changed files with 115 additions and 17 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to Rust's notion of
## Unreleased ## Unreleased
### Added
- `shardtree::tree::Tree::{is_leaf, map, try_map}`
- `shardtree::tree::LocatedTree::{map, try_map}`
## [0.3.1] - 2024-04-03 ## [0.3.1] - 2024-04-03
### Fixed ### Fixed

View File

@ -302,10 +302,11 @@ impl<H: Hashable + Clone + PartialEq> PrunableTree<H> {
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::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; if a leaf // we can prune right-hand leaves that are not marked or reference leaves; if a
// is a checkpoint then that information will be propagated to // leaf is a checkpoint then that information will be propagated to the replacement
// the replacement leaf // leaf
if lv.1 == RetentionFlags::EPHEMERAL && (rv.1 & RetentionFlags::MARKED) == RetentionFlags::EPHEMERAL => if lv.1 == RetentionFlags::EPHEMERAL &&
(rv.1 & (RetentionFlags::MARKED | RetentionFlags::REFERENCE)) == RetentionFlags::EPHEMERAL =>
{ {
Tree( Tree(
Node::Leaf { Node::Leaf {
@ -600,12 +601,45 @@ 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)),
Tree(Node::Leaf { value: (value, _) }) => { Tree(Node::Leaf {
value: (value, retention),
}) => {
if root_addr == subtree.root_addr { if root_addr == subtree.root_addr {
// The current leaf is at the location we wish to transplant the root
// of the subtree being inserted, so we either replace the leaf
// entirely with the subtree, or reannotate the root so as to avoid
// discarding the existing leaf value.
if is_complete { if is_complete {
Ok((
if subtree.root.is_leaf() {
// When replacing a leaf with a leaf, `REFERENCE` retention
// will be discarded unless both leaves have `REFERENCE`
// retention.
subtree
.root
.try_map::<(H, RetentionFlags), InsertionError, _>(
&|(v0, ret0)| {
if v0 == value {
let retention_result: RetentionFlags =
((*retention | *ret0)
- RetentionFlags::REFERENCE)
| (RetentionFlags::REFERENCE
& *retention
& *ret0);
Ok((value.clone(), retention_result))
} else {
Err(InsertionError::Conflict(root_addr))
}
},
)?
} 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 a complete subtree.
Ok((subtree.root, vec![])) subtree.root
},
vec![],
))
} else if subtree.root.node_value().iter().all(|v| v == &value) { } else if subtree.root.node_value().iter().all(|v| v == &value) {
Ok(( Ok((
// at this point we statically know the root to be a parent // at this point we statically know the root to be a parent
@ -932,7 +966,7 @@ mod tests {
use super::{LocatedPrunableTree, PrunableTree, RetentionFlags}; use super::{LocatedPrunableTree, PrunableTree, RetentionFlags};
use crate::{ use crate::{
error::QueryError, error::{InsertionError, QueryError},
tree::{ tree::{
tests::{leaf, nil, parent}, tests::{leaf, nil, parent},
LocatedTree, LocatedTree,
@ -1101,21 +1135,16 @@ mod tests {
root: parent(leaf(("a".to_string(), RetentionFlags::MARKED)), nil()), root: parent(leaf(("a".to_string(), RetentionFlags::MARKED)), nil()),
}; };
let conflict_addr = Address::from_parts(1.into(), 2);
assert_eq!( assert_eq!(
t.insert_subtree( t.insert_subtree(
LocatedTree { LocatedTree {
root_addr: Address::from_parts(1.into(), 2), root_addr: conflict_addr,
root: leaf(("b".to_string(), RetentionFlags::EPHEMERAL)), root: leaf(("b".to_string(), RetentionFlags::EPHEMERAL)),
}, },
false, false,
), ),
Ok(( Err(InsertionError::Conflict(conflict_addr)),
LocatedTree {
root_addr: Address::from_parts(2.into(), 1),
root: parent(leaf(("b".to_string(), RetentionFlags::EPHEMERAL)), nil()),
},
vec![],
)),
); );
} }

View File

@ -117,6 +117,11 @@ impl<A, V> Tree<A, V> {
Tree(self.0.reannotate(ann)) Tree(self.0.reannotate(ann))
} }
/// Returns `true` this is a [`Node::Leaf`], `false` otherwise.
pub fn is_leaf(&self) -> bool {
matches!(&self.0, Node::Leaf { .. })
}
/// Returns `true` if no [`Node::Nil`] nodes are present in the tree, `false` otherwise. /// Returns `true` if no [`Node::Nil`] nodes are present in the tree, `false` otherwise.
pub fn is_complete(&self) -> bool { pub fn is_complete(&self) -> bool {
match &self.0 { match &self.0 {
@ -152,6 +157,41 @@ impl<A, V> Tree<A, V> {
Node::Nil => vec![root_addr], Node::Nil => vec![root_addr],
} }
} }
/// Applies the provided function to each leaf of the tree and returns
/// a new tree having the same structure as the original.
pub fn map<B, F: Fn(&V) -> B>(&self, f: &F) -> Tree<A, B>
where
A: Clone,
{
match &self.0 {
Node::Parent { ann, left, right } => Tree(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),
}
}
/// Applies the provided function to each leaf of the tree and returns
/// a new tree having the same structure as the original, or an error
/// if any transformation of the leaf fails.
pub fn try_map<B, E, F: Fn(&V) -> Result<B, E>>(&self, f: &F) -> Result<Tree<A, B>, E>
where
A: Clone,
{
Ok(Tree(match &self.0 {
Node::Parent { ann, left, right } => Node::Parent {
ann: ann.clone(),
left: Arc::new(left.try_map(f)?),
right: Arc::new(right.try_map(f)?),
},
Node::Leaf { value } => Node::Leaf { value: f(value)? },
Node::Nil => Node::Nil,
}))
}
} }
/// A binary Merkle tree with its root at the given address. /// A binary Merkle tree with its root at the given address.
@ -240,6 +280,31 @@ impl<A, V> LocatedTree<A, V> {
None None
} }
} }
/// Applies the provided function to each leaf of the tree and returns
/// a new tree having the same structure as the original.
pub fn map<B, F: Fn(&V) -> B>(&self, f: &F) -> LocatedTree<A, B>
where
A: Clone,
{
LocatedTree {
root_addr: self.root_addr,
root: self.root.map(f),
}
}
/// Applies the provided function to each leaf of the tree and returns
/// a new tree having the same structure as the original, or an error
/// if any transformation of the leaf fails.
pub fn try_map<B, E, F: Fn(&V) -> Result<B, E>>(&self, f: &F) -> Result<LocatedTree<A, B>, E>
where
A: Clone,
{
Ok(LocatedTree {
root_addr: self.root_addr,
root: self.root.try_map(f)?,
})
}
} }
impl<A: Default + Clone, V: Clone> LocatedTree<A, V> { impl<A: Default + Clone, V: Clone> LocatedTree<A, V> {