Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@
- Added Ownable2Step as an Account Component ([#2572](https://github.com/0xMiden/protocol/pull/2572))
- [BREAKING] Introduced `PrivateNoteHeader` for output notes and removed `RawOutputNote::Header` variant ([#2569](https://github.com/0xMiden/protocol/pull/2569)).

### Fixes

- Fixed `PartialAccountTree::track_account` rejecting provably-empty leaves in sparse trees by handling `SmtLeaf::Empty` correctly ([#2598](https://github.com/0xMiden/protocol/pull/2598)).

## 0.13.3 (2026-01-27)

- Fixed `CLAIM` note creation to use `NetworkAccountTarget` attachment ([#2352](https://github.com/0xMiden/miden-base/pull/2352)).
Expand Down
54 changes: 47 additions & 7 deletions crates/miden-protocol/src/block/account_tree/partial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,20 @@ impl PartialAccountTree {
let id_prefix = witness.id().prefix();
let id_key = AccountIdKey::from(witness.id()).as_word();

// If a leaf with the same prefix is already tracked by this partial tree, consider it an
// If there exists a tracked leaf with a non-empty entry whose key differs from the one
// we're about to track, then two different account IDs share the same prefix, which is an
// error.
//
// We return an error even for empty leaves, because tracking the same ID prefix twice
// indicates that different IDs are attempted to be tracked. It would technically not
// violate the invariant of the tree that it only tracks zero or one entries per leaf, but
// since tracking the same ID twice should practically never happen, we return an error, out
// of an abundance of caution.
if self.smt.get_leaf(&id_key).is_ok() {
// Note that if the leaf is empty, that's fine: `PartialSmt::get_leaf` returns
// `Ok(SmtLeaf::Empty)` for any leaf position reachable through provably-empty subtrees,
// even if no proof was explicitly added for that position. In a sparse tree this covers
// most of the leaf space, so treating empty leaves as duplicates would reject nearly every
// second witness.
//
// Also note that the multiple variant cannot occur by construction of the account tree.
if let Ok(SmtLeaf::Single((existing_key, _))) = self.smt.get_leaf(&id_key)
&& id_key != existing_key
{
return Err(AccountTreeError::DuplicateIdPrefix { duplicate_prefix: id_prefix });
}

Expand Down Expand Up @@ -275,6 +280,41 @@ mod tests {
Ok(())
}

/// Verifies that tracking multiple witnesses succeeds in a sparse tree, where most leaf
/// positions are reachable through provably-empty subtrees, including `SmtLeaf::Empty`
/// leaves that are provably empty but not actually occupied.
#[test]
fn track_succeeds_for_multiple_witnesses_in_sparse_tree() -> anyhow::Result<()> {
let id0 = AccountIdBuilder::default().build_with_seed([10; 32]);
let id1 = AccountIdBuilder::default().build_with_seed([11; 32]);
let id2 = AccountIdBuilder::default().build_with_seed([12; 32]);

let commitment0 = Word::from([1, 2, 3, 4u32]);
let commitment1 = Word::from([5, 6, 7, 8u32]);

// Create a tree with only one account (very sparse).
let account_tree = AccountTree::with_entries([(id0, commitment0)])?;

// Get witnesses for one existing and two new (empty) accounts.
let witness0 = account_tree.open(id0);
let witness1 = account_tree.open(id1);
let witness2 = account_tree.open(id2);

// Building a partial tree from all three witnesses should succeed:
// id1 and id2 have empty leaves that are provably empty via the sparse tree structure,
// but they are NOT duplicates of id0.
let mut partial_tree = PartialAccountTree::with_witnesses([witness0, witness1, witness2])?;

// Verify the existing account has its commitment.
assert_eq!(partial_tree.get(id0)?, commitment0);

// We should be able to insert new state commitments for the new accounts.
partial_tree.upsert_state_commitments([(id1, commitment1)])?;
assert_eq!(partial_tree.get(id1)?, commitment1);

Ok(())
}

#[test]
fn track_fails_on_duplicate_prefix() {
// Use a raw Smt since an account tree would not allow us to get the witnesses for two
Expand Down
4 changes: 2 additions & 2 deletions crates/miden-testing/src/kernel_tests/block/header_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,10 +439,10 @@ async fn block_building_fails_on_creating_account_with_duplicate_account_id_pref

let err = block.into_header_and_body().unwrap_err();

// This should fail when we try to _track_ the same two prefixes in the partial tree.
// This should fail when we try to _insert_ the same two prefixes in the partial tree.
assert_matches!(
err,
ProposedBlockError::AccountWitnessTracking {
ProposedBlockError::AccountIdPrefixDuplicate {
source: AccountTreeError::DuplicateIdPrefix { duplicate_prefix }
} if duplicate_prefix == id0.prefix()
);
Expand Down
Loading