Skip to main content

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

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