Skip to main content

miden_standards/account/metadata/
schema_commitment.rs

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