Skip to main content

miden_standards/account/metadata/
mod.rs

1use alloc::collections::BTreeMap;
2
3use miden_protocol::Word;
4use miden_protocol::account::component::StorageSchema;
5use miden_protocol::account::{AccountComponent, StorageSlot, StorageSlotName};
6use miden_protocol::errors::AccountComponentTemplateError;
7use miden_protocol::utils::sync::LazyLock;
8
9use crate::account::components::storage_schema_library;
10
11pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
12    StorageSlotName::new("miden::standards::metadata::storage_schema")
13        .expect("storage slot name should be valid")
14});
15
16/// An [`AccountComponent`] exposing the account storage schema commitment.
17///
18/// The [`AccountSchemaCommitment`] component can be constructed from a list of [`StorageSchema`],
19/// from which a commitment is computed and then inserted into the [`SCHEMA_COMMITMENT_SLOT_NAME`]
20/// slot.
21///
22/// It reexports the `get_schema_commitment` procedure from
23/// `miden::standards::metadata::storage_schema`.
24///
25/// ## Storage Layout
26///
27/// - [`Self::schema_commitment_slot`]: Storage schema commitment.
28pub struct AccountSchemaCommitment {
29    schema_commitment: Word,
30}
31
32impl AccountSchemaCommitment {
33    /// Creates a new [`AccountSchemaCommitment`] component from a list of storage schemas.
34    ///
35    /// The input schemas are merged into a single schema before the final commitment is computed.
36    ///
37    /// # Errors
38    ///
39    /// Returns an error if the schemas contain conflicting definitions for the same slot name.
40    pub fn new(schemas: &[StorageSchema]) -> Result<Self, AccountComponentTemplateError> {
41        Ok(Self {
42            schema_commitment: compute_schema_commitment(schemas)?,
43        })
44    }
45
46    /// Creates a new [`AccountSchemaCommitment`] component from a [`StorageSchema`].
47    pub fn from_schema(
48        storage_schema: &StorageSchema,
49    ) -> Result<Self, AccountComponentTemplateError> {
50        Self::new(core::slice::from_ref(storage_schema))
51    }
52
53    /// Returns the [`StorageSlotName`] where the schema commitment is stored.
54    pub fn schema_commitment_slot() -> &'static StorageSlotName {
55        &SCHEMA_COMMITMENT_SLOT_NAME
56    }
57}
58
59impl From<AccountSchemaCommitment> for AccountComponent {
60    fn from(schema_commitment: AccountSchemaCommitment) -> Self {
61        AccountComponent::new(
62            storage_schema_library(),
63            vec![StorageSlot::with_value(
64                AccountSchemaCommitment::schema_commitment_slot().clone(),
65                schema_commitment.schema_commitment,
66            )],
67        )
68        .expect(
69            "AccountSchemaCommitment component should satisfy the requirements of a valid account component",
70        )
71        .with_supports_all_types()
72    }
73}
74
75/// Computes the schema commitment.
76///
77/// The account schema commitment is computed from the merged schema commitment.
78/// If the passed list of schemas is empty, [`Word::empty()`] is returned.
79fn compute_schema_commitment(
80    schemas: &[StorageSchema],
81) -> Result<Word, AccountComponentTemplateError> {
82    if schemas.is_empty() {
83        return Ok(Word::empty());
84    }
85
86    let mut merged_slots = BTreeMap::new();
87    for schema in schemas {
88        for (slot_name, slot_schema) in schema.iter() {
89            match merged_slots.get(slot_name) {
90                None => {
91                    merged_slots.insert(slot_name.clone(), slot_schema.clone());
92                },
93                // Slot exists, check if the schema is the same before erroring
94                Some(existing) => {
95                    if existing != slot_schema {
96                        return Err(AccountComponentTemplateError::InvalidSchema(format!(
97                            "conflicting definitions for storage slot `{slot_name}`",
98                        )));
99                    }
100                },
101            }
102        }
103    }
104
105    let merged_schema = StorageSchema::new(merged_slots)?;
106
107    Ok(merged_schema.commitment())
108}
109
110// TESTS
111// ================================================================================================
112
113#[cfg(test)]
114mod tests {
115    use miden_protocol::Word;
116    use miden_protocol::account::AccountBuilder;
117    use miden_protocol::account::component::AccountComponentMetadata;
118
119    use super::AccountSchemaCommitment;
120    use crate::account::auth::NoAuth;
121
122    #[test]
123    fn storage_schema_commitment_is_order_independent() {
124        let toml_a = r#"
125            name = "Component A"
126            description = "Component A schema"
127            version = "0.1.0"
128            supported-types = []
129
130            [[storage.slots]]
131            name = "test::slot_a"
132            type = "word"
133        "#;
134
135        let toml_b = r#"
136            name = "Component B"
137            description = "Component B schema"
138            version = "0.1.0"
139            supported-types = []
140
141            [[storage.slots]]
142            name = "test::slot_b"
143            description = "description is committed to"
144            type = "word"
145        "#;
146
147        let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap();
148        let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap();
149
150        let schema_a = metadata_a.storage_schema().clone();
151        let schema_b = metadata_b.storage_schema().clone();
152
153        // Create one component for each of two different accounts, but switch orderings
154        let component_a =
155            AccountSchemaCommitment::new(&[schema_a.clone(), schema_b.clone()]).unwrap();
156        let component_b = AccountSchemaCommitment::new(&[schema_b, schema_a]).unwrap();
157
158        let account_a = AccountBuilder::new([1u8; 32])
159            .with_auth_component(NoAuth)
160            .with_component(component_a)
161            .build()
162            .unwrap();
163
164        let account_b = AccountBuilder::new([2u8; 32])
165            .with_auth_component(NoAuth)
166            .with_component(component_b)
167            .build()
168            .unwrap();
169
170        let slot_name = AccountSchemaCommitment::schema_commitment_slot();
171        let commitment_a = account_a.storage().get_item(slot_name).unwrap();
172        let commitment_b = account_b.storage().get_item(slot_name).unwrap();
173
174        assert_eq!(commitment_a, commitment_b);
175    }
176
177    #[test]
178    fn storage_schema_commitment_is_empty_for_no_schemas() {
179        let component = AccountSchemaCommitment::new(&[]).unwrap();
180
181        assert_eq!(component.schema_commitment, Word::empty());
182    }
183}