Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
15e97a2
Introducing a dedicated AccountIdKey type to unify and centralize all…
swaploard Feb 23, 2026
2975792
Merge branch '0xMiden:next' into refactor(account)--introducing-Accou…
swaploard Feb 23, 2026
2e23665
changelog for introduce AccountIdKey type
swaploard Feb 23, 2026
275beb5
refactor: clean up comments and improve code readability in AccountId…
swaploard Feb 23, 2026
4d60a5a
Merge branch 'next' into refactor(account)--introducing-AccountIdKey
swaploard Feb 26, 2026
b9034ff
Merge branch 'next' into refactor(account)--introducing-AccountIdKey
bobbinth Feb 27, 2026
671bb0c
Merge branch 'next' into refactor(account)--introducing-AccountIdKey
swaploard Mar 4, 2026
12bc8be
refactor: update AccountIdKey conversion method and clean up imports
swaploard Mar 4, 2026
89cf428
Merge branch 'next' into refactor(account)--introducing-AccountIdKey
swaploard Mar 4, 2026
c50f5dc
Merge branch 'next' into refactor(account)--introducing-AccountIdKey
swaploard Mar 4, 2026
ae80bac
refactor: reorganize AccountIdKey indices and clean up related code
swaploard Mar 6, 2026
d35cda6
Merge branch 'next' into refactor(account)--introducing-AccountIdKey
swaploard Mar 6, 2026
9bd1b89
Apply suggestions from code review
PhilippGackstatter Mar 6, 2026
f6ff249
Update crates/miden-protocol/src/block/account_tree/account_id_key.rs
PhilippGackstatter Mar 6, 2026
7d73320
Update crates/miden-protocol/src/block/account_tree/account_id_key.rs
PhilippGackstatter Mar 6, 2026
16bf071
feat: validate account ID in `AccountTree::new`
PhilippGackstatter Mar 6, 2026
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 @@ -69,6 +69,7 @@
- [BREAKING] Renamed `AccountHeader::commitment`, `Account::commitment` and `PartialAccount::commitment` to `to_commitment` ([#2442](https://github.com/0xMiden/miden-base/pull/2442)).
- [BREAKING] Remove `BlockSigner` trait ([#2447](https://github.com/0xMiden/miden-base/pull/2447)).
- Updated account schema commitment construction to accept borrowed schema iterators; added extension trait to enable `AccountBuilder::with_schema_commitment()` helper ([#2419](https://github.com/0xMiden/miden-base/pull/2419)).
- Introducing a dedicated AccountIdKey type to unify and centralize all AccountId → SMT and advice-map key conversions ([#2495](https://github.com/0xMiden/miden-base/pull/2495)).
- [BREAKING] Renamed `SchemaTypeId` to `SchemaType` ([#2494](https://github.com/0xMiden/miden-base/pull/2494)).
- Updated stale `miden-base` references to `protocol` across docs, READMEs, code comments, and Cargo.toml repository URL ([#2503](https://github.com/0xMiden/protocol/pull/2503)).
- [BREAKING] Reverse the order of the transaction summary on the stack ([#2512](https://github.com/0xMiden/miden-base/pull/2512)).
Expand Down
158 changes: 158 additions & 0 deletions crates/miden-protocol/src/block/account_tree/account_id_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use miden_crypto::merkle::smt::LeafIndex;

use super::AccountId;
use crate::Word;
use crate::block::account_tree::AccountIdPrefix;
use crate::crypto::merkle::smt::SMT_DEPTH;
use crate::errors::AccountIdError;

/// The account ID encoded as a key for use in AccountTree and advice maps in
/// `TransactionAdviceInputs`.
///
/// Canonical word layout:
///
/// [0, 0, suffix, prefix]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AccountIdKey(AccountId);

impl AccountIdKey {
// Indices in the word layout where the prefix and suffix are stored.
const KEY_SUFFIX_IDX: usize = 2;
const KEY_PREFIX_IDX: usize = 3;

/// Create from AccountId
pub fn new(id: AccountId) -> Self {
Self(id)
}

/// Returns the underlying AccountId
pub fn account_id(&self) -> AccountId {
self.0
}

// SMT WORD REPRESENTATION
//---------------------------------------------------------------------------------------------------

/// Returns `[0, 0, suffix, prefix]`
pub fn as_word(&self) -> Word {
let mut key = Word::empty();

key[Self::KEY_SUFFIX_IDX] = self.0.suffix();
key[Self::KEY_PREFIX_IDX] = self.0.prefix().as_felt();

key
}

/// Construct from SMT word representation.
///
/// Validates structure before converting.
pub fn try_from_word(word: Word) -> Result<AccountId, AccountIdError> {
AccountId::try_from_elements(word[Self::KEY_SUFFIX_IDX], word[Self::KEY_PREFIX_IDX])
}

// LEAF INDEX
//---------------------------------------------------------------------------------------------------

/// Converts to SMT leaf index used by AccountTree
pub fn to_leaf_index(&self) -> LeafIndex<SMT_DEPTH> {
LeafIndex::from(self.as_word())
}

}

impl From<AccountId> for AccountIdKey {
fn from(id: AccountId) -> Self {
Self(id)
}
}

// TESTS
//---------------------------------------------------------------------------------------------------

#[cfg(test)]
mod tests {
Comment on lines +72 to +73
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: let's add TESTS section separator above this line.


use miden_core::ZERO;

use super::{AccountId, *};
use crate::account::{AccountIdVersion, AccountStorageMode, AccountType};
#[test]
fn test_as_word_layout() {
let id = AccountId::dummy(
[1u8; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let key = AccountIdKey::from(id);
let word = key.as_word();

assert_eq!(word[0], ZERO);
assert_eq!(word[1], ZERO);
assert_eq!(word[2], id.suffix());
assert_eq!(word[3], id.prefix().as_felt());
}

#[test]
fn test_roundtrip_word_conversion() {
let id = AccountId::dummy(
[1u8; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);

let key = AccountIdKey::from(id);
let recovered =
AccountIdKey::try_from_word(key.as_word()).expect("valid account id conversion");

assert_eq!(id, recovered);
}

#[test]
fn test_leaf_index_consistency() {
let id = AccountId::dummy(
[1u8; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let key = AccountIdKey::from(id);

let idx1 = key.to_leaf_index();
let idx2 = key.to_leaf_index();

assert_eq!(idx1, idx2);
}

#[test]
fn test_from_conversion() {
let id = AccountId::dummy(
[1u8; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let key: AccountIdKey = id.into();

assert_eq!(key.account_id(), id);
}

#[test]
fn test_multiple_roundtrips() {
for _ in 0..100 {
let id = AccountId::dummy(
[1u8; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let key = AccountIdKey::from(id);

let recovered =
AccountIdKey::try_from_word(key.as_word()).expect("valid account id conversion");

assert_eq!(id, recovered);
}
}
}
4 changes: 2 additions & 2 deletions crates/miden-protocol/src/block/account_tree/backend.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use alloc::boxed::Box;
use alloc::vec::Vec;

use super::{AccountId, AccountIdPrefix, AccountTree, AccountTreeError, account_id_to_smt_key};
use super::{AccountId, AccountIdKey, AccountIdPrefix, AccountTree, AccountTreeError};
use crate::Word;
use crate::crypto::merkle::MerkleError;
#[cfg(feature = "std")]
Expand Down Expand Up @@ -203,7 +203,7 @@ impl AccountTree<Smt> {
let smt = Smt::with_entries(
entries
.into_iter()
.map(|(id, commitment)| (account_id_to_smt_key(id), commitment)),
.map(|(id, commitment)| (AccountIdKey::from(id).as_word(), commitment)),
)
.map_err(|err| {
let MerkleError::DuplicateValuesForIndex(leaf_idx) = err else {
Expand Down
91 changes: 40 additions & 51 deletions crates/miden-protocol/src/block/account_tree/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use alloc::string::ToString;
use alloc::vec::Vec;

use miden_crypto::merkle::smt::LeafIndex;

use crate::Word;
use crate::account::{AccountId, AccountIdPrefix};
use crate::crypto::merkle::MerkleError;
Expand All @@ -25,39 +23,8 @@ pub use witness::AccountWitness;
mod backend;
pub use backend::AccountTreeBackend;

// FREE HELPER FUNCTIONS
// ================================================================================================
// These module-level functions provide conversions between AccountIds and SMT keys.
// They avoid the need for awkward syntax like account_id_to_smt_key().

const KEY_SUFFIX_IDX: usize = 2;
const KEY_PREFIX_IDX: usize = 3;

/// Converts an [`AccountId`] to an SMT key for use in account trees.
///
/// The key is constructed with the account ID suffix at index 2 and prefix at index 3.
pub fn account_id_to_smt_key(account_id: AccountId) -> Word {
let mut key = Word::empty();
key[KEY_SUFFIX_IDX] = account_id.suffix();
key[KEY_PREFIX_IDX] = account_id.prefix().as_felt();
key
}

/// Recovers an [`AccountId`] from an SMT key.
///
/// # Panics
///
/// Panics if the key does not represent a valid account ID. This should never happen when used
/// with keys from account trees, as the tree only stores valid IDs.
pub fn smt_key_to_account_id(key: Word) -> AccountId {
AccountId::try_from_elements(key[KEY_SUFFIX_IDX], key[KEY_PREFIX_IDX])
.expect("account tree should only contain valid IDs")
}

/// Converts an AccountId to an SMT leaf index for use with MerkleStore operations.
pub fn account_id_to_smt_index(account_id: AccountId) -> LeafIndex<SMT_DEPTH> {
account_id_to_smt_key(account_id).into()
}
mod account_id_key;
pub use account_id_key::AccountIdKey;

// ACCOUNT TREE
// ================================================================================================
Expand Down Expand Up @@ -110,7 +77,8 @@ where
/// # Errors
///
/// Returns an error if:
/// - The SMT contains duplicate account ID prefixes
/// - The SMT contains invalid account IDs.
/// - The SMT contains duplicate account ID prefixes.
pub fn new(smt: S) -> Result<Self, AccountTreeError> {
for (_leaf_idx, leaf) in smt.leaves() {
match leaf {
Expand All @@ -120,13 +88,19 @@ where
},
SmtLeaf::Single((key, _)) => {
// Single entry is good - verify it's a valid account ID
smt_key_to_account_id(key);
AccountIdKey::try_from_word(key).map_err(|err| {
AccountTreeError::InvalidAccountIdKey { key, source: err }
})?;
},
SmtLeaf::Multiple(entries) => {
// Multiple entries means duplicate prefixes
// Extract one of the keys to identify the duplicate prefix
if let Some((key, _)) = entries.first() {
let account_id = smt_key_to_account_id(*key);
let key = *key;
let account_id = AccountIdKey::try_from_word(key).map_err(|err| {
AccountTreeError::InvalidAccountIdKey { key, source: err }
})?;

return Err(AccountTreeError::DuplicateIdPrefix {
duplicate_prefix: account_id.prefix(),
});
Expand Down Expand Up @@ -164,15 +138,15 @@ where
///
/// Panics if the SMT backend fails to open the leaf (only possible with `LargeSmt` backend).
pub fn open(&self, account_id: AccountId) -> AccountWitness {
let key = account_id_to_smt_key(account_id);
let key = AccountIdKey::from(account_id).as_word();
let proof = self.smt.open(&key);

AccountWitness::from_smt_proof(account_id, proof)
}

/// Returns the current state commitment of the given account ID.
pub fn get(&self, account_id: AccountId) -> Word {
let key = account_id_to_smt_key(account_id);
let key = AccountIdKey::from(account_id).as_word();
self.smt.get_value(&key)
}

Expand Down Expand Up @@ -240,7 +214,7 @@ where
.compute_mutations(Vec::from_iter(
account_commitments
.into_iter()
.map(|(id, commitment)| (account_id_to_smt_key(id), commitment)),
.map(|(id, commitment)| (AccountIdKey::from(id).as_word(), commitment)),
))
.map_err(AccountTreeError::ComputeMutations)?;

Expand All @@ -254,7 +228,9 @@ where
// valid. If it does not match, then we would insert a duplicate.
if existing_key != *id_key {
return Err(AccountTreeError::DuplicateIdPrefix {
duplicate_prefix: smt_key_to_account_id(*id_key).prefix(),
duplicate_prefix: AccountIdKey::try_from_word(*id_key)
.expect("account tree should only contain valid IDs")
.prefix(),
});
}
},
Expand Down Expand Up @@ -287,7 +263,7 @@ where
account_id: AccountId,
state_commitment: Word,
) -> Result<Word, AccountTreeError> {
let key = account_id_to_smt_key(account_id);
let key = AccountIdKey::from(account_id).as_word();
// SAFETY: account tree should not contain multi-entry leaves and so the maximum number
// of entries per leaf should never be exceeded.
let prev_value = self.smt.insert(key, state_commitment)
Expand Down Expand Up @@ -378,9 +354,10 @@ impl Deserializable for AccountTree {
}

// Create the SMT with validated entries
let smt =
Smt::with_entries(entries.into_iter().map(|(k, v)| (account_id_to_smt_key(k), v)))
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
let smt = Smt::with_entries(
entries.into_iter().map(|(k, v)| (AccountIdKey::from(k).as_word(), v)),
)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
Ok(Self::new_unchecked(smt))
}
}
Expand Down Expand Up @@ -562,7 +539,7 @@ pub(super) mod tests {
assert_eq!(tree.num_accounts(), 2);

for id in [id0, id1] {
let proof = tree.smt.open(&account_id_to_smt_key(id));
let proof = tree.smt.open(&AccountIdKey::from(id).as_word());
let (control_path, control_leaf) = proof.into_parts();
let witness = tree.open(id);

Expand Down Expand Up @@ -606,7 +583,10 @@ pub(super) mod tests {
// Create AccountTree with LargeSmt backend
let tree = LargeSmt::<MemoryStorage>::with_entries(
MemoryStorage::default(),
[(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)],
[
(AccountIdKey::from(id0).as_word(), digest0),
(AccountIdKey::from(id1).as_word(), digest1),
],
)
.map(AccountTree::new_unchecked)
.unwrap();
Expand All @@ -623,7 +603,10 @@ pub(super) mod tests {
// Test mutations
let mut tree_mut = LargeSmt::<MemoryStorage>::with_entries(
MemoryStorage::default(),
[(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)],
[
(AccountIdKey::from(id0).as_word(), digest0),
(AccountIdKey::from(id1).as_word(), digest1),
],
)
.map(AccountTree::new_unchecked)
.unwrap();
Expand Down Expand Up @@ -672,7 +655,10 @@ pub(super) mod tests {

let mut tree = LargeSmt::with_entries(
MemoryStorage::default(),
[(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)],
[
(AccountIdKey::from(id0).as_word(), digest0),
(AccountIdKey::from(id1).as_word(), digest1),
],
)
.map(AccountTree::new_unchecked)
.unwrap();
Expand Down Expand Up @@ -703,7 +689,10 @@ pub(super) mod tests {
// Create tree with LargeSmt backend
let large_tree = LargeSmt::with_entries(
MemoryStorage::default(),
[(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)],
[
(AccountIdKey::from(id0).as_word(), digest0),
(AccountIdKey::from(id1).as_word(), digest1),
],
)
.map(AccountTree::new_unchecked)
.unwrap();
Expand Down
Loading
Loading