Skip to main content

miden_protocol/account/component/storage/toml/
mod.rs

1use alloc::collections::BTreeMap;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4
5use miden_core::{Felt, Word};
6use semver::Version;
7use serde::de::Error as _;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use super::super::{
11    FeltSchema,
12    MapSlotSchema,
13    StorageSchema,
14    StorageSlotSchema,
15    StorageValueName,
16    ValueSlotSchema,
17    WordSchema,
18    WordValue,
19};
20use crate::account::StorageSlotName;
21use crate::account::component::storage::type_registry::SCHEMA_TYPE_REGISTRY;
22use crate::account::component::{AccountComponentMetadata, SchemaType};
23use crate::errors::ComponentMetadataError;
24
25mod init_storage_data;
26mod serde_impls;
27
28#[cfg(test)]
29mod tests;
30
31// ACCOUNT COMPONENT METADATA TOML FROM/TO
32// ================================================================================================
33
34#[derive(Debug, Deserialize)]
35#[serde(rename_all = "kebab-case", deny_unknown_fields)]
36struct RawAccountComponentMetadata {
37    name: String,
38    description: String,
39    version: Version,
40    #[serde(rename = "storage")]
41    #[serde(default)]
42    storage: RawStorageSchema,
43}
44
45impl AccountComponentMetadata {
46    /// Deserializes `toml_string` and validates the resulting [AccountComponentMetadata]
47    ///
48    /// # Errors
49    ///
50    /// - If deserialization fails
51    /// - If the schema specifies storage slots with duplicates.
52    /// - If the schema contains invalid slot definitions.
53    pub fn from_toml(toml_string: &str) -> Result<Self, ComponentMetadataError> {
54        let raw: RawAccountComponentMetadata = toml::from_str(toml_string)
55            .map_err(ComponentMetadataError::TomlDeserializationError)?;
56
57        if !raw.description.is_ascii() {
58            return Err(ComponentMetadataError::InvalidSchema(
59                "description must contain only ASCII characters".to_string(),
60            ));
61        }
62
63        let RawStorageSchema { slots } = raw.storage;
64        let mut fields = Vec::with_capacity(slots.len());
65
66        for slot in slots {
67            fields.push(slot.try_into_slot_schema()?);
68        }
69
70        let storage_schema = StorageSchema::new(fields)?;
71        Ok(Self::new(raw.name)
72            .with_description(raw.description)
73            .with_version(raw.version)
74            .with_storage_schema(storage_schema))
75    }
76
77    /// Serializes the account component metadata into a TOML string.
78    pub fn to_toml(&self) -> Result<String, ComponentMetadataError> {
79        let toml = toml::to_string(self).map_err(ComponentMetadataError::TomlSerializationError)?;
80        Ok(toml)
81    }
82}
83
84// ACCOUNT STORAGE SCHEMA SERIALIZATION
85// ================================================================================================
86
87/// Raw TOML storage schema:
88///
89/// - `[[storage.slots]]` for both value and map slots.
90///
91/// Slot kind is inferred by the shape of the `type` field:
92/// - `type = "..."` or `type = [ ... ]` => value slot
93/// - `type = { ... }` => map slot
94#[derive(Debug, Default, Deserialize, Serialize)]
95#[serde(rename_all = "kebab-case", deny_unknown_fields)]
96struct RawStorageSchema {
97    #[serde(default)]
98    slots: Vec<RawStorageSlotSchema>,
99}
100
101/// Storage slot type descriptor.
102///
103/// This field accepts either:
104/// - a type identifier (e.g. `"word"`, `"u16"`, `"miden::standards::auth::pub_key"`) for simple
105///   word slots,
106/// - an array of 4 [`FeltSchema`] descriptors for composite word slots, or
107/// - a table `{ key = ..., value = ... }` for map slots.
108#[derive(Debug, Clone, Deserialize, Serialize)]
109#[serde(untagged)]
110enum RawSlotType {
111    Word(RawWordType),
112    Map(RawMapType),
113}
114
115/// A word type descriptor.
116#[derive(Debug, Clone, Deserialize, Serialize)]
117#[serde(untagged)]
118enum RawWordType {
119    TypeIdentifier(SchemaType),
120    FeltSchemaArray(Vec<FeltSchema>),
121}
122
123/// A map type descriptor.
124#[derive(Debug, Clone, Deserialize, Serialize)]
125#[serde(rename_all = "kebab-case", deny_unknown_fields)]
126struct RawMapType {
127    key: RawWordType,
128    value: RawWordType,
129}
130
131// ACCOUNT STORAGE SCHEMA SERDE
132// ================================================================================================
133
134impl Serialize for StorageSchema {
135    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
136    where
137        S: Serializer,
138    {
139        let slots = self
140            .slots()
141            .iter()
142            .map(|(slot_name, schema)| RawStorageSlotSchema::from_slot(slot_name, schema))
143            .collect();
144
145        RawStorageSchema { slots }.serialize(serializer)
146    }
147}
148
149impl<'de> Deserialize<'de> for StorageSchema {
150    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
151    where
152        D: Deserializer<'de>,
153    {
154        // First, look at the raw representation
155        let raw = RawStorageSchema::deserialize(deserializer)?;
156        let mut fields = Vec::with_capacity(raw.slots.len());
157
158        for slot in raw.slots {
159            let (slot_name, schema) = slot.try_into_slot_schema().map_err(D::Error::custom)?;
160            fields.push((slot_name, schema));
161        }
162
163        StorageSchema::new(fields).map_err(D::Error::custom)
164    }
165}
166
167// ACCOUNT STORAGE SCHEMA SERDE HELPERS
168// ================================================================================================
169
170/// Raw storage slot schemas contain the raw representation that can get deserialized from TOML.
171/// Specifically, it expresses the different combination of fields that expose the different types
172/// of slots.
173#[derive(Debug, Deserialize, Serialize)]
174#[serde(rename_all = "kebab-case", deny_unknown_fields)]
175struct RawStorageSlotSchema {
176    /// The name of the storage slot, in `StorageSlotName` format (e.g.
177    /// `my_project::module::slot`).
178    name: String,
179    #[serde(default)]
180    description: Option<String>,
181    /// Slot type descriptor.
182    ///
183    /// - If `type = { ... }`, this is a map slot.
184    /// - If `type = [ ... ]`, this is a composite word slot whose schema is described by 4
185    ///   [`FeltSchema`] descriptors.
186    /// - Otherwise, if `type = "..."`, this is a simple word slot whose value is supplied at
187    ///   instantiation time unless `default-value` is set (or the type is `void`).
188    #[serde(rename = "type")]
189    r#type: RawSlotType,
190    /// The (overridable) default value for a simple word slot.
191    #[serde(default)]
192    default_value: Option<WordValue>,
193    /// Default map entries.
194    ///
195    /// These entries must be fully-specified values. If the map should be populated at
196    /// instantiation time, omit `default-values` and provide entries via init storage data.
197    #[serde(default)]
198    default_values: Option<Vec<RawMapEntrySchema>>,
199}
200
201#[derive(Debug, Deserialize, Serialize)]
202#[serde(deny_unknown_fields)]
203struct RawMapEntrySchema {
204    key: WordValue,
205    value: WordValue,
206}
207
208impl RawStorageSlotSchema {
209    // SERIALIZATION
210    // --------------------------------------------------------------------------------------------
211
212    fn from_slot(slot_name: &StorageSlotName, schema: &StorageSlotSchema) -> Self {
213        match schema {
214            StorageSlotSchema::Value(schema) => Self::from_value_slot(slot_name, schema),
215            StorageSlotSchema::Map(schema) => Self::from_map_slot(slot_name, schema),
216        }
217    }
218
219    fn from_value_slot(slot_name: &StorageSlotName, schema: &ValueSlotSchema) -> Self {
220        let word = schema.word();
221        let (r#type, default_value) = match word {
222            WordSchema::Simple { r#type, default_value } => (
223                RawSlotType::Word(RawWordType::TypeIdentifier(r#type.clone())),
224                default_value.map(|word| WordValue::from_word(r#type, word)),
225            ),
226            WordSchema::Composite { value } => {
227                (RawSlotType::Word(RawWordType::FeltSchemaArray(value.to_vec())), None)
228            },
229        };
230
231        Self {
232            name: slot_name.as_str().to_string(),
233            description: schema.description().cloned(),
234            r#type,
235            default_value,
236            default_values: None,
237        }
238    }
239
240    fn from_map_slot(slot_name: &StorageSlotName, schema: &MapSlotSchema) -> Self {
241        let default_values = schema.default_values().map(|default_values| {
242            default_values
243                .into_iter()
244                .map(|(key, value)| RawMapEntrySchema {
245                    key: WordValue::from_word(&schema.key_schema().word_type(), key),
246                    value: WordValue::from_word(&schema.value_schema().word_type(), value),
247                })
248                .collect()
249        });
250
251        let key_type = match schema.key_schema() {
252            WordSchema::Simple { r#type, .. } => RawWordType::TypeIdentifier(r#type.clone()),
253            WordSchema::Composite { value } => RawWordType::FeltSchemaArray(value.to_vec()),
254        };
255
256        let value_type = match schema.value_schema() {
257            WordSchema::Simple { r#type, .. } => RawWordType::TypeIdentifier(r#type.clone()),
258            WordSchema::Composite { value } => RawWordType::FeltSchemaArray(value.to_vec()),
259        };
260
261        Self {
262            name: slot_name.as_str().to_string(),
263            description: schema.description().cloned(),
264            r#type: RawSlotType::Map(RawMapType { key: key_type, value: value_type }),
265            default_value: None,
266            default_values,
267        }
268    }
269
270    // DESERIALIZATION
271    // --------------------------------------------------------------------------------------------
272
273    /// Converts the raw representation into a tuple of the storage slot name and its schema.
274    fn try_into_slot_schema(
275        self,
276    ) -> Result<(StorageSlotName, StorageSlotSchema), ComponentMetadataError> {
277        let RawStorageSlotSchema {
278            name,
279            description,
280            r#type,
281            default_value,
282            default_values,
283        } = self;
284
285        let slot_name_raw = name;
286        let slot_name = StorageSlotName::new(slot_name_raw.clone()).map_err(|err| {
287            ComponentMetadataError::InvalidSchema(format!(
288                "invalid storage slot name `{slot_name_raw}`: {err}"
289            ))
290        })?;
291
292        let description =
293            description.and_then(|d| if d.trim().is_empty() { None } else { Some(d) });
294
295        let slot_prefix = StorageValueName::from_slot_name(&slot_name);
296
297        if default_value.is_some() && default_values.is_some() {
298            return Err(ComponentMetadataError::InvalidSchema(
299                "storage slot schema cannot define both `default-value` and `default-values`"
300                    .into(),
301            ));
302        }
303
304        match r#type {
305            RawSlotType::Map(map_type) => {
306                if default_value.is_some() {
307                    return Err(ComponentMetadataError::InvalidSchema(
308                        "map slots cannot define `default-value`".into(),
309                    ));
310                }
311
312                let RawMapType { key: key_type, value: value_type } = map_type;
313                let key_schema = Self::parse_word_schema(key_type, "`type.key`")?;
314                let value_schema = Self::parse_word_schema(value_type, "`type.value`")?;
315
316                let default_values = default_values
317                    .map(|entries| {
318                        Self::parse_default_map_entries(
319                            entries,
320                            &key_schema,
321                            &value_schema,
322                            &slot_prefix,
323                        )
324                    })
325                    .transpose()?;
326
327                Ok((
328                    slot_name,
329                    StorageSlotSchema::Map(MapSlotSchema::new(
330                        description,
331                        default_values,
332                        key_schema,
333                        value_schema,
334                    )),
335                ))
336            },
337
338            RawSlotType::Word(word_type) => {
339                if default_values.is_some() {
340                    return Err(ComponentMetadataError::InvalidSchema(
341                        "`default-values` can be specified only for map slots (use `type = { ... }`)"
342                            .into(),
343                    ));
344                }
345
346                match word_type {
347                    RawWordType::TypeIdentifier(r#type) => {
348                        if r#type.as_str() == "map" {
349                            return Err(ComponentMetadataError::InvalidSchema(
350                                "value slots cannot use `type = \"map\"`; use `type = { key = <key-type>, value = <value-type>}` instead"
351                                    .into(),
352                            ));
353                        }
354
355                        let word = default_value
356                            .as_ref()
357                            .map(|default_value| {
358                                default_value.try_parse_as_typed_word(
359                                    &r#type,
360                                    &slot_prefix,
361                                    "default value",
362                                )
363                            })
364                            .transpose()?;
365
366                        let word_schema = match word {
367                            Some(word) => WordSchema::new_simple_with_default(r#type, word),
368                            None => WordSchema::new_simple(r#type),
369                        };
370
371                        Ok((
372                            slot_name,
373                            StorageSlotSchema::Value(ValueSlotSchema::new(
374                                description,
375                                word_schema,
376                            )),
377                        ))
378                    },
379
380                    RawWordType::FeltSchemaArray(elements) => {
381                        if default_value.is_some() {
382                            return Err(ComponentMetadataError::InvalidSchema(
383                                "composite word slots cannot define `default-value`".into(),
384                            ));
385                        }
386
387                        let elements = Self::parse_felt_schema_array(elements, "word slot `type`")?;
388                        Ok((
389                            slot_name,
390                            StorageSlotSchema::Value(ValueSlotSchema::new(
391                                description,
392                                WordSchema::new_value(elements),
393                            )),
394                        ))
395                    },
396                }
397            },
398        }
399    }
400
401    fn parse_word_schema(
402        raw: RawWordType,
403        label: &str,
404    ) -> Result<WordSchema, ComponentMetadataError> {
405        match raw {
406            RawWordType::TypeIdentifier(r#type) => Ok(WordSchema::new_simple(r#type)),
407            RawWordType::FeltSchemaArray(elements) => {
408                let elements = Self::parse_felt_schema_array(elements, label)?;
409                Ok(WordSchema::new_value(elements))
410            },
411        }
412    }
413
414    fn parse_felt_schema_array(
415        elements: Vec<FeltSchema>,
416        label: &str,
417    ) -> Result<[FeltSchema; 4], ComponentMetadataError> {
418        if elements.len() != 4 {
419            return Err(ComponentMetadataError::InvalidSchema(format!(
420                "{label} must be an array of 4 elements, got {}",
421                elements.len()
422            )));
423        }
424        Ok(elements.try_into().expect("length is 4"))
425    }
426
427    fn parse_default_map_entries(
428        entries: Vec<RawMapEntrySchema>,
429        key_schema: &WordSchema,
430        value_schema: &WordSchema,
431        slot_prefix: &StorageValueName,
432    ) -> Result<BTreeMap<Word, Word>, ComponentMetadataError> {
433        let mut map = BTreeMap::new();
434
435        let parse = |schema: &WordSchema, raw: &WordValue, label: &str| {
436            super::schema::parse_storage_value_with_schema(schema, raw, slot_prefix).map_err(
437                |err| {
438                    ComponentMetadataError::InvalidSchema(format!("invalid map `{label}`: {err}"))
439                },
440            )
441        };
442
443        for (index, entry) in entries.into_iter().enumerate() {
444            let key_label = format!("default-values[{index}].key");
445            let value_label = format!("default-values[{index}].value");
446
447            let key = parse(key_schema, &entry.key, &key_label)?;
448            let value = parse(value_schema, &entry.value, &value_label)?;
449
450            if map.insert(key, value).is_some() {
451                return Err(ComponentMetadataError::InvalidSchema(format!(
452                    "map storage slot `default-values[{index}]` contains a duplicate key"
453                )));
454            }
455        }
456
457        Ok(map)
458    }
459}
460
461impl WordValue {
462    pub(super) fn try_parse_as_typed_word(
463        &self,
464        schema_type: &SchemaType,
465        slot_prefix: &StorageValueName,
466        label: &str,
467    ) -> Result<Word, ComponentMetadataError> {
468        let word = match self {
469            WordValue::FullyTyped(word) => *word,
470            WordValue::Atomic(value) => SCHEMA_TYPE_REGISTRY
471                .try_parse_word(schema_type, value)
472                .map_err(ComponentMetadataError::StorageValueParsingError)?,
473            WordValue::Elements(elements) => {
474                let felts = elements
475                    .iter()
476                    .map(|element| {
477                        SCHEMA_TYPE_REGISTRY.try_parse_felt(&SchemaType::native_felt(), element)
478                    })
479                    .collect::<Result<Vec<Felt>, _>>()
480                    .map_err(ComponentMetadataError::StorageValueParsingError)?;
481                let felts: [Felt; 4] = felts.try_into().expect("length is 4");
482                Word::from(felts)
483            },
484        };
485
486        WordSchema::new_simple(schema_type.clone()).validate_word_value(
487            slot_prefix,
488            label,
489            word,
490        )?;
491        Ok(word)
492    }
493
494    pub(super) fn from_word(schema_type: &SchemaType, word: Word) -> Self {
495        WordValue::Atomic(SCHEMA_TYPE_REGISTRY.display_word(schema_type, word).value().to_string())
496    }
497}