miden_objects/account/component/template/
mod.rs

1use alloc::{
2    collections::{btree_map::Entry, BTreeMap, BTreeSet},
3    string::{String, ToString},
4    vec::Vec,
5};
6use core::str::FromStr;
7
8use assembly::Library;
9use semver::Version;
10use vm_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
11use vm_processor::DeserializationError;
12
13use super::AccountType;
14use crate::errors::AccountComponentTemplateError;
15
16mod storage;
17pub use storage::*;
18
19// ACCOUNT COMPONENT TEMPLATE
20// ================================================================================================
21
22/// Represents a template containing a component's metadata and its associated library.
23///
24/// The [AccountComponentTemplate] encapsulates all necessary information to initialize and manage
25/// an account component within the system. It includes the configuration details and the compiled
26/// library code required for the component's operation.
27///
28/// A template can be instantiated into [AccountComponent](super::AccountComponent) objects.
29/// The component metadata can be defined with placeholders that can be replaced at instantiation
30/// time.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct AccountComponentTemplate {
33    /// The component's metadata. This describes the component and how the storage is laid out,
34    /// alongside how storage values are initialized.
35    metadata: AccountComponentMetadata,
36    /// The account component's assembled code. This defines all functionality related to the
37    /// component.
38    library: Library,
39}
40
41impl AccountComponentTemplate {
42    /// Creates a new [AccountComponentTemplate].
43    ///
44    /// This template holds everything needed to describe and implement a component, including the
45    /// compiled procedures (via the [Library]) and the metadata that defines the component’s
46    /// storage layout ([AccountComponentMetadata]). The metadata can include storage placeholders
47    /// that get filled in at the time of the [AccountComponent](super::AccountComponent)
48    /// instantiation.
49    pub fn new(metadata: AccountComponentMetadata, library: Library) -> Self {
50        Self { metadata, library }
51    }
52
53    /// Returns a reference to the template's [AccountComponentMetadata].
54    pub fn metadata(&self) -> &AccountComponentMetadata {
55        &self.metadata
56    }
57
58    /// Returns a reference to the underlying [Library] of this component.
59    pub fn library(&self) -> &Library {
60        &self.library
61    }
62}
63
64impl Serializable for AccountComponentTemplate {
65    fn write_into<W: vm_core::utils::ByteWriter>(&self, target: &mut W) {
66        target.write(&self.metadata);
67        target.write(&self.library);
68    }
69}
70
71impl Deserializable for AccountComponentTemplate {
72    fn read_from<R: vm_core::utils::ByteReader>(
73        source: &mut R,
74    ) -> Result<Self, vm_processor::DeserializationError> {
75        // Read and deserialize the configuration from a TOML string.
76        let metadata: AccountComponentMetadata = source.read()?;
77        let library = Library::read_from(source)?;
78
79        Ok(AccountComponentTemplate::new(metadata, library))
80    }
81}
82
83// ACCOUNT COMPONENT METADATA
84// ================================================================================================
85
86/// Represents the full component template configuration.
87///
88/// An account component metadata describes the component alongside its storage layout.
89/// On the storage layout, [placeholders](StoragePlaceholder) can be utilized to identify
90/// [values](StorageValue) that should be provided at the moment of instantiation.
91///
92/// When the `std` feature is enabled, this struct allows for serialization and deserialization to
93/// and from a TOML file.
94///
95/// # Guarantees
96///
97/// - The metadata's storage layout does not contain duplicate slots, and it always starts at slot
98///   index 0.
99/// - Storage slots are laid out in a contiguous manner.
100/// - Storage placeholders can appear multiple times, but only if the expected [StorageValue] is of
101///   the same type in all instances. The expected placeholders can be retrieved with
102///   [AccountComponentMetadata::get_unique_storage_placeholders()], which returns a map from
103///   [StoragePlaceholder] to [PlaceholderType] (which, in turn, indicates the expected value type
104///   for the placeholder).
105///
106/// # Example
107///
108/// ```
109/// # use semver::Version;
110/// # use std::collections::BTreeSet;
111/// # use miden_objects::{testing::account_code::CODE, account::{
112/// #     AccountComponent, AccountComponentMetadata, InitStorageData, StorageEntry,
113/// #     StoragePlaceholder, StorageValue,
114/// #     AccountComponentTemplate, FeltRepresentation, WordRepresentation},
115/// #     assembly::Assembler, Felt};
116/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
117/// let first_felt = FeltRepresentation::Decimal(Felt::new(0u64));
118/// let second_felt = FeltRepresentation::Decimal(Felt::new(1u64));
119/// let third_felt = FeltRepresentation::Decimal(Felt::new(2u64));
120/// // Templated element:
121/// let last_element = FeltRepresentation::Template(StoragePlaceholder::new("foo")?);
122///
123/// let storage_entry = StorageEntry::new_value(
124///     "test-entry",
125///     Some("a test entry"),
126///     0,
127///     WordRepresentation::Array([first_felt, second_felt, third_felt, last_element]),
128/// );
129///
130/// let init_storage_data = InitStorageData::new([(
131///     StoragePlaceholder::new("foo")?,
132///     StorageValue::Felt(Felt::new(300u64)),
133/// )]);
134///
135/// let component_template = AccountComponentMetadata::new(
136///     "test name".into(),
137///     "description of the component".into(),
138///     Version::parse("0.1.0")?,
139///     BTreeSet::new(),
140///     vec![],
141/// )?;
142///
143/// let library = Assembler::default().assemble_library([CODE]).unwrap();
144/// let template = AccountComponentTemplate::new(component_template, library);
145///
146/// let component = AccountComponent::from_template(&template, &init_storage_data)?;
147/// # Ok(())
148/// # }
149/// ```
150#[derive(Debug, Clone, PartialEq, Eq)]
151#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
152pub struct AccountComponentMetadata {
153    /// The human-readable name of the component.
154    name: String,
155
156    /// A brief description of what this component is and how it works.
157    description: String,
158
159    /// The version of the component using semantic versioning.
160    /// This can be used to track and manage component upgrades.
161    version: Version,
162
163    /// A set of supported target account types for this component.
164    targets: BTreeSet<AccountType>,
165
166    /// A list of storage entries defining the component's storage layout and initialization
167    /// values.
168    storage: Vec<StorageEntry>,
169}
170
171impl AccountComponentMetadata {
172    /// Create a new [AccountComponentMetadata].
173    ///
174    /// # Errors
175    ///
176    /// - If the specified storage slots contain duplicates.
177    /// - If the slot numbers do not start at zero.
178    /// - If the slots are not contiguous.
179    pub fn new(
180        name: String,
181        description: String,
182        version: Version,
183        targets: BTreeSet<AccountType>,
184        storage: Vec<StorageEntry>,
185    ) -> Result<Self, AccountComponentTemplateError> {
186        let component = Self {
187            name,
188            description,
189            version,
190            targets,
191            storage,
192        };
193        component.validate()?;
194        Ok(component)
195    }
196
197    /// Retrieves a map of unique storage placeholders mapped to their expected type that require
198    /// a value at the moment of component instantiation.
199    ///
200    /// These values will be used for
201    /// initializing storage slot values, or storage map entries. For a full example on how a
202    /// placeholder may be utilized, please refer to the docs for [AccountComponentMetadata].
203    ///
204    /// Types for the returned storage placeholders are inferred based on their location in the
205    /// storage layout structure.
206    pub fn get_unique_storage_placeholders(&self) -> BTreeMap<StoragePlaceholder, PlaceholderType> {
207        let mut placeholder_map = BTreeMap::new();
208        for storage_entry in &self.storage {
209            for (placeholder, placeholder_type) in storage_entry.all_placeholders_iter() {
210                // The constructors of this type guarantee each placeholder has the same type, so
211                // reinserting them multiple times is fine.
212                placeholder_map.insert(placeholder.clone(), placeholder_type);
213            }
214        }
215        placeholder_map
216    }
217
218    /// Returns the name of the account component.
219    pub fn name(&self) -> &str {
220        &self.name
221    }
222
223    /// Returns the description of the account component.
224    pub fn description(&self) -> &str {
225        &self.description
226    }
227
228    /// Returns the semantic version of the account component.
229    pub fn version(&self) -> &Version {
230        &self.version
231    }
232
233    /// Returns the account types supported by the component.
234    pub fn targets(&self) -> &BTreeSet<AccountType> {
235        &self.targets
236    }
237
238    /// Returns the list of storage entries of the component.
239    pub fn storage_entries(&self) -> &Vec<StorageEntry> {
240        &self.storage
241    }
242
243    /// Validate the [AccountComponentMetadata].
244    ///
245    /// # Errors
246    ///
247    /// - If the specified storage slots contain duplicates.
248    /// - If the template contains multiple storage placeholders of different type.
249    /// - If the slot numbers do not start at zero.
250    /// - If the slots are not contiguous.
251    fn validate(&self) -> Result<(), AccountComponentTemplateError> {
252        let mut all_slots: Vec<u8> = self
253            .storage
254            .iter()
255            .flat_map(|entry| entry.slot_indices().iter().copied())
256            .collect();
257
258        // Check that slots start at 0 and are contiguous
259        all_slots.sort_unstable();
260        if let Some(&first_slot) = all_slots.first() {
261            if first_slot != 0 {
262                return Err(AccountComponentTemplateError::StorageSlotsDoNotStartAtZero(
263                    first_slot,
264                ));
265            }
266        }
267
268        for slots in all_slots.windows(2) {
269            if slots[1] == slots[0] {
270                return Err(AccountComponentTemplateError::DuplicateSlot(slots[0]));
271            }
272
273            if slots[1] != slots[0] + 1 {
274                return Err(AccountComponentTemplateError::NonContiguousSlots(slots[0], slots[1]));
275            }
276        }
277
278        // Check that placeholders do not appear more than once with a different type
279        let mut placeholders = BTreeMap::new();
280        for storage_entry in &self.storage {
281            for (placeholder, placeholder_type) in storage_entry.all_placeholders_iter() {
282                match placeholders.entry(placeholder.clone()) {
283                    Entry::Occupied(entry) => {
284                        // if already exists, make sure it's the same type
285                        if *entry.get() != placeholder_type {
286                            return Err(
287                                AccountComponentTemplateError::StoragePlaceholderTypeMismatch(
288                                    placeholder.clone(),
289                                    *entry.get(),
290                                    placeholder_type,
291                                ),
292                            );
293                        }
294                    },
295                    Entry::Vacant(slot) => {
296                        slot.insert(placeholder_type);
297                    },
298                }
299            }
300        }
301
302        for entry in self.storage_entries() {
303            entry.validate()?;
304        }
305
306        Ok(())
307    }
308}
309
310// SERIALIZATION
311// ================================================================================================
312
313impl Serializable for AccountComponentMetadata {
314    fn write_into<W: ByteWriter>(&self, target: &mut W) {
315        self.name.write_into(target);
316        self.description.write_into(target);
317        self.version.to_string().write_into(target);
318        self.targets.write_into(target);
319        self.storage.write_into(target);
320    }
321}
322
323impl Deserializable for AccountComponentMetadata {
324    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
325        Ok(Self {
326            name: String::read_from(source)?,
327            description: String::read_from(source)?,
328            version: semver::Version::from_str(&String::read_from(source)?).map_err(
329                |err: semver::Error| DeserializationError::InvalidValue(err.to_string()),
330            )?,
331            targets: BTreeSet::<AccountType>::read_from(source)?,
332            storage: Vec::<StorageEntry>::read_from(source)?,
333        })
334    }
335}
336
337// TESTS
338// ================================================================================================
339
340#[cfg(test)]
341mod tests {
342    use assembly::Assembler;
343    use assert_matches::assert_matches;
344    use storage::WordRepresentation;
345    use vm_core::{Felt, FieldElement};
346
347    use super::*;
348    use crate::{account::AccountComponent, testing::account_code::CODE, AccountError};
349
350    #[test]
351    fn test_contiguous_value_slots() {
352        let storage = vec![
353            StorageEntry::Value {
354                name: "slot0".into(),
355                description: None,
356                slot: 0,
357                value: WordRepresentation::Value(Default::default()),
358            },
359            StorageEntry::MultiSlot {
360                name: "slot1".into(),
361                description: None,
362                slots: vec![1, 2],
363                values: vec![
364                    WordRepresentation::Array(Default::default()),
365                    WordRepresentation::Value(Default::default()),
366                ],
367            },
368        ];
369
370        let original_config = AccountComponentMetadata::new(
371            "test".into(),
372            "desc".into(),
373            Version::parse("0.1.0").unwrap(),
374            BTreeSet::new(),
375            storage,
376        )
377        .unwrap();
378
379        let serialized = original_config.as_toml().unwrap();
380
381        let deserialized = AccountComponentMetadata::from_toml(&serialized).unwrap();
382        assert_eq!(deserialized, original_config)
383    }
384
385    #[test]
386    fn test_new_non_contiguous_value_slots() {
387        let storage = vec![
388            StorageEntry::Value {
389                name: "slot0".into(),
390                description: None,
391                slot: 0,
392                value: Default::default(),
393            },
394            StorageEntry::Value {
395                name: "slot2".into(),
396                description: None,
397                slot: 2,
398                value: Default::default(),
399            },
400        ];
401
402        let result = AccountComponentMetadata::new(
403            "test".into(),
404            "desc".into(),
405            Version::parse("0.1.0").unwrap(),
406            BTreeSet::new(),
407            storage,
408        );
409        assert_matches!(result, Err(AccountComponentTemplateError::NonContiguousSlots(0, 2)));
410    }
411
412    #[test]
413    fn test_binary_serde_roundtrip() {
414        let storage = vec![
415            StorageEntry::MultiSlot {
416                name: "slot1".into(),
417                description: None,
418                slots: vec![1, 2],
419                values: vec![
420                    WordRepresentation::Array(Default::default()),
421                    WordRepresentation::Value(Default::default()),
422                ],
423            },
424            StorageEntry::Value {
425                name: "slot0".into(),
426                description: None,
427                slot: 0,
428                value: WordRepresentation::Value(Default::default()),
429            },
430        ];
431
432        let component_template = AccountComponentMetadata::new(
433            "test".into(),
434            "desc".into(),
435            Version::parse("0.1.0").unwrap(),
436            BTreeSet::new(),
437            storage,
438        )
439        .unwrap();
440
441        let library = Assembler::default().assemble_library([CODE]).unwrap();
442        let template = AccountComponentTemplate::new(component_template, library);
443        _ = AccountComponent::from_template(&template, &InitStorageData::default()).unwrap();
444
445        let serialized = template.to_bytes();
446        let deserialized = AccountComponentTemplate::read_from_bytes(&serialized).unwrap();
447
448        assert_eq!(deserialized, template)
449    }
450
451    #[test]
452    pub fn fail_duplicate_key() {
453        let toml_text = r#"
454            name = "Test Component"
455            description = "This is a test component"
456            version = "1.0.1"
457            targets = ["FungibleFaucet"]
458
459            [[storage]]
460            name = "map"
461            description = "A storage map entry"
462            slot = 0
463            values = [
464                { key = "0x1", value = ["{{value.test}}", "0x1", "0x2", "0x3"] },
465                { key = "0x1", value = ["0x1", "0x2", "0x3", "{{value.test}}"] },
466            ]
467        "#;
468
469        let result = AccountComponentMetadata::from_toml(toml_text);
470        assert_matches!(result, Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys(_)));
471    }
472
473    #[test]
474    pub fn fail_duplicate_key_instance() {
475        let toml_text = r#"
476            name = "Test Component"
477            description = "This is a test component"
478            version = "1.0.1"
479            targets = ["FungibleFaucet"]
480
481            [[storage]]
482            name = "map"
483            description = "A storage map entry"
484            slot = 0
485            values = [
486                { key = ["0","0","0","1"], value = ["{{value.test}}", "0x1", "0x2", "0x3"] },
487                { key = "{{word.test}}", value = ["0x1", "0x2", "0x3", "{{value.test}}"] },
488            ]
489        "#;
490
491        let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
492        let library = Assembler::default().assemble_library([CODE]).unwrap();
493        let template = AccountComponentTemplate::new(metadata, library);
494
495        let init_storage_data = InitStorageData::new([
496            (
497                StoragePlaceholder::new("word.test").unwrap(),
498                StorageValue::Word([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]),
499            ),
500            (StoragePlaceholder::new("value.test").unwrap(), StorageValue::Felt(Felt::ONE)),
501        ]);
502        let account_component = AccountComponent::from_template(&template, &init_storage_data);
503        assert_matches!(
504            account_component,
505            Err(AccountError::AccountComponentTemplateInstantiationError(
506                AccountComponentTemplateError::StorageMapHasDuplicateKeys(_)
507            ))
508        );
509
510        let valid_init_storage_data = InitStorageData::new([
511            (
512                StoragePlaceholder::new("word.test").unwrap(),
513                StorageValue::Word([Felt::new(30), Felt::new(20), Felt::new(10), Felt::ZERO]),
514            ),
515            (StoragePlaceholder::new("value.test").unwrap(), StorageValue::Felt(Felt::ONE)),
516        ]);
517        AccountComponent::from_template(&template, &valid_init_storage_data).unwrap();
518    }
519}