From 0cb1cec21fd5d60c4b38ebf88fa25954863feb6b Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 13 Jan 2023 08:40:57 -0700 Subject: [PATCH] Add `shardtree` witness operation & implement property tests. --- incrementalmerkletree/src/testing.rs | 235 +++++++++++--- .../src/testing/complete_tree.rs | 2 +- shardtree/proptest-regressions/lib.txt | 18 ++ shardtree/src/lib.rs | 287 +++++++++++++++++- 4 files changed, 489 insertions(+), 53 deletions(-) create mode 100644 shardtree/proptest-regressions/lib.txt diff --git a/incrementalmerkletree/src/testing.rs b/incrementalmerkletree/src/testing.rs index 8435157..51ed920 100644 --- a/incrementalmerkletree/src/testing.rs +++ b/incrementalmerkletree/src/testing.rs @@ -541,7 +541,7 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> ]) ); - tree.append("b".to_string(), Retention::Ephemeral); + tree.append("b".to_string(), Ephemeral); assert_eq!( tree.witness(0.into(), 0), Some(vec![ @@ -552,7 +552,7 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> ]) ); - tree.append("c".to_string(), Retention::Marked); + tree.append("c".to_string(), Marked); assert_eq!( tree.witness(Position::from(2), 0), Some(vec![ @@ -563,7 +563,7 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> ]) ); - tree.append("d".to_string(), Retention::Ephemeral); + tree.append("d".to_string(), Ephemeral); assert_eq!( tree.witness(Position::from(2), 0), Some(vec![ @@ -574,7 +574,7 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> ]) ); - tree.append("e".to_string(), Retention::Ephemeral); + tree.append("e".to_string(), Ephemeral); assert_eq!( tree.witness(Position::from(2), 0), Some(vec![ @@ -586,12 +586,12 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> ); let mut tree = new_tree(100); - tree.append("a".to_string(), Retention::Marked); + tree.append("a".to_string(), Marked); for c in 'b'..'g' { - tree.append(c.to_string(), Retention::Ephemeral); + tree.append(c.to_string(), Ephemeral); } - tree.append("g".to_string(), Retention::Marked); - tree.append("h".to_string(), Retention::Ephemeral); + tree.append("g".to_string(), Marked); + tree.append("h".to_string(), Ephemeral); assert_eq!( tree.witness(0.into(), 0), @@ -604,13 +604,13 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> ); let mut tree = new_tree(100); - tree.append("a".to_string(), Retention::Marked); - tree.append("b".to_string(), Retention::Ephemeral); - tree.append("c".to_string(), Retention::Ephemeral); - tree.append("d".to_string(), Retention::Marked); - tree.append("e".to_string(), Retention::Marked); - tree.append("f".to_string(), Retention::Marked); - tree.append("g".to_string(), Retention::Ephemeral); + tree.append("a".to_string(), Marked); + tree.append("b".to_string(), Ephemeral); + tree.append("c".to_string(), Ephemeral); + tree.append("d".to_string(), Marked); + tree.append("e".to_string(), Marked); + tree.append("f".to_string(), Marked); + tree.append("g".to_string(), Ephemeral); assert_eq!( tree.witness(Position::from(5), 0), @@ -624,10 +624,10 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> let mut tree = new_tree(100); for c in 'a'..'k' { - tree.append(c.to_string(), Retention::Ephemeral); + assert!(tree.append(c.to_string(), Ephemeral)); } - tree.append('k'.to_string(), Retention::Marked); - tree.append('l'.to_string(), Retention::Ephemeral); + assert!(tree.append('k'.to_string(), Marked)); + assert!(tree.append('l'.to_string(), Ephemeral)); assert_eq!( tree.witness(Position::from(10), 0), @@ -642,18 +642,18 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> let mut tree = new_tree(100); assert!(tree.append( 'a'.to_string(), - Retention::Checkpoint { + Checkpoint { id: 1, is_marked: true } )); assert!(tree.rewind()); for c in 'b'..'e' { - tree.append(c.to_string(), Retention::Ephemeral); + tree.append(c.to_string(), Ephemeral); } - tree.append("e".to_string(), Retention::Marked); + tree.append("e".to_string(), Marked); for c in 'f'..'i' { - tree.append(c.to_string(), Retention::Ephemeral); + tree.append(c.to_string(), Ephemeral); } assert_eq!( tree.witness(0.into(), 0), @@ -666,20 +666,20 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> ); let mut tree = new_tree(100); - tree.append('a'.to_string(), Retention::Ephemeral); - tree.append('b'.to_string(), Retention::Ephemeral); - tree.append('c'.to_string(), Retention::Marked); - tree.append('d'.to_string(), Retention::Ephemeral); - tree.append('e'.to_string(), Retention::Ephemeral); - tree.append('f'.to_string(), Retention::Ephemeral); + tree.append('a'.to_string(), Ephemeral); + tree.append('b'.to_string(), Ephemeral); + tree.append('c'.to_string(), Marked); + tree.append('d'.to_string(), Ephemeral); + tree.append('e'.to_string(), Ephemeral); + tree.append('f'.to_string(), Ephemeral); assert!(tree.append( 'g'.to_string(), - Retention::Checkpoint { + Checkpoint { id: 1, is_marked: true } )); - tree.append('h'.to_string(), Retention::Ephemeral); + tree.append('h'.to_string(), Ephemeral); assert!(tree.rewind()); assert_eq!( tree.witness(Position::from(2), 0), @@ -692,18 +692,18 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> ); let mut tree = new_tree(100); - tree.append('a'.to_string(), Retention::Ephemeral); - tree.append('b'.to_string(), Retention::Marked); + tree.append('a'.to_string(), Ephemeral); + tree.append('b'.to_string(), Marked); assert_eq!(tree.witness(Position::from(0), 0), None); let mut tree = new_tree(100); for c in 'a'..'m' { - tree.append(c.to_string(), Retention::Ephemeral); + tree.append(c.to_string(), Ephemeral); } - tree.append('m'.to_string(), Retention::Marked); - tree.append('n'.to_string(), Retention::Marked); - tree.append('o'.to_string(), Retention::Ephemeral); - tree.append('p'.to_string(), Retention::Ephemeral); + tree.append('m'.to_string(), Marked); + tree.append('n'.to_string(), Marked); + tree.append('o'.to_string(), Ephemeral); + tree.append('p'.to_string(), Ephemeral); assert_eq!( tree.witness(Position::from(12), 0), @@ -717,9 +717,9 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> let ops = ('a'..='l') .into_iter() - .map(|c| Append(c.to_string(), Retention::Marked)) - .chain(Some(Append('m'.to_string(), Retention::Ephemeral))) - .chain(Some(Append('n'.to_string(), Retention::Ephemeral))) + .map(|c| Append(c.to_string(), Marked)) + .chain(Some(Append('m'.to_string(), Ephemeral))) + .chain(Some(Append('n'.to_string(), Ephemeral))) .chain(Some(Witness(11usize.into(), 0))) .collect::>(); @@ -736,6 +736,153 @@ pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> ] )) ); + + let ops = vec![ + Append("a".to_string(), Ephemeral), + Append("b".to_string(), Ephemeral), + Append("c".to_string(), Ephemeral), + Append( + "d".to_string(), + Checkpoint { + id: 1, + is_marked: true, + }, + ), + Append("e".to_string(), Marked), + Operation::Checkpoint(2), + Append( + "f".to_string(), + Checkpoint { + id: 3, + is_marked: false, + }, + ), + Append( + "g".to_string(), + Checkpoint { + id: 4, + is_marked: false, + }, + ), + Append( + "h".to_string(), + Checkpoint { + id: 5, + is_marked: false, + }, + ), + Witness(3usize.into(), 5), + ]; + let mut tree = new_tree(100); + assert_eq!( + Operation::apply_all(&ops, &mut tree), + Some(( + Position::from(3), + vec![ + "c".to_string(), + "ab".to_string(), + "____".to_string(), + "________".to_string() + ] + )) + ); + let ops = vec![ + Append("a".to_string(), Ephemeral), + Append("a".to_string(), Ephemeral), + Append("a".to_string(), Ephemeral), + Append( + "a".to_string(), + Checkpoint { + id: 1, + is_marked: true, + }, + ), + Append("a".to_string(), Ephemeral), + Append("a".to_string(), Ephemeral), + Append("a".to_string(), Ephemeral), + Append( + "a".to_string(), + Checkpoint { + id: 2, + is_marked: false, + }, + ), + Append("a".to_string(), Ephemeral), + Append("a".to_string(), Ephemeral), + Witness(Position(3), 1), + ]; + let mut tree = new_tree(100); + assert_eq!( + Operation::apply_all(&ops, &mut tree), + Some(( + Position::from(3), + vec![ + "a".to_string(), + "aa".to_string(), + "aaaa".to_string(), + "________".to_string() + ] + )) + ); + + let ops = vec![ + Append("a".to_string(), Marked), + Append("a".to_string(), Ephemeral), + Append("a".to_string(), Ephemeral), + Append("a".to_string(), Ephemeral), + Append("a".to_string(), Ephemeral), + Append("a".to_string(), Ephemeral), + Append("a".to_string(), Ephemeral), + Operation::Checkpoint(1), + Append("a".to_string(), Marked), + Operation::Checkpoint(2), + Operation::Checkpoint(3), + Append( + "a".to_string(), + Checkpoint { + id: 4, + is_marked: false, + }, + ), + Rewind, + Rewind, + Witness(Position(7), 2), + ]; + let mut tree = new_tree(100); + assert_eq!(Operation::apply_all(&ops, &mut tree), None); + + let ops = vec![ + Append("a".to_string(), Marked), + Append("a".to_string(), Ephemeral), + Append( + "a".to_string(), + Checkpoint { + id: 1, + is_marked: true, + }, + ), + Append( + "a".to_string(), + Checkpoint { + id: 4, + is_marked: false, + }, + ), + Witness(Position(2), 2), + ]; + let mut tree = new_tree(100); + assert_eq!( + Operation::apply_all(&ops, &mut tree), + Some(( + Position::from(2), + vec![ + "_".to_string(), + "aa".to_string(), + "____".to_string(), + "________".to_string() + ] + )) + ); } pub fn check_checkpoint_rewind, F: Fn(usize) -> T>(new_tree: F) { @@ -821,15 +968,15 @@ pub fn check_rewind_remove_mark, F: Fn(usize) -> T>(new_t // use a maximum number of checkpoints of 1 let mut tree = new_tree(1); - tree.append("e".to_string(), Retention::Marked); - tree.checkpoint(1); + assert!(tree.append("e".to_string(), Retention::Marked)); + assert!(tree.checkpoint(1)); assert!(tree.marked_positions().contains(&0usize.into())); - tree.append("f".to_string(), Retention::Ephemeral); + assert!(tree.append("f".to_string(), Retention::Ephemeral)); // simulate a spend of `e` at `f` assert!(tree.remove_mark(0usize.into())); // even though the mark has been staged for removal, it's not gone yet assert!(tree.marked_positions().contains(&0usize.into())); - tree.checkpoint(2); + assert!(tree.checkpoint(2)); // the newest checkpoint will have caused the oldest to roll off, and // so the forgotten node will be unmarked assert!(!tree.marked_positions().contains(&0usize.into())); diff --git a/incrementalmerkletree/src/testing/complete_tree.rs b/incrementalmerkletree/src/testing/complete_tree.rs index 53a2989..389883d 100644 --- a/incrementalmerkletree/src/testing/complete_tree.rs +++ b/incrementalmerkletree/src/testing/complete_tree.rs @@ -380,7 +380,7 @@ mod tests { } #[test] - fn witness() { + fn witnesses() { check_witnesses(|max_checkpoints| { CompleteTree::::new(max_checkpoints, 0) }); diff --git a/shardtree/proptest-regressions/lib.txt b/shardtree/proptest-regressions/lib.txt new file mode 100644 index 0000000..4054830 --- /dev/null +++ b/shardtree/proptest-regressions/lib.txt @@ -0,0 +1,18 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 38b4ca3c029291dfe2a6b5907c33a2e8ae7900f19c759c5db74b616ab19f6c5c # shrinks to ops = [Append(SipHashable(0), MC), Rewind, Rewind] +cc f1a96f73b9f3ba2a2e4b5271037322150450c573b6c3a5ef34f71a540ff0fad2 # shrinks to ops = [Append("a", C), Rewind, Rewind] +cc ad5f5d4276adea6e928376c6dc8c013745e60a8b507e6fa4e716f6c35477fe65 # shrinks to ops = [Append("a", E), Append("a", E), Append("a", E), Append("a", E), Append("a", E), Append("a", E), Append("a", E), Append("a", E), Append("a", E)] +cc ab2690ff3cf593d2e7a78b2f56d76699e525391b87852bbf15315a1f36742f48 # shrinks to ops = [Append(SipHashable(0), C), Append(SipHashable(0), E), Append(SipHashable(0), E), Unmark(Position(0))] +cc 1603359084b0c614a2ff9008036a5e10db9f32fb31b3333e59aaa517686e174d # shrinks to ops = [Append(SipHashable(0), Checkpoint { id: (), is_marked: false })] +cc faaf929be4b27e652712b705e297bfe15c53767102516e56882d177ac6fc58d9 # shrinks to ops = [CurrentPosition, Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Checkpoint { id: (), is_marked: true }), Append("a", Marked), Checkpoint(()), Append("a", Checkpoint { id: (), is_marked: false }), Append("a", Checkpoint { id: (), is_marked: false }), Append("a", Checkpoint { id: (), is_marked: false }), Witness(Position(3), 5)] +cc 8836c27ead7afb8d10092bdaeed33eb31007eaa47e3c3fca248cc00fbce772a3 # shrinks to ops = [Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral)] +cc 2303f82f255c60d7bdd45e2d13ff526bdc1d7f5ce846a7c07b38ab7f255c0300 # shrinks to ops = [Append("a", Marked), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral)] +cc cf1a33ef6df58bbf7cc199b8b1879c3a078e7784aa3edc0aba9ca03772bea5f2 # shrinks to ops = [Append(SipHashable(0), Checkpoint { id: (), is_marked: false }), Append(SipHashable(0), Checkpoint { id: (), is_marked: false }), Append(SipHashable(0), Checkpoint { id: (), is_marked: false }), Checkpoint(()), Append(SipHashable(0), Checkpoint { id: (), is_marked: false }), Checkpoint(()), Rewind, Rewind, Rewind, Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Checkpoint { id: (), is_marked: false }), Append(SipHashable(0), Checkpoint { id: (), is_marked: false }), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Ephemeral), Append(SipHashable(0), Checkpoint { id: (), is_marked: true }), Witness(Position(8), 2)] +cc 544e027d994eaf7f97b1c8d9ee7b35522a64a610b1430d56d74ec947018b759d # shrinks to ops = [Append("a", Marked), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Append("a", Ephemeral), Checkpoint(()), Append("a", Marked), Checkpoint(()), Checkpoint(()), Append("a", Checkpoint { id: (), is_marked: false }), Rewind, Rewind, Witness(Position(7), 2)] +cc 55d00b68a0f0a02f83ab53f18a29d16d0233153b69a01414a1622104e0eead31 # shrinks to ops = [Append("a", Marked), Append("a", Checkpoint { id: (), is_marked: false }), Append("a", Marked), Checkpoint(()), Checkpoint(()), Checkpoint(()), Append("a", Checkpoint { id: (), is_marked: false }), Append("a", Checkpoint { id: (), is_marked: false }), Witness(Position(0), 7)] +cc 9dd966ff1ab66965c5b84153ae13f684258560cdd5e84c7deb24f724cb12aba7 # shrinks to ops = [Append("a", Marked), Append("a", Ephemeral), Append("a", Checkpoint { id: (), is_marked: true }), Checkpoint(()), Append("a", Checkpoint { id: (), is_marked: false }), Rewind, Rewind, Append("a", Checkpoint { id: (), is_marked: false }), Append("a", Checkpoint { id: (), is_marked: false }), Checkpoint(()), Witness(Position(2), 4)] diff --git a/shardtree/src/lib.rs b/shardtree/src/lib.rs index 6840151..b42f91e 100644 --- a/shardtree/src/lib.rs +++ b/shardtree/src/lib.rs @@ -1951,6 +1951,61 @@ impl< Ok(()) } + /// Truncates the tree, discarding all information after the checkpoint at the specified depth. + /// + /// This will also discard all checkpoints with depth <= the specified depth. Returns `true` + /// if the truncation succeeds or has no effect, or `false` if no checkpoint exists at the + /// specified depth. + pub fn truncate_removing_checkpoint(&mut self, checkpoint_depth: usize) -> bool { + if checkpoint_depth == 0 { + true + } else if self.checkpoints.len() > 1 { + match self.checkpoint_at_depth(checkpoint_depth) { + Some((checkpoint_id, c)) => { + let checkpoint_id = checkpoint_id.clone(); + match c.tree_state { + TreeState::Empty => { + if (self + .store + .truncate(Address::from_parts(Self::subtree_level(), 0))) + .is_err() + { + return false; + } + self.checkpoints.split_off(&checkpoint_id); + true + } + TreeState::AtPosition(position) => { + let subtree_addr = + Address::above_position(Self::subtree_level(), position); + let replacement = self + .store + .get_shard(subtree_addr) + .and_then(|s| s.truncate_to_position(position)); + match replacement { + Some(truncated) => { + if self.store.truncate(subtree_addr).is_err() + || self.store.put_shard(truncated).is_err() + { + false + } else { + self.checkpoints.split_off(&checkpoint_id); + true + } + } + None => false, + } + } + } + } + None => false, + } + } else { + // do not remove the first checkpoint. + false + } + } + /// Computes the root of any subtree of this tree rooted at the given address, with the overall /// tree truncated to the specified position. /// @@ -2109,6 +2164,68 @@ impl< |pos| self.root(Self::root_addr(), pos + 1), ) } + + /// Computes the witness for the leaf at the specified position. + /// + /// Returns the witness as of the most recently appended leaf if `checkpoint_depth == 0`. Note + /// that if the most recently appended leaf is also a checkpoint, this will return the same + /// result as `checkpoint_depth == 1`. + pub fn witness( + &self, + position: Position, + checkpoint_depth: usize, + ) -> Result, QueryError> { + let max_leaf_position = self + .max_leaf_position(checkpoint_depth) + .and_then(|v| v.ok_or_else(|| QueryError::TreeIncomplete(vec![Self::root_addr()])))?; + + if position > max_leaf_position { + Err(QueryError::NotContained(Address::from_parts( + Level::from(0), + position.into(), + ))) + } else { + let subtree_addr = Address::above_position(Self::subtree_level(), position); + + // compute the witness for the specified position up to the subtree root + let mut witness = self.store.get_shard(subtree_addr).map_or_else( + || Err(QueryError::TreeIncomplete(vec![subtree_addr])), + |subtree| subtree.witness(position, max_leaf_position + 1), + )?; + + // compute the remaining parts of the witness up to the root + let root_addr = Self::root_addr(); + let mut cur_addr = subtree_addr; + while cur_addr != root_addr { + witness.push(self.root(cur_addr.sibling(), max_leaf_position + 1)?); + cur_addr = cur_addr.parent(); + } + + Ok(witness) + } + } + + /// Make a marked leaf at a position eligible to be pruned. + /// + /// If the checkpoint associated with the specified identifier does not exist because the + /// corresponding checkpoint would have been more than `max_checkpoints` deep, the removal + /// is recorded as of the first existing checkpoint and the associated leaves will be pruned + /// when that checkpoint is subsequently removed. + pub fn remove_mark(&mut self, position: Position, as_of_checkpoint: &C) -> bool { + if self.get_marked_leaf(position).is_some() { + if let Some(checkpoint) = self.checkpoints.get_mut(as_of_checkpoint) { + checkpoint.marks_removed.insert(position); + return true; + } + + if let Some((_, checkpoint)) = self.checkpoints.iter_mut().next() { + checkpoint.marks_removed.insert(position); + return true; + } + } + + false + } } // We need an applicative functor for Result for this function so that we can correctly @@ -2174,16 +2291,20 @@ pub mod testing { #[cfg(test)] mod tests { use crate::{ - LocatedPrunableTree, LocatedTree, Node, PrunableTree, QueryError, ShardStore, ShardTree, - Tree, EPHEMERAL, MARKED, + IncompleteAt, LocatedPrunableTree, LocatedTree, Node, PrunableTree, QueryError, ShardStore, + ShardTree, Tree, EPHEMERAL, MARKED, }; + use assert_matches::assert_matches; use core::convert::Infallible; use incrementalmerkletree::{ testing::{ - self, check_append, check_root_hashes, complete_tree::CompleteTree, CombinedTree, + self, arb_operation, check_append, check_checkpoint_rewind, check_operations, + check_rewind_remove_mark, check_root_hashes, check_witnesses, + complete_tree::CompleteTree, CombinedTree, SipHashable, }, Address, Hashable, Level, Position, Retention, }; + use proptest::prelude::*; use std::collections::BTreeSet; use std::rc::Rc; @@ -2495,6 +2616,97 @@ mod tests { assert_eq!(complete.subtree.right_filled_root(), Ok("abcd".to_string())); } + #[test] + fn shardtree_insertion() { + let mut tree: ShardTree, 4, 3> = + ShardTree::new(vec![], 100, 0); + assert_matches!( + tree.batch_insert( + Position::from(1), + vec![ + ("b".to_string(), Retention::Checkpoint { id: 1, is_marked: false }), + ("c".to_string(), Retention::Ephemeral), + ("d".to_string(), Retention::Marked), + ].into_iter() + ), + Ok(Some((pos, incomplete))) if + pos == Position::from(3) && + incomplete == vec![ + IncompleteAt { + address: Address::from_parts(Level::from(0), 0), + required_for_witness: true + } + ] + ); + + assert_matches!( + tree.root_at_checkpoint(1), + Err(QueryError::TreeIncomplete(v)) if v == vec![Address::from_parts(Level::from(0), 0)] + ); + + assert_matches!( + tree.batch_insert( + Position::from(0), + vec![ + ("a".to_string(), Retention::Ephemeral), + ].into_iter() + ), + Ok(Some((pos, incomplete))) if + pos == Position::from(0) && + incomplete == vec![] + ); + + assert_matches!( + tree.root_at_checkpoint(0), + Ok(h) if h == *"abcd____________" + ); + + assert_matches!( + tree.root_at_checkpoint(1), + Ok(h) if h == *"ab______________" + ); + + assert_matches!( + tree.batch_insert( + Position::from(10), + vec![ + ("k".to_string(), Retention::Ephemeral), + ("l".to_string(), Retention::Checkpoint { id: 2, is_marked: false }), + ("m".to_string(), Retention::Ephemeral), + ].into_iter() + ), + Ok(Some((pos, incomplete))) if + pos == Position::from(12) && + incomplete == vec![ + IncompleteAt { + address: Address::from_parts(Level::from(1), 4), + required_for_witness: false + }, + IncompleteAt { + address: Address::from_parts(Level::from(0), 13), + required_for_witness: false + }, + IncompleteAt { + address: Address::from_parts(Level::from(1), 7), + required_for_witness: false + }, + ] + ); + + assert_matches!( + tree.root_at_checkpoint(0), + // The (0, 13) and (1, 7) incomplete subtrees are + // not considered incomplete here because they appear + // at the tip of the tree. + Err(QueryError::TreeIncomplete(xs)) if xs == vec![ + Address::from_parts(Level::from(2), 1), + Address::from_parts(Level::from(1), 4), + ] + ); + + assert!(tree.truncate_removing_checkpoint(1)); + } + impl< H: Hashable + Ord + Clone, C: Clone + Ord + core::fmt::Debug, @@ -2527,12 +2739,16 @@ mod tests { ShardTree::root_at_checkpoint(self, checkpoint_depth).ok() } - fn witness(&self, _position: Position, _checkpoint_depth: usize) -> Option> { - todo!() + fn witness(&self, position: Position, checkpoint_depth: usize) -> Option> { + ShardTree::witness(self, position, checkpoint_depth).ok() } - fn remove_mark(&mut self, _position: Position) -> bool { - todo!() + fn remove_mark(&mut self, position: Position) -> bool { + if let Some(c) = self.checkpoints.iter().rev().map(|(c, _)| c.clone()).next() { + ShardTree::remove_mark(self, position, &c) + } else { + false + } } fn checkpoint(&mut self, checkpoint_id: C) -> bool { @@ -2540,7 +2756,7 @@ mod tests { } fn rewind(&mut self) -> bool { - todo!() + ShardTree::truncate_removing_checkpoint(self, 1) } } @@ -2557,6 +2773,28 @@ mod tests { ShardTree::, 4, 3>::new(vec![], m, 0) }); } + + #[test] + fn witnesses() { + check_witnesses(|m| { + ShardTree::, 4, 3>::new(vec![], m, 0) + }); + } + + #[test] + fn checkpoint_rewind() { + check_checkpoint_rewind(|m| { + ShardTree::, 4, 3>::new(vec![], m, 0) + }); + } + + #[test] + fn rewind_remove_mark() { + check_rewind_remove_mark(|m| { + ShardTree::, 4, 3>::new(vec![], m, 0) + }); + } + // Combined tree tests #[allow(clippy::type_complexity)] fn new_combined_tree( @@ -2577,4 +2815,37 @@ mod tests { fn combined_append() { check_append(new_combined_tree); } + + #[test] + fn combined_rewind_remove_mark() { + check_rewind_remove_mark(new_combined_tree); + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100000))] + + #[test] + fn check_randomized_u64_ops( + ops in proptest::collection::vec( + arb_operation((0..32u64).prop_map(SipHashable), 0usize..100), + 1..100 + ) + ) { + let tree = new_combined_tree(100); + let indexed_ops = ops.iter().enumerate().map(|(i, op)| op.map_checkpoint_id(|_| i)).collect::>(); + check_operations(tree, &indexed_ops)?; + } + + #[test] + fn check_randomized_str_ops( + ops in proptest::collection::vec( + arb_operation((97u8..123).prop_map(|c| char::from(c).to_string()), 0usize..100), + 1..100 + ) + ) { + let tree = new_combined_tree(100); + let indexed_ops = ops.iter().enumerate().map(|(i, op)| op.map_checkpoint_id(|_| i)).collect::>(); + check_operations(tree, &indexed_ops)?; + } + } }