diff --git a/CHANGELOG.md b/CHANGELOG.md index 3369c0e5b..49064ae53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ - [BREAKING] Migrated to `miden-vm` v0.20 and `miden-crypto` v0.19 ([#2158](https://github.com/0xMiden/miden-base/pull/2158)). - [BREAKING] Refactored `AccountStorageDelta` to use a new `StorageSlotDelta` type ([#2182](https://github.com/0xMiden/miden-base/pull/2182)). - [BREAKING] Removed OLD_MAP_ROOT from being returned when calling [`native_account::set_map_item`](crates/miden-lib/asm/miden/native_account.masm) ([#2194](https://github.com/0xMiden/miden-base/pull/2194)). +- [BREAKING] Refactored account component templates into `StorageSchema` ([#2193](https://github.com/0xMiden/miden-base/pull/2193)). +- [BREAKING] Refactored `InitStorageData` to support native types ([#2230](https://github.com/0xMiden/miden-base/pull/2230)). +- Added `StorageSchema::commitment()` ([#2244](https://github.com/0xMiden/miden-base/pull/2244)). - [BREAKING] Refactored account component templates into `AccountStorageSchema` ([#2193](https://github.com/0xMiden/miden-base/pull/2193)). - [BREAKING] Refactor note tags to be arbitrary `u32` values and drop previous validation ([#2219](https://github.com/0xMiden/miden-base/pull/2219)). - [BREAKING] Refactored `InitStorageData` to support native types ([#2230](https://github.com/0xMiden/miden-base/pull/2230)). diff --git a/crates/miden-protocol/src/account/component/metadata/mod.rs b/crates/miden-protocol/src/account/component/metadata/mod.rs index db68b27b0..1d8662dc1 100644 --- a/crates/miden-protocol/src/account/component/metadata/mod.rs +++ b/crates/miden-protocol/src/account/component/metadata/mod.rs @@ -7,7 +7,7 @@ use miden_mast_package::{Package, SectionId}; use miden_processor::DeserializationError; use semver::Version; -use super::{AccountStorageSchema, AccountType, SchemaRequirement, StorageValueName}; +use super::{AccountType, SchemaRequirement, StorageSchema, StorageValueName}; use crate::AccountError; // ACCOUNT COMPONENT METADATA @@ -40,10 +40,10 @@ use crate::AccountError; /// use miden_protocol::account::StorageSlotName; /// use miden_protocol::account::component::{ /// AccountComponentMetadata, -/// AccountStorageSchema, /// FeltSchema, /// InitStorageData, /// SchemaTypeId, +/// StorageSchema, /// StorageSlotSchema, /// StorageValueName, /// ValueSlotSchema, @@ -61,7 +61,7 @@ use crate::AccountError; /// FeltSchema::new_typed(SchemaTypeId::native_felt(), "foo"), /// ]); /// -/// let storage_schema = AccountStorageSchema::new([( +/// let storage_schema = StorageSchema::new([( /// slot_name.clone(), /// StorageSlotSchema::Value(ValueSlotSchema::new(Some("demo slot".into()), word)), /// )])?; @@ -102,7 +102,7 @@ pub struct AccountComponentMetadata { /// Storage schema defining the component's storage layout, defaults, and init-supplied values. #[cfg_attr(feature = "std", serde(rename = "storage"))] - storage_schema: AccountStorageSchema, + storage_schema: StorageSchema, } impl AccountComponentMetadata { @@ -112,7 +112,7 @@ impl AccountComponentMetadata { description: String, version: Version, targets: BTreeSet, - storage_schema: AccountStorageSchema, + storage_schema: StorageSchema, ) -> Self { Self { name, @@ -154,7 +154,7 @@ impl AccountComponentMetadata { } /// Returns the storage schema of the component. - pub fn storage_schema(&self) -> &AccountStorageSchema { + pub fn storage_schema(&self) -> &StorageSchema { &self.storage_schema } } @@ -200,14 +200,24 @@ impl Serializable for AccountComponentMetadata { impl Deserializable for AccountComponentMetadata { fn read_from(source: &mut R) -> Result { + let name = String::read_from(source)?; + let description = String::read_from(source)?; + if !description.is_ascii() { + return Err(DeserializationError::InvalidValue( + "description must contain only ASCII characters".to_string(), + )); + } + let version = semver::Version::from_str(&String::read_from(source)?) + .map_err(|err: semver::Error| DeserializationError::InvalidValue(err.to_string()))?; + let supported_types = BTreeSet::::read_from(source)?; + let storage_schema = StorageSchema::read_from(source)?; + Ok(Self { - name: String::read_from(source)?, - description: String::read_from(source)?, - version: semver::Version::from_str(&String::read_from(source)?).map_err( - |err: semver::Error| DeserializationError::InvalidValue(err.to_string()), - )?, - supported_types: BTreeSet::::read_from(source)?, - storage_schema: AccountStorageSchema::read_from(source)?, + name, + description, + version, + supported_types, + storage_schema, }) } } diff --git a/crates/miden-protocol/src/account/component/mod.rs b/crates/miden-protocol/src/account/component/mod.rs index 9f709b28d..57e62bf28 100644 --- a/crates/miden-protocol/src/account/component/mod.rs +++ b/crates/miden-protocol/src/account/component/mod.rs @@ -271,7 +271,7 @@ mod tests { "A test component".to_string(), Version::new(1, 0, 0), BTreeSet::from_iter([AccountType::RegularAccountImmutableCode]), - AccountStorageSchema::default(), + StorageSchema::default(), ); let metadata_bytes = metadata.to_bytes(); @@ -329,7 +329,7 @@ mod tests { AccountType::RegularAccountImmutableCode, AccountType::RegularAccountUpdatableCode, ]), - AccountStorageSchema::default(), + StorageSchema::default(), ); // Test with empty init data - this tests the complete workflow: diff --git a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs index ff870dd32..a9df7e07e 100644 --- a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs @@ -23,6 +23,12 @@ pub enum WordValue { Elements([String; 4]), } +impl From for WordValue { + fn from(value: Word) -> Self { + WordValue::FullyTyped(value) + } +} + impl From for WordValue { fn from(value: String) -> Self { WordValue::Atomic(value) @@ -35,11 +41,8 @@ impl From<&str> for WordValue { } } -impl From for WordValue { - fn from(value: Word) -> Self { - WordValue::FullyTyped(value) - } -} +// CONVERSIONS +// ==================================================================================================== impl From for WordValue { /// Converts a [`Felt`] to a [`WordValue`] as a Word in the form `[0, 0, 0, felt]`. @@ -120,7 +123,7 @@ impl InitStorageData { /// - `[Felt; 4]`: converted to a Word /// - `Felt`: converted to `[0, 0, 0, felt]` /// - `String` or `&str`: a parseable string value - /// - `WordValue`: a raw or fully-typed word value + /// - `WordValue`: a word value (fully typed, atomic, or elements) pub fn insert_value( &mut self, name: StorageValueName, @@ -201,6 +204,12 @@ impl InitStorageData { self.map_entries.entry(slot_name).or_default().extend(entries); } } + + /// Merges another [`InitStorageData`] into this one, overwriting value entries and appending + /// map entries. + pub fn merge_from(&mut self, other: InitStorageData) { + self.merge_with(other); + } } // ERRORS diff --git a/crates/miden-protocol/src/account/component/storage/schema.rs b/crates/miden-protocol/src/account/component/storage/schema.rs index e8054ac48..710e70af9 100644 --- a/crates/miden-protocol/src/account/component/storage/schema.rs +++ b/crates/miden-protocol/src/account/component/storage/schema.rs @@ -10,20 +10,21 @@ use super::type_registry::{SCHEMA_TYPE_REGISTRY, SchemaRequirement, SchemaTypeId use super::{InitStorageData, StorageValueName, WordValue}; use crate::account::storage::is_reserved_slot_name; use crate::account::{StorageMap, StorageSlot, StorageSlotName}; +use crate::crypto::utils::bytes_to_elements_with_padding; use crate::errors::AccountComponentTemplateError; -use crate::{Felt, FieldElement, Word}; +use crate::{Felt, FieldElement, Hasher, Word}; // STORAGE SCHEMA // ================================================================================================ /// Describes the storage schema of an account component in terms of its named storage slots. #[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct AccountStorageSchema { +pub struct StorageSchema { slots: BTreeMap, } -impl AccountStorageSchema { - /// Creates a new [`AccountStorageSchema`]. +impl StorageSchema { + /// Creates a new [`StorageSchema`]. /// /// # Errors /// - If `fields` contains duplicate slot names. @@ -66,6 +67,16 @@ impl AccountStorageSchema { .collect() } + /// Returns a commitment to this storage schema definition. + /// + /// The commitment is computed over the serialized schema and does not include defaults. + pub fn commitment(&self) -> Word { + let mut bytes = Vec::new(); + self.write_into_with_optional_defaults(&mut bytes, false); + let elements = bytes_to_elements_with_padding(&bytes); + Hasher::hash_elements(&elements) + } + /// Returns init-value requirements for the entire schema. /// /// The returned map includes both required values (no `default_value`) and optional values @@ -80,6 +91,21 @@ impl AccountStorageSchema { Ok(requirements) } + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { + target.write_u16(self.slots.len() as u16); + for (slot_name, schema) in self.slots.iter() { + target.write(slot_name); + schema.write_into_with_optional_defaults(target, include_defaults); + } + } + + /// Validates schema-level invariants across all slots. fn validate(&self) -> Result<(), AccountComponentTemplateError> { let mut init_values = BTreeMap::new(); @@ -88,7 +114,7 @@ impl AccountStorageSchema { return Err(AccountComponentTemplateError::ReservedSlotName(slot_name.clone())); } - schema.validate(slot_name)?; + schema.validate()?; schema.collect_init_value_requirements(slot_name, &mut init_values)?; } @@ -96,17 +122,13 @@ impl AccountStorageSchema { } } -impl Serializable for AccountStorageSchema { +impl Serializable for StorageSchema { fn write_into(&self, target: &mut W) { - target.write_u16(self.slots.len() as u16); - for (slot_name, schema) in self.slots.iter() { - target.write(slot_name); - target.write(schema); - } + self.write_into_with_optional_defaults(target, true); } } -impl Deserializable for AccountStorageSchema { +impl Deserializable for StorageSchema { fn read_from(source: &mut R) -> Result { let num_entries = source.read_u16()? as usize; let mut fields = BTreeMap::new(); @@ -122,12 +144,22 @@ impl Deserializable for AccountStorageSchema { } } - let schema = AccountStorageSchema::new(fields) + let schema = StorageSchema::new(fields) .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; Ok(schema) } } +fn validate_description_ascii(description: &str) -> Result<(), AccountComponentTemplateError> { + if description.is_ascii() { + Ok(()) + } else { + Err(AccountComponentTemplateError::InvalidSchema( + "description must contain only ASCII characters".to_string(), + )) + } +} + // STORAGE SLOT SCHEMA // ================================================================================================ @@ -174,34 +206,42 @@ impl StorageSlotSchema { } } - pub(crate) fn validate( - &self, - slot_name: &StorageSlotName, - ) -> Result<(), AccountComponentTemplateError> { + /// Validates this slot schema's internal invariants. + pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> { match self { - StorageSlotSchema::Value(slot) => slot.validate(slot_name)?, + StorageSlotSchema::Value(slot) => slot.validate()?, StorageSlotSchema::Map(slot) => slot.validate()?, } Ok(()) } -} -impl Serializable for StorageSlotSchema { - fn write_into(&self, target: &mut W) { + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { match self { StorageSlotSchema::Value(slot) => { target.write_u8(0u8); - slot.write_into(target); + slot.write_into_with_optional_defaults(target, include_defaults); }, StorageSlotSchema::Map(slot) => { target.write_u8(1u8); - slot.write_into(target); + slot.write_into_with_optional_defaults(target, include_defaults); }, } } } +impl Serializable for StorageSlotSchema { + fn write_into(&self, target: &mut W) { + self.write_into_with_optional_defaults(target, true); + } +} + impl Deserializable for StorageSlotSchema { fn read_from(source: &mut R) -> Result { let variant_tag = source.read_u8()?; @@ -306,7 +346,7 @@ impl WordSchema { } } - /// Validates that the defined word type exists and its inner felts (if any) are valid. + /// Validates the word schema type, defaults, and inner felts (if any). fn validate(&self) -> Result<(), AccountComponentTemplateError> { let type_exists = SCHEMA_TYPE_REGISTRY.contains_word_type(&self.word_type()); if !type_exists { @@ -429,24 +469,37 @@ impl WordSchema { }, } } -} -impl Serializable for WordSchema { - fn write_into(&self, target: &mut W) { + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { match self { WordSchema::Simple { r#type, default_value } => { target.write_u8(0); target.write(r#type); + let default_value = if include_defaults { *default_value } else { None }; target.write(default_value); }, WordSchema::Composite { value } => { target.write_u8(1); - target.write(value); + for felt in value.iter() { + felt.write_into_with_optional_defaults(target, include_defaults); + } }, } } } +impl Serializable for WordSchema { + fn write_into(&self, target: &mut W) { + self.write_into_with_optional_defaults(target, true); + } +} + impl Deserializable for WordSchema { fn read_from(source: &mut R) -> Result { let tag = source.read_u8()?; @@ -655,8 +708,26 @@ impl FeltSchema { Err(AccountComponentTemplateError::InitValueNotProvided(value_name)) } - /// Validates that the defined felt type exists. + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { + target.write(&self.name); + target.write(&self.description); + target.write(&self.r#type); + let default_value = if include_defaults { self.default_value } else { None }; + target.write(default_value); + } + + /// Validates the felt type, naming rules, and default value (if any). fn validate(&self) -> Result<(), AccountComponentTemplateError> { + if let Some(description) = self.description.as_deref() { + validate_description_ascii(description)?; + } + let type_exists = SCHEMA_TYPE_REGISTRY.contains_felt_type(&self.felt_type()); if !type_exists { return Err(AccountComponentTemplateError::InvalidType( @@ -696,10 +767,7 @@ impl FeltSchema { impl Serializable for FeltSchema { fn write_into(&self, target: &mut W) { - target.write(&self.name); - target.write(&self.description); - target.write(&self.r#type); - target.write(self.default_value); + self.write_into_with_optional_defaults(target, true); } } @@ -754,10 +822,22 @@ impl ValueSlotSchema { self.word.try_build_word(init_storage_data, slot_name) } - pub(crate) fn validate( + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( &self, - _slot_name: &StorageSlotName, - ) -> Result<(), AccountComponentTemplateError> { + target: &mut W, + include_defaults: bool, + ) { + target.write(&self.description); + self.word.write_into_with_optional_defaults(target, include_defaults); + } + + /// Validates the slot's word schema. + pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> { + if let Some(description) = self.description.as_deref() { + validate_description_ascii(description)?; + } self.word.validate()?; Ok(()) } @@ -765,8 +845,7 @@ impl ValueSlotSchema { impl Serializable for ValueSlotSchema { fn write_into(&self, target: &mut W) { - target.write(&self.description); - target.write(&self.word); + self.write_into_with_optional_defaults(target, true); } } @@ -865,7 +944,29 @@ impl MapSlotSchema { self.default_values.clone() } + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { + target.write(&self.description); + let default_values = if include_defaults { + self.default_values.clone() + } else { + None + }; + target.write(&default_values); + self.key_schema.write_into_with_optional_defaults(target, include_defaults); + self.value_schema.write_into_with_optional_defaults(target, include_defaults); + } + + /// Validates key/value word schemas for this map slot. fn validate(&self) -> Result<(), AccountComponentTemplateError> { + if let Some(description) = self.description.as_deref() { + validate_description_ascii(description)?; + } self.key_schema.validate()?; self.value_schema.validate()?; Ok(()) @@ -956,10 +1057,7 @@ fn parse_composite_elements( impl Serializable for MapSlotSchema { fn write_into(&self, target: &mut W) { - target.write(&self.description); - target.write(&self.default_values); - target.write(&self.key_schema); - target.write(&self.value_schema); + self.write_into_with_optional_defaults(target, true); } } diff --git a/crates/miden-protocol/src/account/component/storage/toml/mod.rs b/crates/miden-protocol/src/account/component/storage/toml/mod.rs index a5e679959..3a9c13726 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/mod.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/mod.rs @@ -8,9 +8,9 @@ use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::super::{ - AccountStorageSchema, FeltSchema, MapSlotSchema, + StorageSchema, StorageSlotSchema, StorageValueName, ValueSlotSchema, @@ -55,6 +55,12 @@ impl AccountComponentMetadata { let raw: RawAccountComponentMetadata = toml::from_str(toml_string) .map_err(AccountComponentTemplateError::TomlDeserializationError)?; + if !raw.description.is_ascii() { + return Err(AccountComponentTemplateError::InvalidSchema( + "description must contain only ASCII characters".to_string(), + )); + } + let RawStorageSchema { slots } = raw.storage; let mut fields = Vec::with_capacity(slots.len()); @@ -62,7 +68,7 @@ impl AccountComponentMetadata { fields.push(slot.try_into_slot_schema()?); } - let storage_schema = AccountStorageSchema::new(fields)?; + let storage_schema = StorageSchema::new(fields)?; Ok(Self::new( raw.name, raw.description, @@ -130,7 +136,7 @@ struct RawMapType { // ACCOUNT STORAGE SCHEMA SERDE // ================================================================================================ -impl Serialize for AccountStorageSchema { +impl Serialize for StorageSchema { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -145,7 +151,7 @@ impl Serialize for AccountStorageSchema { } } -impl<'de> Deserialize<'de> for AccountStorageSchema { +impl<'de> Deserialize<'de> for StorageSchema { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -159,7 +165,7 @@ impl<'de> Deserialize<'de> for AccountStorageSchema { fields.push((slot_name, schema)); } - AccountStorageSchema::new(fields).map_err(D::Error::custom) + StorageSchema::new(fields).map_err(D::Error::custom) } } diff --git a/crates/miden-protocol/src/account/component/storage/toml/tests.rs b/crates/miden-protocol/src/account/component/storage/toml/tests.rs index 627b78361..dd6ead16c 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/tests.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/tests.rs @@ -1,7 +1,8 @@ use alloc::string::ToString; use core::error::Error; -use miden_core::{Felt, FieldElement, Word}; +use miden_air::FieldElement; +use miden_core::{Felt, Word}; use crate::account::component::toml::init_storage_data::InitStorageDataError; use crate::account::component::{ @@ -259,6 +260,163 @@ fn metadata_from_toml_parses_named_storage_schema() { assert!(!requirements.contains_key(&"demo::my_map".parse::().unwrap())); } +#[test] +fn metadata_from_toml_rejects_non_ascii_component_description() { + let toml_str = r#" + name = "Test Component" + description = "Invalid \u00e9" + version = "0.1.0" + supported-types = [] + "#; + + assert_matches::assert_matches!( + AccountComponentMetadata::from_toml(toml_str), + Err(AccountComponentTemplateError::InvalidSchema(_)) + ); +} + +#[test] +fn metadata_from_toml_rejects_non_ascii_slot_description() { + let toml_str = r#" + name = "Test Component" + description = "Test description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::test_value" + description = "Invalid \u00e9" + type = "word" + "#; + + assert_matches::assert_matches!( + AccountComponentMetadata::from_toml(toml_str), + Err(AccountComponentTemplateError::InvalidSchema(_)) + ); +} + +#[test] +fn metadata_schema_commitment_ignores_defaults_and_ordering() { + let toml_a = r#" + name = "Commitment Test" + description = "Schema commitments are equal regardless of defaults and ordering" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::first" + type = "word" + default-value = "0x1" + + [[storage.slots]] + name = "demo::map" + type = { key = "word", value = "word" } + default-values = [ + { key = "0x1", value = "0x10" }, + ] + + [[storage.slots]] + name = "demo::composed" + type = [ + { name = "a", type = "u8", description = "field a", default-value = "1" }, + { name = "b", description = "field b", default-value = "2" }, + { name = "c", type = "u16", description = "field c", default-value = "3" }, + { type = "void", description = "padding" }, + ] + "#; + + let toml_b = r#" + name = "Commitment Test" + description = "" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::map" + type = { key = "word", value = "word" } + default-values = [ + { key = "0x2", value = "0x20" }, + ] + + [[storage.slots]] + name = "demo::composed" + type = [ + { name = "a", type = "u8", description = "field a", default-value = "9" }, + { name = "b", description = "field b", default-value = "8" }, + { name = "c", type = "u16", description = "field c", default-value = "7" }, + { type = "void", description = "padding" }, + ] + + [[storage.slots]] + name = "demo::first" + type = "word" + default-value = "0x9" + "#; + + let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap(); + let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap(); + + assert_ne!(metadata_a.storage_schema(), metadata_b.storage_schema()); + assert_eq!( + metadata_a.storage_schema().commitment(), + metadata_b.storage_schema().commitment() + ); +} + +#[test] +fn metadata_schema_commitment_includes_descriptions() { + let toml_a = r#" + name = "Commitment Test" + description = "Component description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::value" + description = "slot description a" + type = "word" + "#; + + let toml_bad_description = r#" + name = "Commitment Test" + description = "Component description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::value" + description = "incorrect description" + type = "word" + "#; + + let toml_bad_name = r#" + name = "Commitment Test" + description = "Component description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::bad_value" + description = "slot description a" + type = "word" + "#; + + let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap(); + let metadata_bad_description = + AccountComponentMetadata::from_toml(toml_bad_description).unwrap(); + let metadata_bad_slot_name = AccountComponentMetadata::from_toml(toml_bad_name).unwrap(); + + assert_ne!( + metadata_a.storage_schema().commitment(), + metadata_bad_description.storage_schema().commitment() + ); + + assert_ne!( + metadata_a.storage_schema().commitment(), + metadata_bad_slot_name.storage_schema().commitment() + ); +} + #[test] fn metadata_from_toml_rejects_typed_fields_in_static_map_values() { let toml_str = r#"