miden_standards/account/inspection/
schema_commitment.rs1use 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
32account_component_code!(
36 SCHEMA_COMMITMENT_CODE,
37 "miden-standards-inspection-schema-commitment.masp"
38);
39
40static 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
46pub struct AccountSchemaCommitment {
62 schema_commitment: Word,
63}
64
65impl AccountSchemaCommitment {
66 const NAME: &str = "miden::standards::inspection::storage_schema";
69 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 pub fn from_schema(storage_schema: &StorageSchema) -> Result<Self, ComponentMetadataError> {
86 Self::new(core::slice::from_ref(storage_schema))
87 }
88
89 pub fn schema_commitment_slot() -> &'static StorageSlotName {
91 &SCHEMA_COMMITMENT_SLOT_NAME
92 }
93
94 pub fn code() -> &'static AccountComponentCode {
96 &SCHEMA_COMMITMENT_CODE
97 }
98
99 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
129pub trait AccountBuilderSchemaCommitmentExt {
135 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
158fn 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 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#[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 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 assert_eq!(account.storage().num_slots(), 3);
286
287 assert!(account.storage().get_item(AuthSingleSig::public_key_slot()).is_ok());
289
290 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}