Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- [BREAKING] Refactored BLAKE3 to use `Digest<N>` 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)).

## 0.22.2 (2026-02-01)
Expand Down
4 changes: 3 additions & 1 deletion miden-crypto/src/merkle/smt/large_forest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@ impl<B: Backend> LargeSmtForest<B> {
}
});

self.non_empty_histories.extend(newly_empty);
for l in &newly_empty {
self.non_empty_histories.remove(l);
}
}
}

Expand Down
160 changes: 160 additions & 0 deletions miden-crypto/src/merkle/smt/large_forest/tests.rs
Original file line number Diff line number Diff line change
@@ -1 +1,161 @@
//! This module contains the handwritten tests for the SMT forest.

#![cfg(all(test, feature = "std"))]

use alloc::vec::Vec;

use super::{
LargeSmtForest, LineageData,
backend::{self, Backend, MutationSet},
history::{History, LeafChanges, NodeChanges},
operation::{SmtForestUpdateBatch, SmtUpdateBatch},
root::{LineageId, RootValue, TreeEntry, TreeId, VersionId},
};
use crate::{Map, Set, Word, merkle::smt::SmtProof, rand::test_utils::rand_value};

// MOCK BACKEND
// ================================================================================================

/// A minimal mock backend for testing forest methods that do not touch the backend.
#[derive(Debug)]
struct MockBackend;

impl Backend for MockBackend {
fn open(&self, _: LineageId, _: Word) -> backend::Result<SmtProof> {
unimplemented!("not needed for this test")
}

fn get(&self, _: LineageId, _: Word) -> backend::Result<Option<Word>> {
unimplemented!("not needed for this test")
}

fn version(&self, _: LineageId) -> backend::Result<VersionId> {
unimplemented!("not needed for this test")
}

fn lineages(&self) -> backend::Result<impl Iterator<Item = LineageId>> {
Ok(core::iter::empty())
}

fn trees(&self) -> backend::Result<impl Iterator<Item = (TreeId, RootValue)>> {
Ok(core::iter::empty())
}

fn entry_count(&self, _: TreeId) -> backend::Result<usize> {
unimplemented!("not needed for this test")
}

fn entries(&self, _: TreeId) -> backend::Result<impl Iterator<Item = TreeEntry>> {
Ok(core::iter::empty())
}

fn update_tree(
&mut self,
_: LineageId,
_: VersionId,
_: SmtUpdateBatch,
) -> backend::Result<MutationSet> {
unimplemented!("not needed for this test")
}

fn update_forest(
&mut self,
_: VersionId,
_: SmtForestUpdateBatch,
) -> backend::Result<Vec<MutationSet>> {
unimplemented!("not needed for this test")
}
}

// TESTS
// ================================================================================================

/// Regression test: `truncate` must remove lineages whose histories become empty from
/// `non_empty_histories`. Previously, `extend` was used instead of `remove`, which caused emptied
/// lineages to be incorrectly retained (or even duplicated) in the set.
#[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 {
backend: MockBackend,
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"
);
}

/// Verifies that `truncate` retains lineages in `non_empty_histories` when their history is only
/// partially truncated and still contains versions.
#[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::default();
lineage_map.insert(lineage, lineage_data);

let mut non_empty = Set::default();
non_empty.insert(lineage);

let mut forest = LargeSmtForest {
backend: MockBackend,
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"
);
}