Skip to main content

miden_protocol/account/storage/
header.rs

1use alloc::collections::BTreeMap;
2use alloc::format;
3use alloc::string::ToString;
4use alloc::vec::Vec;
5
6use super::map::EMPTY_STORAGE_MAP_ROOT;
7use super::{AccountStorage, Felt, StorageSlotType, Word};
8use crate::ZERO;
9use crate::account::{StorageSlot, StorageSlotId, StorageSlotName};
10use crate::crypto::SequentialCommit;
11use crate::errors::AccountError;
12use crate::utils::serde::{
13    ByteReader,
14    ByteWriter,
15    Deserializable,
16    DeserializationError,
17    Serializable,
18};
19
20// ACCOUNT STORAGE HEADER
21// ================================================================================================
22
23/// The header of an [`AccountStorage`], storing only the slot name, slot type and value of each
24/// storage slot.
25///
26/// The stored value differs based on the slot type:
27/// - [`StorageSlotType::Value`]: The value of the slot itself.
28/// - [`StorageSlotType::Map`]: The root of the SMT that represents the storage map.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct AccountStorageHeader {
31    slots: Vec<StorageSlotHeader>,
32}
33
34impl AccountStorageHeader {
35    // CONSTRUCTOR
36    // --------------------------------------------------------------------------------------------
37
38    /// Returns a new instance of account storage header initialized with the provided slots.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if:
43    /// - The number of provided slots is greater than [`AccountStorage::MAX_NUM_STORAGE_SLOTS`].
44    /// - The slots are not sorted by [`StorageSlotId`].
45    /// - There are multiple storage slots with the same [`StorageSlotName`].
46    pub fn new(slots: Vec<StorageSlotHeader>) -> Result<Self, AccountError> {
47        if slots.len() > AccountStorage::MAX_NUM_STORAGE_SLOTS {
48            return Err(AccountError::StorageTooManySlots(slots.len() as u64));
49        }
50
51        if !slots.is_sorted_by_key(|slot| slot.id()) {
52            return Err(AccountError::UnsortedStorageSlots);
53        }
54
55        // Check for slot name uniqueness by checking each neighboring slot's IDs. This is
56        // sufficient because the slots are sorted.
57        for slots in slots.windows(2) {
58            if slots[0].id() == slots[1].id() {
59                return Err(AccountError::DuplicateStorageSlotName(slots[0].name().clone()));
60            }
61        }
62
63        Ok(Self { slots })
64    }
65
66    /// Returns a new instance of account storage header initialized with the provided slot tuples.
67    ///
68    /// This is a convenience method that converts tuples to [`StorageSlotHeader`]s.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if:
73    /// - The number of provided slots is greater than [`AccountStorage::MAX_NUM_STORAGE_SLOTS`].
74    /// - The slots are not sorted by [`StorageSlotId`].
75    #[cfg(any(feature = "testing", test))]
76    pub fn from_tuples(
77        slots: Vec<(StorageSlotName, StorageSlotType, Word)>,
78    ) -> Result<Self, AccountError> {
79        let slots = slots
80            .into_iter()
81            .map(|(name, slot_type, value)| StorageSlotHeader::new(name, slot_type, value))
82            .collect();
83
84        Self::new(slots)
85    }
86
87    // PUBLIC ACCESSORS
88    // --------------------------------------------------------------------------------------------
89
90    /// Returns an iterator over the storage header slots.
91    pub fn slots(&self) -> impl Iterator<Item = &StorageSlotHeader> {
92        self.slots.iter()
93    }
94
95    /// Returns an iterator over the storage header map slots.
96    pub fn map_slot_roots(&self) -> impl Iterator<Item = Word> + '_ {
97        self.slots.iter().filter_map(|slot| match slot.slot_type() {
98            StorageSlotType::Value => None,
99            StorageSlotType::Map => Some(slot.value()),
100        })
101    }
102
103    /// Returns the number of slots contained in the storage header.
104    pub fn num_slots(&self) -> u8 {
105        // SAFETY: The constructors of this type ensure this value fits in a u8.
106        self.slots.len() as u8
107    }
108
109    /// Returns the storage slot header for the slot with the given name.
110    ///
111    /// Returns `None` if a slot with the provided name does not exist.
112    pub fn find_slot_header_by_name(
113        &self,
114        slot_name: &StorageSlotName,
115    ) -> Option<&StorageSlotHeader> {
116        self.find_slot_header_by_id(slot_name.id())
117    }
118
119    /// Returns the storage slot header for the slot with the given ID.
120    ///
121    /// Returns `None` if a slot with the provided slot ID does not exist.
122    pub fn find_slot_header_by_id(&self, slot_id: StorageSlotId) -> Option<&StorageSlotHeader> {
123        self.slots.iter().find(|slot| slot.id() == slot_id)
124    }
125
126    /// Indicates whether the slot with the given `name` is a map slot.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if:
131    /// - a slot with the provided name does not exist.
132    pub fn is_map_slot(&self, name: &StorageSlotName) -> Result<bool, AccountError> {
133        match self
134            .find_slot_header_by_name(name)
135            .ok_or(AccountError::StorageSlotNameNotFound { slot_name: name.clone() })?
136            .slot_type()
137        {
138            StorageSlotType::Map => Ok(true),
139            StorageSlotType::Value => Ok(false),
140        }
141    }
142
143    /// Converts storage slots of this account storage header into a vector of field elements.
144    ///
145    /// This is done by first converting each storage slot into exactly 8 elements as follows:
146    ///
147    /// ```text
148    /// [[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE]
149    /// ```
150    ///
151    /// And then concatenating the resulting elements into a single vector.
152    pub fn to_elements(&self) -> Vec<Felt> {
153        <Self as SequentialCommit>::to_elements(self)
154    }
155
156    /// Reconstructs an [`AccountStorageHeader`] from field elements with provided slot names.
157    ///
158    /// The elements are expected to be groups of 8 elements per slot:
159    /// `[[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE]`
160    pub fn try_from_elements(
161        elements: &[Felt],
162        slot_names: &BTreeMap<StorageSlotId, StorageSlotName>,
163    ) -> Result<Self, AccountError> {
164        if !elements.len().is_multiple_of(StorageSlot::NUM_ELEMENTS) {
165            return Err(AccountError::other(
166                "storage header elements length must be divisible by 8",
167            ));
168        }
169
170        let mut slots = Vec::new();
171        for chunk in elements.chunks_exact(StorageSlot::NUM_ELEMENTS) {
172            // Parse slot type from second element.
173            let slot_type_felt = chunk[1];
174            let slot_type = slot_type_felt.try_into()?;
175
176            // Parse slot ID from third and fourth elements.
177            let slot_id_suffix = chunk[2];
178            let slot_id_prefix = chunk[3];
179            let parsed_slot_id = StorageSlotId::new(slot_id_suffix, slot_id_prefix);
180
181            // Retrieve slot name from the map.
182            let slot_name = slot_names.get(&parsed_slot_id).cloned().ok_or(AccountError::other(
183                format!("slot name not found for slot ID {}", parsed_slot_id),
184            ))?;
185
186            // Parse slot value from last 4 elements.
187            let slot_value = Word::new([chunk[4], chunk[5], chunk[6], chunk[7]]);
188
189            let slot_header = StorageSlotHeader::new(slot_name, slot_type, slot_value);
190            slots.push(slot_header);
191        }
192
193        // Sort slots by ID.
194        slots.sort_by_key(|slot| slot.id());
195
196        Self::new(slots)
197    }
198
199    /// Returns the commitment to the [`AccountStorage`] this header represents.
200    pub fn to_commitment(&self) -> Word {
201        <Self as SequentialCommit>::to_commitment(self)
202    }
203}
204
205impl From<&AccountStorage> for AccountStorageHeader {
206    fn from(value: &AccountStorage) -> Self {
207        value.to_header()
208    }
209}
210
211// SEQUENTIAL COMMIT
212// ================================================================================================
213
214impl SequentialCommit for AccountStorageHeader {
215    type Commitment = Word;
216
217    fn to_elements(&self) -> Vec<Felt> {
218        self.slots().flat_map(|slot| slot.to_elements()).collect()
219    }
220}
221
222// SERIALIZATION
223// ================================================================================================
224
225impl Serializable for AccountStorageHeader {
226    fn write_into<W: ByteWriter>(&self, target: &mut W) {
227        let len = self.slots.len() as u8;
228        target.write_u8(len);
229        target.write_many(self.slots())
230    }
231}
232
233impl Deserializable for AccountStorageHeader {
234    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
235        let len = source.read_u8()?;
236        let slots: Vec<StorageSlotHeader> =
237            source.read_many_iter(len as usize)?.collect::<Result<_, _>>()?;
238        Self::new(slots).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
239    }
240}
241
242// STORAGE SLOT HEADER
243// ================================================================================================
244
245/// The header of a [`StorageSlot`], storing only the slot name (or ID), slot type and value of the
246/// slot.
247///
248/// The stored value differs based on the slot type:
249/// - [`StorageSlotType::Value`]: The value of the slot itself.
250/// - [`StorageSlotType::Map`]: The root of the SMT that represents the storage map.
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct StorageSlotHeader {
253    name: StorageSlotName,
254    r#type: StorageSlotType,
255    value: Word,
256}
257
258impl StorageSlotHeader {
259    // CONSTRUCTORS
260    // --------------------------------------------------------------------------------------------
261
262    /// Returns a new instance of storage slot header.
263    pub fn new(name: StorageSlotName, r#type: StorageSlotType, value: Word) -> Self {
264        Self { name, r#type, value }
265    }
266
267    /// Returns a new instance of storage slot header with an empty value slot.
268    pub fn with_empty_value(name: StorageSlotName) -> StorageSlotHeader {
269        StorageSlotHeader::new(name, StorageSlotType::Value, Word::default())
270    }
271
272    /// Returns a new instance of storage slot header with an empty map slot.
273    pub fn with_empty_map(name: StorageSlotName) -> StorageSlotHeader {
274        StorageSlotHeader::new(name, StorageSlotType::Map, EMPTY_STORAGE_MAP_ROOT)
275    }
276
277    // ACCESSORS
278    // --------------------------------------------------------------------------------------------
279
280    /// Returns a reference to the slot name.
281    pub fn name(&self) -> &StorageSlotName {
282        &self.name
283    }
284
285    /// Returns the slot ID.
286    pub fn id(&self) -> StorageSlotId {
287        self.name.id()
288    }
289
290    /// Returns the slot type.
291    pub fn slot_type(&self) -> StorageSlotType {
292        self.r#type
293    }
294
295    /// Returns the slot value.
296    pub fn value(&self) -> Word {
297        self.value
298    }
299
300    /// Returns this storage slot header as field elements.
301    ///
302    /// This is done by converting this storage slot into 8 field elements as follows:
303    /// ```text
304    /// [[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE]
305    /// ```
306    pub(crate) fn to_elements(&self) -> [Felt; StorageSlot::NUM_ELEMENTS] {
307        let id = self.id();
308        let mut elements = [ZERO; StorageSlot::NUM_ELEMENTS];
309        elements[0..4].copy_from_slice(&[
310            Felt::ZERO,
311            self.r#type.as_felt(),
312            id.suffix(),
313            id.prefix(),
314        ]);
315        elements[4..8].copy_from_slice(self.value.as_elements());
316        elements
317    }
318}
319
320impl From<&StorageSlot> for StorageSlotHeader {
321    fn from(slot: &StorageSlot) -> Self {
322        StorageSlotHeader::new(slot.name().clone(), slot.slot_type(), slot.value())
323    }
324}
325
326// SERIALIZATION
327// ================================================================================================
328
329impl Serializable for StorageSlotHeader {
330    fn write_into<W: ByteWriter>(&self, target: &mut W) {
331        self.name.write_into(target);
332        self.r#type.write_into(target);
333        self.value.write_into(target);
334    }
335}
336
337impl Deserializable for StorageSlotHeader {
338    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
339        let name = StorageSlotName::read_from(source)?;
340        let slot_type = StorageSlotType::read_from(source)?;
341        let value = Word::read_from(source)?;
342        Ok(Self::new(name, slot_type, value))
343    }
344}
345
346// TESTS
347// ================================================================================================
348
349#[cfg(test)]
350mod tests {
351    use alloc::collections::BTreeMap;
352    use alloc::string::ToString;
353
354    use miden_core::Felt;
355
356    use super::AccountStorageHeader;
357    use crate::Word;
358    use crate::account::{AccountStorage, StorageSlotHeader, StorageSlotName, StorageSlotType};
359    use crate::testing::storage::{MOCK_MAP_SLOT, MOCK_VALUE_SLOT0, MOCK_VALUE_SLOT1};
360    use crate::utils::serde::{Deserializable, Serializable};
361
362    #[test]
363    fn test_from_account_storage() {
364        let storage_map = AccountStorage::mock_map();
365
366        // create new storage header from AccountStorage
367        let mut slots = vec![
368            (MOCK_VALUE_SLOT0.clone(), StorageSlotType::Value, Word::from([1, 2, 3, 4u32])),
369            (
370                MOCK_VALUE_SLOT1.clone(),
371                StorageSlotType::Value,
372                Word::from([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]),
373            ),
374            (MOCK_MAP_SLOT.clone(), StorageSlotType::Map, storage_map.root()),
375        ];
376        slots.sort_unstable_by_key(|(slot_name, ..)| slot_name.id());
377
378        let expected_header = AccountStorageHeader::from_tuples(slots).unwrap();
379        let account_storage = AccountStorage::mock();
380
381        assert_eq!(expected_header, AccountStorageHeader::from(&account_storage))
382    }
383
384    #[test]
385    fn test_serde_account_storage_header() {
386        // create new storage header
387        let storage = AccountStorage::mock();
388        let storage_header = AccountStorageHeader::from(&storage);
389
390        // serde storage header
391        let bytes = storage_header.to_bytes();
392        let deserialized = AccountStorageHeader::read_from_bytes(&bytes).unwrap();
393
394        // assert deserialized == storage header
395        assert_eq!(storage_header, deserialized);
396    }
397
398    #[test]
399    fn test_to_elements_from_elements_empty() {
400        // Construct empty header.
401        let empty_header = AccountStorageHeader::new(vec![]).unwrap();
402        let empty_elements = empty_header.to_elements();
403
404        // Call from_elements.
405        let empty_slot_names = BTreeMap::new();
406        let reconstructed_empty =
407            AccountStorageHeader::try_from_elements(&empty_elements, &empty_slot_names).unwrap();
408        assert_eq!(empty_header, reconstructed_empty);
409    }
410
411    #[test]
412    fn test_to_elements_from_elements_single_slot() {
413        // Construct single slot header.
414        let slot_name1 = StorageSlotName::new("test::value::slot1".to_string()).unwrap();
415        let slot1 = StorageSlotHeader::new(
416            slot_name1,
417            StorageSlotType::Value,
418            Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]),
419        );
420
421        let single_slot_header = AccountStorageHeader::new(vec![slot1.clone()]).unwrap();
422        let single_elements = single_slot_header.to_elements();
423
424        // Call from_elements.
425        let slot_names = BTreeMap::from([(slot1.id(), slot1.name().clone())]);
426        let reconstructed_single =
427            AccountStorageHeader::try_from_elements(&single_elements, &slot_names).unwrap();
428
429        assert_eq!(single_slot_header, reconstructed_single);
430    }
431
432    #[test]
433    fn test_to_elements_from_elements_multiple_slot() {
434        // Construct multi slot header.
435        let slot_name2 = StorageSlotName::new("test::map::slot2".to_string()).unwrap();
436        let slot_name3 = StorageSlotName::new("test::value::slot3".to_string()).unwrap();
437
438        let slot2 = StorageSlotHeader::new(
439            slot_name2,
440            StorageSlotType::Map,
441            Word::new([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]),
442        );
443        let slot3 = StorageSlotHeader::new(
444            slot_name3,
445            StorageSlotType::Value,
446            Word::new([Felt::new(9), Felt::new(10), Felt::new(11), Felt::new(12)]),
447        );
448
449        let mut slots = vec![slot2, slot3];
450        slots.sort_by_key(|slot| slot.id());
451        let multi_slot_header = AccountStorageHeader::new(slots.clone()).unwrap();
452        let multi_elements = multi_slot_header.to_elements();
453
454        // Call from_elements.
455        let slot_names = BTreeMap::from([
456            (slots[0].id(), slots[0].name.clone()),
457            (slots[1].id(), slots[1].name.clone()),
458        ]);
459        let reconstructed_multi =
460            AccountStorageHeader::try_from_elements(&multi_elements, &slot_names).unwrap();
461
462        assert_eq!(multi_slot_header, reconstructed_multi);
463    }
464
465    #[test]
466    fn test_from_elements_errors() {
467        // Test with invalid length (not divisible by 8).
468        let invalid_elements = vec![Felt::new(1), Felt::new(2), Felt::new(3)];
469        let empty_slot_names = BTreeMap::new();
470        assert!(
471            AccountStorageHeader::try_from_elements(&invalid_elements, &empty_slot_names).is_err()
472        );
473
474        // Test with invalid slot type.
475        let mut invalid_type_elements = vec![crate::ZERO; 8];
476        invalid_type_elements[1] = Felt::new(5); // Invalid slot type.
477        assert!(
478            AccountStorageHeader::try_from_elements(&invalid_type_elements, &empty_slot_names)
479                .is_err()
480        );
481    }
482
483    #[test]
484    fn test_from_elements_with_slot_names() {
485        use alloc::collections::BTreeMap;
486
487        // Create original slot with known name.
488        let slot_name1 = StorageSlotName::new("test::value::slot1".to_string()).unwrap();
489        let slot1 = StorageSlotHeader::new(
490            slot_name1.clone(),
491            StorageSlotType::Value,
492            Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]),
493        );
494
495        // Serialize the single slot to elements
496        let elements = slot1.to_elements();
497
498        // Create slot names map using the slot's ID
499        let mut slot_names = BTreeMap::new();
500        slot_names.insert(slot1.id(), slot_name1.clone());
501
502        // Test from_elements with provided slot names on raw slot elements.
503        let reconstructed_header =
504            AccountStorageHeader::try_from_elements(&elements, &slot_names).unwrap();
505
506        // Verify that the original slot names are preserved.
507        assert_eq!(reconstructed_header.slots().count(), 1);
508        let reconstructed_slot = reconstructed_header.slots().next().unwrap();
509
510        assert_eq!(slot_name1.as_str(), reconstructed_slot.name().as_str());
511        assert_eq!(slot1.slot_type(), reconstructed_slot.slot_type());
512        assert_eq!(slot1.value(), reconstructed_slot.value());
513    }
514}