From 386f7942660884e543b29fc5c7235c51e5d5eaee Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Mon, 10 Feb 2025 13:07:23 -0300 Subject: [PATCH 1/5] feat: add native types to storage placeholders --- CHANGELOG.md | 1 + .../src/account/component/mod.rs | 6 +- .../src/account/component/template/mod.rs | 245 +++--- .../template/storage/entry_content.rs | 737 +++++++++--------- .../template/storage/init_storage_data.rs | 12 +- .../account/component/template/storage/mod.rs | 531 ++++++------- .../component/template/storage/placeholder.rs | 518 +++++++++--- .../component/template/storage/toml.rs | 467 +++++------ crates/miden-objects/src/account/mod.rs | 4 +- crates/miden-objects/src/errors.rs | 23 +- 10 files changed, 1397 insertions(+), 1147 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2091f0aa5..24d223596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - [BREAKING] Refactored config file for `miden-proving-service` to be based on environment variables (#1120). - Added block number as a public input to the transaction kernel. Updated prologue logic to validate the global input block number is consistent with the commitment block number (#1126). - Made NoteFile and AccountFile more consistent (#1133). +- [BREAKING] Added native types to `AccountComponentTemplate` (#1124). ## 0.7.2 (2025-01-28) - `miden-objects` crate only diff --git a/crates/miden-objects/src/account/component/mod.rs b/crates/miden-objects/src/account/component/mod.rs index dde17e027..4a6318557 100644 --- a/crates/miden-objects/src/account/component/mod.rs +++ b/crates/miden-objects/src/account/component/mod.rs @@ -5,9 +5,9 @@ use vm_processor::MastForest; mod template; pub use template::{ - AccountComponentMetadata, AccountComponentTemplate, FeltRepresentation, InitStorageData, - MapRepresentation, PlaceholderType, StorageEntry, StoragePlaceholder, StorageValue, - WordRepresentation, + AccountComponentMetadata, AccountComponentTemplate, FeltRepresentation, FeltType, + InitStorageData, MapEntry, StorageEntry, StorageValueError, StorageValueName, + StorageValueNameError, WordRepresentation, WordType, }; use crate::{ diff --git a/crates/miden-objects/src/account/component/template/mod.rs b/crates/miden-objects/src/account/component/template/mod.rs index e199341a2..eb52e9f1f 100644 --- a/crates/miden-objects/src/account/component/template/mod.rs +++ b/crates/miden-objects/src/account/component/template/mod.rs @@ -1,5 +1,5 @@ use alloc::{ - collections::{btree_map::Entry, BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet}, string::{String, ToString}, vec::Vec, }; @@ -109,28 +109,29 @@ impl Deserializable for AccountComponentTemplate { /// # use semver::Version; /// # use std::collections::BTreeSet; /// # use miden_objects::{testing::account_code::CODE, account::{ -/// # AccountComponent, AccountComponentMetadata, InitStorageData, StorageEntry, -/// # StoragePlaceholder, StorageValue, +/// # AccountComponent, AccountComponentMetadata, StorageEntry, +/// # StorageValueName, /// # AccountComponentTemplate, FeltRepresentation, WordRepresentation}, /// # assembly::Assembler, Felt}; +/// # use miden_objects::account::FeltType; +/// # use miden_objects::account::InitStorageData; /// # fn main() -> Result<(), Box> { -/// let first_felt = FeltRepresentation::Decimal(Felt::new(0u64)); -/// let second_felt = FeltRepresentation::Decimal(Felt::new(1u64)); -/// let third_felt = FeltRepresentation::Decimal(Felt::new(2u64)); +/// let first_felt = FeltRepresentation::from(Felt::new(0u64)); +/// let second_felt = FeltRepresentation::from(Felt::new(1u64)); +/// let third_felt = FeltRepresentation::from(Felt::new(2u64)); /// // Templated element: -/// let last_element = FeltRepresentation::Template(StoragePlaceholder::new("foo")?); +/// let last_element = +/// FeltRepresentation::new_template(FeltType::Felt, StorageValueName::new("foo")?, None); /// -/// let storage_entry = StorageEntry::new_value( -/// "test-entry", -/// Some("a test entry"), -/// 0, -/// WordRepresentation::Array([first_felt, second_felt, third_felt, last_element]), +/// let word_representation = WordRepresentation::new_value( +/// [first_felt, second_felt, third_felt, last_element], +/// Some(StorageValueName::new("test_value")?), +/// Some("This is a test value".into()), /// ); +/// let storage_entry = StorageEntry::new_value(0, word_representation); /// -/// let init_storage_data = InitStorageData::new([( -/// StoragePlaceholder::new("foo")?, -/// StorageValue::Felt(Felt::new(300u64)), -/// )]); +/// let init_storage_data = +/// InitStorageData::new([(StorageValueName::new("test_value.foo")?, "300".to_string())]); /// /// let component_template = AccountComponentMetadata::new( /// "test name".into(), @@ -197,22 +198,21 @@ impl AccountComponentMetadata { /// Retrieves a map of unique storage placeholders mapped to their expected type that require /// a value at the moment of component instantiation. /// - /// These values will be used for - /// initializing storage slot values, or storage map entries. For a full example on how a - /// placeholder may be utilized, please refer to the docs for [AccountComponentMetadata]. + /// These values will be used for initializing storage slot values, or storage map entries. + /// For a full example on how a placeholder may be utilized, please refer to the docs for + /// [AccountComponentMetadata]. /// /// Types for the returned storage placeholders are inferred based on their location in the /// storage layout structure. - pub fn get_unique_storage_placeholders(&self) -> BTreeMap { - let mut placeholder_map = BTreeMap::new(); - for storage_entry in &self.storage { - for (placeholder, placeholder_type) in storage_entry.all_placeholders_iter() { - // The constructors of this type guarantee each placeholder has the same type, so - // reinserting them multiple times is fine. - placeholder_map.insert(placeholder.clone(), placeholder_type); + pub fn get_template_requirements(&self) -> BTreeMap { + let mut templates = BTreeMap::new(); + for entry in self.storage_entries() { + for (name, requirement) in entry.template_requirements() { + templates.insert(name.to_string(), requirement); } } - placeholder_map + + templates } /// Returns the name of the account component. @@ -275,30 +275,6 @@ impl AccountComponentMetadata { } } - // Check that placeholders do not appear more than once with a different type - let mut placeholders = BTreeMap::new(); - for storage_entry in &self.storage { - for (placeholder, placeholder_type) in storage_entry.all_placeholders_iter() { - match placeholders.entry(placeholder.clone()) { - Entry::Occupied(entry) => { - // if already exists, make sure it's the same type - if *entry.get() != placeholder_type { - return Err( - AccountComponentTemplateError::StoragePlaceholderTypeMismatch( - placeholder.clone(), - *entry.get(), - placeholder_type, - ), - ); - } - }, - Entry::Vacant(slot) => { - slot.insert(placeholder_type); - }, - } - } - } - for entry in self.storage_entries() { entry.validate()?; } @@ -336,67 +312,75 @@ impl Deserializable for AccountComponentMetadata { // TESTS // ================================================================================================ - #[cfg(test)] mod tests { + use std::{ + collections::BTreeSet, + string::{String, ToString}, + }; + use assembly::Assembler; use assert_matches::assert_matches; - use storage::WordRepresentation; - use vm_core::{Felt, FieldElement}; - - use super::*; - use crate::{account::AccountComponent, testing::account_code::CODE, AccountError}; + use semver::Version; + use vm_core::{ + utils::{Deserializable, Serializable}, + Felt, FieldElement, + }; + + use super::FeltRepresentation; + // Import the new types and helpers. + use crate::{ + account::{ + component::template::{ + storage::StorageEntry, AccountComponentMetadata, AccountComponentTemplate, + InitStorageData, + }, + AccountComponent, StorageValueName, + }, + errors::AccountComponentTemplateError, + testing::account_code::CODE, + AccountError, + }; + + fn default_felt_array() -> [FeltRepresentation; 4] { + [ + FeltRepresentation::from(Felt::ZERO), + FeltRepresentation::from(Felt::ZERO), + FeltRepresentation::from(Felt::ZERO), + FeltRepresentation::from(Felt::ZERO), + ] + } #[test] fn test_contiguous_value_slots() { let storage = vec![ - StorageEntry::Value { - name: "slot0".into(), - description: None, - slot: 0, - value: WordRepresentation::Value(Default::default()), - }, - StorageEntry::MultiSlot { - name: "slot1".into(), - description: None, - slots: vec![1, 2], - values: vec![ - WordRepresentation::Array(Default::default()), - WordRepresentation::Value(Default::default()), - ], - }, + StorageEntry::new_value(0, default_felt_array()), + StorageEntry::new_multislot( + StorageValueName::new("slot1").unwrap(), + Some("multi-slot value of arity 2".into()), + vec![1, 2], + vec![default_felt_array(), default_felt_array()], + ), ]; - let original_config = AccountComponentMetadata::new( - "test".into(), - "desc".into(), - Version::parse("0.1.0").unwrap(), - BTreeSet::new(), + let original_config = AccountComponentMetadata { + name: "test".into(), + description: "desc".into(), + version: Version::parse("0.1.0").unwrap(), + targets: BTreeSet::new(), storage, - ) - .unwrap(); + }; let serialized = original_config.as_toml().unwrap(); - let deserialized = AccountComponentMetadata::from_toml(&serialized).unwrap(); - assert_eq!(deserialized, original_config) + assert_eq!(deserialized, original_config); } #[test] fn test_new_non_contiguous_value_slots() { let storage = vec![ - StorageEntry::Value { - name: "slot0".into(), - description: None, - slot: 0, - value: Default::default(), - }, - StorageEntry::Value { - name: "slot2".into(), - description: None, - slot: 2, - value: Default::default(), - }, + StorageEntry::new_value(0, default_felt_array()), + StorageEntry::new_value(2, default_felt_array()), ]; let result = AccountComponentMetadata::new( @@ -412,40 +396,31 @@ mod tests { #[test] fn test_binary_serde_roundtrip() { let storage = vec![ - StorageEntry::MultiSlot { - name: "slot1".into(), - description: None, - slots: vec![1, 2], - values: vec![ - WordRepresentation::Array(Default::default()), - WordRepresentation::Value(Default::default()), - ], - }, - StorageEntry::Value { - name: "slot0".into(), - description: None, - slot: 0, - value: WordRepresentation::Value(Default::default()), - }, + StorageEntry::new_multislot( + StorageValueName::new("slot1").unwrap(), + Option::::None, + vec![1, 2], + vec![default_felt_array(), default_felt_array()], + ), + StorageEntry::new_value(0, default_felt_array()), ]; - let component_template = AccountComponentMetadata::new( - "test".into(), - "desc".into(), - Version::parse("0.1.0").unwrap(), - BTreeSet::new(), + let component_metadata = AccountComponentMetadata { + name: "test".into(), + description: "desc".into(), + version: Version::parse("0.1.0").unwrap(), + targets: BTreeSet::new(), storage, - ) - .unwrap(); + }; let library = Assembler::default().assemble_library([CODE]).unwrap(); - let template = AccountComponentTemplate::new(component_template, library); - _ = AccountComponent::from_template(&template, &InitStorageData::default()).unwrap(); + let template = AccountComponentTemplate::new(component_metadata, library); + let _ = AccountComponent::from_template(&template, &InitStorageData::default()).unwrap(); let serialized = template.to_bytes(); let deserialized = AccountComponentTemplate::read_from_bytes(&serialized).unwrap(); - assert_eq!(deserialized, template) + assert_eq!(deserialized, template); } #[test] @@ -461,8 +436,8 @@ mod tests { description = "A storage map entry" slot = 0 values = [ - { key = "0x1", value = ["{{value.test}}", "0x1", "0x2", "0x3"] }, - { key = "0x1", value = ["0x1", "0x2", "0x3", "{{value.test}}"] }, + { key = "0x1", value = ["0x3", "0x1", "0x2", "0x3"] }, + { key = "0x1", value = ["0x1", "0x2", "0x3", "0x10"] } ] "#; @@ -483,8 +458,8 @@ mod tests { description = "A storage map entry" slot = 0 values = [ - { key = ["0","0","0","1"], value = ["{{value.test}}", "0x1", "0x2", "0x3"] }, - { key = "{{word.test}}", value = ["0x1", "0x2", "0x3", "{{value.test}}"] }, + { key = ["0", "0", "0", "1"], value = ["0x9", "0x12", "0x31", "0x18"] }, + { key = {name="duplicate_key" }, value = ["0x1", "0x2", "0x3", "0x4"] } ] "#; @@ -492,13 +467,12 @@ mod tests { let library = Assembler::default().assemble_library([CODE]).unwrap(); let template = AccountComponentTemplate::new(metadata, library); - let init_storage_data = InitStorageData::new([ - ( - StoragePlaceholder::new("word.test").unwrap(), - StorageValue::Word([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]), - ), - (StoragePlaceholder::new("value.test").unwrap(), StorageValue::Felt(Felt::ONE)), - ]); + // Fail to instantiate on a duplicate key + + let init_storage_data = InitStorageData::new([( + StorageValueName::new("map.duplicate_key").unwrap(), + "0x0000000000000000000000000000000000000000000000000100000000000000".to_string(), + )]); let account_component = AccountComponent::from_template(&template, &init_storage_data); assert_matches!( account_component, @@ -507,13 +481,12 @@ mod tests { )) ); - let valid_init_storage_data = InitStorageData::new([ - ( - StoragePlaceholder::new("word.test").unwrap(), - StorageValue::Word([Felt::new(30), Felt::new(20), Felt::new(10), Felt::ZERO]), - ), - (StoragePlaceholder::new("value.test").unwrap(), StorageValue::Felt(Felt::ONE)), - ]); + // Succesfully instantiate a map (keys are not duplicate) + + let valid_init_storage_data = InitStorageData::new([( + StorageValueName::new("map.duplicate_key").unwrap(), + "0x30".to_string(), + )]); AccountComponent::from_template(&template, &valid_init_storage_data).unwrap(); } } diff --git a/crates/miden-objects/src/account/component/template/storage/entry_content.rs b/crates/miden-objects/src/account/component/template/storage/entry_content.rs index e7c700eca..ac1f84141 100644 --- a/crates/miden-objects/src/account/component/template/storage/entry_content.rs +++ b/crates/miden-objects/src/account/component/template/storage/entry_content.rs @@ -1,4 +1,5 @@ -use alloc::{boxed::Box, collections::BTreeSet, string::ToString, vec::Vec}; +use alloc::{boxed::Box, collections::BTreeSet, string::String, vec::Vec}; +use core::iter; use vm_core::{ utils::{ByteReader, ByteWriter, Deserializable, Serializable}, @@ -6,38 +7,133 @@ use vm_core::{ }; use vm_processor::{DeserializationError, Digest}; -use super::{placeholder::PlaceholderType, InitStorageData, MapEntry, StoragePlaceholder}; +use super::{ + placeholder::{FeltType, PlaceholderTypeRequirement, StorageValueName, TemplateType, WordType}, + InitStorageData, MapEntry, TemplateRequirementsIter, +}; use crate::account::{component::template::AccountComponentTemplateError, StorageMap}; // WORDS // ================================================================================================ -/// Supported word representations. Represents slot values and keys. +/// Defines how a word is represented within the component's storage description. +/// +/// Each word representation can be: +/// - A template that defines a type but does not carry a value. +/// - A predefined value that may contain a hardcoded word or a mix of fixed and templated felts. #[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] pub enum WordRepresentation { - /// A word represented by its four felts. - Value([Felt; 4]), - /// A word represented by 4 [FeltRepresentation]. - Array([FeltRepresentation; 4]), - /// A placeholder value, represented as "{{key}}", where `key` is the inner value of the - /// [StoragePlaceholder]. - Template(StoragePlaceholder), + /// A templated value that serves as a placeholder for instantiation. + /// + /// This variant defines a type but does not store a value. The actual value is provided at the + /// time of instantiation. The name is required to identify this template externally. + Template { + /// A human-readable identifier for the template. + name: StorageValueName, + /// An optional description explaining the purpose of this template. + description: Option, + /// The type associated with this templated word. + r#type: WordType, + }, + + /// A predefined value that can be used directly within storage. + /// + /// This variant may contain either a fully hardcoded word or a structured set of felts, some + /// of which may themselves be templates. + Value { + /// An optional name to identify this value externally. + name: Option, + /// An optional description explaining the role of this value. + description: Option, + /// The 4-felt representation of the stored word. + value: [FeltRepresentation; 4], + }, } impl WordRepresentation { - /// Returns an iterator over all storage placeholder references within the [WordRepresentation] - /// along with their expected types - pub fn all_placeholders_iter( + /// Constructs a new `Template` variant. + pub fn new_template( + name: impl Into, + description: Option, + r#type: WordType, + ) -> Self { + WordRepresentation::Template { name: name.into(), description, r#type } + } + + /// Constructs a new `Value` variant. + pub fn new_value( + value: impl Into<[FeltRepresentation; 4]>, + name: Option, + description: Option, + ) -> Self { + WordRepresentation::Value { name, description, value: value.into() } + } + + /// Returns the name associated with the word representation. + /// - For the `Template` variant, it always returns a reference to the name. + /// - For the `Value` variant, it returns `Some(&str)` if a name is present, or `None` + /// otherwise. + pub fn name(&self) -> Option<&StorageValueName> { + match self { + WordRepresentation::Template { name, .. } => Some(name), + WordRepresentation::Value { name, .. } => name.as_ref(), + } + } + + /// Returns the description associated with the word representation. + /// Both variants store an `Option`, which is converted to an `Option<&str>`. + pub fn description(&self) -> Option<&str> { + match self { + WordRepresentation::Template { description, .. } => description.as_deref(), + WordRepresentation::Value { description, .. } => description.as_deref(), + } + } + + /// Returns the `WordType` if this is a `Template` variant; otherwise, returns `None`. + pub fn word_type(&self) -> Option { + match self { + WordRepresentation::Template { r#type, .. } => Some(*r#type), + WordRepresentation::Value { .. } => None, + } + } + + /// Returns the default value (an array of 4 `FeltRepresentation`s) if this is a `Value` + /// variant; otherwise, returns `None`. + pub fn value(&self) -> Option<&[FeltRepresentation; 4]> { + match self { + WordRepresentation::Value { value, .. } => Some(value), + WordRepresentation::Template { .. } => None, + } + } + + /// Returns an iterator over the word's placeholders. + /// + /// For [`WordRepresentation::Value`], there correspond to inner iterators (since inner + /// elements can be templated as well). + /// For [`WordRepresentation::Template`] it returns the words's placeholder requirements + /// as defined. + pub fn template_requirements( &self, - ) -> Box + '_> { + placeholder_prefix: StorageValueName, + ) -> TemplateRequirementsIter<'_> { + let placeholder_key = + placeholder_prefix.with_suffix(self.name().unwrap_or(&StorageValueName::default())); match self { - WordRepresentation::Array(array) => { - Box::new(array.iter().flat_map(|felt| felt.all_placeholders_iter())) - }, - WordRepresentation::Template(storage_placeholder) => { - Box::new(core::iter::once((storage_placeholder, PlaceholderType::Word))) - }, - WordRepresentation::Value(_) => Box::new(core::iter::empty()), + // If it's a template, return the corresponding requirements + WordRepresentation::Template { description, r#type, .. } => Box::new(iter::once(( + placeholder_key, + PlaceholderTypeRequirement { + description: description.clone(), + r#type: Box::new(*r#type), + }, + ))), + // Otherwise, return inner iterators + WordRepresentation::Value { value, .. } => Box::new( + value + .iter() + .flat_map(move |felt| felt.template_requirements(placeholder_key.clone())), + ), } } @@ -46,110 +142,100 @@ impl WordRepresentation { /// If the representation is a template, the value is retrieved from /// `init_storage_data`, identified by its key. If any of the inner elements /// within the value are a template, they are retrieved in the same way. - pub fn try_build_word( + pub(crate) fn try_build_word( &self, init_storage_data: &InitStorageData, + placeholder_prefix: StorageValueName, ) -> Result { match self { - WordRepresentation::Value(word) => Ok(*word), - WordRepresentation::Array(array) => { + WordRepresentation::Template { name: placeholder_key, r#type, .. } => { + let placeholder_prefix = placeholder_prefix.with_suffix(placeholder_key); + let value = init_storage_data.get(&placeholder_prefix); + if let Some(v) = value { + let parsed_value = r#type.try_parse_word(v).unwrap(); + Ok(parsed_value) + } else { + Err(AccountComponentTemplateError::PlaceholderValueNotProvided( + placeholder_prefix, + )) + } + }, + WordRepresentation::Value { value, name, .. } => { let mut result = [Felt::ZERO; 4]; - for (index, felt_repr) in array.iter().enumerate() { - result[index] = felt_repr.clone().try_build_felt(init_storage_data)?; + + for (index, felt_repr) in value.iter().enumerate() { + let placeholder = + placeholder_prefix.clone().with_suffix(&name.clone().unwrap_or_default()); + result[index] = + felt_repr.clone().try_build_felt(init_storage_data, placeholder)?; } // SAFETY: result is guaranteed to have all its 4 indices rewritten Ok(result) }, - WordRepresentation::Template(storage_placeholder) => { - let user_value = init_storage_data - .get(storage_placeholder) - .ok_or_else(|| { - AccountComponentTemplateError::PlaceholderValueNotProvided( - storage_placeholder.clone(), - ) - })? - .as_word()?; - Ok(*user_value) - }, } } } -impl From for WordRepresentation { - fn from(value: Word) -> Self { - WordRepresentation::Value(value) - } -} - impl Serializable for WordRepresentation { fn write_into(&self, target: &mut W) { match self { - WordRepresentation::Value(value) => { + WordRepresentation::Template { name, description, r#type } => { target.write_u8(0); - target.write(value); + target.write(name); + target.write(description); + target.write(r#type); }, - WordRepresentation::Array(value) => { + WordRepresentation::Value { name, description, value } => { target.write_u8(1); + target.write(name); + target.write(description); target.write(value); }, - WordRepresentation::Template(storage_placeholder) => { - target.write_u8(2); - target.write(storage_placeholder); - }, } } } impl Deserializable for WordRepresentation { fn read_from(source: &mut R) -> Result { - let variant_tag = source.read_u8()?; - - match variant_tag { + let tag = source.read_u8()?; + match tag { 0 => { - // Hexadecimal - let value = <[Felt; 4]>::read_from(source)?; - Ok(WordRepresentation::Value(value)) + let name = StorageValueName::read_from(source)?; + let description = Option::::read_from(source)?; + let r#type = WordType::read_from(source)?; + Ok(WordRepresentation::Template { name, description, r#type }) }, 1 => { - // Array + let name = Option::::read_from(source)?; + let description = Option::::read_from(source)?; let value = <[FeltRepresentation; 4]>::read_from(source)?; - Ok(WordRepresentation::Array(value)) + Ok(WordRepresentation::Value { name, description, value }) }, - 2 => { - // Template - let storage_placeholder = StoragePlaceholder::read_from(source)?; - Ok(WordRepresentation::Template(storage_placeholder)) - }, - _ => Err(DeserializationError::InvalidValue(format!( - "unknown variant tag for WordRepresentation: {variant_tag}" + other => Err(DeserializationError::InvalidValue(format!( + "Unknown tag for WordRepresentation: {}", + other ))), } } } -impl Default for WordRepresentation { - fn default() -> Self { - WordRepresentation::Value(Default::default()) +impl From<[FeltRepresentation; 4]> for WordRepresentation { + fn from(value: [FeltRepresentation; 4]) -> Self { + WordRepresentation::new_value( + value, + Option::::None, + Option::::None, + ) } } -impl core::fmt::Display for WordRepresentation { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - WordRepresentation::Value(hex) => f.write_str(&Digest::from(hex).to_hex()), - WordRepresentation::Array(array) => { - f.write_str("[")?; - f.write_fmt(format_args!("{}, ", array[0]))?; - f.write_fmt(format_args!("{}, ", array[1]))?; - f.write_fmt(format_args!("{}, ", array[2]))?; - f.write_fmt(format_args!("{}, ", array[3]))?; - - f.write_str("]") - }, - WordRepresentation::Template(storage_placeholder) => { - f.write_fmt(format_args!("{}", storage_placeholder)) - }, - } +impl From<[Felt; 4]> for WordRepresentation { + fn from(value: [Felt; 4]) -> Self { + WordRepresentation::new_value( + value.map(FeltRepresentation::from), + Option::::None, + Option::::None, + ) } } @@ -157,73 +243,146 @@ impl core::fmt::Display for WordRepresentation { // ================================================================================================ /// Supported element representations for a component's storage entries. +/// +/// Each felt element in a storage entry can either be: +/// - A concrete value that holds a predefined felt. +/// - A template that specifies the type of felt expected, with the actual value to be provided +/// later. #[derive(Debug, Clone, PartialEq, Eq)] pub enum FeltRepresentation { - /// Hexadecimal representation of a field element. - Hexadecimal(Felt), - /// Single decimal representation of a field element. - Decimal(Felt), - /// A placeholder value, represented as "{{key}}". - Template(StoragePlaceholder), + /// A concrete felt value. + /// + /// This variant holds a felt that is part of the component's storage. + /// The optional name allows for identification, and the description offers additional context. + Value { + /// An optional identifier for this felt value. + name: Option, + /// An optional explanation of the felt's purpose. + description: Option, + /// The actual felt value. + value: Felt, + }, + + /// A templated felt element. + /// + /// This variant specifies the expected type of the felt without providing a concrete value. + /// The name is required to uniquely identify the template, and an optional description can + /// further clarify its intended use. + Template { + /// A unique name for the felt template. + name: StorageValueName, + /// An optional description that explains the purpose of this template. + description: Option, + /// The expected type for this felt element. + r#type: FeltType, + }, } impl FeltRepresentation { - /// Returns the storage placeholders within this representation, alongside their expected type. - pub fn all_placeholders_iter( - &self, - ) -> impl Iterator { - let maybe_key = match self { - FeltRepresentation::Template(storage_placeholder) => { - Some((storage_placeholder, PlaceholderType::Felt)) - }, - _ => None, - }; + /// Creates a new [`FeltRepresentation::Value`] variant. + pub fn new_value( + value: impl Into, + name: Option>, + description: Option, + ) -> FeltRepresentation { + FeltRepresentation::Value { + value: value.into(), + name: name.map(|n| n.into()), + description, + } + } - maybe_key.into_iter() + /// Creates a new [`FeltRepresentation::Template`] variant. + /// + /// The name will be used for identification at the moment of instantiating the componentn. + pub fn new_template( + r#type: FeltType, + name: impl Into, + description: Option, + ) -> FeltRepresentation { + FeltRepresentation::Template { name: name.into(), description, r#type } } /// Attempts to convert the [FeltRepresentation] into a [Felt]. /// /// If the representation is a template, the value is retrieved from `init_storage_data`, /// identified by its key. Otherwise, the returned value is just the inner element. - pub fn try_build_felt( + pub(crate) fn try_build_felt( self, init_storage_data: &InitStorageData, + placeholder_prefix: StorageValueName, ) -> Result { match self { - FeltRepresentation::Hexadecimal(base_element) => Ok(base_element), - FeltRepresentation::Decimal(base_element) => Ok(base_element), - FeltRepresentation::Template(storage_placeholder) => init_storage_data - .get(&storage_placeholder) - .ok_or(AccountComponentTemplateError::PlaceholderValueNotProvided( - storage_placeholder, - ))? - .as_felt() - .copied(), + FeltRepresentation::Template { name, r#type, .. } => { + let placeholder_key = placeholder_prefix.with_suffix(&name); + let raw_value = init_storage_data.get(&placeholder_key).ok_or( + AccountComponentTemplateError::PlaceholderValueNotProvided(placeholder_key), + )?; + + Ok(r#type + .parse_value(raw_value) + .map_err(AccountComponentTemplateError::StorageValueParsingError)?) + }, + FeltRepresentation::Value { value, .. } => Ok(value), + } + } + + /// Returns an iterator over the felt's template. + /// + /// For [`FeltRepresentation::Value`], these is an empty set; for + /// [`FeltRepresentation::Template`] it returns the felt's placeholder key based on the + /// felt's name within the component description. + pub fn template_requirements( + &self, + placeholder_prefix: StorageValueName, + ) -> TemplateRequirementsIter<'_> { + match self { + FeltRepresentation::Template { name, description, r#type } => Box::new(iter::once(( + placeholder_prefix.with_suffix(name), + PlaceholderTypeRequirement { + description: description.clone(), + r#type: Box::new(*r#type), + }, + ))), + _ => Box::new(iter::empty()), } } } +impl From for FeltRepresentation { + fn from(value: Felt) -> Self { + FeltRepresentation::new_value( + value, + Option::::None, + Option::::None, + ) + } +} + impl Default for FeltRepresentation { fn default() -> Self { - FeltRepresentation::Hexadecimal(Felt::default()) + FeltRepresentation::new_value( + Felt::default(), + Option::::None, + Option::::None, + ) } } impl Serializable for FeltRepresentation { fn write_into(&self, target: &mut W) { match self { - FeltRepresentation::Hexadecimal(felt) => { + FeltRepresentation::Value { name, description, value } => { target.write_u8(0); - target.write(felt); + target.write(name); + target.write(description); + target.write(value); }, - FeltRepresentation::Decimal(felt) => { + FeltRepresentation::Template { name, description, r#type } => { target.write_u8(1); - target.write(felt); - }, - FeltRepresentation::Template(storage_placeholder) => { - target.write_u8(2); - target.write(storage_placeholder); + target.write(name); + target.write(description); + target.write(r#type); }, } } @@ -231,138 +390,113 @@ impl Serializable for FeltRepresentation { impl Deserializable for FeltRepresentation { fn read_from(source: &mut R) -> Result { - let variant_tag = source.read_u8()?; - - match variant_tag { + let tag = source.read_u8()?; + match tag { 0 => { - // Hexadecimal - let felt = Felt::read_from(source)?; - Ok(FeltRepresentation::Hexadecimal(felt)) + let name = Option::::read_from(source)?; + let description = Option::::read_from(source)?; + let value = Felt::read_from(source)?; + Ok(FeltRepresentation::new_value(value, name, description)) }, 1 => { - // Decimal - let felt = Felt::read_from(source)?; - Ok(FeltRepresentation::Decimal(felt)) - }, - 2 => { - // Template - let storage_placeholder = StoragePlaceholder::read_from(source)?; - Ok(FeltRepresentation::Template(storage_placeholder)) + let name = StorageValueName::read_from(source)?; + let description = Option::::read_from(source)?; + let r#type = FeltType::read_from(source)?; + Ok(FeltRepresentation::new_template(r#type, name, description)) }, - _ => Err(DeserializationError::InvalidValue(format!( - "unknown variant tag for FeltRepresentation: {}", - variant_tag + other => Err(DeserializationError::InvalidValue(format!( + "Unknown tag for FeltRepresentation: {}", + other ))), } } } -impl core::fmt::Display for FeltRepresentation { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - FeltRepresentation::Hexadecimal(base_element) => { - f.write_fmt(format_args!("{}", base_element)) - }, - FeltRepresentation::Decimal(base_element) => { - f.write_fmt(format_args!("{}", base_element)) - }, - FeltRepresentation::Template(storage_placeholder) => { - f.write_fmt(format_args!("{}", storage_placeholder)) - }, - } - } -} - // MAP REPRESENTATION // ================================================================================================ /// Supported map representations for a component's storage entries. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "std", derive(::serde::Deserialize, ::serde::Serialize))] -#[cfg_attr(feature = "std", serde(untagged))] -pub enum MapRepresentation { - List(Vec), - Template(StoragePlaceholder), +pub struct MapRepresentation { + /// The human-readable name of the map slot. + name: StorageValueName, + /// An optional description for the slot, explaining its purpose. + description: Option, + /// Storage map entries, consisting of a list of keys associated with their values. + entries: Vec, } impl MapRepresentation { - /// Returns an iterator over all of the storage entries's placeholder keys, alongside their + /// Creates a new `MapRepresentation` from a vector of map entries. + pub fn new( + entries: Vec, + name: impl Into, + description: Option, + ) -> Self { + Self { entries, name: name.into(), description } + } + + /// Returns an iterator over all of the storage entries' placeholder keys, alongside their /// expected type. - pub fn all_placeholders_iter( - &self, - ) -> Box + '_> { - match self { - MapRepresentation::Template(storage_placeholder) => { - Box::new(core::iter::once((storage_placeholder, PlaceholderType::Map))) - }, - MapRepresentation::List(entries) => { - Box::new(entries.iter().flat_map(|entry| entry.all_placeholders_iter())) - }, - } + pub fn template_requirements(&self) -> TemplateRequirementsIter<'_> { + Box::new( + self.entries + .iter() + .flat_map(move |entry| entry.template_requirements(self.name.clone())), + ) } - /// Returns the amount of key-value pairs in the entry, or `None` if the representation is a - /// placeholder. - pub fn len(&self) -> Option { - match self { - MapRepresentation::List(vec) => Some(vec.len()), - MapRepresentation::Template(_) => None, - } + pub fn entries(&self) -> &[MapEntry] { + &self.entries + } + + pub fn name(&self) -> &StorageValueName { + &self.name + } + + pub fn description(&self) -> Option<&String> { + self.description.as_ref() } - /// Returns `true` if the map is represented by a list of key-value entries, and the list is - /// empty. Returns false otherwise + /// Returns the number of key-value pairs in the map. + pub fn len(&self) -> usize { + self.entries.len() + } + + /// Returns `true` if there are no entries in the map. pub fn is_empty(&self) -> bool { - match self { - MapRepresentation::List(vec) => vec.is_empty(), - MapRepresentation::Template(_) => false, - } + self.entries.is_empty() } /// Attempts to convert the [MapRepresentation] into a [StorageMap]. /// - /// If the representation is a template, the value is retrieved from - /// `init_storage_data`, identified by its key. If any of the inner elements - /// within the value are a template, they are retrieved in the same way. + /// If any of the inner elements are templates, their values are retrieved from + /// `init_storage_data`, identified by their key. pub fn try_build_map( &self, init_storage_data: &InitStorageData, ) -> Result { - let map = match self { - MapRepresentation::List(vec) => { - let entries = vec - .iter() - .map(|map_entry| { - let key = map_entry.key().try_build_word(init_storage_data)?; - let value = map_entry.value().try_build_word(init_storage_data)?; - Ok((key.into(), value)) - }) - .collect::, _>>()?; - - // validate that no key appears multiple times - let mut seen_keys = BTreeSet::new(); - for (map_key, _map_value) in entries.iter() { - if !seen_keys.insert(map_key) { - return Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys( - map_key.to_hex(), - )); - } - } + let entries = self + .entries + .iter() + .map(|map_entry| { + let key = map_entry.key().try_build_word(init_storage_data, self.name().clone())?; + let value = + map_entry.value().try_build_word(init_storage_data, self.name().clone())?; + Ok((key.into(), value)) + }) + .collect::, _>>()?; + + // Validate that no key appears multiple times. + let mut seen_keys = BTreeSet::new(); + for (map_key, _) in entries.iter() { + if !seen_keys.insert(map_key) { + return Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys(*map_key)); + } + } - StorageMap::with_entries(entries) - }, - MapRepresentation::Template(storage_placeholder) => init_storage_data - .get(storage_placeholder) - .ok_or_else(|| { - AccountComponentTemplateError::PlaceholderValueNotProvided( - storage_placeholder.clone(), - ) - })? - .as_map() - .cloned()?, - }; - - Ok(map) + Ok(StorageMap::with_entries(entries)) } /// Validates map keys by checking for duplicates. @@ -370,18 +504,20 @@ impl MapRepresentation { /// Because keys can be represented in a variety of ways, the `to_string()` implementation is /// used to check for duplicates. pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> { - match self { - MapRepresentation::List(entries) => { - let mut seen_keys = BTreeSet::new(); - for entry in entries { - if !seen_keys.insert(entry.key().to_string()) { - return Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys( - entry.key().to_string(), - )); - } + let mut seen_keys = BTreeSet::new(); + for entry in self.entries() { + // Until instantiating the component we can only tell if keys are duplicate for any + // predefined entries, so try to build any keys in the map to see if we have + // duplicates + if let Ok(key) = entry + .key() + .try_build_word(&InitStorageData::default(), StorageValueName::default()) + { + let key: Digest = key.into(); + if !seen_keys.insert(key) { + return Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys(key)); } - }, - MapRepresentation::Template(_) => {}, + }; } Ok(()) } @@ -389,142 +525,17 @@ impl MapRepresentation { impl Serializable for MapRepresentation { fn write_into(&self, target: &mut W) { - match self { - MapRepresentation::List(entries) => { - target.write_u8(0); - entries.write_into(target); - }, - MapRepresentation::Template(storage_placeholder) => { - target.write_u8(1); - storage_placeholder.write_into(target); - }, - } + self.entries.write_into(target); + self.name.write_into(target); + self.description.write_into(target); } } impl Deserializable for MapRepresentation { fn read_from(source: &mut R) -> Result { - match source.read_u8()? { - 0 => Ok(MapRepresentation::List(Vec::::read_from(source)?)), - 1 => Ok(MapRepresentation::Template(StoragePlaceholder::read_from(source)?)), - other => Err(DeserializationError::InvalidValue(format!( - "Unknown variant tag for MapRepresentation: {}", - other - ))), - } - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use vm_core::{ - utils::{Deserializable, Serializable}, - Felt, Word, - }; - - use crate::account::component::template::{ - storage::{FeltRepresentation, StorageValue, WordRepresentation}, - InitStorageData, StoragePlaceholder, - }; - - #[test] - fn test_storage_placeholder_try_from_str() { - let invalid_strings = vec![ - "{invalid}", - "no_braces", - "{{unclosed", - "unopened}}", - "{}", - "{{}}", - "{{.}}", - "{{foo..bar}}", - ]; - - for s in invalid_strings { - let result = StoragePlaceholder::try_from(s); - result.unwrap_err(); - } - - let s = "{{storage_placeholder}}"; - let tk = StoragePlaceholder::try_from(s).unwrap(); - assert_eq!(tk.inner(), "storage_placeholder"); - } - - #[test] - fn test_storage_placeholder_serialization_deserialization() { - let original = StoragePlaceholder::new("serialize_test").unwrap(); - let serialized = original.to_bytes(); - let deserialized = StoragePlaceholder::read_from_bytes(&serialized).unwrap(); - assert_eq!(original, deserialized); - } - - #[test] - fn test_felt_representation_serde() { - let felt = Felt::new(1234); - let original = FeltRepresentation::Hexadecimal(felt); - let serialized = original.to_bytes(); - let deserialized = FeltRepresentation::read_from_bytes(&serialized).unwrap(); - assert_eq!(original, deserialized); - - let felt = Felt::new(45563); - let original = FeltRepresentation::Decimal(felt); - let serialized = original.to_bytes(); - let deserialized = FeltRepresentation::read_from_bytes(&serialized).unwrap(); - assert_eq!(original, deserialized); - - let storage_placeholder = StoragePlaceholder::new("template_felt").unwrap(); - let original = FeltRepresentation::Template(storage_placeholder.clone()); - let serialized = original.to_bytes(); - let deserialized = FeltRepresentation::read_from_bytes(&serialized).unwrap(); - assert_eq!(original, deserialized); - } - - #[test] - fn test_felt_representation_try_build_felt() { - let dyn_key = StoragePlaceholder::new("felt_key").unwrap(); - let template = FeltRepresentation::Template(dyn_key.clone()); - let init_storage_data = InitStorageData::new([( - StoragePlaceholder::new("felt_key").unwrap(), - StorageValue::Felt(Felt::new(300)), - )]); - let built = template.try_build_felt(&init_storage_data).unwrap(); - assert_eq!(built, Felt::new(300)); - - let dyn_key = StoragePlaceholder::new("missing_key").unwrap(); - let template = FeltRepresentation::Template(dyn_key.clone()); - let result = template.try_build_felt(&init_storage_data); - result.unwrap_err(); - } - - #[test] - fn test_word_representation_serde() { - let word = Word::default(); - let original = WordRepresentation::Value(word); - let serialized = original.to_bytes(); - let deserialized = WordRepresentation::read_from_bytes(&serialized).unwrap(); - assert_eq!(original, deserialized); - - let array = [ - FeltRepresentation::Hexadecimal(Felt::new(10)), - FeltRepresentation::Decimal(Felt::new(20)), - FeltRepresentation::Template(StoragePlaceholder::new("word_key1").unwrap()), - FeltRepresentation::Template(StoragePlaceholder::new("word_key2").unwrap()), - ]; - let original = WordRepresentation::Array(array); - let serialized = original.to_bytes(); - let deserialized = WordRepresentation::read_from_bytes(&serialized).unwrap(); - assert_eq!(original, deserialized); - } - - #[test] - fn test_word_representation_template_serde() { - let storage_placeholder = StoragePlaceholder::new("temlpate_word").unwrap(); - let original = WordRepresentation::Template(storage_placeholder.clone()); - let serialized = original.to_bytes(); - let deserialized = WordRepresentation::read_from_bytes(&serialized).unwrap(); - assert_eq!(original, deserialized); + let entries = Vec::::read_from(source)?; + let name = StorageValueName::read_from(source)?; + let description = Option::::read_from(source)?; + Ok(Self { entries, name, description }) } } diff --git a/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs b/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs index 990f38a8b..394c8e9a7 100644 --- a/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs +++ b/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs @@ -1,6 +1,6 @@ -use alloc::collections::BTreeMap; +use alloc::{collections::BTreeMap, string::String}; -use super::{StoragePlaceholder, StorageValue}; +use super::StorageValueName; /// Represents the data required to initialize storage entries when instantiating an /// [AccountComponent](crate::account::AccountComponent) from a @@ -8,7 +8,7 @@ use super::{StoragePlaceholder, StorageValue}; #[derive(Clone, Debug, Default)] pub struct InitStorageData { /// A mapping of storage placeholder names to their corresponding storage values. - storage_placeholders: BTreeMap, + storage_placeholders: BTreeMap, } impl InitStorageData { @@ -17,20 +17,20 @@ impl InitStorageData { /// # Parameters /// /// - `entries`: An iterable collection of key-value pairs. - pub fn new(entries: impl IntoIterator) -> Self { + pub fn new(entries: impl IntoIterator) -> Self { InitStorageData { storage_placeholders: entries.into_iter().collect(), } } /// Retrieves a reference to the storage placeholders. - pub fn placeholders(&self) -> &BTreeMap { + pub fn placeholders(&self) -> &BTreeMap { &self.storage_placeholders } /// Returns a reference to the [StorageValue] corresponding to the placeholder, or /// [`Option::None`] if the placeholder is not present. - pub fn get(&self, key: &StoragePlaceholder) -> Option<&StorageValue> { + pub fn get(&self, key: &StorageValueName) -> Option<&String> { self.storage_placeholders.get(key) } } diff --git a/crates/miden-objects/src/account/component/template/storage/mod.rs b/crates/miden-objects/src/account/component/template/storage/mod.rs index adda76fa8..a76df0759 100644 --- a/crates/miden-objects/src/account/component/template/storage/mod.rs +++ b/crates/miden-objects/src/account/component/template/storage/mod.rs @@ -1,6 +1,9 @@ use alloc::{boxed::Box, string::String, vec::Vec}; -use vm_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; +use vm_core::{ + utils::{ByteReader, ByteWriter, Deserializable, Serializable}, + Felt, FieldElement, +}; use vm_processor::DeserializationError; mod entry_content; @@ -10,7 +13,10 @@ use super::AccountComponentTemplateError; use crate::account::StorageSlot; mod placeholder; -pub use placeholder::{PlaceholderType, StoragePlaceholder, StorageValue}; +pub use placeholder::{ + FeltType, PlaceholderTypeRequirement, StorageValueError, StorageValueName, + StorageValueNameError, WordType, +}; mod init_storage_data; pub use init_storage_data::InitStorageData; @@ -18,6 +24,11 @@ pub use init_storage_data::InitStorageData; #[cfg(feature = "std")] pub mod toml; +/// Alias used for iterators that collect all placeholders and their types within a component +/// template. +pub type TemplateRequirementsIter<'a> = + Box + 'a>; + // STORAGE ENTRY // ================================================================================================ @@ -29,25 +40,18 @@ pub mod toml; /// - A multi-slot entry spanning multiple contiguous slots with multiple words (but not maps) that /// represent a single logical value. #[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] pub enum StorageEntry { - /// A value slot, which can contain one or more words. Each word is a hex-encoded string. + /// A value slot, which can contain one word. Value { - /// The human-readable name of the slot. - name: String, - /// An optional description for the slot, explaining its purpose. - description: Option, - /// The numeric index of this slot in the component's storage layout. + /// The numeric index of this map slot in the component's storage. slot: u8, - /// The initial value for this slot. - value: WordRepresentation, + /// An description of a word, representing either a predefined value or a templated one. + word_entry: WordRepresentation, }, /// A map slot, containing multiple key-value pairs. Keys and values are hex-encoded strings. Map { - /// The human-readable name of the map slot. - name: String, - /// An optional description for the slot, explaining its purpose. - description: Option, /// The numeric index of this map slot in the component's storage. slot: u8, /// A list of key-value pairs to initialize in this map slot. @@ -57,66 +61,47 @@ pub enum StorageEntry { /// A multi-slot entry, representing a single logical value across multiple slots. MultiSlot { /// The human-readable name of this multi-slot entry. - name: String, + name: StorageValueName, /// An optional description for the slot, explaining its purpose. description: Option, /// The indices of the slots that form this multi-slot entry. slots: Vec, /// A list of values to fill the logical slot, with a length equal to the amount of slots. - values: Vec, + values: Vec<[FeltRepresentation; 4]>, }, } impl StorageEntry { - /// Creates a new [`StorageEntry::Value`] variant. - pub fn new_value( - name: impl Into, - description: Option>, - slot: u8, - value: impl Into, - ) -> Self { - StorageEntry::Value { - name: name.into(), - description: description.map(Into::::into), - slot, - value: value.into(), - } + pub fn new_value(slot: u8, word_entry: impl Into) -> Self { + StorageEntry::Value { slot, word_entry: word_entry.into() } } - /// Creates a new [`StorageEntry::Map`] variant. - pub fn new_map( - name: impl Into, - description: Option>, - slot: u8, - map_representation: MapRepresentation, - ) -> Result { - let entry = StorageEntry::Map { - name: name.into(), - description: description.map(Into::::into), - slot, - map: map_representation, - }; - - entry.validate()?; - Ok(entry) + pub fn new_map(slot: u8, map: MapRepresentation) -> Self { + StorageEntry::Map { slot, map } } - /// Creates a new [`StorageEntry::MultiSlot`] variant. - pub fn new_multi_slot( - name: impl Into, - description: Option>, + pub fn new_multislot( + name: impl Into, + description: Option, slots: Vec, - values: Vec>, - ) -> Result { - let entry = StorageEntry::MultiSlot { + values: Vec<[FeltRepresentation; 4]>, + ) -> Self { + StorageEntry::MultiSlot { name: name.into(), - description: description.map(Into::::into), + description: description.map(Into::into), slots, - values: values.into_iter().map(Into::into).collect(), - }; + values, + } + } - entry.validate()?; - Ok(entry) + pub fn name(&self) -> &StorageValueName { + match self { + StorageEntry::Value { word_entry, .. } => { + word_entry.name().expect("by construction, all top level entries have names") + }, + StorageEntry::Map { map, .. } => map.name(), + StorageEntry::MultiSlot { name, .. } => name, + } } /// Returns the slot indices that the storage entry covers. @@ -128,48 +113,22 @@ impl StorageEntry { } } - /// Returns the name of the storage entry. - pub fn name(&self) -> &str { - match self { - StorageEntry::Value { name, .. } => name.as_str(), - StorageEntry::Map { name, .. } => name.as_str(), - StorageEntry::MultiSlot { name, .. } => name.as_str(), - } - } - - /// Returns the optional description of the storage entry. - pub fn description(&self) -> Option<&str> { - match self { - StorageEntry::Value { description, .. } => description.as_deref(), - StorageEntry::Map { description, .. } => description.as_deref(), - StorageEntry::MultiSlot { description, .. } => description.as_deref(), - } - } - - /// Returns all the `WordRepresentation` values covered by this entry. - /// For `Value` entries, this returns a single-element slice. - /// For `MultiSlot` entries, this returns all values. - /// For `Map` entries, since they're key-value pairs, return an empty slice. - pub fn word_values(&self) -> &[WordRepresentation] { - match self { - StorageEntry::Value { value, .. } => core::slice::from_ref(value), - StorageEntry::MultiSlot { values, .. } => values.as_slice(), - StorageEntry::Map { .. } => &[], - } - } - - /// Returns an iterator over all of the storage entries's placeholder keys, alongside their + /// Returns an iterator over all of the storage entries's value names, alongside their /// expected type. - pub fn all_placeholders_iter( - &self, - ) -> Box + '_> { - match self { - StorageEntry::Value { value, .. } => value.all_placeholders_iter(), - StorageEntry::Map { map: map_entries, .. } => map_entries.all_placeholders_iter(), + pub fn template_requirements(&self) -> TemplateRequirementsIter { + let requirements = match self { + StorageEntry::Value { word_entry, .. } => { + word_entry.template_requirements(StorageValueName::default()) + }, + StorageEntry::Map { map: map_entries, .. } => map_entries.template_requirements(), StorageEntry::MultiSlot { values, .. } => { - Box::new(values.iter().flat_map(|word| word.all_placeholders_iter())) + Box::new(values.iter().flat_map(move |word| { + word.iter().flat_map(move |f| f.template_requirements(self.name().clone())) + })) }, - } + }; + + requirements } /// Attempts to convert the storage entry into a list of [StorageSlot]. @@ -185,8 +144,9 @@ impl StorageEntry { init_storage_data: &InitStorageData, ) -> Result, AccountComponentTemplateError> { match self { - StorageEntry::Value { value, .. } => { - let slot = value.try_build_word(init_storage_data)?; + StorageEntry::Value { word_entry, .. } => { + let slot = + word_entry.try_build_word(init_storage_data, StorageValueName::default())?; Ok(vec![StorageSlot::Value(slot)]) }, StorageEntry::Map { map: values, .. } => { @@ -196,7 +156,15 @@ impl StorageEntry { StorageEntry::MultiSlot { values, .. } => Ok(values .iter() .map(|word_repr| { - word_repr.clone().try_build_word(init_storage_data).map(StorageSlot::Value) + let mut result = [Felt::ZERO; 4]; + + for (index, felt_repr) in word_repr.iter().enumerate() { + result[index] = felt_repr + .clone() + .try_build_felt(init_storage_data, self.name().clone())?; + } + // SAFETY: result is guaranteed to have all its 4 indices rewritten + Ok(StorageSlot::Value(result)) }) .collect::, _>>()?), } @@ -237,19 +205,15 @@ impl StorageEntry { impl Serializable for StorageEntry { fn write_into(&self, target: &mut W) { match self { - StorageEntry::Value { name, description, slot, value } => { + StorageEntry::Value { slot, word_entry } => { target.write_u8(0u8); - target.write(name); - target.write(description); target.write_u8(*slot); - target.write(value); + target.write(word_entry); }, - StorageEntry::Map { name, description, slot, map: values } => { + StorageEntry::Map { slot, map, .. } => { target.write_u8(1u8); - target.write(name); - target.write(description); target.write_u8(*slot); - target.write(values); + target.write(map); }, StorageEntry::MultiSlot { name, description, slots, values } => { target.write_u8(2u8); @@ -265,42 +229,27 @@ impl Serializable for StorageEntry { impl Deserializable for StorageEntry { fn read_from(source: &mut R) -> Result { let variant_tag = source.read_u8()?; - let name: String = source.read()?; - let description: Option = source.read()?; - match variant_tag { - // Value 0 => { let slot = source.read_u8()?; - let value: WordRepresentation = source.read()?; - - Ok(StorageEntry::Value { name, description, slot, value }) + let word_entry: WordRepresentation = source.read()?; + Ok(StorageEntry::Value { slot, word_entry }) }, - - // Map 1 => { let slot = source.read_u8()?; - let map_representation: MapRepresentation = source.read()?; - - Ok(StorageEntry::Map { - name, - description, - slot, - map: map_representation, - }) + let map: MapRepresentation = source.read()?; + Ok(StorageEntry::Map { slot, map }) }, - - // MultiSlot 2 => { + let name: StorageValueName = source.read()?; + let description: Option = source.read()?; let slots: Vec = source.read()?; - let values: Vec = source.read()?; - + let values: Vec<[FeltRepresentation; 4]> = source.read()?; Ok(StorageEntry::MultiSlot { name, description, slots, values }) }, - - // Unknown tag => error _ => Err(DeserializationError::InvalidValue(format!( - "unknown variant tag `{variant_tag}` for StorageEntry" + "unknown variant tag `{}` for StorageEntry", + variant_tag ))), } } @@ -330,18 +279,20 @@ impl MapEntry { &self.value } - /// Returns an iterator over all of the storage entries's placeholder keys, alongside their - /// expected type. - pub fn all_placeholders_iter( - &self, - ) -> impl Iterator { - self.key.all_placeholders_iter().chain(self.value.all_placeholders_iter()) - } - pub fn into_parts(self) -> (WordRepresentation, WordRepresentation) { let MapEntry { key, value } = self; (key, value) } + + pub fn template_requirements( + &self, + placeholder_prefix: StorageValueName, + ) -> TemplateRequirementsIter<'_> { + let key_iter = self.key.template_requirements(placeholder_prefix.clone()); + let value_iter = self.value.template_requirements(placeholder_prefix); + + Box::new(key_iter.chain(value_iter)) + } } impl Serializable for MapEntry { @@ -365,92 +316,129 @@ impl Deserializable for MapEntry { #[cfg(test)] mod tests { use core::panic; - use std::collections::BTreeSet; + use std::string::ToString; use assembly::Assembler; - use assert_matches::assert_matches; use semver::Version; - use vm_core::{Felt, FieldElement}; + use vm_core::{ + utils::{Deserializable, Serializable}, + Felt, FieldElement, Word, + }; - use super::*; use crate::{ account::{ - component::template::{AccountComponentMetadata, AccountComponentTemplate}, - AccountComponent, AccountType, StorageMap, + component::template::{ + storage::placeholder::{FeltType, WordType}, + AccountComponentMetadata, InitStorageData, MapEntry, MapRepresentation, + StorageValueName, + }, + AccountComponent, AccountComponentTemplate, AccountType, FeltRepresentation, + StorageEntry, StorageSlot, StorageValueError, WordRepresentation, }, digest, + errors::AccountComponentTemplateError, testing::account_code::CODE, AccountError, }; #[test] fn test_storage_entry_serialization() { - let array = [ - FeltRepresentation::Decimal(Felt::new(0xabc)), - FeltRepresentation::Decimal(Felt::new(1218)), - FeltRepresentation::Hexadecimal(Felt::new(0xdba3)), - FeltRepresentation::Template(StoragePlaceholder::new("test.array.dyn").unwrap()), + let felt_array: [FeltRepresentation; 4] = [ + FeltRepresentation::from(Felt::new(0xabc)), + FeltRepresentation::from(Felt::new(1218)), + FeltRepresentation::from(Felt::new(0xdba3)), + FeltRepresentation::new_template( + FeltType::default(), + StorageValueName::new("slot3").unwrap(), + Some("dummy description".into()), + ), ]; + + let test_word: Word = digest!("0x000001").into(); + let test_word = test_word.map(FeltRepresentation::from); + + let map_representation = MapRepresentation::new( + vec![ + MapEntry { + key: WordRepresentation::new_template( + StorageValueName::new("foo").unwrap(), + None, + WordType::RpoFalcon512PublicKey, + ), + value: WordRepresentation::new_value(test_word.clone(), None, None), + }, + MapEntry { + key: WordRepresentation::new_value(test_word.clone(), None, None), + value: WordRepresentation::new_template( + StorageValueName::new("bar").unwrap(), + Some("bar description".into()), + WordType::RpoFalcon512PublicKey, + ), + }, + MapEntry { + key: WordRepresentation::new_template( + StorageValueName::new("baz").unwrap(), + Some("baz description".into()), + WordType::RpoFalcon512PublicKey, + ), + value: WordRepresentation::new_value(test_word, None, None), + }, + ], + StorageValueName::new("map").unwrap(), + Some("A storage map entry".into()), + ); + let storage = vec![ - StorageEntry::Value { - name: "slot0".into(), - description: Some("First slot".into()), - slot: 0, - value: WordRepresentation::Value(digest!("0x333123").into()), - }, - StorageEntry::Map { - name: "map".into(), - description: Some("A storage map entry".into()), - slot: 1, - map: MapRepresentation::List(vec![ - MapEntry { - key: WordRepresentation::Template( - StoragePlaceholder::new("foo.bar").unwrap(), + StorageEntry::new_value(0, felt_array.clone()), + StorageEntry::new_map(1, map_representation), + StorageEntry::new_multislot( + StorageValueName::new("multi").unwrap(), + Some("Multi slot entry".into()), + vec![2, 3], + vec![ + [ + FeltRepresentation::new_template( + FeltType::Felt, + StorageValueName::new("test").unwrap(), + None, ), - value: WordRepresentation::Value(digest!("0x2").into()), - }, - MapEntry { - key: WordRepresentation::Value(digest!("0x2").into()), - value: WordRepresentation::Template( - StoragePlaceholder::new("bar.baz").unwrap(), + FeltRepresentation::new_template( + FeltType::Felt, + StorageValueName::new("test2").unwrap(), + None, ), - }, - MapEntry { - key: WordRepresentation::Value(digest!("0x3").into()), - value: WordRepresentation::Value(digest!("0x4").into()), - }, - ]), - }, - StorageEntry::MultiSlot { - name: "multi".into(), - description: Some("Multi slot entry".into()), - slots: vec![2, 3, 4], - values: vec![ - WordRepresentation::Template(StoragePlaceholder::new("test.Template").unwrap()), - WordRepresentation::Array(array), - WordRepresentation::Value(digest!("0xabcdef123abcdef123").into()), + FeltRepresentation::new_template( + FeltType::Felt, + StorageValueName::new("test3").unwrap(), + None, + ), + FeltRepresentation::new_template( + FeltType::Felt, + StorageValueName::new("test4").unwrap(), + None, + ), + ], + felt_array, ], - }, - StorageEntry::Value { - name: "single-slot".into(), - description: Some("Slot with storage placeholder".into()), - slot: 5, - value: WordRepresentation::Template( - StoragePlaceholder::new("single-slot-key").unwrap(), + ), + StorageEntry::new_value( + 4, + WordRepresentation::new_template( + StorageValueName::new("single").unwrap(), + None, + WordType::Words(1), ), - }, + ), ]; let config = AccountComponentMetadata { name: "Test Component".into(), description: "This is a test component".into(), version: Version::parse("1.0.0").unwrap(), - targets: BTreeSet::from([AccountType::FungibleFaucet]), + targets: std::collections::BTreeSet::from([AccountType::FungibleFaucet]), storage, }; - let toml = config.as_toml().unwrap(); - let deserialized = AccountComponentMetadata::from_toml(&toml).unwrap(); assert_eq!(deserialized, config); @@ -459,55 +447,49 @@ mod tests { #[test] pub fn test_toml() { let toml_text = r#" - name = "Test Component" - description = "This is a test component" - version = "1.0.1" - targets = ["FungibleFaucet", "RegularAccountImmutableCode"] - - [[storage]] - name = "map" - description = "A storage map entry" - slot = 0 - values = [ - { key = "0x1", value = ["{{value.test}}", "0x1", "0x2", "0x3"] }, - { key = "{{map.key.test}}", value = "0x3" }, - { key = "0x3", value = "0x4" } - ] - - [[storage]] - name = "test-word" - description = "word" - slot = 1 - value = "{{word.test}}" - - [[storage]] - name = "multitest" - description = "a multi slot test" - slots = [2, 3] - values = [ - "{{word.test}}", - ["1", "0", "0", "0"], - ] - - [[storage]] - name = "map-template" - description = "a templated map" - slot = 4 - values = "{{map.template}}" + name = "Test Component" + description = "This is a test component" + version = "1.0.1" + targets = ["FungibleFaucet", "RegularAccountImmutableCode"] + + [[storage]] + name = "map_entry" + slot = 0 + values = [ + { key = "0x1", value = ["0x1","0x2","0x3","0"]}, + { key = "0x3", value = "0x123" }, + { key = { name = "map_key_template", description = "this tests that the default type is correctly set" }, value = "0x3" } + ] + + [[storage]] + name = "token_metadata" + description = "Contains metadata about the token associated to the faucet account" + slot = 1 + value = [ + { type = "felt", name = "max_supply", description = "Maximum supply of the token in base units" }, # placeholder + { type = "tokensymbol", value = "TST" }, # hardcoded non-felt type + { type = "u8", name = "decimals", description = "Number of decimal places" }, # placeholder + { value = "0" }, # could also be just "0"? maybe type can be inferred to either felt or word depending on "position" + ] + + [[storage]] + name = "default_recallable_height" + slot = 2 + type = "u32" "#; let component_metadata = AccountComponentMetadata::from_toml(toml_text).unwrap(); - for (key, placeholder_type) in component_metadata.get_unique_storage_placeholders() { - match key.inner() { - "map.key.test" | "word.test" => assert_eq!(placeholder_type, PlaceholderType::Word), - "value.test" => assert_eq!(placeholder_type, PlaceholderType::Felt), - "map.template" => assert_eq!(placeholder_type, PlaceholderType::Map), - _ => panic!("all cases are covered"), + for (key, requirement) in component_metadata.get_template_requirements().iter() { + match key.as_str() { + "token_metadata.max_supply" + | "token_metadata.decimals" + | "default_recallable_height" => continue, + "map_entry.map_key_template" => assert!(requirement.r#type.to_string() == "word"), + _ => panic!("all cases should have been covered"), } } let library = Assembler::default().assemble_library([CODE]).unwrap(); - let template = AccountComponentTemplate::new(component_metadata, library); let template_bytes = template.to_bytes(); @@ -515,27 +497,47 @@ mod tests { AccountComponentTemplate::read_from_bytes(&template_bytes).unwrap(); assert_eq!(template, template_deserialized); + // Fail to parse because 2800 > u8 + let storage_placeholders = InitStorageData::new([ ( - StoragePlaceholder::new("map.key.test").unwrap(), - StorageValue::Word(Default::default()), + StorageValueName::new("map_entry.map_key_template").unwrap(), + "0x123".to_string(), ), ( - StoragePlaceholder::new("value.test").unwrap(), - StorageValue::Felt(Felt::new(64)), + StorageValueName::new("token_metadata.max_supply").unwrap(), + 20_000u64.to_string(), ), + (StorageValueName::new("token_metadata.decimals").unwrap(), "2800".into()), + (StorageValueName::new("default_recallable_height").unwrap(), "0".into()), + ]); + + let component = AccountComponent::from_template(&template, &storage_placeholders); + assert_matches::assert_matches!( + component, + Err(AccountError::AccountComponentTemplateInstantiationError( + AccountComponentTemplateError::StorageValueParsingError( + StorageValueError::ParseError(_, _) + ) + )) + ); + + // Instantiate succesfully + + let storage_placeholders = InitStorageData::new([ ( - StoragePlaceholder::new("word.test").unwrap(), - StorageValue::Word([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(128)]), + StorageValueName::new("map_entry.map_key_template").unwrap(), + "0x123".to_string(), ), ( - StoragePlaceholder::new("map.template").unwrap(), - StorageValue::Map(StorageMap::default()), + StorageValueName::new("token_metadata.max_supply").unwrap(), + 20_000u64.to_string(), ), + (StorageValueName::new("token_metadata.decimals").unwrap(), "128".into()), + (StorageValueName::new("default_recallable_height").unwrap(), "0".into()), ]); let component = AccountComponent::from_template(&template, &storage_placeholders).unwrap(); - assert_eq!( component.supported_types(), &[AccountType::FungibleFaucet, AccountType::RegularAccountImmutableCode] @@ -549,49 +551,22 @@ mod tests { _ => panic!("should be map"), } - let value_entry = component.storage_slots().get(1).unwrap(); + let value_entry = component.storage_slots().get(2).unwrap(); match value_entry { StorageSlot::Value(v) => { - assert_eq!(v, &[Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(128)]) + assert_eq!(v, &[Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO]) }, _ => panic!("should be value"), } let failed_instantiation = AccountComponent::from_template(&template, &InitStorageData::default()); - assert_matches!( + + assert_matches::assert_matches!( failed_instantiation, Err(AccountError::AccountComponentTemplateInstantiationError( AccountComponentTemplateError::PlaceholderValueNotProvided(_) )) ); } - - #[test] - pub fn fail_placeholder_type_mismatch() { - let toml_text = r#" - name = "Test Component" - description = "This is a test component" - version = "1.0.1" - targets = ["FungibleFaucet"] - - [[storage]] - name = "map" - description = "A storage map entry" - slot = 0 - values = [ - { key = "0x1", value = ["{{value.test}}", "0x1", "0x2", "0x3"] }, - ] - - [[storage]] - name = "word" - slot = 1 - value = "{{value.test}}" - "#; - let component_metadata = AccountComponentMetadata::from_toml(toml_text); - assert_matches!( - component_metadata, - Err(AccountComponentTemplateError::StoragePlaceholderTypeMismatch(_, _, _)) - ); - } } diff --git a/crates/miden-objects/src/account/component/template/storage/placeholder.rs b/crates/miden-objects/src/account/component/template/storage/placeholder.rs index fb3134638..5cd5f9aee 100644 --- a/crates/miden-objects/src/account/component/template/storage/placeholder.rs +++ b/crates/miden-objects/src/account/component/template/storage/placeholder.rs @@ -1,54 +1,59 @@ -use alloc::string::{String, ToString}; +use alloc::{ + boxed::Box, + str, + string::{String, ToString}, + vec::Vec, +}; +use core::{ + fmt::{self, Display}, + str::FromStr, +}; use thiserror::Error; use vm_core::{ utils::{ByteReader, ByteWriter, Deserializable, Serializable}, - Felt, Word, + Felt, FieldElement, Word, }; use vm_processor::DeserializationError; -use crate::account::{component::template::AccountComponentTemplateError, StorageMap}; +use crate::{asset::TokenSymbol, utils::parse_hex_string_as_word}; -// STORAGE PLACEHOLDER +// CONSTANTS // ================================================================================================ -/// A simple wrapper type around a string key that enables templating. +const FELT_TYPE_U8: &str = "u8"; +const FELT_TYPE_U16: &str = "u16"; +const FELT_TYPE_U32: &str = "u32"; +const FELT_TYPE_FELT: &str = "felt"; +const FELT_TYPE_TOKEN_SYMBOL: &str = "tokensymbol"; + +const WORD_TYPE: &str = "word"; +const FALCON_PUBKEY_TYPE: &str = "auth::rpo_falcon512::pub_key"; + +// STORAGE VALUE NAME +// ================================================================================================ + +/// A simple wrapper type around a string key that identifies values. +/// +/// A storage value name is a string that identifies dynamic values within a component's metadata +/// storage entries. /// -/// A storage placeholder is a string that identifies dynamic values within a component's metadata -/// storage entries. Storage placeholders are serialized as "{{key}}" and can be used as -/// placeholders in map keys, map values, or individual [Felt]s within a [Word]. +/// These names can be chained together, in a way that allows to compose unique keys for +/// inner templated elements. /// -/// At component instantiation, a map of keys to [StorageValue] must be provided to dynamically +/// At component instantiation, a map of names to values must be provided to dynamically /// replace these placeholders with the instance’s actual values. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct StoragePlaceholder { - key: String, +#[derive(Clone, Debug, Default, Ord, PartialOrd, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(::serde::Deserialize, ::serde::Serialize))] +#[cfg_attr(feature = "std", serde(transparent))] +pub struct StorageValueName { + fully_qualified_name: String, } -/// An identifier for the expected type for a storage placeholder. -/// These indicate which variant of [StorageValue] should be provided when instantiating a -/// component. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum PlaceholderType { - Felt, - Map, - Word, -} - -impl core::fmt::Display for PlaceholderType { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - PlaceholderType::Felt => f.write_str("Felt"), - PlaceholderType::Map => f.write_str("Map"), - PlaceholderType::Word => f.write_str("Word"), - } - } -} - -impl StoragePlaceholder { - /// Creates a new [StoragePlaceholder] from the provided string. +impl StorageValueName { + /// Creates a new [`StorageValueName``] from the provided string. /// - /// A [StoragePlaceholder] serves as an identifier for storage values that are determined at + /// A [`StorageValueName``] serves as an identifier for storage values that are determined at /// instantiation time of an [AccountComponentTemplate](super::super::AccountComponentTemplate). /// /// The key can consist of one or more segments separated by dots (`.`). @@ -60,142 +65,403 @@ impl StoragePlaceholder { /// This method returns an error if: /// - Any segment (or the whole key) is empty. /// - Any segment contains invalid characters. - pub fn new(key: impl Into) -> Result { - let key: String = key.into(); - Self::validate(&key)?; - Ok(Self { key }) + pub fn new(base: impl Into) -> Result { + let base: String = base.into(); + for segment in base.split('.') { + Self::validate_segment(segment)?; + } + Ok(Self { fully_qualified_name: base }) + } + + /// Adds a suffix to the storage value name, separated with a dot. + #[must_use] + pub fn with_suffix(self, suffix: &StorageValueName) -> StorageValueName { + let mut key = self; + if !suffix.inner().is_empty() { + if !key.inner().is_empty() { + key.fully_qualified_name.push('.'); + } + key.fully_qualified_name.push_str(&suffix.to_string()); + } + + key } - /// Returns the key name pub fn inner(&self) -> &str { - &self.key + &self.fully_qualified_name } - /// Checks if the given string is a valid key. - /// A storage placeholder is valid if it's made of one or more segments that are non-empty - /// alphanumeric strings. - fn validate(key: &str) -> Result<(), StoragePlaceholderError> { - if key.is_empty() { - return Err(StoragePlaceholderError::EmptyKey); + fn validate_segment(segment: &str) -> Result<(), StorageValueNameError> { + if segment.is_empty() { + return Err(StorageValueNameError::EmptySegment); + } + if !segment.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') { + return Err(StorageValueNameError::InvalidCharacter { + part: segment.to_string(), + character: segment + .chars() + .find(|c| !(c.is_ascii_alphanumeric() || *c == '_' || *c == '-')) + .unwrap(), + }); } + Ok(()) + } +} - for segment in key.split('.') { - if segment.is_empty() { - return Err(StoragePlaceholderError::EmptyKey); - } +impl Display for StorageValueName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.inner()) + } +} - for c in segment.chars() { - if !(c.is_ascii_alphanumeric() || c == '_' || c == '-') { - return Err(StoragePlaceholderError::InvalidChar(key.into(), c)); - } - } - } +impl Serializable for StorageValueName { + fn write_into(&self, target: &mut W) { + target.write(&self.fully_qualified_name); + } +} - Ok(()) +impl Deserializable for StorageValueName { + fn read_from(source: &mut R) -> Result { + let key: String = source.read()?; + Ok(StorageValueName { fully_qualified_name: key }) } } -impl TryFrom<&str> for StoragePlaceholder { - type Error = StoragePlaceholderError; +#[derive(Debug, Error)] +pub enum StorageValueNameError { + #[error("key segment is empty")] + EmptySegment, + #[error("key segment '{part}' contains invalid character '{character}'")] + InvalidCharacter { part: String, character: char }, +} + +// TEMPLATE REQUIREMENT +// ================================================================================================ - fn try_from(value: &str) -> Result { - if value.starts_with("{{") && value.ends_with("}}") { - let inner = &value[2..value.len() - 2]; - Self::validate(inner)?; +/// Describes the expected type of value for a templated storage entry. +/// +/// These types must be able to be parsed from [`String`] into the native type, and then converted +/// into the correct storage type ([`Felt`], or one or more [`Word`]s). +#[derive(Debug)] +pub struct PlaceholderTypeRequirement { + pub r#type: Box, + pub description: Option, +} + +// TEMPLATE TYPE +// ================================================================================================ + +/// The [`TemplateType`] trait defines an interface for converting strings into storage values. +/// +/// Types implementing this trait support conversion from a string into either a single [`Felt`] +/// (a field element) or into one or more [`Word`]s. These conversions are used during the +/// instantiation of account component templates, where placeholder values provided as strings +/// must be parsed into their native storage representations. +pub trait TemplateType: alloc::fmt::Debug + ToString { + /// Attempts to parse the given string into a [`Felt`]. + /// + /// # Errors + /// + /// Returns a [`StorageValueError`] if the string cannot be parsed into a [`Felt`]. + fn try_parse_felt(&self, value: &str) -> Result; - Ok(StoragePlaceholder { key: inner.to_string() }) - } else { - Err(StoragePlaceholderError::FormatError(value.into())) + /// Attempts to parse the given string into a vector of [`Word`]s. + /// + /// # Errors + /// + /// Returns a [`StorageValueError`] if the string cannot be parsed into the expected vector + /// of [`Word`]s. + fn try_parse_words(&self, value: &str) -> Result, StorageValueError>; + + /// Attempts to parse the given string into a single [`Word`]. + /// + /// This method calls `try_parse_words` internally and returns an error if the result does + /// not contain exactly one word. + /// + /// # Errors + /// + /// Returns a [`StorageValueError::TypeArityMismatch`] if the parsed result does not have + /// exactly one element. + fn try_parse_word(&self, value: &str) -> Result { + let mut words = self.try_parse_words(value)?; + if words.len() != 1 { + return Err(StorageValueError::TypeArityMismatch); } + Ok(words.pop().expect("checked that there's one value")) } } -impl TryFrom<&String> for StoragePlaceholder { - type Error = StoragePlaceholderError; +impl TemplateType for FeltType { + fn try_parse_felt(&self, value: &str) -> Result { + self.parse_value(value) + } - fn try_from(value: &String) -> Result { - Self::try_from(value.as_str()) + fn try_parse_words(&self, value: &str) -> Result, StorageValueError> { + let felt = self.parse_value(value)?; + Ok(vec![[Felt::ZERO, Felt::ZERO, Felt::ZERO, felt]]) } } -impl core::fmt::Display for StoragePlaceholder { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{{{{{}}}}}", self.key) +impl TemplateType for WordType { + fn try_parse_felt(&self, value: &str) -> Result { + match self { + WordType::FeltType(ft) => ft.try_parse_felt(value), + _ => Err(StorageValueError::TypeArityMismatch), + } + } + + fn try_parse_words(&self, value: &str) -> Result, StorageValueError> { + match self { + WordType::FeltType(ft) => { + let felt = ft.parse_value(value)?; + Ok(vec![[Felt::ZERO, Felt::ZERO, Felt::ZERO, felt]]) + }, + WordType::Words(1) => { + let word = parse_hex_string_as_word(value).map_err(|e| { + StorageValueError::ParseError(WordType::Words(1).to_string(), e.to_string()) + })?; + Ok(vec![word]) + }, + _ => todo!("No native types for multi-slot values are yet implemented"), + } } } -#[derive(Debug, Error)] -pub enum StoragePlaceholderError { - #[error("entire key and key segments cannot be empty")] - EmptyKey, - #[error("key `{0}` is invalid (expected string in {{...}} format)")] - FormatError(String), - #[error( - "key `{0}` contains invalid character ({1}) (must be alphanumeric, underscore, or hyphen)" - )] - InvalidChar(String, char), -} - -// SERIALIZATION +// FELT TYPE // ================================================================================================ -impl Serializable for StoragePlaceholder { +/// Describes native types that fit within a single Felt. +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", serde(try_from = "String", into = "String"))] +#[repr(u8)] +pub enum FeltType { + U8, + U16, + U32, + #[default] + Felt, + TokenSymbol, +} + +impl FeltType { + pub fn parse_value(&self, value_str: &str) -> Result { + let felt = match self { + FeltType::U8 => Felt::from(value_str.parse::().map_err(|_| { + StorageValueError::ParseError(value_str.to_string(), self.to_string()) + })?), + FeltType::U16 => Felt::from(value_str.parse::().map_err(|_| { + StorageValueError::ParseError(value_str.to_string(), self.to_string()) + })?), + FeltType::U32 => Felt::from(value_str.parse::().map_err(|_| { + StorageValueError::ParseError(value_str.to_string(), self.to_string()) + })?), + FeltType::Felt => parse_felt_from_str(value_str).map_err(|_| { + StorageValueError::ParseError( + FeltType::TokenSymbol.to_string(), + value_str.to_string(), + ) + })?, + FeltType::TokenSymbol => Felt::from(TokenSymbol::new(value_str).map_err(|_| { + StorageValueError::ParseError(FeltType::TokenSymbol.to_string(), self.to_string()) + })?), + }; + Ok(felt) + } +} + +impl Serializable for FeltType { fn write_into(&self, target: &mut W) { - target.write(&self.key); + target.write_u8(*self as u8); } } -impl Deserializable for StoragePlaceholder { +impl Deserializable for FeltType { fn read_from(source: &mut R) -> Result { - let key: String = source.read()?; - StoragePlaceholder::new(key) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + let tag = source.read_u8()?; + FeltType::try_from(tag).map_err(|_| { + DeserializationError::InvalidValue(format!("unknown tag {} for FeltType", tag)) + }) + } +} + +impl TryFrom for FeltType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(FeltType::U8), + 1 => Ok(FeltType::U16), + 2 => Ok(FeltType::U32), + 3 => Ok(FeltType::Felt), + 4 => Ok(FeltType::TokenSymbol), + _ => Err(()), + } + } +} + +impl fmt::Display for FeltType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FeltType::U8 => write!(f, "{}", FELT_TYPE_U8), + FeltType::U16 => write!(f, "{}", FELT_TYPE_U16), + FeltType::U32 => write!(f, "{}", FELT_TYPE_U32), + FeltType::Felt => write!(f, "{}", FELT_TYPE_FELT), + FeltType::TokenSymbol => write!(f, "{}", FELT_TYPE_TOKEN_SYMBOL), + } } } -// STORAGE VALUE +impl FromStr for FeltType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + x if x == FELT_TYPE_U8 => Ok(FeltType::U8), + x if x == FELT_TYPE_U16 => Ok(FeltType::U16), + x if x == FELT_TYPE_U32 => Ok(FeltType::U32), + x if x == FELT_TYPE_FELT => Ok(FeltType::Felt), + x if x == FELT_TYPE_TOKEN_SYMBOL => Ok(FeltType::TokenSymbol), + _ => Err(String::from("invalid felt type: ") + s), + } + } +} + +impl TryFrom for FeltType { + type Error = String; + + fn try_from(value: String) -> Result { + value.parse::() + } +} + +impl From for String { + fn from(ft: FeltType) -> Self { + ft.to_string() + } +} + +// WORD TYPE // ================================================================================================ -/// Represents a value used within a templating context. -/// -/// A [StorageValue] can be one of: -/// - `Felt(Felt)`: a single [Felt] value -/// - `Word(Word)`: a single [Word] value -/// - `Map(StorageMap)`: a storage map -/// -/// These values are used to resolve dynamic placeholders at component instantiation. -#[derive(Clone, Debug)] -pub enum StorageValue { - Felt(Felt), - Word(Word), - Map(StorageMap), +/// Describes native types that fit within a certain amount of words. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", serde(try_from = "String", into = "String"))] +pub enum WordType { + Words(u8), + RpoFalcon512PublicKey, + FeltType(FeltType), +} + +impl fmt::Display for WordType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WordType::Words(1) => f.write_str(WORD_TYPE), + WordType::Words(n) => write!(f, "[word;{}]", n), + WordType::FeltType(ft) => write!(f, "{}", ft), + WordType::RpoFalcon512PublicKey => f.write_str(FALCON_PUBKEY_TYPE), + } + } } -impl StorageValue { - /// Returns `Some(&Felt)` if the variant is `Felt`, otherwise errors. - pub fn as_felt(&self) -> Result<&Felt, AccountComponentTemplateError> { - if let StorageValue::Felt(felt) = self { - Ok(felt) - } else { - Err(AccountComponentTemplateError::IncorrectStorageValue("Felt".into())) +impl FromStr for WordType { + type Err = String; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + if let (Some(inner), Some(_)) = (s.strip_prefix("[word;"), s.strip_suffix("]")) { + let num_str = inner.trim(); + if num_str.is_empty() { + return Err("missing number for word type".into()); + } + return num_str + .parse::() + .map(WordType::Words) + .map_err(|_| format!("invalid number in word type: {}", s)); + } + + if s == FALCON_PUBKEY_TYPE { + return Ok(WordType::RpoFalcon512PublicKey); } + + if s == WORD_TYPE { + return Ok(WordType::Words(1)); + } + + s.parse::() + .map(WordType::FeltType) + .map_err(|e| format!("failed to parse as FeltType: {}", e)) } +} - /// Returns `Ok(&Word)` if the variant is `Word`, otherwise errors. - pub fn as_word(&self) -> Result<&Word, AccountComponentTemplateError> { - if let StorageValue::Word(word) = self { - Ok(word) - } else { - Err(AccountComponentTemplateError::IncorrectStorageValue("Word".into())) +impl Serializable for WordType { + fn write_into(&self, target: &mut W) { + match self { + WordType::Words(n) => { + target.write_u8(0); + target.write_u8(*n); + }, + WordType::FeltType(ft) => { + target.write_u8(1); + target.write(ft); + }, + WordType::RpoFalcon512PublicKey => { + target.write_u8(2); + }, } } +} - /// Returns `Ok(&StorageMap>` if the variant is `Map`, otherwise errors. - pub fn as_map(&self) -> Result<&StorageMap, AccountComponentTemplateError> { - if let StorageValue::Map(map) = self { - Ok(map) - } else { - Err(AccountComponentTemplateError::IncorrectStorageValue("Map".into())) +impl Deserializable for WordType { + fn read_from(source: &mut R) -> Result { + match source.read_u8()? { + 0 => Ok(WordType::Words(source.read_u8()?)), + 1 => Ok(WordType::FeltType(FeltType::read_from(source)?)), + 2 => Ok(WordType::RpoFalcon512PublicKey), + tag => { + Err(DeserializationError::InvalidValue(format!("unknown tag {} for WordType", tag))) + }, } } } + +impl TryFrom for WordType { + type Error = String; + + fn try_from(value: String) -> Result { + value.parse::() + } +} + +impl From for String { + fn from(value: WordType) -> Self { + value.to_string() + } +} + +/// PLACEHOLDER VALUE +// ================================================================================================ + +#[derive(Debug, Error)] +pub enum StorageValueError { + #[error("failed to convert type into felt: {0}")] + ConversionError(String), + #[error("failed to parse string `{0}` as `{1}`")] + ParseError(String, String), + #[error("parsed value does not fit into expected slot")] + TypeArityMismatch, +} + +// HELPERS +// ================================================================================================ + +pub(crate) fn parse_felt_from_str(s: &str) -> Result { + let n = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { + u64::from_str_radix(hex, 16) + } else { + s.parse::() + } + .map_err(|e| e.to_string())?; + Felt::try_from(n).map_err(|e| e.to_string()) +} diff --git a/crates/miden-objects/src/account/component/template/storage/toml.rs b/crates/miden-objects/src/account/component/template/storage/toml.rs index a8191a0d4..c6029b4f2 100644 --- a/crates/miden-objects/src/account/component/template/storage/toml.rs +++ b/crates/miden-objects/src/account/component/template/storage/toml.rs @@ -4,15 +4,19 @@ use alloc::{ }; use core::fmt; -use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; -use vm_core::Felt; -use vm_processor::Digest; - -use super::{ - FeltRepresentation, MapRepresentation, StorageEntry, StoragePlaceholder, WordRepresentation, +use serde::{ + de::{value::MapAccessDeserializer, Error, SeqAccess, Visitor}, + ser::SerializeStruct, + Deserialize, Deserializer, Serialize, Serializer, }; + +use super::{FeltRepresentation, MapEntry, MapRepresentation, StorageEntry, WordRepresentation}; use crate::{ - account::AccountComponentMetadata, errors::AccountComponentTemplateError, + account::{ + component::template::storage::placeholder::{parse_felt_from_str, FeltType, WordType}, + AccountComponentMetadata, StorageValueName, + }, + errors::AccountComponentTemplateError, utils::parse_hex_string_as_word, }; @@ -31,13 +35,15 @@ impl AccountComponentMetadata { pub fn from_toml(toml_string: &str) -> Result { let component: AccountComponentMetadata = toml::from_str(toml_string) .map_err(AccountComponentTemplateError::DeserializationError)?; + component.validate()?; Ok(component) } /// Serializes the account component template into a TOML string. pub fn as_toml(&self) -> Result { - let toml = toml::to_string(self).unwrap(); + let toml = toml::to_string_pretty(self) + .map_err(AccountComponentTemplateError::SerializationError)?; Ok(toml) } } @@ -45,85 +51,113 @@ impl AccountComponentMetadata { // WORD REPRESENTATION SERIALIZATION // ================================================================================================ -impl serde::Serialize for WordRepresentation { +impl Serialize for WordRepresentation { fn serialize(&self, serializer: S) -> Result where - S: serde::Serializer, + S: Serializer, { - use serde::ser::SerializeSeq; match self { - WordRepresentation::Value(word) => { - // Ensure that the length of the vector is exactly 4 - let word = Digest::from(word); - serializer.serialize_str(&word.to_string()) + WordRepresentation::Template { name, description, r#type } => { + // Serialize as a table with keys: "name", "description", "type". + let mut state = serializer.serialize_struct("WordRepresentation", 3)?; + state.serialize_field("name", name)?; + state.serialize_field("description", description)?; + state.serialize_field("type", r#type)?; + state.end() }, - WordRepresentation::Array(words) => { - let mut seq = serializer.serialize_seq(Some(4))?; - for word in words { - seq.serialize_element(word)?; - } - seq.end() + WordRepresentation::Value { name, description, value } => { + // Serialize as a table with keys: "name", "description", "value". + let mut state = serializer.serialize_struct("WordRepresentation", 3)?; + state.serialize_field("name", name)?; + state.serialize_field("description", description)?; + state.serialize_field("value", value)?; + state.end() }, - WordRepresentation::Template(key) => key.serialize(serializer), } } } -impl<'de> serde::Deserialize<'de> for WordRepresentation { +impl<'de> Deserialize<'de> for WordRepresentation { fn deserialize(deserializer: D) -> Result where - D: serde::Deserializer<'de>, + D: Deserializer<'de>, { - use serde::de::{Error, SeqAccess, Visitor}; struct WordRepresentationVisitor; impl<'de> Visitor<'de> for WordRepresentationVisitor { type Value = WordRepresentation; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a single hex/decimal Word or an array of 4 elements") + formatter.write_str("a string or a map representing a WordRepresentation") } + // A bare stirng is interpreted it as a Value variant. fn visit_str(self, value: &str) -> Result where E: Error, { - // Attempt to deserialize as storage placeholder first - if let Ok(tk) = StoragePlaceholder::try_from(value) { - return Ok(WordRepresentation::Template(tk)); - } - - // try hex parsing otherwise - let word = parse_hex_string_as_word(value).map_err(|_err| { + let parsed_value = parse_hex_string_as_word(value).map_err(|_err| { E::invalid_value( serde::de::Unexpected::Str(value), - &"a valid hexadecimal string or storage placeholder (in '{{key}}' format)", + &"a valid hexadecimal string", ) })?; + Ok(parsed_value.into()) + } - Ok(WordRepresentation::Value(word)) + fn visit_string(self, value: String) -> Result + where + E: Error, + { + self.visit_str(&value) } - fn visit_seq(self, mut seq: A) -> Result + fn visit_seq(self, seq: A) -> Result where A: SeqAccess<'de>, { - let mut elements = Vec::with_capacity(4); - while let Some(felt_repr) = seq.next_element::()? { - elements.push(felt_repr); + // Deserialize as a list of felt representations + let elements: Vec = + Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))?; + if elements.len() != 4 { + return Err(Error::invalid_length( + elements.len(), + &"expected an array of 4 elements", + )); + } + let array: [FeltRepresentation; 4] = + elements.try_into().expect("length was checked"); + Ok(WordRepresentation::new_value(array, None, None)) + } + + fn visit_map(self, map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + #[derive(Deserialize, Debug)] + struct WordRepresentationHelper { + name: Option, + description: Option, + // The "value" field (if present) must be an array of 4 FeltRepresentations. + value: Option<[FeltRepresentation; 4]>, + #[serde(rename = "type")] + r#type: Option, } - if elements.len() == 4 { - let array: [FeltRepresentation; 4] = - elements.clone().try_into().map_err(|_| { - Error::invalid_length( - elements.len(), - &"expected an array of 4 elements", - ) - })?; - Ok(WordRepresentation::Array(array)) + let helper = + WordRepresentationHelper::deserialize(MapAccessDeserializer::new(map))?; + + // If a value field is present, assume a Value variant. + if let Some(val) = helper.value { + let name = helper.name.map(parse_name).transpose()?; + Ok(WordRepresentation::new_value(val, name, helper.description)) } else { - Err(Error::invalid_length(elements.len(), &"expected an array of 4 elements")) + // Otherwise, we expect a Template variant (name is required for identification) + let name = expect_parse_value_name(helper.name, "word template")?; + + // If type not defined, assume Word + let r#type = helper.r#type.unwrap_or(WordType::Words(1)); + Ok(WordRepresentation::new_template(name, helper.description, r#type)) } } } @@ -135,67 +169,89 @@ impl<'de> serde::Deserialize<'de> for WordRepresentation { // FELT REPRESENTATION SERIALIZATION // ================================================================================================ -impl<'de> serde::Deserialize<'de> for FeltRepresentation { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - if let Some(hex_str) = value.strip_prefix("0x").or_else(|| value.strip_prefix("0X")) { - let felt_value = u64::from_str_radix(hex_str, 16).map_err(serde::de::Error::custom)?; - Ok(FeltRepresentation::Hexadecimal(Felt::new(felt_value))) - } else if let Ok(decimal_value) = value.parse::() { - Ok(FeltRepresentation::Decimal( - Felt::try_from(decimal_value).map_err(serde::de::Error::custom)?, - )) - } else if let Ok(key) = StoragePlaceholder::try_from(&value) { - Ok(FeltRepresentation::Template(key)) - } else { - Err(serde::de::Error::custom( - "deserialized string value is not a valid variant of FeltRepresentation", - )) - } - } -} - -impl serde::Serialize for FeltRepresentation { +impl Serialize for FeltRepresentation { fn serialize(&self, serializer: S) -> Result where - S: serde::Serializer, + S: Serializer, { match self { - FeltRepresentation::Hexadecimal(felt) => { - let output = format!("0x{:x}", felt.as_int()); - serializer.serialize_str(&output) + FeltRepresentation::Value { name, description, value } => { + let hex = value.to_string(); + if name.is_none() && description.is_none() { + serializer.serialize_str(&hex) + } else { + let mut state = serializer.serialize_struct("FeltRepresentation", 3)?; + state.serialize_field("name", name)?; + state.serialize_field("description", description)?; + state.serialize_field("value", &hex)?; + state.end() + } }, - FeltRepresentation::Decimal(felt) => { - let output = felt.as_int().to_string(); - serializer.serialize_str(&output) + FeltRepresentation::Template { name, description, r#type } => { + let mut state = serializer.serialize_struct("FeltRepresentation", 3)?; + state.serialize_field("name", name)?; + state.serialize_field("description", description)?; + state.serialize_field("type", r#type)?; + state.end() }, - FeltRepresentation::Template(key) => key.serialize(serializer), } } } -// KEY SERIALIZATION -// ================================================================================================ - -impl serde::Serialize for StoragePlaceholder { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> serde::Deserialize<'de> for StoragePlaceholder { +impl<'de> Deserialize<'de> for FeltRepresentation { fn deserialize(deserializer: D) -> Result where - D: serde::Deserializer<'de>, + D: Deserializer<'de>, { - let s = String::deserialize(deserializer)?; - StoragePlaceholder::try_from(s.as_str()).map_err(serde::de::Error::custom) + // Felts can be deserialized as either: + // + // - Scalars (parsed from strings) + // - A table object that can or cannot harcode a value. If not present, this is a + // placeholder type + #[derive(Deserialize)] + #[serde(untagged)] + enum Intermediate { + Map { + name: Option, + description: Option, + #[serde(default)] + value: Option, + #[serde(rename = "type")] + r#type: Option, + }, + Scalar(String), + } + + let intermediate = Intermediate::deserialize(deserializer)?; + match intermediate { + Intermediate::Scalar(s) => { + let felt = parse_felt_from_str(&s) + .map_err(|e| D::Error::custom(format!("failed to parse Felt: {}", e)))?; + Ok(FeltRepresentation::Value { + name: None, + description: None, + value: felt, + }) + }, + Intermediate::Map { name, description, value, r#type } => { + // Get the defined type, or fall back to FeltType::Felt as default + let felt_type = r#type.unwrap_or_default(); + + if let Some(val_str) = value { + // Parse into felt from the input string + let felt = felt_type + .parse_value(&val_str) + .map_err(|e| D::Error::custom(format!("failed to parse Felt: {}", e)))?; + let name = name.map(parse_name).transpose()?; + Ok(FeltRepresentation::new_value(felt, name, description)) + } else { + // No value provided, so this is a placeholder + let name = expect_parse_value_name(name, "map template")?; + + Ok(FeltRepresentation::new_template(felt_type, name, description)) + } + }, + } } } @@ -203,41 +259,24 @@ impl<'de> serde::Deserialize<'de> for StoragePlaceholder { // ================================================================================================ /// Represents the type of values that can be found in a storage slot's `values` field. -#[derive(serde::Deserialize, serde::Serialize)] +#[derive(Debug, Deserialize)] #[serde(untagged)] enum StorageValues { /// List of individual words (for multi-slot entries). - Words(Vec), + Words(Vec<[FeltRepresentation; 4]>), /// List of key-value entries (for map storage slots). - MapEntries(MapRepresentation), + MapEntries(Vec), } -impl StorageValues { - pub fn is_list_of_words(&self) -> bool { - match self { - StorageValues::Words(_) => true, - StorageValues::MapEntries(_) => false, - } - } - - pub fn into_words(self) -> Option> { - match self { - StorageValues::Words(vec) => Some(vec), - StorageValues::MapEntries(_) => None, - } - } - - pub fn into_map_entries(self) -> Option { - match self { - StorageValues::Words(_) => None, - StorageValues::MapEntries(map) => Some(map), - } - } - - pub fn len(&self) -> Option { +// NOTE: We serialize manually here for forcing inline collection +impl Serialize for StorageValues { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { match self { - StorageValues::Words(vec) => Some(vec.len()), - StorageValues::MapEntries(map) => map.len(), + StorageValues::Words(arrays) => serializer.collect_seq(arrays), + StorageValues::MapEntries(entries) => serializer.collect_seq(entries), } } } @@ -245,36 +284,46 @@ impl StorageValues { // STORAGE ENTRY SERIALIZATION // ================================================================================================ -/// Used as a helper for validating and (de)serializing storage entries -#[derive(Default, Deserialize, Serialize)] +#[derive(Default, Debug, Deserialize, Serialize)] struct RawStorageEntry { - name: String, + name: Option, description: Option, slot: Option, slots: Option>, - value: Option, + #[serde(rename = "type")] + word_type: Option, + value: Option<[FeltRepresentation; 4]>, values: Option, } impl From for RawStorageEntry { fn from(entry: StorageEntry) -> Self { match entry { - StorageEntry::Value { name, description, slot, value } => RawStorageEntry { - name, - description, - slot: Some(slot), - value: Some(value), - ..Default::default() + StorageEntry::Value { slot, word_entry } => match word_entry { + WordRepresentation::Value { name, description, value } => RawStorageEntry { + slot: Some(slot), + name: name.as_ref().map(StorageValueName::to_string), + description: description.map(String::from), + value: Some(value), + ..Default::default() + }, + WordRepresentation::Template { name, description, r#type } => RawStorageEntry { + slot: Some(slot), + name: Some(name.to_string()), + description: description.map(String::from), + word_type: Some(r#type), + ..Default::default() + }, }, - StorageEntry::Map { name, description, slot, map: values } => RawStorageEntry { - name, - description, + StorageEntry::Map { slot, map } => RawStorageEntry { + name: Some(map.name().to_string()), + description: map.description().cloned(), slot: Some(slot), - values: Some(StorageValues::MapEntries(values)), + values: Some(StorageValues::MapEntries(map.entries().to_vec())), ..Default::default() }, StorageEntry::MultiSlot { name, description, slots, values } => RawStorageEntry { - name, + name: Some(name.to_string()), description, slots: Some(slots), values: Some(StorageValues::Words(values)), @@ -301,90 +350,64 @@ impl<'de> Deserialize<'de> for StorageEntry { { let raw = RawStorageEntry::deserialize(deserializer)?; - // Determine presence of fields and do early validation - let slot_present = raw.slot.is_some(); - let value_present = raw.value.is_some(); - - // Use a match on the combination of presence flags to choose variant - match (raw.slots, raw.values) { - (None, None) => { - // Expect a Value variant: "slot" and "value" must be present - // "slots" and "values" must not be present. - Ok(StorageEntry::Value { - name: raw.name, - description: raw.description, - slot: raw - .slot - .ok_or(D::Error::custom("missing 'slot' field for single-slot entry"))?, - value: raw - .value - .ok_or(D::Error::custom("missing 'value' field for single-slot entry"))?, - }) - }, - (Some(_), None) => { - Err(D::Error::custom("`slots` is defined but no `values` field was found")) - }, - (None, Some(values)) => { - // Expect a Map variant: - // - `slot` must be present - // - `values` must be present and convertible to map entries - // - `slots` must not be present - // - `value` must not be present - if value_present { - return Err(D::Error::custom( - "fields 'value' and 'values' are mutually exclusive", - )); - } + if let Some(word_entry) = raw.value { + // If a value was provided, this is a WordRepresentation::Value entry + let slot = raw.slot.ok_or_else(|| missing_field_for("slot", "value entry"))?; + let name = raw.name.map(parse_name).transpose()?; + + Ok(StorageEntry::Value { + slot, + word_entry: WordRepresentation::new_value(word_entry, name, raw.description), + }) + } else if let Some(word_type) = raw.word_type { + // If a type was provided instead, this is a WordRepresentation::Value entry + let slot = raw.slot.ok_or_else(|| missing_field_for("slot", "single-slot entry"))?; + let name = expect_parse_value_name(raw.name, "single-slot entry")?; + let word_entry = WordRepresentation::new_template(name, raw.description, word_type); + + Ok(StorageEntry::Value { slot, word_entry }) + } else if let Some(StorageValues::MapEntries(map_entries)) = raw.values { + // If `values` field contains key/value pairs, deserialize as map + let name = expect_parse_value_name(raw.name, "map entry")?; + let slot = raw.slot.ok_or_else(|| missing_field_for("slot", "map entry"))?; + let map = MapRepresentation::new(map_entries, name, raw.description); + + Ok(StorageEntry::Map { slot, map }) + } else if let Some(StorageValues::Words(values)) = raw.values { + let name = expect_parse_value_name(raw.name, "multislot entry")?; + let slots = raw.slots.ok_or_else(|| missing_field_for("slots", "multislot entry"))?; + + if slots.len() != values.len() { + return Err(D::Error::custom(format!( + "number of slots ({}) does not match number of values ({}) for multislot entry", + slots.len(), + values.len() + ))); + } + Ok(StorageEntry::new_multislot(name, raw.description, slots, values)) + } else { + Err(D::Error::custom("invalid combination of fields for storage entry")) + } + } +} - let map_entries = values - .into_map_entries() - .ok_or_else(|| D::Error::custom("invalid 'values' for map entry"))?; +// UTILS / HELPERS +// ================================================================================================ - Ok(StorageEntry::Map { - name: raw.name, - description: raw.description, - slot: raw.slot.ok_or(D::Error::missing_field("slot"))?, - map: map_entries, - }) - }, - (Some(slots), Some(values)) => { - // Expect a MultiSlot variant: - // - `slots` must be present - // - `values` must be present and represent words - // - `slot` must not be present - // - `value` must not be present - if slot_present { - return Err(D::Error::custom( - "fields 'slot' and 'slots' are mutually exclusive", - )); - } - if value_present { - return Err(D::Error::custom( - "fields 'value' and 'values' are mutually exclusive", - )); - } +fn missing_field_for(field: &str, context: &str) -> E { + E::custom(format!("missing '{}' field for {}", field, context)) +} - let has_list_of_values = values.is_list_of_words(); - if has_list_of_values { - let slots_count = slots.len(); - let values_count = values.len().expect("checked that it's a list of values"); - if slots_count != values_count { - return Err(D::Error::custom(format!( - "number of slots ({}) does not match number of values ({}) for multi-slot storage entry", - slots_count, values_count - ))); - } - } +/// Checks than an optional (but expected) name field has been defined and is correct. +fn expect_parse_value_name( + n: Option, + context: &str, +) -> Result { + let name = n.ok_or_else(|| missing_field_for("name", context))?; + parse_name(name) +} - Ok(StorageEntry::MultiSlot { - name: raw.name, - description: raw.description, - slots, - values: values - .into_words() - .ok_or_else(|| D::Error::custom("invalid values for multi-slot"))?, - }) - }, - } - } +/// Tries to parse a string into a [StorageValueName]. +fn parse_name(n: String) -> Result { + StorageValueName::new(n).map_err(|err| E::custom(format!("invalid `name`: {err}"))) } diff --git a/crates/miden-objects/src/account/mod.rs b/crates/miden-objects/src/account/mod.rs index 2893733c5..6928fdfa4 100644 --- a/crates/miden-objects/src/account/mod.rs +++ b/crates/miden-objects/src/account/mod.rs @@ -23,8 +23,8 @@ pub use code::{procedure::AccountProcedureInfo, AccountCode}; mod component; pub use component::{ AccountComponent, AccountComponentMetadata, AccountComponentTemplate, FeltRepresentation, - InitStorageData, MapRepresentation, PlaceholderType, StorageEntry, StoragePlaceholder, - StorageValue, WordRepresentation, + FeltType, InitStorageData, MapEntry, StorageEntry, StorageValueError, StorageValueName, + StorageValueNameError, WordRepresentation, WordType, }; pub mod delta; diff --git a/crates/miden-objects/src/errors.rs b/crates/miden-objects/src/errors.rs index a47752c69..0e4a5f51a 100644 --- a/crates/miden-objects/src/errors.rs +++ b/crates/miden-objects/src/errors.rs @@ -17,8 +17,8 @@ use super::{ }; use crate::{ account::{ - AccountCode, AccountIdPrefix, AccountStorage, AccountType, PlaceholderType, - StoragePlaceholder, + AccountCode, AccountIdPrefix, AccountStorage, AccountType, StorageValueError, + StorageValueName, StorageValueNameError, }, block::BlockNumber, note::{NoteAssets, NoteExecutionHint, NoteTag, NoteType, Nullifier}, @@ -37,24 +37,25 @@ pub enum AccountComponentTemplateError { DeserializationError(#[source] toml::de::Error), #[error("slot {0} is defined multiple times")] DuplicateSlot(u8), - #[error("storage value was not of the expected type {0}")] - IncorrectStorageValue(String), + #[error("storage value name is incorrect: {0}")] + IncorrectStorageValueName(#[source] StorageValueNameError), #[error("multi-slot entry should contain as many values as storage slots indices")] MultiSlotArityMismatch, #[error("error deserializing component metadata: {0}")] MetadataDeserializationError(String), #[error("component storage slots are not contiguous ({0} is followed by {1})")] NonContiguousSlots(u8, u8), - #[error("storage value for placeholder `{0}` was not provided in the map")] - PlaceholderValueNotProvided(StoragePlaceholder), + #[error("storage value for placeholder `{0}` was not provided in the init storage data")] + PlaceholderValueNotProvided(StorageValueName), + #[cfg(feature = "std")] + #[error("error trying to deserialize from toml")] + SerializationError(#[source] toml::ser::Error), + #[error("error converting value into expected type: ")] + StorageValueParsingError(#[source] StorageValueError), #[error("storage map contains duplicate key `{0}`")] - StorageMapHasDuplicateKeys(String), + StorageMapHasDuplicateKeys(Digest), #[error("component storage slots have to start at 0, but they start at {0}")] StorageSlotsDoNotStartAtZero(u8), - #[error( - "storage placeholder `{0}` appears more than once representing different types `{0}` and `{1}`" - )] - StoragePlaceholderTypeMismatch(StoragePlaceholder, PlaceholderType, PlaceholderType), } // ACCOUNT ERROR From 8bfc6b04ff27a576133019ca6c2a18964cdc8d97 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Tue, 11 Feb 2025 16:19:55 -0300 Subject: [PATCH 2/5] reviews: Propagate error correctly, change test, refactor code --- .../src/account/component/mod.rs | 4 +- .../src/account/component/template/mod.rs | 21 ++++---- .../template/storage/entry_content.rs | 10 ++-- .../account/component/template/storage/mod.rs | 36 +++++++------ .../component/template/storage/placeholder.rs | 54 +++++++++---------- crates/miden-objects/src/account/mod.rs | 4 +- crates/miden-objects/src/errors.rs | 6 +-- 7 files changed, 72 insertions(+), 63 deletions(-) diff --git a/crates/miden-objects/src/account/component/mod.rs b/crates/miden-objects/src/account/component/mod.rs index 4a6318557..bff824f30 100644 --- a/crates/miden-objects/src/account/component/mod.rs +++ b/crates/miden-objects/src/account/component/mod.rs @@ -6,8 +6,8 @@ use vm_processor::MastForest; mod template; pub use template::{ AccountComponentMetadata, AccountComponentTemplate, FeltRepresentation, FeltType, - InitStorageData, MapEntry, StorageEntry, StorageValueError, StorageValueName, - StorageValueNameError, WordRepresentation, WordType, + InitStorageData, MapEntry, StorageEntry, StorageValueName, StorageValueNameError, + TemplateTypeError, WordRepresentation, WordType, }; use crate::{ diff --git a/crates/miden-objects/src/account/component/template/mod.rs b/crates/miden-objects/src/account/component/template/mod.rs index eb52e9f1f..da9c78c46 100644 --- a/crates/miden-objects/src/account/component/template/mod.rs +++ b/crates/miden-objects/src/account/component/template/mod.rs @@ -138,7 +138,7 @@ impl Deserializable for AccountComponentTemplate { /// "description of the component".into(), /// Version::parse("0.1.0")?, /// BTreeSet::new(), -/// vec![], +/// vec![storage_entry], /// )?; /// /// let library = Assembler::default().assemble_library([CODE]).unwrap(); @@ -150,6 +150,7 @@ impl Deserializable for AccountComponentTemplate { /// ``` #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "std", serde(rename_all = "kebab-case"))] pub struct AccountComponentMetadata { /// The human-readable name of the component. name: String, @@ -162,7 +163,7 @@ pub struct AccountComponentMetadata { version: Version, /// A set of supported target account types for this component. - targets: BTreeSet, + supported_types: BTreeSet, /// A list of storage entries defining the component's storage layout and initialization /// values. @@ -188,7 +189,7 @@ impl AccountComponentMetadata { name, description, version, - targets, + supported_types: targets, storage, }; component.validate()?; @@ -232,7 +233,7 @@ impl AccountComponentMetadata { /// Returns the account types supported by the component. pub fn targets(&self) -> &BTreeSet { - &self.targets + &self.supported_types } /// Returns the list of storage entries of the component. @@ -291,7 +292,7 @@ impl Serializable for AccountComponentMetadata { self.name.write_into(target); self.description.write_into(target); self.version.to_string().write_into(target); - self.targets.write_into(target); + self.supported_types.write_into(target); self.storage.write_into(target); } } @@ -304,7 +305,7 @@ impl Deserializable for AccountComponentMetadata { version: semver::Version::from_str(&String::read_from(source)?).map_err( |err: semver::Error| DeserializationError::InvalidValue(err.to_string()), )?, - targets: BTreeSet::::read_from(source)?, + supported_types: BTreeSet::::read_from(source)?, storage: Vec::::read_from(source)?, }) } @@ -367,7 +368,7 @@ mod tests { name: "test".into(), description: "desc".into(), version: Version::parse("0.1.0").unwrap(), - targets: BTreeSet::new(), + supported_types: BTreeSet::new(), storage, }; @@ -409,7 +410,7 @@ mod tests { name: "test".into(), description: "desc".into(), version: Version::parse("0.1.0").unwrap(), - targets: BTreeSet::new(), + supported_types: BTreeSet::new(), storage, }; @@ -429,7 +430,7 @@ mod tests { name = "Test Component" description = "This is a test component" version = "1.0.1" - targets = ["FungibleFaucet"] + supported-types = ["FungibleFaucet"] [[storage]] name = "map" @@ -451,7 +452,7 @@ mod tests { name = "Test Component" description = "This is a test component" version = "1.0.1" - targets = ["FungibleFaucet"] + supported-types = ["FungibleFaucet"] [[storage]] name = "map" diff --git a/crates/miden-objects/src/account/component/template/storage/entry_content.rs b/crates/miden-objects/src/account/component/template/storage/entry_content.rs index ac1f84141..324be12a5 100644 --- a/crates/miden-objects/src/account/component/template/storage/entry_content.rs +++ b/crates/miden-objects/src/account/component/template/storage/entry_content.rs @@ -72,8 +72,7 @@ impl WordRepresentation { /// Returns the name associated with the word representation. /// - For the `Template` variant, it always returns a reference to the name. - /// - For the `Value` variant, it returns `Some(&str)` if a name is present, or `None` - /// otherwise. + /// - For the `Value` variant, it returns `Some` if a name is present, or `None` otherwise. pub fn name(&self) -> Option<&StorageValueName> { match self { WordRepresentation::Template { name, .. } => Some(name), @@ -98,7 +97,7 @@ impl WordRepresentation { } } - /// Returns the default value (an array of 4 `FeltRepresentation`s) if this is a `Value` + /// Returns the value (an array of 4 `FeltRepresentation`s) if this is a `Value` /// variant; otherwise, returns `None`. pub fn value(&self) -> Option<&[FeltRepresentation; 4]> { match self { @@ -152,7 +151,10 @@ impl WordRepresentation { let placeholder_prefix = placeholder_prefix.with_suffix(placeholder_key); let value = init_storage_data.get(&placeholder_prefix); if let Some(v) = value { - let parsed_value = r#type.try_parse_word(v).unwrap(); + let parsed_value = r#type + .try_parse_word(v) + .map_err(AccountComponentTemplateError::StorageValueParsingError)?; + Ok(parsed_value) } else { Err(AccountComponentTemplateError::PlaceholderValueNotProvided( diff --git a/crates/miden-objects/src/account/component/template/storage/mod.rs b/crates/miden-objects/src/account/component/template/storage/mod.rs index a76df0759..3d828bcfe 100644 --- a/crates/miden-objects/src/account/component/template/storage/mod.rs +++ b/crates/miden-objects/src/account/component/template/storage/mod.rs @@ -14,8 +14,8 @@ use crate::account::StorageSlot; mod placeholder; pub use placeholder::{ - FeltType, PlaceholderTypeRequirement, StorageValueError, StorageValueName, - StorageValueNameError, WordType, + FeltType, PlaceholderTypeRequirement, StorageValueName, StorageValueNameError, + TemplateTypeError, WordType, }; mod init_storage_data; @@ -333,7 +333,7 @@ mod tests { StorageValueName, }, AccountComponent, AccountComponentTemplate, AccountType, FeltRepresentation, - StorageEntry, StorageSlot, StorageValueError, WordRepresentation, + StorageEntry, StorageSlot, TemplateTypeError, WordRepresentation, }, digest, errors::AccountComponentTemplateError, @@ -435,7 +435,7 @@ mod tests { name: "Test Component".into(), description: "This is a test component".into(), version: Version::parse("1.0.0").unwrap(), - targets: std::collections::BTreeSet::from([AccountType::FungibleFaucet]), + supported_types: std::collections::BTreeSet::from([AccountType::FungibleFaucet]), storage, }; let toml = config.as_toml().unwrap(); @@ -450,7 +450,7 @@ mod tests { name = "Test Component" description = "This is a test component" version = "1.0.1" - targets = ["FungibleFaucet", "RegularAccountImmutableCode"] + supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"] [[storage]] name = "map_entry" @@ -479,15 +479,21 @@ mod tests { "#; let component_metadata = AccountComponentMetadata::from_toml(toml_text).unwrap(); - for (key, requirement) in component_metadata.get_template_requirements().iter() { - match key.as_str() { - "token_metadata.max_supply" - | "token_metadata.decimals" - | "default_recallable_height" => continue, - "map_entry.map_key_template" => assert!(requirement.r#type.to_string() == "word"), - _ => panic!("all cases should have been covered"), - } - } + let requirements = component_metadata.get_template_requirements(); + + assert_eq!(requirements.len(), 4); + + let supply = requirements.get("token_metadata.max_supply").unwrap(); + assert_eq!(supply.r#type.to_string(), "felt"); + + let decimals = requirements.get("token_metadata.decimals").unwrap(); + assert_eq!(decimals.r#type.to_string(), "u8"); + + let default_recallable_height = requirements.get("default_recallable_height").unwrap(); + assert_eq!(default_recallable_height.r#type.to_string(), "u32"); + + let map_key_template = requirements.get("map_entry.map_key_template").unwrap(); + assert_eq!(map_key_template.r#type.to_string(), "word"); let library = Assembler::default().assemble_library([CODE]).unwrap(); let template = AccountComponentTemplate::new(component_metadata, library); @@ -517,7 +523,7 @@ mod tests { component, Err(AccountError::AccountComponentTemplateInstantiationError( AccountComponentTemplateError::StorageValueParsingError( - StorageValueError::ParseError(_, _) + TemplateTypeError::ParseError(_, _) ) )) ); diff --git a/crates/miden-objects/src/account/component/template/storage/placeholder.rs b/crates/miden-objects/src/account/component/template/storage/placeholder.rs index 5cd5f9aee..fa25df6d4 100644 --- a/crates/miden-objects/src/account/component/template/storage/placeholder.rs +++ b/crates/miden-objects/src/account/component/template/storage/placeholder.rs @@ -77,17 +77,17 @@ impl StorageValueName { #[must_use] pub fn with_suffix(self, suffix: &StorageValueName) -> StorageValueName { let mut key = self; - if !suffix.inner().is_empty() { - if !key.inner().is_empty() { + if !suffix.as_str().is_empty() { + if !key.as_str().is_empty() { key.fully_qualified_name.push('.'); } - key.fully_qualified_name.push_str(&suffix.to_string()); + key.fully_qualified_name.push_str(suffix.as_str()); } key } - pub fn inner(&self) -> &str { + pub fn as_str(&self) -> &str { &self.fully_qualified_name } @@ -95,22 +95,22 @@ impl StorageValueName { if segment.is_empty() { return Err(StorageValueNameError::EmptySegment); } - if !segment.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') { + if let Some(offending_char) = + segment.chars().find(|&c| !(c.is_ascii_alphanumeric() || c == '_' || c == '-')) + { return Err(StorageValueNameError::InvalidCharacter { part: segment.to_string(), - character: segment - .chars() - .find(|c| !(c.is_ascii_alphanumeric() || *c == '_' || *c == '-')) - .unwrap(), + character: offending_char, }); } + Ok(()) } } impl Display for StorageValueName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.inner()) + f.write_str(self.as_str()) } } @@ -163,7 +163,7 @@ pub trait TemplateType: alloc::fmt::Debug + ToString { /// # Errors /// /// Returns a [`StorageValueError`] if the string cannot be parsed into a [`Felt`]. - fn try_parse_felt(&self, value: &str) -> Result; + fn try_parse_felt(&self, value: &str) -> Result; /// Attempts to parse the given string into a vector of [`Word`]s. /// @@ -171,7 +171,7 @@ pub trait TemplateType: alloc::fmt::Debug + ToString { /// /// Returns a [`StorageValueError`] if the string cannot be parsed into the expected vector /// of [`Word`]s. - fn try_parse_words(&self, value: &str) -> Result, StorageValueError>; + fn try_parse_words(&self, value: &str) -> Result, TemplateTypeError>; /// Attempts to parse the given string into a single [`Word`]. /// @@ -182,35 +182,35 @@ pub trait TemplateType: alloc::fmt::Debug + ToString { /// /// Returns a [`StorageValueError::TypeArityMismatch`] if the parsed result does not have /// exactly one element. - fn try_parse_word(&self, value: &str) -> Result { + fn try_parse_word(&self, value: &str) -> Result { let mut words = self.try_parse_words(value)?; if words.len() != 1 { - return Err(StorageValueError::TypeArityMismatch); + return Err(TemplateTypeError::TypeArityMismatch); } Ok(words.pop().expect("checked that there's one value")) } } impl TemplateType for FeltType { - fn try_parse_felt(&self, value: &str) -> Result { + fn try_parse_felt(&self, value: &str) -> Result { self.parse_value(value) } - fn try_parse_words(&self, value: &str) -> Result, StorageValueError> { + fn try_parse_words(&self, value: &str) -> Result, TemplateTypeError> { let felt = self.parse_value(value)?; Ok(vec![[Felt::ZERO, Felt::ZERO, Felt::ZERO, felt]]) } } impl TemplateType for WordType { - fn try_parse_felt(&self, value: &str) -> Result { + fn try_parse_felt(&self, value: &str) -> Result { match self { WordType::FeltType(ft) => ft.try_parse_felt(value), - _ => Err(StorageValueError::TypeArityMismatch), + _ => Err(TemplateTypeError::TypeArityMismatch), } } - fn try_parse_words(&self, value: &str) -> Result, StorageValueError> { + fn try_parse_words(&self, value: &str) -> Result, TemplateTypeError> { match self { WordType::FeltType(ft) => { let felt = ft.parse_value(value)?; @@ -218,7 +218,7 @@ impl TemplateType for WordType { }, WordType::Words(1) => { let word = parse_hex_string_as_word(value).map_err(|e| { - StorageValueError::ParseError(WordType::Words(1).to_string(), e.to_string()) + TemplateTypeError::ParseError(WordType::Words(1).to_string(), e.to_string()) })?; Ok(vec![word]) }, @@ -245,25 +245,25 @@ pub enum FeltType { } impl FeltType { - pub fn parse_value(&self, value_str: &str) -> Result { + pub fn parse_value(&self, value_str: &str) -> Result { let felt = match self { FeltType::U8 => Felt::from(value_str.parse::().map_err(|_| { - StorageValueError::ParseError(value_str.to_string(), self.to_string()) + TemplateTypeError::ParseError(value_str.to_string(), self.to_string()) })?), FeltType::U16 => Felt::from(value_str.parse::().map_err(|_| { - StorageValueError::ParseError(value_str.to_string(), self.to_string()) + TemplateTypeError::ParseError(value_str.to_string(), self.to_string()) })?), FeltType::U32 => Felt::from(value_str.parse::().map_err(|_| { - StorageValueError::ParseError(value_str.to_string(), self.to_string()) + TemplateTypeError::ParseError(value_str.to_string(), self.to_string()) })?), FeltType::Felt => parse_felt_from_str(value_str).map_err(|_| { - StorageValueError::ParseError( + TemplateTypeError::ParseError( FeltType::TokenSymbol.to_string(), value_str.to_string(), ) })?, FeltType::TokenSymbol => Felt::from(TokenSymbol::new(value_str).map_err(|_| { - StorageValueError::ParseError(FeltType::TokenSymbol.to_string(), self.to_string()) + TemplateTypeError::ParseError(FeltType::TokenSymbol.to_string(), self.to_string()) })?), }; Ok(felt) @@ -444,7 +444,7 @@ impl From for String { // ================================================================================================ #[derive(Debug, Error)] -pub enum StorageValueError { +pub enum TemplateTypeError { #[error("failed to convert type into felt: {0}")] ConversionError(String), #[error("failed to parse string `{0}` as `{1}`")] diff --git a/crates/miden-objects/src/account/mod.rs b/crates/miden-objects/src/account/mod.rs index 6928fdfa4..57aa9d512 100644 --- a/crates/miden-objects/src/account/mod.rs +++ b/crates/miden-objects/src/account/mod.rs @@ -23,8 +23,8 @@ pub use code::{procedure::AccountProcedureInfo, AccountCode}; mod component; pub use component::{ AccountComponent, AccountComponentMetadata, AccountComponentTemplate, FeltRepresentation, - FeltType, InitStorageData, MapEntry, StorageEntry, StorageValueError, StorageValueName, - StorageValueNameError, WordRepresentation, WordType, + FeltType, InitStorageData, MapEntry, StorageEntry, StorageValueName, StorageValueNameError, + TemplateTypeError, WordRepresentation, WordType, }; pub mod delta; diff --git a/crates/miden-objects/src/errors.rs b/crates/miden-objects/src/errors.rs index 0e4a5f51a..70daf5376 100644 --- a/crates/miden-objects/src/errors.rs +++ b/crates/miden-objects/src/errors.rs @@ -17,8 +17,8 @@ use super::{ }; use crate::{ account::{ - AccountCode, AccountIdPrefix, AccountStorage, AccountType, StorageValueError, - StorageValueName, StorageValueNameError, + AccountCode, AccountIdPrefix, AccountStorage, AccountType, StorageValueName, + StorageValueNameError, TemplateTypeError, }, block::BlockNumber, note::{NoteAssets, NoteExecutionHint, NoteTag, NoteType, Nullifier}, @@ -51,7 +51,7 @@ pub enum AccountComponentTemplateError { #[error("error trying to deserialize from toml")] SerializationError(#[source] toml::ser::Error), #[error("error converting value into expected type: ")] - StorageValueParsingError(#[source] StorageValueError), + StorageValueParsingError(#[source] TemplateTypeError), #[error("storage map contains duplicate key `{0}`")] StorageMapHasDuplicateKeys(Digest), #[error("component storage slots have to start at 0, but they start at {0}")] From ed3a3c66cf70d30286a46bce76a17100fc539f4e Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Tue, 11 Feb 2025 16:36:00 -0300 Subject: [PATCH 3/5] refactor: Fix docs, imports --- .../src/account/component/mod.rs | 11 +++-------- .../src/account/component/template/mod.rs | 19 +++++++++---------- .../template/storage/init_storage_data.rs | 2 +- .../account/component/template/storage/mod.rs | 2 +- crates/miden-objects/src/account/mod.rs | 4 ++-- 5 files changed, 16 insertions(+), 22 deletions(-) diff --git a/crates/miden-objects/src/account/component/mod.rs b/crates/miden-objects/src/account/component/mod.rs index bff824f30..e0afc5b1e 100644 --- a/crates/miden-objects/src/account/component/mod.rs +++ b/crates/miden-objects/src/account/component/mod.rs @@ -4,11 +4,7 @@ use assembly::{Assembler, Compile, Library}; use vm_processor::MastForest; mod template; -pub use template::{ - AccountComponentMetadata, AccountComponentTemplate, FeltRepresentation, FeltType, - InitStorageData, MapEntry, StorageEntry, StorageValueName, StorageValueNameError, - TemplateTypeError, WordRepresentation, WordType, -}; +pub use template::*; use crate::{ account::{AccountType, StorageSlot}, @@ -92,9 +88,8 @@ impl AccountComponent { /// Instantiates an [AccountComponent] from the [AccountComponentTemplate]. /// - /// The template's component metadata might contain templated values, which can be input by - /// mapping [storage placeholders](StoragePlaceholder) to [values](StorageValue) through the - /// `init_storage_data` parameter. + /// The template's component metadata might contain placeholders, which can be replaced by + /// mapping storage placeholders to values through the `init_storage_data` parameter. /// /// # Errors /// diff --git a/crates/miden-objects/src/account/component/template/mod.rs b/crates/miden-objects/src/account/component/template/mod.rs index da9c78c46..6350bf298 100644 --- a/crates/miden-objects/src/account/component/template/mod.rs +++ b/crates/miden-objects/src/account/component/template/mod.rs @@ -86,8 +86,8 @@ impl Deserializable for AccountComponentTemplate { /// Represents the full component template configuration. /// /// An account component metadata describes the component alongside its storage layout. -/// On the storage layout, [placeholders](StoragePlaceholder) can be utilized to identify -/// [values](StorageValue) that should be provided at the moment of instantiation. +/// On the storage layout, placeholders can be utilized to identify values that should be provided +/// at the moment of instantiation. /// /// When the `std` feature is enabled, this struct allows for serialization and deserialization to /// and from a TOML file. @@ -97,11 +97,10 @@ impl Deserializable for AccountComponentTemplate { /// - The metadata's storage layout does not contain duplicate slots, and it always starts at slot /// index 0. /// - Storage slots are laid out in a contiguous manner. -/// - Storage placeholders can appear multiple times, but only if the expected [StorageValue] is of -/// the same type in all instances. The expected placeholders can be retrieved with -/// [AccountComponentMetadata::get_unique_storage_placeholders()], which returns a map from -/// [StoragePlaceholder] to [PlaceholderType] (which, in turn, indicates the expected value type -/// for the placeholder). +/// - Each placeholder represents a single value. The expected placeholders can be retrieved with +/// [AccountComponentMetadata::get_placeholder_requirements()], which returns a map from keys to +/// [PlaceholderTypeRequirement] (which, in turn, indicates the expected value type for the +/// placeholder). /// /// # Example /// @@ -196,8 +195,8 @@ impl AccountComponentMetadata { Ok(component) } - /// Retrieves a map of unique storage placeholders mapped to their expected type that require - /// a value at the moment of component instantiation. + /// Retrieves a map of unique storage placeholder names mapped to their expected type that + /// require a value at the moment of component instantiation. /// /// These values will be used for initializing storage slot values, or storage map entries. /// For a full example on how a placeholder may be utilized, please refer to the docs for @@ -205,7 +204,7 @@ impl AccountComponentMetadata { /// /// Types for the returned storage placeholders are inferred based on their location in the /// storage layout structure. - pub fn get_template_requirements(&self) -> BTreeMap { + pub fn get_placeholder_requirements(&self) -> BTreeMap { let mut templates = BTreeMap::new(); for entry in self.storage_entries() { for (name, requirement) in entry.template_requirements() { diff --git a/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs b/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs index 394c8e9a7..d90203b4d 100644 --- a/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs +++ b/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs @@ -28,7 +28,7 @@ impl InitStorageData { &self.storage_placeholders } - /// Returns a reference to the [StorageValue] corresponding to the placeholder, or + /// Returns a reference to the name corresponding to the placeholder, or /// [`Option::None`] if the placeholder is not present. pub fn get(&self, key: &StorageValueName) -> Option<&String> { self.storage_placeholders.get(key) diff --git a/crates/miden-objects/src/account/component/template/storage/mod.rs b/crates/miden-objects/src/account/component/template/storage/mod.rs index 3d828bcfe..fb62905ed 100644 --- a/crates/miden-objects/src/account/component/template/storage/mod.rs +++ b/crates/miden-objects/src/account/component/template/storage/mod.rs @@ -479,7 +479,7 @@ mod tests { "#; let component_metadata = AccountComponentMetadata::from_toml(toml_text).unwrap(); - let requirements = component_metadata.get_template_requirements(); + let requirements = component_metadata.get_placeholder_requirements(); assert_eq!(requirements.len(), 4); diff --git a/crates/miden-objects/src/account/mod.rs b/crates/miden-objects/src/account/mod.rs index 57aa9d512..31d5eb190 100644 --- a/crates/miden-objects/src/account/mod.rs +++ b/crates/miden-objects/src/account/mod.rs @@ -23,8 +23,8 @@ pub use code::{procedure::AccountProcedureInfo, AccountCode}; mod component; pub use component::{ AccountComponent, AccountComponentMetadata, AccountComponentTemplate, FeltRepresentation, - FeltType, InitStorageData, MapEntry, StorageEntry, StorageValueName, StorageValueNameError, - TemplateTypeError, WordRepresentation, WordType, + FeltType, InitStorageData, MapEntry, PlaceholderTypeRequirement, StorageEntry, + StorageValueName, StorageValueNameError, TemplateTypeError, WordRepresentation, WordType, }; pub mod delta; From 9a4f5e4c8b5d65abd5dfbe0e958d403a16aeede8 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Wed, 12 Feb 2025 00:30:46 -0300 Subject: [PATCH 4/5] refactor: publish component's libraries --- crates/miden-lib/src/account/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-lib/src/account/mod.rs b/crates/miden-lib/src/account/mod.rs index 6ec9777ce..d1141e765 100644 --- a/crates/miden-lib/src/account/mod.rs +++ b/crates/miden-lib/src/account/mod.rs @@ -1,6 +1,6 @@ use super::auth::AuthScheme; pub mod auth; -pub(super) mod components; +pub mod components; pub mod faucets; pub mod wallets; From 96703f60414a1ed33398768a25e0947c40a91b83 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Wed, 12 Feb 2025 01:38:27 -0300 Subject: [PATCH 5/5] refactor: change key in template requirements --- .../src/account/component/template/mod.rs | 6 ++++-- .../account/component/template/storage/mod.rs | 16 ++++++++++++---- .../component/template/storage/placeholder.rs | 10 ++++------ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/miden-objects/src/account/component/template/mod.rs b/crates/miden-objects/src/account/component/template/mod.rs index 6350bf298..4b5a2218f 100644 --- a/crates/miden-objects/src/account/component/template/mod.rs +++ b/crates/miden-objects/src/account/component/template/mod.rs @@ -204,11 +204,13 @@ impl AccountComponentMetadata { /// /// Types for the returned storage placeholders are inferred based on their location in the /// storage layout structure. - pub fn get_placeholder_requirements(&self) -> BTreeMap { + pub fn get_placeholder_requirements( + &self, + ) -> BTreeMap { let mut templates = BTreeMap::new(); for entry in self.storage_entries() { for (name, requirement) in entry.template_requirements() { - templates.insert(name.to_string(), requirement); + templates.insert(name, requirement); } } diff --git a/crates/miden-objects/src/account/component/template/storage/mod.rs b/crates/miden-objects/src/account/component/template/storage/mod.rs index fb62905ed..b52b4fed7 100644 --- a/crates/miden-objects/src/account/component/template/storage/mod.rs +++ b/crates/miden-objects/src/account/component/template/storage/mod.rs @@ -483,16 +483,24 @@ mod tests { assert_eq!(requirements.len(), 4); - let supply = requirements.get("token_metadata.max_supply").unwrap(); + let supply = requirements + .get(&StorageValueName::new("token_metadata.max_supply").unwrap()) + .unwrap(); assert_eq!(supply.r#type.to_string(), "felt"); - let decimals = requirements.get("token_metadata.decimals").unwrap(); + let decimals = requirements + .get(&StorageValueName::new("token_metadata.decimals").unwrap()) + .unwrap(); assert_eq!(decimals.r#type.to_string(), "u8"); - let default_recallable_height = requirements.get("default_recallable_height").unwrap(); + let default_recallable_height = requirements + .get(&StorageValueName::new("default_recallable_height").unwrap()) + .unwrap(); assert_eq!(default_recallable_height.r#type.to_string(), "u32"); - let map_key_template = requirements.get("map_entry.map_key_template").unwrap(); + let map_key_template = requirements + .get(&StorageValueName::new("map_entry.map_key_template").unwrap()) + .unwrap(); assert_eq!(map_key_template.r#type.to_string(), "word"); let library = Assembler::default().assemble_library([CODE]).unwrap(); diff --git a/crates/miden-objects/src/account/component/template/storage/placeholder.rs b/crates/miden-objects/src/account/component/template/storage/placeholder.rs index fa25df6d4..661760544 100644 --- a/crates/miden-objects/src/account/component/template/storage/placeholder.rs +++ b/crates/miden-objects/src/account/component/template/storage/placeholder.rs @@ -244,6 +244,7 @@ pub enum FeltType { TokenSymbol, } +// TODO: Refactor/remove this. We also want to make parsing consistent (ie, hex vs decimal) impl FeltType { pub fn parse_value(&self, value_str: &str) -> Result { let felt = match self { @@ -257,13 +258,10 @@ impl FeltType { TemplateTypeError::ParseError(value_str.to_string(), self.to_string()) })?), FeltType::Felt => parse_felt_from_str(value_str).map_err(|_| { - TemplateTypeError::ParseError( - FeltType::TokenSymbol.to_string(), - value_str.to_string(), - ) + TemplateTypeError::ParseError(value_str.to_string(), self.to_string()) })?, FeltType::TokenSymbol => Felt::from(TokenSymbol::new(value_str).map_err(|_| { - TemplateTypeError::ParseError(FeltType::TokenSymbol.to_string(), self.to_string()) + TemplateTypeError::ParseError(value_str.to_string(), self.to_string()) })?), }; Ok(felt) @@ -447,7 +445,7 @@ impl From for String { pub enum TemplateTypeError { #[error("failed to convert type into felt: {0}")] ConversionError(String), - #[error("failed to parse string `{0}` as `{1}`")] + #[error("failed to parse input `{0}` as `{1}`")] ParseError(String, String), #[error("parsed value does not fit into expected slot")] TypeArityMismatch,