miden_standards/account/metadata/
mod.rs1use 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
22pub struct AccountSchemaCommitment {
35 schema_commitment: Word,
36}
37
38impl AccountSchemaCommitment {
39 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 pub fn from_schema(storage_schema: &StorageSchema) -> Result<Self, ComponentMetadataError> {
56 Self::new(core::slice::from_ref(storage_schema))
57 }
58
59 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
85pub trait AccountBuilderSchemaCommitmentExt {
91 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
114fn 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 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#[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 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 assert_eq!(account.storage().num_slots(), 3);
244
245 assert!(account.storage().get_item(AuthSingleSig::public_key_slot()).is_ok());
247
248 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}