Modify test infrastructure to allow shardtree testing.

This removes the `mark` function from the `tree` interface, in favor
of always appending nodes as marked, as that's what's needed in
practice, rather than the ability to mutably mark the latest position at
arbitrary states of the tree.

This removal does mean that a few patterns of interleaved mark and
checkpoint operations are no longer being tested in the shared test
suite; however, such interleaving of operations is not something that
we should need to support anyway.
This commit is contained in:
Kris Nuttycombe 2022-12-15 15:02:44 -07:00
parent 07a88c34ac
commit 14bb9c6b1b
5 changed files with 550 additions and 453 deletions

View File

@ -10,3 +10,5 @@ cc f1a0c0e8114919f9f675e0b31cdecf37af31579e365119312a7ebefcabb639f1 # shrinks to
cc ac8dad1a92cb9563a291802cbd3dfca2a89da35fe4ed377701f5ad85b43700f4 # shrinks to ops = [Append("a"), Append("a"), Checkpoint, Checkpoint, Checkpoint, Mark, Checkpoint, Authpath(Position(1), 2)] cc ac8dad1a92cb9563a291802cbd3dfca2a89da35fe4ed377701f5ad85b43700f4 # shrinks to ops = [Append("a"), Append("a"), Checkpoint, Checkpoint, Checkpoint, Mark, Checkpoint, Authpath(Position(1), 2)]
cc cebb95fe896dc1d1e3c9a65efd50e786e6a4a3503c86fb2a6817bb05d25e751e # shrinks to tree = BridgeTree { depth: 8, prior_bridges: [MerkleBridge { prior_position: None, tracking: {Address { level: Level(2), index: 0 }}, ommers: {}, frontier: NonEmptyFrontier { position: Position(3), leaf: "a", ommers: ["a", "aa"] } }], current_bridge: Some(MerkleBridge { prior_position: Some(Position(3)), tracking: {Address { level: Level(2), index: 0 }}, ommers: {}, frontier: NonEmptyFrontier { position: Position(3), leaf: "a", ommers: ["a", "aa"] } }), saved: {Position(3): 0}, checkpoints: [Checkpoint { bridges_len: 0, marked: {Position(3)}, forgotten: {Position(3): 0} }], max_checkpoints: 10 } cc cebb95fe896dc1d1e3c9a65efd50e786e6a4a3503c86fb2a6817bb05d25e751e # shrinks to tree = BridgeTree { depth: 8, prior_bridges: [MerkleBridge { prior_position: None, tracking: {Address { level: Level(2), index: 0 }}, ommers: {}, frontier: NonEmptyFrontier { position: Position(3), leaf: "a", ommers: ["a", "aa"] } }], current_bridge: Some(MerkleBridge { prior_position: Some(Position(3)), tracking: {Address { level: Level(2), index: 0 }}, ommers: {}, frontier: NonEmptyFrontier { position: Position(3), leaf: "a", ommers: ["a", "aa"] } }), saved: {Position(3): 0}, checkpoints: [Checkpoint { bridges_len: 0, marked: {Position(3)}, forgotten: {Position(3): 0} }], max_checkpoints: 10 }
cc cdab33688ef9270768481b72d1615ff1d209fdb3d8d45be2ad564a8d5e0addc1 # shrinks to ops = [Append(SipHashable(0)), Mark, Mark, Checkpoint, Mark, Rewind, Append(SipHashable(0)), Mark] cc cdab33688ef9270768481b72d1615ff1d209fdb3d8d45be2ad564a8d5e0addc1 # shrinks to ops = [Append(SipHashable(0)), Mark, Mark, Checkpoint, Mark, Rewind, Append(SipHashable(0)), Mark]
cc 4058fadeb645f982bb83d666ea20c9541c9b920438877c145ee750cb09e22337 # shrinks to ops = [Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), CurrentLeaf, CurrentLeaf, Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Checkpoint { id: (), is_marked: true }), Unmark(Position(4))]
cc befc65ab8360534feebaa4c0e3da24518d134b03326235f1ccede15ad998d066 # shrinks to ops = [Append("a", Ephemeral), Append("a", Checkpoint { id: (), is_marked: true }), Append("a", Ephemeral), Append("a", Checkpoint { id: (), is_marked: false }), Authpath(Position(1), 0)]

View File

@ -37,7 +37,7 @@ use std::fmt::Debug;
use std::mem::size_of; use std::mem::size_of;
use std::ops::Range; use std::ops::Range;
pub use incrementalmerkletree::{Address, Hashable, Level, Position}; pub use incrementalmerkletree::{Address, Hashable, Level, Position, Retention};
/// Validation errors that can occur during reconstruction of a Merkle frontier from /// Validation errors that can occur during reconstruction of a Merkle frontier from
/// its constituent parts. /// its constituent parts.
@ -614,6 +614,8 @@ impl<'a, H: Hashable + Ord + Clone + 'a> MerkleBridge<H> {
/// previous state. /// previous state.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Checkpoint { pub struct Checkpoint {
/// The unique identifier for this checkpoint.
id: usize,
/// The number of bridges that will be retained in a rewind. /// The number of bridges that will be retained in a rewind.
bridges_len: usize, bridges_len: usize,
/// A set of the positions that have been marked during the period that this /// A set of the positions that have been marked during the period that this
@ -628,11 +630,13 @@ pub struct Checkpoint {
impl Checkpoint { impl Checkpoint {
/// Creates a new checkpoint from its constituent parts. /// Creates a new checkpoint from its constituent parts.
pub fn from_parts( pub fn from_parts(
id: usize,
bridges_len: usize, bridges_len: usize,
marked: BTreeSet<Position>, marked: BTreeSet<Position>,
forgotten: BTreeSet<Position>, forgotten: BTreeSet<Position>,
) -> Self { ) -> Self {
Self { Self {
id,
bridges_len, bridges_len,
marked, marked,
forgotten, forgotten,
@ -640,14 +644,21 @@ impl Checkpoint {
} }
/// Creates a new empty checkpoint for the specified [`BridgeTree`] state. /// Creates a new empty checkpoint for the specified [`BridgeTree`] state.
pub fn at_length(bridges_len: usize) -> Self { pub fn at_length(bridges_len: usize, id: usize) -> Self {
Checkpoint { Checkpoint {
id,
bridges_len, bridges_len,
marked: BTreeSet::new(), marked: BTreeSet::new(),
forgotten: BTreeSet::new(), forgotten: BTreeSet::new(),
} }
} }
/// The unique identifier for the checkpoint, which is simply an automatically incrementing
/// index over all checkpoints that have ever been created in the history of the tree.
pub fn id(&self) -> usize {
self.id
}
/// Returns the length of the [`BridgeTree::prior_bridges`] vector of the [`BridgeTree`] to /// Returns the length of the [`BridgeTree::prior_bridges`] vector of the [`BridgeTree`] to
/// which this checkpoint refers. /// which this checkpoint refers.
/// ///
@ -750,13 +761,13 @@ impl<H, const DEPTH: u8> BridgeTree<H, DEPTH> {
/// ///
/// Panics if `max_checkpoints < 1` because mark/rewind logic depends upon the presence /// Panics if `max_checkpoints < 1` because mark/rewind logic depends upon the presence
/// of checkpoints to function. /// of checkpoints to function.
pub fn new(max_checkpoints: usize) -> Self { pub fn new(max_checkpoints: usize, initial_checkpoint_id: usize) -> Self {
assert!(max_checkpoints >= 1); assert!(max_checkpoints >= 1);
Self { Self {
prior_bridges: vec![], prior_bridges: vec![],
current_bridge: None, current_bridge: None,
saved: BTreeMap::new(), saved: BTreeMap::new(),
checkpoints: VecDeque::from(vec![Checkpoint::at_length(0)]), checkpoints: VecDeque::from(vec![Checkpoint::at_length(0, initial_checkpoint_id)]),
max_checkpoints, max_checkpoints,
} }
} }
@ -1033,34 +1044,42 @@ impl<H: Hashable + Ord + Clone, const DEPTH: u8> BridgeTree<H, DEPTH> {
} }
} }
/// Creates a new checkpoint for the current tree state. It is valid to /// Creates a new checkpoint for the current tree state, with the given identifier.
/// have multiple checkpoints for the same tree state, and each `rewind` ///
/// call will remove a single checkpoint. /// It is valid to have multiple checkpoints for the same tree state, and each `rewind` call
pub fn checkpoint(&mut self) { /// will remove a single checkpoint. Successive checkpoint identifiers must always be provided
match self.current_bridge.take() { /// in increasing order.
Some(cur_b) => { pub fn checkpoint(&mut self, id: usize) -> bool {
// Do not create a duplicate bridge if Some(id) > self.checkpoints.back().map(|c| c.id) {
if self match self.current_bridge.take() {
.prior_bridges Some(cur_b) => {
.last() // Do not create a duplicate bridge
.map_or(false, |pb| pb.position() == cur_b.position()) if self
{ .prior_bridges
self.current_bridge = Some(cur_b); .last()
} else { .map_or(false, |pb| pb.position() == cur_b.position())
self.current_bridge = Some(cur_b.successor(false)); {
self.prior_bridges.push(cur_b); self.current_bridge = Some(cur_b);
} else {
self.current_bridge = Some(cur_b.successor(false));
self.prior_bridges.push(cur_b);
}
self.checkpoints
.push_back(Checkpoint::at_length(self.prior_bridges.len(), id));
}
None => {
self.checkpoints.push_back(Checkpoint::at_length(0, id));
} }
self.checkpoints
.push_back(Checkpoint::at_length(self.prior_bridges.len()));
} }
None => {
self.checkpoints.push_back(Checkpoint::at_length(0));
}
}
if self.checkpoints.len() > self.max_checkpoints { if self.checkpoints.len() > self.max_checkpoints {
self.drop_oldest_checkpoint(); self.drop_oldest_checkpoint();
}
true
} else {
false
} }
} }
@ -1283,9 +1302,18 @@ mod tests {
} }
} }
impl<H: Hashable + Ord + Clone, const DEPTH: u8> Tree<H> for BridgeTree<H, DEPTH> { impl<H: Hashable + Ord + Clone, const DEPTH: u8> Tree<H, usize> for BridgeTree<H, DEPTH> {
fn append(&mut self, value: H) -> bool { fn append(&mut self, value: H, retention: Retention<usize>) -> bool {
BridgeTree::append(self, value) let appended = BridgeTree::append(self, value);
if appended {
if retention.is_marked() {
BridgeTree::mark(self);
}
if let Retention::Checkpoint { id, .. } = retention {
BridgeTree::checkpoint(self, id);
}
}
appended
} }
fn depth(&self) -> u8 { fn depth(&self) -> u8 {
@ -1304,10 +1332,6 @@ mod tests {
BridgeTree::get_marked_leaf(self, position) BridgeTree::get_marked_leaf(self, position)
} }
fn mark(&mut self) -> Option<Position> {
BridgeTree::mark(self)
}
fn marked_positions(&self) -> BTreeSet<Position> { fn marked_positions(&self) -> BTreeSet<Position> {
BridgeTree::marked_positions(self) BridgeTree::marked_positions(self)
} }
@ -1324,8 +1348,8 @@ mod tests {
BridgeTree::remove_mark(self, position) BridgeTree::remove_mark(self, position)
} }
fn checkpoint(&mut self) { fn checkpoint(&mut self, id: usize) -> bool {
BridgeTree::checkpoint(self) BridgeTree::checkpoint(self, id)
} }
fn rewind(&mut self) -> bool { fn rewind(&mut self) -> bool {
@ -1430,7 +1454,7 @@ mod tests {
#[test] #[test]
fn tree_depth() { fn tree_depth() {
let mut tree = BridgeTree::<String, 3>::new(100); let mut tree = BridgeTree::<String, 3>::new(100, 0);
for c in 'a'..'i' { for c in 'a'..'i' {
assert!(tree.append(c.to_string())) assert!(tree.append(c.to_string()))
} }
@ -1441,8 +1465,8 @@ mod tests {
mut tree: BridgeTree<H, DEPTH>, mut tree: BridgeTree<H, DEPTH>,
) { ) {
// Add checkpoints until we're sure everything that can be gc'ed will be gc'ed // Add checkpoints until we're sure everything that can be gc'ed will be gc'ed
for _ in 0..tree.max_checkpoints { for i in 0..tree.max_checkpoints {
tree.checkpoint(); tree.checkpoint(i + 1);
} }
let mut tree_mut = tree.clone(); let mut tree_mut = tree.clone();
@ -1462,9 +1486,9 @@ mod tests {
{ {
proptest::collection::vec(arb_operation(item_gen, 0..max_count), 0..max_count).prop_map( proptest::collection::vec(arb_operation(item_gen, 0..max_count), 0..max_count).prop_map(
|ops| { |ops| {
let mut tree: BridgeTree<G::Value, 8> = BridgeTree::new(10); let mut tree: BridgeTree<G::Value, 8> = BridgeTree::new(10, 0);
for op in ops { for (i, op) in ops.into_iter().enumerate() {
apply_operation(&mut tree, op); apply_operation(&mut tree, op.map_checkpoint_id(|_| i));
} }
tree tree
}, },
@ -1498,45 +1522,47 @@ mod tests {
#[test] #[test]
fn root_hashes() { fn root_hashes() {
check_root_hashes(BridgeTree::<String, 4>::new); check_root_hashes(|max_checkpoints| BridgeTree::<String, 4>::new(max_checkpoints, 0));
} }
#[test] #[test]
fn witnesss() { fn witness() {
check_witnesses(BridgeTree::<String, 4>::new); check_witnesses(|max_checkpoints| BridgeTree::<String, 4>::new(max_checkpoints, 0));
} }
#[test] #[test]
fn checkpoint_rewind() { fn checkpoint_rewind() {
check_checkpoint_rewind(BridgeTree::<String, 4>::new); check_checkpoint_rewind(|max_checkpoints| BridgeTree::<String, 4>::new(max_checkpoints, 0));
} }
#[test] #[test]
fn rewind_remove_mark() { fn rewind_remove_mark() {
check_rewind_remove_mark(BridgeTree::<String, 4>::new); check_rewind_remove_mark(|max_checkpoints| {
BridgeTree::<String, 4>::new(max_checkpoints, 0)
});
} }
#[test] #[test]
fn garbage_collect() { fn garbage_collect() {
let mut tree: BridgeTree<String, 7> = BridgeTree::new(100); let mut tree: BridgeTree<String, 7> = BridgeTree::new(1000, 0);
let empty_root = tree.root(0); let empty_root = tree.root(0);
tree.append("a".to_string()); tree.append("a".to_string());
for _ in 0..100 { for i in 0..100 {
tree.checkpoint(); tree.checkpoint(i + 1);
} }
tree.garbage_collect(); tree.garbage_collect();
assert!(tree.root(0) != empty_root); assert!(tree.root(0) != empty_root);
tree.rewind(); tree.rewind();
assert!(tree.root(0) != empty_root); assert!(tree.root(0) != empty_root);
let mut t = BridgeTree::<String, 7>::new(10); let mut t = BridgeTree::<String, 7>::new(10, 0);
let mut to_unmark = vec![]; let mut to_unmark = vec![];
let mut has_witness = vec![]; let mut has_witness = vec![];
for i in 0usize..100 { for i in 0usize..100 {
let elem: String = format!("{},", i); let elem: String = format!("{},", i);
assert!(t.append(elem), "Append should succeed."); assert!(t.append(elem), "Append should succeed.");
if i % 5 == 0 { if i % 5 == 0 {
t.checkpoint(); t.checkpoint(i + 1);
} }
if i % 7 == 0 { if i % 7 == 0 {
t.mark(); t.mark();
@ -1553,7 +1579,7 @@ mod tests {
} }
// 32 = 20 (checkpointed) + 14 (marked) - 2 (marked & checkpointed) // 32 = 20 (checkpointed) + 14 (marked) - 2 (marked & checkpointed)
assert_eq!(t.prior_bridges().len(), 20 + 14 - 2); assert_eq!(t.prior_bridges().len(), 20 + 14 - 2);
let witnesss = has_witness let witness = has_witness
.iter() .iter()
.map(|pos| match t.witness(*pos, 0) { .map(|pos| match t.witness(*pos, 0) {
Ok(path) => path, Ok(path) => path,
@ -1563,20 +1589,20 @@ mod tests {
t.garbage_collect(); t.garbage_collect();
// 20 = 32 - 10 (removed checkpoints) + 1 (not removed due to mark) - 3 (removed marks) // 20 = 32 - 10 (removed checkpoints) + 1 (not removed due to mark) - 3 (removed marks)
assert_eq!(t.prior_bridges().len(), 32 - 10 + 1 - 3); assert_eq!(t.prior_bridges().len(), 32 - 10 + 1 - 3);
let retained_witnesss = has_witness let retained_witness = has_witness
.iter() .iter()
.map(|pos| t.witness(*pos, 0).expect("Must be able to get auth path")) .map(|pos| t.witness(*pos, 0).expect("Must be able to get auth path"))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(witnesss, retained_witnesss); assert_eq!(witness, retained_witness);
} }
// Combined tree tests // Combined tree tests
fn new_combined_tree<H: Hashable + Ord + Clone + Debug>( fn new_combined_tree<H: Hashable + Ord + Clone + Debug>(
max_checkpoints: usize, max_checkpoints: usize,
) -> CombinedTree<H, CompleteTree<H, 4>, BridgeTree<H, 4>> { ) -> CombinedTree<H, usize, CompleteTree<H, usize, 4>, BridgeTree<H, 4>> {
CombinedTree::new( CombinedTree::new(
CompleteTree::<H, 4>::new(max_checkpoints), CompleteTree::<H, usize, 4>::new(max_checkpoints, 0),
BridgeTree::<H, 4>::new(max_checkpoints), BridgeTree::<H, 4>::new(max_checkpoints, 0),
) )
} }
@ -1601,7 +1627,8 @@ mod tests {
) )
) { ) {
let tree = new_combined_tree(100); let tree = new_combined_tree(100);
check_operations(tree, &ops)?; let indexed_ops = ops.iter().enumerate().map(|(i, op)| op.map_checkpoint_id(|_| i + 1)).collect::<Vec<_>>();
check_operations(tree, &indexed_ops)?;
} }
#[test] #[test]
@ -1612,7 +1639,8 @@ mod tests {
) )
) { ) {
let tree = new_combined_tree(100); let tree = new_combined_tree(100);
check_operations(tree, &ops)?; let indexed_ops = ops.iter().enumerate().map(|(i, op)| op.map_checkpoint_id(|_| i + 1)).collect::<Vec<_>>();
check_operations(tree, &indexed_ops)?;
} }
} }
} }

View File

@ -30,6 +30,17 @@ impl<C> Retention<C> {
Retention::Marked => true, Retention::Marked => true,
} }
} }
pub fn map<'a, D, F: Fn(&'a C) -> D>(&'a self, f: F) -> Retention<D> {
match self {
Retention::Ephemeral => Retention::Ephemeral,
Retention::Checkpoint { id, is_marked } => Retention::Checkpoint {
id: f(id),
is_marked: *is_marked,
},
Retention::Marked => Retention::Marked,
}
}
} }
/// A type representing the position of a leaf in a Merkle tree. /// A type representing the position of a leaf in a Merkle tree.

View File

@ -5,7 +5,7 @@ use core::marker::PhantomData;
use proptest::prelude::*; use proptest::prelude::*;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use crate::{Hashable, Level, Position}; use crate::{Hashable, Level, Position, Retention};
pub mod complete_tree; pub mod complete_tree;
@ -28,14 +28,14 @@ pub trait Frontier<H> {
/// A Merkle tree that supports incremental appends, marking of /// A Merkle tree that supports incremental appends, marking of
/// leaf nodes for construction of witnesses, checkpoints and rollbacks. /// leaf nodes for construction of witnesses, checkpoints and rollbacks.
pub trait Tree<H> { pub trait Tree<H, C> {
/// Returns the depth of the tree. /// Returns the depth of the tree.
fn depth(&self) -> u8; fn depth(&self) -> u8;
/// Appends a new value to the tree at the next available slot. /// Appends a new value to the tree at the next available slot.
/// Returns true if successful and false if the tree would exceed /// Returns true if successful and false if the tree would exceed
/// the maximum allowed depth. /// the maximum allowed depth.
fn append(&mut self, value: H) -> bool; fn append(&mut self, value: H, retention: Retention<C>) -> bool;
/// Returns the most recently appended leaf value. /// Returns the most recently appended leaf value.
fn current_position(&self) -> Option<Position>; fn current_position(&self) -> Option<Position>;
@ -47,12 +47,6 @@ pub trait Tree<H> {
/// a witness for it. /// a witness for it.
fn get_marked_leaf(&self, position: Position) -> Option<&H>; fn get_marked_leaf(&self, position: Position) -> Option<&H>;
/// Marks the current leaf as one for which we're interested in producing
/// a witness. Returns an optional value containing the
/// current position if successful or if the current value was already
/// marked, or None if the tree is empty.
fn mark(&mut self) -> Option<Position>;
/// Return a set of all the positions for which we have marked. /// Return a set of all the positions for which we have marked.
fn marked_positions(&self) -> BTreeSet<Position>; fn marked_positions(&self) -> BTreeSet<Position>;
@ -74,10 +68,13 @@ pub trait Tree<H> {
/// false if we were already not maintaining a mark at this position. /// false if we were already not maintaining a mark at this position.
fn remove_mark(&mut self, position: Position) -> bool; fn remove_mark(&mut self, position: Position) -> bool;
/// Creates a new checkpoint for the current tree state. It is valid to /// Creates a new checkpoint for the current tree state.
/// have multiple checkpoints for the same tree state, and each `rewind` ///
/// call will remove a single checkpoint. /// It is valid to have multiple checkpoints for the same tree state, and
fn checkpoint(&mut self); /// each `rewind` call will remove a single checkpoint. Returns `false`
/// if the checkpoint identifier provided is less than or equal to the
/// maximum checkpoint identifier observed.
fn checkpoint(&mut self, id: C) -> bool;
/// Rewinds the tree state to the previous checkpoint, and then removes /// Rewinds the tree state to the previous checkpoint, and then removes
/// that checkpoint record. If there are multiple checkpoints at a given /// that checkpoint record. If there are multiple checkpoints at a given
@ -138,15 +135,14 @@ impl<H: Hashable> Hashable for Option<H> {
// //
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Operation<A> { pub enum Operation<A, C> {
Append(A), Append(A, Retention<C>),
CurrentPosition, CurrentPosition,
CurrentLeaf, CurrentLeaf,
Mark,
MarkedLeaf(Position), MarkedLeaf(Position),
MarkedPositions, MarkedPositions,
Unmark(Position), Unmark(Position),
Checkpoint, Checkpoint(C),
Rewind, Rewind,
Authpath(Position, usize), Authpath(Position, usize),
GarbageCollect, GarbageCollect,
@ -154,39 +150,35 @@ pub enum Operation<A> {
use Operation::*; use Operation::*;
pub fn append_str(x: &str) -> Operation<String> { pub fn append_str<C>(x: &str, retention: Retention<C>) -> Operation<String, C> {
Operation::Append(x.to_string()) Operation::Append(x.to_string(), retention)
} }
pub fn unmark<T>(pos: usize) -> Operation<T> { pub fn unmark<H, C>(pos: usize) -> Operation<H, C> {
Operation::Unmark(Position::from(pos)) Operation::Unmark(Position::from(pos))
} }
pub fn witness<T>(pos: usize, depth: usize) -> Operation<T> { pub fn witness<H, C>(pos: usize, depth: usize) -> Operation<H, C> {
Operation::Authpath(Position::from(pos), depth) Operation::Authpath(Position::from(pos), depth)
} }
impl<H: Hashable + Clone> Operation<H> { impl<H: Hashable + Clone, C: Clone> Operation<H, C> {
pub fn apply<T: Tree<H>>(&self, tree: &mut T) -> Option<(Position, Vec<H>)> { pub fn apply<T: Tree<H, C>>(&self, tree: &mut T) -> Option<(Position, Vec<H>)> {
match self { match self {
Append(a) => { Append(a, r) => {
assert!(tree.append(a.clone()), "append failed"); assert!(tree.append(a.clone(), r.clone()), "append failed");
None None
} }
CurrentPosition => None, CurrentPosition => None,
CurrentLeaf => None, CurrentLeaf => None,
Mark => {
assert!(tree.mark().is_some(), "mark failed");
None
}
MarkedLeaf(_) => None, MarkedLeaf(_) => None,
MarkedPositions => None, MarkedPositions => None,
Unmark(p) => { Unmark(p) => {
assert!(tree.remove_mark(*p), "remove mark failed"); assert!(tree.remove_mark(*p), "remove mark failed");
None None
} }
Checkpoint => { Checkpoint(id) => {
tree.checkpoint(); tree.checkpoint(id.clone());
None None
} }
Rewind => { Rewind => {
@ -198,25 +190,50 @@ impl<H: Hashable + Clone> Operation<H> {
} }
} }
pub fn apply_all<T: Tree<H>>(ops: &[Operation<H>], tree: &mut T) -> Option<(Position, Vec<H>)> { pub fn apply_all<T: Tree<H, C>>(
ops: &[Operation<H, C>],
tree: &mut T,
) -> Option<(Position, Vec<H>)> {
let mut result = None; let mut result = None;
for op in ops { for op in ops {
result = op.apply(tree); result = op.apply(tree);
} }
result result
} }
pub fn map_checkpoint_id<D, F: Fn(&C) -> D>(&self, f: F) -> Operation<H, D> {
match self {
Append(a, r) => Append(a.clone(), r.map(f)),
CurrentPosition => CurrentPosition,
CurrentLeaf => CurrentLeaf,
MarkedLeaf(l) => MarkedLeaf(*l),
MarkedPositions => MarkedPositions,
Unmark(p) => Unmark(*p),
Checkpoint(id) => Checkpoint(f(id)),
Rewind => Rewind,
Authpath(p, d) => Authpath(*p, *d),
GarbageCollect => GarbageCollect,
}
}
}
pub fn arb_retention() -> impl Strategy<Value = Retention<()>> {
prop_oneof![
Just(Retention::Ephemeral),
any::<bool>().prop_map(|is_marked| Retention::Checkpoint { id: (), is_marked }),
Just(Retention::Marked),
]
} }
pub fn arb_operation<G: Strategy + Clone>( pub fn arb_operation<G: Strategy + Clone>(
item_gen: G, item_gen: G,
pos_gen: impl Strategy<Value = usize> + Clone, pos_gen: impl Strategy<Value = usize> + Clone,
) -> impl Strategy<Value = Operation<G::Value>> ) -> impl Strategy<Value = Operation<G::Value, ()>>
where where
G::Value: Clone + 'static, G::Value: Clone + 'static,
{ {
prop_oneof![ prop_oneof![
item_gen.prop_map(Operation::Append), (item_gen, arb_retention()).prop_map(|(i, r)| Operation::Append(i, r)),
Just(Operation::Mark),
prop_oneof![ prop_oneof![
Just(Operation::CurrentLeaf), Just(Operation::CurrentLeaf),
Just(Operation::CurrentPosition), Just(Operation::CurrentPosition),
@ -229,7 +246,7 @@ where
pos_gen pos_gen
.clone() .clone()
.prop_map(|i| Operation::Unmark(Position::from(i))), .prop_map(|i| Operation::Unmark(Position::from(i))),
Just(Operation::Checkpoint), Just(Operation::Checkpoint(())),
Just(Operation::Rewind), Just(Operation::Rewind),
pos_gen pos_gen
.prop_flat_map(|i| (0usize..10) .prop_flat_map(|i| (0usize..10)
@ -237,19 +254,16 @@ where
] ]
} }
pub fn apply_operation<H, T: Tree<H>>(tree: &mut T, op: Operation<H>) { pub fn apply_operation<H, C, T: Tree<H, C>>(tree: &mut T, op: Operation<H, C>) {
match op { match op {
Append(value) => { Append(value, r) => {
tree.append(value); tree.append(value, r);
}
Mark => {
tree.mark();
} }
Unmark(position) => { Unmark(position) => {
tree.remove_mark(position); tree.remove_mark(position);
} }
Checkpoint => { Checkpoint(id) => {
tree.checkpoint(); tree.checkpoint(id);
} }
Rewind => { Rewind => {
tree.rewind(); tree.rewind();
@ -263,9 +277,9 @@ pub fn apply_operation<H, T: Tree<H>>(tree: &mut T, op: Operation<H>) {
} }
} }
pub fn check_operations<H: Hashable + Ord + Clone + Debug, T: Tree<H>>( pub fn check_operations<H: Hashable + Ord + Clone, C: Clone, T: Tree<H, C>>(
mut tree: T, mut tree: T,
ops: &[Operation<H>], ops: &[Operation<H, C>],
) -> Result<(), TestCaseError> { ) -> Result<(), TestCaseError> {
let mut tree_size = 0; let mut tree_size = 0;
let mut tree_values: Vec<H> = vec![]; let mut tree_values: Vec<H> = vec![];
@ -275,11 +289,14 @@ pub fn check_operations<H: Hashable + Ord + Clone + Debug, T: Tree<H>>(
for op in ops { for op in ops {
prop_assert_eq!(tree_size, tree_values.len()); prop_assert_eq!(tree_size, tree_values.len());
match op { match op {
Append(value) => { Append(value, r) => {
if tree.append(value.clone()) { if tree.append(value.clone(), r.clone()) {
prop_assert!(tree_size < (1 << tree.depth())); prop_assert!(tree_size < (1 << tree.depth()));
tree_size += 1; tree_size += 1;
tree_values.push(value.clone()); tree_values.push(value.clone());
if r.is_checkpoint() {
tree_checkpoints.push(tree_size);
}
} else { } else {
prop_assert_eq!(tree_size, 1 << tree.depth()); prop_assert_eq!(tree_size, 1 << tree.depth());
} }
@ -293,13 +310,6 @@ pub fn check_operations<H: Hashable + Ord + Clone + Debug, T: Tree<H>>(
CurrentLeaf => { CurrentLeaf => {
prop_assert_eq!(tree_values.last(), tree.current_leaf()); prop_assert_eq!(tree_values.last(), tree.current_leaf());
} }
Mark => {
if tree.mark().is_some() {
prop_assert!(tree_size != 0);
} else {
prop_assert_eq!(tree_size, 0);
}
}
MarkedLeaf(position) => { MarkedLeaf(position) => {
if tree.get_marked_leaf(*position).is_some() { if tree.get_marked_leaf(*position).is_some() {
prop_assert!(<usize>::from(*position) < tree_size); prop_assert!(<usize>::from(*position) < tree_size);
@ -309,9 +319,9 @@ pub fn check_operations<H: Hashable + Ord + Clone + Debug, T: Tree<H>>(
tree.remove_mark(*position); tree.remove_mark(*position);
} }
MarkedPositions => {} MarkedPositions => {}
Checkpoint => { Checkpoint(id) => {
tree_checkpoints.push(tree_size); tree_checkpoints.push(tree_size);
tree.checkpoint(); tree.checkpoint(id.clone());
} }
Rewind => { Rewind => {
if tree.rewind() { if tree.rewind() {
@ -379,31 +389,35 @@ pub fn compute_root_from_witness<H: Hashable>(value: H, position: Position, path
// //
#[derive(Clone)] #[derive(Clone)]
pub struct CombinedTree<H, I: Tree<H>, E: Tree<H>> { pub struct CombinedTree<H, C, I: Tree<H, C>, E: Tree<H, C>> {
inefficient: I, inefficient: I,
efficient: E, efficient: E,
_phantom: PhantomData<H>, _phantom_h: PhantomData<H>,
_phantom_c: PhantomData<C>,
} }
impl<H: Hashable + Ord + Clone + Debug, I: Tree<H>, E: Tree<H>> CombinedTree<H, I, E> { impl<H: Hashable + Ord + Clone + Debug, C, I: Tree<H, C>, E: Tree<H, C>> CombinedTree<H, C, I, E> {
pub fn new(inefficient: I, efficient: E) -> Self { pub fn new(inefficient: I, efficient: E) -> Self {
assert_eq!(inefficient.depth(), efficient.depth()); assert_eq!(inefficient.depth(), efficient.depth());
CombinedTree { CombinedTree {
inefficient, inefficient,
efficient, efficient,
_phantom: PhantomData, _phantom_h: PhantomData,
_phantom_c: PhantomData,
} }
} }
} }
impl<H: Hashable + Ord + Clone + Debug, I: Tree<H>, E: Tree<H>> Tree<H> for CombinedTree<H, I, E> { impl<H: Hashable + Ord + Clone + Debug, C: Clone, I: Tree<H, C>, E: Tree<H, C>> Tree<H, C>
for CombinedTree<H, C, I, E>
{
fn depth(&self) -> u8 { fn depth(&self) -> u8 {
self.inefficient.depth() self.inefficient.depth()
} }
fn append(&mut self, value: H) -> bool { fn append(&mut self, value: H, retention: Retention<C>) -> bool {
let a = self.inefficient.append(value.clone()); let a = self.inefficient.append(value.clone(), retention.clone());
let b = self.efficient.append(value); let b = self.efficient.append(value, retention);
assert_eq!(a, b); assert_eq!(a, b);
a a
} }
@ -436,16 +450,6 @@ impl<H: Hashable + Ord + Clone + Debug, I: Tree<H>, E: Tree<H>> Tree<H> for Comb
a a
} }
fn mark(&mut self) -> Option<Position> {
let a = self.inefficient.mark();
let b = self.efficient.mark();
assert_eq!(a, b);
let apos = self.inefficient.marked_positions();
let bpos = self.efficient.marked_positions();
assert_eq!(apos, bpos);
a
}
fn marked_positions(&self) -> BTreeSet<Position> { fn marked_positions(&self) -> BTreeSet<Position> {
let a = self.inefficient.marked_positions(); let a = self.inefficient.marked_positions();
let b = self.efficient.marked_positions(); let b = self.efficient.marked_positions();
@ -467,9 +471,11 @@ impl<H: Hashable + Ord + Clone + Debug, I: Tree<H>, E: Tree<H>> Tree<H> for Comb
a a
} }
fn checkpoint(&mut self) { fn checkpoint(&mut self, id: C) -> bool {
self.inefficient.checkpoint(); let a = self.inefficient.checkpoint(id.clone());
self.efficient.checkpoint(); let b = self.efficient.checkpoint(id);
assert_eq!(a, b);
a
} }
fn rewind(&mut self) -> bool { fn rewind(&mut self) -> bool {
@ -479,38 +485,42 @@ impl<H: Hashable + Ord + Clone + Debug, I: Tree<H>, E: Tree<H>> Tree<H> for Comb
a a
} }
} }
// //
// Shared example tests // Shared example tests
// //
pub fn check_root_hashes<T: Tree<String>, F: Fn(usize) -> T>(new_tree: F) { pub fn check_root_hashes<T: Tree<String, usize>, F: Fn(usize) -> T>(new_tree: F) {
let mut tree = new_tree(100); let mut tree = new_tree(100);
assert_eq!(tree.root(0).unwrap(), "________________"); assert_eq!(tree.root(0).unwrap(), "________________");
tree.append("a".to_string()); tree.append("a".to_string(), Retention::Ephemeral);
assert_eq!(tree.root(0).unwrap().len(), 16); assert_eq!(tree.root(0).unwrap().len(), 16);
assert_eq!(tree.root(0).unwrap(), "a_______________"); assert_eq!(tree.root(0).unwrap(), "a_______________");
tree.append("b".to_string()); tree.append("b".to_string(), Retention::Ephemeral);
assert_eq!(tree.root(0).unwrap(), "ab______________"); assert_eq!(tree.root(0).unwrap(), "ab______________");
tree.append("c".to_string()); tree.append("c".to_string(), Retention::Ephemeral);
assert_eq!(tree.root(0).unwrap(), "abc_____________"); assert_eq!(tree.root(0).unwrap(), "abc_____________");
let mut t = new_tree(100); let mut t = new_tree(100);
t.append("a".to_string()); t.append(
t.checkpoint(); "a".to_string(),
t.mark(); Retention::Checkpoint {
t.append("a".to_string()); id: 1,
t.append("a".to_string()); is_marked: true,
t.append("a".to_string()); },
);
t.append("a".to_string(), Retention::Ephemeral);
t.append("a".to_string(), Retention::Ephemeral);
t.append("a".to_string(), Retention::Ephemeral);
assert_eq!(t.root(0).unwrap(), "aaaa____________"); assert_eq!(t.root(0).unwrap(), "aaaa____________");
} }
pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new_tree: F) { pub fn check_witnesses<T: Tree<String, usize> + std::fmt::Debug, F: Fn(usize) -> T>(new_tree: F) {
let mut tree = new_tree(100); let mut tree = new_tree(100);
tree.append("a".to_string()); tree.append("a".to_string(), Retention::Marked);
tree.mark();
assert_eq!( assert_eq!(
tree.witness(Position::from(0), 0), tree.witness(Position::from(0), 0),
Some(vec![ Some(vec![
@ -521,7 +531,7 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
]) ])
); );
tree.append("b".to_string()); tree.append("b".to_string(), Retention::Ephemeral);
assert_eq!( assert_eq!(
tree.witness(0.into(), 0), tree.witness(0.into(), 0),
Some(vec![ Some(vec![
@ -532,8 +542,7 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
]) ])
); );
tree.append("c".to_string()); tree.append("c".to_string(), Retention::Marked);
tree.mark();
assert_eq!( assert_eq!(
tree.witness(Position::from(2), 0), tree.witness(Position::from(2), 0),
Some(vec![ Some(vec![
@ -544,7 +553,7 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
]) ])
); );
tree.append("d".to_string()); tree.append("d".to_string(), Retention::Ephemeral);
assert_eq!( assert_eq!(
tree.witness(Position::from(2), 0), tree.witness(Position::from(2), 0),
Some(vec![ Some(vec![
@ -555,7 +564,7 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
]) ])
); );
tree.append("e".to_string()); tree.append("e".to_string(), Retention::Ephemeral);
assert_eq!( assert_eq!(
tree.witness(Position::from(2), 0), tree.witness(Position::from(2), 0),
Some(vec![ Some(vec![
@ -567,13 +576,12 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
); );
let mut tree = new_tree(100); let mut tree = new_tree(100);
tree.append("a".to_string()); tree.append("a".to_string(), Retention::Marked);
tree.mark(); for c in 'b'..'g' {
for c in 'b'..'h' { tree.append(c.to_string(), Retention::Ephemeral);
tree.append(c.to_string());
} }
tree.mark(); tree.append("g".to_string(), Retention::Marked);
tree.append("h".to_string()); tree.append("h".to_string(), Retention::Ephemeral);
assert_eq!( assert_eq!(
tree.witness(0.into(), 0), tree.witness(0.into(), 0),
@ -586,17 +594,13 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
); );
let mut tree = new_tree(100); let mut tree = new_tree(100);
tree.append("a".to_string()); tree.append("a".to_string(), Retention::Marked);
tree.mark(); tree.append("b".to_string(), Retention::Ephemeral);
tree.append("b".to_string()); tree.append("c".to_string(), Retention::Ephemeral);
tree.append("c".to_string()); tree.append("d".to_string(), Retention::Marked);
tree.append("d".to_string()); tree.append("e".to_string(), Retention::Marked);
tree.mark(); tree.append("f".to_string(), Retention::Marked);
tree.append("e".to_string()); tree.append("g".to_string(), Retention::Ephemeral);
tree.mark();
tree.append("f".to_string());
tree.mark();
tree.append("g".to_string());
assert_eq!( assert_eq!(
tree.witness(Position::from(5), 0), tree.witness(Position::from(5), 0),
@ -609,11 +613,11 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
); );
let mut tree = new_tree(100); let mut tree = new_tree(100);
for c in 'a'..'l' { for c in 'a'..'k' {
tree.append(c.to_string()); tree.append(c.to_string(), Retention::Ephemeral);
} }
tree.mark(); tree.append('k'.to_string(), Retention::Marked);
tree.append('l'.to_string()); tree.append('l'.to_string(), Retention::Ephemeral);
assert_eq!( assert_eq!(
tree.witness(Position::from(10), 0), tree.witness(Position::from(10), 0),
@ -626,16 +630,20 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
); );
let mut tree = new_tree(100); let mut tree = new_tree(100);
tree.append('a'.to_string()); assert!(tree.append(
tree.mark(); 'a'.to_string(),
tree.checkpoint(); Retention::Checkpoint {
id: 1,
is_marked: true
}
));
assert!(tree.rewind()); assert!(tree.rewind());
for c in 'b'..'f' { for c in 'b'..'e' {
tree.append(c.to_string()); tree.append(c.to_string(), Retention::Ephemeral);
} }
tree.mark(); tree.append("e".to_string(), Retention::Marked);
for c in 'f'..'i' { for c in 'f'..'i' {
tree.append(c.to_string()); tree.append(c.to_string(), Retention::Ephemeral);
} }
assert_eq!( assert_eq!(
tree.witness(0.into(), 0), tree.witness(0.into(), 0),
@ -648,17 +656,20 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
); );
let mut tree = new_tree(100); let mut tree = new_tree(100);
tree.append('a'.to_string()); tree.append('a'.to_string(), Retention::Ephemeral);
tree.append('b'.to_string()); tree.append('b'.to_string(), Retention::Ephemeral);
tree.append('c'.to_string()); tree.append('c'.to_string(), Retention::Marked);
tree.mark(); tree.append('d'.to_string(), Retention::Ephemeral);
tree.append('d'.to_string()); tree.append('e'.to_string(), Retention::Ephemeral);
tree.append('e'.to_string()); tree.append('f'.to_string(), Retention::Ephemeral);
tree.append('f'.to_string()); assert!(tree.append(
tree.append('g'.to_string()); 'g'.to_string(),
tree.mark(); Retention::Checkpoint {
tree.checkpoint(); id: 1,
tree.append('h'.to_string()); is_marked: true
}
));
tree.append('h'.to_string(), Retention::Ephemeral);
assert!(tree.rewind()); assert!(tree.rewind());
assert_eq!( assert_eq!(
tree.witness(Position::from(2), 0), tree.witness(Position::from(2), 0),
@ -671,20 +682,18 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
); );
let mut tree = new_tree(100); let mut tree = new_tree(100);
tree.append('a'.to_string()); tree.append('a'.to_string(), Retention::Ephemeral);
tree.append('b'.to_string()); tree.append('b'.to_string(), Retention::Marked);
tree.mark();
assert_eq!(tree.witness(Position::from(0), 0), None); assert_eq!(tree.witness(Position::from(0), 0), None);
let mut tree = new_tree(100); let mut tree = new_tree(100);
for c in 'a'..'n' { for c in 'a'..'m' {
tree.append(c.to_string()); tree.append(c.to_string(), Retention::Ephemeral);
} }
tree.mark(); tree.append('m'.to_string(), Retention::Marked);
tree.append('n'.to_string()); tree.append('n'.to_string(), Retention::Marked);
tree.mark(); tree.append('o'.to_string(), Retention::Ephemeral);
tree.append('o'.to_string()); tree.append('p'.to_string(), Retention::Ephemeral);
tree.append('p'.to_string());
assert_eq!( assert_eq!(
tree.witness(Position::from(12), 0), tree.witness(Position::from(12), 0),
@ -698,10 +707,9 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
let ops = ('a'..='l') let ops = ('a'..='l')
.into_iter() .into_iter()
.map(|c| Append(c.to_string())) .map(|c| Append(c.to_string(), Retention::Marked))
.chain(Some(Mark)) .chain(Some(Append('m'.to_string(), Retention::Ephemeral)))
.chain(Some(Append('m'.to_string()))) .chain(Some(Append('n'.to_string(), Retention::Ephemeral)))
.chain(Some(Append('n'.to_string())))
.chain(Some(Authpath(11usize.into(), 0))) .chain(Some(Authpath(11usize.into(), 0)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -720,76 +728,66 @@ pub fn check_witnesses<T: Tree<String> + std::fmt::Debug, F: Fn(usize) -> T>(new
); );
} }
pub fn check_checkpoint_rewind<T: Tree<String>, F: Fn(usize) -> T>(new_tree: F) { pub fn check_checkpoint_rewind<T: Tree<String, usize>, F: Fn(usize) -> T>(new_tree: F) {
let mut t = new_tree(100); let mut t = new_tree(100);
assert!(!t.rewind()); assert!(!t.rewind());
let mut t = new_tree(100); let mut t = new_tree(100);
t.checkpoint(); t.checkpoint(1);
assert!(t.rewind()); assert!(t.rewind());
let mut t = new_tree(100); let mut t = new_tree(100);
t.append("a".to_string()); t.append("a".to_string(), Retention::Ephemeral);
t.checkpoint(); t.checkpoint(1);
t.append("b".to_string()); t.append("b".to_string(), Retention::Marked);
t.mark();
assert!(t.rewind()); assert!(t.rewind());
assert_eq!(Some(Position::from(0)), t.current_position()); assert_eq!(Some(Position::from(0)), t.current_position());
let mut t = new_tree(100); let mut t = new_tree(100);
t.append("a".to_string()); t.append("a".to_string(), Retention::Marked);
t.mark(); t.checkpoint(1);
t.checkpoint();
assert!(t.rewind()); assert!(t.rewind());
let mut t = new_tree(100); let mut t = new_tree(100);
t.append("a".to_string()); t.append("a".to_string(), Retention::Marked);
t.checkpoint(); t.checkpoint(1);
t.mark(); t.append("a".to_string(), Retention::Ephemeral);
t.append("a".to_string());
assert!(t.rewind()); assert!(t.rewind());
assert_eq!(Some(Position::from(0)), t.current_position()); assert_eq!(Some(Position::from(0)), t.current_position());
let mut t = new_tree(100); let mut t = new_tree(100);
t.append("a".to_string()); t.append("a".to_string(), Retention::Ephemeral);
t.checkpoint(); t.checkpoint(1);
t.checkpoint(); t.checkpoint(2);
assert!(t.rewind()); assert!(t.rewind());
t.append("b".to_string()); t.append("b".to_string(), Retention::Ephemeral);
assert!(t.rewind()); assert!(t.rewind());
t.append("b".to_string()); t.append("b".to_string(), Retention::Ephemeral);
assert_eq!(t.root(0).unwrap(), "ab______________"); assert_eq!(t.root(0).unwrap(), "ab______________");
} }
pub fn check_remove_mark<T: Tree<String>, F: Fn(usize) -> T>(new_tree: F) { pub fn check_remove_mark<T: Tree<String, usize>, F: Fn(usize) -> T>(new_tree: F) {
let samples = vec![ let samples = vec![
vec![ vec![
append_str("a"), append_str("a", Retention::Ephemeral),
append_str("a"), append_str(
Checkpoint, "a",
Mark, Retention::Checkpoint {
id: 1,
is_marked: true,
},
),
witness(1, 1), witness(1, 1),
], ],
vec![ vec![
append_str("a"), append_str("a", Retention::Ephemeral),
append_str("a"), append_str("a", Retention::Ephemeral),
append_str("a"), append_str("a", Retention::Ephemeral),
append_str("a"), append_str("a", Retention::Marked),
Mark, Checkpoint(1),
Checkpoint,
unmark(3), unmark(3),
witness(3, 0), witness(3, 0),
], ],
vec![
append_str("a"),
append_str("a"),
Checkpoint,
Checkpoint,
Checkpoint,
Mark,
Checkpoint,
witness(1, 3),
],
]; ];
for (i, sample) in samples.iter().enumerate() { for (i, sample) in samples.iter().enumerate() {
@ -803,35 +801,25 @@ pub fn check_remove_mark<T: Tree<String>, F: Fn(usize) -> T>(new_tree: F) {
} }
} }
pub fn check_rewind_remove_mark<T: Tree<String>, F: Fn(usize) -> T>(new_tree: F) { pub fn check_rewind_remove_mark<T: Tree<String, usize>, F: Fn(usize) -> T>(new_tree: F) {
// rewinding doesn't remove a mark // rewinding doesn't remove a mark
let mut tree = new_tree(100); let mut tree = new_tree(100);
tree.append("e".to_string()); tree.append("e".to_string(), Retention::Marked);
tree.mark(); tree.checkpoint(1);
tree.checkpoint();
assert!(tree.rewind()); assert!(tree.rewind());
assert!(tree.remove_mark(0usize.into())); assert!(tree.remove_mark(0usize.into()));
// the order of checkpoint & mark does not matter
let mut tree = new_tree(100);
tree.append("e".to_string());
tree.checkpoint();
tree.mark();
assert!(tree.rewind());
assert!(!tree.remove_mark(0usize.into()));
// use a maximum number of checkpoints of 1 // use a maximum number of checkpoints of 1
let mut tree = new_tree(1); let mut tree = new_tree(1);
tree.append("e".to_string()); tree.append("e".to_string(), Retention::Marked);
tree.mark(); tree.checkpoint(1);
tree.checkpoint();
assert!(tree.marked_positions().contains(&0usize.into())); assert!(tree.marked_positions().contains(&0usize.into()));
tree.append("f".to_string()); tree.append("f".to_string(), Retention::Ephemeral);
// simulate a spend of `e` at `f` // simulate a spend of `e` at `f`
assert!(tree.remove_mark(0usize.into())); assert!(tree.remove_mark(0usize.into()));
// even though the mark has been staged for removal, it's not gone yet // even though the mark has been staged for removal, it's not gone yet
assert!(tree.marked_positions().contains(&0usize.into())); assert!(tree.marked_positions().contains(&0usize.into()));
tree.checkpoint(); tree.checkpoint(2);
// the newest checkpoint will have caused the oldest to roll off, and // the newest checkpoint will have caused the oldest to roll off, and
// so the forgotten node will be unmarked // so the forgotten node will be unmarked
assert!(!tree.marked_positions().contains(&0usize.into())); assert!(!tree.marked_positions().contains(&0usize.into()));
@ -842,42 +830,41 @@ pub fn check_rewind_remove_mark<T: Tree<String>, F: Fn(usize) -> T>(new_tree: F)
// chain state restoration. // chain state restoration.
let samples = vec![ let samples = vec![
vec![append_str("x"), Checkpoint, Mark, Rewind, unmark(0)],
vec![ vec![
append_str("d"), append_str("x", Retention::Marked),
Checkpoint, Checkpoint(1),
Mark, Rewind,
unmark(0),
],
vec![
append_str("d", Retention::Marked),
Checkpoint(1),
unmark(0), unmark(0),
Rewind, Rewind,
unmark(0), unmark(0),
], ],
vec![ vec![
append_str("o"), append_str("o", Retention::Marked),
Checkpoint, Checkpoint(1),
Mark, Checkpoint(2),
Checkpoint,
unmark(0), unmark(0),
Rewind, Rewind,
Rewind, Rewind,
], ],
vec![ vec![
append_str("s"), append_str("s", Retention::Marked),
Mark, append_str("m", Retention::Ephemeral),
append_str("m"), Checkpoint(1),
Checkpoint,
unmark(0), unmark(0),
Rewind, Rewind,
unmark(0), unmark(0),
unmark(0), unmark(0),
], ],
vec![ vec![
append_str("a"), append_str("a", Retention::Marked),
Mark, Checkpoint(1),
Checkpoint,
Mark,
Rewind, Rewind,
append_str("a"), append_str("a", Retention::Marked),
Mark,
], ],
]; ];
@ -892,131 +879,121 @@ pub fn check_rewind_remove_mark<T: Tree<String>, F: Fn(usize) -> T>(new_tree: F)
} }
} }
pub fn check_witness_consistency<T: Tree<String>, F: Fn(usize) -> T>(new_tree: F) { pub fn check_witness_consistency<T: Tree<String, usize>, F: Fn(usize) -> T>(new_tree: F) {
let samples = vec![ let samples = vec![
// Reduced examples // Reduced examples
vec![ vec![
append_str("a"), append_str("a", Retention::Ephemeral),
append_str("b"), append_str("b", Retention::Marked),
Checkpoint, Checkpoint(1),
Mark,
witness(0, 1), witness(0, 1),
], ],
vec![ vec![
append_str("c"), append_str("c", Retention::Ephemeral),
append_str("d"), append_str("d", Retention::Marked),
Mark, Checkpoint(1),
Checkpoint,
witness(1, 1), witness(1, 1),
], ],
vec![ vec![
append_str("e"), append_str("e", Retention::Marked),
Checkpoint, Checkpoint(1),
Mark, append_str("f", Retention::Ephemeral),
append_str("f"),
witness(0, 1), witness(0, 1),
], ],
vec![ vec![
append_str("g"), append_str("g", Retention::Marked),
Mark, Checkpoint(1),
Checkpoint,
unmark(0), unmark(0),
append_str("h"), append_str("h", Retention::Ephemeral),
witness(0, 0), witness(0, 0),
], ],
vec![ vec![
append_str("i"), append_str("i", Retention::Marked),
Checkpoint, Checkpoint(1),
Mark,
unmark(0), unmark(0),
append_str("j"), append_str("j", Retention::Ephemeral),
witness(0, 0), witness(0, 0),
], ],
vec![ vec![
append_str("i"), append_str("i", Retention::Marked),
Mark, append_str("j", Retention::Ephemeral),
append_str("j"), Checkpoint(1),
Checkpoint, append_str("k", Retention::Ephemeral),
append_str("k"),
witness(0, 1), witness(0, 1),
], ],
vec![ vec![
append_str("l"), append_str("l", Retention::Marked),
Checkpoint, Checkpoint(1),
Mark, Checkpoint(2),
Checkpoint, append_str("m", Retention::Ephemeral),
append_str("m"), Checkpoint(3),
Checkpoint,
witness(0, 2), witness(0, 2),
], ],
vec![Checkpoint, append_str("n"), Mark, witness(0, 1)],
vec![ vec![
append_str("a"), Checkpoint(1),
Mark, append_str("n", Retention::Marked),
Checkpoint,
unmark(0),
Checkpoint,
append_str("b"),
witness(0, 1), witness(0, 1),
], ],
vec![ vec![
append_str("a"), append_str("a", Retention::Marked),
Mark, Checkpoint(1),
append_str("b"),
unmark(0), unmark(0),
Checkpoint, Checkpoint(2),
append_str("b", Retention::Ephemeral),
witness(0, 1),
],
vec![
append_str("a", Retention::Marked),
append_str("b", Retention::Ephemeral),
unmark(0),
Checkpoint(1),
witness(0, 0), witness(0, 0),
], ],
vec![ vec![
append_str("a"), append_str("a", Retention::Marked),
Mark, Checkpoint(1),
Checkpoint,
unmark(0), unmark(0),
Checkpoint, Checkpoint(2),
Rewind, Rewind,
append_str("b"), append_str("b", Retention::Ephemeral),
witness(0, 0), witness(0, 0),
], ],
vec![ vec![
append_str("a"), append_str("a", Retention::Marked),
Mark, Checkpoint(1),
Checkpoint, Checkpoint(2),
Checkpoint,
Rewind, Rewind,
append_str("a"), append_str("a", Retention::Ephemeral),
unmark(0), unmark(0),
witness(0, 1), witness(0, 1),
], ],
// Unreduced examples // Unreduced examples
vec![ vec![
append_str("o"), append_str("o", Retention::Ephemeral),
append_str("p"), append_str("p", Retention::Marked),
Mark, append_str("q", Retention::Ephemeral),
append_str("q"), Checkpoint(1),
Checkpoint,
unmark(1), unmark(1),
witness(1, 1), witness(1, 1),
], ],
vec![ vec![
append_str("r"), append_str("r", Retention::Ephemeral),
append_str("s"), append_str("s", Retention::Ephemeral),
append_str("t"), append_str("t", Retention::Marked),
Mark, Checkpoint(1),
Checkpoint,
unmark(2), unmark(2),
Checkpoint, Checkpoint(2),
witness(2, 2), witness(2, 2),
], ],
vec![ vec![
append_str("u"), append_str("u", Retention::Marked),
Mark, append_str("v", Retention::Ephemeral),
append_str("v"), append_str("w", Retention::Ephemeral),
append_str("w"), Checkpoint(1),
Checkpoint,
unmark(0), unmark(0),
append_str("x"), append_str("x", Retention::Ephemeral),
Checkpoint, Checkpoint(2),
Checkpoint, Checkpoint(3),
witness(0, 3), witness(0, 3),
], ],
]; ];

View File

@ -1,8 +1,8 @@
//! Sample implementation of the Tree interface. //! Sample implementation of the Tree interface.
use std::cmp::min; use std::cmp::min;
use std::collections::{BTreeSet, VecDeque}; use std::collections::{BTreeMap, BTreeSet};
use crate::{testing::Tree, Hashable, Level, Position}; use crate::{testing::Tree, Hashable, Level, Position, Retention};
pub(crate) fn root<H: Hashable + Clone>(leaves: &[H], depth: u8) -> H { pub(crate) fn root<H: Hashable + Clone>(leaves: &[H], depth: u8) -> H {
let empty_leaf = H::empty_leaf(); let empty_leaf = H::empty_leaf();
@ -38,7 +38,7 @@ pub(crate) fn root<H: Hashable + Clone>(leaves: &[H], depth: u8) -> H {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Checkpoint { pub struct Checkpoint {
/// The number of leaves that will be retained /// The number of leaves in the tree when the checkpoint was created.
leaves_len: usize, leaves_len: usize,
/// A set of the positions that have been marked during the period that this /// A set of the positions that have been marked during the period that this
/// checkpoint is the current checkpoint. /// checkpoint is the current checkpoint.
@ -50,7 +50,7 @@ pub struct Checkpoint {
} }
impl Checkpoint { impl Checkpoint {
pub fn at_length(leaves_len: usize) -> Self { fn at_length(leaves_len: usize) -> Self {
Checkpoint { Checkpoint {
leaves_len, leaves_len,
marked: BTreeSet::new(), marked: BTreeSet::new(),
@ -60,71 +60,69 @@ impl Checkpoint {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CompleteTree<H, const DEPTH: u8> { pub struct CompleteTree<H, C: Ord, const DEPTH: u8> {
leaves: Vec<Option<H>>, leaves: Vec<Option<H>>,
marks: BTreeSet<Position>, marks: BTreeSet<Position>,
checkpoints: VecDeque<Checkpoint>, checkpoints: BTreeMap<C, Checkpoint>,
max_checkpoints: usize, max_checkpoints: usize,
} }
impl<H: Hashable, const DEPTH: u8> CompleteTree<H, DEPTH> { impl<H: Hashable, C: Clone + Ord + core::fmt::Debug, const DEPTH: u8> CompleteTree<H, C, DEPTH> {
/// Creates a new, empty binary tree /// Creates a new, empty binary tree
pub fn new(max_checkpoints: usize) -> Self { pub fn new(max_checkpoints: usize, initial_checkpoint_id: C) -> Self {
Self { Self {
leaves: vec![], leaves: vec![],
marks: BTreeSet::new(), marks: BTreeSet::new(),
checkpoints: VecDeque::from(vec![Checkpoint::at_length(0)]), checkpoints: BTreeMap::from([(initial_checkpoint_id, Checkpoint::at_length(0))]),
max_checkpoints, max_checkpoints,
} }
} }
fn append(&mut self, value: H) -> bool { /// Appends a new value to the tree at the next available slot.
if self.leaves.len() == (1 << DEPTH) { ///
false /// Returns true if successful and false if the tree is full or, for values with `Checkpoint`
} else { /// retention, if a checkpoint id would be introduced that is less than or equal to the current
self.leaves.push(Some(value)); /// maximum checkpoint id.
true fn append(&mut self, value: H, retention: Retention<C>) -> Result<(), AppendError<C>> {
} fn append<H, C>(
} leaves: &mut Vec<Option<H>>,
value: H,
fn leaves_at_checkpoint_depth(&self, checkpoint_depth: usize) -> Option<usize> { depth: u8,
if checkpoint_depth == 0 { ) -> Result<(), AppendError<C>> {
Some(self.leaves.len()) if leaves.len() < (1 << depth) {
} else if checkpoint_depth <= self.checkpoints.len() { leaves.push(Some(value));
self.checkpoints Ok(())
.get(self.checkpoints.len() - checkpoint_depth) } else {
.map(|c| c.leaves_len) Err(AppendError::TreeFull)
} else {
None
}
}
}
impl<H: Hashable + PartialEq + Clone, const DEPTH: u8> CompleteTree<H, DEPTH> {
/// Removes the oldest checkpoint. Returns true if successful and false if
/// there are fewer than `self.max_checkpoints` checkpoints.
fn drop_oldest_checkpoint(&mut self) -> bool {
if self.checkpoints.len() > self.max_checkpoints {
let c = self.checkpoints.pop_front().unwrap();
for pos in c.forgotten.iter() {
self.marks.remove(pos);
} }
true
} else {
false
} }
}
}
impl<H: Hashable + PartialEq + Clone + std::fmt::Debug, const DEPTH: u8> Tree<H> match retention {
for CompleteTree<H, DEPTH> Retention::Marked => {
{ append(&mut self.leaves, value, DEPTH)?;
fn depth(&self) -> u8 { self.mark();
DEPTH }
} Retention::Checkpoint { id, is_marked } => {
let latest_checkpoint = self.checkpoints.keys().rev().next();
if Some(&id) > latest_checkpoint {
append(&mut self.leaves, value, DEPTH)?;
if is_marked {
self.mark();
}
self.checkpoint(id, self.current_position());
} else {
return Err(AppendError::CheckpointOutOfOrder {
current_max: latest_checkpoint.cloned(),
checkpoint: id,
});
}
}
Retention::Ephemeral => {
append(&mut self.leaves, value, DEPTH)?;
}
}
fn append(&mut self, value: H) -> bool { Ok(())
Self::append(self, value)
} }
fn current_position(&self) -> Option<Position> { fn current_position(&self) -> Option<Position> {
@ -135,6 +133,90 @@ impl<H: Hashable + PartialEq + Clone + std::fmt::Debug, const DEPTH: u8> Tree<H>
} }
} }
fn mark(&mut self) -> Option<Position> {
match self.current_position() {
Some(pos) => {
if !self.marks.contains(&pos) {
self.marks.insert(pos);
self.checkpoints
.iter_mut()
.rev()
.next()
.unwrap()
.1
.marked
.insert(pos);
}
Some(pos)
}
None => None,
}
}
fn checkpoint(&mut self, id: C, pos: Option<Position>) {
self.checkpoints.insert(
id,
Checkpoint::at_length(pos.map_or_else(|| 0, |p| usize::from(p) + 1)),
);
if self.checkpoints.len() > self.max_checkpoints {
self.drop_oldest_checkpoint();
}
}
fn leaves_at_checkpoint_depth(&self, checkpoint_depth: usize) -> Option<usize> {
if checkpoint_depth == 0 {
Some(self.leaves.len())
} else {
self.checkpoints
.iter()
.rev()
.skip(checkpoint_depth - 1)
.map(|(_, c)| c.leaves_len)
.next()
}
}
/// Removes the oldest checkpoint. Returns true if successful and false if
/// there are fewer than `self.max_checkpoints` checkpoints.
fn drop_oldest_checkpoint(&mut self) -> bool {
if self.checkpoints.len() > self.max_checkpoints {
let (id, c) = self.checkpoints.iter().next().unwrap();
for pos in c.forgotten.iter() {
self.marks.remove(pos);
}
let id = id.clone(); // needed to avoid mutable/immutable borrow conflict
self.checkpoints.remove(&id);
true
} else {
false
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum AppendError<C> {
TreeFull,
CheckpointOutOfOrder {
current_max: Option<C>,
checkpoint: C,
},
}
impl<H: Hashable + PartialEq + Clone, C: Ord + Clone + core::fmt::Debug, const DEPTH: u8> Tree<H, C>
for CompleteTree<H, C, DEPTH>
{
fn depth(&self) -> u8 {
DEPTH
}
fn append(&mut self, value: H, retention: Retention<C>) -> bool {
Self::append(self, value, retention).is_ok()
}
fn current_position(&self) -> Option<Position> {
Self::current_position(self)
}
fn current_leaf(&self) -> Option<&H> { fn current_leaf(&self) -> Option<&H> {
self.leaves.last().and_then(|opt: &Option<H>| opt.as_ref()) self.leaves.last().and_then(|opt: &Option<H>| opt.as_ref())
} }
@ -149,19 +231,6 @@ impl<H: Hashable + PartialEq + Clone + std::fmt::Debug, const DEPTH: u8> Tree<H>
} }
} }
fn mark(&mut self) -> Option<Position> {
match self.current_position() {
Some(pos) => {
if !self.marks.contains(&pos) {
self.marks.insert(pos);
self.checkpoints.back_mut().unwrap().marked.insert(pos);
}
Some(pos)
}
None => None,
}
}
fn marked_positions(&self) -> BTreeSet<Position> { fn marked_positions(&self) -> BTreeSet<Position> {
self.marks.clone() self.marks.clone()
} }
@ -173,18 +242,13 @@ impl<H: Hashable + PartialEq + Clone + std::fmt::Debug, const DEPTH: u8> Tree<H>
fn witness(&self, position: Position, checkpoint_depth: usize) -> Option<Vec<H>> { fn witness(&self, position: Position, checkpoint_depth: usize) -> Option<Vec<H>> {
if self.marks.contains(&position) && checkpoint_depth <= self.checkpoints.len() { if self.marks.contains(&position) && checkpoint_depth <= self.checkpoints.len() {
let checkpoint_idx = self.checkpoints.len() - checkpoint_depth; let leaves_len = self.leaves_at_checkpoint_depth(checkpoint_depth)?;
let len = if checkpoint_depth == 0 { let c_idx = self.checkpoints.len() - checkpoint_depth;
self.leaves.len()
} else {
self.checkpoints[checkpoint_idx].leaves_len
};
if self if self
.checkpoints .checkpoints
.iter() .iter()
.skip(checkpoint_idx) .skip(c_idx)
.any(|c| c.marked.contains(&position)) .any(|(_, c)| c.marked.contains(&position))
{ {
// The requested position was marked after the checkpoint was created, so we // The requested position was marked after the checkpoint was created, so we
// cannot create a witness. // cannot create a witness.
@ -195,8 +259,8 @@ impl<H: Hashable + PartialEq + Clone + std::fmt::Debug, const DEPTH: u8> Tree<H>
let mut leaf_idx: usize = position.into(); let mut leaf_idx: usize = position.into();
for bit in 0..DEPTH { for bit in 0..DEPTH {
leaf_idx ^= 1 << bit; leaf_idx ^= 1 << bit;
path.push(if leaf_idx < len { path.push(if leaf_idx < leaves_len {
let subtree_end = min(leaf_idx + (1 << bit), len); let subtree_end = min(leaf_idx + (1 << bit), leaves_len);
root(&self.leaves[leaf_idx..subtree_end], bit)? root(&self.leaves[leaf_idx..subtree_end], bit)?
} else { } else {
H::empty_root(Level::from(bit)) H::empty_root(Level::from(bit))
@ -214,8 +278,11 @@ impl<H: Hashable + PartialEq + Clone + std::fmt::Debug, const DEPTH: u8> Tree<H>
fn remove_mark(&mut self, position: Position) -> bool { fn remove_mark(&mut self, position: Position) -> bool {
if self.marks.contains(&position) { if self.marks.contains(&position) {
self.checkpoints self.checkpoints
.back_mut() .iter_mut()
.rev()
.next()
.unwrap() .unwrap()
.1
.forgotten .forgotten
.insert(position); .insert(position);
true true
@ -224,21 +291,24 @@ impl<H: Hashable + PartialEq + Clone + std::fmt::Debug, const DEPTH: u8> Tree<H>
} }
} }
fn checkpoint(&mut self) { fn checkpoint(&mut self, id: C) -> bool {
self.checkpoints if Some(&id) > self.checkpoints.iter().rev().next().map(|(id, _)| id) {
.push_back(Checkpoint::at_length(self.leaves.len())); Self::checkpoint(self, id, self.current_position());
if self.checkpoints.len() > self.max_checkpoints { true
self.drop_oldest_checkpoint(); } else {
false
} }
} }
fn rewind(&mut self) -> bool { fn rewind(&mut self) -> bool {
if self.checkpoints.len() > 1 { if self.checkpoints.len() > 1 {
let c = self.checkpoints.pop_back().unwrap(); let (id, c) = self.checkpoints.iter().rev().next().unwrap();
self.leaves.truncate(c.leaves_len); self.leaves.truncate(c.leaves_len);
for pos in c.marked.iter() { for pos in c.marked.iter() {
self.marks.remove(pos); self.marks.remove(pos);
} }
let id = id.clone(); // needed to avoid mutable/immutable borrow conflict
self.checkpoints.remove(&id);
true true
} else { } else {
false false
@ -256,7 +326,7 @@ mod tests {
check_checkpoint_rewind, check_rewind_remove_mark, check_root_hashes, check_witnesses, check_checkpoint_rewind, check_rewind_remove_mark, check_root_hashes, check_witnesses,
compute_root_from_witness, SipHashable, Tree, compute_root_from_witness, SipHashable, Tree,
}, },
Hashable, Level, Position, Hashable, Level, Position, Retention,
}; };
#[test] #[test]
@ -267,7 +337,7 @@ mod tests {
expected = SipHashable::combine(lvl.into(), &expected, &expected); expected = SipHashable::combine(lvl.into(), &expected, &expected);
} }
let tree = CompleteTree::<SipHashable, DEPTH>::new(100); let tree = CompleteTree::<SipHashable, (), DEPTH>::new(100, ());
assert_eq!(tree.root(0).unwrap(), expected); assert_eq!(tree.root(0).unwrap(), expected);
} }
@ -276,11 +346,11 @@ mod tests {
const DEPTH: u8 = 3; const DEPTH: u8 = 3;
let values = (0..(1 << DEPTH)).into_iter().map(SipHashable); let values = (0..(1 << DEPTH)).into_iter().map(SipHashable);
let mut tree = CompleteTree::<SipHashable, DEPTH>::new(100); let mut tree = CompleteTree::<SipHashable, (), DEPTH>::new(100, ());
for value in values { for value in values {
assert!(tree.append(value)); assert!(tree.append(value, Retention::Ephemeral).is_ok());
} }
assert!(!tree.append(SipHashable(0))); assert!(tree.append(SipHashable(0), Retention::Ephemeral).is_err());
let expected = SipHashable::combine( let expected = SipHashable::combine(
Level::from(2), Level::from(2),
@ -301,25 +371,30 @@ mod tests {
#[test] #[test]
fn root_hashes() { fn root_hashes() {
check_root_hashes(CompleteTree::<String, 4>::new); check_root_hashes(|max_checkpoints| {
CompleteTree::<String, usize, 4>::new(max_checkpoints, 0)
});
} }
#[test] #[test]
fn witness() { fn witness() {
check_witnesses(CompleteTree::<String, 4>::new); check_witnesses(|max_checkpoints| {
CompleteTree::<String, usize, 4>::new(max_checkpoints, 0)
});
} }
#[test] #[test]
fn correct_witness() { fn correct_witness() {
use crate::{testing::Tree, Retention};
const DEPTH: u8 = 3; const DEPTH: u8 = 3;
let values = (0..(1 << DEPTH)).into_iter().map(SipHashable); let values = (0..(1 << DEPTH)).into_iter().map(SipHashable);
let mut tree = CompleteTree::<SipHashable, DEPTH>::new(100); let mut tree = CompleteTree::<SipHashable, (), DEPTH>::new(100, ());
for value in values { for value in values {
assert!(tree.append(value)); assert!(Tree::append(&mut tree, value, Retention::Marked));
tree.mark();
} }
assert!(!tree.append(SipHashable(0))); assert!(tree.append(SipHashable(0), Retention::Ephemeral).is_err());
let expected = SipHashable::combine( let expected = SipHashable::combine(
<Level>::from(2), <Level>::from(2),
@ -349,11 +424,15 @@ mod tests {
#[test] #[test]
fn checkpoint_rewind() { fn checkpoint_rewind() {
check_checkpoint_rewind(CompleteTree::<String, 4>::new); check_checkpoint_rewind(|max_checkpoints| {
CompleteTree::<String, usize, 4>::new(max_checkpoints, 0)
});
} }
#[test] #[test]
fn rewind_remove_mark() { fn rewind_remove_mark() {
check_rewind_remove_mark(CompleteTree::<String, 4>::new); check_rewind_remove_mark(|max_checkpoints| {
CompleteTree::<String, usize, 4>::new(max_checkpoints, 0)
});
} }
} }