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 [`Library`](crate::assembly::Library) of code and the initial
30/// value and types of the [`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 `library`,
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 [`Library`](crate::assembly::Library) and account
88 /// component metadata
89 /// * `init_storage_data` - The initialization data for storage slots
90 ///
91 /// # Errors
92 ///
93 /// Returns an error if:
94 /// - The package does not contain a library artifact
95 /// - The package does not contain account component metadata
96 /// - The metadata cannot be deserialized from the package
97 /// - The storage initialization fails due to invalid or missing data
98 /// - The component creation fails
99 pub fn from_package(
100 package: &Package,
101 init_storage_data: &InitStorageData,
102 ) -> Result<Self, AccountError> {
103 let metadata = AccountComponentMetadata::try_from(package)?;
104 let library = package.clone();
105
106 let component_code = AccountComponentCode::from(library);
107 Self::from_library(&component_code, &metadata, init_storage_data)
108 }
109
110 /// Creates an [`AccountComponent`] from an [`AccountComponentCode`] and
111 /// [`AccountComponentMetadata`].
112 ///
113 /// This method provides type safety by leveraging the component's metadata to validate
114 /// the passed storage initialization data ([`InitStorageData`]).
115 ///
116 /// # Arguments
117 ///
118 /// * `library` - The component's assembled code
119 /// * `metadata` - The component's metadata, which describes the storage layout
120 /// * `init_storage_data` - The initialization data for storage slots
121 ///
122 /// # Errors
123 ///
124 /// Returns an error if:
125 /// - The package does not contain a library artifact
126 /// - The package does not contain account component metadata
127 /// - The metadata cannot be deserialized from the package
128 /// - The storage initialization fails due to invalid or missing data
129 /// - The component creation fails
130 pub fn from_library(
131 library: &AccountComponentCode,
132 metadata: &AccountComponentMetadata,
133 init_storage_data: &InitStorageData,
134 ) -> Result<Self, AccountError> {
135 let storage_slots = metadata
136 .storage_schema()
137 .build_storage_slots(init_storage_data)
138 .map_err(|err| {
139 AccountError::other_with_source("failed to instantiate account component", err)
140 })?;
141
142 AccountComponent::new(library.clone(), storage_slots, metadata.clone())
143 }
144
145 // ACCESSORS
146 // --------------------------------------------------------------------------------------------
147
148 /// Returns the number of storage slots accessible from this component.
149 pub fn storage_size(&self) -> u8 {
150 u8::try_from(self.storage_slots.len())
151 .expect("storage slots len should fit in u8 per the constructor")
152 }
153
154 /// Returns a reference to the underlying [`AccountComponentCode`] of this component.
155 pub fn component_code(&self) -> &AccountComponentCode {
156 &self.code
157 }
158
159 /// Returns a reference to the underlying [`MastForest`] of this component.
160 pub fn mast_forest(&self) -> &MastForest {
161 self.code.mast_forest()
162 }
163
164 /// Returns a slice of the underlying [`StorageSlot`]s of this component.
165 pub fn storage_slots(&self) -> &[StorageSlot] {
166 self.storage_slots.as_slice()
167 }
168
169 /// Returns the component metadata.
170 pub fn metadata(&self) -> &AccountComponentMetadata {
171 &self.metadata
172 }
173
174 /// Returns the storage schema associated with this component.
175 pub fn storage_schema(&self) -> &StorageSchema {
176 self.metadata.storage_schema()
177 }
178
179 /// Returns an iterator over ([`AccountProcedureRoot`], is_auth) for all interface procedures
180 /// in this component.
181 ///
182 /// A procedure is considered an authentication procedure if it has the `@auth_script`
183 /// attribute. A procedure is part of the component interface if it has either the
184 /// `@account_procedure` or `@auth_script` attributes.
185 pub fn procedures(&self) -> impl Iterator<Item = (AccountProcedureRoot, bool)> + '_ {
186 self.code.exports().map(|proc_export| {
187 // When the export has a node id, use the forest node digest as the source of truth.
188 // This keeps procedure roots tied to the actual component MAST forest.
189 let digest = if let Some(node) = proc_export.node {
190 self.code
191 .mast_forest()
192 .get_node_by_id(node)
193 .expect("export node not in the forest")
194 .digest()
195 } else {
196 proc_export.digest
197 };
198 let is_auth = proc_export.attributes.has(AUTH_SCRIPT_ATTRIBUTE);
199 (AccountProcedureRoot::from_raw(digest), is_auth)
200 })
201 }
202
203 /// Returns the [`AccountProcedureRoot`] of the procedure with the specified path, or `None`
204 /// if it was not found in this component's library.
205 pub fn get_procedure_root_by_path(
206 &self,
207 proc_name: impl AsRef<Path>,
208 ) -> Option<AccountProcedureRoot> {
209 self.code.get_procedure_root_by_path(proc_name)
210 }
211
212 /// Returns `true` if `root` is the procedure root of any procedure exported by this
213 /// component.
214 pub fn has_procedure(&self, root: AccountProcedureRoot) -> bool {
215 self.procedures().any(|(proc_root, _)| proc_root == root)
216 }
217}
218
219impl From<AccountComponent> for AccountComponentCode {
220 fn from(component: AccountComponent) -> Self {
221 component.code
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use alloc::string::ToString;
228
229 use miden_mast_package::{Section, SectionId};
230 use semver::Version;
231
232 use super::*;
233 use crate::testing::account_code::CODE;
234 use crate::testing::assembler::assemble_test_library;
235 use crate::utils::serde::Serializable;
236
237 #[test]
238 fn test_extract_metadata_from_package() {
239 // Create a simple library for testing
240 let library =
241 assemble_test_library("test-extract-metadata", "test::extract_metadata", CODE);
242
243 // Test with metadata
244 let metadata = AccountComponentMetadata::new("test_component")
245 .with_description("A test component")
246 .with_version(Version::new(1, 0, 0));
247
248 let metadata_bytes = metadata.to_bytes();
249 let mut package_with_metadata = library.clone();
250 package_with_metadata
251 .sections
252 .push(Section::new(SectionId::ACCOUNT_COMPONENT_METADATA, metadata_bytes.clone()));
253
254 let extracted_metadata =
255 AccountComponentMetadata::try_from(&package_with_metadata).unwrap();
256 assert_eq!(extracted_metadata.name(), "test_component");
257
258 // Test without metadata - should fail
259 let package_without_metadata = library;
260
261 let result = AccountComponentMetadata::try_from(&package_without_metadata);
262 assert!(result.is_err());
263 let error_msg = result.unwrap_err().to_string();
264 assert!(error_msg.contains("package does not contain account component metadata"));
265 }
266
267 #[test]
268 fn test_from_library_with_init_data() {
269 // Create a simple library for testing
270 let library =
271 assemble_test_library("test-from-library-init-data", "test::from_library", CODE);
272 let component_code = AccountComponentCode::from(library.clone());
273
274 // Create metadata for the component
275 let metadata = AccountComponentMetadata::new("test_component")
276 .with_description("A test component")
277 .with_version(Version::new(1, 0, 0));
278
279 // Test with empty init data - this tests the complete workflow:
280 // Library + Metadata -> AccountComponent
281 let init_data = InitStorageData::default();
282 let component =
283 AccountComponent::from_library(&component_code, &metadata, &init_data).unwrap();
284
285 // Verify the component was created correctly
286 assert_eq!(component.storage_size(), 0);
287
288 // Test without metadata - should fail
289 let package_without_metadata = library;
290
291 let result = AccountComponent::from_package(&package_without_metadata, &init_data);
292 assert!(result.is_err());
293 let error_msg = result.unwrap_err().to_string();
294 assert!(error_msg.contains("package does not contain account component metadata"));
295 }
296}