Skip to content

enhance(LargeSmtForest tests): expand LargeSmtForest property test coverage#886

Open
AlexWaker wants to merge 18 commits into0xMiden:nextfrom
AlexWaker:issue879
Open

enhance(LargeSmtForest tests): expand LargeSmtForest property test coverage#886
AlexWaker wants to merge 18 commits into0xMiden:nextfrom
AlexWaker:issue879

Conversation

@AlexWaker
Copy link
Copy Markdown

@AlexWaker AlexWaker commented Mar 10, 2026

This PR expands the property-based test coverage for LargeSmtForest.

Previously, the forest-level property tests were mostly limited to entries(). This change adds a broader set of reference-model-driven property tests covering core query, mutation, metadata, constructor, and truncation behavior.

The new tests now exercise:

  • query consistency for get(), entries(), and entry_count()
  • selected open() consistency checks against a reference Smt
  • metadata consistency for latest_version(), latest_root(), lineage_roots(), roots(), and root_info()
  • mutation behavior for add_lineage(), update_tree(), and update_forest()
  • failure semantics, including duplicate lineages, bad version transitions, and invalid forest batches
  • constructor behavior when initializing a forest from a pre-populated backend
  • retention and visibility semantics for with_config() and truncate()

In addition, the property test helpers and generators were refactored to make the coverage easier to extend and maintain.

Closes #879.

@AlexWaker AlexWaker changed the title enhance(): xpand LargeSmtForest property test coverage enhance(LargeSmtForest tests): expand LargeSmtForest property test coverage Mar 10, 2026
@huitseeker huitseeker self-requested a review March 18, 2026 09:10
let bad_version = forest.update_tree(lineage, version, extra_entries);
prop_assert!(bad_version.is_err());
prop_assert_eq!(
bad_version.unwrap_err().to_string(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be safer to assert the error variant instead of exact to_string() text? Matching full message strings can make this test fail on wording-only changes.

@AlexWaker
Copy link
Copy Markdown
Author

Resolved the merge conflict by aligning property_tests.rs with the upstream API changes, including the new fallible entries() usage.

Also addressed the two review comments:

  • replaced string-based error assertions with matching on concrete error variants
  • added post-error metadata checks to ensure failed updates do not change lineage/tree metadata or create new root_info entries

Finally, ran formatting checks and applied rustfmt fixes (cargo +nightly fmt --all and cargo +nightly fmt --all --check).

@AlexWaker
Copy link
Copy Markdown
Author

Would this PR need a changelog entry?

My understanding is that it probably does not, since this change only expands internal test coverage and adapts the tests to upstream API changes, without introducing any user-facing behavior change or public API change. From the current CHANGELOG.md, it also seems like entries are generally reserved for externally visible features, fixes, and breaking changes.

That said, if you would still prefer one to be added, I can update the changelog right away.

@huitseeker huitseeker added the no changelog This PR does not require an entry in the `CHANGELOG.md` file label Mar 23, 2026
@huitseeker huitseeker requested a review from iamrecursion March 23, 2026 10:35
Copy link
Copy Markdown
Contributor

@huitseeker huitseeker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is shaping up nicely! See comments inline.

TreeId::new(lineage, version),
&tree_v1,
&sample_keys,
false,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper skips open() on the historical tree by passing false here. Can we also property-test historical open() against the reference Smt? That feels like the trickiest query path in LargeSmtForest, and right now it still looks like it only has the hand-written unit test coverage in tests.rs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this should 100% test historical openings as well.

@@ -0,0 +1 @@
cc abf493f7aece0d6db6c370e3c95651effc9f46f7a2f97e2dccddacfce2afd16d No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any behavioral fixes in this PR. We usually only keep regression seeds like this when they allowed us to identify an interesting failure, which required fixing. This seems like it was just a development artifact and so does not need keeping.

Copy link
Copy Markdown
Collaborator

@iamrecursion iamrecursion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable in general! I've left a few comments in-line.

One thing I would love to see for every single potential modification is that we assert complete correctness with regards to the state change. Some of these—like the one @huitseeker commented on—do not assert the complete soundness of all propertiess should a bug arise.

Perhaps this is out of scope for this PR, but I would also be interested in expanding the FallibleEntriesBackend to be a more general mechanism for testing state coherence in the face of a number of different kinds of failure. @huitseeker, thoughts? See #911 for a separate batch of work in this vein.

};

// ENTRIES
// HELPERS
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these helpers should be moved to large_forest::test_utils like the other strategies are. I would also expect them to have doc comments, as that is the first line of understanding for someone reading the property test after a failure.


fn apply_batch(tree: &mut Smt, batch: SmtUpdateBatch) -> core::result::Result<(), TestCaseError> {
let mutations = tree
.compute_mutations(Vec::<(Word, Word)>::from(batch).into_iter())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should call batch.consume().into_iter() instead. That guarantees the correct deduplication behaviouor.

}

fn sorted_forest_entries(
forest: &LargeSmtForest<ForestInMemoryBackend>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realistically, this and all subsequent cases that take a forest should be parametric in the backend. While we currently only do these with the in-memory backend, there is no reason that has to remain the case forever.

.collect::<crate::merkle::smt::large_forest::Result<Vec<_>>>()
.map_err(to_fail)?
.into_iter()
.sorted()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of sorting them? Does the default derived Ord implementation order them correctly?

Ok(())
}

// PROPERTY TESTS
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, please reorder these property tests to match the method order in the large forest itself. It makes for easier code navigation.

TreeId::new(lineage, version),
&tree_v1,
&sample_keys,
false,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this should 100% test historical openings as well.

@iamrecursion
Copy link
Copy Markdown
Collaborator

Ended up filing #911 for my ask above.

@AlexWaker
Copy link
Copy Markdown
Author

Thanks for the review. I’ve addressed the suggestions raised in this PR, with the exception of the broader failure-injection work described in #911, which I have not tackled in this patch.

Changes made in this round:

  • Moved the shared property-test helpers into large_forest::test_utils and added doc comments to clarify their intent.
  • Updated the reference-tree helper path to use the batch’s canonicalized semantics explicitly (batch.consume().into_iter()), so the test model matches the production deduplication behavior.
  • Strengthened the failure-path assertions to check that forest metadata remains unchanged after rejected operations, including latest_version, latest_root, root_info, roots, lineage_count, and tree_count.
  • Kept error assertions variant-based rather than matching full string messages.
  • Reordered the property tests to follow the method order in LargeSmtForest more closely.
  • Made the sorting in the test helpers explicit ((key, value) ordering) and documented that it exists only to normalize unspecified iterator order for comparisons.

I also expanded the property coverage for historical open() paths, as suggested. While doing that, I found and fixed a bug in historical opening reconstruction:

  • the historical leaf reconstruction needed deterministic entry ordering before hashing, and
  • the deepest sibling path element for historical openings needed to be recomputed from the historical sibling leaf when that leaf had changed in history.

My current understanding is that this was a real bug exposed by the stronger historical open() coverage🤔. If you think this change is not the right fix, or if I’ve misunderstood the intended semantics here, I’d be very happy to dig into it further and discuss.

After these changes, the targeted large_forest test suite passes locally.

I have not implemented the more general failure-injection / “FallibleBackend” work from #911 in this PR, and I’m leaving that follow-up scoped to the separate issue.

Copy link
Copy Markdown
Contributor

@huitseeker huitseeker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for this update, this looks super nice!

@huitseeker huitseeker requested a review from iamrecursion March 24, 2026 16:38
Copy link
Copy Markdown
Collaborator

@iamrecursion iamrecursion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the bug, but we definitely need a more principled fix that doesn't incur unnecessary performance penalties for correctness.

Once my suggested fix is applied, I'd also like to see comparative benchmarks for before and after this commit, now that hot-path code has been touched.

Comment on lines +1087 to +1092
let sibling_key = Word::new([
Felt::new(0),
Felt::new(0),
Felt::new(0),
Felt::new(sibling_leaf_index.position()),
]);
Copy link
Copy Markdown
Collaborator

@iamrecursion iamrecursion Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a hack to me. My suggestion would be to add a new method to Backend that is get_leaf that simply gets the entire leaf for a given leaf index. That would also be a lot more performant than having to do the work to compute a full opening in the backend, which can be quite expensive.

Something like this.

fn get_leaf(&self, index: LeafIndex<SMT_DEPTH>) -> Option<SmtLeaf>;

// Third item should be the simulated error.
let third = iter.next();
assert_matches!(&third, Some(Err(LargeSmtForestError::Unspecified(msg))) if msg == "simulated read failure");
assert_matches!(&third, Some(Err(LargeSmtForestError::Unspecified(_))));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think removing the message assertion is the correct thing to do. We might get spurious passes for other errors that way. I would create a constant for the error message and use it at the emit site and then check for it here in the test.

// This takes the WithHistory iteration path, which will hit our fallible iterator.
let result = forest.entry_count(TreeId::new(lineage, version_1));
assert_matches!(result, Err(LargeSmtForestError::Unspecified(msg)) if msg == "simulated read failure");
assert_matches!(result, Err(LargeSmtForestError::Unspecified(_)));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

iamrecursion's suggestions
@AlexWaker AlexWaker requested a review from iamrecursion March 26, 2026 08:26
Copy link
Copy Markdown
Collaborator

@iamrecursion iamrecursion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is even better, but I'd like the approach I've outlined to be added. Please also check that the changes I've suggested (and none of the others in this PR) do not cause performance regressions, and please post before and after numbers.

Ok(tree.tree.open(&key))
}

/// Returns the leaf stored at `leaf_index` in the SMT with the specified `lineage`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should also mention something about it explicitly always returning a leaf, even if said leaf is not stored explicitly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I just saw your new comment. I’ve been busy running benchmark tests, but I’ll fix your latest suggestion shortly.

}

path.push(if is_right { left } else { right });
/// Returns the leaf stored at `leaf_index` in the SMT with the specified `lineage`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Comment on lines +269 to +274
let key = Word::new([
EMPTY_WORD[0],
EMPTY_WORD[1],
EMPTY_WORD[2],
Felt::new(leaf_index.position()),
]);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still not the way to do it. You should add a load_leaf_raw akin to this that you would just call with LeafKey { lineage, index: leaf_index.position() }.

/// Gets the leaf with the provided `key` from disk, or returns [`None`] if it is not stored.
///
/// # Errors
///
/// - [`BackendError::Internal`] if the database cannot be successfully queried.
#[inline(always)]
fn load_leaf_raw(&self, key: &LeafKey) -> Result<Option<SmtLeaf>> {
    let col = self.cf(LEAVES_CF)?;
    let key_bytes = key.to_bytes();
    let leaf_bytes = self.db.get_cf(col, key_bytes)?;
    let leaf = match leaf_bytes {
        Some(bytes) => Some(SmtLeaf::read_from_bytes_with_budget(&bytes, bytes.len())?),
        None => None,
    };

    Ok(leaf)
}

You can then use this inside load_leaf_for to avoid code duplication:

/// Gets the leaf from disk in the provided `lineage` that would contain `key`.
///
/// # Errors
///
/// - [`BackendError::Internal`] if the database cannot be successfully queried.
fn load_leaf_for(&self, lineage: LineageId, key: Word) -> Result<Option<SmtLeaf>> {
    let key = LeafKey {
        lineage,
        index: LeafIndex::from(key).position(),
    };
    self.load_leaf_raw(&key)
}

The current approach is too directly tied to the internal representation and is brittle, while this is principled.

// top of the current state.
let new_leaf = self.merge_leaves(opening.leaf(), view)?;
let new_leaf = Self::merge_leaves(opening.leaf(), view)?;
let new_path = self.merge_paths(tree.lineage(), leaf_index, opening.path(), view)?;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not technically part of this PR, but merge_paths should also be a static method rather than an instance method. It never touches self. Would you be able to fix that?

let mut entries = leaf_entries.into_iter().collect::<Vec<_>>();
// We sort the entries to ensure a consistent ordering, as the map above is a HashMap
// which does not guarantee iteration order.
let mut entries: Vec<_> = leaf_entries.into_iter().collect();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the turbofish style for the collect type.

@AlexWaker
Copy link
Copy Markdown
Author

Thanks for the suggestion @iamrecursion . I addressed this by adding a Backend::get_leaf(...) method and switching the historical path reconstruction to use that directly instead of synthesizing a sibling key and calling open() on the backend.

In particular:

  • Backend now exposes fn get_leaf(&self, lineage: LineageId, leaf_index: LeafIndex<SMT_DEPTH>) -> -Result<SmtLeaf>
  • Both the in-memory and persistent backends implement this method.
  • In merge_paths, when the deepest sibling leaf has changed in history, I now fetch the sibling leaf directly from the backend, apply the historical view with merge_leaves(...), and then recompute the path element from that historical leaf hash.
  • That removes the earlier hack, keeps the fix more principled, and avoids paying the cost of constructing a full backend opening just to recover a leaf.

I also reran the existing large_smt_forest open benchmarks against origin/next and the current branch on Mac mini M4. The current branch does appear to regress open() slightly relative to next:

  • open_full_tree: 84.527 µs on origin/next vs 87.321 µs on this branch (~3.3% slower)
  • open_historical_tree: 84.201 µs on origin/next vs 87.270 µs on this branch (~3.6% slower)

I think this level of regression is acceptable.
Screenshot 2026-03-26 at 17 13 27

@iamrecursion
Copy link
Copy Markdown
Collaborator

Hm. That regression is small, sure, but open is an operation we expect to be calling a significant amount, especially in a loop. Do you have any inkling as to why it may have regressed and/or how it could be fixed?

Are there regressions in any other benchmarks or just the open ones?

@AlexWaker
Copy link
Copy Markdown
Author

Hm. That regression is small, sure, but open is an operation we expect to be calling a significant amount, especially in a loop. Do you have any inkling as to why it may have regressed and/or how it could be fixed?

Are there regressions in any other benchmarks or just the open ones?

I just addressed the inline comments. I haven’t had time yet to dig deeply into the performance regression, but my initial assessment is that it may be caused by repeated scans of the historical change set. I’ll continue with a deeper analysis and try to fix it shortly

@iamrecursion
Copy link
Copy Markdown
Collaborator

A fix would be appreciated as this is very hot-path code.

@AlexWaker
Copy link
Copy Markdown
Author

A fix would be appreciated as this is very hot-path code.

I reran the broader large_smt_forest query benchmarks, not just the two open cases, and compared this branch against origin/next using a separate worktree. To reduce Criterion baseline noise, I cleared target/criterion before each run and repeated the comparison three times on each side.

The benchmarks I checked were:

  • large_smt_forest_persistent_open_full_tree
  • large_smt_forest_persistent_open_historical_tree
  • large_smt_forest_persistent_get_full_tree
  • large_smt_forest_persistent_get_historical_tree
  • large_smt_forest_persistent_entry_count_current_tree
  • large_smt_forest_persistent_entry_count_historical_tree
  • large_smt_forest_persistent_entries_current_tree
  • large_smt_forest_persistent_entries_historical_tree

For reference, these were the median times I got across the repeated runs.

This branch:

  • open_full_tree: 84.513 µs, 85.225 µs, 84.878 µs
  • open_historical_tree: 85.734 µs, 84.906 µs, 85.465 µs
  • get_full_tree: 5.1540 µs, 5.4865 µs, 5.1044 µs
  • get_historical_tree: 5.3200 µs, 5.0286 µs, 5.4136 µs
  • entry_count_current_tree: 25.507 ns, 25.413 ns, 25.747 ns
  • entry_count_historical_tree: 25.514 ns, 25.530 ns, 25.695 ns
  • entries_current_tree: 1.8042 ms, 1.7490 ms, 1.7797 ms
  • entries_historical_tree: 1.7686 ms, 1.7519 ms, 1.7731 ms

origin/next:

  • open_full_tree: 84.964 µs, 84.183 µs, 84.667 µs
  • open_historical_tree: 84.288 µs, 84.203 µs, 83.506 µs
  • get_full_tree: 5.3373 µs, 4.9015 µs, 4.5793 µs
  • get_historical_tree: 5.4174 µs, 5.1055 µs, 5.5000 µs
  • entry_count_current_tree: 25.879 ns, 25.938 ns, 26.052 ns
  • entry_count_historical_tree: 25.698 ns, 25.794 ns, 26.118 ns
  • entries_current_tree: 1.7864 ms, 1.7850 ms, 1.7747 ms
  • entries_historical_tree: 1.7947 ms, 1.8035 ms, 1.7789 ms

Across those repeated runs, I did not see evidence of a broad regression. The only regression signal that remained reasonably consistent was on large_smt_forest_persistent_open_historical_tree, and it was small, roughly around 1.5%. large_smt_forest_persistent_open_full_tree was effectively flat. The other query benchmarks were either flat, slightly better, or noisy enough that I would not over-interpret them as a real slowdown.

My current understanding is that this matches the code changes: the fix is specifically on the historical open() reconstruction path, where we now recompute the deepest sibling path element from the historical sibling leaf when that leaf changed in history. So if there is a real cost, it should be concentrated there rather than showing up as a broad regression across unrelated operations.

Would you consider this level of performance cost acceptable, or do you think it still needs to be optimized further?

If further optimization is needed, my current idea is a low-risk local change: during a single historical open(), we currently end up scanning the historical change set multiple times. I think this can likely be reduced by preprocessing the relevant history once per query and reusing it. In practice, the query only really cares about the queried leaf and the deepest sibling leaf, so I would try doing a single pass over the historical change set, collecting the changes relevant to those two leaves and whether the sibling leaf changed, and then feeding that preprocessed data into both merge_leaves() and merge_paths(). That should reduce repeated work without changing the semantics of the correctness fix.

Original data:

My branch:

=== pr run 1 ===
Finished bench profile [optimized] target(s) in 0.07s
Running benches/large_smt_forest.rs (target/release/deps/large_smt_forest-eba5deb85ec7af19)
Gnuplot not found, using plotters backend
large_smt_forest_persistent_open_full_tree/benchmark
time: [84.374 µs 84.513 µs 84.655 µs]
Found 7 outliers among 100 measurements (7.00%)
1 (1.00%) low mild
5 (5.00%) high mild
1 (1.00%) high severe

large_smt_forest_persistent_open_historical_tree/benchmark
time: [85.603 µs 85.734 µs 85.865 µs]
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high severe

large_smt_forest_persistent_get_full_tree/benchmark
time: [5.1428 µs 5.1540 µs 5.1640 µs]
Found 4 outliers among 100 measurements (4.00%)
4 (4.00%) low mild

large_smt_forest_persistent_get_historical_tree/benchmark
time: [5.3125 µs 5.3200 µs 5.3273 µs]
Found 2 outliers among 100 measurements (2.00%)
1 (1.00%) high mild
1 (1.00%) high severe

large_smt_forest_persistent_entry_count_current_tree/benchmark
time: [25.439 ns 25.507 ns 25.595 ns]
Found 8 outliers among 100 measurements (8.00%)
2 (2.00%) high mild
6 (6.00%) high severe

large_smt_forest_persistent_entry_count_historical_tree/benchmark
time: [25.455 ns 25.514 ns 25.601 ns]
Found 12 outliers among 100 measurements (12.00%)
6 (6.00%) high mild
6 (6.00%) high severe

large_smt_forest_persistent_entries_current_tree/benchmark
time: [1.7970 ms 1.8042 ms 1.8128 ms]
Found 8 outliers among 100 measurements (8.00%)
2 (2.00%) low mild
4 (4.00%) high mild
2 (2.00%) high severe

large_smt_forest_persistent_entries_historical_tree/benchmark
time: [1.7653 ms 1.7686 ms 1.7723 ms]
Found 3 outliers among 100 measurements (3.00%)
1 (1.00%) low mild
1 (1.00%) high mild
1 (1.00%) high severe

=== pr run 2 ===
Finished bench profile [optimized] target(s) in 0.07s
Running benches/large_smt_forest.rs (target/release/deps/large_smt_forest-eba5deb85ec7af19)
Gnuplot not found, using plotters backend
large_smt_forest_persistent_open_full_tree/benchmark
time: [84.906 µs 85.225 µs 85.623 µs]
Found 5 outliers among 100 measurements (5.00%)
2 (2.00%) high mild
3 (3.00%) high severe

large_smt_forest_persistent_open_historical_tree/benchmark
time: [84.787 µs 84.906 µs 85.024 µs]
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high severe

large_smt_forest_persistent_get_full_tree/benchmark
time: [5.4646 µs 5.4865 µs 5.5108 µs]
Found 3 outliers among 100 measurements (3.00%)
3 (3.00%) high mild

large_smt_forest_persistent_get_historical_tree/benchmark
time: [5.0124 µs 5.0286 µs 5.0441 µs]

large_smt_forest_persistent_entry_count_current_tree/benchmark
time: [25.405 ns 25.413 ns 25.422 ns]
Found 10 outliers among 100 measurements (10.00%)
8 (8.00%) high mild
2 (2.00%) high severe

large_smt_forest_persistent_entry_count_historical_tree/benchmark
time: [25.454 ns 25.530 ns 25.632 ns]
Found 13 outliers among 100 measurements (13.00%)
2 (2.00%) high mild
11 (11.00%) high severe

large_smt_forest_persistent_entries_current_tree/benchmark
time: [1.7460 ms 1.7490 ms 1.7519 ms]
Found 5 outliers among 100 measurements (5.00%)
3 (3.00%) low mild
1 (1.00%) high mild
1 (1.00%) high severe

large_smt_forest_persistent_entries_historical_tree/benchmark
time: [1.7458 ms 1.7519 ms 1.7590 ms]
Found 4 outliers among 100 measurements (4.00%)
4 (4.00%) high severe

=== pr run 3 ===
Finished bench profile [optimized] target(s) in 0.07s
Running benches/large_smt_forest.rs (target/release/deps/large_smt_forest-eba5deb85ec7af19)
Gnuplot not found, using plotters backend
large_smt_forest_persistent_open_full_tree/benchmark
time: [84.696 µs 84.878 µs 85.101 µs]
Found 6 outliers among 100 measurements (6.00%)
6 (6.00%) high mild

large_smt_forest_persistent_open_historical_tree/benchmark
time: [85.103 µs 85.465 µs 85.908 µs]
Found 6 outliers among 100 measurements (6.00%)
2 (2.00%) high mild
4 (4.00%) high severe

large_smt_forest_persistent_get_full_tree/benchmark
time: [5.0943 µs 5.1044 µs 5.1141 µs]

large_smt_forest_persistent_get_historical_tree/benchmark
time: [5.3926 µs 5.4136 µs 5.4389 µs]
Found 9 outliers among 100 measurements (9.00%)
3 (3.00%) low mild
2 (2.00%) high mild
4 (4.00%) high severe

large_smt_forest_persistent_entry_count_current_tree/benchmark
time: [25.676 ns 25.747 ns 25.847 ns]
Found 11 outliers among 100 measurements (11.00%)
3 (3.00%) high mild
8 (8.00%) high severe

large_smt_forest_persistent_entry_count_historical_tree/benchmark
time: [25.625 ns 25.695 ns 25.796 ns]
Found 11 outliers among 100 measurements (11.00%)
4 (4.00%) high mild
7 (7.00%) high severe

large_smt_forest_persistent_entries_current_tree/benchmark
time: [1.7727 ms 1.7797 ms 1.7883 ms]
Found 7 outliers among 100 measurements (7.00%)
1 (1.00%) low mild
1 (1.00%) high mild
5 (5.00%) high severe

large_smt_forest_persistent_entries_historical_tree/benchmark
time: [1.7676 ms 1.7731 ms 1.7796 ms]
Found 5 outliers among 100 measurements (5.00%)
2 (2.00%) high mild
3 (3.00%) high severe

origin/next

=== next run 1 ===
Finished bench profile [optimized] target(s) in 0.16s
Running benches/large_smt_forest.rs (target/release/deps/large_smt_forest-eba5deb85ec7af19)
Gnuplot not found, using plotters backend
large_smt_forest_persistent_open_full_tree/benchmark
time: [84.695 µs 84.964 µs 85.289 µs]
Found 5 outliers among 100 measurements (5.00%)
5 (5.00%) high severe

large_smt_forest_persistent_open_historical_tree/benchmark
time: [83.962 µs 84.288 µs 84.699 µs]
Found 6 outliers among 100 measurements (6.00%)
6 (6.00%) high severe

large_smt_forest_persistent_get_full_tree/benchmark
time: [5.3131 µs 5.3373 µs 5.3662 µs]
Found 4 outliers among 100 measurements (4.00%)
1 (1.00%) high mild
3 (3.00%) high severe

large_smt_forest_persistent_get_historical_tree/benchmark
time: [5.3964 µs 5.4174 µs 5.4400 µs]
Found 2 outliers among 100 measurements (2.00%)
1 (1.00%) high mild
1 (1.00%) high severe

large_smt_forest_persistent_entry_count_current_tree/benchmark
time: [25.763 ns 25.879 ns 26.025 ns]
Found 16 outliers among 100 measurements (16.00%)
3 (3.00%) high mild
13 (13.00%) high severe

large_smt_forest_persistent_entry_count_historical_tree/benchmark
time: [25.625 ns 25.698 ns 25.797 ns]
Found 13 outliers among 100 measurements (13.00%)
2 (2.00%) high mild
11 (11.00%) high severe

large_smt_forest_persistent_entries_current_tree/benchmark
time: [1.7818 ms 1.7864 ms 1.7918 ms]
Found 7 outliers among 100 measurements (7.00%)
5 (5.00%) high mild
2 (2.00%) high severe

large_smt_forest_persistent_entries_historical_tree/benchmark
time: [1.7894 ms 1.7947 ms 1.8011 ms]
Found 6 outliers among 100 measurements (6.00%)
1 (1.00%) high mild
5 (5.00%) high severe

=== next run 2 ===
Finished bench profile [optimized] target(s) in 0.27s
Running benches/large_smt_forest.rs (target/release/deps/large_smt_forest-eba5deb85ec7af19)
Gnuplot not found, using plotters backend
large_smt_forest_persistent_open_full_tree/benchmark
time: [83.929 µs 84.183 µs 84.498 µs]
Found 5 outliers among 100 measurements (5.00%)
3 (3.00%) high mild
2 (2.00%) high severe

large_smt_forest_persistent_open_historical_tree/benchmark
time: [83.901 µs 84.203 µs 84.567 µs]
Found 10 outliers among 100 measurements (10.00%)
5 (5.00%) high mild
5 (5.00%) high severe

large_smt_forest_persistent_get_full_tree/benchmark
time: [4.8870 µs 4.9015 µs 4.9181 µs]
Found 3 outliers among 100 measurements (3.00%)
2 (2.00%) high mild
1 (1.00%) high severe

large_smt_forest_persistent_get_historical_tree/benchmark
time: [5.0809 µs 5.1055 µs 5.1336 µs]
Found 9 outliers among 100 measurements (9.00%)
7 (7.00%) high mild
2 (2.00%) high severe

large_smt_forest_persistent_entry_count_current_tree/benchmark
time: [25.856 ns 25.938 ns 26.049 ns]
Found 7 outliers among 100 measurements (7.00%)
1 (1.00%) low mild
2 (2.00%) high mild
4 (4.00%) high severe

large_smt_forest_persistent_entry_count_historical_tree/benchmark
time: [25.681 ns 25.794 ns 25.945 ns]
Found 11 outliers among 100 measurements (11.00%)
1 (1.00%) low mild
4 (4.00%) high mild
6 (6.00%) high severe

large_smt_forest_persistent_entries_current_tree/benchmark
time: [1.7789 ms 1.7850 ms 1.7921 ms]
Found 5 outliers among 100 measurements (5.00%)
1 (1.00%) high mild
4 (4.00%) high severe

large_smt_forest_persistent_entries_historical_tree/benchmark
time: [1.7926 ms 1.8035 ms 1.8191 ms]
Found 6 outliers among 100 measurements (6.00%)
1 (1.00%) low mild
2 (2.00%) high mild
3 (3.00%) high severe

=== next run 3 ===
Finished bench profile [optimized] target(s) in 0.26s
Running benches/large_smt_forest.rs (target/release/deps/large_smt_forest-eba5deb85ec7af19)
Gnuplot not found, using plotters backend
large_smt_forest_persistent_open_full_tree/benchmark
time: [84.518 µs 84.667 µs 84.821 µs]

large_smt_forest_persistent_open_historical_tree/benchmark
time: [83.355 µs 83.506 µs 83.658 µs]

large_smt_forest_persistent_get_full_tree/benchmark
time: [4.5600 µs 4.5793 µs 4.6091 µs]
Found 4 outliers among 100 measurements (4.00%)
2 (2.00%) high mild
2 (2.00%) high severe

large_smt_forest_persistent_get_historical_tree/benchmark
time: [5.4188 µs 5.5000 µs 5.6606 µs]
Found 6 outliers among 100 measurements (6.00%)
3 (3.00%) high mild
3 (3.00%) high severe

large_smt_forest_persistent_entry_count_current_tree/benchmark
time: [25.981 ns 26.052 ns 26.150 ns]
Found 16 outliers among 100 measurements (16.00%)
7 (7.00%) high mild
9 (9.00%) high severe

large_smt_forest_persistent_entry_count_historical_tree/benchmark
time: [25.991 ns 26.118 ns 26.278 ns]
Found 14 outliers among 100 measurements (14.00%)
6 (6.00%) high mild
8 (8.00%) high severe

large_smt_forest_persistent_entries_current_tree/benchmark
time: [1.7691 ms 1.7747 ms 1.7811 ms]
Found 13 outliers among 100 measurements (13.00%)
5 (5.00%) low mild
5 (5.00%) high mild
3 (3.00%) high severe

large_smt_forest_persistent_entries_historical_tree/benchmark
time: [1.7714 ms 1.7789 ms 1.7875 ms]
Found 8 outliers among 100 measurements (8.00%)
1 (1.00%) low mild
2 (2.00%) high mild
5 (5.00%) high severe

@iamrecursion
Copy link
Copy Markdown
Collaborator

I think that performance delta is acceptable for this PR. Would you file an issue for your performance improvement idea? It's worth exploring regardless.

@AlexWaker
Copy link
Copy Markdown
Author

I think that performance delta is acceptable for this PR. Would you file an issue for your performance improvement idea? It's worth exploring regardless.

Thanks, I agree that this performance delta is acceptable for this PR.

I’ve filed a follow-up issue for the optimization work here: #923. I’ll continue the performance investigation there and look into optimizing the repeated history-scan path afterward.

@huitseeker huitseeker requested a review from iamrecursion March 28, 2026 09:04
Copy link
Copy Markdown
Collaborator

@iamrecursion iamrecursion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Just a couple of minor documentation fixes please!


/// Returns the leaf stored at `leaf_index` in the SMT with the specified `lineage`.
/// Returns the leaf stored at `leaf_index` in the SMT with the specified `lineage`. If no
/// leaf is explicitly stored at the given index, an empty leaf for that index is returned.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// leaf is explicitly stored at the given index, an empty leaf for that index is returned.
/// Returns the leaf stored at `leaf_index` in the SMT with the specified `lineage`.
///
/// If no leaf is explicitly stored at the given index, an empty leaf for that index
/// is returned.

You may need to wrap this correctly by hand. I wrote it in the GH PR editor. :'D

}

/// Returns the leaf stored at `leaf_index` in the SMT with the specified `lineage`.
/// Returns the leaf stored at `leaf_index` in the SMT with the specified `lineage`. If no
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the suggested doc fix here too.

@AlexWaker
Copy link
Copy Markdown
Author

I resolved the conflict by keeping the incoming side, since historical entry_count now uses the stored count directly instead of iterating through entries. That makes the success assertion the correct behavior here. I will pay closer attention to these doc formatting nits details going forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no changelog This PR does not require an entry in the `CHANGELOG.md` file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve property tests for LargeSmtForest

3 participants