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
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 });
}
Comment on lines +125 to 129
Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking out loud: This allows adding the same account witness twice, but that should be fine. Since PartialSmt::add_proof will error if a root mismatch occurs, the only allowed case is if the previously added account witness is the same as the current one, so we could technically return early if id_key == existing_key. Though, I'd leave things as is to avoid introducing special logic that needs to be tested, etc.


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])?;

Comment on lines +306 to +307
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add a partial_tree.track_account(witness1) here just to have a code path that adds the same witness twice (to make sure this does not error)?

// 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