Skip to main content

miden_standards/account/inspection/
schema_commitment.rs

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