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