From 98c040f9c204da84d52df9fb311fca464c7ef988 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Wed, 7 Jan 2026 19:14:12 -0300 Subject: [PATCH 01/12] feat: Extend InitStorageData and allow passing native structs --- CHANGELOG.md | 1 + .../src/account/component/metadata/mod.rs | 20 +- .../component/storage/init_storage_data.rs | 224 ++++++++++-- .../src/account/component/storage/mod.rs | 2 +- .../src/account/component/storage/schema.rs | 342 ++++++++++-------- .../storage/toml/init_storage_data.rs | 209 ++++------- .../src/account/component/storage/toml/mod.rs | 16 +- .../account/component/storage/toml/tests.rs | 68 ++-- .../account/component/storage/value_name.rs | 42 ++- 9 files changed, 543 insertions(+), 381 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f44c133564..45cf3a449b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - [BREAKING] Refactored `AccountStorageDelta` to use a new `StorageSlotDelta` type ([#2182](https://github.com/0xMiden/miden-base/pull/2182)). - [BREAKING] Removed OLD_MAP_ROOT from being returned when calling [`native_account::set_map_item`](crates/miden-lib/asm/miden/native_account.masm) ([#2194](https://github.com/0xMiden/miden-base/pull/2194)). - [BREAKING] Refactored account component templates into `AccountStorageSchema` ([#2193](https://github.com/0xMiden/miden-base/pull/2193)). +- [BREAKING] Refactored `InitStorageData` to support native types ([#2230](https://github.com/0xMiden/miden-base/pull/2230)). ## 0.12.4 (2025-11-26) diff --git a/crates/miden-protocol/src/account/component/metadata/mod.rs b/crates/miden-protocol/src/account/component/metadata/mod.rs index 2f96c19274..f29dbbc87c 100644 --- a/crates/miden-protocol/src/account/component/metadata/mod.rs +++ b/crates/miden-protocol/src/account/component/metadata/mod.rs @@ -27,15 +27,15 @@ use crate::AccountError; /// /// - The metadata's storage schema does not contain duplicate slot names. /// - The schema cannot contain protocol-reserved slot names. -/// - Each init-time value name uniquely identifies a single value. The expected init-time -/// requirements can be retrieved with [AccountComponentMetadata::schema_requirements()], which -/// returns a map from keys to [SchemaRequirement] (which indicates the expected value type and -/// optional defaults). +/// - Each init-time value name uniquely identifies a single value. The expected init-time metadata +/// can be retrieved with [AccountComponentMetadata::schema_requirements()], which returns a map +/// from keys to [SchemaRequirement] (which indicates the expected value type and optional +/// defaults). /// /// # Example /// /// ``` -/// use std::collections::BTreeSet; +/// use std::collections::{BTreeMap, BTreeSet}; /// /// use miden_protocol::account::StorageSlotName; /// use miden_protocol::account::component::{ @@ -45,6 +45,7 @@ use crate::AccountError; /// InitStorageData, /// SchemaTypeId, /// StorageSlotSchema, +/// StorageValue, /// StorageValueName, /// ValueSlotSchema, /// WordSchema, @@ -74,10 +75,11 @@ use crate::AccountError; /// ); /// /// // Init value keys are derived from slot name: `demo::test_value.foo`. +/// let value_name = StorageValueName::from_slot_name_with_suffix(&slot_name, "foo")?; /// let init_storage_data = InitStorageData::new( -/// [(StorageValueName::from_slot_name(&slot_name).with_suffix("foo")?, "300".into())], -/// [], -/// ); +/// BTreeMap::from([(value_name, StorageValue::Parseable("300".into()))]), +/// BTreeMap::new(), +/// )?; /// /// let storage_slots = metadata.storage_schema().build_storage_slots(&init_storage_data)?; /// assert_eq!(storage_slots.len(), 1); @@ -123,7 +125,7 @@ impl AccountComponentMetadata { } } - /// Returns the init-time value requirements for this schema. + /// Returns the init-time values's requirements for this schema. /// /// These values are used for initializing storage slot values or storage map entries. For a /// full example, refer to the docs for [AccountComponentMetadata]. diff --git a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs index bb2d5c0a49..a1be0b197e 100644 --- a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs @@ -1,12 +1,16 @@ use alloc::collections::BTreeMap; -use alloc::string::String; +use alloc::string::{String, ToString}; use alloc::vec::Vec; +use thiserror::Error; + use super::StorageValueName; +use crate::account::StorageSlotName; +use crate::{Felt, FieldElement, Word}; /// A raw word value provided via [`InitStorageData`]. /// -/// This is used for defining specific values in relation to a component's schema, where each values +/// This is used for defining specific values in relation to a component's schema, where each value /// is supplied as either an atomic string (e.g. `"0x1234"`, `"16"`, `"BTC"`) or an array of 4 field /// elements. #[derive(Clone, Debug, PartialEq, Eq)] @@ -31,6 +35,61 @@ impl From<&str> for WordValue { } } +// STORAGE VALUE +// ==================================================================================================== + +/// Represents a storage value supplied at initialization time. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum StorageValue { + /// A fully-typed word value. + Word(Word), + /// A raw value which will be parsed into a word using the slot schema. + Parseable(WordValue), +} + +// CONVERSIONS +// ==================================================================================================== + +impl From for StorageValue { + fn from(value: Word) -> Self { + StorageValue::Word(value) + } +} + +impl From for StorageValue { + fn from(value: WordValue) -> Self { + StorageValue::Parseable(value) + } +} + +impl From for StorageValue { + fn from(value: String) -> Self { + StorageValue::Parseable(WordValue::Atomic(value)) + } +} + +impl From<&str> for StorageValue { + fn from(value: &str) -> Self { + StorageValue::Parseable(WordValue::Atomic(String::from(value))) + } +} + +impl From for StorageValue { + /// Converts a [`Felt`] to a [`StorageValue`] as a Word in the form `[0, 0, 0, felt]`. + fn from(value: Felt) -> Self { + StorageValue::Word(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, value])) + } +} + +impl From<[Felt; 4]> for StorageValue { + fn from(value: [Felt; 4]) -> Self { + StorageValue::Word(Word::from(value)) + } +} + +// INIT STORAGE DATA +// ==================================================================================================== + /// Represents the data required to initialize storage entries when instantiating an /// [AccountComponent](crate::account::AccountComponent) from component metadata (either provided /// directly or extracted from a package). @@ -38,40 +97,161 @@ impl From<&str> for WordValue { /// An [`InitStorageData`] can be created from a TOML string when the `std` feature flag is set. #[derive(Clone, Debug, Default)] pub struct InitStorageData { - /// A mapping of init value names to their raw values. - value_entries: BTreeMap, - /// A mapping of storage map slot names to their raw key/value entries. - map_entries: BTreeMap>, + /// A mapping of storage value names to their init values. + value_entries: BTreeMap, + /// A mapping of storage map slot names to their init key/value entries. + map_entries: BTreeMap>, } impl InitStorageData { - /// Creates a new instance of [InitStorageData]. + /// Creates a new instance of [InitStorageData], validating that there are no conflicting + /// entries. + /// + /// # Errors /// - /// A [`BTreeMap`] is constructed from the passed iterator, so duplicate keys will cause - /// overridden values. + /// Returns an error if: + /// - A slot has both value entries and map entries + /// - A slot has both a slot-level value and field values pub fn new( - entries: impl IntoIterator, - map_entries: impl IntoIterator)>, - ) -> Self { - InitStorageData { - value_entries: entries.into_iter().collect(), - map_entries: map_entries.into_iter().collect(), + value_entries: BTreeMap, + map_entries: BTreeMap>, + ) -> Result { + // Check for conflicts between value entries and map entries + for slot_name in map_entries.keys() { + if value_entries.keys().any(|v| v.slot_name() == slot_name) { + return Err(InitStorageDataError::ConflictingEntries(slot_name.as_str().into())); + } + } + + // Check for conflicts between slot-level values and field values + for value_name in value_entries.keys() { + if value_name.field_name().is_none() { + // This is a slot-level value; check if there are field entries for this slot + let has_field_entries = value_entries.keys().any(|other| { + other.slot_name() == value_name.slot_name() && other.field_name().is_some() + }); + if has_field_entries { + return Err(InitStorageDataError::ConflictingEntries( + value_name.slot_name().as_str().into(), + )); + } + } } + + Ok(InitStorageData { value_entries, map_entries }) } /// Returns a reference to the underlying init values map. - pub fn values(&self) -> &BTreeMap { + pub fn values(&self) -> &BTreeMap { &self.value_entries } - /// Returns a reference to the stored init value, or [`Option::None`] if the key is not - /// present. - pub fn get(&self, key: &StorageValueName) -> Option<&WordValue> { - self.value_entries.get(key) + /// Returns a reference to the underlying init map entries. + pub fn maps(&self) -> &BTreeMap> { + &self.map_entries + } + + /// Returns a reference to the stored init value for the given name. + pub fn value_entry(&self, name: &StorageValueName) -> Option<&StorageValue> { + self.value_entries.get(name) + } + + /// Returns a reference to the stored init value for a full slot name. + pub fn slot_value_entry(&self, slot_name: &StorageSlotName) -> Option<&StorageValue> { + let name = StorageValueName::from_slot_name(slot_name); + self.value_entries.get(&name) } /// Returns the map entries associated with the given storage map slot name, if any. - pub fn map_entries(&self, key: &StorageValueName) -> Option<&Vec<(WordValue, WordValue)>> { - self.map_entries.get(key) + pub fn map_entries( + &self, + slot_name: &StorageSlotName, + ) -> Option<&Vec<(StorageValue, StorageValue)>> { + self.map_entries.get(slot_name) } + + /// Merges another [`InitStorageData`] into this one, overwriting value entries and appending + /// map entries. + pub fn merge_from(&mut self, other: InitStorageData) { + self.value_entries.extend(other.value_entries); + for (slot_name, entries) in other.map_entries { + self.map_entries.entry(slot_name).or_default().extend(entries); + } + } + + /// Returns true if any init value entry targets the given slot name. + pub fn has_value_entries_for_slot(&self, slot_name: &StorageSlotName) -> bool { + self.value_entries.keys().any(|name| name.slot_name() == slot_name) + } + + /// Returns true if any init value entry targets a field of the given slot name. + pub fn has_field_entries_for_slot(&self, slot_name: &StorageSlotName) -> bool { + self.value_entries + .keys() + .any(|name| name.slot_name() == slot_name && name.field_name().is_some()) + } + + // MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Inserts a value entry, returning an error on duplicate or conflicting keys. + /// + /// The value can be any type that implements `Into`, e.g.: + /// + /// - `Word`: a fully-typed word value + /// - `[Felt; 4]`: converted to a Word + /// - `Felt`: converted to `[0, 0, 0, felt]` + /// - `String` or `&str`: a parseable string value + /// - `WordValue`: a raw word value (atomic or elements) + pub fn insert_value( + &mut self, + name: StorageValueName, + value: impl Into, + ) -> Result<(), InitStorageDataError> { + if self.value_entries.contains_key(&name) { + return Err(InitStorageDataError::DuplicateKey(name.to_string())); + } + if self.map_entries.contains_key(name.slot_name()) { + return Err(InitStorageDataError::ConflictingEntries(name.slot_name().as_str().into())); + } + self.value_entries.insert(name, value.into()); + Ok(()) + } + + /// Inserts map entries, returning an error if there are conflicting value entries. + pub fn set_map_values( + &mut self, + slot_name: StorageSlotName, + entries: Vec<(StorageValue, StorageValue)>, + ) -> Result<(), InitStorageDataError> { + if self.has_value_entries_for_slot(&slot_name) { + return Err(InitStorageDataError::ConflictingEntries(slot_name.as_str().into())); + } + self.map_entries.entry(slot_name).or_default().extend(entries); + Ok(()) + } + + /// Inserts a single map entry. + /// + /// See [`Self::insert_value`] for examples of supported types for `key` and `value`. + pub fn insert_map_entry( + &mut self, + slot_name: StorageSlotName, + key: impl Into, + value: impl Into, + ) { + self.map_entries.entry(slot_name).or_default().push((key.into(), value.into())); + } +} + +// ERRORS +// ==================================================================================================== + +/// Error returned when creating [`InitStorageData`] with invalid entries. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum InitStorageDataError { + #[error("duplicate init key `{0}`")] + DuplicateKey(String), + #[error("conflicting init entries for `{0}`")] + ConflictingEntries(String), } diff --git a/crates/miden-protocol/src/account/component/storage/mod.rs b/crates/miden-protocol/src/account/component/storage/mod.rs index 2c28296c49..4933e935a9 100644 --- a/crates/miden-protocol/src/account/component/storage/mod.rs +++ b/crates/miden-protocol/src/account/component/storage/mod.rs @@ -8,7 +8,7 @@ mod type_registry; pub use type_registry::{SchemaRequirement, SchemaTypeError, SchemaTypeId}; mod init_storage_data; -pub use init_storage_data::{InitStorageData, WordValue}; +pub use init_storage_data::{InitStorageData, InitStorageDataError, StorageValue, WordValue}; #[cfg(feature = "std")] pub mod toml; diff --git a/crates/miden-protocol/src/account/component/storage/schema.rs b/crates/miden-protocol/src/account/component/storage/schema.rs index 9ae69f7b7a..203a01e4f7 100644 --- a/crates/miden-protocol/src/account/component/storage/schema.rs +++ b/crates/miden-protocol/src/account/component/storage/schema.rs @@ -7,7 +7,7 @@ use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; use miden_processor::DeserializationError; use super::type_registry::{SCHEMA_TYPE_REGISTRY, SchemaRequirement, SchemaTypeId}; -use super::{InitStorageData, StorageValueName, WordValue}; +use super::{InitStorageData, StorageValue, StorageValueName, WordValue}; use crate::account::storage::is_reserved_slot_name; use crate::account::{StorageMap, StorageSlot, StorageSlotName}; use crate::errors::AccountComponentTemplateError; @@ -162,14 +162,13 @@ impl StorageSlotSchema { slot_name: &StorageSlotName, init_storage_data: &InitStorageData, ) -> Result { - let slot_prefix = StorageValueName::from_slot_name(slot_name); match self { StorageSlotSchema::Value(slot) => { - let word = slot.try_build_word(init_storage_data, slot_prefix)?; + let word = slot.try_build_word(init_storage_data, slot_name)?; Ok(StorageSlot::with_value(slot_name.clone(), word)) }, StorageSlotSchema::Map(slot) => { - let storage_map = slot.try_build_map(init_storage_data, slot_prefix)?; + let storage_map = slot.try_build_map(init_storage_data, slot_name)?; Ok(StorageSlot::with_map(slot_name.clone(), storage_map)) }, } @@ -339,47 +338,59 @@ impl WordSchema { pub(crate) fn try_build_word( &self, init_storage_data: &InitStorageData, - value_prefix: StorageValueName, + slot_name: &StorageSlotName, ) -> Result { + let slot_prefix = StorageValueName::from_slot_name(slot_name); + let slot_value = init_storage_data.slot_value_entry(slot_name); + let has_fields = init_storage_data.has_field_entries_for_slot(slot_name); + let has_map = init_storage_data.map_entries(slot_name).is_some(); match self { WordSchema::Simple { r#type, default_value } => { - let value_name = value_prefix; - match init_storage_data.get(&value_name) { - Some(WordValue::Atomic(raw)) => SCHEMA_TYPE_REGISTRY - .try_parse_word(r#type, raw) - .map_err(AccountComponentTemplateError::StorageValueParsingError), - Some(WordValue::Elements(elements)) => { - let felts = elements - .iter() - .map(|element| { - SCHEMA_TYPE_REGISTRY - .try_parse_felt(&SchemaTypeId::native_felt(), element) - }) - .collect::, _>>() - .map_err(AccountComponentTemplateError::StorageValueParsingError)?; - let felts: [Felt; 4] = felts.try_into().expect("length is 4"); - let word = Word::from(felts); - SCHEMA_TYPE_REGISTRY - .validate_word_value(r#type, word) - .map_err(AccountComponentTemplateError::StorageValueParsingError)?; - Ok(word) - }, + if has_map { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a value, got a map".into(), + )); + } + if has_fields { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a value, got field entries".into(), + )); + } + match slot_value { + Some(value) => parse_storage_value_with_schema(self, value, &slot_prefix), None => { if *r#type == SchemaTypeId::void() { Ok(Word::empty()) } else { default_value.as_ref().copied().ok_or_else(|| { - AccountComponentTemplateError::InitValueNotProvided(value_name) + AccountComponentTemplateError::InitValueNotProvided(slot_prefix) }) } }, } }, WordSchema::Composite { value } => { + if has_map { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a value, got a map".into(), + )); + } + if let Some(value) = slot_value { + if has_fields { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a single value, got both value and field entries".into(), + )); + } + return parse_storage_value_with_schema(self, value, &slot_prefix); + } + let mut result = [Felt::ZERO; 4]; for (index, felt_schema) in value.iter().enumerate() { - result[index] = - felt_schema.try_build_felt(init_storage_data, value_prefix.clone())?; + result[index] = felt_schema.try_build_felt(init_storage_data, slot_name)?; } Ok(Word::from(result)) }, @@ -560,10 +571,9 @@ impl FeltSchema { "non-void felt elements must be named".into(), )); }; - let value_name = slot_prefix - .clone() - .with_suffix(name) - .map_err(|err| AccountComponentTemplateError::InvalidSchema(err.to_string()))?; + let value_name = + StorageValueName::from_slot_name_with_suffix(slot_prefix.slot_name(), name) + .map_err(|err| AccountComponentTemplateError::InvalidSchema(err.to_string()))?; let default_value = self .default_value @@ -593,31 +603,38 @@ impl FeltSchema { pub(crate) fn try_build_felt( &self, init_storage_data: &InitStorageData, - value_prefix: StorageValueName, + slot_name: &StorageSlotName, ) -> Result { - let value_name = - match self.name.as_deref() { - Some(name) => Some(value_prefix.with_suffix(name).map_err(|err| { - AccountComponentTemplateError::InvalidSchema(err.to_string()) - })?), - None => None, - }; - - if let Some(value_name) = value_name.clone() { - match init_storage_data.get(&value_name) { - Some(WordValue::Atomic(raw)) => { + let value_name = match self.name.as_deref() { + Some(name) => Some( + StorageValueName::from_slot_name_with_suffix(slot_name, name) + .map_err(|err| AccountComponentTemplateError::InvalidSchema(err.to_string()))?, + ), + None => None, + }; + + if let Some(value_name) = value_name.clone() + && let Some(raw_value) = init_storage_data.value_entry(&value_name) + { + match raw_value { + StorageValue::Parseable(WordValue::Atomic(raw)) => { let felt = SCHEMA_TYPE_REGISTRY .try_parse_felt(&self.r#type, raw) .map_err(AccountComponentTemplateError::StorageValueParsingError)?; return Ok(felt); }, - Some(WordValue::Elements(_)) => { + StorageValue::Parseable(WordValue::Elements(_)) => { return Err(AccountComponentTemplateError::InvalidInitStorageValue( value_name, "expected an atomic value, got a 4-element array".into(), )); }, - None => {}, + StorageValue::Word(_) => { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + value_name, + "expected an atomic value, got a word".into(), + )); + }, } } @@ -731,9 +748,9 @@ impl ValueSlotSchema { pub fn try_build_word( &self, init_storage_data: &InitStorageData, - value_prefix: StorageValueName, + slot_name: &StorageSlotName, ) -> Result { - self.word.try_build_word(init_storage_data, value_prefix) + self.word.try_build_word(init_storage_data, slot_name) } pub(crate) fn validate( @@ -791,46 +808,33 @@ impl MapSlotSchema { pub fn try_build_map( &self, init_storage_data: &InitStorageData, - slot_prefix: StorageValueName, + slot_name: &StorageSlotName, ) -> Result { let mut entries = self.default_values.clone().unwrap_or_default(); + let slot_prefix = StorageValueName::from_slot_name(slot_name); - if init_storage_data.get(&slot_prefix).is_some() - && init_storage_data.map_entries(&slot_prefix).is_none() - { + if init_storage_data.slot_value_entry(slot_name).is_some() { return Err(AccountComponentTemplateError::InvalidInitStorageValue( slot_prefix, "expected a map, got a value".into(), )); } - - if let Some(init_entries) = init_storage_data.map_entries(&slot_prefix) { + if init_storage_data.has_field_entries_for_slot(slot_name) { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a map, got field entries".into(), + )); + } + if let Some(init_entries) = init_storage_data.map_entries(slot_name) { let mut parsed_entries = Vec::with_capacity(init_entries.len()); - for (index, (raw_key, raw_value)) in init_entries.iter().enumerate() { - let key_label = format!("map entry[{index}].key"); - let value_label = format!("map entry[{index}].value"); - - let key = parse_word_value_with_schema( - &self.key_schema, - raw_key, - &slot_prefix, - key_label.as_str(), - )?; - let value = parse_word_value_with_schema( - &self.value_schema, - raw_value, - &slot_prefix, - value_label.as_str(), - )?; + for (raw_key, raw_value) in init_entries.iter() { + let key = parse_storage_value_with_schema(&self.key_schema, raw_key, &slot_prefix)?; + let value = + parse_storage_value_with_schema(&self.value_schema, raw_value, &slot_prefix)?; parsed_entries.push((key, value)); } - // Reject duplicate keys in init-provided entries. - let _ = StorageMap::with_entries(parsed_entries.iter().copied()).map_err(|err| { - AccountComponentTemplateError::StorageMapHasDuplicateKeys(Box::new(err)) - })?; - for (key, value) in parsed_entries.iter() { entries.insert(*key, *value); } @@ -863,70 +867,91 @@ impl MapSlotSchema { } } -pub(super) fn parse_word_value_with_schema( +pub(super) fn parse_storage_value_with_schema( schema: &WordSchema, - raw_value: &WordValue, + raw_value: &StorageValue, slot_prefix: &StorageValueName, - label: &str, ) -> Result { - match schema { - WordSchema::Simple { r#type, .. } => match raw_value { - WordValue::Atomic(value) => { - SCHEMA_TYPE_REGISTRY.try_parse_word(r#type, value).map_err(|err| { - AccountComponentTemplateError::InvalidInitStorageValue( - slot_prefix.clone(), - format!("failed to parse {label} as `{}`: {err}", r#type), - ) - }) + let word = match raw_value { + StorageValue::Word(word) => *word, + StorageValue::Parseable(raw_value) => match schema { + WordSchema::Simple { r#type, .. } => { + parse_simple_word_value(r#type, raw_value, slot_prefix)? }, - WordValue::Elements(elements) => { - let felts: Vec = elements - .iter() - .map(|element| { - SCHEMA_TYPE_REGISTRY.try_parse_felt(&SchemaTypeId::native_felt(), element) - }) - .collect::>() + WordSchema::Composite { value } => match raw_value { + WordValue::Elements(elements) => { + parse_composite_elements(value, elements, slot_prefix)? + }, + WordValue::Atomic(value) => SCHEMA_TYPE_REGISTRY + .try_parse_word(&SchemaTypeId::native_word(), value) .map_err(|err| { AccountComponentTemplateError::InvalidInitStorageValue( slot_prefix.clone(), - format!("failed to parse {label} element as `felt`: {err}"), + format!("failed to parse value as `word`: {err}"), ) - })?; - let felts: [Felt; 4] = felts.try_into().expect("length is 4"); - let word = Word::from(felts); - schema.validate_word_value(slot_prefix, label, word)?; - Ok(word) + })?, }, }, - WordSchema::Composite { value } => match raw_value { - WordValue::Elements(elements) => { - let mut felts = [Felt::ZERO; 4]; - for index in 0..4 { - let felt_type = value[index].felt_type(); - felts[index] = SCHEMA_TYPE_REGISTRY - .try_parse_felt(&felt_type, &elements[index]) - .map_err(|err| { - AccountComponentTemplateError::InvalidInitStorageValue( - slot_prefix.clone(), - format!("failed to parse {label}[{index}] as `{felt_type}`: {err}"), - ) - })?; - } + }; - Ok(Word::from(felts)) - }, - WordValue::Atomic(value) => { - Err(AccountComponentTemplateError::InvalidInitStorageValue( + schema.validate_word_value(slot_prefix, "value", word)?; + Ok(word) +} + +fn parse_simple_word_value( + schema_type: &SchemaTypeId, + raw_value: &WordValue, + slot_prefix: &StorageValueName, +) -> Result { + match raw_value { + WordValue::Atomic(value) => { + SCHEMA_TYPE_REGISTRY.try_parse_word(schema_type, value).map_err(|err| { + AccountComponentTemplateError::InvalidInitStorageValue( slot_prefix.clone(), - format!( - "{label} must be an array of 4 elements for a composite schema, got atomic `{value}`" - ), - )) - }, + format!("failed to parse value as `{}`: {err}", schema_type), + ) + }) + }, + WordValue::Elements(elements) => { + let felts: Vec = elements + .iter() + .map(|element| { + SCHEMA_TYPE_REGISTRY.try_parse_felt(&SchemaTypeId::native_felt(), element) + }) + .collect::>() + .map_err(|err| { + AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix.clone(), + format!("failed to parse value element as `felt`: {err}"), + ) + })?; + let felts: [Felt; 4] = felts.try_into().expect("length is 4"); + Ok(Word::from(felts)) }, } } +fn parse_composite_elements( + schema: &[FeltSchema; 4], + elements: &[String; 4], + slot_prefix: &StorageValueName, +) -> Result { + let mut felts = [Felt::ZERO; 4]; + for (index, felt_schema) in schema.iter().enumerate() { + let felt_type = felt_schema.felt_type(); + felts[index] = + SCHEMA_TYPE_REGISTRY + .try_parse_felt(&felt_type, &elements[index]) + .map_err(|err| { + AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix.clone(), + format!("failed to parse value[{index}] as `{felt_type}`: {err}"), + ) + })?; + } + Ok(Word::from(felts)) +} + impl Serializable for MapSlotSchema { fn write_into(&self, target: &mut W) { target.write(&self.description); @@ -1028,24 +1053,37 @@ mod tests { #[test] fn value_slot_schema_accepts_typed_word_init_value() { let slot = ValueSlotSchema::new(None, WordSchema::new_simple(SchemaTypeId::native_word())); - let slot_prefix: StorageValueName = "demo::slot".parse().unwrap(); + let slot_name: StorageSlotName = "demo::slot".parse().unwrap(); let expected = Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); - let init_data = - InitStorageData::new([(slot_prefix.clone(), expected.to_string().into())], []); + let init_data = InitStorageData::new( + BTreeMap::from([( + StorageValueName::from_slot_name(&slot_name), + StorageValue::Word(expected), + )]), + BTreeMap::new(), + ) + .unwrap(); - let built = slot.try_build_word(&init_data, slot_prefix).unwrap(); + let built = slot.try_build_word(&init_data, &slot_name).unwrap(); assert_eq!(built, expected); } #[test] fn value_slot_schema_accepts_felt_typed_word_init_value() { let slot = ValueSlotSchema::new(None, WordSchema::new_simple(SchemaTypeId::u8())); - let slot_prefix: StorageValueName = "demo::u8_word".parse().unwrap(); - - let init_data = InitStorageData::new([(slot_prefix.clone(), "6".into())], []); + let slot_name: StorageSlotName = "demo::u8_word".parse().unwrap(); + + let init_data = InitStorageData::new( + BTreeMap::from([( + StorageValueName::from_slot_name(&slot_name), + StorageValue::Parseable("6".into()), + )]), + BTreeMap::new(), + ) + .unwrap(); - let built = slot.try_build_word(&init_data, slot_prefix).unwrap(); + let built = slot.try_build_word(&init_data, &slot_name).unwrap(); assert_eq!(built, Word::from([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(6)])); } @@ -1058,10 +1096,18 @@ mod tests { FeltSchema::new_typed_with_default(SchemaTypeId::native_felt(), "d", Felt::new(4)), ]); let slot = ValueSlotSchema::new(None, word); + let slot_name: StorageSlotName = "demo::slot".parse().unwrap(); + + let init_data = InitStorageData::new( + BTreeMap::from([( + StorageValueName::from_slot_name_with_suffix(&slot_name, "a").unwrap(), + StorageValue::Parseable("1".into()), + )]), + BTreeMap::new(), + ) + .unwrap(); - let init_data = InitStorageData::new([("demo::slot.a".parse().unwrap(), "1".into())], []); - - let built = slot.try_build_word(&init_data, "demo::slot".parse().unwrap()).unwrap(); + let built = slot.try_build_word(&init_data, &slot_name).unwrap(); assert_eq!(built, Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)])); } @@ -1069,15 +1115,29 @@ mod tests { fn map_slot_schema_accepts_typed_map_init_value() { let word_schema = WordSchema::new_simple(SchemaTypeId::native_word()); let slot = MapSlotSchema::new(None, None, word_schema.clone(), word_schema); - let slot_prefix: StorageValueName = "demo::map".parse().unwrap(); + let slot_name: StorageSlotName = "demo::map".parse().unwrap(); let entries = vec![( - WordValue::Elements(["1".into(), "0".into(), "0".into(), "0".into()]), - WordValue::Elements(["10".into(), "11".into(), "12".into(), "13".into()]), + StorageValue::Parseable(WordValue::Elements([ + "1".into(), + "0".into(), + "0".into(), + "0".into(), + ])), + StorageValue::Parseable(WordValue::Elements([ + "10".into(), + "11".into(), + "12".into(), + "13".into(), + ])), )]; - let init_data = InitStorageData::new([], [(slot_prefix.clone(), entries.clone())]); + let init_data = InitStorageData::new( + BTreeMap::new(), + BTreeMap::from([(slot_name.clone(), entries.clone())]), + ) + .unwrap(); - let built = slot.try_build_map(&init_data, slot_prefix).unwrap(); + let built = slot.try_build_map(&init_data, &slot_name).unwrap(); let expected = StorageMap::with_entries([( Word::from([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]), Word::from([Felt::new(10), Felt::new(11), Felt::new(12), Felt::new(13)]), @@ -1091,7 +1151,7 @@ mod tests { let word_schema = WordSchema::new_simple(SchemaTypeId::native_word()); let slot = MapSlotSchema::new(None, None, word_schema.clone(), word_schema); let built = slot - .try_build_map(&InitStorageData::default(), "demo::map".parse().unwrap()) + .try_build_map(&InitStorageData::default(), &"demo::map".parse().unwrap()) .unwrap(); assert_eq!(built, StorageMap::new()); } diff --git a/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs index 72a4a79044..e91ffba37f 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs @@ -1,148 +1,81 @@ -use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec::Vec; use serde::Deserialize; use thiserror::Error; -use super::super::{InitStorageData, StorageValueName, StorageValueNameError, WordValue}; +use super::super::{ + InitStorageData, + InitStorageDataError as CoreInitStorageDataError, + StorageValue, + StorageValueName, + StorageValueNameError, + WordValue, +}; use super::RawMapEntrySchema; impl InitStorageData { /// Creates an instance of [`InitStorageData`] from a TOML string. - /// - /// This method parses the provided TOML and flattens nested tables into - /// dot‑separated keys using [`StorageValueName`] as keys. - /// - /// Atomic values must be strings (e.g. `"0x1234"`, `"16"`, `"BTC"`). - /// - /// Arrays are supported for: - /// - storage map slots: an array of inline tables of the form `{ key = , value = - /// }`, - /// - word values: a 4-element array of field elements. - /// - /// # Errors - /// - /// - If the TOML string fails to parse - /// - If duplicate keys are found after parsing - /// - If empty tables are found in the string - /// - If the TOML string includes unsupported arrays pub fn from_toml(toml_str: &str) -> Result { - // TOML documents are always parsed as a root table. let table: toml::Table = toml::from_str(toml_str)?; - let mut value_entries = BTreeMap::new(); - let mut map_entries = BTreeMap::new(); - // Start at the root (no prefix yet). - Self::flatten_parse_toml_value( - None, - toml::Value::Table(table), - &mut value_entries, - &mut map_entries, - )?; + let mut data = InitStorageData::default(); - Ok(InitStorageData::new(value_entries, map_entries)) - } + for (key, value) in table { + let name: StorageValueName = + key.parse().map_err(InitStorageDataError::InvalidStorageValueName)?; - /// Recursively flattens a TOML `Value` into a flat mapping. - /// - /// When recursing into nested tables, keys are combined using - /// [`StorageValueName::with_suffix`]. If an encountered table is empty (and not the top-level), - /// an error is returned. - fn flatten_parse_toml_value( - prefix: Option, - value: toml::Value, - value_entries: &mut BTreeMap, - map_entries: &mut BTreeMap>, - ) -> Result<(), InitStorageDataError> { - match value { - toml::Value::Table(table) => { - // If this is not the root and the table is empty, error - if let Some(prefix) = prefix.as_ref() - && table.is_empty() - { - return Err(InitStorageDataError::EmptyTable(prefix.to_string())); - } - for (key, val) in table { - let new_prefix = match prefix.as_ref() { - None => { - key.parse().map_err(InitStorageDataError::InvalidStorageValueName)? - }, - Some(prefix) => prefix - .clone() - .with_suffix(&key) - .map_err(InitStorageDataError::InvalidStorageValueName)?, - }; - Self::flatten_parse_toml_value( - Some(new_prefix), - val, - value_entries, - map_entries, - )?; - } - }, - toml::Value::Array(items) if items.is_empty() => { - let prefix = prefix.expect("arrays must have a key prefix"); - if value_entries.contains_key(&prefix) || map_entries.contains_key(&prefix) { - return Err(InitStorageDataError::DuplicateKey(prefix.to_string())); - } - map_entries.insert(prefix, Vec::new()); - }, - toml::Value::Array(items) => { - let prefix = prefix.expect("arrays must have a key prefix"); - // Arrays can be either: - // - map entries: an array of inline tables `{ key = ..., value = ... }` - // - a 4-element word value: an array of 4 field elements - if items.iter().all(|item| matches!(item, toml::Value::Table(_))) { - let entries = items.into_iter().map(parse_map_entry_value).collect::, - _, - >>( - )?; - if value_entries.contains_key(&prefix) || map_entries.contains_key(&prefix) { - return Err(InitStorageDataError::DuplicateKey(prefix.to_string())); + match value { + // ["slot::name"] + // field = "value" + toml::Value::Table(nested) => { + if nested.is_empty() { + return Err(InitStorageDataError::EmptyTable(name.to_string())); + } + if name.field_name().is_some() { + return Err(InitStorageDataError::ExcessiveNesting(name.to_string())); + } + for (field, field_value) in nested { + let field_name = + StorageValueName::from_slot_name_with_suffix(name.slot_name(), &field) + .map_err(InitStorageDataError::InvalidStorageValueName)?; + let word = WordValue::deserialize(field_value).map_err(|_| { + InitStorageDataError::InvalidValue(field_name.to_string()) + })?; + data.insert_value(field_name, StorageValue::Parseable(word))?; } - map_entries.insert(prefix, entries); - } else if items.len() == 4 - && items.iter().all(|item| matches!(item, toml::Value::String(_))) + }, + // "slot::name" = [{ key = "...", value = "..." }, ...] + toml::Value::Array(items) + if items.iter().all(|v| matches!(v, toml::Value::Table(_))) => { - let elements: [String; 4] = items - .into_iter() - .map(|value| match value { - toml::Value::String(s) => Ok(s), - _ => Err(InitStorageDataError::ArraysNotSupported { - key: prefix.to_string(), - len: 4, - }), - }) - .collect::, _>>()? - .try_into() - .expect("length was checked above"); - if value_entries.contains_key(&prefix) || map_entries.contains_key(&prefix) { - return Err(InitStorageDataError::DuplicateKey(prefix.to_string())); + if name.field_name().is_some() { + return Err(InitStorageDataError::InvalidMapEntryKey(name.to_string())); } - value_entries.insert(prefix, WordValue::Elements(elements)); - } else { - return Err(InitStorageDataError::ArraysNotSupported { - key: prefix.to_string(), - len: items.len(), - }); - } - }, - toml_value => match toml_value { - toml::Value::String(s) => { - let prefix = prefix.expect("atomic values must have a key prefix"); - if value_entries.contains_key(&prefix) || map_entries.contains_key(&prefix) { - return Err(InitStorageDataError::DuplicateKey(prefix.to_string())); + let mut entries = Vec::with_capacity(items.len()); + for item in items { + // Try deserializing as map entry + let entry: RawMapEntrySchema = RawMapEntrySchema::deserialize(item) + .map_err(|e| { + InitStorageDataError::InvalidMapEntrySchema(e.to_string()) + })?; + + entries.push(( + StorageValue::Parseable(entry.key), + StorageValue::Parseable(entry.value), + )); } - value_entries.insert(prefix, WordValue::Atomic(s)); + data.set_map_values(name.slot_name().clone(), entries)?; }, - _ => { - let prefix = prefix.expect("atomic values must have a key prefix"); - return Err(InitStorageDataError::NonStringAtomic(prefix.to_string())); + // "slot::name" = "value" or "slot::name" = ["a", "b", "c", "d"] + other => { + let word = WordValue::deserialize(other) + .map_err(|_| InitStorageDataError::InvalidValue(name.to_string()))?; + data.insert_value(name, StorageValue::Parseable(word))?; }, - }, + } } - Ok(()) + + Ok(data) } } @@ -154,16 +87,19 @@ pub enum InitStorageDataError { #[error("empty table encountered for key `{0}`")] EmptyTable(String), - #[error("duplicate init key `{0}`")] - DuplicateKey(String), + #[error(transparent)] + InvalidData(#[from] CoreInitStorageDataError), + + #[error("invalid map entry key `{0}`: map entries must target a slot name")] + InvalidMapEntryKey(String), + + #[error("excessive nesting for key `{0}`: only one level of table nesting is allowed")] + ExcessiveNesting(String), #[error( - "invalid input for `{key}`: unsupported array value (length {len}); expected either a map entry list (array of inline tables with `key` and `value`) or a 4-element word array of strings" + "invalid input for `{0}`: expected a string, a 4-element string array, or a map entry list" )] - ArraysNotSupported { key: String, len: usize }, - - #[error("invalid input for `{0}`: init values must be strings")] - NonStringAtomic(String), + InvalidValue(String), #[error("invalid storage value name")] InvalidStorageValueName(#[source] StorageValueNameError), @@ -171,14 +107,3 @@ pub enum InitStorageDataError { #[error("invalid map entry: {0}")] InvalidMapEntrySchema(String), } - -/// Parses a `{ key, value }` table into a `(Word, Word)` pair, rejecting typed fields. -fn parse_map_entry_value( - item: toml::Value, -) -> Result<(WordValue, WordValue), InitStorageDataError> { - // Try to deserialize the user input as a map entry - let entry: RawMapEntrySchema = RawMapEntrySchema::deserialize(item) - .map_err(|err| InitStorageDataError::InvalidMapEntrySchema(err.to_string()))?; - - Ok((entry.key, entry.value)) -} diff --git a/crates/miden-protocol/src/account/component/storage/toml/mod.rs b/crates/miden-protocol/src/account/component/storage/toml/mod.rs index 1269e7db8b..b803c77bc0 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/mod.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/mod.rs @@ -12,6 +12,7 @@ use super::super::{ FeltSchema, MapSlotSchema, StorageSlotSchema, + StorageValue, StorageValueName, ValueSlotSchema, WordSchema, @@ -432,13 +433,16 @@ impl RawStorageSlotSchema { let mut map = BTreeMap::new(); let parse = |schema: &WordSchema, raw: &WordValue, label: &str| { - super::schema::parse_word_value_with_schema(schema, raw, slot_prefix, label).map_err( - |err| { - AccountComponentTemplateError::InvalidSchema(format!( - "invalid map `{label}`: {err}" - )) - }, + super::schema::parse_storage_value_with_schema( + schema, + &StorageValue::Parseable(raw.clone()), + slot_prefix, ) + .map_err(|err| { + AccountComponentTemplateError::InvalidSchema(format!( + "invalid map `{label}`: {err}" + )) + }) }; for (index, entry) in entries.into_iter().enumerate() { diff --git a/crates/miden-protocol/src/account/component/storage/toml/tests.rs b/crates/miden-protocol/src/account/component/storage/toml/tests.rs index 419992673e..c52babe8e6 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/tests.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/tests.rs @@ -7,8 +7,10 @@ use crate::account::component::toml::init_storage_data::InitStorageDataError; use crate::account::component::{ AccountComponentMetadata, InitStorageData, + InitStorageDataError as CoreInitStorageDataError, SchemaTypeId, StorageSlotSchema, + StorageValue, StorageValueName, WordSchema, WordValue, @@ -36,6 +38,7 @@ fn from_toml_str_with_nested_table_and_flattened() { let storage_inline = InitStorageData::from_toml(toml_inline).unwrap(); assert_eq!(storage_table.values(), storage_inline.values()); + assert_eq!(storage_table.maps(), storage_inline.maps()); } #[test] @@ -48,7 +51,7 @@ fn from_toml_str_with_deeply_nested_tables_is_rejected() { assert_matches::assert_matches!( InitStorageData::from_toml(toml_str), - Err(InitStorageDataError::InvalidStorageValueName(_)) + Err(InitStorageDataError::InvalidValue(_)) ); } @@ -59,22 +62,17 @@ fn from_toml_rejects_non_string_atomics() { "#; let result = InitStorageData::from_toml(toml_str); - assert_matches::assert_matches!(result.unwrap_err(), InitStorageDataError::NonStringAtomic(_)); + assert_matches::assert_matches!(result.unwrap_err(), InitStorageDataError::InvalidValue(_)); } #[test] fn test_error_on_array() { let toml_str = r#" - ["demo::token_metadata"] - v = ["1", "2", "3", "4", "5"] + "demo::token_metadata.v" = ["1", "2", "3", "4", "5"] "#; let err = InitStorageData::from_toml(toml_str).unwrap_err(); - assert_matches::assert_matches!( - &err, - InitStorageDataError::ArraysNotSupported { key, len } - if key == "demo::token_metadata.v" && *len == 5 - ); + assert_matches::assert_matches!(&err, InitStorageDataError::InvalidValue(key) if key == "demo::token_metadata.v"); } #[test] @@ -87,26 +85,30 @@ fn parse_map_entries_from_array() { "#; let storage = InitStorageData::from_toml(toml_str).expect("Failed to parse map entries"); - let map_name: StorageValueName = "demo::my_map".parse().unwrap(); + let map_name: StorageSlotName = "demo::my_map".parse().unwrap(); let entries = storage.map_entries(&map_name).expect("map entries missing"); assert_eq!(entries.len(), 2); assert_matches::assert_matches!( &entries[0].0, - WordValue::Atomic(v) + StorageValue::Parseable(WordValue::Atomic(v)) if v == "0x0000000000000000000000000000000000000000000000000000000000000001" ); assert_matches::assert_matches!( &entries[0].1, - WordValue::Atomic(v) + StorageValue::Parseable(WordValue::Atomic(v)) if v == "0x0000000000000000000000000000000000000000000000000000000000000010" ); - assert_matches::assert_matches!(&entries[1].1, WordValue::Elements(elements) if elements == &[ - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - ]); + assert_matches::assert_matches!( + &entries[1].1, + StorageValue::Parseable(WordValue::Elements(elements)) + if elements == &[ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + ] + ); } #[test] @@ -119,7 +121,7 @@ fn error_on_empty_subtable() { let result = InitStorageData::from_toml(toml_str); assert_matches::assert_matches!( result.unwrap_err(), - InitStorageDataError::EmptyTable(key) if key == "demo::token_metadata.max_supply" + InitStorageDataError::InvalidValue(key) if key == "demo::token_metadata.max_supply" ); } @@ -149,7 +151,8 @@ fn error_on_duplicate_keys_after_flattening() { let err = InitStorageData::from_toml(toml_str).unwrap_err(); assert_matches::assert_matches!( err, - InitStorageDataError::DuplicateKey(key) if key == "demo::token_metadata.max_supply" + InitStorageDataError::InvalidData(CoreInitStorageDataError::DuplicateKey(key)) + if key == "demo::token_metadata.max_supply" ); } @@ -200,29 +203,6 @@ fn metadata_from_toml_rejects_typed_fields_in_static_map_values() { ); } -#[test] -fn metadata_from_toml_rejects_short_composite_schema() { - let toml_str = r#" - name = "Test Component" - description = "Test description" - version = "0.1.0" - supported-types = [] - - [[storage.slots]] - name = "demo::short_composite" - type = [ - { type = "u8", name = "a" }, - { type = "void" }, - { type = "void" }, - ] - "#; - assert_matches::assert_matches!( - AccountComponentMetadata::from_toml(toml_str), - Err(AccountComponentTemplateError::InvalidSchema(msg)) - if msg.contains("array of 4 elements") - ); -} - #[test] fn metadata_from_toml_rejects_reserved_slot_names() { let reserved_slot = AccountStorage::faucet_sysdata_slot().as_str(); @@ -621,7 +601,7 @@ fn extensive_schema_metadata_and_init_toml_example() { "#; let init_with_overrides = InitStorageData::from_toml(init_toml_with_overrides).unwrap(); let parsed_entries = init_with_overrides - .map_entries(&"demo::typed_map_new".parse::().unwrap()) + .map_entries(&"demo::typed_map_new".parse::().unwrap()) .expect("demo::typed_map_new map entries missing"); assert_eq!(parsed_entries.len(), 2); let slots_with_maps = diff --git a/crates/miden-protocol/src/account/component/storage/value_name.rs b/crates/miden-protocol/src/account/component/storage/value_name.rs index bb27c3764a..302916d9de 100644 --- a/crates/miden-protocol/src/account/component/storage/value_name.rs +++ b/crates/miden-protocol/src/account/component/storage/value_name.rs @@ -14,6 +14,9 @@ use crate::errors::StorageSlotNameError; /// /// A storage value name is a string that identifies values supplied during component /// instantiation (via [`InitStorageData`](super::InitStorageData)). +/// +/// Each name is either a storage slot name, or a storage slot name with a suffixed identifier for +/// composite types (where the suffix identifies the inner type). #[derive(Clone, Debug)] #[cfg_attr(feature = "std", derive(::serde::Deserialize, ::serde::Serialize))] #[cfg_attr(feature = "std", serde(try_from = "String", into = "String"))] @@ -31,23 +34,30 @@ impl StorageValueName { } } - /// Adds a field-name suffix to a slot-name key, separated by a period, that identifies a - /// specific element (e.g., "basic_faucet::metadata.decimals") - pub fn with_suffix(self, suffix: &str) -> Result { - let mut key = self; + /// Creates a [`StorageValueName`] for the given storage slot and field suffix. + /// + /// A suffixed slot name is used to identify a specific field element's type in a schema + /// (e.g., `miden::contracts::fungible_faucets::token_metadata.max_supply` can specify the + /// `max_supply` element in the `token_metadata` storage slot) + pub fn from_slot_name_with_suffix( + slot_name: &StorageSlotName, + suffix: &str, + ) -> Result { + Self::validate_field_segment(suffix)?; + Ok(StorageValueName { + slot_name: slot_name.clone(), + element_field: Some(suffix.to_string()), + }) + } - // `StorageValueName` keys are either `slot` or `slot.field`. Appending to a key that is - // already suffixed is create an invalid name. - if key.element_field.is_some() { - return Err(StorageValueNameError::InvalidCharacter { - part: key.to_string(), - character: '.', - }); - } + /// Returns the storage slot name prefix of this value name. + pub fn slot_name(&self) -> &StorageSlotName { + &self.slot_name + } - Self::validate_field_segment(suffix)?; - key.element_field = Some(suffix.to_string()); - Ok(key) + /// Returns the optional field suffix of this value name. + pub fn field_name(&self) -> Option<&str> { + self.element_field.as_deref() } fn validate_field_segment(segment: &str) -> Result<(), StorageValueNameError> { @@ -191,7 +201,7 @@ impl Deserializable for StorageValueName { #[derive(Debug, Error)] pub enum StorageValueNameError { - #[error("key segment is empty")] + #[error("key suffix is empty")] EmptySuffix, #[error("key segment '{part}' contains invalid character '{character}'")] InvalidCharacter { part: String, character: char }, From c1c7f8f9892a81254b8ce0e2daec7a4d60cfa8d2 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Wed, 7 Jan 2026 19:18:36 -0300 Subject: [PATCH 02/12] chore: re-add refactored comments --- .../storage/toml/init_storage_data.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs index e91ffba37f..b85ca6a45b 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs @@ -16,6 +16,25 @@ use super::RawMapEntrySchema; impl InitStorageData { /// Creates an instance of [`InitStorageData`] from a TOML string. + /// + /// # Supported formats + /// + /// ```toml + /// # Value entry (string) + /// "slot::name" = "0x1234" + /// + /// # Value entry (4-element word) + /// "slot::name" = ["0", "0", "0", "100"] + /// + /// # Nested table (flattened to slot::name.field) + /// ["slot::name"] + /// field = "value" + /// + /// # Map entries + /// "slot::map" = [ + /// { key = "0x01", value = "0x10" }, + /// ] + /// ``` pub fn from_toml(toml_str: &str) -> Result { let table: toml::Table = toml::from_str(toml_str)?; let mut data = InitStorageData::default(); From a4250a74578250936d63eb95ca4543d65e0b3def Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Fri, 9 Jan 2026 17:16:24 -0300 Subject: [PATCH 03/12] review: refactor and simplify structs --- .../src/account/component/metadata/mod.rs | 4 +- .../component/storage/init_storage_data.rs | 123 +++++++++--------- .../src/account/component/storage/mod.rs | 2 +- .../src/account/component/storage/schema.rs | 59 ++++----- .../storage/toml/init_storage_data.rs | 10 +- .../src/account/component/storage/toml/mod.rs | 15 +-- .../account/component/storage/toml/tests.rs | 7 +- 7 files changed, 104 insertions(+), 116 deletions(-) diff --git a/crates/miden-protocol/src/account/component/metadata/mod.rs b/crates/miden-protocol/src/account/component/metadata/mod.rs index f29dbbc87c..82d09fefeb 100644 --- a/crates/miden-protocol/src/account/component/metadata/mod.rs +++ b/crates/miden-protocol/src/account/component/metadata/mod.rs @@ -45,10 +45,10 @@ use crate::AccountError; /// InitStorageData, /// SchemaTypeId, /// StorageSlotSchema, -/// StorageValue, /// StorageValueName, /// ValueSlotSchema, /// WordSchema, +/// WordValue, /// }; /// use semver::Version; /// @@ -77,7 +77,7 @@ use crate::AccountError; /// // Init value keys are derived from slot name: `demo::test_value.foo`. /// let value_name = StorageValueName::from_slot_name_with_suffix(&slot_name, "foo")?; /// let init_storage_data = InitStorageData::new( -/// BTreeMap::from([(value_name, StorageValue::Parseable("300".into()))]), +/// BTreeMap::from([(value_name, WordValue::Atomic("300".into()))]), /// BTreeMap::new(), /// )?; /// diff --git a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs index a1be0b197e..de0726cc4b 100644 --- a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs @@ -2,88 +2,91 @@ use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec::Vec; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; use thiserror::Error; use super::StorageValueName; use crate::account::StorageSlotName; use crate::{Felt, FieldElement, Word}; -/// A raw word value provided via [`InitStorageData`]. +/// A word value provided via [`InitStorageData`]. /// /// This is used for defining specific values in relation to a component's schema, where each value -/// is supplied as either an atomic string (e.g. `"0x1234"`, `"16"`, `"BTC"`) or an array of 4 field -/// elements. +/// is supplied as either a fully-typed word, an atomic string (e.g. `"0x1234"`, `"16"`, `"BTC"`), +/// or an array of 4 field elements. #[derive(Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "std", serde(untagged))] pub enum WordValue { + /// A fully-typed word value. + FullyTyped(Word), /// Represents a single word value, given by a single string input. Atomic(String), /// Represents a word through four string-encoded field elements. Elements([String; 4]), } -impl From for WordValue { - fn from(value: String) -> Self { - WordValue::Atomic(value) - } -} - -impl From<&str> for WordValue { - fn from(value: &str) -> Self { - WordValue::Atomic(String::from(value)) +#[cfg(feature = "std")] +impl Serialize for WordValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + WordValue::Atomic(value) => serializer.serialize_str(value), + WordValue::Elements(elements) => elements.serialize(serializer), + WordValue::FullyTyped(word) => serializer.serialize_str(&word.to_string()), + } } } -// STORAGE VALUE -// ==================================================================================================== - -/// Represents a storage value supplied at initialization time. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum StorageValue { - /// A fully-typed word value. - Word(Word), - /// A raw value which will be parsed into a word using the slot schema. - Parseable(WordValue), -} - -// CONVERSIONS -// ==================================================================================================== +#[cfg(feature = "std")] +impl<'de> Deserialize<'de> for WordValue { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum RawWordValue { + Atomic(String), + Elements([String; 4]), + } -impl From for StorageValue { - fn from(value: Word) -> Self { - StorageValue::Word(value) + match RawWordValue::deserialize(deserializer)? { + RawWordValue::Atomic(value) => Ok(WordValue::Atomic(value)), + RawWordValue::Elements(elements) => Ok(WordValue::Elements(elements)), + } } } -impl From for StorageValue { - fn from(value: WordValue) -> Self { - StorageValue::Parseable(value) +impl From for WordValue { + fn from(value: String) -> Self { + WordValue::Atomic(value) } } -impl From for StorageValue { - fn from(value: String) -> Self { - StorageValue::Parseable(WordValue::Atomic(value)) +impl From<&str> for WordValue { + fn from(value: &str) -> Self { + WordValue::Atomic(String::from(value)) } } -impl From<&str> for StorageValue { - fn from(value: &str) -> Self { - StorageValue::Parseable(WordValue::Atomic(String::from(value))) +impl From for WordValue { + fn from(value: Word) -> Self { + WordValue::FullyTyped(value) } } -impl From for StorageValue { - /// Converts a [`Felt`] to a [`StorageValue`] as a Word in the form `[0, 0, 0, felt]`. +impl From for WordValue { + /// Converts a [`Felt`] to a [`WordValue`] as a Word in the form `[0, 0, 0, felt]`. fn from(value: Felt) -> Self { - StorageValue::Word(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, value])) + WordValue::FullyTyped(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, value])) } } -impl From<[Felt; 4]> for StorageValue { +impl From<[Felt; 4]> for WordValue { fn from(value: [Felt; 4]) -> Self { - StorageValue::Word(Word::from(value)) + WordValue::FullyTyped(Word::from(value)) } } @@ -98,9 +101,9 @@ impl From<[Felt; 4]> for StorageValue { #[derive(Clone, Debug, Default)] pub struct InitStorageData { /// A mapping of storage value names to their init values. - value_entries: BTreeMap, + value_entries: BTreeMap, /// A mapping of storage map slot names to their init key/value entries. - map_entries: BTreeMap>, + map_entries: BTreeMap>, } impl InitStorageData { @@ -113,8 +116,8 @@ impl InitStorageData { /// - A slot has both value entries and map entries /// - A slot has both a slot-level value and field values pub fn new( - value_entries: BTreeMap, - map_entries: BTreeMap>, + value_entries: BTreeMap, + map_entries: BTreeMap>, ) -> Result { // Check for conflicts between value entries and map entries for slot_name in map_entries.keys() { @@ -142,22 +145,22 @@ impl InitStorageData { } /// Returns a reference to the underlying init values map. - pub fn values(&self) -> &BTreeMap { + pub fn values(&self) -> &BTreeMap { &self.value_entries } /// Returns a reference to the underlying init map entries. - pub fn maps(&self) -> &BTreeMap> { + pub fn maps(&self) -> &BTreeMap> { &self.map_entries } /// Returns a reference to the stored init value for the given name. - pub fn value_entry(&self, name: &StorageValueName) -> Option<&StorageValue> { + pub fn value_entry(&self, name: &StorageValueName) -> Option<&WordValue> { self.value_entries.get(name) } /// Returns a reference to the stored init value for a full slot name. - pub fn slot_value_entry(&self, slot_name: &StorageSlotName) -> Option<&StorageValue> { + pub fn slot_value_entry(&self, slot_name: &StorageSlotName) -> Option<&WordValue> { let name = StorageValueName::from_slot_name(slot_name); self.value_entries.get(&name) } @@ -166,7 +169,7 @@ impl InitStorageData { pub fn map_entries( &self, slot_name: &StorageSlotName, - ) -> Option<&Vec<(StorageValue, StorageValue)>> { + ) -> Option<&Vec<(WordValue, WordValue)>> { self.map_entries.get(slot_name) } @@ -196,17 +199,17 @@ impl InitStorageData { /// Inserts a value entry, returning an error on duplicate or conflicting keys. /// - /// The value can be any type that implements `Into`, e.g.: + /// The value can be any type that implements `Into`, e.g.: /// /// - `Word`: a fully-typed word value /// - `[Felt; 4]`: converted to a Word /// - `Felt`: converted to `[0, 0, 0, felt]` /// - `String` or `&str`: a parseable string value - /// - `WordValue`: a raw word value (atomic or elements) + /// - `WordValue`: a raw or fully-typed word value pub fn insert_value( &mut self, name: StorageValueName, - value: impl Into, + value: impl Into, ) -> Result<(), InitStorageDataError> { if self.value_entries.contains_key(&name) { return Err(InitStorageDataError::DuplicateKey(name.to_string())); @@ -222,7 +225,7 @@ impl InitStorageData { pub fn set_map_values( &mut self, slot_name: StorageSlotName, - entries: Vec<(StorageValue, StorageValue)>, + entries: Vec<(WordValue, WordValue)>, ) -> Result<(), InitStorageDataError> { if self.has_value_entries_for_slot(&slot_name) { return Err(InitStorageDataError::ConflictingEntries(slot_name.as_str().into())); @@ -237,8 +240,8 @@ impl InitStorageData { pub fn insert_map_entry( &mut self, slot_name: StorageSlotName, - key: impl Into, - value: impl Into, + key: impl Into, + value: impl Into, ) { self.map_entries.entry(slot_name).or_default().push((key.into(), value.into())); } diff --git a/crates/miden-protocol/src/account/component/storage/mod.rs b/crates/miden-protocol/src/account/component/storage/mod.rs index 4933e935a9..06b24bd768 100644 --- a/crates/miden-protocol/src/account/component/storage/mod.rs +++ b/crates/miden-protocol/src/account/component/storage/mod.rs @@ -8,7 +8,7 @@ mod type_registry; pub use type_registry::{SchemaRequirement, SchemaTypeError, SchemaTypeId}; mod init_storage_data; -pub use init_storage_data::{InitStorageData, InitStorageDataError, StorageValue, WordValue}; +pub use init_storage_data::{InitStorageData, InitStorageDataError, WordValue}; #[cfg(feature = "std")] pub mod toml; diff --git a/crates/miden-protocol/src/account/component/storage/schema.rs b/crates/miden-protocol/src/account/component/storage/schema.rs index 203a01e4f7..e05c839a7b 100644 --- a/crates/miden-protocol/src/account/component/storage/schema.rs +++ b/crates/miden-protocol/src/account/component/storage/schema.rs @@ -7,7 +7,7 @@ use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; use miden_processor::DeserializationError; use super::type_registry::{SCHEMA_TYPE_REGISTRY, SchemaRequirement, SchemaTypeId}; -use super::{InitStorageData, StorageValue, StorageValueName, WordValue}; +use super::{InitStorageData, StorageValueName, WordValue}; use crate::account::storage::is_reserved_slot_name; use crate::account::{StorageMap, StorageSlot, StorageSlotName}; use crate::errors::AccountComponentTemplateError; @@ -617,19 +617,19 @@ impl FeltSchema { && let Some(raw_value) = init_storage_data.value_entry(&value_name) { match raw_value { - StorageValue::Parseable(WordValue::Atomic(raw)) => { + WordValue::Atomic(raw) => { let felt = SCHEMA_TYPE_REGISTRY .try_parse_felt(&self.r#type, raw) .map_err(AccountComponentTemplateError::StorageValueParsingError)?; return Ok(felt); }, - StorageValue::Parseable(WordValue::Elements(_)) => { + WordValue::Elements(_) => { return Err(AccountComponentTemplateError::InvalidInitStorageValue( value_name, "expected an atomic value, got a 4-element array".into(), )); }, - StorageValue::Word(_) => { + WordValue::FullyTyped(_) => { return Err(AccountComponentTemplateError::InvalidInitStorageValue( value_name, "expected an atomic value, got a word".into(), @@ -869,29 +869,25 @@ impl MapSlotSchema { pub(super) fn parse_storage_value_with_schema( schema: &WordSchema, - raw_value: &StorageValue, + raw_value: &WordValue, slot_prefix: &StorageValueName, ) -> Result { - let word = match raw_value { - StorageValue::Word(word) => *word, - StorageValue::Parseable(raw_value) => match schema { - WordSchema::Simple { r#type, .. } => { - parse_simple_word_value(r#type, raw_value, slot_prefix)? - }, - WordSchema::Composite { value } => match raw_value { - WordValue::Elements(elements) => { - parse_composite_elements(value, elements, slot_prefix)? - }, - WordValue::Atomic(value) => SCHEMA_TYPE_REGISTRY - .try_parse_word(&SchemaTypeId::native_word(), value) - .map_err(|err| { - AccountComponentTemplateError::InvalidInitStorageValue( - slot_prefix.clone(), - format!("failed to parse value as `word`: {err}"), - ) - })?, - }, + let word = match (schema, raw_value) { + (_, WordValue::FullyTyped(word)) => *word, + (WordSchema::Simple { r#type, .. }, raw_value) => { + parse_simple_word_value(r#type, raw_value, slot_prefix)? }, + (WordSchema::Composite { value }, WordValue::Elements(elements)) => { + parse_composite_elements(value, elements, slot_prefix)? + }, + (WordSchema::Composite { .. }, WordValue::Atomic(value)) => SCHEMA_TYPE_REGISTRY + .try_parse_word(&SchemaTypeId::native_word(), value) + .map_err(|err| { + AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix.clone(), + format!("failed to parse value as `word`: {err}"), + ) + })?, }; schema.validate_word_value(slot_prefix, "value", word)?; @@ -928,6 +924,7 @@ fn parse_simple_word_value( let felts: [Felt; 4] = felts.try_into().expect("length is 4"); Ok(Word::from(felts)) }, + WordValue::FullyTyped(word) => Ok(*word), } } @@ -1059,7 +1056,7 @@ mod tests { let init_data = InitStorageData::new( BTreeMap::from([( StorageValueName::from_slot_name(&slot_name), - StorageValue::Word(expected), + WordValue::FullyTyped(expected), )]), BTreeMap::new(), ) @@ -1077,7 +1074,7 @@ mod tests { let init_data = InitStorageData::new( BTreeMap::from([( StorageValueName::from_slot_name(&slot_name), - StorageValue::Parseable("6".into()), + WordValue::Atomic("6".into()), )]), BTreeMap::new(), ) @@ -1101,7 +1098,7 @@ mod tests { let init_data = InitStorageData::new( BTreeMap::from([( StorageValueName::from_slot_name_with_suffix(&slot_name, "a").unwrap(), - StorageValue::Parseable("1".into()), + WordValue::Atomic("1".into()), )]), BTreeMap::new(), ) @@ -1118,18 +1115,18 @@ mod tests { let slot_name: StorageSlotName = "demo::map".parse().unwrap(); let entries = vec![( - StorageValue::Parseable(WordValue::Elements([ + WordValue::Elements([ "1".into(), "0".into(), "0".into(), "0".into(), - ])), - StorageValue::Parseable(WordValue::Elements([ + ]), + WordValue::Elements([ "10".into(), "11".into(), "12".into(), "13".into(), - ])), + ]), )]; let init_data = InitStorageData::new( BTreeMap::new(), diff --git a/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs index b85ca6a45b..f36f6487cc 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs @@ -7,7 +7,6 @@ use thiserror::Error; use super::super::{ InitStorageData, InitStorageDataError as CoreInitStorageDataError, - StorageValue, StorageValueName, StorageValueNameError, WordValue, @@ -60,7 +59,7 @@ impl InitStorageData { let word = WordValue::deserialize(field_value).map_err(|_| { InitStorageDataError::InvalidValue(field_name.to_string()) })?; - data.insert_value(field_name, StorageValue::Parseable(word))?; + data.insert_value(field_name, word)?; } }, // "slot::name" = [{ key = "...", value = "..." }, ...] @@ -78,10 +77,7 @@ impl InitStorageData { InitStorageDataError::InvalidMapEntrySchema(e.to_string()) })?; - entries.push(( - StorageValue::Parseable(entry.key), - StorageValue::Parseable(entry.value), - )); + entries.push((entry.key, entry.value)); } data.set_map_values(name.slot_name().clone(), entries)?; }, @@ -89,7 +85,7 @@ impl InitStorageData { other => { let word = WordValue::deserialize(other) .map_err(|_| InitStorageDataError::InvalidValue(name.to_string()))?; - data.insert_value(name, StorageValue::Parseable(word))?; + data.insert_value(name, word)?; }, } } diff --git a/crates/miden-protocol/src/account/component/storage/toml/mod.rs b/crates/miden-protocol/src/account/component/storage/toml/mod.rs index b803c77bc0..a92db14bd0 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/mod.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/mod.rs @@ -12,7 +12,6 @@ use super::super::{ FeltSchema, MapSlotSchema, StorageSlotSchema, - StorageValue, StorageValueName, ValueSlotSchema, WordSchema, @@ -433,11 +432,7 @@ impl RawStorageSlotSchema { let mut map = BTreeMap::new(); let parse = |schema: &WordSchema, raw: &WordValue, label: &str| { - super::schema::parse_storage_value_with_schema( - schema, - &StorageValue::Parseable(raw.clone()), - slot_prefix, - ) + super::schema::parse_storage_value_with_schema(schema, raw, slot_prefix) .map_err(|err| { AccountComponentTemplateError::InvalidSchema(format!( "invalid map `{label}`: {err}" @@ -471,6 +466,7 @@ impl WordValue { label: &str, ) -> Result { let word = match self { + WordValue::FullyTyped(word) => *word, WordValue::Atomic(value) => SCHEMA_TYPE_REGISTRY .try_parse_word(schema_type, value) .map_err(AccountComponentTemplateError::StorageValueParsingError)?, @@ -487,11 +483,8 @@ impl WordValue { }, }; - WordSchema::new_simple(schema_type.clone()).validate_word_value( - slot_prefix, - label, - word, - )?; + WordSchema::new_simple(schema_type.clone()) + .validate_word_value(slot_prefix, label, word)?; Ok(word) } diff --git a/crates/miden-protocol/src/account/component/storage/toml/tests.rs b/crates/miden-protocol/src/account/component/storage/toml/tests.rs index c52babe8e6..2cd1bc1fc4 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/tests.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/tests.rs @@ -10,7 +10,6 @@ use crate::account::component::{ InitStorageDataError as CoreInitStorageDataError, SchemaTypeId, StorageSlotSchema, - StorageValue, StorageValueName, WordSchema, WordValue, @@ -91,17 +90,17 @@ fn parse_map_entries_from_array() { assert_matches::assert_matches!( &entries[0].0, - StorageValue::Parseable(WordValue::Atomic(v)) + WordValue::Atomic(v) if v == "0x0000000000000000000000000000000000000000000000000000000000000001" ); assert_matches::assert_matches!( &entries[0].1, - StorageValue::Parseable(WordValue::Atomic(v)) + WordValue::Atomic(v) if v == "0x0000000000000000000000000000000000000000000000000000000000000010" ); assert_matches::assert_matches!( &entries[1].1, - StorageValue::Parseable(WordValue::Elements(elements)) + WordValue::Elements(elements) if elements == &[ "1".to_string(), "2".to_string(), From 935e5219656e30c2c103a84ad090a8bb8a50d115 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Mon, 12 Jan 2026 13:42:58 -0300 Subject: [PATCH 04/12] chore: lints --- .../component/storage/init_storage_data.rs | 5 +---- .../src/account/component/storage/schema.rs | 14 ++----------- .../src/account/component/storage/toml/mod.rs | 20 +++++++++++-------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs index de0726cc4b..0c8fdb4663 100644 --- a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs @@ -166,10 +166,7 @@ impl InitStorageData { } /// Returns the map entries associated with the given storage map slot name, if any. - pub fn map_entries( - &self, - slot_name: &StorageSlotName, - ) -> Option<&Vec<(WordValue, WordValue)>> { + pub fn map_entries(&self, slot_name: &StorageSlotName) -> Option<&Vec<(WordValue, WordValue)>> { self.map_entries.get(slot_name) } diff --git a/crates/miden-protocol/src/account/component/storage/schema.rs b/crates/miden-protocol/src/account/component/storage/schema.rs index e05c839a7b..00b36e993c 100644 --- a/crates/miden-protocol/src/account/component/storage/schema.rs +++ b/crates/miden-protocol/src/account/component/storage/schema.rs @@ -1115,18 +1115,8 @@ mod tests { let slot_name: StorageSlotName = "demo::map".parse().unwrap(); let entries = vec![( - WordValue::Elements([ - "1".into(), - "0".into(), - "0".into(), - "0".into(), - ]), - WordValue::Elements([ - "10".into(), - "11".into(), - "12".into(), - "13".into(), - ]), + WordValue::Elements(["1".into(), "0".into(), "0".into(), "0".into()]), + WordValue::Elements(["10".into(), "11".into(), "12".into(), "13".into()]), )]; let init_data = InitStorageData::new( BTreeMap::new(), diff --git a/crates/miden-protocol/src/account/component/storage/toml/mod.rs b/crates/miden-protocol/src/account/component/storage/toml/mod.rs index a92db14bd0..a5e679959f 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/mod.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/mod.rs @@ -432,12 +432,13 @@ impl RawStorageSlotSchema { let mut map = BTreeMap::new(); let parse = |schema: &WordSchema, raw: &WordValue, label: &str| { - super::schema::parse_storage_value_with_schema(schema, raw, slot_prefix) - .map_err(|err| { - AccountComponentTemplateError::InvalidSchema(format!( - "invalid map `{label}`: {err}" - )) - }) + super::schema::parse_storage_value_with_schema(schema, raw, slot_prefix).map_err( + |err| { + AccountComponentTemplateError::InvalidSchema(format!( + "invalid map `{label}`: {err}" + )) + }, + ) }; for (index, entry) in entries.into_iter().enumerate() { @@ -483,8 +484,11 @@ impl WordValue { }, }; - WordSchema::new_simple(schema_type.clone()) - .validate_word_value(slot_prefix, label, word)?; + WordSchema::new_simple(schema_type.clone()).validate_word_value( + slot_prefix, + label, + word, + )?; Ok(word) } From 012de9041f10848fe61fcb0b9cf375e923108f9e Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Mon, 12 Jan 2026 14:24:12 -0300 Subject: [PATCH 05/12] chore: move serde code --- .../component/storage/init_storage_data.rs | 36 ----------------- .../component/storage/toml/serde_impls.rs | 39 ++++++++++++++++++- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs index 0c8fdb4663..d89e8c108a 100644 --- a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs @@ -2,8 +2,6 @@ use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec::Vec; -#[cfg(feature = "std")] -use serde::{Deserialize, Serialize}; use thiserror::Error; use super::StorageValueName; @@ -25,40 +23,6 @@ pub enum WordValue { Elements([String; 4]), } -#[cfg(feature = "std")] -impl Serialize for WordValue { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - WordValue::Atomic(value) => serializer.serialize_str(value), - WordValue::Elements(elements) => elements.serialize(serializer), - WordValue::FullyTyped(word) => serializer.serialize_str(&word.to_string()), - } - } -} - -#[cfg(feature = "std")] -impl<'de> Deserialize<'de> for WordValue { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(untagged)] - enum RawWordValue { - Atomic(String), - Elements([String; 4]), - } - - match RawWordValue::deserialize(deserializer)? { - RawWordValue::Atomic(value) => Ok(WordValue::Atomic(value)), - RawWordValue::Elements(elements) => Ok(WordValue::Elements(elements)), - } - } -} - impl From for WordValue { fn from(value: String) -> Self { WordValue::Atomic(value) diff --git a/crates/miden-protocol/src/account/component/storage/toml/serde_impls.rs b/crates/miden-protocol/src/account/component/storage/toml/serde_impls.rs index 3dfb29551f..3dc8f9033c 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/serde_impls.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/serde_impls.rs @@ -1,11 +1,11 @@ -use alloc::string::String; +use alloc::string::{String, ToString}; use serde::de::Error as _; use serde::ser::{Error as SerError, SerializeStruct}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::super::type_registry::SCHEMA_TYPE_REGISTRY; -use super::super::{FeltSchema, SchemaTypeId}; +use super::super::{FeltSchema, SchemaTypeId, WordValue}; // FELT SCHEMA SERIALIZATION // ================================================================================================ @@ -120,3 +120,38 @@ impl<'de> Deserialize<'de> for FeltSchema { }) } } + +// WORD VALUE SERIALIZATION +// ================================================================================================ + +impl Serialize for WordValue { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + WordValue::Atomic(value) => serializer.serialize_str(value), + WordValue::Elements(elements) => elements.serialize(serializer), + WordValue::FullyTyped(word) => serializer.serialize_str(&word.to_string()), + } + } +} + +impl<'de> Deserialize<'de> for WordValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum RawWordValue { + Atomic(String), + Elements([String; 4]), + } + + match RawWordValue::deserialize(deserializer)? { + RawWordValue::Atomic(value) => Ok(WordValue::Atomic(value)), + RawWordValue::Elements(elements) => Ok(WordValue::Elements(elements)), + } + } +} From 8de688297b0a4cc65594c14a80278418b14d84f8 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 13 Jan 2026 10:18:56 +0000 Subject: [PATCH 06/12] chore: unit test for ExcessiveNesting --- .../src/account/component/storage/toml/tests.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/miden-protocol/src/account/component/storage/toml/tests.rs b/crates/miden-protocol/src/account/component/storage/toml/tests.rs index 2cd1bc1fc4..363e514802 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/tests.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/tests.rs @@ -54,6 +54,19 @@ fn from_toml_str_with_deeply_nested_tables_is_rejected() { ); } +#[test] +fn from_toml_str_excessive_key_nesting_rejected() { + let toml_str = r#" + ["demo::token_metadata.nested"] + value = "42" + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::ExcessiveNesting(_)) + ); +} + #[test] fn from_toml_rejects_non_string_atomics() { let toml_str = r#" From b6180d299fc7754e05c03ee1a1ac2a255d82da28 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 13 Jan 2026 10:20:56 +0000 Subject: [PATCH 07/12] unit test for InvalidMapEntryKey --- .../src/account/component/storage/toml/tests.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/miden-protocol/src/account/component/storage/toml/tests.rs b/crates/miden-protocol/src/account/component/storage/toml/tests.rs index 363e514802..1678834abb 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/tests.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/tests.rs @@ -123,6 +123,20 @@ fn parse_map_entries_from_array() { ); } +#[test] +fn map_entries_reject_field_key() { + let toml_str = r#" + "demo::my_map.entry" = [ + { key = "0x1", value = "0x2" } + ] + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::InvalidMapEntryKey(_)) + ); +} + #[test] fn error_on_empty_subtable() { let toml_str = r#" From 5b1f9eddcf5c8ca65a9efc90f9072e07380260a4 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 13 Jan 2026 10:23:30 +0000 Subject: [PATCH 08/12] chore: unit test for InvalidMapEntrySchema --- .../src/account/component/storage/toml/tests.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/miden-protocol/src/account/component/storage/toml/tests.rs b/crates/miden-protocol/src/account/component/storage/toml/tests.rs index 1678834abb..e128ad4b77 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/tests.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/tests.rs @@ -137,6 +137,21 @@ fn map_entries_reject_field_key() { ); } +#[test] +fn map_entries_reject_invalid_schema() { + // Missing required `value` field in the entry table should fail schema deserialization. + let toml_str = r#" + "demo::my_map" = [ + { key = "0x1" } + ] + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::InvalidMapEntrySchema(_)) + ); +} + #[test] fn error_on_empty_subtable() { let toml_str = r#" From e1a3fd3a3c20055c25547b5056017d7626f7fe92 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 13 Jan 2026 11:13:34 +0000 Subject: [PATCH 09/12] chore: unit test for EmptyTable & InvalidStorageValueName --- .../account/component/storage/toml/tests.rs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/miden-protocol/src/account/component/storage/toml/tests.rs b/crates/miden-protocol/src/account/component/storage/toml/tests.rs index e128ad4b77..627b783611 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/tests.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/tests.rs @@ -11,6 +11,7 @@ use crate::account::component::{ SchemaTypeId, StorageSlotSchema, StorageValueName, + StorageValueNameError, WordSchema, WordValue, }; @@ -40,6 +41,42 @@ fn from_toml_str_with_nested_table_and_flattened() { assert_eq!(storage_table.maps(), storage_inline.maps()); } +#[test] +fn empty_table_is_rejected() { + let toml_str = r#" + ["demo::empty_table"] + + ["demo::valid_table"] + value = "42" + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::EmptyTable(key)) if key == "demo::empty_table" + ); +} + +#[test] +fn invalid_storage_value_name_is_rejected() { + // Nested table fields are flattened to `slot.field` and thus must be valid field segments. + let toml_str = r#" + ["demo::valid_token_metadata"] + max_supply = "1000000000" + + "demo::another_valid_token_metadata.supply" = "1000000000" + + ["demo::invalid_token_metadata"] + "bad.field" = "42" + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::InvalidStorageValueName( + StorageValueNameError::InvalidCharacter { part, character } + )) if part == "bad.field" && character == '.' + ); +} + #[test] fn from_toml_str_with_deeply_nested_tables_is_rejected() { let toml_str = r#" From a4844ab2349dac8a22f2e0b948dd3b1703ebe6d3 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Tue, 13 Jan 2026 11:48:08 -0300 Subject: [PATCH 10/12] reviews: amend comment --- crates/miden-protocol/src/account/component/metadata/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-protocol/src/account/component/metadata/mod.rs b/crates/miden-protocol/src/account/component/metadata/mod.rs index 82d09fefeb..03fc168c5b 100644 --- a/crates/miden-protocol/src/account/component/metadata/mod.rs +++ b/crates/miden-protocol/src/account/component/metadata/mod.rs @@ -125,7 +125,7 @@ impl AccountComponentMetadata { } } - /// Returns the init-time values's requirements for this schema. + /// Returns the init-time values requirements for this schema. /// /// These values are used for initializing storage slot values or storage map entries. For a /// full example, refer to the docs for [AccountComponentMetadata]. From 2a145c1609e9d0398ab24b3cbde0c4af3e9f7c82 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Tue, 13 Jan 2026 12:36:14 -0300 Subject: [PATCH 11/12] reviews: remove initstoragedata::new --- .../src/account/component/metadata/mod.rs | 8 +- .../component/storage/init_storage_data.rs | 87 +++++++++---------- .../src/account/component/storage/schema.rs | 57 +++++------- 3 files changed, 64 insertions(+), 88 deletions(-) diff --git a/crates/miden-protocol/src/account/component/metadata/mod.rs b/crates/miden-protocol/src/account/component/metadata/mod.rs index 03fc168c5b..db68b27b0a 100644 --- a/crates/miden-protocol/src/account/component/metadata/mod.rs +++ b/crates/miden-protocol/src/account/component/metadata/mod.rs @@ -35,7 +35,7 @@ use crate::AccountError; /// # Example /// /// ``` -/// use std::collections::{BTreeMap, BTreeSet}; +/// use std::collections::BTreeSet; /// /// use miden_protocol::account::StorageSlotName; /// use miden_protocol::account::component::{ @@ -76,10 +76,8 @@ use crate::AccountError; /// /// // Init value keys are derived from slot name: `demo::test_value.foo`. /// let value_name = StorageValueName::from_slot_name_with_suffix(&slot_name, "foo")?; -/// let init_storage_data = InitStorageData::new( -/// BTreeMap::from([(value_name, WordValue::Atomic("300".into()))]), -/// BTreeMap::new(), -/// )?; +/// let mut init_storage_data = InitStorageData::default(); +/// init_storage_data.set_value(value_name, WordValue::Atomic("300".into()))?; /// /// let storage_slots = metadata.storage_schema().build_storage_slots(&init_storage_data)?; /// assert_eq!(storage_slots.len(), 1); diff --git a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs index d89e8c108a..a17d88889b 100644 --- a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs @@ -71,43 +71,6 @@ pub struct InitStorageData { } impl InitStorageData { - /// Creates a new instance of [InitStorageData], validating that there are no conflicting - /// entries. - /// - /// # Errors - /// - /// Returns an error if: - /// - A slot has both value entries and map entries - /// - A slot has both a slot-level value and field values - pub fn new( - value_entries: BTreeMap, - map_entries: BTreeMap>, - ) -> Result { - // Check for conflicts between value entries and map entries - for slot_name in map_entries.keys() { - if value_entries.keys().any(|v| v.slot_name() == slot_name) { - return Err(InitStorageDataError::ConflictingEntries(slot_name.as_str().into())); - } - } - - // Check for conflicts between slot-level values and field values - for value_name in value_entries.keys() { - if value_name.field_name().is_none() { - // This is a slot-level value; check if there are field entries for this slot - let has_field_entries = value_entries.keys().any(|other| { - other.slot_name() == value_name.slot_name() && other.field_name().is_some() - }); - if has_field_entries { - return Err(InitStorageDataError::ConflictingEntries( - value_name.slot_name().as_str().into(), - )); - } - } - } - - Ok(InitStorageData { value_entries, map_entries }) - } - /// Returns a reference to the underlying init values map. pub fn values(&self) -> &BTreeMap { &self.value_entries @@ -182,7 +145,36 @@ impl InitStorageData { Ok(()) } - /// Inserts map entries, returning an error if there are conflicting value entries. + /// Inserts a single map entry, returning an error on duplicate or conflicting keys. + /// + /// See [`Self::insert_value`] for examples of supported types for `key` and `value`. + pub fn insert_map_entry( + &mut self, + slot_name: StorageSlotName, + key: impl Into, + value: impl Into, + ) -> Result<(), InitStorageDataError> { + if self.has_value_entries_for_slot(&slot_name) { + return Err(InitStorageDataError::ConflictingEntries(slot_name.as_str().into())); + } + + let key = key.into(); + if let Some(entries) = self.map_entries.get(&slot_name) + && entries.iter().any(|(existing_key, _)| existing_key == &key) + { + return Err(InitStorageDataError::DuplicateKey(format!( + "{}[{key:?}]", + slot_name.as_str() + ))); + } + + self.map_entries.entry(slot_name).or_default().push((key, value.into())); + Ok(()) + } + + /// Sets map entries for the slot, replacing any existing entries. + /// + /// Returns an error if there are conflicting value entries. pub fn set_map_values( &mut self, slot_name: StorageSlotName, @@ -191,20 +183,23 @@ impl InitStorageData { if self.has_value_entries_for_slot(&slot_name) { return Err(InitStorageDataError::ConflictingEntries(slot_name.as_str().into())); } - self.map_entries.entry(slot_name).or_default().extend(entries); + self.map_entries.insert(slot_name, entries); Ok(()) } - /// Inserts a single map entry. + /// Sets a value entry, overriding any existing entry for the name. /// - /// See [`Self::insert_value`] for examples of supported types for `key` and `value`. - pub fn insert_map_entry( + /// Returns an error if the [`StorageValueName`] has been used for a map slot. + pub fn set_value( &mut self, - slot_name: StorageSlotName, - key: impl Into, + name: StorageValueName, value: impl Into, - ) { - self.map_entries.entry(slot_name).or_default().push((key.into(), value.into())); + ) -> Result<(), InitStorageDataError> { + if self.map_entries.contains_key(name.slot_name()) { + return Err(InitStorageDataError::ConflictingEntries(name.slot_name().as_str().into())); + } + self.value_entries.insert(name, value.into()); + Ok(()) } } diff --git a/crates/miden-protocol/src/account/component/storage/schema.rs b/crates/miden-protocol/src/account/component/storage/schema.rs index 00b36e993c..3a4f0a8522 100644 --- a/crates/miden-protocol/src/account/component/storage/schema.rs +++ b/crates/miden-protocol/src/account/component/storage/schema.rs @@ -146,10 +146,10 @@ impl StorageSlotSchema { slot_name: &StorageSlotName, requirements: &mut BTreeMap, ) -> Result<(), AccountComponentTemplateError> { - let slot_prefix = StorageValueName::from_slot_name(slot_name); + let slot_name = StorageValueName::from_slot_name(slot_name); match self { StorageSlotSchema::Value(slot) => { - slot.collect_init_value_requirements(slot_prefix, requirements) + slot.collect_init_value_requirements(slot_name, requirements) }, StorageSlotSchema::Map(_) => Ok(()), } @@ -267,7 +267,7 @@ impl WordSchema { fn collect_init_value_requirements( &self, - slot_prefix: StorageValueName, + value_name: StorageValueName, description: Option, requirements: &mut BTreeMap, ) -> Result<(), AccountComponentTemplateError> { @@ -283,7 +283,7 @@ impl WordSchema { if requirements .insert( - slot_prefix.clone(), + value_name.clone(), SchemaRequirement { description, r#type: r#type.clone(), @@ -292,14 +292,14 @@ impl WordSchema { ) .is_some() { - return Err(AccountComponentTemplateError::DuplicateInitValueName(slot_prefix)); + return Err(AccountComponentTemplateError::DuplicateInitValueName(value_name)); } Ok(()) }, WordSchema::Composite { value } => { for felt in value.iter() { - felt.collect_init_value_requirements(slot_prefix.clone(), requirements)?; + felt.collect_init_value_requirements(value_name.clone(), requirements)?; } Ok(()) }, @@ -735,11 +735,11 @@ impl ValueSlotSchema { fn collect_init_value_requirements( &self, - slot_prefix: StorageValueName, + value_name: StorageValueName, requirements: &mut BTreeMap, ) -> Result<(), AccountComponentTemplateError> { self.word.collect_init_value_requirements( - slot_prefix, + value_name, self.description.clone(), requirements, ) @@ -1053,14 +1053,10 @@ mod tests { let slot_name: StorageSlotName = "demo::slot".parse().unwrap(); let expected = Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); - let init_data = InitStorageData::new( - BTreeMap::from([( - StorageValueName::from_slot_name(&slot_name), - WordValue::FullyTyped(expected), - )]), - BTreeMap::new(), - ) - .unwrap(); + let mut init_data = InitStorageData::default(); + init_data + .set_value(StorageValueName::from_slot_name(&slot_name), expected) + .unwrap(); let built = slot.try_build_word(&init_data, &slot_name).unwrap(); assert_eq!(built, expected); @@ -1071,14 +1067,8 @@ mod tests { let slot = ValueSlotSchema::new(None, WordSchema::new_simple(SchemaTypeId::u8())); let slot_name: StorageSlotName = "demo::u8_word".parse().unwrap(); - let init_data = InitStorageData::new( - BTreeMap::from([( - StorageValueName::from_slot_name(&slot_name), - WordValue::Atomic("6".into()), - )]), - BTreeMap::new(), - ) - .unwrap(); + let mut init_data = InitStorageData::default(); + init_data.set_value(StorageValueName::from_slot_name(&slot_name), "6").unwrap(); let built = slot.try_build_word(&init_data, &slot_name).unwrap(); assert_eq!(built, Word::from([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(6)])); @@ -1095,14 +1085,10 @@ mod tests { let slot = ValueSlotSchema::new(None, word); let slot_name: StorageSlotName = "demo::slot".parse().unwrap(); - let init_data = InitStorageData::new( - BTreeMap::from([( - StorageValueName::from_slot_name_with_suffix(&slot_name, "a").unwrap(), - WordValue::Atomic("1".into()), - )]), - BTreeMap::new(), - ) - .unwrap(); + let mut init_data = InitStorageData::default(); + init_data + .set_value(StorageValueName::from_slot_name_with_suffix(&slot_name, "a").unwrap(), "1") + .unwrap(); let built = slot.try_build_word(&init_data, &slot_name).unwrap(); assert_eq!(built, Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)])); @@ -1118,11 +1104,8 @@ mod tests { WordValue::Elements(["1".into(), "0".into(), "0".into(), "0".into()]), WordValue::Elements(["10".into(), "11".into(), "12".into(), "13".into()]), )]; - let init_data = InitStorageData::new( - BTreeMap::new(), - BTreeMap::from([(slot_name.clone(), entries.clone())]), - ) - .unwrap(); + let mut init_data = InitStorageData::default(); + init_data.set_map_values(slot_name.clone(), entries.clone()).unwrap(); let built = slot.try_build_map(&init_data, &slot_name).unwrap(); let expected = StorageMap::with_entries([( From fde5efda49d27c095e1c7526b3d24ed0fd914715 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Fri, 16 Jan 2026 11:44:26 -0300 Subject: [PATCH 12/12] reviews: comments, move code around, conflict on duplicates when parsing --- .../component/storage/init_storage_data.rs | 42 +++++++++---------- .../src/account/component/storage/schema.rs | 31 ++++++++------ .../storage/toml/init_storage_data.rs | 5 +-- 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs index a17d88889b..ff870dd329 100644 --- a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs @@ -97,15 +97,6 @@ impl InitStorageData { self.map_entries.get(slot_name) } - /// Merges another [`InitStorageData`] into this one, overwriting value entries and appending - /// map entries. - pub fn merge_from(&mut self, other: InitStorageData) { - self.value_entries.extend(other.value_entries); - for (slot_name, entries) in other.map_entries { - self.map_entries.entry(slot_name).or_default().extend(entries); - } - } - /// Returns true if any init value entry targets the given slot name. pub fn has_value_entries_for_slot(&self, slot_name: &StorageSlotName) -> bool { self.value_entries.keys().any(|name| name.slot_name() == slot_name) @@ -145,6 +136,21 @@ impl InitStorageData { Ok(()) } + /// Sets a value entry, overriding any existing entry for the name. + /// + /// Returns an error if the [`StorageValueName`] has been used for a map slot. + pub fn set_value( + &mut self, + name: StorageValueName, + value: impl Into, + ) -> Result<(), InitStorageDataError> { + if self.map_entries.contains_key(name.slot_name()) { + return Err(InitStorageDataError::ConflictingEntries(name.slot_name().as_str().into())); + } + self.value_entries.insert(name, value.into()); + Ok(()) + } + /// Inserts a single map entry, returning an error on duplicate or conflicting keys. /// /// See [`Self::insert_value`] for examples of supported types for `key` and `value`. @@ -187,19 +193,13 @@ impl InitStorageData { Ok(()) } - /// Sets a value entry, overriding any existing entry for the name. - /// - /// Returns an error if the [`StorageValueName`] has been used for a map slot. - pub fn set_value( - &mut self, - name: StorageValueName, - value: impl Into, - ) -> Result<(), InitStorageDataError> { - if self.map_entries.contains_key(name.slot_name()) { - return Err(InitStorageDataError::ConflictingEntries(name.slot_name().as_str().into())); + /// Merges another [`InitStorageData`] into this one, overwriting value entries and appending + /// map entries. + pub fn merge_with(&mut self, other: InitStorageData) { + self.value_entries.extend(other.value_entries); + for (slot_name, entries) in other.map_entries { + self.map_entries.entry(slot_name).or_default().extend(entries); } - self.value_entries.insert(name, value.into()); - Ok(()) } } diff --git a/crates/miden-protocol/src/account/component/storage/schema.rs b/crates/miden-protocol/src/account/component/storage/schema.rs index 3a4f0a8522..e8054ac482 100644 --- a/crates/miden-protocol/src/account/component/storage/schema.rs +++ b/crates/miden-protocol/src/account/component/storage/schema.rs @@ -335,6 +335,11 @@ impl WordSchema { Ok(()) } + /// Builds a [`Word`] from the provided initialization data according to this schema. + /// + /// For simple schemas, expects a direct slot value (not map or field entries). + /// For composite schemas, either parses a single value or builds the word from individual + /// felt entries. pub(crate) fn try_build_word( &self, init_storage_data: &InitStorageData, @@ -343,15 +348,16 @@ impl WordSchema { let slot_prefix = StorageValueName::from_slot_name(slot_name); let slot_value = init_storage_data.slot_value_entry(slot_name); let has_fields = init_storage_data.has_field_entries_for_slot(slot_name); - let has_map = init_storage_data.map_entries(slot_name).is_some(); + + if init_storage_data.map_entries(slot_name).is_some() { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a value, got a map".into(), + )); + } + match self { WordSchema::Simple { r#type, default_value } => { - if has_map { - return Err(AccountComponentTemplateError::InvalidInitStorageValue( - slot_prefix, - "expected a value, got a map".into(), - )); - } if has_fields { return Err(AccountComponentTemplateError::InvalidInitStorageValue( slot_prefix, @@ -372,12 +378,6 @@ impl WordSchema { } }, WordSchema::Composite { value } => { - if has_map { - return Err(AccountComponentTemplateError::InvalidInitStorageValue( - slot_prefix, - "expected a value, got a map".into(), - )); - } if let Some(value) = slot_value { if has_fields { return Err(AccountComponentTemplateError::InvalidInitStorageValue( @@ -745,6 +745,7 @@ impl ValueSlotSchema { ) } + /// Builds a [Word] from the provided initialization data using the inner word schema. pub fn try_build_word( &self, init_storage_data: &InitStorageData, @@ -805,6 +806,10 @@ impl MapSlotSchema { self.description.as_ref() } + /// Builds a [`StorageMap`] from the provided initialization data. + /// + /// Merges any default values with entries from the init data, validating that the data + /// contains map entries (not a direct value or field entries). pub fn try_build_map( &self, init_storage_data: &InitStorageData, diff --git a/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs index f36f6487cc..cf681dc852 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs @@ -1,5 +1,4 @@ use alloc::string::{String, ToString}; -use alloc::vec::Vec; use serde::Deserialize; use thiserror::Error; @@ -69,7 +68,6 @@ impl InitStorageData { if name.field_name().is_some() { return Err(InitStorageDataError::InvalidMapEntryKey(name.to_string())); } - let mut entries = Vec::with_capacity(items.len()); for item in items { // Try deserializing as map entry let entry: RawMapEntrySchema = RawMapEntrySchema::deserialize(item) @@ -77,9 +75,8 @@ impl InitStorageData { InitStorageDataError::InvalidMapEntrySchema(e.to_string()) })?; - entries.push((entry.key, entry.value)); + data.insert_map_entry(name.slot_name().clone(), entry.key, entry.value)?; } - data.set_map_values(name.slot_name().clone(), entries)?; }, // "slot::name" = "value" or "slot::name" = ["a", "b", "c", "d"] other => {