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