diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a0a95d6..e1cbe9186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - [BREAKING] `PartialMmr::open()` now returns `Option` instead of `Option` ([#787](https://github.com/0xMiden/crypto/pull/787)). - [BREAKING] Refactored BLAKE3 to use `Digest` struct, added `Digest192` type alias ([#811](https://github.com/0xMiden/crypto/pull/811)). - [BREAKING] Removed `hashbrown` dependency and `hashmaps` feature; `Map`/`Set` type aliases are now tied to the `std` feature ([#813](https://github.com/0xMiden/crypto/pull/813)). +- Fixed `LargeSmtForest::truncate` to remove emptied lineages from `non_empty_histories` ([#818](https://github.com/0xMiden/crypto/pull/818)). - [BREAKING] Renamed `NodeIndex::value()` to `NodeIndex::position()`, `NodeIndex::is_value_odd()` to `NodeIndex::is_position_odd()`, and `LeafIndex::value()` to `LeafIndex::position()` ([#814](https://github.com/0xMiden/crypto/pull/814)). - [BREAKING] Fix OOMs in Merkle/SMT deserialization ([#820](https://github.com/0xMiden/crypto/pull/820)). - Fixed `SmtForest` to remove nodes with zero reference count from store ([#821](https://github.com/0xMiden/crypto/pull/821)). diff --git a/miden-crypto/src/merkle/smt/large_forest/mod.rs b/miden-crypto/src/merkle/smt/large_forest/mod.rs index 6016026cc..ea5f482a0 100644 --- a/miden-crypto/src/merkle/smt/large_forest/mod.rs +++ b/miden-crypto/src/merkle/smt/large_forest/mod.rs @@ -526,7 +526,9 @@ impl LargeSmtForest { } }); - self.non_empty_histories.extend(newly_empty); + for l in &newly_empty { + self.non_empty_histories.remove(l); + } } } diff --git a/miden-crypto/src/merkle/smt/large_forest/tests.rs b/miden-crypto/src/merkle/smt/large_forest/tests.rs index 25ad0aa6b..b06036a58 100644 --- a/miden-crypto/src/merkle/smt/large_forest/tests.rs +++ b/miden-crypto/src/merkle/smt/large_forest/tests.rs @@ -13,16 +13,20 @@ use itertools::Itertools; use super::{Config, Result}; use crate::{ - EMPTY_WORD, Word, + EMPTY_WORD, Map, Set, Word, merkle::{ EmptySubtreeRoots, smt::{ Backend, ForestInMemoryBackend, ForestOperation, LargeSmtForest, LargeSmtForestError, LeafIndex, RootInfo, Smt, SmtForestUpdateBatch, SmtUpdateBatch, TreeId, VersionId, - large_forest::root::{LineageId, TreeEntry, TreeWithRoot}, + large_forest::{ + LineageData, + history::{History, LeafChanges, NodeChanges}, + root::{LineageId, TreeEntry, TreeWithRoot}, + }, }, }, - rand::test_utils::ContinuousRng, + rand::test_utils::{ContinuousRng, rand_value}, }; // TYPE ALIASES @@ -1112,3 +1116,93 @@ fn update_forest() -> Result<()> { Ok(()) } + +// TRUNCATION +// ================================================================================================ + +#[test] +fn truncate_removes_emptied_lineages_from_non_empty_histories() { + let lineage: LineageId = rand_value(); + let root: Word = rand_value(); + + // Build a lineage with one historical version at version 5, and a latest version of 10. + let mut history = History::empty(4); + let nodes = NodeChanges::default(); + let leaves = LeafChanges::default(); + history.add_version(rand_value(), 5, nodes, leaves).unwrap(); + assert_eq!(history.num_versions(), 1); + + let lineage_data = LineageData { + history, + latest_version: 10, + latest_root: root, + }; + + let mut lineage_map = Map::default(); + lineage_map.insert(lineage, lineage_data); + + let mut non_empty = Set::default(); + non_empty.insert(lineage); + + let mut forest = LargeSmtForest { + config: Config::default(), + backend: ForestInMemoryBackend::new(), + lineage_data: lineage_map, + non_empty_histories: non_empty, + }; + + // Sanity: the lineage is tracked as having a non-empty history. + assert!(forest.non_empty_histories.contains(&lineage)); + + // Truncate to a version >= latest_version, which clears the history entirely. + forest.truncate(10); + + // The lineage's history should now be empty, and it must have been removed from the set. + assert!( + !forest.non_empty_histories.contains(&lineage), + "emptied lineage must be removed from non_empty_histories after truncation" + ); +} + +#[test] +fn truncate_retains_non_empty_lineages_in_non_empty_histories() { + let lineage: LineageId = rand_value(); + let root: Word = rand_value(); + + // Build a lineage with two historical versions (5 and 8), latest version 15. + let mut history = History::empty(4); + let nodes = NodeChanges::default(); + let leaves = LeafChanges::default(); + history.add_version(rand_value(), 5, nodes.clone(), leaves.clone()).unwrap(); + history.add_version(rand_value(), 8, nodes, leaves).unwrap(); + assert_eq!(history.num_versions(), 2); + + let lineage_data = LineageData { + history, + latest_version: 15, + latest_root: root, + }; + + let mut lineage_map = Map::new(); + lineage_map.insert(lineage, lineage_data); + + let mut non_empty = Set::default(); + non_empty.insert(lineage); + + let mut forest = LargeSmtForest { + config: Config::default(), + backend: ForestInMemoryBackend::new(), + lineage_data: lineage_map, + non_empty_histories: non_empty, + }; + + // Truncate to version 7: removes versions older than 7, but version 8 should remain. + // Since version < latest_version (15), LineageData::truncate returns false. + forest.truncate(7); + + // The history still has data, so the lineage must stay in non_empty_histories. + assert!( + forest.non_empty_histories.contains(&lineage), + "lineage with remaining history must stay in non_empty_histories" + ); +} diff --git a/miden-crypto/src/rand/test_utils.rs b/miden-crypto/src/rand/test_utils.rs index 296a3e178..1a5bef3eb 100644 --- a/miden-crypto/src/rand/test_utils.rs +++ b/miden-crypto/src/rand/test_utils.rs @@ -53,7 +53,6 @@ fn rng_value(rng: &mut impl Rng) -> T { /// let x: u64 = rand_value(); /// let y: u128 = rand_value(); /// ``` -#[cfg(feature = "std")] pub fn rand_value() -> T { rng_value(&mut rand::rng()) }