Skip to content

feat: add validation to PartialMmr deserialization and from_parts#812

Open
Farukest wants to merge 3 commits into0xMiden:nextfrom
Farukest:feat/partial-mmr-validation
Open

feat: add validation to PartialMmr deserialization and from_parts#812
Farukest wants to merge 3 commits into0xMiden:nextfrom
Farukest:feat/partial-mmr-validation

Conversation

@Farukest
Copy link
Contributor

Summary

  • Adds validation to PartialMmr::from_parts() to ensure consistency between components
  • Adds from_parts_unchecked() for performance-critical trusted code paths
  • Updates Deserializable implementation to use the validating constructor

Validation Checks

  • track_latest can only be true when forest has a single leaf tree
  • All node indices must be within forest bounds

Note

PartialMmr::from_parts and Deserializable did not validate consistency between components. This is a security concern when deserializing from untrusted sources (e.g., via ProvenTransaction).

Changes

  • error.rs: Added InconsistentPartialMmr error variant
  • partial.rs: from_parts() now returns Result<Self, MmrError> with validation
  • partial.rs: Added from_parts_unchecked() for trusted/performance-critical paths
  • partial.rs: Deserializable now validates via MmrPeaks::new() and from_parts()

Closes #802

) -> Result<Self, MmrError> {
let forest = peaks.forest();

// Validate track_latest: can only be true if forest has an odd element (single leaf tree)
Copy link
Contributor

Choose a reason for hiding this comment

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

The new check rejects inputs where track_latest=true but the forest has no single-leaf tree.

This breaks old data that set the flag loosely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This validation is intentional for security (untrusted deserialization from #802).
If track_latest=true but there's no single-leaf tree, the data is logically inconsistent, there's no "latest leaf" to track.
For trusted sources or migration paths, from_parts_unchecked() is available which skips all validation.
Should we add a note in the docs about this breaking change for invalid data ?

Copy link
Contributor

Choose a reason for hiding this comment

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

We should perhaps just mark the change as breaking in the Changelog.

let result = PartialMmr::from_parts(peaks_even.clone(), BTreeMap::new(), false);
assert!(result.is_ok());

// Invalid case: node index out of bounds
Copy link
Contributor

Choose a reason for hiding this comment

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

The test only checks leaf nodes. Please add tests for:

  • Index 0 (invalid)
  • Large even indices beyond the forest range
  • Bad inputs during deserialization

These matter because internal nodes are used for auth paths.

@Farukest Farukest force-pushed the feat/partial-mmr-validation branch from 2f03f9f to d9d8176 Compare February 3, 2026 14:27
@Farukest
Copy link
Contributor Author

Farukest commented Feb 3, 2026

All done @huitseeker. kindly need review again.

Copy link
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, thanks!

// For an empty forest, no nodes are valid
if !nodes.is_empty() && forest.is_empty() {
return Err(MmrError::InconsistentPartialMmr(
"nodes present but forest is empty".into(),
Copy link
Contributor

@huitseeker huitseeker Feb 4, 2026

Choose a reason for hiding this comment

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

The current validation only checks that indices are within rightmost_in_order_index(), but this includes "separator" positions between trees in multi-tree forests. For example, in a forest with 7 leaves (0b111), index 6 falls between trees but would pass validation even though it does not correspond to a real MMR node.

These separator indices are not valid for NodeMap. See:

  • track() only inserts at idx.sibling() where idx starts from a valid leaf position
  • add() only inserts at positions derived from rightmost_in_order_index() and its sibling
  • The NodeMap documentation states it contains "authentication nodes" which are always real tree nodes, never separators

Consider adding a Forest::is_valid_in_order_index() helper that checks if an index falls within an actual tree span rather than a separator gap.

let result = PartialMmr::from_parts(peaks.clone(), BTreeMap::new(), true);
assert!(result.is_ok());

// Build an MMR with 8 leaves (no single leaf tree: 0b1000)
Copy link
Contributor

Choose a reason for hiding this comment

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

The tests don't cover the case where an index is in-range but points to a separator between trees. See the comment above on that topic.

assert!(result.is_err());

// Invalid case: index 0 (which is never valid for InOrderIndex)
let mut nodes_with_zero = BTreeMap::new();
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment says the rightmost in-order index for 7 leaves is 12, but Forest::new(0b0111).rightmost_in_order_index() returns 13. The test at line 191 in tests.rs confirms this. Please fix or remove the comment to avoid confusion.

) -> Result<Self, MmrError> {
let forest = peaks.forest();

// Validate track_latest: can only be true if forest has an odd element (single leaf tree)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should perhaps just mark the change as breaking in the Changelog.

@Farukest
Copy link
Contributor Author

Farukest commented Feb 5, 2026

All done @huitseeker 👍

Copy link
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 LGTM!

@iamrecursion iamrecursion removed their request for review February 10, 2026 08:44
@huitseeker huitseeker requested a review from krushimir February 11, 2026 13:51
This commit adds validation to `PartialMmr::from_parts()` and the
`Deserializable` implementation to ensure consistency between components:

- Validates that `track_latest` is only true when forest has a single leaf tree
- Validates that all node indices are within forest bounds
- Adds `from_parts_unchecked()` for performance-critical trusted code paths
- Updates `Deserializable` to use the validating constructor

This addresses security concerns when deserializing from untrusted sources.

Closes 0xMiden#802
Address review feedback:
- Reject index 0 as invalid (InOrderIndex starts at 1)
- Check all indices against forest.rightmost_in_order_index()
- Handle empty forest case explicitly
- Add tests for index 0, large even indices, and deserialization
@Farukest Farukest force-pushed the feat/partial-mmr-validation branch from 9f3ce04 to fbd1ce9 Compare February 14, 2026 06:09
- Add Forest::is_valid_in_order_index() to check if an index points to
  an actual node (not a separator position between trees)
- Update from_parts() to reject separator indices
- Add tests for separator index validation (indices 8 and 12 for 7-leaf forest)
- Fix comment: rightmost in-order index for 7 leaves is 13, not 12
- Mark PR as [BREAKING] in CHANGELOG
@Farukest Farukest force-pushed the feat/partial-mmr-validation branch from fbd1ce9 to f78b142 Compare February 14, 2026 10:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add validation to PartialMmr deserialization and from_parts

2 participants