diff --git a/Cargo.toml b/Cargo.toml index ce8070c..fd76a54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,5 @@ -[package] -name = "bridgetree" -version = "0.2.0" -authors = [ - "Kris Nuttycombe ", - "Sean Bowe ", +[workspace] +members = [ + "incrementalmerkletree", + "bridgetree", ] -edition = "2018" -license = "MIT OR Apache-2.0" -description = "A space-efficient Merkle tree with witnessing of marked leaves, checkpointing & state restoration." -homepage = "https://github.com/zcash/bridgetree" -repository = "https://github.com/zcash/bridgetree" -categories = ["algorithms", "data-structures"] - -[dependencies] -serde = { version = "1", features = ["derive"] } -proptest = { version = "1.0.0", optional = true } - -[dev-dependencies] -proptest = "1.0.0" - -[features] -test-dependencies = ["proptest"] diff --git a/CHANGELOG-bridgetree.md b/bridgetree/CHANGELOG.md similarity index 94% rename from CHANGELOG-bridgetree.md rename to bridgetree/CHANGELOG.md index e30cf7f..8b12559 100644 --- a/CHANGELOG-bridgetree.md +++ b/bridgetree/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to Rust's notion of ## [Unreleased] +### Removed + +- The `testing` module has been removed in favor of depending on + `incrementalmerkletree::testing`. + ## [bridgetree-v0.2.0] - 2022-05-10 The `bridgetree` crate is a fork of `incrementalmerkletree`, with the contents @@ -42,6 +47,8 @@ referred to by their new location. - `BridgeTree::authentication_path` has been renamed to `BridgeTree::witness` - `BridgeTree::witnessed` has been renamed to `BridgeTree::marked` - `BridgeTree::witnessed_indices` has been renamed to `BridgeTree::marked_indices` +- `BridgeTree::append` and `NonEmptyFrontier::append` now take ownership of the + value being appended instead of the value being passed by reference. The following types have been moved from the `bridgetree` module of `incrementalmerkletree` to the crate root: @@ -84,7 +91,7 @@ The `Tree` interface reflects the renaming of `witness` to `mark` described abov ### Removed relative to `incrementalmerkletree-0.3.0` -- `bridgetree::Leaf` +- `bridgetree::Leaf` - `bridgetree::AuthFragment` - `NonEmptyFrontier::size` - `NonEmptyFrontier::max_altitude` diff --git a/bridgetree/COPYING.md b/bridgetree/COPYING.md new file mode 100644 index 0000000..4f1b0b7 --- /dev/null +++ b/bridgetree/COPYING.md @@ -0,0 +1,16 @@ +# License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +# Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. + diff --git a/bridgetree/Cargo.toml b/bridgetree/Cargo.toml new file mode 100644 index 0000000..c6b2262 --- /dev/null +++ b/bridgetree/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bridgetree" +version = "0.2.0" +authors = [ + "Kris Nuttycombe ", + "Sean Bowe ", +] +edition = "2018" +license = "MIT OR Apache-2.0" +description = "A space-efficient Merkle tree designed for linear appends with witnessing of marked leaves, checkpointing & state restoration." +homepage = "https://github.com/zcash/incrementalmerkletree" +repository = "https://github.com/zcash/incrementalmerkletree" +categories = ["algorithms", "data-structures"] + +[dependencies] +incrementalmerkletree = { version = "0.3", path = "../incrementalmerkletree" } +serde = { version = "1", features = ["derive"] } +proptest = { version = "1.0.0", optional = true } + +[dev-dependencies] +incrementalmerkletree = { version = "0.3", path = "../incrementalmerkletree", features = ["test-dependencies"] } +proptest = "1.0.0" + +[features] +test-dependencies = ["proptest"] diff --git a/bridgetree/LICENSE-APACHE b/bridgetree/LICENSE-APACHE new file mode 100644 index 0000000..1e5006d --- /dev/null +++ b/bridgetree/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/bridgetree/LICENSE-MIT b/bridgetree/LICENSE-MIT new file mode 100644 index 0000000..94ac1a7 --- /dev/null +++ b/bridgetree/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2021 The Electric Coin Company + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/bridgetree/README.md similarity index 100% rename from README.md rename to bridgetree/README.md diff --git a/proptest-regressions/bridgetree.txt b/bridgetree/proptest-regressions/bridgetree.txt similarity index 100% rename from proptest-regressions/bridgetree.txt rename to bridgetree/proptest-regressions/bridgetree.txt diff --git a/src/lib.rs b/bridgetree/src/lib.rs similarity index 90% rename from src/lib.rs rename to bridgetree/src/lib.rs index 8da840b..7d898b7 100644 --- a/src/lib.rs +++ b/bridgetree/src/lib.rs @@ -30,12 +30,6 @@ //! reset the state to. //! //! In this module, the term "ommer" is used as for the sibling of a parent node in a binary tree. -mod hashing; -mod position; - -#[cfg(any(bench, test, feature = "test-dependencies"))] -pub mod testing; - use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; use std::convert::TryFrom; @@ -43,11 +37,7 @@ use std::fmt::Debug; use std::mem::size_of; use std::ops::Range; -use crate::position::Source; -pub use crate::{ - hashing::Hashable, - position::{Address, Level, Position}, -}; +pub use incrementalmerkletree::{Address, Hashable, Level, Position}; /// Validation errors that can occur during reconstruction of a Merkle frontier from /// its constituent parts. @@ -84,6 +74,59 @@ pub enum WitnessingError { BridgeAddressInvalid(Address), } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum Source { + /// The sibling to the address can be derived from the incremental frontier + /// at the contained ommer index + Past(usize), + /// The sibling to the address must be obtained from values discovered by + /// the addition of more nodes to the tree + Future, +} + +#[must_use = "iterators are lazy and do nothing unless consumed"] +struct WitnessAddrsIter { + root_level: Level, + current: Address, + ommer_count: usize, +} + +/// Returns an iterator over the addresses of nodes required to create a witness for this +/// position, beginning with the sibling of the leaf at this position and ending with the +/// sibling of the ancestor of the leaf at this position that is required to compute a root at +/// the specified level. +fn witness_addrs(position: Position, root_level: Level) -> impl Iterator { + WitnessAddrsIter { + root_level, + current: Address::from(position), + ommer_count: 0, + } +} + +impl Iterator for WitnessAddrsIter { + type Item = (Address, Source); + + fn next(&mut self) -> Option<(Address, Source)> { + if self.current.level() < self.root_level { + let current = self.current; + let source = if current.is_complete_node() { + Source::Past(self.ommer_count) + } else { + Source::Future + }; + + self.current = current.parent(); + if matches!(source, Source::Past(_)) { + self.ommer_count += 1; + } + + Some((current.sibling(), source)) + } else { + None + } + } +} + /// A [`NonEmptyFrontier`] is a reduced representation of a Merkle tree, containing a single leaf /// value, along with the vector of hashes produced by the reduction of previously appended leaf /// values that will be required when producing a witness for the current leaf. @@ -154,7 +197,7 @@ impl NonEmptyFrontier { let mut carry = Some((prior_leaf, 0.into())); let mut new_ommers = Vec::with_capacity(self.position.past_ommer_count()); - for (addr, source) in prior_position.witness_addrs(new_root_level) { + for (addr, source) in witness_addrs(prior_position, new_root_level) { if let Source::Past(i) = source { if let Some((carry_ommer, carry_lvl)) = carry.as_ref() { if *carry_lvl == addr.level() { @@ -188,8 +231,7 @@ impl NonEmptyFrontier { /// Generate the root of the Merkle tree by hashing against empty subtree roots. pub fn root(&self, root_level: Option) -> H { let max_level = root_level.unwrap_or_else(|| self.position.root_level()); - self.position - .witness_addrs(max_level) + witness_addrs(self.position, max_level) .fold( (self.leaf.clone(), Level::from(0)), |(digest, complete_lvl), (addr, source)| { @@ -220,8 +262,7 @@ impl NonEmptyFrontier { { // construct a complete trailing edge that includes the data from // the following frontier not yet included in the trailing edge. - self.position() - .witness_addrs(depth.into()) + witness_addrs(self.position(), depth.into()) .map(|(addr, source)| match source { Source::Past(i) => Ok(self.ommers[i].clone()), Source::Future => { @@ -285,16 +326,16 @@ impl Frontier { /// Appends a new value to the frontier at the next available slot. /// Returns true if successful and false if the frontier would exceed /// the maximum allowed depth. - pub fn append(&mut self, value: &H) -> bool { + pub fn append(&mut self, value: H) -> bool { if let Some(frontier) = self.frontier.as_mut() { if frontier.position().is_complete_subtree(DEPTH.into()) { false } else { - frontier.append(value.clone()); + frontier.append(value); true } } else { - self.frontier = Some(NonEmptyFrontier::new(value.clone())); + self.frontier = Some(NonEmptyFrontier::new(value)); true } } @@ -876,7 +917,7 @@ impl BridgeTree { /// Appends a new value to the tree at the next available slot. /// Returns true if successful and false if the tree would exceed /// the maximum allowed depth. - pub fn append(&mut self, value: &H) -> bool { + pub fn append(&mut self, value: H) -> bool { if let Some(bridge) = self.current_bridge.as_mut() { if bridge .frontier @@ -885,11 +926,11 @@ impl BridgeTree { { false } else { - bridge.append(value.clone()); + bridge.append(value); true } } else { - self.current_bridge = Some(MerkleBridge::new(value.clone())); + self.current_bridge = Some(MerkleBridge::new(value)); true } } @@ -1204,10 +1245,8 @@ impl BridgeTree { // Add the elements of the auth path to the set of addresses we should // continue to track and retain information for - for (addr, source) in cur_bridge - .frontier - .position() - .witness_addrs(Level::from(DEPTH)) + for (addr, source) in + witness_addrs(cur_bridge.frontier.position(), Level::from(DEPTH)) { if source == Source::Future { ommer_addrs.insert(addr); @@ -1255,12 +1294,21 @@ impl BridgeTree { #[cfg(test)] mod tests { use proptest::prelude::*; + use std::fmt::Debug; use super::*; - use crate::testing::{apply_operation, arb_operation, tests, Frontier, Tree}; + use incrementalmerkletree::{ + testing::{ + apply_operation, arb_operation, check_checkpoint_rewind, check_operations, + check_rewind_remove_mark, check_rewind_remove_mark_consistency, check_root_hashes, + check_witnesses, complete_tree::CompleteTree, CombinedTree, Frontier, SipHashable, + Tree, + }, + Hashable, + }; impl Frontier for super::Frontier { - fn append(&mut self, value: &H) -> bool { + fn append(&mut self, value: H) -> bool { super::Frontier::append(self, value) } @@ -1270,7 +1318,7 @@ mod tests { } impl Tree for BridgeTree { - fn append(&mut self, value: &H) -> bool { + fn append(&mut self, value: H) -> bool { BridgeTree::append(self, value) } @@ -1315,6 +1363,46 @@ mod tests { } } + #[test] + fn position_witness_addrs() { + use Source::*; + let path_elem = |l, i, s| (Address::from_parts(Level::from(l), i), s); + assert_eq!( + vec![path_elem(0, 1, Future), path_elem(1, 1, Future)], + witness_addrs(Position::from(0), Level::from(2)).collect::>() + ); + assert_eq!( + vec![path_elem(0, 3, Future), path_elem(1, 0, Past(0))], + witness_addrs(Position::from(2), Level::from(2)).collect::>() + ); + assert_eq!( + vec![ + path_elem(0, 2, Past(0)), + path_elem(1, 0, Past(1)), + path_elem(2, 1, Future) + ], + witness_addrs(Position::from(3), Level::from(3)).collect::>() + ); + assert_eq!( + vec![ + path_elem(0, 5, Future), + path_elem(1, 3, Future), + path_elem(2, 0, Past(0)), + path_elem(3, 1, Future) + ], + witness_addrs(Position::from(4), Level::from(4)).collect::>() + ); + assert_eq!( + vec![ + path_elem(0, 7, Future), + path_elem(1, 2, Past(0)), + path_elem(2, 0, Past(1)), + path_elem(3, 1, Future) + ], + witness_addrs(Position::from(6), Level::from(4)).collect::>() + ); + } + #[test] fn nonempty_frontier_root() { let mut frontier = NonEmptyFrontier::new("a".to_string()); @@ -1340,13 +1428,13 @@ mod tests { assert_eq!(frontier.root().len(), 16); assert_eq!(frontier.root(), "________________"); - frontier.append(&"a".to_string()); + frontier.append("a".to_string()); assert_eq!(frontier.root(), "a_______________"); - frontier.append(&"b".to_string()); + frontier.append("b".to_string()); assert_eq!(frontier.root(), "ab______________"); - frontier.append(&"c".to_string()); + frontier.append("c".to_string()); assert_eq!(frontier.root(), "abc_____________"); } @@ -1374,9 +1462,9 @@ mod tests { fn tree_depth() { let mut tree = BridgeTree::::new(100); for c in 'a'..'i' { - assert!(tree.append(&c.to_string())) + assert!(tree.append(c.to_string())) } - assert!(!tree.append(&'i'.to_string())); + assert!(!tree.append('i'.to_string())); } fn arb_bridgetree( @@ -1441,10 +1529,10 @@ mod tests { fn drop_oldest_checkpoint() { let mut t = BridgeTree::::new(100); t.checkpoint(); - t.append(&"a".to_string()); + t.append("a".to_string()); t.mark(); - t.append(&"b".to_string()); - t.append(&"c".to_string()); + t.append("b".to_string()); + t.append("c".to_string()); assert!( t.drop_oldest_checkpoint(), "Checkpoint drop is expected to succeed" @@ -1454,22 +1542,22 @@ mod tests { #[test] fn root_hashes() { - tests::check_root_hashes(BridgeTree::::new); + check_root_hashes(BridgeTree::::new); } #[test] fn witnesss() { - tests::check_witnesss(BridgeTree::::new); + check_witnesses(BridgeTree::::new); } #[test] fn checkpoint_rewind() { - tests::check_checkpoint_rewind(BridgeTree::::new); + check_checkpoint_rewind(BridgeTree::::new); } #[test] fn rewind_remove_mark() { - tests::check_rewind_remove_mark(BridgeTree::::new); + check_rewind_remove_mark(BridgeTree::::new); } #[test] @@ -1479,7 +1567,7 @@ mod tests { let mut has_witness = vec![]; for i in 0usize..100 { let elem: String = format!("{},", i); - assert!(t.append(&elem), "Append should succeed."); + assert!(t.append(elem), "Append should succeed."); if i % 5 == 0 { t.checkpoint(); } @@ -1522,7 +1610,7 @@ mod tests { fn garbage_collect_idx() { let mut tree: BridgeTree = BridgeTree::new(100); let empty_root = tree.root(0); - tree.append(&"a".to_string()); + tree.append("a".to_string()); for _ in 0..100 { tree.checkpoint(); } @@ -1531,4 +1619,50 @@ mod tests { tree.rewind(); assert!(tree.root(0) != empty_root); } + + // Combined tree tests + fn new_combined_tree( + max_checkpoints: usize, + ) -> CombinedTree, BridgeTree> { + CombinedTree::new( + CompleteTree::new(4, max_checkpoints), + BridgeTree::::new(max_checkpoints), + ) + } + + #[test] + fn test_rewind_remove_mark() { + check_rewind_remove_mark(new_combined_tree); + } + + #[test] + fn test_rewind_remove_mark_consistency() { + check_rewind_remove_mark_consistency(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); + check_operations(tree, 4, &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); + check_operations(tree, 4, &ops)?; + } + } } diff --git a/CHANGELOG.md b/incrementalmerkletree/CHANGELOG.md similarity index 79% rename from CHANGELOG.md rename to incrementalmerkletree/CHANGELOG.md index 5b8a0d9..abfc85f 100644 --- a/CHANGELOG.md +++ b/incrementalmerkletree/CHANGELOG.md @@ -7,9 +7,46 @@ and this project adheres to Rust's notion of ## [Unreleased] +### Added + +- `Position::is_odd` +- `Position::ommer_index` +- `Position::root_level` +- `Position::past_ommer_count` +- `Address` A type used to uniquely identify node locations within a binary tree. + +A `test-dependencies` feature has been added. This makes available a `testing` +module to users of this crate, which contains `proptest` generators for types +from this crate as well as a number of tools for comparison testing between +`Tree` implementations and a number of generalized example tests. The +`Frontier` and `Tree` traits have been moved to the `testing` module, as there +is not another good use case for polymorphism over tree implementations. + +### Changed + +- `Altitude` has been renamed to `Level` +- `Position::is_complete` has been renamed to `Position::is_complete_subtree`. +- `witness` is now used as the name of the operation to construct the witness for a leaf. + We now use `mark` to refer to the process of marking a node for which we may later wish + to construct a witness. + - `Tree::witness` has been renamed to `Tree::mark` + - `Tree::witnessed_positions` has been renamed to `Tree::marked_positions` + - `Tree::get_witnessed_leaf` has been renamed to `Tree::get_marked_leaf` + - `Tree::remove_witness` has been renamed to `Tree::remove_mark` + - `Tree::authentication_path` has been renamed to `Tree::witness` +- `Tree::append` now takes ownership of the value being appended instead of a value passed + by reference. + ### Removed - The `bridgetree` module has been moved out a to a separate `bridgetree` crate. +- `Position::witness_addrs` +- `Position::altitudes_required` +- `Position::all_altitudes_required` +- `Position::auth_path` +- `Position::max_altitude` +- `Position::ommer_altitudes` +- `impl Sub for Altitude` ## [0.3.0] - 2022-05-10 @@ -27,7 +64,7 @@ and this project adheres to Rust's notion of addition to the previous behavior which only allowed computation of the path as of the most recent tree state. The provided `as_of_root` value must be equal to either the current root of the tree, or to the root of the tree at a previous checkpoint that - contained a note at the given position. + contained a note at the given position. ### Removed @@ -43,7 +80,7 @@ and this project adheres to Rust's notion of witnessed leaves by their position in the tree. - `Tree::witnessed_positions`, to allow a user to query for all positions that have been witnessed. - - `Tree::garbage_collect`, to prune checkpoint and removed witness information + - `Tree::garbage_collect`, to prune checkpoint and removed witness information that is no longer reachable by rewinds of the tree. - `incrementalmerkletree::bridgetree`: - `NonEmptyFrontier::current_leaf` @@ -61,11 +98,11 @@ and this project adheres to Rust's notion of - `Tree::current_leaf` and `Tree::witness` have both been changed to only return the leaf value, instead of both the leaf value and the position. - `incrementalmerkletree::bridgetree`: - - The type of `BridgeTree::saved` and `Checkpoint::forgotten` have been changed from + - The type of `BridgeTree::saved` and `Checkpoint::forgotten` have been changed from `BTreeMap<(Position, H), usize>` to `BTreeMap`. This change is also reflected in the rturn type of the `BridgeTree::witnessed_indices` method. - The `Checkpoint` type is no longer parameterized by `H`. - - `BridgeTree::bridges` has been split into two parts: + - `BridgeTree::bridges` has been split into two parts: - `BridgeTree::prior_bridges` now tracks past bridges not including the current frontier. - `BridgeTree::current_bridge` now tracks current mutable frontier. - The signature of `BridgeTree::from_parts` has been modified to reflect these changes. @@ -83,8 +120,8 @@ and this project adheres to Rust's notion of ### Fixed -- A bug in `BridgeTree::garbage_collect` that caused garbage collection to in some - cases incorrectly rewrite checkpointed bridge lengths, resulting in a condition +- A bug in `BridgeTree::garbage_collect` that caused garbage collection to in some + cases incorrectly rewrite checkpointed bridge lengths, resulting in a condition where a rewind could panic after a GC operation. ## [0.3.0-beta.1] - 2022-03-22 @@ -133,7 +170,7 @@ and this project adheres to Rust's notion of or `None` if the tree is empty, instead of a boolean value. - `incrementalmerkletree::bridgetree`: - Most `MerkleBridge` methods now require `H: Ord`. - - Changed the return type of `MerkleBridge::auth_fragments` from + - Changed the return type of `MerkleBridge::auth_fragments` from `HashMap` to `BTreeMap`; the `saved` argument to `BridgeTree::from_parts` is similarly altered. - `MerkleBridge::successor` now takes a boolean argument for tracking the most @@ -157,7 +194,7 @@ and this project adheres to Rust's notion of ## [0.2.0] - 2022-03-22 v0.2.0 is essentially a complete rewrite relative to v0.1.0, and should be considered -the first usable release. +the first usable release. ## [0.1.0] - 2021-06-23 Initial release! diff --git a/incrementalmerkletree/Cargo.toml b/incrementalmerkletree/Cargo.toml new file mode 100644 index 0000000..31c2161 --- /dev/null +++ b/incrementalmerkletree/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "incrementalmerkletree" +version = "0.3.0" +authors = [ + "Sean Bowe ", + "Kris Nuttycombe ", +] +edition = "2018" +license = "MIT OR Apache-2.0" +description = "Common types, interfaces, and utilities for Merkle tree data structures" +homepage = "https://github.com/zcash/incrementalmerkletree" +repository = "https://github.com/zcash/incrementalmerkletree" +categories = ["algorithms", "data-structures"] + +[dependencies] +either = "1.8" +serde = { version = "1", features = ["derive"] } +proptest = { version = "1.0.0", optional = true } + +[dev-dependencies] +proptest = "1.0.0" + +[features] +test-dependencies = ["proptest"] diff --git a/incrementalmerkletree/LICENSE-APACHE b/incrementalmerkletree/LICENSE-APACHE new file mode 100644 index 0000000..1e5006d --- /dev/null +++ b/incrementalmerkletree/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/incrementalmerkletree/LICENSE-MIT b/incrementalmerkletree/LICENSE-MIT new file mode 100644 index 0000000..94ac1a7 --- /dev/null +++ b/incrementalmerkletree/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2021 The Electric Coin Company + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/incrementalmerkletree/README.md b/incrementalmerkletree/README.md new file mode 100644 index 0000000..34851e1 --- /dev/null +++ b/incrementalmerkletree/README.md @@ -0,0 +1,6 @@ +incrementalmerkletree +===================== + +This crate contains common utility types and testing infrastructure used in the implementation +of the `bridgetree` and `shardtree` crates. + diff --git a/src/position.rs b/incrementalmerkletree/src/lib.rs similarity index 72% rename from src/position.rs rename to incrementalmerkletree/src/lib.rs index 9288c15..32957aa 100644 --- a/src/position.rs +++ b/incrementalmerkletree/src/lib.rs @@ -1,10 +1,84 @@ -//! Types that describe positions within a Merkle tree +//! Common types and utilities used in incremental Merkle tree implementations. use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use std::num::TryFromIntError; use std::ops::{Add, AddAssign, Range}; +#[cfg(feature = "test-dependencies")] +pub mod testing; + +/// A type representing the position of a leaf in a Merkle tree. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[repr(transparent)] +pub struct Position(usize); + +impl Position { + /// Return whether the position is odd-valued. + pub fn is_odd(&self) -> bool { + self.0 & 0x1 == 1 + } + + /// Returns the minimum possible level of the root of a binary tree containing at least + /// `self + 1` nodes. + pub fn root_level(&self) -> Level { + Level(64 - self.0.leading_zeros() as u8) + } + + /// Returns the number of cousins and/or ommers required to construct an authentication + /// path to the root of a merkle tree that has `self + 1` nodes. + pub fn past_ommer_count(&self) -> usize { + (0..self.root_level().0) + .filter(|i| (self.0 >> i) & 0x1 == 1) + .count() + } + + /// Returns whether the binary tree having `self` as the position of the rightmost leaf + /// contains a perfect balanced tree with a root at level `root_level` that contains the + /// aforesaid leaf. + pub fn is_complete_subtree(&self, root_level: Level) -> bool { + !(0..(root_level.0)).any(|l| self.0 & (1 << l) == 0) + } +} + +impl From for usize { + fn from(p: Position) -> usize { + p.0 + } +} + +impl From for u64 { + fn from(p: Position) -> Self { + p.0 as u64 + } +} + +impl Add for Position { + type Output = Position; + fn add(self, other: usize) -> Self { + Position(self.0 + other) + } +} + +impl AddAssign for Position { + fn add_assign(&mut self, other: usize) { + self.0 += other + } +} + +impl From for Position { + fn from(sz: usize) -> Self { + Self(sz) + } +} + +impl TryFrom for Position { + type Error = TryFromIntError; + fn try_from(sz: u64) -> Result { + ::try_from(sz).map(Self) + } +} + /// A type-safe wrapper for indexing into "levels" of a binary tree, such that /// nodes at level `0` are leaves, nodes at level `1` are parents of nodes at /// level `0`, and so forth. This type is capable of representing levels in @@ -46,92 +120,6 @@ impl From for usize { } } -/// A type representing the position of a leaf in a Merkle tree. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[repr(transparent)] -pub struct Position(usize); - -impl Position { - /// Return whether the position is odd-valued. - pub fn is_odd(&self) -> bool { - self.0 & 0x1 == 1 - } - - /// Returns the minimum possible level of the root of a binary tree containing at least - /// `self + 1` nodes. - pub fn root_level(&self) -> Level { - Level(64 - self.0.leading_zeros() as u8) - } - - /// Returns the number of cousins and/or ommers required to construct an authentication - /// path to the root of a merkle tree that has `self + 1` nodes. - pub fn past_ommer_count(&self) -> usize { - (0..self.root_level().0) - .filter(|i| (self.0 >> i) & 0x1 == 1) - .count() - } - - /// Returns whether the binary tree having `self` as the position of the rightmost leaf - /// contains a perfect balanced tree with a root at level `root_level` that contains the - /// aforesaid leaf. - pub fn is_complete_subtree(&self, root_level: Level) -> bool { - !(0..(root_level.0)).any(|l| self.0 & (1 << l) == 0) - } - - /// Returns an iterator over the addresses of nodes required to create a witness for this - /// position, beginning with the sibling of the leaf at this position and ending with the - /// sibling of the ancestor of the leaf at this position that is required to compute a root at - /// the specified level. - pub(crate) fn witness_addrs( - &self, - root_level: Level, - ) -> impl Iterator { - WitnessAddrsIter { - root_level, - current: Address::from(self), - ommer_count: 0, - } - } -} - -impl From for usize { - fn from(p: Position) -> usize { - p.0 - } -} - -impl From for u64 { - fn from(p: Position) -> Self { - p.0 as u64 - } -} - -impl Add for Position { - type Output = Position; - fn add(self, other: usize) -> Self { - Position(self.0 + other) - } -} - -impl AddAssign for Position { - fn add_assign(&mut self, other: usize) { - self.0 += other - } -} - -impl From for Position { - fn from(sz: usize) -> Self { - Self(sz) - } -} - -impl TryFrom for Position { - type Error = TryFromIntError; - fn try_from(sz: u64) -> Result { - ::try_from(sz).map(Self) - } -} - /// The address of an internal node of the Merkle tree. /// When `level == 0`, the index has the same value as the /// position. @@ -141,16 +129,6 @@ pub struct Address { index: usize, } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub(crate) enum Source { - /// The sibling to the address can be derived from the incremental frontier - /// at the contained ommer index - Past(usize), - /// The sibling to the address must be obtained from values discovered by - /// the addition of more nodes to the tree - Future, -} - impl Address { pub fn from_parts(level: Level, index: usize) -> Self { Address { level, index } @@ -261,40 +239,23 @@ impl<'a> From<&'a Address> for Option { } } -#[must_use = "iterators are lazy and do nothing unless consumed"] -pub(crate) struct WitnessAddrsIter { - root_level: Level, - current: Address, - ommer_count: usize, -} +/// A trait describing the operations that make a type suitable for use as +/// a leaf or node value in a merkle tree. +pub trait Hashable: Sized { + fn empty_leaf() -> Self; -impl Iterator for WitnessAddrsIter { - type Item = (Address, Source); + fn combine(level: Level, a: &Self, b: &Self) -> Self; - fn next(&mut self) -> Option<(Address, Source)> { - if self.current.level() < self.root_level { - let current = self.current; - let source = if current.is_complete_node() { - Source::Past(self.ommer_count) - } else { - Source::Future - }; - - self.current = current.parent(); - if matches!(source, Source::Past(_)) { - self.ommer_count += 1; - } - - Some((current.sibling(), source)) - } else { - None - } + fn empty_root(level: Level) -> Self { + Level::from(0) + .iter_to(level) + .fold(Self::empty_leaf(), |v, lvl| Self::combine(lvl, &v, &v)) } } #[cfg(test)] pub(crate) mod tests { - use super::{Address, Level, Position, Source}; + use super::{Address, Level, Position}; #[test] fn position_is_complete_subtree() { @@ -350,44 +311,4 @@ pub(crate) mod tests { assert_eq!(addr(1, 2), addr(0, 4).next_incomplete_parent()); assert_eq!(addr(3, 0), addr(1, 2).next_incomplete_parent()); } - - #[test] - fn position_witness_addrs() { - use Source::*; - let path_elem = |l, i, s| (Address::from_parts(Level(l), i), s); - assert_eq!( - vec![path_elem(0, 1, Future), path_elem(1, 1, Future)], - Position(0).witness_addrs(Level(2)).collect::>() - ); - assert_eq!( - vec![path_elem(0, 3, Future), path_elem(1, 0, Past(0))], - Position(2).witness_addrs(Level(2)).collect::>() - ); - assert_eq!( - vec![ - path_elem(0, 2, Past(0)), - path_elem(1, 0, Past(1)), - path_elem(2, 1, Future) - ], - Position(3).witness_addrs(Level(3)).collect::>() - ); - assert_eq!( - vec![ - path_elem(0, 5, Future), - path_elem(1, 3, Future), - path_elem(2, 0, Past(0)), - path_elem(3, 1, Future) - ], - Position(4).witness_addrs(Level(4)).collect::>() - ); - assert_eq!( - vec![ - path_elem(0, 7, Future), - path_elem(1, 2, Past(0)), - path_elem(2, 0, Past(1)), - path_elem(3, 1, Future) - ], - Position(6).witness_addrs(Level(4)).collect::>() - ); - } } diff --git a/incrementalmerkletree/src/testing.rs b/incrementalmerkletree/src/testing.rs new file mode 100644 index 0000000..81341f0 --- /dev/null +++ b/incrementalmerkletree/src/testing.rs @@ -0,0 +1,1084 @@ +//! Traits and types used to permit comparison testing between tree implementations. + +use core::fmt::Debug; +use core::marker::PhantomData; +use proptest::prelude::*; +use std::collections::BTreeSet; + +use crate::{Hashable, Level, Position}; + +pub mod complete_tree; + +// +// Traits used to permit comparison testing between tree implementations. +// + +/// A possibly-empty incremental Merkle frontier. +pub trait Frontier { + /// Appends a new value to the frontier at the next available slot. + /// Returns true if successful and false if the frontier would exceed + /// the maximum allowed depth. + fn append(&mut self, value: H) -> bool; + + /// Obtains the current root of this Merkle frontier by hashing + /// against empty nodes up to the maximum height of the pruned + /// tree that the frontier represents. + fn root(&self) -> H; +} + +/// A Merkle tree that supports incremental appends, marking of +/// leaf nodes for construction of witnesses, checkpoints and rollbacks. +pub trait Tree { + /// Appends a new value to the tree at the next available slot. + /// Returns true if successful and false if the tree would exceed + /// the maximum allowed depth. + fn append(&mut self, value: H) -> bool; + + /// Returns the most recently appended leaf value. + fn current_position(&self) -> Option; + + /// Returns the most recently appended leaf value. + fn current_leaf(&self) -> Option<&H>; + + /// Returns the leaf at the specified position if the tree can produce + /// a witness for it. + 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; + + /// Return a set of all the positions for which we have marked. + fn marked_positions(&self) -> BTreeSet; + + /// Obtains the root of the Merkle tree at the specified checkpoint depth + /// by hashing against empty nodes up to the maximum height of the tree. + /// Returns `None` if there are not enough checkpoints available to reach the + /// requested checkpoint depth. + fn root(&self, checkpoint_depth: usize) -> Option; + + /// Obtains a witness to the value at the specified position, + /// as of the tree state corresponding to the given root. + /// Returns `None` if there is no available witness to that + /// position or if the root does not correspond to a checkpointed + /// root of the tree. + fn witness(&self, position: Position, as_of_root: &H) -> Option>; + + /// Marks the value at the specified position as a value we're no longer + /// interested in maintaining a mark for. Returns true if successful and + /// false if we were already not maintaining a mark at this position. + fn remove_mark(&mut self, position: Position) -> bool; + + /// Creates a new checkpoint for the current tree state. It is valid to + /// have multiple checkpoints for the same tree state, and each `rewind` + /// call will remove a single checkpoint. + fn checkpoint(&mut self); + + /// Rewinds the tree state to the previous checkpoint, and then removes + /// that checkpoint record. If there are multiple checkpoints at a given + /// tree state, the tree state will not be altered until all checkpoints + /// at that tree state have been removed using `rewind`. This function + /// return false and leave the tree unmodified if no checkpoints exist. + fn rewind(&mut self) -> bool; +} + +// +// Types and utilities for shared example tests. +// + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct SipHashable(pub u64); + +impl Hashable for SipHashable { + fn empty_leaf() -> Self { + SipHashable(0) + } + + fn combine(_level: Level, a: &Self, b: &Self) -> Self { + #![allow(deprecated)] + use std::hash::{Hasher, SipHasher}; + + let mut hasher = SipHasher::new(); + hasher.write_u64(a.0); + hasher.write_u64(b.0); + SipHashable(hasher.finish()) + } +} + +impl Hashable for String { + fn empty_leaf() -> Self { + "_".to_string() + } + + fn combine(_: Level, a: &Self, b: &Self) -> Self { + a.to_string() + b + } +} + +// +// Operations +// + +#[derive(Clone, Debug)] +pub enum Operation { + Append(A), + CurrentPosition, + CurrentLeaf, + Mark, + MarkedLeaf(Position), + MarkedPositions, + Unmark(Position), + Checkpoint, + Rewind, + Authpath(Position, usize), + GarbageCollect, +} + +use Operation::*; + +pub fn append_str(x: &str) -> Operation { + Operation::Append(x.to_string()) +} + +pub fn unmark(pos: usize) -> Operation { + Operation::Unmark(Position::from(pos)) +} + +pub fn witness(pos: usize, depth: usize) -> Operation { + Operation::Authpath(Position::from(pos), depth) +} + +impl Operation { + pub fn apply>(&self, tree: &mut T) -> Option<(Position, Vec)> { + match self { + Append(a) => { + assert!(tree.append(a.clone()), "append failed"); + None + } + CurrentPosition => None, + CurrentLeaf => None, + Mark => { + assert!(tree.mark().is_some(), "mark failed"); + None + } + MarkedLeaf(_) => None, + MarkedPositions => None, + Unmark(p) => { + assert!(tree.remove_mark(*p), "remove mark failed"); + None + } + Checkpoint => { + tree.checkpoint(); + None + } + Rewind => { + assert!(tree.rewind(), "rewind failed"); + None + } + Authpath(p, d) => tree + .root(*d) + .and_then(|root| tree.witness(*p, &root)) + .map(|xs| (*p, xs)), + GarbageCollect => None, + } + } + + pub fn apply_all>(ops: &[Operation], tree: &mut T) -> Option<(Position, Vec)> { + let mut result = None; + for op in ops { + result = op.apply(tree); + } + result + } +} + +pub fn arb_operation( + item_gen: G, + pos_gen: impl Strategy + Clone, +) -> impl Strategy> +where + G::Value: Clone + 'static, +{ + prop_oneof![ + item_gen.prop_map(Operation::Append), + Just(Operation::Mark), + prop_oneof![ + Just(Operation::CurrentLeaf), + Just(Operation::CurrentPosition), + Just(Operation::MarkedPositions), + ], + Just(Operation::GarbageCollect), + pos_gen + .clone() + .prop_map(|i| Operation::MarkedLeaf(Position::from(i))), + pos_gen + .clone() + .prop_map(|i| Operation::Unmark(Position::from(i))), + Just(Operation::Checkpoint), + Just(Operation::Rewind), + pos_gen + .prop_flat_map(|i| (0usize..10) + .prop_map(move |depth| Operation::Authpath(Position::from(i), depth))), + ] +} + +pub fn apply_operation>(tree: &mut T, op: Operation) { + match op { + Append(value) => { + tree.append(value); + } + Mark => { + tree.mark(); + } + Unmark(position) => { + tree.remove_mark(position); + } + Checkpoint => { + tree.checkpoint(); + } + Rewind => { + tree.rewind(); + } + CurrentPosition => {} + CurrentLeaf => {} + Authpath(_, _) => {} + MarkedLeaf(_) => {} + MarkedPositions => {} + GarbageCollect => {} + } +} + +pub fn check_operations>( + mut tree: T, + tree_depth: u8, + ops: &[Operation], +) -> Result<(), TestCaseError> { + let mut tree_size = 0; + let mut tree_values: Vec = vec![]; + // the number of leaves in the tree at the time that a checkpoint is made + let mut tree_checkpoints: Vec = vec![]; + + for op in ops { + prop_assert_eq!(tree_size, tree_values.len()); + match op { + Append(value) => { + if tree.append(value.clone()) { + prop_assert!(tree_size < (1 << tree_depth)); + tree_size += 1; + tree_values.push(value.clone()); + } else { + prop_assert_eq!(tree_size, 1 << tree_depth); + } + } + CurrentPosition => { + if let Some(pos) = tree.current_position() { + prop_assert!(tree_size > 0); + prop_assert_eq!(tree_size - 1, pos.into()); + } + } + CurrentLeaf => { + 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) => { + if tree.get_marked_leaf(*position).is_some() { + prop_assert!(::from(*position) < tree_size); + } + } + Unmark(position) => { + tree.remove_mark(*position); + } + MarkedPositions => {} + Checkpoint => { + tree_checkpoints.push(tree_size); + tree.checkpoint(); + } + Rewind => { + if tree.rewind() { + prop_assert!(!tree_checkpoints.is_empty()); + let checkpointed_tree_size = tree_checkpoints.pop().unwrap(); + tree_values.truncate(checkpointed_tree_size); + tree_size = checkpointed_tree_size; + } + } + Authpath(position, depth) => { + if let Some(path) = tree.root(*depth).and_then(|r| tree.witness(*position, &r)) { + let value: H = tree_values[::from(*position)].clone(); + let tree_root = tree.root(*depth); + + if tree_checkpoints.len() >= *depth { + let mut extended_tree_values = tree_values.clone(); + if *depth > 0 { + // prune the tree back to the checkpointed size. + if let Some(checkpointed_tree_size) = + tree_checkpoints.get(tree_checkpoints.len() - depth) + { + extended_tree_values.truncate(*checkpointed_tree_size); + } + } + // extend the tree with empty leaves until it is full + extended_tree_values.resize(1 << tree_depth, H::empty_leaf()); + + // compute the root + let expected_root = complete_tree::root::(extended_tree_values); + prop_assert_eq!(&tree_root.unwrap(), &expected_root); + + prop_assert_eq!( + &compute_root_from_witness(value, *position, &path), + &expected_root + ); + } + } + } + GarbageCollect => {} + } + } + + Ok(()) +} + +pub fn compute_root_from_witness(value: H, position: Position, path: &[H]) -> H { + let mut cur = value; + let mut lvl = 0.into(); + for (i, v) in path + .iter() + .enumerate() + .map(|(i, v)| (((::from(position) >> i) & 1) == 1, v)) + { + if i { + cur = H::combine(lvl, v, &cur); + } else { + cur = H::combine(lvl, &cur, v); + } + lvl = lvl + 1; + } + cur +} + +// +// Types and utilities for cross-verification property tests +// + +#[derive(Clone)] +pub struct CombinedTree, E: Tree> { + inefficient: I, + efficient: E, + _phantom: PhantomData, +} + +impl, E: Tree> CombinedTree { + pub fn new(inefficient: I, efficient: E) -> Self { + CombinedTree { + inefficient, + efficient, + _phantom: PhantomData, + } + } +} + +impl, E: Tree> Tree for CombinedTree { + fn append(&mut self, value: H) -> bool { + let a = self.inefficient.append(value.clone()); + let b = self.efficient.append(value); + assert_eq!(a, b); + a + } + + fn root(&self, checkpoint_depth: usize) -> Option { + let a = self.inefficient.root(checkpoint_depth); + let b = self.efficient.root(checkpoint_depth); + assert_eq!(a, b); + a + } + + fn current_position(&self) -> Option { + let a = self.inefficient.current_position(); + let b = self.efficient.current_position(); + assert_eq!(a, b); + a + } + + fn current_leaf(&self) -> Option<&H> { + let a = self.inefficient.current_leaf(); + let b = self.efficient.current_leaf(); + assert_eq!(a, b); + a + } + + fn get_marked_leaf(&self, position: Position) -> Option<&H> { + let a = self.inefficient.get_marked_leaf(position); + let b = self.efficient.get_marked_leaf(position); + assert_eq!(a, b); + a + } + + fn mark(&mut self) -> Option { + 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 { + let a = self.inefficient.marked_positions(); + let b = self.efficient.marked_positions(); + assert_eq!(a, b); + a + } + + fn witness(&self, position: Position, as_of_root: &H) -> Option> { + let a = self.inefficient.witness(position, as_of_root); + let b = self.efficient.witness(position, as_of_root); + assert_eq!(a, b); + a + } + + fn remove_mark(&mut self, position: Position) -> bool { + let a = self.inefficient.remove_mark(position); + let b = self.efficient.remove_mark(position); + assert_eq!(a, b); + a + } + + fn checkpoint(&mut self) { + self.inefficient.checkpoint(); + self.efficient.checkpoint(); + } + + fn rewind(&mut self) -> bool { + let a = self.inefficient.rewind(); + let b = self.efficient.rewind(); + assert_eq!(a, b); + a + } +} +// +// Shared example tests +// + +pub fn check_root_hashes, F: Fn(usize) -> T>(new_tree: F) { + let mut tree = new_tree(100); + assert_eq!(tree.root(0).unwrap(), "________________"); + + tree.append("a".to_string()); + assert_eq!(tree.root(0).unwrap().len(), 16); + assert_eq!(tree.root(0).unwrap(), "a_______________"); + + tree.append("b".to_string()); + assert_eq!(tree.root(0).unwrap(), "ab______________"); + + tree.append("c".to_string()); + assert_eq!(tree.root(0).unwrap(), "abc_____________"); + + let mut t = new_tree(100); + t.append("a".to_string()); + t.checkpoint(); + t.mark(); + t.append("a".to_string()); + t.append("a".to_string()); + t.append("a".to_string()); + assert_eq!(t.root(0).unwrap(), "aaaa____________"); +} + +pub fn check_witnesses + std::fmt::Debug, F: Fn(usize) -> T>(new_tree: F) { + let mut tree = new_tree(100); + tree.append("a".to_string()); + tree.mark(); + assert_eq!( + tree.witness(Position::from(0), &tree.root(0).unwrap()), + Some(vec![ + "_".to_string(), + "__".to_string(), + "____".to_string(), + "________".to_string() + ]) + ); + + tree.append("b".to_string()); + assert_eq!( + tree.witness(0.into(), &tree.root(0).unwrap()), + Some(vec![ + "b".to_string(), + "__".to_string(), + "____".to_string(), + "________".to_string() + ]) + ); + + tree.append("c".to_string()); + tree.mark(); + assert_eq!( + tree.witness(Position::from(2), &tree.root(0).unwrap()), + Some(vec![ + "_".to_string(), + "ab".to_string(), + "____".to_string(), + "________".to_string() + ]) + ); + + tree.append("d".to_string()); + assert_eq!( + tree.witness(Position::from(2), &tree.root(0).unwrap()), + Some(vec![ + "d".to_string(), + "ab".to_string(), + "____".to_string(), + "________".to_string() + ]) + ); + + tree.append("e".to_string()); + assert_eq!( + tree.witness(Position::from(2), &tree.root(0).unwrap()), + Some(vec![ + "d".to_string(), + "ab".to_string(), + "e___".to_string(), + "________".to_string() + ]) + ); + + let mut tree = new_tree(100); + tree.append("a".to_string()); + tree.mark(); + for c in 'b'..'h' { + tree.append(c.to_string()); + } + tree.mark(); + tree.append("h".to_string()); + + assert_eq!( + tree.witness(0.into(), &tree.root(0).unwrap()), + Some(vec![ + "b".to_string(), + "cd".to_string(), + "efgh".to_string(), + "________".to_string() + ]) + ); + + let mut tree = new_tree(100); + tree.append("a".to_string()); + tree.mark(); + tree.append("b".to_string()); + tree.append("c".to_string()); + tree.append("d".to_string()); + tree.mark(); + tree.append("e".to_string()); + tree.mark(); + tree.append("f".to_string()); + tree.mark(); + tree.append("g".to_string()); + + assert_eq!( + tree.witness(Position::from(5), &tree.root(0).unwrap()), + Some(vec![ + "e".to_string(), + "g_".to_string(), + "abcd".to_string(), + "________".to_string() + ]) + ); + + let mut tree = new_tree(100); + for c in 'a'..'l' { + tree.append(c.to_string()); + } + tree.mark(); + tree.append('l'.to_string()); + + assert_eq!( + tree.witness(Position::from(10), &tree.root(0).unwrap()), + Some(vec![ + "l".to_string(), + "ij".to_string(), + "____".to_string(), + "abcdefgh".to_string() + ]) + ); + + let mut tree = new_tree(100); + tree.append('a'.to_string()); + tree.mark(); + tree.checkpoint(); + assert!(tree.rewind()); + for c in 'b'..'f' { + tree.append(c.to_string()); + } + tree.mark(); + for c in 'f'..'i' { + tree.append(c.to_string()); + } + + assert_eq!( + tree.witness(0.into(), &tree.root(0).unwrap()), + Some(vec![ + "b".to_string(), + "cd".to_string(), + "efgh".to_string(), + "________".to_string() + ]) + ); + + let mut tree = new_tree(100); + tree.append('a'.to_string()); + tree.append('b'.to_string()); + tree.append('c'.to_string()); + tree.mark(); + tree.append('d'.to_string()); + tree.append('e'.to_string()); + tree.append('f'.to_string()); + tree.append('g'.to_string()); + tree.mark(); + tree.checkpoint(); + tree.append('h'.to_string()); + assert!(tree.rewind()); + + assert_eq!( + tree.witness(Position::from(2), &tree.root(0).unwrap()), + Some(vec![ + "d".to_string(), + "ab".to_string(), + "efg_".to_string(), + "________".to_string() + ]) + ); + + let mut tree = new_tree(100); + tree.append('a'.to_string()); + tree.append('b'.to_string()); + tree.mark(); + assert_eq!( + tree.witness(Position::from(0), &tree.root(0).unwrap()), + None + ); + + let mut tree = new_tree(100); + for c in 'a'..'n' { + tree.append(c.to_string()); + } + tree.mark(); + tree.append('n'.to_string()); + tree.mark(); + tree.append('o'.to_string()); + tree.append('p'.to_string()); + + assert_eq!( + tree.witness(Position::from(12), &tree.root(0).unwrap()), + Some(vec![ + "n".to_string(), + "op".to_string(), + "ijkl".to_string(), + "abcdefgh".to_string() + ]) + ); + + let ops = ('a'..='l') + .into_iter() + .map(|c| Append(c.to_string())) + .chain(Some(Mark)) + .chain(Some(Append('m'.to_string()))) + .chain(Some(Append('n'.to_string()))) + .chain(Some(Authpath(11usize.into(), 0))) + .collect::>(); + + let mut tree = new_tree(100); + assert_eq!( + Operation::apply_all(&ops, &mut tree), + Some(( + Position::from(11), + vec![ + "k".to_string(), + "ij".to_string(), + "mn__".to_string(), + "abcdefgh".to_string() + ] + )) + ); +} + +pub fn check_checkpoint_rewind, F: Fn(usize) -> T>(new_tree: F) { + let mut t = new_tree(100); + assert!(!t.rewind()); + + let mut t = new_tree(100); + t.checkpoint(); + assert!(t.rewind()); + + let mut t = new_tree(100); + t.append("a".to_string()); + t.checkpoint(); + t.append("b".to_string()); + t.mark(); + assert!(t.rewind()); + assert_eq!(Some(Position::from(0)), t.current_position()); + + let mut t = new_tree(100); + t.append("a".to_string()); + t.mark(); + t.checkpoint(); + assert!(t.rewind()); + + let mut t = new_tree(100); + t.append("a".to_string()); + t.checkpoint(); + t.mark(); + t.append("a".to_string()); + assert!(t.rewind()); + assert_eq!(Some(Position::from(0)), t.current_position()); + + let mut t = new_tree(100); + t.append("a".to_string()); + t.checkpoint(); + t.checkpoint(); + assert!(t.rewind()); + t.append("b".to_string()); + assert!(t.rewind()); + t.append("b".to_string()); + assert_eq!(t.root(0).unwrap(), "ab______________"); +} + +pub fn check_rewind_remove_mark, F: Fn(usize) -> T>(new_tree: F) { + let mut tree = new_tree(100); + tree.append("e".to_string()); + tree.mark(); + tree.checkpoint(); + assert!(tree.rewind()); + assert!(tree.remove_mark(0usize.into())); + + let mut tree = new_tree(100); + tree.append("e".to_string()); + tree.checkpoint(); + tree.mark(); + assert!(tree.rewind()); + assert!(!tree.remove_mark(0usize.into())); + + let mut tree = new_tree(100); + tree.append("e".to_string()); + tree.mark(); + tree.checkpoint(); + assert!(tree.remove_mark(0usize.into())); + assert!(tree.rewind()); + assert!(tree.remove_mark(0usize.into())); + + let mut tree = new_tree(100); + tree.append("e".to_string()); + tree.mark(); + assert!(tree.remove_mark(0usize.into())); + tree.checkpoint(); + assert!(tree.rewind()); + assert!(!tree.remove_mark(0usize.into())); + + let mut tree = new_tree(100); + tree.append("a".to_string()); + assert!(!tree.remove_mark(0usize.into())); + tree.checkpoint(); + assert!(tree.mark().is_some()); + assert!(tree.rewind()); + + let mut tree = new_tree(100); + tree.append("a".to_string()); + tree.checkpoint(); + assert!(tree.mark().is_some()); + assert!(tree.remove_mark(0usize.into())); + assert!(tree.rewind()); + assert!(!tree.remove_mark(0usize.into())); + + // The following check_operations tests cover errors where the + // test framework itself previously did not correctly handle + // chain state restoration. + + let samples = vec![ + vec![append_str("x"), Checkpoint, Mark, Rewind, unmark(0)], + vec![ + append_str("d"), + Checkpoint, + Mark, + unmark(0), + Rewind, + unmark(0), + ], + vec![ + append_str("o"), + Checkpoint, + Mark, + Checkpoint, + unmark(0), + Rewind, + Rewind, + ], + vec![ + append_str("s"), + Mark, + append_str("m"), + Checkpoint, + unmark(0), + Rewind, + unmark(0), + unmark(0), + ], + ]; + + for (i, sample) in samples.iter().enumerate() { + let result = check_operations(new_tree(100), 4, sample); + assert!( + matches!(result, Ok(())), + "Reference/Test mismatch at index {}: {:?}", + i, + result + ); + } +} + +pub fn check_witness_consistency, F: Fn(usize) -> T>(new_tree: F) { + let samples = vec![ + // Reduced examples + vec![ + append_str("a"), + append_str("b"), + Checkpoint, + Mark, + witness(0, 1), + ], + vec![ + append_str("c"), + append_str("d"), + Mark, + Checkpoint, + witness(1, 1), + ], + vec![ + append_str("e"), + Checkpoint, + Mark, + append_str("f"), + witness(0, 1), + ], + vec![ + append_str("g"), + Mark, + Checkpoint, + unmark(0), + append_str("h"), + witness(0, 0), + ], + vec![ + append_str("i"), + Checkpoint, + Mark, + unmark(0), + append_str("j"), + witness(0, 0), + ], + vec![ + append_str("i"), + Mark, + append_str("j"), + Checkpoint, + append_str("k"), + witness(0, 1), + ], + vec![ + append_str("l"), + Checkpoint, + Mark, + Checkpoint, + append_str("m"), + Checkpoint, + witness(0, 2), + ], + vec![Checkpoint, append_str("n"), Mark, witness(0, 1)], + vec![ + append_str("a"), + Mark, + Checkpoint, + unmark(0), + Checkpoint, + append_str("b"), + witness(0, 1), + ], + vec![ + append_str("a"), + Mark, + append_str("b"), + unmark(0), + Checkpoint, + witness(0, 0), + ], + vec![ + append_str("a"), + Mark, + Checkpoint, + unmark(0), + Checkpoint, + Rewind, + append_str("b"), + witness(0, 0), + ], + vec![ + append_str("a"), + Mark, + Checkpoint, + Checkpoint, + Rewind, + append_str("a"), + unmark(0), + witness(0, 1), + ], + // Unreduced examples + vec![ + append_str("o"), + append_str("p"), + Mark, + append_str("q"), + Checkpoint, + unmark(1), + witness(1, 1), + ], + vec![ + append_str("r"), + append_str("s"), + append_str("t"), + Mark, + Checkpoint, + unmark(2), + Checkpoint, + witness(2, 2), + ], + vec![ + append_str("u"), + Mark, + append_str("v"), + append_str("w"), + Checkpoint, + unmark(0), + append_str("x"), + Checkpoint, + Checkpoint, + witness(0, 3), + ], + ]; + + for (i, sample) in samples.iter().enumerate() { + let result = check_operations(new_tree(100), 4, sample); + assert!( + matches!(result, Ok(())), + "Reference/Test mismatch at index {}: {:?}", + i, + result + ); + } +} + +pub fn check_rewind_remove_mark_consistency, F: Fn(usize) -> T>(new_tree: F) { + let samples = vec![ + vec![append_str("x"), Checkpoint, Mark, Rewind, unmark(0)], + vec![ + append_str("d"), + Checkpoint, + Mark, + unmark(0), + Rewind, + unmark(0), + ], + vec![ + append_str("o"), + Checkpoint, + Mark, + Checkpoint, + unmark(0), + Rewind, + Rewind, + ], + vec![ + append_str("s"), + Mark, + append_str("m"), + Checkpoint, + unmark(0), + Rewind, + unmark(0), + unmark(0), + ], + ]; + for (i, sample) in samples.iter().enumerate() { + let result = check_operations(new_tree(100), 4, sample); + assert!( + matches!(result, Ok(())), + "Reference/Test mismatch at index {}: {:?}", + i, + result + ); + } +} + +#[cfg(test)] +pub(crate) mod tests { + use crate::{ + testing::{compute_root_from_witness, SipHashable}, + Hashable, Level, Position, + }; + + #[test] + fn test_compute_root_from_witness() { + let expected = SipHashable::combine( + ::from(2), + &SipHashable::combine( + Level::from(1), + &SipHashable::combine(0.into(), &SipHashable(0), &SipHashable(1)), + &SipHashable::combine(0.into(), &SipHashable(2), &SipHashable(3)), + ), + &SipHashable::combine( + Level::from(1), + &SipHashable::combine(0.into(), &SipHashable(4), &SipHashable(5)), + &SipHashable::combine(0.into(), &SipHashable(6), &SipHashable(7)), + ), + ); + + assert_eq!( + compute_root_from_witness::( + SipHashable(0), + 0.into(), + &[ + SipHashable(1), + SipHashable::combine(0.into(), &SipHashable(2), &SipHashable(3)), + SipHashable::combine( + Level::from(1), + &SipHashable::combine(0.into(), &SipHashable(4), &SipHashable(5)), + &SipHashable::combine(0.into(), &SipHashable(6), &SipHashable(7)) + ) + ] + ), + expected + ); + + assert_eq!( + compute_root_from_witness( + SipHashable(4), + Position::from(4), + &[ + SipHashable(5), + SipHashable::combine(0.into(), &SipHashable(6), &SipHashable(7)), + SipHashable::combine( + Level::from(1), + &SipHashable::combine(0.into(), &SipHashable(0), &SipHashable(1)), + &SipHashable::combine(0.into(), &SipHashable(2), &SipHashable(3)) + ) + ] + ), + expected + ); + } +} diff --git a/src/testing/complete_tree.rs b/incrementalmerkletree/src/testing/complete_tree.rs similarity index 89% rename from src/testing/complete_tree.rs rename to incrementalmerkletree/src/testing/complete_tree.rs index 234007d..080f645 100644 --- a/src/testing/complete_tree.rs +++ b/incrementalmerkletree/src/testing/complete_tree.rs @@ -1,12 +1,37 @@ //! Sample implementation of the Tree interface. use std::collections::BTreeSet; -use super::{Frontier, Tree}; use crate::{ - hashing::Hashable, - position::{Level, Position}, + testing::{Frontier, Tree}, + Hashable, Level, Position, }; +pub(crate) fn root(mut leaves: Vec) -> H { + leaves.resize(leaves.len().next_power_of_two(), H::empty_leaf()); + + //leaves are always at level zero, so we start there. + let mut level = Level::from(0); + while leaves.len() != 1 { + leaves = leaves + .iter() + .enumerate() + .filter(|(i, _)| (i % 2) == 0) + .map(|(_, a)| a) + .zip( + leaves + .iter() + .enumerate() + .filter(|(i, _)| (i % 2) == 1) + .map(|(_, b)| b), + ) + .map(|(a, b)| H::combine(level, a, b)) + .collect(); + level = level + 1; + } + + leaves[0].clone() +} + #[derive(Clone, Debug)] pub struct TreeState { leaves: Vec, @@ -17,7 +42,6 @@ pub struct TreeState { impl TreeState { /// Creates a new, empty binary tree of specified depth. - #[cfg(test)] pub fn new(depth: usize) -> Self { Self { leaves: vec![H::empty_leaf(); 1 << depth], @@ -29,11 +53,11 @@ impl TreeState { } impl Frontier for TreeState { - fn append(&mut self, value: &H) -> bool { + fn append(&mut self, value: H) -> bool { if self.current_offset == (1 << self.depth) { false } else { - self.leaves[self.current_offset] = value.clone(); + self.leaves[self.current_offset] = value; self.current_offset += 1; true } @@ -41,7 +65,7 @@ impl Frontier for TreeState { /// Obtains the current root of this Merkle tree. fn root(&self) -> H { - lazy_root(self.leaves.clone()) + root(self.leaves.clone()) } } @@ -89,9 +113,7 @@ impl TreeState { let mut leaf_idx: usize = position.into(); for bit in 0..self.depth { leaf_idx ^= 1 << bit; - path.push(lazy_root::( - self.leaves[leaf_idx..][0..(1 << bit)].to_vec(), - )); + path.push(root::(self.leaves[leaf_idx..][0..(1 << bit)].to_vec())); leaf_idx &= usize::MAX << (bit + 1); } @@ -118,7 +140,6 @@ pub struct CompleteTree { impl CompleteTree { /// Creates a new, empty binary tree of specified depth. - #[cfg(test)] pub fn new(depth: usize, max_checkpoints: usize) -> Self { Self { tree_state: TreeState::new(depth), @@ -158,7 +179,7 @@ impl CompleteTree { impl Tree for CompleteTree { /// Appends a new value to the tree at the next available slot. Returns true /// if successful and false if the tree is full. - fn append(&mut self, value: &H) -> bool { + fn append(&mut self, value: H) -> bool { self.tree_state.append(value) } @@ -226,42 +247,17 @@ impl Tree for CompleteTree } } -pub(crate) fn lazy_root(mut leaves: Vec) -> H { - //leaves are always at level zero, so we start there. - let mut level = Level::from(0); - while leaves.len() != 1 { - leaves = leaves - .iter() - .enumerate() - .filter(|(i, _)| (i % 2) == 0) - .map(|(_, a)| a) - .zip( - leaves - .iter() - .enumerate() - .filter(|(i, _)| (i % 2) == 1) - .map(|(_, b)| b), - ) - .map(|(a, b)| H::combine(level, a, b)) - .collect(); - level = level + 1; - } - - leaves[0].clone() -} - #[cfg(test)] mod tests { use std::convert::TryFrom; use super::CompleteTree; use crate::{ - hashing::Hashable, - position::{Level, Position}, testing::{ - tests::{self, compute_root_from_witness}, - SipHashable, Tree, + check_checkpoint_rewind, check_rewind_remove_mark, check_root_hashes, check_witnesses, + compute_root_from_witness, SipHashable, Tree, }, + Hashable, Level, Position, }; #[test] @@ -283,9 +279,9 @@ mod tests { let mut tree = CompleteTree::::new(DEPTH, 100); for value in values { - assert!(tree.append(&value)); + assert!(tree.append(value)); } - assert!(!tree.append(&SipHashable(0))); + assert!(!tree.append(SipHashable(0))); let expected = SipHashable::combine( Level::from(2), @@ -306,12 +302,12 @@ mod tests { #[test] fn root_hashes() { - tests::check_root_hashes(|max_c| CompleteTree::::new(4, max_c)); + check_root_hashes(|max_c| CompleteTree::::new(4, max_c)); } #[test] fn witnesss() { - tests::check_witnesss(|max_c| CompleteTree::::new(4, max_c)); + check_witnesses(|max_c| CompleteTree::::new(4, max_c)); } #[test] @@ -321,10 +317,10 @@ mod tests { let mut tree = CompleteTree::::new(DEPTH, 100); for value in values { - assert!(tree.append(&value)); + assert!(tree.append(value)); tree.mark(); } - assert!(!tree.append(&SipHashable(0))); + assert!(!tree.append(SipHashable(0))); let expected = SipHashable::combine( ::from(2), @@ -354,11 +350,11 @@ mod tests { #[test] fn checkpoint_rewind() { - tests::check_checkpoint_rewind(|max_c| CompleteTree::::new(4, max_c)); + check_checkpoint_rewind(|max_c| CompleteTree::::new(4, max_c)); } #[test] fn rewind_remove_mark() { - tests::check_rewind_remove_mark(|max_c| CompleteTree::::new(4, max_c)); + check_rewind_remove_mark(|max_c| CompleteTree::::new(4, max_c)); } } diff --git a/src/hashing.rs b/src/hashing.rs deleted file mode 100644 index 71bfb8e..0000000 --- a/src/hashing.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::position::Level; - -/// A trait describing the operations that make a type suitable for use as -/// a leaf or node value in a merkle tree. -pub trait Hashable: Sized { - fn empty_leaf() -> Self; - - fn combine(level: Level, a: &Self, b: &Self) -> Self; - - fn empty_root(level: Level) -> Self { - Level::from(0) - .iter_to(level) - .fold(Self::empty_leaf(), |v, lvl| Self::combine(lvl, &v, &v)) - } -} diff --git a/src/testing.rs b/src/testing.rs deleted file mode 100644 index b17b364..0000000 --- a/src/testing.rs +++ /dev/null @@ -1,1099 +0,0 @@ -mod complete_tree; - -use proptest::prelude::*; -use std::collections::BTreeSet; - -use super::{ - hashing::Hashable, - position::{Level, Position}, -}; - -// -// Traits used to permit comparison testing between tree implementations. -// - -/// A possibly-empty incremental Merkle frontier. -pub trait Frontier { - /// Appends a new value to the frontier at the next available slot. - /// Returns true if successful and false if the frontier would exceed - /// the maximum allowed depth. - fn append(&mut self, value: &H) -> bool; - - /// Obtains the current root of this Merkle frontier by hashing - /// against empty nodes up to the maximum height of the pruned - /// tree that the frontier represents. - fn root(&self) -> H; -} - -/// A Merkle tree that supports incremental appends, marking of -/// leaf nodes for construction of witnesses, checkpoints and rollbacks. -pub trait Tree { - /// Appends a new value to the tree at the next available slot. - /// Returns true if successful and false if the tree would exceed - /// the maximum allowed depth. - fn append(&mut self, value: &H) -> bool; - - /// Returns the most recently appended leaf value. - fn current_position(&self) -> Option; - - /// Returns the most recently appended leaf value. - fn current_leaf(&self) -> Option<&H>; - - /// Returns the leaf at the specified position if the tree can produce - /// a witness for it. - 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; - - /// Return a set of all the positions for which we have marked. - fn marked_positions(&self) -> BTreeSet; - - /// Obtains the root of the Merkle tree at the specified checkpoint depth - /// by hashing against empty nodes up to the maximum height of the tree. - /// Returns `None` if there are not enough checkpoints available to reach the - /// requested checkpoint depth. - fn root(&self, checkpoint_depth: usize) -> Option; - - /// Obtains a witness to the value at the specified position, - /// as of the tree state corresponding to the given root. - /// Returns `None` if there is no available witness to that - /// position or if the root does not correspond to a checkpointed - /// root of the tree. - fn witness(&self, position: Position, as_of_root: &H) -> Option>; - - /// Marks the value at the specified position as a value we're no longer - /// interested in maintaining a mark for. Returns true if successful and - /// false if we were already not maintaining a mark at this position. - fn remove_mark(&mut self, position: Position) -> bool; - - /// Creates a new checkpoint for the current tree state. It is valid to - /// have multiple checkpoints for the same tree state, and each `rewind` - /// call will remove a single checkpoint. - fn checkpoint(&mut self); - - /// Rewinds the tree state to the previous checkpoint, and then removes - /// that checkpoint record. If there are multiple checkpoints at a given - /// tree state, the tree state will not be altered until all checkpoints - /// at that tree state have been removed using `rewind`. This function - /// return false and leave the tree unmodified if no checkpoints exist. - fn rewind(&mut self) -> bool; -} - -// -// Types and utilities for shared example tests. -// - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) struct SipHashable(pub(crate) u64); - -impl Hashable for SipHashable { - fn empty_leaf() -> Self { - SipHashable(0) - } - - fn combine(_level: Level, a: &Self, b: &Self) -> Self { - #![allow(deprecated)] - use std::hash::{Hasher, SipHasher}; - - let mut hasher = SipHasher::new(); - hasher.write_u64(a.0); - hasher.write_u64(b.0); - SipHashable(hasher.finish()) - } -} - -impl Hashable for String { - fn empty_leaf() -> Self { - "_".to_string() - } - - fn combine(_: Level, a: &Self, b: &Self) -> Self { - a.to_string() + b - } -} - -// -// Operations -// - -#[derive(Clone, Debug)] -pub enum Operation { - Append(A), - CurrentPosition, - CurrentLeaf, - Mark, - MarkedLeaf(Position), - MarkedPositions, - Unmark(Position), - Checkpoint, - Rewind, - Authpath(Position, usize), - GarbageCollect, -} - -use Operation::*; - -impl Operation { - pub fn apply>(&self, tree: &mut T) -> Option<(Position, Vec)> { - match self { - Append(a) => { - assert!(tree.append(a), "append failed"); - None - } - CurrentPosition => None, - CurrentLeaf => None, - Mark => { - assert!(tree.mark().is_some(), "mark failed"); - None - } - MarkedLeaf(_) => None, - MarkedPositions => None, - Unmark(p) => { - assert!(tree.remove_mark(*p), "remove mark failed"); - None - } - Checkpoint => { - tree.checkpoint(); - None - } - Rewind => { - assert!(tree.rewind(), "rewind failed"); - None - } - Authpath(p, d) => tree - .root(*d) - .and_then(|root| tree.witness(*p, &root)) - .map(|xs| (*p, xs)), - GarbageCollect => None, - } - } - - pub fn apply_all>(ops: &[Operation], tree: &mut T) -> Option<(Position, Vec)> { - let mut result = None; - for op in ops { - result = op.apply(tree); - } - result - } -} - -pub fn arb_operation( - item_gen: G, - pos_gen: impl Strategy + Clone, -) -> impl Strategy> -where - G::Value: Clone + 'static, -{ - prop_oneof![ - item_gen.prop_map(Operation::Append), - Just(Operation::Mark), - prop_oneof![ - Just(Operation::CurrentLeaf), - Just(Operation::CurrentPosition), - Just(Operation::MarkedPositions), - ], - Just(Operation::GarbageCollect), - pos_gen - .clone() - .prop_map(|i| Operation::MarkedLeaf(Position::from(i))), - pos_gen - .clone() - .prop_map(|i| Operation::Unmark(Position::from(i))), - Just(Operation::Checkpoint), - Just(Operation::Rewind), - pos_gen - .prop_flat_map(|i| (0usize..10) - .prop_map(move |depth| Operation::Authpath(Position::from(i), depth))), - ] -} - -pub fn apply_operation>(tree: &mut T, op: Operation) { - match op { - Append(value) => { - tree.append(&value); - } - Mark => { - tree.mark(); - } - Unmark(position) => { - tree.remove_mark(position); - } - Checkpoint => { - tree.checkpoint(); - } - Rewind => { - tree.rewind(); - } - CurrentPosition => {} - CurrentLeaf => {} - Authpath(_, _) => {} - MarkedLeaf(_) => {} - MarkedPositions => {} - GarbageCollect => {} - } -} - -#[cfg(test)] -pub(crate) mod tests { - use proptest::prelude::*; - use std::collections::BTreeSet; - use std::fmt::Debug; - - use crate::{ - hashing::Hashable, - position::{Level, Position}, - BridgeTree, - }; - - use super::{ - arb_operation, - complete_tree::{lazy_root, CompleteTree}, - Operation, - Operation::*, - SipHashable, Tree, - }; - - // - // Shared example tests - // - - pub(crate) fn check_root_hashes, F: Fn(usize) -> T>(new_tree: F) { - let mut tree = new_tree(100); - assert_eq!(tree.root(0).unwrap(), "________________"); - - tree.append(&"a".to_string()); - assert_eq!(tree.root(0).unwrap().len(), 16); - assert_eq!(tree.root(0).unwrap(), "a_______________"); - - tree.append(&"b".to_string()); - assert_eq!(tree.root(0).unwrap(), "ab______________"); - - tree.append(&"c".to_string()); - assert_eq!(tree.root(0).unwrap(), "abc_____________"); - - let mut t = new_tree(100); - t.append(&"a".to_string()); - t.checkpoint(); - t.mark(); - t.append(&"a".to_string()); - t.append(&"a".to_string()); - t.append(&"a".to_string()); - assert_eq!(t.root(0).unwrap(), "aaaa____________"); - } - - pub(crate) fn check_witnesss + std::fmt::Debug, F: Fn(usize) -> T>( - new_tree: F, - ) { - let mut tree = new_tree(100); - tree.append(&"a".to_string()); - tree.mark(); - assert_eq!( - tree.witness(Position::from(0), &tree.root(0).unwrap()), - Some(vec![ - "_".to_string(), - "__".to_string(), - "____".to_string(), - "________".to_string() - ]) - ); - - tree.append(&"b".to_string()); - assert_eq!( - tree.witness(0.into(), &tree.root(0).unwrap()), - Some(vec![ - "b".to_string(), - "__".to_string(), - "____".to_string(), - "________".to_string() - ]) - ); - - tree.append(&"c".to_string()); - tree.mark(); - assert_eq!( - tree.witness(Position::from(2), &tree.root(0).unwrap()), - Some(vec![ - "_".to_string(), - "ab".to_string(), - "____".to_string(), - "________".to_string() - ]) - ); - - tree.append(&"d".to_string()); - assert_eq!( - tree.witness(Position::from(2), &tree.root(0).unwrap()), - Some(vec![ - "d".to_string(), - "ab".to_string(), - "____".to_string(), - "________".to_string() - ]) - ); - - tree.append(&"e".to_string()); - assert_eq!( - tree.witness(Position::from(2), &tree.root(0).unwrap()), - Some(vec![ - "d".to_string(), - "ab".to_string(), - "e___".to_string(), - "________".to_string() - ]) - ); - - let mut tree = new_tree(100); - tree.append(&"a".to_string()); - tree.mark(); - for c in 'b'..'h' { - tree.append(&c.to_string()); - } - tree.mark(); - tree.append(&"h".to_string()); - - assert_eq!( - tree.witness(0.into(), &tree.root(0).unwrap()), - Some(vec![ - "b".to_string(), - "cd".to_string(), - "efgh".to_string(), - "________".to_string() - ]) - ); - - let mut tree = new_tree(100); - tree.append(&"a".to_string()); - tree.mark(); - tree.append(&"b".to_string()); - tree.append(&"c".to_string()); - tree.append(&"d".to_string()); - tree.mark(); - tree.append(&"e".to_string()); - tree.mark(); - tree.append(&"f".to_string()); - tree.mark(); - tree.append(&"g".to_string()); - - assert_eq!( - tree.witness(Position::from(5), &tree.root(0).unwrap()), - Some(vec![ - "e".to_string(), - "g_".to_string(), - "abcd".to_string(), - "________".to_string() - ]) - ); - - let mut tree = new_tree(100); - for c in 'a'..'l' { - tree.append(&c.to_string()); - } - tree.mark(); - tree.append(&'l'.to_string()); - - assert_eq!( - tree.witness(Position::from(10), &tree.root(0).unwrap()), - Some(vec![ - "l".to_string(), - "ij".to_string(), - "____".to_string(), - "abcdefgh".to_string() - ]) - ); - - let mut tree = new_tree(100); - tree.append(&'a'.to_string()); - tree.mark(); - tree.checkpoint(); - assert!(tree.rewind()); - for c in 'b'..'f' { - tree.append(&c.to_string()); - } - tree.mark(); - for c in 'f'..'i' { - tree.append(&c.to_string()); - } - - assert_eq!( - tree.witness(0.into(), &tree.root(0).unwrap()), - Some(vec![ - "b".to_string(), - "cd".to_string(), - "efgh".to_string(), - "________".to_string() - ]) - ); - - let mut tree = new_tree(100); - tree.append(&'a'.to_string()); - tree.append(&'b'.to_string()); - tree.append(&'c'.to_string()); - tree.mark(); - tree.append(&'d'.to_string()); - tree.append(&'e'.to_string()); - tree.append(&'f'.to_string()); - tree.append(&'g'.to_string()); - tree.mark(); - tree.checkpoint(); - tree.append(&'h'.to_string()); - assert!(tree.rewind()); - - assert_eq!( - tree.witness(Position::from(2), &tree.root(0).unwrap()), - Some(vec![ - "d".to_string(), - "ab".to_string(), - "efg_".to_string(), - "________".to_string() - ]) - ); - - let mut tree = new_tree(100); - tree.append(&'a'.to_string()); - tree.append(&'b'.to_string()); - tree.mark(); - assert_eq!( - tree.witness(Position::from(0), &tree.root(0).unwrap()), - None - ); - - let mut tree = new_tree(100); - for c in 'a'..'n' { - tree.append(&c.to_string()); - } - tree.mark(); - tree.append(&'n'.to_string()); - tree.mark(); - tree.append(&'o'.to_string()); - tree.append(&'p'.to_string()); - - assert_eq!( - tree.witness(Position::from(12), &tree.root(0).unwrap()), - Some(vec![ - "n".to_string(), - "op".to_string(), - "ijkl".to_string(), - "abcdefgh".to_string() - ]) - ); - - let ops = ('a'..='l') - .into_iter() - .map(|c| Append(c.to_string())) - .chain(Some(Mark)) - .chain(Some(Append('m'.to_string()))) - .chain(Some(Append('n'.to_string()))) - .chain(Some(Authpath(11usize.into(), 0))) - .collect::>(); - - let mut tree = new_tree(100); - assert_eq!( - Operation::apply_all(&ops, &mut tree), - Some(( - Position::from(11), - vec![ - "k".to_string(), - "ij".to_string(), - "mn__".to_string(), - "abcdefgh".to_string() - ] - )) - ); - } - - pub(crate) fn check_checkpoint_rewind, F: Fn(usize) -> T>(new_tree: F) { - let mut t = new_tree(100); - assert!(!t.rewind()); - - let mut t = new_tree(100); - t.checkpoint(); - assert!(t.rewind()); - - let mut t = new_tree(100); - t.append(&"a".to_string()); - t.checkpoint(); - t.append(&"b".to_string()); - t.mark(); - assert!(t.rewind()); - assert_eq!(Some(Position::from(0)), t.current_position()); - - let mut t = new_tree(100); - t.append(&"a".to_string()); - t.mark(); - t.checkpoint(); - assert!(t.rewind()); - - let mut t = new_tree(100); - t.append(&"a".to_string()); - t.checkpoint(); - t.mark(); - t.append(&"a".to_string()); - assert!(t.rewind()); - assert_eq!(Some(Position::from(0)), t.current_position()); - - let mut t = new_tree(100); - t.append(&"a".to_string()); - t.checkpoint(); - t.checkpoint(); - assert!(t.rewind()); - t.append(&"b".to_string()); - assert!(t.rewind()); - t.append(&"b".to_string()); - assert_eq!(t.root(0).unwrap(), "ab______________"); - } - - fn append(x: &str) -> Operation { - Operation::Append(x.to_string()) - } - - fn unmark(pos: usize) -> Operation { - Operation::Unmark(Position::from(pos)) - } - - fn witness(pos: usize, depth: usize) -> Operation { - Operation::Authpath(Position::from(pos), depth) - } - - pub(crate) fn check_rewind_remove_mark, F: Fn(usize) -> T>(new_tree: F) { - let mut tree = new_tree(100); - tree.append(&"e".to_string()); - tree.mark(); - tree.checkpoint(); - assert!(tree.rewind()); - assert!(tree.remove_mark(0usize.into())); - - let mut tree = new_tree(100); - tree.append(&"e".to_string()); - tree.checkpoint(); - tree.mark(); - assert!(tree.rewind()); - assert!(!tree.remove_mark(0usize.into())); - - let mut tree = new_tree(100); - tree.append(&"e".to_string()); - tree.mark(); - tree.checkpoint(); - assert!(tree.remove_mark(0usize.into())); - assert!(tree.rewind()); - assert!(tree.remove_mark(0usize.into())); - - let mut tree = new_tree(100); - tree.append(&"e".to_string()); - tree.mark(); - assert!(tree.remove_mark(0usize.into())); - tree.checkpoint(); - assert!(tree.rewind()); - assert!(!tree.remove_mark(0usize.into())); - - let mut tree = new_tree(100); - tree.append(&"a".to_string()); - assert!(!tree.remove_mark(0usize.into())); - tree.checkpoint(); - assert!(tree.mark().is_some()); - assert!(tree.rewind()); - - let mut tree = new_tree(100); - tree.append(&"a".to_string()); - tree.checkpoint(); - assert!(tree.mark().is_some()); - assert!(tree.remove_mark(0usize.into())); - assert!(tree.rewind()); - assert!(!tree.remove_mark(0usize.into())); - - // The following check_operations tests cover errors where the - // test framework itself previously did not correctly handle - // chain state restoration. - - let samples = vec![ - vec![append("x"), Checkpoint, Mark, Rewind, unmark(0)], - vec![append("d"), Checkpoint, Mark, unmark(0), Rewind, unmark(0)], - vec![ - append("o"), - Checkpoint, - Mark, - Checkpoint, - unmark(0), - Rewind, - Rewind, - ], - vec![ - append("s"), - Mark, - append("m"), - Checkpoint, - unmark(0), - Rewind, - unmark(0), - unmark(0), - ], - ]; - - for (i, sample) in samples.iter().enumerate() { - let result = check_operations(sample); - assert!( - matches!(result, Ok(())), - "Reference/Test mismatch at index {}: {:?}", - i, - result - ); - } - } - - // - // Types and utilities for cross-verification property tests - // - - #[derive(Clone)] - pub struct CombinedTree { - inefficient: CompleteTree, - efficient: BridgeTree, - } - - impl CombinedTree { - pub fn new() -> Self { - CombinedTree { - inefficient: CompleteTree::new(DEPTH.into(), 100), - efficient: BridgeTree::new(100), - } - } - } - - impl Tree for CombinedTree { - fn append(&mut self, value: &H) -> bool { - let a = self.inefficient.append(value); - let b = self.efficient.append(value); - assert_eq!(a, b); - a - } - - fn root(&self, checkpoint_depth: usize) -> Option { - let a = self.inefficient.root(checkpoint_depth); - let b = self.efficient.root(checkpoint_depth); - assert_eq!(a, b); - a - } - - fn current_position(&self) -> Option { - let a = self.inefficient.current_position(); - let b = self.efficient.current_position(); - assert_eq!(a, b); - a - } - - fn current_leaf(&self) -> Option<&H> { - let a = self.inefficient.current_leaf(); - let b = self.efficient.current_leaf(); - assert_eq!(a, b); - a - } - - fn get_marked_leaf(&self, position: Position) -> Option<&H> { - let a = self.inefficient.get_marked_leaf(position); - let b = self.efficient.get_marked_leaf(position); - assert_eq!(a, b); - a - } - - fn mark(&mut self) -> Option { - 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 { - let a = self.inefficient.marked_positions(); - let b = self.efficient.marked_positions(); - assert_eq!(a, b); - a - } - - fn witness(&self, position: Position, as_of_root: &H) -> Option> { - let a = self.inefficient.witness(position, as_of_root); - let b = self.efficient.witness(position, as_of_root); - assert_eq!(a, b); - a - } - - fn remove_mark(&mut self, position: Position) -> bool { - let a = self.inefficient.remove_mark(position); - let b = self.efficient.remove_mark(position); - assert_eq!(a, b); - a - } - - fn checkpoint(&mut self) { - self.inefficient.checkpoint(); - self.efficient.checkpoint(); - } - - fn rewind(&mut self) -> bool { - let a = self.inefficient.rewind(); - let b = self.efficient.rewind(); - assert_eq!(a, b); - a - } - } - - pub(crate) fn compute_root_from_witness( - value: H, - position: Position, - path: &[H], - ) -> H { - let mut cur = value; - let mut lvl = 0.into(); - for (i, v) in path - .iter() - .enumerate() - .map(|(i, v)| (((::from(position) >> i) & 1) == 1, v)) - { - if i { - cur = H::combine(lvl, v, &cur); - } else { - cur = H::combine(lvl, &cur, v); - } - lvl = lvl + 1; - } - cur - } - - #[test] - fn test_compute_root_from_witness() { - let expected = SipHashable::combine( - ::from(2), - &SipHashable::combine( - Level::from(1), - &SipHashable::combine(0.into(), &SipHashable(0), &SipHashable(1)), - &SipHashable::combine(0.into(), &SipHashable(2), &SipHashable(3)), - ), - &SipHashable::combine( - Level::from(1), - &SipHashable::combine(0.into(), &SipHashable(4), &SipHashable(5)), - &SipHashable::combine(0.into(), &SipHashable(6), &SipHashable(7)), - ), - ); - - assert_eq!( - compute_root_from_witness::( - SipHashable(0), - 0.into(), - &[ - SipHashable(1), - SipHashable::combine(0.into(), &SipHashable(2), &SipHashable(3)), - SipHashable::combine( - Level::from(1), - &SipHashable::combine(0.into(), &SipHashable(4), &SipHashable(5)), - &SipHashable::combine(0.into(), &SipHashable(6), &SipHashable(7)) - ) - ] - ), - expected - ); - - assert_eq!( - compute_root_from_witness( - SipHashable(4), - Position::from(4), - &[ - SipHashable(5), - SipHashable::combine(0.into(), &SipHashable(6), &SipHashable(7)), - SipHashable::combine( - Level::from(1), - &SipHashable::combine(0.into(), &SipHashable(0), &SipHashable(1)), - &SipHashable::combine(0.into(), &SipHashable(2), &SipHashable(3)) - ) - ] - ), - expected - ); - } - - #[test] - fn test_witness_consistency() { - let samples = vec![ - // Reduced examples - vec![append("a"), append("b"), Checkpoint, Mark, witness(0, 1)], - vec![append("c"), append("d"), Mark, Checkpoint, witness(1, 1)], - vec![append("e"), Checkpoint, Mark, append("f"), witness(0, 1)], - vec![ - append("g"), - Mark, - Checkpoint, - unmark(0), - append("h"), - witness(0, 0), - ], - vec![ - append("i"), - Checkpoint, - Mark, - unmark(0), - append("j"), - witness(0, 0), - ], - vec![ - append("i"), - Mark, - append("j"), - Checkpoint, - append("k"), - witness(0, 1), - ], - vec![ - append("l"), - Checkpoint, - Mark, - Checkpoint, - append("m"), - Checkpoint, - witness(0, 2), - ], - vec![Checkpoint, append("n"), Mark, witness(0, 1)], - vec![ - append("a"), - Mark, - Checkpoint, - unmark(0), - Checkpoint, - append("b"), - witness(0, 1), - ], - vec![ - append("a"), - Mark, - append("b"), - unmark(0), - Checkpoint, - witness(0, 0), - ], - vec![ - append("a"), - Mark, - Checkpoint, - unmark(0), - Checkpoint, - Rewind, - append("b"), - witness(0, 0), - ], - vec![ - append("a"), - Mark, - Checkpoint, - Checkpoint, - Rewind, - append("a"), - unmark(0), - witness(0, 1), - ], - // Unreduced examples - vec![ - append("o"), - append("p"), - Mark, - append("q"), - Checkpoint, - unmark(1), - witness(1, 1), - ], - vec![ - append("r"), - append("s"), - append("t"), - Mark, - Checkpoint, - unmark(2), - Checkpoint, - witness(2, 2), - ], - vec![ - append("u"), - Mark, - append("v"), - append("w"), - Checkpoint, - unmark(0), - append("x"), - Checkpoint, - Checkpoint, - witness(0, 3), - ], - ]; - - for (i, sample) in samples.iter().enumerate() { - let result = check_operations(sample); - assert!( - matches!(result, Ok(())), - "Reference/Test mismatch at index {}: {:?}", - i, - result - ); - } - } - - // These check_operations tests cover errors where the test framework itself previously did not - // correctly handle chain state restoration. - #[test] - fn test_rewind_remove_mark_consistency() { - let samples = vec![ - vec![append("x"), Checkpoint, Mark, Rewind, unmark(0)], - vec![append("d"), Checkpoint, Mark, unmark(0), Rewind, unmark(0)], - vec![ - append("o"), - Checkpoint, - Mark, - Checkpoint, - unmark(0), - Rewind, - Rewind, - ], - vec![ - append("s"), - Mark, - append("m"), - Checkpoint, - unmark(0), - Rewind, - unmark(0), - unmark(0), - ], - ]; - for (i, sample) in samples.iter().enumerate() { - let result = check_operations(sample); - assert!( - matches!(result, Ok(())), - "Reference/Test mismatch at index {}: {:?}", - i, - result - ); - } - } - - fn check_operations( - ops: &[Operation], - ) -> Result<(), TestCaseError> { - const DEPTH: u8 = 4; - let mut tree = CombinedTree::::new(); - - let mut tree_size = 0; - let mut tree_values: Vec = vec![]; - // the number of leaves in the tree at the time that a checkpoint is made - let mut tree_checkpoints: Vec = vec![]; - - for op in ops { - prop_assert_eq!(tree_size, tree_values.len()); - match op { - Append(value) => { - if tree.append(value) { - prop_assert!(tree_size < (1 << DEPTH)); - tree_size += 1; - tree_values.push(value.clone()); - } else { - prop_assert_eq!(tree_size, 1 << DEPTH); - } - } - CurrentPosition => { - if let Some(pos) = tree.current_position() { - prop_assert!(tree_size > 0); - prop_assert_eq!(tree_size - 1, pos.into()); - } - } - CurrentLeaf => { - 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) => { - if tree.get_marked_leaf(*position).is_some() { - prop_assert!(::from(*position) < tree_size); - } - } - Unmark(position) => { - tree.remove_mark(*position); - } - MarkedPositions => {} - Checkpoint => { - tree_checkpoints.push(tree_size); - tree.checkpoint(); - } - Rewind => { - if tree.rewind() { - prop_assert!(!tree_checkpoints.is_empty()); - let checkpointed_tree_size = tree_checkpoints.pop().unwrap(); - tree_values.truncate(checkpointed_tree_size); - tree_size = checkpointed_tree_size; - } - } - Authpath(position, depth) => { - if let Some(path) = tree.root(*depth).and_then(|r| tree.witness(*position, &r)) - { - let value: H = tree_values[::from(*position)].clone(); - let tree_root = tree.root(*depth); - - if tree_checkpoints.len() >= *depth { - let mut extended_tree_values = tree_values.clone(); - if *depth > 0 { - // prune the tree back to the checkpointed size. - if let Some(checkpointed_tree_size) = - tree_checkpoints.get(tree_checkpoints.len() - depth) - { - extended_tree_values.truncate(*checkpointed_tree_size); - } - } - // extend the tree with empty leaves until it is full - extended_tree_values.resize(1 << DEPTH, H::empty_leaf()); - - // compute the root - let expected_root = lazy_root::(extended_tree_values); - prop_assert_eq!(&tree_root.unwrap(), &expected_root); - - prop_assert_eq!( - &compute_root_from_witness(value, *position, &path), - &expected_root - ); - } - } - } - GarbageCollect => {} - } - } - - Ok(()) - } - - 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 - ) - ) { - check_operations(&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 - ) - ) { - check_operations::(&ops)?; - } - } -}