Skip to main content

miden_protocol/account/component/
mod.rs

1use alloc::collections::BTreeSet;
2use alloc::vec::Vec;
3
4use miden_mast_package::{MastArtifact, Package};
5use miden_processor::mast::MastNodeExt;
6
7mod metadata;
8pub use metadata::*;
9
10pub mod storage;
11pub use storage::*;
12
13mod code;
14pub use code::AccountComponentCode;
15
16use crate::account::{AccountProcedureRoot, AccountType, StorageSlot};
17use crate::assembly::Path;
18use crate::errors::AccountError;
19use crate::{MastForest, Word};
20
21/// The attribute name used to mark the authentication procedure in an account component.
22const AUTH_SCRIPT_ATTRIBUTE: &str = "auth_script";
23
24// ACCOUNT COMPONENT
25// ================================================================================================
26
27/// An [`AccountComponent`] defines a [`Library`](miden_assembly::Library) of code and the initial
28/// value and types of the [`StorageSlot`]s it accesses.
29///
30/// One or more components can be used to built [`AccountCode`](crate::account::AccountCode) and
31/// [`AccountStorage`](crate::account::AccountStorage).
32///
33/// Each component is independent of other components and can only access its own storage slots.
34/// Each component defines its own storage layout starting at index 0 up to the length of the
35/// storage slots vector.
36///
37/// Components define the [`AccountType`]s they support, meaning whether the component can be used
38/// to instantiate an account of that type. For example, a component implementing a fungible faucet
39/// would only specify support for [`AccountType::FungibleFaucet`]. Using it to instantiate a
40/// regular account would fail. By default, the set of supported types is empty, so each component
41/// is forced to explicitly define what it supports.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct AccountComponent {
44    pub(super) code: AccountComponentCode,
45    pub(super) storage_slots: Vec<StorageSlot>,
46    pub(super) metadata: AccountComponentMetadata,
47}
48
49impl AccountComponent {
50    // CONSTRUCTORS
51    // --------------------------------------------------------------------------------------------
52
53    /// Returns a new [`AccountComponent`] constructed from the provided `library`,
54    /// `storage_slots`, and `metadata`.
55    ///
56    /// All procedures exported from the provided code will become members of the account's public
57    /// interface when added to an [`AccountCode`](crate::account::AccountCode).
58    ///
59    /// # Errors
60    ///
61    /// The following list of errors is exhaustive and can be relied upon for `expect`ing the call
62    /// to this function. It is recommended that custom components ensure these conditions by design
63    /// or in their fallible constructors.
64    ///
65    /// Returns an error if:
66    /// - The number of given [`StorageSlot`]s exceeds 255.
67    pub fn new(
68        code: impl Into<AccountComponentCode>,
69        storage_slots: Vec<StorageSlot>,
70        metadata: AccountComponentMetadata,
71    ) -> Result<Self, AccountError> {
72        // Check that we have less than 256 storage slots.
73        u8::try_from(storage_slots.len())
74            .map_err(|_| AccountError::StorageTooManySlots(storage_slots.len() as u64))?;
75
76        Ok(Self {
77            code: code.into(),
78            storage_slots,
79            metadata,
80        })
81    }
82
83    /// Creates an [`AccountComponent`] from a [`Package`] using [`InitStorageData`].
84    ///
85    /// This method provides type safety by leveraging the component's metadata to validate
86    /// storage initialization data. The package must contain explicit account component metadata.
87    ///
88    /// # Arguments
89    ///
90    /// * `package` - The package containing the [`Library`](miden_assembly::Library) and account
91    ///   component metadata
92    /// * `init_storage_data` - The initialization data for storage slots
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if:
97    /// - The package does not contain a library artifact
98    /// - The package does not contain account component metadata
99    /// - The metadata cannot be deserialized from the package
100    /// - The storage initialization fails due to invalid or missing data
101    /// - The component creation fails
102    pub fn from_package(
103        package: &Package,
104        init_storage_data: &InitStorageData,
105    ) -> Result<Self, AccountError> {
106        let metadata = AccountComponentMetadata::try_from(package)?;
107        let library = match &package.mast {
108            MastArtifact::Library(library) => library.as_ref().clone(),
109            MastArtifact::Executable(_) => {
110                return Err(AccountError::other(
111                    "expected Package to contain a library, but got an executable",
112                ));
113            },
114        };
115
116        let component_code = AccountComponentCode::from(library);
117        Self::from_library(&component_code, &metadata, init_storage_data)
118    }
119
120    /// Creates an [`AccountComponent`] from an [`AccountComponentCode`] and
121    /// [`AccountComponentMetadata`].
122    ///
123    /// This method provides type safety by leveraging the component's metadata to validate
124    /// the passed storage initialization data ([`InitStorageData`]).
125    ///
126    /// # Arguments
127    ///
128    /// * `library` - The component's assembled code
129    /// * `metadata` - The component's metadata, which describes the storage layout
130    /// * `init_storage_data` - The initialization data for storage slots
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if:
135    /// - The package does not contain a library artifact
136    /// - The package does not contain account component metadata
137    /// - The metadata cannot be deserialized from the package
138    /// - The storage initialization fails due to invalid or missing data
139    /// - The component creation fails
140    pub fn from_library(
141        library: &AccountComponentCode,
142        metadata: &AccountComponentMetadata,
143        init_storage_data: &InitStorageData,
144    ) -> Result<Self, AccountError> {
145        let storage_slots = metadata
146            .storage_schema()
147            .build_storage_slots(init_storage_data)
148            .map_err(|err| {
149                AccountError::other_with_source("failed to instantiate account component", err)
150            })?;
151
152        AccountComponent::new(library.clone(), storage_slots, metadata.clone())
153    }
154
155    // ACCESSORS
156    // --------------------------------------------------------------------------------------------
157
158    /// Returns the number of storage slots accessible from this component.
159    pub fn storage_size(&self) -> u8 {
160        u8::try_from(self.storage_slots.len())
161            .expect("storage slots len should fit in u8 per the constructor")
162    }
163
164    /// Returns a reference to the underlying [`AccountComponentCode`] of this component.
165    pub fn component_code(&self) -> &AccountComponentCode {
166        &self.code
167    }
168
169    /// Returns a reference to the underlying [`MastForest`] of this component.
170    pub fn mast_forest(&self) -> &MastForest {
171        self.code.mast_forest()
172    }
173
174    /// Returns a slice of the underlying [`StorageSlot`]s of this component.
175    pub fn storage_slots(&self) -> &[StorageSlot] {
176        self.storage_slots.as_slice()
177    }
178
179    /// Returns the component metadata.
180    pub fn metadata(&self) -> &AccountComponentMetadata {
181        &self.metadata
182    }
183
184    /// Returns the storage schema associated with this component.
185    pub fn storage_schema(&self) -> &StorageSchema {
186        self.metadata.storage_schema()
187    }
188
189    /// Returns a reference to the supported [`AccountType`]s.
190    pub fn supported_types(&self) -> &BTreeSet<AccountType> {
191        self.metadata.supported_types()
192    }
193
194    /// Returns `true` if this component supports the given `account_type`, `false` otherwise.
195    pub fn supports_type(&self, account_type: AccountType) -> bool {
196        self.metadata.supported_types().contains(&account_type)
197    }
198
199    /// Returns an iterator over ([`AccountProcedureRoot`], is_auth) for all procedures in this
200    /// component.
201    ///
202    /// A procedure is considered an authentication procedure if it has the `@auth_script`
203    /// attribute.
204    pub fn procedures(&self) -> impl Iterator<Item = (AccountProcedureRoot, bool)> + '_ {
205        let library = self.code.as_library();
206        library.exports().filter_map(|export| {
207            export.as_procedure().map(|proc_export| {
208                let digest = library
209                    .mast_forest()
210                    .get_node_by_id(proc_export.node)
211                    .expect("export node not in the forest")
212                    .digest();
213                let is_auth = proc_export.attributes.has(AUTH_SCRIPT_ATTRIBUTE);
214                (AccountProcedureRoot::from_raw(digest), is_auth)
215            })
216        })
217    }
218
219    /// Returns the digest of the procedure with the specified path, or `None` if it was not found
220    /// in this component's library or its library path is malformed.
221    pub fn get_procedure_root_by_path(&self, proc_name: impl AsRef<Path>) -> Option<Word> {
222        self.code.as_library().get_procedure_root_by_path(proc_name)
223    }
224}
225
226impl From<AccountComponent> for AccountComponentCode {
227    fn from(component: AccountComponent) -> Self {
228        component.code
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use alloc::string::ToString;
235    use alloc::sync::Arc;
236
237    use miden_assembly::Assembler;
238    use miden_mast_package::{
239        MastArtifact,
240        Package,
241        PackageKind,
242        PackageManifest,
243        Section,
244        SectionId,
245    };
246    use semver::Version;
247
248    use super::*;
249    use crate::testing::account_code::CODE;
250    use crate::utils::serde::Serializable;
251
252    #[test]
253    fn test_extract_metadata_from_package() {
254        // Create a simple library for testing
255        let library = Assembler::default().assemble_library([CODE]).unwrap();
256
257        // Test with metadata
258        let metadata = AccountComponentMetadata::new(
259            "test_component",
260            [AccountType::RegularAccountImmutableCode],
261        )
262        .with_description("A test component")
263        .with_version(Version::new(1, 0, 0));
264
265        let metadata_bytes = metadata.to_bytes();
266        let package_with_metadata = Package {
267            name: "test_package".to_string(),
268            mast: MastArtifact::Library(Arc::new(library.clone())),
269            manifest: PackageManifest::new(None),
270            kind: PackageKind::AccountComponent,
271            sections: vec![Section::new(
272                SectionId::ACCOUNT_COMPONENT_METADATA,
273                metadata_bytes.clone(),
274            )],
275            version: Default::default(),
276            description: None,
277        };
278
279        let extracted_metadata =
280            AccountComponentMetadata::try_from(&package_with_metadata).unwrap();
281        assert_eq!(extracted_metadata.name(), "test_component");
282        assert!(
283            extracted_metadata
284                .supported_types()
285                .contains(&AccountType::RegularAccountImmutableCode)
286        );
287
288        // Test without metadata - should fail
289        let package_without_metadata = Package {
290            name: "test_package_no_metadata".to_string(),
291            mast: MastArtifact::Library(Arc::new(library)),
292            manifest: PackageManifest::new(None),
293            kind: PackageKind::AccountComponent,
294            sections: vec![], // No metadata section
295            version: Default::default(),
296            description: None,
297        };
298
299        let result = AccountComponentMetadata::try_from(&package_without_metadata);
300        assert!(result.is_err());
301        let error_msg = result.unwrap_err().to_string();
302        assert!(error_msg.contains("package does not contain account component metadata"));
303    }
304
305    #[test]
306    fn test_from_library_with_init_data() {
307        // Create a simple library for testing
308        let library = Assembler::default().assemble_library([CODE]).unwrap();
309        let component_code = AccountComponentCode::from(library.clone());
310
311        // Create metadata for the component
312        let metadata = AccountComponentMetadata::new("test_component", AccountType::regular())
313            .with_description("A test component")
314            .with_version(Version::new(1, 0, 0));
315
316        // Test with empty init data - this tests the complete workflow:
317        // Library + Metadata -> AccountComponent
318        let init_data = InitStorageData::default();
319        let component =
320            AccountComponent::from_library(&component_code, &metadata, &init_data).unwrap();
321
322        // Verify the component was created correctly
323        assert_eq!(component.storage_size(), 0);
324        assert!(component.supports_type(AccountType::RegularAccountImmutableCode));
325        assert!(component.supports_type(AccountType::RegularAccountUpdatableCode));
326        assert!(!component.supports_type(AccountType::FungibleFaucet));
327
328        // Test without metadata - should fail
329        let package_without_metadata = Package {
330            name: "test_package_no_metadata".to_string(),
331            mast: MastArtifact::Library(Arc::new(library)),
332            kind: PackageKind::AccountComponent,
333            manifest: PackageManifest::new(None),
334            sections: vec![], // No metadata section
335            version: Default::default(),
336            description: None,
337        };
338
339        let result = AccountComponent::from_package(&package_without_metadata, &init_data);
340        assert!(result.is_err());
341        let error_msg = result.unwrap_err().to_string();
342        assert!(error_msg.contains("package does not contain account component metadata"));
343    }
344}