Skip to main content

miden_standards/account/metadata/
mod.rs

1use alloc::collections::BTreeMap;
2
3use miden_protocol::Word;
4use miden_protocol::account::component::{AccountComponentMetadata, StorageSchema};
5use miden_protocol::account::{
6    Account,
7    AccountBuilder,
8    AccountComponent,
9    StorageSlot,
10    StorageSlotName,
11};
12use miden_protocol::errors::{AccountError, ComponentMetadataError};
13use miden_protocol::utils::sync::LazyLock;
14
15use crate::account::components::storage_schema_library;
16
17pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
18    StorageSlotName::new("miden::standards::metadata::storage_schema")
19        .expect("storage slot name should be valid")
20});
21
22/// An [`AccountComponent`] exposing the account storage schema commitment.
23///
24/// The [`AccountSchemaCommitment`] component can be constructed from a list of [`StorageSchema`],
25/// from which a commitment is computed and then inserted into the [`SCHEMA_COMMITMENT_SLOT_NAME`]
26/// slot.
27///
28/// It reexports the `get_schema_commitment` procedure from
29/// `miden::standards::metadata::storage_schema`.
30///
31/// ## Storage Layout
32///
33/// - [`Self::schema_commitment_slot`]: Storage schema commitment.
34pub struct AccountSchemaCommitment {
35    schema_commitment: Word,
36}
37
38impl AccountSchemaCommitment {
39    /// Creates a new [`AccountSchemaCommitment`] component from storage schemas.
40    ///
41    /// The input schemas are merged into a single schema before the final commitment is computed.
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if the schemas contain conflicting definitions for the same slot name.
46    pub fn new<'a>(
47        schemas: impl IntoIterator<Item = &'a StorageSchema>,
48    ) -> Result<Self, ComponentMetadataError> {
49        Ok(Self {
50            schema_commitment: compute_schema_commitment(schemas)?,
51        })
52    }
53
54    /// Creates a new [`AccountSchemaCommitment`] component from a [`StorageSchema`].
55    pub fn from_schema(storage_schema: &StorageSchema) -> Result<Self, ComponentMetadataError> {
56        Self::new(core::slice::from_ref(storage_schema))
57    }
58
59    /// Returns the [`StorageSlotName`] where the schema commitment is stored.
60    pub fn schema_commitment_slot() -> &'static StorageSlotName {
61        &SCHEMA_COMMITMENT_SLOT_NAME
62    }
63}
64
65impl From<AccountSchemaCommitment> for AccountComponent {
66    fn from(schema_commitment: AccountSchemaCommitment) -> Self {
67        let metadata = AccountComponentMetadata::new("miden::metadata::schema_commitment")
68            .with_description("Component exposing the account storage schema commitment")
69            .with_supports_all_types();
70
71        AccountComponent::new(
72            storage_schema_library(),
73            vec![StorageSlot::with_value(
74                AccountSchemaCommitment::schema_commitment_slot().clone(),
75                schema_commitment.schema_commitment,
76            )],
77            metadata,
78        )
79        .expect(
80            "AccountSchemaCommitment component should satisfy the requirements of a valid account component",
81        )
82    }
83}
84
85// ACCOUNT BUILDER EXTENSION
86// ================================================================================================
87
88/// An extension trait for [`AccountBuilder`] that provides a convenience method for building an
89/// account with an [`AccountSchemaCommitment`] component.
90pub trait AccountBuilderSchemaCommitmentExt {
91    /// Builds an [`Account`] out of the configured builder after computing the storage schema
92    /// commitment from all components currently in the builder and adding an
93    /// [`AccountSchemaCommitment`] component.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if:
98    /// - The components' storage schemas contain conflicting definitions for the same slot name.
99    /// - [`AccountBuilder::build`] fails.
100    fn build_with_schema_commitment(self) -> Result<Account, AccountError>;
101}
102
103impl AccountBuilderSchemaCommitmentExt for AccountBuilder {
104    fn build_with_schema_commitment(self) -> Result<Account, AccountError> {
105        let schema_commitment =
106            AccountSchemaCommitment::new(self.storage_schemas()).map_err(|err| {
107                AccountError::other_with_source("failed to compute account schema commitment", err)
108            })?;
109
110        self.with_component(schema_commitment).build()
111    }
112}
113
114// HELPERS
115// ================================================================================================
116
117/// Computes the schema commitment.
118///
119/// The account schema commitment is computed from the merged schema commitment.
120/// If the passed list of schemas is empty, [`Word::empty()`] is returned.
121fn compute_schema_commitment<'a>(
122    schemas: impl IntoIterator<Item = &'a StorageSchema>,
123) -> Result<Word, ComponentMetadataError> {
124    let mut schemas = schemas.into_iter().peekable();
125    if schemas.peek().is_none() {
126        return Ok(Word::empty());
127    }
128
129    let mut merged_slots = BTreeMap::new();
130
131    for schema in schemas {
132        for (slot_name, slot_schema) in schema.iter() {
133            match merged_slots.get(slot_name) {
134                None => {
135                    merged_slots.insert(slot_name.clone(), slot_schema.clone());
136                },
137                // Slot exists, check if the schema is the same before erroring
138                Some(existing) => {
139                    if existing != slot_schema {
140                        return Err(ComponentMetadataError::InvalidSchema(format!(
141                            "conflicting definitions for storage slot `{slot_name}`",
142                        )));
143                    }
144                },
145            }
146        }
147    }
148
149    let merged_schema = StorageSchema::new(merged_slots)?;
150
151    Ok(merged_schema.commitment())
152}
153
154// TESTS
155// ================================================================================================
156
157#[cfg(test)]
158mod tests {
159    use miden_protocol::Word;
160    use miden_protocol::account::AccountBuilder;
161    use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
162    use miden_protocol::account::component::AccountComponentMetadata;
163
164    use super::{AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment};
165    use crate::account::auth::{AuthSingleSig, NoAuth};
166
167    #[test]
168    fn storage_schema_commitment_is_order_independent() {
169        let toml_a = r#"
170            name = "Component A"
171            description = "Component A schema"
172            version = "0.1.0"
173            supported-types = []
174
175            [[storage.slots]]
176            name = "test::slot_a"
177            type = "word"
178        "#;
179
180        let toml_b = r#"
181            name = "Component B"
182            description = "Component B schema"
183            version = "0.1.0"
184            supported-types = []
185
186            [[storage.slots]]
187            name = "test::slot_b"
188            description = "description is committed to"
189            type = "word"
190        "#;
191
192        let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap();
193        let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap();
194
195        let schema_a = metadata_a.storage_schema().clone();
196        let schema_b = metadata_b.storage_schema().clone();
197
198        // Create one component for each of two different accounts, but switch orderings
199        let component_a =
200            AccountSchemaCommitment::new(&[schema_a.clone(), schema_b.clone()]).unwrap();
201        let component_b = AccountSchemaCommitment::new(&[schema_b, schema_a]).unwrap();
202
203        let account_a = AccountBuilder::new([1u8; 32])
204            .with_auth_component(NoAuth)
205            .with_component(component_a)
206            .build()
207            .unwrap();
208
209        let account_b = AccountBuilder::new([2u8; 32])
210            .with_auth_component(NoAuth)
211            .with_component(component_b)
212            .build()
213            .unwrap();
214
215        let slot_name = AccountSchemaCommitment::schema_commitment_slot();
216        let commitment_a = account_a.storage().get_item(slot_name).unwrap();
217        let commitment_b = account_b.storage().get_item(slot_name).unwrap();
218
219        assert_eq!(commitment_a, commitment_b);
220    }
221
222    #[test]
223    fn storage_schema_commitment_is_empty_for_no_schemas() {
224        let component = AccountSchemaCommitment::new(&[]).unwrap();
225
226        assert_eq!(component.schema_commitment, Word::empty());
227    }
228
229    #[test]
230    fn build_with_schema_commitment_adds_schema_commitment_component() {
231        let auth_component = AuthSingleSig::new(
232            PublicKeyCommitment::from(Word::empty()),
233            AuthScheme::EcdsaK256Keccak,
234        );
235
236        let account = AccountBuilder::new([1u8; 32])
237            .with_auth_component(auth_component)
238            .build_with_schema_commitment()
239            .unwrap();
240
241        // The auth component has 2 slots (public key and scheme ID) and the schema commitment adds
242        // 1 more.
243        assert_eq!(account.storage().num_slots(), 3);
244
245        // The auth component's public key slot should be accessible.
246        assert!(account.storage().get_item(AuthSingleSig::public_key_slot()).is_ok());
247
248        // The schema commitment slot should be non-empty since we have a component with a schema.
249        let slot_name = AccountSchemaCommitment::schema_commitment_slot();
250        let commitment = account.storage().get_item(slot_name).unwrap();
251        assert_ne!(commitment, Word::empty());
252    }
253}