miden_standards/account/metadata/
schema_commitment.rs1use 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
24static 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
41pub struct AccountSchemaCommitment {
57 schema_commitment: Word,
58}
59
60impl AccountSchemaCommitment {
61 const NAME: &str = "miden::standards::metadata::storage_schema";
64
65 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 pub fn from_schema(storage_schema: &StorageSchema) -> Result<Self, ComponentMetadataError> {
82 Self::new(core::slice::from_ref(storage_schema))
83 }
84
85 pub fn schema_commitment_slot() -> &'static StorageSlotName {
87 &SCHEMA_COMMITMENT_SLOT_NAME
88 }
89
90 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
120pub trait AccountBuilderSchemaCommitmentExt {
126 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
149fn 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 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#[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 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 assert_eq!(account.storage().num_slots(), 3);
279
280 assert!(account.storage().get_item(AuthSingleSig::public_key_slot()).is_ok());
282
283 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}