miden_standards/account/metadata/
schema_commitment.rs1use alloc::collections::BTreeMap;
10
11use miden_protocol::Word;
12use miden_protocol::account::component::{
13 AccountComponentMetadata,
14 SchemaType,
15 StorageSchema,
16 StorageSlotSchema,
17 WordSchema,
18};
19use miden_protocol::account::{
20 Account,
21 AccountBuilder,
22 AccountComponent,
23 StorageSlot,
24 StorageSlotName,
25};
26use miden_protocol::assembly::Library;
27use miden_protocol::errors::{AccountError, ComponentMetadataError};
28use miden_protocol::utils::serde::Deserializable;
29use miden_protocol::utils::sync::LazyLock;
30
31static STORAGE_SCHEMA_LIBRARY: LazyLock<Library> = LazyLock::new(|| {
36 let bytes = include_bytes!(concat!(
37 env!("OUT_DIR"),
38 "/assets/account_components/metadata/schema_commitment.masl"
39 ));
40 Library::read_from_bytes(bytes).expect("Shipped Storage Schema library is well-formed")
41});
42
43static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
45 StorageSlotName::new("miden::standards::metadata::storage_schema::commitment")
46 .expect("storage slot name should be valid")
47});
48
49pub struct AccountSchemaCommitment {
65 schema_commitment: Word,
66}
67
68impl AccountSchemaCommitment {
69 const NAME: &str = "miden::standards::metadata::storage_schema";
72 pub fn new<'a>(
80 schemas: impl IntoIterator<Item = &'a StorageSchema>,
81 ) -> Result<Self, ComponentMetadataError> {
82 Ok(Self {
83 schema_commitment: compute_schema_commitment(schemas)?,
84 })
85 }
86
87 pub fn from_schema(storage_schema: &StorageSchema) -> Result<Self, ComponentMetadataError> {
89 Self::new(core::slice::from_ref(storage_schema))
90 }
91
92 pub fn schema_commitment_slot() -> &'static StorageSlotName {
94 &SCHEMA_COMMITMENT_SLOT_NAME
95 }
96
97 pub fn component_metadata() -> AccountComponentMetadata {
99 let storage_schema = StorageSchema::new([(
100 Self::schema_commitment_slot().clone(),
101 StorageSlotSchema::value(
102 "Commitment to the storage schema of an account",
103 WordSchema::new_simple(SchemaType::native_word()),
104 ),
105 )])
106 .expect("storage schema should be valid");
107
108 AccountComponentMetadata::new(Self::NAME)
109 .with_description("Component exposing the account storage schema commitment")
110 .with_storage_schema(storage_schema)
111 }
112}
113
114impl From<AccountSchemaCommitment> for AccountComponent {
115 fn from(schema_commitment: AccountSchemaCommitment) -> Self {
116 let metadata = AccountSchemaCommitment::component_metadata();
117 let storage = vec![StorageSlot::with_value(
118 AccountSchemaCommitment::schema_commitment_slot().clone(),
119 schema_commitment.schema_commitment,
120 )];
121
122 AccountComponent::new(STORAGE_SCHEMA_LIBRARY.clone(), storage, metadata)
123 .expect("AccountSchemaCommitment is a valid account component")
124 }
125}
126
127pub trait AccountBuilderSchemaCommitmentExt {
133 fn build_with_schema_commitment(self) -> Result<Account, AccountError>;
143}
144
145impl AccountBuilderSchemaCommitmentExt for AccountBuilder {
146 fn build_with_schema_commitment(self) -> Result<Account, AccountError> {
147 let schema_commitment =
148 AccountSchemaCommitment::new(self.storage_schemas()).map_err(|err| {
149 AccountError::other_with_source("failed to compute account schema commitment", err)
150 })?;
151
152 self.with_component(schema_commitment).build()
153 }
154}
155
156fn compute_schema_commitment<'a>(
164 schemas: impl IntoIterator<Item = &'a StorageSchema>,
165) -> Result<Word, ComponentMetadataError> {
166 let mut schemas = schemas.into_iter().peekable();
167 if schemas.peek().is_none() {
168 return Ok(Word::empty());
169 }
170
171 let mut merged_slots = BTreeMap::new();
172
173 for schema in schemas {
174 for (slot_name, slot_schema) in schema.iter() {
175 match merged_slots.get(slot_name) {
176 None => {
177 merged_slots.insert(slot_name.clone(), slot_schema.clone());
178 },
179 Some(existing) => {
181 if existing != slot_schema {
182 return Err(ComponentMetadataError::InvalidSchema(format!(
183 "conflicting definitions for storage slot `{slot_name}`",
184 )));
185 }
186 },
187 }
188 }
189 }
190
191 let merged_schema = StorageSchema::new(merged_slots)?;
192
193 Ok(merged_schema.commitment())
194}
195
196#[cfg(test)]
200mod tests {
201 use miden_protocol::Word;
202 use miden_protocol::account::AccountBuilder;
203 use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
204 use miden_protocol::account::component::AccountComponentMetadata;
205
206 use super::{AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment};
207 use crate::account::auth::{AuthSingleSig, NoAuth};
208
209 #[test]
210 fn storage_schema_commitment_is_order_independent() {
211 let toml_a = r#"
212 name = "Component A"
213 description = "Component A schema"
214 version = "0.1.0"
215
216 [[storage.slots]]
217 name = "test::slot_a"
218 type = "word"
219 "#;
220
221 let toml_b = r#"
222 name = "Component B"
223 description = "Component B schema"
224 version = "0.1.0"
225
226 [[storage.slots]]
227 name = "test::slot_b"
228 description = "description is committed to"
229 type = "word"
230 "#;
231
232 let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap();
233 let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap();
234
235 let schema_a = metadata_a.storage_schema().clone();
236 let schema_b = metadata_b.storage_schema().clone();
237
238 let component_a =
240 AccountSchemaCommitment::new(&[schema_a.clone(), schema_b.clone()]).unwrap();
241 let component_b = AccountSchemaCommitment::new(&[schema_b, schema_a]).unwrap();
242
243 let account_a = AccountBuilder::new([1u8; 32])
244 .with_auth_component(NoAuth)
245 .with_component(component_a)
246 .build()
247 .unwrap();
248
249 let account_b = AccountBuilder::new([2u8; 32])
250 .with_auth_component(NoAuth)
251 .with_component(component_b)
252 .build()
253 .unwrap();
254
255 let slot_name = AccountSchemaCommitment::schema_commitment_slot();
256 let commitment_a = account_a.storage().get_item(slot_name).unwrap();
257 let commitment_b = account_b.storage().get_item(slot_name).unwrap();
258
259 assert_eq!(commitment_a, commitment_b);
260 }
261
262 #[test]
263 fn storage_schema_commitment_is_empty_for_no_schemas() {
264 let component = AccountSchemaCommitment::new(&[]).unwrap();
265
266 assert_eq!(component.schema_commitment, Word::empty());
267 }
268
269 #[test]
270 fn build_with_schema_commitment_adds_schema_commitment_component() {
271 let auth_component = AuthSingleSig::new(
272 PublicKeyCommitment::from(Word::empty()),
273 AuthScheme::EcdsaK256Keccak,
274 );
275
276 let account = AccountBuilder::new([1u8; 32])
277 .with_auth_component(auth_component)
278 .build_with_schema_commitment()
279 .unwrap();
280
281 assert_eq!(account.storage().num_slots(), 3);
284
285 assert!(account.storage().get_item(AuthSingleSig::public_key_slot()).is_ok());
287
288 let slot_name = AccountSchemaCommitment::schema_commitment_slot();
290 let commitment = account.storage().get_item(slot_name).unwrap();
291 assert_ne!(commitment, Word::empty());
292 }
293}