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