Skip to main content

miden_protocol/account/component/storage/
schema.rs

1use alloc::boxed::Box;
2use alloc::collections::BTreeMap;
3use alloc::string::{String, ToString};
4use alloc::vec::Vec;
5
6use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
7use miden_processor::DeserializationError;
8
9use super::type_registry::{SCHEMA_TYPE_REGISTRY, SchemaRequirement, SchemaTypeId};
10use super::{InitStorageData, StorageValueName, WordValue};
11use crate::account::storage::is_reserved_slot_name;
12use crate::account::{StorageMap, StorageSlot, StorageSlotName};
13use crate::crypto::utils::bytes_to_elements_with_padding;
14use crate::errors::AccountComponentTemplateError;
15use crate::{Felt, FieldElement, Hasher, Word};
16
17// STORAGE SCHEMA
18// ================================================================================================
19
20/// Describes the storage schema of an account component in terms of its named storage slots.
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct StorageSchema {
23    slots: BTreeMap<StorageSlotName, StorageSlotSchema>,
24}
25
26impl StorageSchema {
27    /// Creates a new [`StorageSchema`].
28    ///
29    /// # Errors
30    /// - If `fields` contains duplicate slot names.
31    /// - If `fields` contains the protocol-reserved faucet metadata slot name.
32    /// - If any slot schema is invalid.
33    /// - If multiple schema fields map to the same init value name.
34    pub fn new(
35        slots: impl IntoIterator<Item = (StorageSlotName, StorageSlotSchema)>,
36    ) -> Result<Self, AccountComponentTemplateError> {
37        let mut map = BTreeMap::new();
38        for (slot_name, schema) in slots {
39            if map.insert(slot_name.clone(), schema).is_some() {
40                return Err(AccountComponentTemplateError::DuplicateSlotName(slot_name));
41            }
42        }
43
44        let schema = Self { slots: map };
45        schema.validate()?;
46        Ok(schema)
47    }
48
49    /// Returns an iterator over `(slot_name, schema)` pairs in slot-id order.
50    pub fn iter(&self) -> impl Iterator<Item = (&StorageSlotName, &StorageSlotSchema)> {
51        self.slots.iter()
52    }
53
54    /// Returns a reference to the underlying slots map.
55    pub fn slots(&self) -> &BTreeMap<StorageSlotName, StorageSlotSchema> {
56        &self.slots
57    }
58
59    /// Builds the initial [`StorageSlot`]s for this schema using the provided initialization data.
60    pub fn build_storage_slots(
61        &self,
62        init_storage_data: &InitStorageData,
63    ) -> Result<Vec<StorageSlot>, AccountComponentTemplateError> {
64        self.slots
65            .iter()
66            .map(|(slot_name, schema)| schema.try_build_storage_slot(slot_name, init_storage_data))
67            .collect()
68    }
69
70    /// Returns a commitment to this storage schema definition.
71    ///
72    /// The commitment is computed over the serialized schema and does not include defaults.
73    pub fn commitment(&self) -> Word {
74        let mut bytes = Vec::new();
75        self.write_into_with_optional_defaults(&mut bytes, false);
76        let elements = bytes_to_elements_with_padding(&bytes);
77        Hasher::hash_elements(&elements)
78    }
79
80    /// Returns init-value requirements for the entire schema.
81    ///
82    /// The returned map includes both required values (no `default_value`) and optional values
83    /// (with `default_value`), and excludes map entries.
84    pub fn schema_requirements(
85        &self,
86    ) -> Result<BTreeMap<StorageValueName, SchemaRequirement>, AccountComponentTemplateError> {
87        let mut requirements = BTreeMap::new();
88        for (slot_name, schema) in self.slots.iter() {
89            schema.collect_init_value_requirements(slot_name, &mut requirements)?;
90        }
91        Ok(requirements)
92    }
93
94    /// Serializes the schema, optionally ignoring the default values (used for committing to a
95    /// schema definition).
96    fn write_into_with_optional_defaults<W: ByteWriter>(
97        &self,
98        target: &mut W,
99        include_defaults: bool,
100    ) {
101        target.write_u16(self.slots.len() as u16);
102        for (slot_name, schema) in self.slots.iter() {
103            target.write(slot_name);
104            schema.write_into_with_optional_defaults(target, include_defaults);
105        }
106    }
107
108    /// Validates schema-level invariants across all slots.
109    fn validate(&self) -> Result<(), AccountComponentTemplateError> {
110        let mut init_values = BTreeMap::new();
111
112        for (slot_name, schema) in self.slots.iter() {
113            if is_reserved_slot_name(slot_name) {
114                return Err(AccountComponentTemplateError::ReservedSlotName(slot_name.clone()));
115            }
116
117            schema.validate()?;
118            schema.collect_init_value_requirements(slot_name, &mut init_values)?;
119        }
120
121        Ok(())
122    }
123}
124
125impl Serializable for StorageSchema {
126    fn write_into<W: ByteWriter>(&self, target: &mut W) {
127        self.write_into_with_optional_defaults(target, true);
128    }
129}
130
131impl Deserializable for StorageSchema {
132    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
133        let num_entries = source.read_u16()? as usize;
134        let mut fields = BTreeMap::new();
135
136        for _ in 0..num_entries {
137            let slot_name = StorageSlotName::read_from(source)?;
138            let schema = StorageSlotSchema::read_from(source)?;
139
140            if fields.insert(slot_name.clone(), schema).is_some() {
141                return Err(DeserializationError::InvalidValue(format!(
142                    "duplicate slot name in storage schema: {slot_name}",
143                )));
144            }
145        }
146
147        let schema = StorageSchema::new(fields)
148            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
149        Ok(schema)
150    }
151}
152
153fn validate_description_ascii(description: &str) -> Result<(), AccountComponentTemplateError> {
154    if description.is_ascii() {
155        Ok(())
156    } else {
157        Err(AccountComponentTemplateError::InvalidSchema(
158            "description must contain only ASCII characters".to_string(),
159        ))
160    }
161}
162
163// STORAGE SLOT SCHEMA
164// ================================================================================================
165
166/// Describes the schema for a storage slot.
167/// Can describe either a value slot, or a map slot.
168#[allow(clippy::large_enum_variant)]
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub enum StorageSlotSchema {
171    Value(ValueSlotSchema),
172    Map(MapSlotSchema),
173}
174
175impl StorageSlotSchema {
176    fn collect_init_value_requirements(
177        &self,
178        slot_name: &StorageSlotName,
179        requirements: &mut BTreeMap<StorageValueName, SchemaRequirement>,
180    ) -> Result<(), AccountComponentTemplateError> {
181        let slot_name = StorageValueName::from_slot_name(slot_name);
182        match self {
183            StorageSlotSchema::Value(slot) => {
184                slot.collect_init_value_requirements(slot_name, requirements)
185            },
186            StorageSlotSchema::Map(_) => Ok(()),
187        }
188    }
189
190    /// Builds a [`StorageSlot`] for the specified `slot_name` using the provided initialization
191    /// data.
192    pub fn try_build_storage_slot(
193        &self,
194        slot_name: &StorageSlotName,
195        init_storage_data: &InitStorageData,
196    ) -> Result<StorageSlot, AccountComponentTemplateError> {
197        match self {
198            StorageSlotSchema::Value(slot) => {
199                let word = slot.try_build_word(init_storage_data, slot_name)?;
200                Ok(StorageSlot::with_value(slot_name.clone(), word))
201            },
202            StorageSlotSchema::Map(slot) => {
203                let storage_map = slot.try_build_map(init_storage_data, slot_name)?;
204                Ok(StorageSlot::with_map(slot_name.clone(), storage_map))
205            },
206        }
207    }
208
209    /// Validates this slot schema's internal invariants.
210    pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> {
211        match self {
212            StorageSlotSchema::Value(slot) => slot.validate()?,
213            StorageSlotSchema::Map(slot) => slot.validate()?,
214        }
215
216        Ok(())
217    }
218
219    /// Serializes the schema, optionally ignoring the default values (used for committing to a
220    /// schema definition).
221    fn write_into_with_optional_defaults<W: ByteWriter>(
222        &self,
223        target: &mut W,
224        include_defaults: bool,
225    ) {
226        match self {
227            StorageSlotSchema::Value(slot) => {
228                target.write_u8(0u8);
229                slot.write_into_with_optional_defaults(target, include_defaults);
230            },
231            StorageSlotSchema::Map(slot) => {
232                target.write_u8(1u8);
233                slot.write_into_with_optional_defaults(target, include_defaults);
234            },
235        }
236    }
237}
238
239impl Serializable for StorageSlotSchema {
240    fn write_into<W: ByteWriter>(&self, target: &mut W) {
241        self.write_into_with_optional_defaults(target, true);
242    }
243}
244
245impl Deserializable for StorageSlotSchema {
246    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
247        let variant_tag = source.read_u8()?;
248        match variant_tag {
249            0 => Ok(StorageSlotSchema::Value(ValueSlotSchema::read_from(source)?)),
250            1 => Ok(StorageSlotSchema::Map(MapSlotSchema::read_from(source)?)),
251            _ => Err(DeserializationError::InvalidValue(format!(
252                "unknown variant tag '{variant_tag}' for StorageSlotSchema"
253            ))),
254        }
255    }
256}
257
258// WORDS
259// ================================================================================================
260
261/// Defines how a word slot is described within the component's storage schema.
262///
263/// Each word schema can either describe a whole-word typed value supplied at instantiation time
264/// (`Simple`) or a composite word that explicitly defines each felt element (`Composite`).
265#[derive(Debug, Clone, PartialEq, Eq)]
266#[allow(clippy::large_enum_variant)]
267pub enum WordSchema {
268    /// A whole-word typed value supplied at instantiation time.
269    Simple {
270        r#type: SchemaTypeId,
271        default_value: Option<Word>,
272    },
273    /// A composed word that may mix defaults and typed fields.
274    Composite { value: [FeltSchema; 4] },
275}
276
277impl WordSchema {
278    pub fn new_simple(r#type: SchemaTypeId) -> Self {
279        WordSchema::Simple { r#type, default_value: None }
280    }
281
282    pub fn new_simple_with_default(r#type: SchemaTypeId, default_value: Word) -> Self {
283        WordSchema::Simple {
284            r#type,
285            default_value: Some(default_value),
286        }
287    }
288
289    pub fn new_value(value: impl Into<[FeltSchema; 4]>) -> Self {
290        WordSchema::Composite { value: value.into() }
291    }
292
293    pub fn value(&self) -> Option<&[FeltSchema; 4]> {
294        match self {
295            WordSchema::Composite { value } => Some(value),
296            WordSchema::Simple { .. } => None,
297        }
298    }
299
300    /// Returns the schema type identifier associated with whole-word init-supplied values.
301    pub fn word_type(&self) -> SchemaTypeId {
302        match self {
303            WordSchema::Simple { r#type, .. } => r#type.clone(),
304            WordSchema::Composite { .. } => SchemaTypeId::native_word(),
305        }
306    }
307
308    fn collect_init_value_requirements(
309        &self,
310        value_name: StorageValueName,
311        description: Option<String>,
312        requirements: &mut BTreeMap<StorageValueName, SchemaRequirement>,
313    ) -> Result<(), AccountComponentTemplateError> {
314        match self {
315            WordSchema::Simple { r#type, default_value } => {
316                if *r#type == SchemaTypeId::void() {
317                    return Ok(());
318                }
319
320                let default_value = default_value.map(|word| {
321                    SCHEMA_TYPE_REGISTRY.display_word(r#type, word).value().to_string()
322                });
323
324                if requirements
325                    .insert(
326                        value_name.clone(),
327                        SchemaRequirement {
328                            description,
329                            r#type: r#type.clone(),
330                            default_value,
331                        },
332                    )
333                    .is_some()
334                {
335                    return Err(AccountComponentTemplateError::DuplicateInitValueName(value_name));
336                }
337
338                Ok(())
339            },
340            WordSchema::Composite { value } => {
341                for felt in value.iter() {
342                    felt.collect_init_value_requirements(value_name.clone(), requirements)?;
343                }
344                Ok(())
345            },
346        }
347    }
348
349    /// Validates the word schema type, defaults, and inner felts (if any).
350    fn validate(&self) -> Result<(), AccountComponentTemplateError> {
351        let type_exists = SCHEMA_TYPE_REGISTRY.contains_word_type(&self.word_type());
352        if !type_exists {
353            return Err(AccountComponentTemplateError::InvalidType(
354                self.word_type().to_string(),
355                "Word".into(),
356            ));
357        }
358
359        if let WordSchema::Simple {
360            r#type,
361            default_value: Some(default_value),
362        } = self
363        {
364            SCHEMA_TYPE_REGISTRY
365                .validate_word_value(r#type, *default_value)
366                .map_err(AccountComponentTemplateError::StorageValueParsingError)?;
367        }
368
369        if let Some(felts) = self.value() {
370            for felt in felts {
371                felt.validate()?;
372            }
373        }
374
375        Ok(())
376    }
377
378    /// Builds a [`Word`] from the provided initialization data according to this schema.
379    ///
380    /// For simple schemas, expects a direct slot value (not map or field entries).
381    /// For composite schemas, either parses a single value or builds the word from individual
382    /// felt entries.
383    pub(crate) fn try_build_word(
384        &self,
385        init_storage_data: &InitStorageData,
386        slot_name: &StorageSlotName,
387    ) -> Result<Word, AccountComponentTemplateError> {
388        let slot_prefix = StorageValueName::from_slot_name(slot_name);
389        let slot_value = init_storage_data.slot_value_entry(slot_name);
390        let has_fields = init_storage_data.has_field_entries_for_slot(slot_name);
391
392        if init_storage_data.map_entries(slot_name).is_some() {
393            return Err(AccountComponentTemplateError::InvalidInitStorageValue(
394                slot_prefix,
395                "expected a value, got a map".into(),
396            ));
397        }
398
399        match self {
400            WordSchema::Simple { r#type, default_value } => {
401                if has_fields {
402                    return Err(AccountComponentTemplateError::InvalidInitStorageValue(
403                        slot_prefix,
404                        "expected a value, got field entries".into(),
405                    ));
406                }
407                match slot_value {
408                    Some(value) => parse_storage_value_with_schema(self, value, &slot_prefix),
409                    None => {
410                        if *r#type == SchemaTypeId::void() {
411                            Ok(Word::empty())
412                        } else {
413                            default_value.as_ref().copied().ok_or_else(|| {
414                                AccountComponentTemplateError::InitValueNotProvided(slot_prefix)
415                            })
416                        }
417                    },
418                }
419            },
420            WordSchema::Composite { value } => {
421                if let Some(value) = slot_value {
422                    if has_fields {
423                        return Err(AccountComponentTemplateError::InvalidInitStorageValue(
424                            slot_prefix,
425                            "expected a single value, got both value and field entries".into(),
426                        ));
427                    }
428                    return parse_storage_value_with_schema(self, value, &slot_prefix);
429                }
430
431                let mut result = [Felt::ZERO; 4];
432                for (index, felt_schema) in value.iter().enumerate() {
433                    result[index] = felt_schema.try_build_felt(init_storage_data, slot_name)?;
434                }
435                Ok(Word::from(result))
436            },
437        }
438    }
439
440    pub(crate) fn validate_word_value(
441        &self,
442        slot_prefix: &StorageValueName,
443        label: &str,
444        word: Word,
445    ) -> Result<(), AccountComponentTemplateError> {
446        match self {
447            WordSchema::Simple { r#type, .. } => {
448                SCHEMA_TYPE_REGISTRY.validate_word_value(r#type, word).map_err(|err| {
449                    AccountComponentTemplateError::InvalidInitStorageValue(
450                        slot_prefix.clone(),
451                        format!("{label} does not match `{}`: {err}", r#type),
452                    )
453                })
454            },
455            WordSchema::Composite { value } => {
456                for (index, felt_schema) in value.iter().enumerate() {
457                    let felt_type = felt_schema.felt_type();
458                    SCHEMA_TYPE_REGISTRY.validate_felt_value(&felt_type, word[index]).map_err(
459                        |err| {
460                            AccountComponentTemplateError::InvalidInitStorageValue(
461                                slot_prefix.clone(),
462                                format!("{label}[{index}] does not match `{felt_type}`: {err}"),
463                            )
464                        },
465                    )?;
466                }
467
468                Ok(())
469            },
470        }
471    }
472
473    /// Serializes the schema, optionally ignoring the default values (used for committing to a
474    /// schema definition).
475    fn write_into_with_optional_defaults<W: ByteWriter>(
476        &self,
477        target: &mut W,
478        include_defaults: bool,
479    ) {
480        match self {
481            WordSchema::Simple { r#type, default_value } => {
482                target.write_u8(0);
483                target.write(r#type);
484                let default_value = if include_defaults { *default_value } else { None };
485                target.write(default_value);
486            },
487            WordSchema::Composite { value } => {
488                target.write_u8(1);
489                for felt in value.iter() {
490                    felt.write_into_with_optional_defaults(target, include_defaults);
491                }
492            },
493        }
494    }
495}
496
497impl Serializable for WordSchema {
498    fn write_into<W: ByteWriter>(&self, target: &mut W) {
499        self.write_into_with_optional_defaults(target, true);
500    }
501}
502
503impl Deserializable for WordSchema {
504    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
505        let tag = source.read_u8()?;
506        match tag {
507            0 => {
508                let r#type = SchemaTypeId::read_from(source)?;
509                let default_value = Option::<Word>::read_from(source)?;
510                Ok(WordSchema::Simple { r#type, default_value })
511            },
512            1 => {
513                let value = <[FeltSchema; 4]>::read_from(source)?;
514                Ok(WordSchema::Composite { value })
515            },
516            other => Err(DeserializationError::InvalidValue(format!(
517                "unknown tag '{other}' for WordSchema"
518            ))),
519        }
520    }
521}
522
523impl From<[FeltSchema; 4]> for WordSchema {
524    fn from(value: [FeltSchema; 4]) -> Self {
525        WordSchema::new_value(value)
526    }
527}
528
529impl From<[Felt; 4]> for WordSchema {
530    fn from(value: [Felt; 4]) -> Self {
531        WordSchema::new_simple_with_default(SchemaTypeId::native_word(), Word::from(value))
532    }
533}
534
535// FELT SCHEMA
536// ================================================================================================
537
538/// Supported element schema descriptors for a component's storage entries.
539///
540/// Each felt element in a composed word slot is typed, can have an optional default value, and can
541/// optionally be named to allow overriding at instantiation time.
542///
543/// To avoid non-overridable constants, unnamed elements are allowed only when `type = "void"`,
544/// which always evaluates to `0` and does not require init data.
545#[derive(Debug, Clone, PartialEq, Eq)]
546pub struct FeltSchema {
547    name: Option<String>,
548    description: Option<String>,
549    r#type: SchemaTypeId,
550    default_value: Option<Felt>,
551}
552
553impl FeltSchema {
554    /// Creates a new required typed felt field.
555    pub fn new_typed(r#type: SchemaTypeId, name: impl Into<String>) -> Self {
556        FeltSchema {
557            name: Some(name.into()),
558            description: None,
559            r#type,
560            default_value: None,
561        }
562    }
563
564    /// Creates a new typed felt field with a default value.
565    pub fn new_typed_with_default(
566        r#type: SchemaTypeId,
567        name: impl Into<String>,
568        default_value: Felt,
569    ) -> Self {
570        FeltSchema {
571            name: Some(name.into()),
572            description: None,
573            r#type,
574            default_value: Some(default_value),
575        }
576    }
577
578    /// Creates an unnamed `void` felt element.
579    pub fn new_void() -> Self {
580        FeltSchema {
581            name: None,
582            description: None,
583            r#type: SchemaTypeId::void(),
584            default_value: None,
585        }
586    }
587
588    /// Sets the description of the [`FeltSchema`] and returns `self`.
589    pub fn with_description(self, description: impl Into<String>) -> Self {
590        FeltSchema {
591            description: Some(description.into()),
592            ..self
593        }
594    }
595
596    /// Returns the felt type.
597    pub fn felt_type(&self) -> SchemaTypeId {
598        self.r#type.clone()
599    }
600
601    pub fn name(&self) -> Option<&str> {
602        self.name.as_deref()
603    }
604
605    pub fn description(&self) -> Option<&String> {
606        self.description.as_ref()
607    }
608
609    pub fn default_value(&self) -> Option<Felt> {
610        self.default_value
611    }
612
613    fn collect_init_value_requirements(
614        &self,
615        slot_prefix: StorageValueName,
616        requirements: &mut BTreeMap<StorageValueName, SchemaRequirement>,
617    ) -> Result<(), AccountComponentTemplateError> {
618        if self.r#type == SchemaTypeId::void() {
619            return Ok(());
620        }
621
622        let Some(name) = self.name.as_deref() else {
623            return Err(AccountComponentTemplateError::InvalidSchema(
624                "non-void felt elements must be named".into(),
625            ));
626        };
627        let value_name =
628            StorageValueName::from_slot_name_with_suffix(slot_prefix.slot_name(), name)
629                .map_err(|err| AccountComponentTemplateError::InvalidSchema(err.to_string()))?;
630
631        let default_value = self
632            .default_value
633            .map(|felt| SCHEMA_TYPE_REGISTRY.display_felt(&self.r#type, felt));
634
635        if requirements
636            .insert(
637                value_name.clone(),
638                SchemaRequirement {
639                    description: self.description.clone(),
640                    r#type: self.r#type.clone(),
641                    default_value,
642                },
643            )
644            .is_some()
645        {
646            return Err(AccountComponentTemplateError::DuplicateInitValueName(value_name));
647        }
648
649        Ok(())
650    }
651
652    /// Attempts to convert the [`FeltSchema`] into a [`Felt`].
653    ///
654    /// If the schema variant is typed, the value is retrieved from `init_storage_data`,
655    /// identified by its key. Otherwise, the returned value is just the inner element.
656    pub(crate) fn try_build_felt(
657        &self,
658        init_storage_data: &InitStorageData,
659        slot_name: &StorageSlotName,
660    ) -> Result<Felt, AccountComponentTemplateError> {
661        let value_name = match self.name.as_deref() {
662            Some(name) => Some(
663                StorageValueName::from_slot_name_with_suffix(slot_name, name)
664                    .map_err(|err| AccountComponentTemplateError::InvalidSchema(err.to_string()))?,
665            ),
666            None => None,
667        };
668
669        if let Some(value_name) = value_name.clone()
670            && let Some(raw_value) = init_storage_data.value_entry(&value_name)
671        {
672            match raw_value {
673                WordValue::Atomic(raw) => {
674                    let felt = SCHEMA_TYPE_REGISTRY
675                        .try_parse_felt(&self.r#type, raw)
676                        .map_err(AccountComponentTemplateError::StorageValueParsingError)?;
677                    return Ok(felt);
678                },
679                WordValue::Elements(_) => {
680                    return Err(AccountComponentTemplateError::InvalidInitStorageValue(
681                        value_name,
682                        "expected an atomic value, got a 4-element array".into(),
683                    ));
684                },
685                WordValue::FullyTyped(_) => {
686                    return Err(AccountComponentTemplateError::InvalidInitStorageValue(
687                        value_name,
688                        "expected an atomic value, got a word".into(),
689                    ));
690                },
691            }
692        }
693
694        if self.r#type == SchemaTypeId::void() {
695            return Ok(Felt::ZERO);
696        }
697
698        if let Some(default_value) = self.default_value {
699            return Ok(default_value);
700        }
701
702        let Some(value_name) = value_name else {
703            return Err(AccountComponentTemplateError::InvalidSchema(
704                "non-void felt elements must be named".into(),
705            ));
706        };
707
708        Err(AccountComponentTemplateError::InitValueNotProvided(value_name))
709    }
710
711    /// Serializes the schema, optionally ignoring the default values (used for committing to a
712    /// schema definition).
713    fn write_into_with_optional_defaults<W: ByteWriter>(
714        &self,
715        target: &mut W,
716        include_defaults: bool,
717    ) {
718        target.write(&self.name);
719        target.write(&self.description);
720        target.write(&self.r#type);
721        let default_value = if include_defaults { self.default_value } else { None };
722        target.write(default_value);
723    }
724
725    /// Validates the felt type, naming rules, and default value (if any).
726    fn validate(&self) -> Result<(), AccountComponentTemplateError> {
727        if let Some(description) = self.description.as_deref() {
728            validate_description_ascii(description)?;
729        }
730
731        let type_exists = SCHEMA_TYPE_REGISTRY.contains_felt_type(&self.felt_type());
732        if !type_exists {
733            return Err(AccountComponentTemplateError::InvalidType(
734                self.felt_type().to_string(),
735                "Felt".into(),
736            ));
737        }
738
739        if self.r#type == SchemaTypeId::void() {
740            if self.name.is_some() {
741                return Err(AccountComponentTemplateError::InvalidSchema(
742                    "void felt elements must be unnamed".into(),
743                ));
744            }
745            if self.default_value.is_some() {
746                return Err(AccountComponentTemplateError::InvalidSchema(
747                    "void felt elements cannot define `default-value`".into(),
748                ));
749            }
750            return Ok(());
751        }
752
753        if self.name.is_none() {
754            return Err(AccountComponentTemplateError::InvalidSchema(
755                "non-void felt elements must be named".into(),
756            ));
757        }
758
759        if let Some(value) = self.default_value {
760            SCHEMA_TYPE_REGISTRY
761                .validate_felt_value(&self.felt_type(), value)
762                .map_err(AccountComponentTemplateError::StorageValueParsingError)?;
763        }
764        Ok(())
765    }
766}
767
768impl Serializable for FeltSchema {
769    fn write_into<W: ByteWriter>(&self, target: &mut W) {
770        self.write_into_with_optional_defaults(target, true);
771    }
772}
773
774impl Deserializable for FeltSchema {
775    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
776        let name = Option::<String>::read_from(source)?;
777        let description = Option::<String>::read_from(source)?;
778        let r#type = SchemaTypeId::read_from(source)?;
779        let default_value = Option::<Felt>::read_from(source)?;
780        Ok(FeltSchema { name, description, r#type, default_value })
781    }
782}
783
784/// Describes the schema for a storage value slot.
785#[derive(Debug, Clone, PartialEq, Eq)]
786pub struct ValueSlotSchema {
787    description: Option<String>,
788    word: WordSchema,
789}
790
791impl ValueSlotSchema {
792    pub fn new(description: Option<String>, word: WordSchema) -> Self {
793        Self { description, word }
794    }
795
796    pub fn description(&self) -> Option<&String> {
797        self.description.as_ref()
798    }
799
800    pub fn word(&self) -> &WordSchema {
801        &self.word
802    }
803
804    fn collect_init_value_requirements(
805        &self,
806        value_name: StorageValueName,
807        requirements: &mut BTreeMap<StorageValueName, SchemaRequirement>,
808    ) -> Result<(), AccountComponentTemplateError> {
809        self.word.collect_init_value_requirements(
810            value_name,
811            self.description.clone(),
812            requirements,
813        )
814    }
815
816    /// Builds a [Word] from the provided initialization data using the inner word schema.
817    pub fn try_build_word(
818        &self,
819        init_storage_data: &InitStorageData,
820        slot_name: &StorageSlotName,
821    ) -> Result<Word, AccountComponentTemplateError> {
822        self.word.try_build_word(init_storage_data, slot_name)
823    }
824
825    /// Serializes the schema, optionally ignoring the default values (used for committing to a
826    /// schema definition).
827    fn write_into_with_optional_defaults<W: ByteWriter>(
828        &self,
829        target: &mut W,
830        include_defaults: bool,
831    ) {
832        target.write(&self.description);
833        self.word.write_into_with_optional_defaults(target, include_defaults);
834    }
835
836    /// Validates the slot's word schema.
837    pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> {
838        if let Some(description) = self.description.as_deref() {
839            validate_description_ascii(description)?;
840        }
841        self.word.validate()?;
842        Ok(())
843    }
844}
845
846impl Serializable for ValueSlotSchema {
847    fn write_into<W: ByteWriter>(&self, target: &mut W) {
848        self.write_into_with_optional_defaults(target, true);
849    }
850}
851
852impl Deserializable for ValueSlotSchema {
853    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
854        let description = Option::<String>::read_from(source)?;
855        let word = WordSchema::read_from(source)?;
856        Ok(ValueSlotSchema::new(description, word))
857    }
858}
859
860/// Describes the schema for a storage map slot.
861#[derive(Debug, Clone, PartialEq, Eq)]
862pub struct MapSlotSchema {
863    description: Option<String>,
864    default_values: Option<BTreeMap<Word, Word>>,
865    key_schema: WordSchema,
866    value_schema: WordSchema,
867}
868
869impl MapSlotSchema {
870    pub fn new(
871        description: Option<String>,
872        default_values: Option<BTreeMap<Word, Word>>,
873        key_schema: WordSchema,
874        value_schema: WordSchema,
875    ) -> Self {
876        Self {
877            description,
878            default_values,
879            key_schema,
880            value_schema,
881        }
882    }
883
884    pub fn description(&self) -> Option<&String> {
885        self.description.as_ref()
886    }
887
888    /// Builds a [`StorageMap`] from the provided initialization data.
889    ///
890    /// Merges any default values with entries from the init data, validating that the data
891    /// contains map entries (not a direct value or field entries).
892    pub fn try_build_map(
893        &self,
894        init_storage_data: &InitStorageData,
895        slot_name: &StorageSlotName,
896    ) -> Result<StorageMap, AccountComponentTemplateError> {
897        let mut entries = self.default_values.clone().unwrap_or_default();
898        let slot_prefix = StorageValueName::from_slot_name(slot_name);
899
900        if init_storage_data.slot_value_entry(slot_name).is_some() {
901            return Err(AccountComponentTemplateError::InvalidInitStorageValue(
902                slot_prefix,
903                "expected a map, got a value".into(),
904            ));
905        }
906        if init_storage_data.has_field_entries_for_slot(slot_name) {
907            return Err(AccountComponentTemplateError::InvalidInitStorageValue(
908                slot_prefix,
909                "expected a map, got field entries".into(),
910            ));
911        }
912        if let Some(init_entries) = init_storage_data.map_entries(slot_name) {
913            let mut parsed_entries = Vec::with_capacity(init_entries.len());
914            for (raw_key, raw_value) in init_entries.iter() {
915                let key = parse_storage_value_with_schema(&self.key_schema, raw_key, &slot_prefix)?;
916                let value =
917                    parse_storage_value_with_schema(&self.value_schema, raw_value, &slot_prefix)?;
918
919                parsed_entries.push((key, value));
920            }
921
922            for (key, value) in parsed_entries.iter() {
923                entries.insert(*key, *value);
924            }
925        }
926
927        if entries.is_empty() {
928            return Ok(StorageMap::new());
929        }
930
931        StorageMap::with_entries(entries)
932            .map_err(|err| AccountComponentTemplateError::StorageMapHasDuplicateKeys(Box::new(err)))
933    }
934
935    pub fn key_schema(&self) -> &WordSchema {
936        &self.key_schema
937    }
938
939    pub fn value_schema(&self) -> &WordSchema {
940        &self.value_schema
941    }
942
943    pub fn default_values(&self) -> Option<BTreeMap<Word, Word>> {
944        self.default_values.clone()
945    }
946
947    /// Serializes the schema, optionally ignoring the default values (used for committing to a
948    /// schema definition).
949    fn write_into_with_optional_defaults<W: ByteWriter>(
950        &self,
951        target: &mut W,
952        include_defaults: bool,
953    ) {
954        target.write(&self.description);
955        let default_values = if include_defaults {
956            self.default_values.clone()
957        } else {
958            None
959        };
960        target.write(&default_values);
961        self.key_schema.write_into_with_optional_defaults(target, include_defaults);
962        self.value_schema.write_into_with_optional_defaults(target, include_defaults);
963    }
964
965    /// Validates key/value word schemas for this map slot.
966    fn validate(&self) -> Result<(), AccountComponentTemplateError> {
967        if let Some(description) = self.description.as_deref() {
968            validate_description_ascii(description)?;
969        }
970        self.key_schema.validate()?;
971        self.value_schema.validate()?;
972        Ok(())
973    }
974}
975
976pub(super) fn parse_storage_value_with_schema(
977    schema: &WordSchema,
978    raw_value: &WordValue,
979    slot_prefix: &StorageValueName,
980) -> Result<Word, AccountComponentTemplateError> {
981    let word = match (schema, raw_value) {
982        (_, WordValue::FullyTyped(word)) => *word,
983        (WordSchema::Simple { r#type, .. }, raw_value) => {
984            parse_simple_word_value(r#type, raw_value, slot_prefix)?
985        },
986        (WordSchema::Composite { value }, WordValue::Elements(elements)) => {
987            parse_composite_elements(value, elements, slot_prefix)?
988        },
989        (WordSchema::Composite { .. }, WordValue::Atomic(value)) => SCHEMA_TYPE_REGISTRY
990            .try_parse_word(&SchemaTypeId::native_word(), value)
991            .map_err(|err| {
992                AccountComponentTemplateError::InvalidInitStorageValue(
993                    slot_prefix.clone(),
994                    format!("failed to parse value as `word`: {err}"),
995                )
996            })?,
997    };
998
999    schema.validate_word_value(slot_prefix, "value", word)?;
1000    Ok(word)
1001}
1002
1003fn parse_simple_word_value(
1004    schema_type: &SchemaTypeId,
1005    raw_value: &WordValue,
1006    slot_prefix: &StorageValueName,
1007) -> Result<Word, AccountComponentTemplateError> {
1008    match raw_value {
1009        WordValue::Atomic(value) => {
1010            SCHEMA_TYPE_REGISTRY.try_parse_word(schema_type, value).map_err(|err| {
1011                AccountComponentTemplateError::InvalidInitStorageValue(
1012                    slot_prefix.clone(),
1013                    format!("failed to parse value as `{}`: {err}", schema_type),
1014                )
1015            })
1016        },
1017        WordValue::Elements(elements) => {
1018            let felts: Vec<Felt> = elements
1019                .iter()
1020                .map(|element| {
1021                    SCHEMA_TYPE_REGISTRY.try_parse_felt(&SchemaTypeId::native_felt(), element)
1022                })
1023                .collect::<Result<_, _>>()
1024                .map_err(|err| {
1025                    AccountComponentTemplateError::InvalidInitStorageValue(
1026                        slot_prefix.clone(),
1027                        format!("failed to parse value element as `felt`: {err}"),
1028                    )
1029                })?;
1030            let felts: [Felt; 4] = felts.try_into().expect("length is 4");
1031            Ok(Word::from(felts))
1032        },
1033        WordValue::FullyTyped(word) => Ok(*word),
1034    }
1035}
1036
1037fn parse_composite_elements(
1038    schema: &[FeltSchema; 4],
1039    elements: &[String; 4],
1040    slot_prefix: &StorageValueName,
1041) -> Result<Word, AccountComponentTemplateError> {
1042    let mut felts = [Felt::ZERO; 4];
1043    for (index, felt_schema) in schema.iter().enumerate() {
1044        let felt_type = felt_schema.felt_type();
1045        felts[index] =
1046            SCHEMA_TYPE_REGISTRY
1047                .try_parse_felt(&felt_type, &elements[index])
1048                .map_err(|err| {
1049                    AccountComponentTemplateError::InvalidInitStorageValue(
1050                        slot_prefix.clone(),
1051                        format!("failed to parse value[{index}] as `{felt_type}`: {err}"),
1052                    )
1053                })?;
1054    }
1055    Ok(Word::from(felts))
1056}
1057
1058impl Serializable for MapSlotSchema {
1059    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1060        self.write_into_with_optional_defaults(target, true);
1061    }
1062}
1063
1064impl Deserializable for MapSlotSchema {
1065    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1066        let description = Option::<String>::read_from(source)?;
1067        let default_values = Option::<BTreeMap<Word, Word>>::read_from(source)?;
1068        let key_schema = WordSchema::read_from(source)?;
1069        let value_schema = WordSchema::read_from(source)?;
1070        Ok(MapSlotSchema::new(description, default_values, key_schema, value_schema))
1071    }
1072}
1073
1074// TESTS
1075// ================================================================================================
1076
1077#[cfg(test)]
1078mod tests {
1079    use alloc::collections::BTreeMap;
1080
1081    use super::*;
1082
1083    #[test]
1084    fn map_slot_schema_default_values_returns_map() {
1085        let word_schema = WordSchema::new_simple(SchemaTypeId::native_word());
1086        let mut default_values = BTreeMap::new();
1087        default_values.insert(
1088            Word::from([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]),
1089            Word::from([Felt::new(10), Felt::new(11), Felt::new(12), Felt::new(13)]),
1090        );
1091        let slot = MapSlotSchema::new(
1092            Some("static map".into()),
1093            Some(default_values),
1094            word_schema.clone(),
1095            word_schema,
1096        );
1097
1098        let mut expected = BTreeMap::new();
1099        expected.insert(
1100            Word::from([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]),
1101            Word::from([Felt::new(10), Felt::new(11), Felt::new(12), Felt::new(13)]),
1102        );
1103
1104        assert_eq!(slot.default_values(), Some(expected));
1105    }
1106
1107    #[test]
1108    fn value_slot_schema_exposes_felt_schema_types() {
1109        let felt_values = [
1110            FeltSchema::new_typed(SchemaTypeId::u8(), "a"),
1111            FeltSchema::new_typed(SchemaTypeId::u16(), "b"),
1112            FeltSchema::new_typed(SchemaTypeId::u32(), "c"),
1113            FeltSchema::new_typed(SchemaTypeId::new("felt").unwrap(), "d"),
1114        ];
1115
1116        let slot = ValueSlotSchema::new(None, WordSchema::new_value(felt_values));
1117        let WordSchema::Composite { value } = slot.word() else {
1118            panic!("expected composite word schema");
1119        };
1120
1121        assert_eq!(value[0].felt_type(), SchemaTypeId::u8());
1122        assert_eq!(value[1].felt_type(), SchemaTypeId::u16());
1123        assert_eq!(value[2].felt_type(), SchemaTypeId::u32());
1124        assert_eq!(value[3].felt_type(), SchemaTypeId::new("felt").unwrap());
1125    }
1126
1127    #[test]
1128    fn map_slot_schema_key_and_value_types() {
1129        let key_schema = WordSchema::new_simple(SchemaTypeId::new("sampling::Key").unwrap());
1130
1131        let value_schema = WordSchema::new_value([
1132            FeltSchema::new_typed(SchemaTypeId::native_felt(), "a"),
1133            FeltSchema::new_typed(SchemaTypeId::native_felt(), "b"),
1134            FeltSchema::new_typed(SchemaTypeId::native_felt(), "c"),
1135            FeltSchema::new_typed(SchemaTypeId::native_felt(), "d"),
1136        ]);
1137
1138        let slot = MapSlotSchema::new(None, None, key_schema, value_schema);
1139
1140        assert_eq!(
1141            slot.key_schema(),
1142            &WordSchema::new_simple(SchemaTypeId::new("sampling::Key").unwrap())
1143        );
1144
1145        let WordSchema::Composite { value } = slot.value_schema() else {
1146            panic!("expected composite word schema for map values");
1147        };
1148        for felt in value.iter() {
1149            assert_eq!(felt.felt_type(), SchemaTypeId::native_felt());
1150        }
1151    }
1152
1153    #[test]
1154    fn value_slot_schema_accepts_typed_word_init_value() {
1155        let slot = ValueSlotSchema::new(None, WordSchema::new_simple(SchemaTypeId::native_word()));
1156        let slot_name: StorageSlotName = "demo::slot".parse().unwrap();
1157
1158        let expected = Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]);
1159        let mut init_data = InitStorageData::default();
1160        init_data
1161            .set_value(StorageValueName::from_slot_name(&slot_name), expected)
1162            .unwrap();
1163
1164        let built = slot.try_build_word(&init_data, &slot_name).unwrap();
1165        assert_eq!(built, expected);
1166    }
1167
1168    #[test]
1169    fn value_slot_schema_accepts_felt_typed_word_init_value() {
1170        let slot = ValueSlotSchema::new(None, WordSchema::new_simple(SchemaTypeId::u8()));
1171        let slot_name: StorageSlotName = "demo::u8_word".parse().unwrap();
1172
1173        let mut init_data = InitStorageData::default();
1174        init_data.set_value(StorageValueName::from_slot_name(&slot_name), "6").unwrap();
1175
1176        let built = slot.try_build_word(&init_data, &slot_name).unwrap();
1177        assert_eq!(built, Word::from([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(6)]));
1178    }
1179
1180    #[test]
1181    fn value_slot_schema_accepts_typed_felt_init_value_in_composed_word() {
1182        let word = WordSchema::new_value([
1183            FeltSchema::new_typed(SchemaTypeId::u8(), "a"),
1184            FeltSchema::new_typed_with_default(SchemaTypeId::native_felt(), "b", Felt::new(2)),
1185            FeltSchema::new_typed_with_default(SchemaTypeId::native_felt(), "c", Felt::new(3)),
1186            FeltSchema::new_typed_with_default(SchemaTypeId::native_felt(), "d", Felt::new(4)),
1187        ]);
1188        let slot = ValueSlotSchema::new(None, word);
1189        let slot_name: StorageSlotName = "demo::slot".parse().unwrap();
1190
1191        let mut init_data = InitStorageData::default();
1192        init_data
1193            .set_value(StorageValueName::from_slot_name_with_suffix(&slot_name, "a").unwrap(), "1")
1194            .unwrap();
1195
1196        let built = slot.try_build_word(&init_data, &slot_name).unwrap();
1197        assert_eq!(built, Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]));
1198    }
1199
1200    #[test]
1201    fn map_slot_schema_accepts_typed_map_init_value() {
1202        let word_schema = WordSchema::new_simple(SchemaTypeId::native_word());
1203        let slot = MapSlotSchema::new(None, None, word_schema.clone(), word_schema);
1204        let slot_name: StorageSlotName = "demo::map".parse().unwrap();
1205
1206        let entries = vec![(
1207            WordValue::Elements(["1".into(), "0".into(), "0".into(), "0".into()]),
1208            WordValue::Elements(["10".into(), "11".into(), "12".into(), "13".into()]),
1209        )];
1210        let mut init_data = InitStorageData::default();
1211        init_data.set_map_values(slot_name.clone(), entries.clone()).unwrap();
1212
1213        let built = slot.try_build_map(&init_data, &slot_name).unwrap();
1214        let expected = StorageMap::with_entries([(
1215            Word::from([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]),
1216            Word::from([Felt::new(10), Felt::new(11), Felt::new(12), Felt::new(13)]),
1217        )])
1218        .unwrap();
1219        assert_eq!(built, expected);
1220    }
1221
1222    #[test]
1223    fn map_slot_schema_missing_init_value_defaults_to_empty_map() {
1224        let word_schema = WordSchema::new_simple(SchemaTypeId::native_word());
1225        let slot = MapSlotSchema::new(None, None, word_schema.clone(), word_schema);
1226        let built = slot
1227            .try_build_map(&InitStorageData::default(), &"demo::map".parse().unwrap())
1228            .unwrap();
1229        assert_eq!(built, StorageMap::new());
1230    }
1231}