miden_objects/account/component/template/storage/
mod.rs

1use alloc::{boxed::Box, string::String, vec::Vec};
2
3use vm_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
4use vm_processor::DeserializationError;
5
6mod entry_content;
7pub use entry_content::*;
8
9use super::AccountComponentTemplateError;
10use crate::account::StorageSlot;
11
12mod placeholder;
13pub use placeholder::{PlaceholderType, StoragePlaceholder, StorageValue};
14
15mod init_storage_data;
16pub use init_storage_data::InitStorageData;
17
18#[cfg(feature = "std")]
19pub mod toml;
20
21// STORAGE ENTRY
22// ================================================================================================
23
24/// Represents a single entry in the component's storage layout.
25///
26/// Each entry can describe:
27/// - A value slot with a single word.
28/// - A map slot with a key-value map that occupies one storage slot.
29/// - A multi-slot entry spanning multiple contiguous slots with multiple words (but not maps) that
30///   represent a single logical value.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum StorageEntry {
33    /// A value slot, which can contain one or more words. Each word is a hex-encoded string.
34    Value {
35        /// The human-readable name of the slot.
36        name: String,
37        /// An optional description for the slot, explaining its purpose.
38        description: Option<String>,
39        /// The numeric index of this slot in the component's storage layout.
40        slot: u8,
41        /// The initial value for this slot.
42        value: WordRepresentation,
43    },
44
45    /// A map slot, containing multiple key-value pairs. Keys and values are hex-encoded strings.
46    Map {
47        /// The human-readable name of the map slot.
48        name: String,
49        /// An optional description for the slot, explaining its purpose.
50        description: Option<String>,
51        /// The numeric index of this map slot in the component's storage.
52        slot: u8,
53        /// A list of key-value pairs to initialize in this map slot.
54        map: MapRepresentation,
55    },
56
57    /// A multi-slot entry, representing a single logical value across multiple slots.
58    MultiSlot {
59        /// The human-readable name of this multi-slot entry.
60        name: String,
61        /// An optional description for the slot, explaining its purpose.
62        description: Option<String>,
63        /// The indices of the slots that form this multi-slot entry.
64        slots: Vec<u8>,
65        /// A list of values to fill the logical slot, with a length equal to the amount of slots.
66        values: Vec<WordRepresentation>,
67    },
68}
69
70impl StorageEntry {
71    /// Creates a new [`StorageEntry::Value`] variant.
72    pub fn new_value(
73        name: impl Into<String>,
74        description: Option<impl Into<String>>,
75        slot: u8,
76        value: impl Into<WordRepresentation>,
77    ) -> Self {
78        StorageEntry::Value {
79            name: name.into(),
80            description: description.map(Into::<String>::into),
81            slot,
82            value: value.into(),
83        }
84    }
85
86    /// Creates a new [`StorageEntry::Map`] variant.
87    pub fn new_map(
88        name: impl Into<String>,
89        description: Option<impl Into<String>>,
90        slot: u8,
91        map_representation: MapRepresentation,
92    ) -> Result<Self, AccountComponentTemplateError> {
93        let entry = StorageEntry::Map {
94            name: name.into(),
95            description: description.map(Into::<String>::into),
96            slot,
97            map: map_representation,
98        };
99
100        entry.validate()?;
101        Ok(entry)
102    }
103
104    /// Creates a new [`StorageEntry::MultiSlot`] variant.
105    pub fn new_multi_slot(
106        name: impl Into<String>,
107        description: Option<impl Into<String>>,
108        slots: Vec<u8>,
109        values: Vec<impl Into<WordRepresentation>>,
110    ) -> Result<Self, AccountComponentTemplateError> {
111        let entry = StorageEntry::MultiSlot {
112            name: name.into(),
113            description: description.map(Into::<String>::into),
114            slots,
115            values: values.into_iter().map(Into::into).collect(),
116        };
117
118        entry.validate()?;
119        Ok(entry)
120    }
121
122    /// Returns the slot indices that the storage entry covers.
123    pub fn slot_indices(&self) -> &[u8] {
124        match self {
125            StorageEntry::MultiSlot { slots, .. } => slots.as_slice(),
126            StorageEntry::Value { slot, .. } => core::slice::from_ref(slot),
127            StorageEntry::Map { slot, .. } => core::slice::from_ref(slot),
128        }
129    }
130
131    /// Returns the name of the storage entry.
132    pub fn name(&self) -> &str {
133        match self {
134            StorageEntry::Value { name, .. } => name.as_str(),
135            StorageEntry::Map { name, .. } => name.as_str(),
136            StorageEntry::MultiSlot { name, .. } => name.as_str(),
137        }
138    }
139
140    /// Returns the optional description of the storage entry.
141    pub fn description(&self) -> Option<&str> {
142        match self {
143            StorageEntry::Value { description, .. } => description.as_deref(),
144            StorageEntry::Map { description, .. } => description.as_deref(),
145            StorageEntry::MultiSlot { description, .. } => description.as_deref(),
146        }
147    }
148
149    /// Returns all the `WordRepresentation` values covered by this entry.
150    /// For `Value` entries, this returns a single-element slice.
151    /// For `MultiSlot` entries, this returns all values.
152    /// For `Map` entries, since they're key-value pairs, return an empty slice.
153    pub fn word_values(&self) -> &[WordRepresentation] {
154        match self {
155            StorageEntry::Value { value, .. } => core::slice::from_ref(value),
156            StorageEntry::MultiSlot { values, .. } => values.as_slice(),
157            StorageEntry::Map { .. } => &[],
158        }
159    }
160
161    /// Returns an iterator over all of the storage entries's placeholder keys, alongside their
162    /// expected type.
163    pub fn all_placeholders_iter(
164        &self,
165    ) -> Box<dyn Iterator<Item = (&StoragePlaceholder, PlaceholderType)> + '_> {
166        match self {
167            StorageEntry::Value { value, .. } => value.all_placeholders_iter(),
168            StorageEntry::Map { map: map_entries, .. } => map_entries.all_placeholders_iter(),
169            StorageEntry::MultiSlot { values, .. } => {
170                Box::new(values.iter().flat_map(|word| word.all_placeholders_iter()))
171            },
172        }
173    }
174
175    /// Attempts to convert the storage entry into a list of [StorageSlot].
176    ///
177    /// - StorageEntry::Value would convert to a [StorageSlot::Value]
178    /// - StorageEntry::MultiSlot would convert to as many [StorageSlot::Value] as defined
179    /// - StorageEntry::Map would convert to a [StorageSlot::Map]
180    ///
181    /// Each of the entry's values could be templated. These values are replaced for values found
182    /// in `init_storage_data`, identified by its key.
183    pub fn try_build_storage_slots(
184        &self,
185        init_storage_data: &InitStorageData,
186    ) -> Result<Vec<StorageSlot>, AccountComponentTemplateError> {
187        match self {
188            StorageEntry::Value { value, .. } => {
189                let slot = value.try_build_word(init_storage_data)?;
190                Ok(vec![StorageSlot::Value(slot)])
191            },
192            StorageEntry::Map { map: values, .. } => {
193                let storage_map = values.try_build_map(init_storage_data)?;
194                Ok(vec![StorageSlot::Map(storage_map)])
195            },
196            StorageEntry::MultiSlot { values, .. } => Ok(values
197                .iter()
198                .map(|word_repr| {
199                    word_repr.clone().try_build_word(init_storage_data).map(StorageSlot::Value)
200                })
201                .collect::<Result<Vec<StorageSlot>, _>>()?),
202        }
203    }
204
205    /// Validates the storage entry for internal consistency.
206    pub(super) fn validate(&self) -> Result<(), AccountComponentTemplateError> {
207        match self {
208            StorageEntry::Map { map, .. } => map.validate(),
209            StorageEntry::MultiSlot { slots, values, .. } => {
210                if slots.len() != values.len() {
211                    return Err(AccountComponentTemplateError::MultiSlotArityMismatch);
212                } else {
213                    let mut all_slots = slots.clone();
214                    all_slots.sort_unstable();
215                    for slots in all_slots.windows(2) {
216                        if slots[1] == slots[0] {
217                            return Err(AccountComponentTemplateError::DuplicateSlot(slots[0]));
218                        }
219
220                        if slots[1] != slots[0] + 1 {
221                            return Err(AccountComponentTemplateError::NonContiguousSlots(
222                                slots[0], slots[1],
223                            ));
224                        }
225                    }
226                }
227                Ok(())
228            },
229            StorageEntry::Value { .. } => Ok(()),
230        }
231    }
232}
233
234// SERIALIZATION
235// ================================================================================================
236
237impl Serializable for StorageEntry {
238    fn write_into<W: ByteWriter>(&self, target: &mut W) {
239        match self {
240            StorageEntry::Value { name, description, slot, value } => {
241                target.write_u8(0u8);
242                target.write(name);
243                target.write(description);
244                target.write_u8(*slot);
245                target.write(value);
246            },
247            StorageEntry::Map { name, description, slot, map: values } => {
248                target.write_u8(1u8);
249                target.write(name);
250                target.write(description);
251                target.write_u8(*slot);
252                target.write(values);
253            },
254            StorageEntry::MultiSlot { name, description, slots, values } => {
255                target.write_u8(2u8);
256                target.write(name);
257                target.write(description);
258                target.write(slots);
259                target.write(values);
260            },
261        }
262    }
263}
264
265impl Deserializable for StorageEntry {
266    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
267        let variant_tag = source.read_u8()?;
268        let name: String = source.read()?;
269        let description: Option<String> = source.read()?;
270
271        match variant_tag {
272            // Value
273            0 => {
274                let slot = source.read_u8()?;
275                let value: WordRepresentation = source.read()?;
276
277                Ok(StorageEntry::Value { name, description, slot, value })
278            },
279
280            // Map
281            1 => {
282                let slot = source.read_u8()?;
283                let map_representation: MapRepresentation = source.read()?;
284
285                Ok(StorageEntry::Map {
286                    name,
287                    description,
288                    slot,
289                    map: map_representation,
290                })
291            },
292
293            // MultiSlot
294            2 => {
295                let slots: Vec<u8> = source.read()?;
296                let values: Vec<WordRepresentation> = source.read()?;
297
298                Ok(StorageEntry::MultiSlot { name, description, slots, values })
299            },
300
301            // Unknown tag => error
302            _ => Err(DeserializationError::InvalidValue(format!(
303                "unknown variant tag `{variant_tag}` for StorageEntry"
304            ))),
305        }
306    }
307}
308
309// MAP ENTRY
310// ================================================================================================
311
312/// Key-value entry for storage maps.
313#[derive(Debug, Clone, PartialEq, Eq)]
314#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
315pub struct MapEntry {
316    key: WordRepresentation,
317    value: WordRepresentation,
318}
319
320impl MapEntry {
321    pub fn new(key: impl Into<WordRepresentation>, value: impl Into<WordRepresentation>) -> Self {
322        Self { key: key.into(), value: value.into() }
323    }
324
325    pub fn key(&self) -> &WordRepresentation {
326        &self.key
327    }
328
329    pub fn value(&self) -> &WordRepresentation {
330        &self.value
331    }
332
333    /// Returns an iterator over all of the storage entries's placeholder keys, alongside their
334    /// expected type.
335    pub fn all_placeholders_iter(
336        &self,
337    ) -> impl Iterator<Item = (&StoragePlaceholder, PlaceholderType)> {
338        self.key.all_placeholders_iter().chain(self.value.all_placeholders_iter())
339    }
340
341    pub fn into_parts(self) -> (WordRepresentation, WordRepresentation) {
342        let MapEntry { key, value } = self;
343        (key, value)
344    }
345}
346
347impl Serializable for MapEntry {
348    fn write_into<W: ByteWriter>(&self, target: &mut W) {
349        self.key.write_into(target);
350        self.value.write_into(target);
351    }
352}
353
354impl Deserializable for MapEntry {
355    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
356        let key = WordRepresentation::read_from(source)?;
357        let value = WordRepresentation::read_from(source)?;
358        Ok(MapEntry { key, value })
359    }
360}
361
362// TESTS
363// ================================================================================================
364
365#[cfg(test)]
366mod tests {
367    use core::panic;
368    use std::collections::BTreeSet;
369
370    use assembly::Assembler;
371    use assert_matches::assert_matches;
372    use semver::Version;
373    use vm_core::{Felt, FieldElement};
374
375    use super::*;
376    use crate::{
377        account::{
378            component::template::{AccountComponentMetadata, AccountComponentTemplate},
379            AccountComponent, AccountType, StorageMap,
380        },
381        digest,
382        testing::account_code::CODE,
383        AccountError,
384    };
385
386    #[test]
387    fn test_storage_entry_serialization() {
388        let array = [
389            FeltRepresentation::Decimal(Felt::new(0xabc)),
390            FeltRepresentation::Decimal(Felt::new(1218)),
391            FeltRepresentation::Hexadecimal(Felt::new(0xdba3)),
392            FeltRepresentation::Template(StoragePlaceholder::new("test.array.dyn").unwrap()),
393        ];
394        let storage = vec![
395            StorageEntry::Value {
396                name: "slot0".into(),
397                description: Some("First slot".into()),
398                slot: 0,
399                value: WordRepresentation::Value(digest!("0x333123").into()),
400            },
401            StorageEntry::Map {
402                name: "map".into(),
403                description: Some("A storage map entry".into()),
404                slot: 1,
405                map: MapRepresentation::List(vec![
406                    MapEntry {
407                        key: WordRepresentation::Template(
408                            StoragePlaceholder::new("foo.bar").unwrap(),
409                        ),
410                        value: WordRepresentation::Value(digest!("0x2").into()),
411                    },
412                    MapEntry {
413                        key: WordRepresentation::Value(digest!("0x2").into()),
414                        value: WordRepresentation::Template(
415                            StoragePlaceholder::new("bar.baz").unwrap(),
416                        ),
417                    },
418                    MapEntry {
419                        key: WordRepresentation::Value(digest!("0x3").into()),
420                        value: WordRepresentation::Value(digest!("0x4").into()),
421                    },
422                ]),
423            },
424            StorageEntry::MultiSlot {
425                name: "multi".into(),
426                description: Some("Multi slot entry".into()),
427                slots: vec![2, 3, 4],
428                values: vec![
429                    WordRepresentation::Template(StoragePlaceholder::new("test.Template").unwrap()),
430                    WordRepresentation::Array(array),
431                    WordRepresentation::Value(digest!("0xabcdef123abcdef123").into()),
432                ],
433            },
434            StorageEntry::Value {
435                name: "single-slot".into(),
436                description: Some("Slot with storage placeholder".into()),
437                slot: 5,
438                value: WordRepresentation::Template(
439                    StoragePlaceholder::new("single-slot-key").unwrap(),
440                ),
441            },
442        ];
443
444        let config = AccountComponentMetadata {
445            name: "Test Component".into(),
446            description: "This is a test component".into(),
447            version: Version::parse("1.0.0").unwrap(),
448            targets: BTreeSet::from([AccountType::FungibleFaucet]),
449            storage,
450        };
451
452        let toml = config.as_toml().unwrap();
453
454        let deserialized = AccountComponentMetadata::from_toml(&toml).unwrap();
455
456        assert_eq!(deserialized, config);
457    }
458
459    #[test]
460    pub fn test_toml() {
461        let toml_text = r#"
462            name = "Test Component"
463            description = "This is a test component"
464            version = "1.0.1"
465            targets = ["FungibleFaucet", "RegularAccountImmutableCode"]
466
467            [[storage]]
468            name = "map"
469            description = "A storage map entry"
470            slot = 0
471            values = [
472                { key = "0x1", value = ["{{value.test}}", "0x1", "0x2", "0x3"] },
473                { key = "{{map.key.test}}", value = "0x3" },
474                { key = "0x3", value = "0x4" }
475            ]
476
477            [[storage]]
478            name = "test-word"
479            description = "word"
480            slot = 1
481            value = "{{word.test}}" 
482
483            [[storage]]
484            name = "multitest"
485            description = "a multi slot test"
486            slots = [2, 3]
487            values = [
488                "{{word.test}}",
489                ["1", "0", "0", "0"],
490            ]
491
492            [[storage]]
493            name = "map-template"
494            description = "a templated map"
495            slot = 4
496            values = "{{map.template}}"
497        "#;
498
499        let component_metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
500        for (key, placeholder_type) in component_metadata.get_unique_storage_placeholders() {
501            match key.inner() {
502                "map.key.test" | "word.test" => assert_eq!(placeholder_type, PlaceholderType::Word),
503                "value.test" => assert_eq!(placeholder_type, PlaceholderType::Felt),
504                "map.template" => assert_eq!(placeholder_type, PlaceholderType::Map),
505                _ => panic!("all cases are covered"),
506            }
507        }
508
509        let library = Assembler::default().assemble_library([CODE]).unwrap();
510
511        let template = AccountComponentTemplate::new(component_metadata, library);
512
513        let template_bytes = template.to_bytes();
514        let template_deserialized =
515            AccountComponentTemplate::read_from_bytes(&template_bytes).unwrap();
516        assert_eq!(template, template_deserialized);
517
518        let storage_placeholders = InitStorageData::new([
519            (
520                StoragePlaceholder::new("map.key.test").unwrap(),
521                StorageValue::Word(Default::default()),
522            ),
523            (
524                StoragePlaceholder::new("value.test").unwrap(),
525                StorageValue::Felt(Felt::new(64)),
526            ),
527            (
528                StoragePlaceholder::new("word.test").unwrap(),
529                StorageValue::Word([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(128)]),
530            ),
531            (
532                StoragePlaceholder::new("map.template").unwrap(),
533                StorageValue::Map(StorageMap::default()),
534            ),
535        ]);
536
537        let component = AccountComponent::from_template(&template, &storage_placeholders).unwrap();
538
539        assert_eq!(
540            component.supported_types(),
541            &[AccountType::FungibleFaucet, AccountType::RegularAccountImmutableCode]
542                .into_iter()
543                .collect()
544        );
545
546        let storage_map = component.storage_slots.first().unwrap();
547        match storage_map {
548            StorageSlot::Map(storage_map) => assert_eq!(storage_map.entries().count(), 3),
549            _ => panic!("should be map"),
550        }
551
552        let value_entry = component.storage_slots().get(1).unwrap();
553        match value_entry {
554            StorageSlot::Value(v) => {
555                assert_eq!(v, &[Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(128)])
556            },
557            _ => panic!("should be value"),
558        }
559
560        let failed_instantiation =
561            AccountComponent::from_template(&template, &InitStorageData::default());
562        assert_matches!(
563            failed_instantiation,
564            Err(AccountError::AccountComponentTemplateInstantiationError(
565                AccountComponentTemplateError::PlaceholderValueNotProvided(_)
566            ))
567        );
568    }
569
570    #[test]
571    pub fn fail_placeholder_type_mismatch() {
572        let toml_text = r#"
573            name = "Test Component"
574            description = "This is a test component"
575            version = "1.0.1"
576            targets = ["FungibleFaucet"]
577
578            [[storage]]
579            name = "map"
580            description = "A storage map entry"
581            slot = 0
582            values = [
583                { key = "0x1", value = ["{{value.test}}", "0x1", "0x2", "0x3"] },
584            ]
585
586            [[storage]]
587            name = "word"
588            slot = 1
589            value = "{{value.test}}"
590        "#;
591        let component_metadata = AccountComponentMetadata::from_toml(toml_text);
592        assert_matches!(
593            component_metadata,
594            Err(AccountComponentTemplateError::StoragePlaceholderTypeMismatch(_, _, _))
595        );
596    }
597}