Skip to main content

miden_protocol/account/component/
mod.rs

1use alloc::vec::Vec;
2
3use miden_mast_package::Package;
4use miden_processor::mast::MastNodeExt;
5
6mod metadata;
7pub use metadata::*;
8
9pub mod storage;
10pub use storage::*;
11
12mod code;
13pub use code::AccountComponentCode;
14
15use crate::MastForest;
16use crate::account::{AccountProcedureRoot, StorageSlot};
17use crate::assembly::Path;
18use crate::errors::AccountError;
19
20/// The attribute name used to mark the authentication procedure in an account component.
21const AUTH_SCRIPT_ATTRIBUTE: &str = "auth_script";
22
23/// The attribute name used to mark a procedure as a member of an account component's interface.
24const ACCOUNT_PROCEDURE_ATTRIBUTE: &str = "account_procedure";
25
26// ACCOUNT COMPONENT
27// ================================================================================================
28
29/// An [`AccountComponent`] defines a [`Package`] of code and the initial value and types of the
30/// [`StorageSlot`]s it accesses.
31///
32/// One or more components can be used to build [`AccountCode`](crate::account::AccountCode) and
33/// [`AccountStorage`](crate::account::AccountStorage).
34///
35/// Each component is independent of other components and can only access its own storage slots.
36/// Each component defines its own storage layout starting at index 0 up to the length of the
37/// storage slots vector.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct AccountComponent {
40    pub(super) code: AccountComponentCode,
41    pub(super) storage_slots: Vec<StorageSlot>,
42    pub(super) metadata: AccountComponentMetadata,
43}
44
45impl AccountComponent {
46    // CONSTRUCTORS
47    // --------------------------------------------------------------------------------------------
48
49    /// Returns a new [`AccountComponent`] constructed from the provided `code`,
50    /// `storage_slots`, and `metadata`.
51    ///
52    /// Procedures exported from the provided code that are marked with the `@account_procedure`
53    /// attribute or with `@auth_script` will become members of the account's public interface when
54    /// added to an [`AccountCode`](crate::account::AccountCode).
55    ///
56    /// # Errors
57    ///
58    /// The following list of errors is exhaustive and can be relied upon for `expect`ing the call
59    /// to this function. It is recommended that custom components ensure these conditions by design
60    /// or in their fallible constructors.
61    ///
62    /// Returns an error if:
63    /// - The number of given [`StorageSlot`]s exceeds 255.
64    pub fn new(
65        code: impl Into<AccountComponentCode>,
66        storage_slots: Vec<StorageSlot>,
67        metadata: AccountComponentMetadata,
68    ) -> Result<Self, AccountError> {
69        // Check that we have less than 256 storage slots.
70        u8::try_from(storage_slots.len())
71            .map_err(|_| AccountError::StorageTooManySlots(storage_slots.len() as u64))?;
72
73        Ok(Self {
74            code: code.into(),
75            storage_slots,
76            metadata,
77        })
78    }
79
80    /// Creates an [`AccountComponent`] from a [`Package`] using [`InitStorageData`].
81    ///
82    /// This method provides type safety by leveraging the component's metadata to validate
83    /// storage initialization data. The package must contain explicit account component metadata.
84    ///
85    /// # Arguments
86    ///
87    /// * `package` - The package containing the account component metadata
88    /// * `init_storage_data` - The initialization data for storage slots
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if:
93    /// - The package does not contain account component metadata
94    /// - The metadata cannot be deserialized from the package
95    /// - The storage initialization fails due to invalid or missing data
96    /// - The component creation fails
97    pub fn from_package(
98        package: &Package,
99        init_storage_data: &InitStorageData,
100    ) -> Result<Self, AccountError> {
101        let metadata = AccountComponentMetadata::try_from(package)?;
102        let component_code = AccountComponentCode::from(package.clone());
103
104        let storage_slots = metadata
105            .storage_schema()
106            .build_storage_slots(init_storage_data)
107            .map_err(|err| {
108                AccountError::other_with_source("failed to instantiate account component", err)
109            })?;
110
111        AccountComponent::new(component_code, storage_slots, metadata)
112    }
113
114    // ACCESSORS
115    // --------------------------------------------------------------------------------------------
116
117    /// Returns the number of storage slots accessible from this component.
118    pub fn storage_size(&self) -> u8 {
119        u8::try_from(self.storage_slots.len())
120            .expect("storage slots len should fit in u8 per the constructor")
121    }
122
123    /// Returns a reference to the underlying [`AccountComponentCode`] of this component.
124    pub fn component_code(&self) -> &AccountComponentCode {
125        &self.code
126    }
127
128    /// Returns a reference to the underlying [`MastForest`] of this component.
129    pub fn mast_forest(&self) -> &MastForest {
130        self.code.mast_forest()
131    }
132
133    /// Returns a slice of the underlying [`StorageSlot`]s of this component.
134    pub fn storage_slots(&self) -> &[StorageSlot] {
135        self.storage_slots.as_slice()
136    }
137
138    /// Returns the component metadata.
139    pub fn metadata(&self) -> &AccountComponentMetadata {
140        &self.metadata
141    }
142
143    /// Returns the storage schema associated with this component.
144    pub fn storage_schema(&self) -> &StorageSchema {
145        self.metadata.storage_schema()
146    }
147
148    /// Returns an iterator over ([`AccountProcedureRoot`], is_auth) for all interface procedures
149    /// in this component.
150    ///
151    /// A procedure is considered an authentication procedure if it has the `@auth_script`
152    /// attribute. A procedure is part of the component interface if it has either the
153    /// `@account_procedure` or `@auth_script` attributes.
154    pub fn procedures(&self) -> impl Iterator<Item = (AccountProcedureRoot, bool)> + '_ {
155        self.code.exports().map(|proc_export| {
156            // When the export has a node id, use the forest node digest as the source of truth.
157            // This keeps procedure roots tied to the actual component MAST forest.
158            let digest = if let Some(node) = proc_export.node {
159                self.code
160                    .mast_forest()
161                    .get_node_by_id(node)
162                    .expect("export node not in the forest")
163                    .digest()
164            } else {
165                proc_export.digest
166            };
167            let is_auth = proc_export.attributes.has(AUTH_SCRIPT_ATTRIBUTE);
168            (AccountProcedureRoot::from_raw(digest), is_auth)
169        })
170    }
171
172    /// Returns the [`AccountProcedureRoot`] of the procedure with the specified path, or `None`
173    /// if it was not found in this component's code.
174    pub fn get_procedure_root_by_path(
175        &self,
176        proc_name: impl AsRef<Path>,
177    ) -> Option<AccountProcedureRoot> {
178        self.code.get_procedure_root_by_path(proc_name)
179    }
180
181    /// Returns `true` if `root` is the procedure root of any procedure exported by this
182    /// component.
183    pub fn has_procedure(&self, root: AccountProcedureRoot) -> bool {
184        self.procedures().any(|(proc_root, _)| proc_root == root)
185    }
186
187    /// Returns `true` if this component exports an authentication procedure (a procedure marked
188    /// with the `@auth_script` attribute).
189    pub fn is_auth_component(&self) -> bool {
190        self.procedures().any(|(_, is_auth)| is_auth)
191    }
192}
193
194impl From<AccountComponent> for AccountComponentCode {
195    fn from(component: AccountComponent) -> Self {
196        component.code
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use alloc::string::ToString;
203
204    use miden_mast_package::{Section, SectionId};
205    use semver::Version;
206
207    use super::*;
208    use crate::testing::account_code::CODE;
209    use crate::testing::assembler::assemble_test_package;
210    use crate::utils::serde::Serializable;
211
212    #[test]
213    fn test_extract_metadata_from_package() {
214        // Create a simple package for testing
215        let package =
216            assemble_test_package("test-extract-metadata", "test::extract_metadata", CODE);
217
218        // Test with metadata
219        let metadata = AccountComponentMetadata::new("test_component")
220            .with_description("A test component")
221            .with_version(Version::new(1, 0, 0));
222
223        let metadata_bytes = metadata.to_bytes();
224        let mut package_with_metadata = package.clone();
225        package_with_metadata
226            .sections
227            .push(Section::new(SectionId::ACCOUNT_COMPONENT_METADATA, metadata_bytes.clone()));
228
229        let extracted_metadata =
230            AccountComponentMetadata::try_from(&package_with_metadata).unwrap();
231        assert_eq!(extracted_metadata.name(), "test_component");
232
233        // Test without metadata - should fail
234        let package_without_metadata = package;
235
236        let result = AccountComponentMetadata::try_from(&package_without_metadata);
237        assert!(result.is_err());
238        let error_msg = result.unwrap_err().to_string();
239        assert!(error_msg.contains("package does not contain account component metadata"));
240    }
241
242    #[test]
243    fn test_from_package_with_init_data() {
244        // Create a simple package for testing
245        let package =
246            assemble_test_package("test-from-package-init-data", "test::from_package", CODE);
247
248        // Create metadata for the component and embed it into the package
249        let metadata = AccountComponentMetadata::new("test_component")
250            .with_description("A test component")
251            .with_version(Version::new(1, 0, 0));
252
253        let mut package_with_metadata = package.clone();
254        package_with_metadata
255            .sections
256            .push(Section::new(SectionId::ACCOUNT_COMPONENT_METADATA, metadata.to_bytes()));
257
258        // Test with empty init data - this tests the complete workflow:
259        // Package -> AccountComponent
260        let init_data = InitStorageData::default();
261        let component = AccountComponent::from_package(&package_with_metadata, &init_data).unwrap();
262
263        // Verify the component was created correctly
264        assert_eq!(component.storage_size(), 0);
265
266        // Test without metadata - should fail
267        let result = AccountComponent::from_package(&package, &init_data);
268        assert!(result.is_err());
269        let error_msg = result.unwrap_err().to_string();
270        assert!(error_msg.contains("package does not contain account component metadata"));
271    }
272}