Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0b46cc5
Add initial impl to test integration
sergerad Jan 5, 2026
7dfd8f6
Misc fixes
sergerad Jan 8, 2026
90e7c94
Add from_elements for AccountStorageHeader
sergerad Jan 8, 2026
abca24a
Add witness impl
sergerad Jan 8, 2026
38ac256
Split fn
sergerad Jan 8, 2026
a69bb1d
Update tests
sergerad Jan 8, 2026
58d5900
Changelog
sergerad Jan 9, 2026
6411474
Merge branch 'next' of github.com:0xPolygonMiden/miden-base into serg…
sergerad Jan 9, 2026
58f9cb3
Update changelog
sergerad Jan 9, 2026
dc7bfc3
Move pub util fn
sergerad Jan 9, 2026
1123941
Fix account_id_to_smt_index
sergerad Jan 9, 2026
cd19a48
impl TryFrom<Felt> for StorageSlotType
sergerad Jan 9, 2026
1a105b9
TransactionInputError::StorageHeaderNotFound
sergerad Jan 9, 2026
5000592
Seed comment
sergerad Jan 9, 2026
6d6b15e
from_elements -> try_from_elements
sergerad Jan 9, 2026
1c71706
Rm account id arg and add doc comments
sergerad Jan 9, 2026
397f674
Simplify account_id_to_smt_index
sergerad Jan 11, 2026
cbae4a4
add foreign account slot names to tx inputs
sergerad Jan 11, 2026
237dd4d
Add serde test
sergerad Jan 11, 2026
e13ae1d
Add read_vault_asset_witnesses impl
sergerad Jan 11, 2026
2e43c90
Move tests
sergerad Jan 12, 2026
5251431
Fix comment
sergerad Jan 12, 2026
bcb87d6
RM nested map for slot names
sergerad Jan 12, 2026
7ea16e3
Fix serde tx inputs
sergerad Jan 12, 2026
1e249ff
Add from felt test
sergerad Jan 12, 2026
631edda
Simplify tests
sergerad Jan 12, 2026
e9a466d
Add read_storage_map_witness impl
sergerad Jan 12, 2026
b263e80
For each slot
sergerad Jan 12, 2026
831f13d
Lint
sergerad Jan 12, 2026
436b155
Update changelog
sergerad Jan 12, 2026
0625fdf
Merge branch 'next' into sergerad-tx-inputs-foreign-acc-inputs
bobbinth Jan 13, 2026
99a9af7
chore: minor clean-up
bobbinth Jan 13, 2026
93c8e90
Add TransactionInputsExtractionError type
sergerad Jan 13, 2026
c00dd2c
Use vault_root in read_vault_asset_witnesses
sergerad Jan 13, 2026
1c41d3a
Simplify leaf index logic
sergerad Jan 13, 2026
123054c
Rm panics
sergerad Jan 13, 2026
3a0f472
Merge branch 'next' of github.com:0xPolygonMiden/miden-base into serg…
sergerad Jan 13, 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 @@ -8,6 +8,7 @@
- [BREAKING] Allowed account components to share identical account code procedures ([#2164](https://github.com/0xMiden/miden-base/pull/2164)).
- Add `From<&ExecutedTransaction> for TransactionHeader` implementation ([#2178](https://github.com/0xMiden/miden-base/pull/2178)).
- Add `AccountId::parse()` helper function to parse both hex and bech32 formats ([#2223](https://github.com/0xMiden/miden-base/pull/2223)).
- Add `TransactionInputs::read_foreign_account_inputs` implementation ([#2246](https://github.com/0xMiden/miden-base/pull/2246)).

### Changes

Expand Down
152 changes: 151 additions & 1 deletion crates/miden-protocol/src/account/storage/header.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alloc::format;
use alloc::string::ToString;
use alloc::vec::Vec;

Expand Down Expand Up @@ -150,6 +151,55 @@ impl AccountStorageHeader {
<Self as SequentialCommit>::to_elements(self)
}

/// Reconstructs an [`AccountStorageHeader`] from field elements.
///
/// The elements are expected to be groups of 8 elements per slot:
/// `[[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE]`
pub fn try_from_elements(elements: &[Felt]) -> Result<Self, AccountError> {
if !elements.len().is_multiple_of(StorageSlot::NUM_ELEMENTS) {
return Err(AccountError::other(
"storage header elements length must be divisible by 8",
));
}

let mut slots = Vec::new();
for chunk in elements.chunks_exact(StorageSlot::NUM_ELEMENTS) {
// Parse slot type from second element.
let slot_type_felt = chunk[1];
let slot_type = slot_type_felt.try_into()?;

// Parse slot ID from third and fourth elements.
let slot_id_suffix = chunk[2];
let slot_id_prefix = chunk[3];

// Parse slot value from last 4 elements.
let slot_value = Word::new([chunk[4], chunk[5], chunk[6], chunk[7]]);

// Create a synthetic slot name.
// Note: The original slot name cannot be recovered here.
let synthetic_name = format!(
"synthetic::{}::{:016x}{:016x}",
match slot_type {
StorageSlotType::Value => "value",
StorageSlotType::Map => "map",
},
slot_id_prefix.as_int(),
slot_id_suffix.as_int()
);
let slot_name = StorageSlotName::new(synthetic_name).map_err(|err| {
AccountError::other_with_source("failed to create synthetic slot name", err)
})?;

let slot_header = StorageSlotHeader::new(slot_name, slot_type, slot_value);
slots.push(slot_header);
}

// Sort slots by ID.
slots.sort_by_key(|slot| slot.id());

Self::new(slots)
}

/// Returns the commitment to the [`AccountStorage`] this header represents.
pub fn to_commitment(&self) -> Word {
<Self as SequentialCommit>::to_commitment(self)
Expand Down Expand Up @@ -301,12 +351,15 @@ impl Deserializable for StorageSlotHeader {

#[cfg(test)]
mod tests {
use alloc::string::ToString;
use alloc::vec::Vec;

use miden_core::Felt;
use miden_core::utils::{Deserializable, Serializable};

use super::AccountStorageHeader;
use crate::Word;
use crate::account::{AccountStorage, StorageSlotType};
use crate::account::{AccountStorage, StorageSlotHeader, StorageSlotName, StorageSlotType};
use crate::testing::storage::{MOCK_MAP_SLOT, MOCK_VALUE_SLOT0, MOCK_VALUE_SLOT1};

#[test]
Expand Down Expand Up @@ -344,4 +397,101 @@ mod tests {
// assert deserialized == storage header
assert_eq!(storage_header, deserialized);
}

#[test]
fn test_to_elements_from_elements_empty() {
// Construct empty header.
let empty_header = AccountStorageHeader::new(vec![]).unwrap();
let empty_elements = empty_header.to_elements();

// Call from_elements.
let reconstructed_empty = AccountStorageHeader::try_from_elements(&empty_elements).unwrap();
assert_eq!(empty_header.slots().count(), reconstructed_empty.slots().count());
}

#[test]
fn test_to_elements_from_elements_single_slot() {
// Construct single slot header.
let slot_name1 = StorageSlotName::new("test::value::slot1".to_string()).unwrap();
let slot1 = StorageSlotHeader::new(
slot_name1,
StorageSlotType::Value,
Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]),
);

let single_slot_header = AccountStorageHeader::new(vec![slot1]).unwrap();
let single_elements = single_slot_header.to_elements();

// Call from_elements.
let reconstructed_single =
AccountStorageHeader::try_from_elements(&single_elements).unwrap();

assert_eq!(single_slot_header.slots().count(), reconstructed_single.slots().count());

// Note: The reconstructed header will have synthetic slot names, so the commitment
// will be different. Instead, we verify that the slot types and values are preserved.
let original_slot = single_slot_header.slots().next().unwrap();
let reconstructed_slot = reconstructed_single.slots().next().unwrap();
assert_eq!(original_slot.slot_type(), reconstructed_slot.slot_type());
assert_eq!(original_slot.value(), reconstructed_slot.value());
}

#[test]
fn test_to_elements_from_elements_multiple_slot() {
// Construct multi slot header.
let slot_name2 = StorageSlotName::new("test::map::slot2".to_string()).unwrap();
let slot_name3 = StorageSlotName::new("test::value::slot3".to_string()).unwrap();

let slot2 = StorageSlotHeader::new(
slot_name2,
StorageSlotType::Map,
Word::new([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]),
);
let slot3 = StorageSlotHeader::new(
slot_name3,
StorageSlotType::Value,
Word::new([Felt::new(9), Felt::new(10), Felt::new(11), Felt::new(12)]),
);

let mut slots = vec![slot2, slot3];
slots.sort_by_key(|slot| slot.id());
let multi_slot_header = AccountStorageHeader::new(slots).unwrap();
let multi_elements = multi_slot_header.to_elements();

// Call from_elements.
let reconstructed_multi = AccountStorageHeader::try_from_elements(&multi_elements).unwrap();

assert_eq!(multi_slot_header.slots().count(), reconstructed_multi.slots().count());

// Verify slot data is preserved (type and value) in the same order.
let original_slots = multi_slot_header.slots().collect::<Vec<_>>();

// Check that we have the same types and values in the elements.
for (i, chunk) in multi_elements.chunks_exact(8).enumerate() {
let original_type = original_slots[i].slot_type();
let original_value = original_slots[i].value();

let element_type = if chunk[1] == crate::ZERO {
StorageSlotType::Value
} else {
StorageSlotType::Map
};
let element_value = Word::new([chunk[4], chunk[5], chunk[6], chunk[7]]);

assert_eq!(original_type, element_type);
assert_eq!(original_value, element_value);
}
}

#[test]
fn test_from_elements_errors() {
// Test with invalid length (not divisible by 8).
let invalid_elements = vec![Felt::new(1), Felt::new(2), Felt::new(3)];
assert!(AccountStorageHeader::try_from_elements(&invalid_elements).is_err());

// Test with invalid slot type.
let mut invalid_type_elements = vec![crate::ZERO; 8];
invalid_type_elements[1] = Felt::new(5); // Invalid slot type.
assert!(AccountStorageHeader::try_from_elements(&invalid_type_elements).is_err());
}
}
16 changes: 16 additions & 0 deletions crates/miden-protocol/src/account/storage/slot/type.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use alloc::string::ToString;
use core::fmt::Display;

use miden_core::{ONE, ZERO};

use crate::utils::serde::{
ByteReader,
ByteWriter,
Expand Down Expand Up @@ -54,6 +56,20 @@ impl TryFrom<u8> for StorageSlotType {
}
}

impl TryFrom<Felt> for StorageSlotType {
type Error = AccountError;

fn try_from(value: Felt) -> Result<Self, Self::Error> {
if value == ZERO {
Ok(StorageSlotType::Value)
Comment on lines +59 to +64
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we not use impl TryFrom<u8> for StorageSlotType instead?

I think the caller should first let byte = u8::try_from(felt) and then StorageSlotType::try_from(byte). Avoids having to test the impl twice, keeping it up to date in two places, etc.

} else if value == ONE {
Ok(StorageSlotType::Map)
} else {
Err(AccountError::other("invalid storage slot type"))
}
}
}

impl Display for StorageSlotType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Expand Down
14 changes: 14 additions & 0 deletions crates/miden-protocol/src/block/account_tree/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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 Down Expand Up @@ -46,6 +48,18 @@ pub fn smt_key_to_account_id(key: Word) -> AccountId {
.expect("account tree should only contain valid IDs")
}

/// Converts an AccountId to an SMT leaf index for use with MerkleStore operations.
///
/// This function builds on [`account_id_to_smt_key()`] by converting the resulting Word
/// into a leaf index that can be used with MerkleStore::get_path().
pub fn account_id_to_smt_index(account_id: AccountId) -> LeafIndex<SMT_DEPTH> {
let key = account_id_to_smt_key(account_id);
// Convert the SMT key to a leaf index by using the first element as the index.
// This follows the same pattern as used in the SMT implementation.
let leaf_index = key.get(3).expect("words have 4 elements").as_int();
LeafIndex::new(leaf_index).expect("SMT_DEPTH is not smaller that SMT_MIN_DEPTH")
}

// ACCOUNT TREE
// ================================================================================================

Expand Down
14 changes: 14 additions & 0 deletions crates/miden-protocol/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,20 @@ pub enum TransactionInputError {
"total number of input notes is {0} which exceeds the maximum of {MAX_INPUT_NOTES_PER_TX}"
)]
TooManyInputNotes(usize),
#[error("specified foreign account id matches the transaction input's account id")]
AccountNotForeign,
#[error("foreign account data not found in advice map for account {0}")]
ForeignAccountNotFound(AccountId),
#[error("foreign account code not found for account {0}")]
ForeignAccountCodeNotFound(AccountId),
#[error("storage header data not found in advice map for account {0}")]
StorageHeaderNotFound(AccountId),
#[error("failed to handle account data")]
AccountError(#[from] AccountError),
#[error("failed to handle merkle data")]
MerkleError(#[from] MerkleError),
#[error("failed to handle account tree data")]
AccountTreeError(#[from] AccountTreeError),
}

// TRANSACTION OUTPUT ERROR
Expand Down
Loading