miden_objects/account/code/
mod.rs

1use alloc::{collections::BTreeSet, sync::Arc, vec::Vec};
2
3use vm_core::{mast::MastForest, prettier::PrettyPrint};
4
5use super::{
6    AccountError, ByteReader, ByteWriter, Deserializable, DeserializationError, Digest, Felt,
7    Hasher, Serializable,
8};
9use crate::account::{AccountComponent, AccountType};
10
11pub mod procedure;
12use procedure::{AccountProcedureInfo, PrintableProcedure};
13
14// ACCOUNT CODE
15// ================================================================================================
16
17/// A public interface of an account.
18///
19/// Account's public interface consists of a set of account procedures, each procedure being a
20/// Miden VM program. Thus, MAST root of each procedure commits to the underlying program.
21///
22/// Each exported procedure is associated with a storage offset and a storage size.
23///
24/// We commit to the entire account interface by building a sequential hash of all procedure MAST
25/// roots and associated storage_offset's. Specifically, each procedure contributes exactly 8 field
26/// elements to the sequence of elements to be hashed. These elements are defined as follows:
27///
28/// ```text
29/// [PROCEDURE_MAST_ROOT, storage_offset, 0, 0, storage_size]
30/// ```
31#[derive(Debug, Clone)]
32pub struct AccountCode {
33    mast: Arc<MastForest>,
34    procedures: Vec<AccountProcedureInfo>,
35    commitment: Digest,
36}
37
38impl AccountCode {
39    /// The maximum number of account interface procedures.
40    pub const MAX_NUM_PROCEDURES: usize = 256;
41
42    // CONSTRUCTORS
43    // --------------------------------------------------------------------------------------------
44
45    /// Creates a new [`AccountCode`] from the provided components' libraries.
46    ///
47    /// For testing use only.
48    #[cfg(any(feature = "testing", test))]
49    pub fn from_components(
50        components: &[AccountComponent],
51        account_type: AccountType,
52    ) -> Result<Self, AccountError> {
53        super::validate_components_support_account_type(components, account_type)?;
54        Self::from_components_unchecked(components, account_type)
55    }
56
57    /// Creates a new [`AccountCode`] from the provided components' libraries.
58    ///
59    /// # Warning
60    ///
61    /// This does not check whether the provided components are valid when combined.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if:
66    /// - The number of procedures in all merged libraries is 0 or exceeds
67    ///   [`AccountCode::MAX_NUM_PROCEDURES`].
68    /// - Two or more libraries export a procedure with the same MAST root.
69    /// - The number of [`StorageSlot`](crate::account::StorageSlot)s of a component or of all
70    ///   components exceeds 255.
71    /// - [`MastForest::merge`] fails on all libraries.
72    pub(super) fn from_components_unchecked(
73        components: &[AccountComponent],
74        account_type: AccountType,
75    ) -> Result<Self, AccountError> {
76        let (merged_mast_forest, _) =
77            MastForest::merge(components.iter().map(|component| component.mast_forest()))
78                .map_err(AccountError::AccountComponentMastForestMergeError)?;
79
80        let mut procedures = Vec::new();
81        let mut proc_root_set = BTreeSet::new();
82
83        // Slot 0 is globally reserved for faucet accounts so the accessible slots begin at 1 if
84        // there is a faucet component present.
85        let mut component_storage_offset = if account_type.is_faucet() { 1 } else { 0 };
86
87        for component in components {
88            let component_storage_size = component.storage_size();
89
90            for module in component.library().module_infos() {
91                for proc_mast_root in module.procedure_digests() {
92                    // We cannot support procedures from multiple components with the same MAST root
93                    // since storage offsets/sizes are set per MAST root. Setting them again for
94                    // procedures where the offset has already been inserted would cause that
95                    // procedure of the earlier component to write to the wrong slot.
96                    if !proc_root_set.insert(proc_mast_root) {
97                        return Err(AccountError::AccountComponentDuplicateProcedureRoot(
98                            proc_mast_root,
99                        ));
100                    }
101
102                    // Components that do not access storage need to have offset and size set to 0.
103                    let (storage_offset, storage_size) = if component_storage_size == 0 {
104                        (0, 0)
105                    } else {
106                        (component_storage_offset, component_storage_size)
107                    };
108
109                    // Note: Offset and size are validated in `AccountProcedureInfo::new`.
110                    procedures.push(AccountProcedureInfo::new(
111                        proc_mast_root,
112                        storage_offset,
113                        storage_size,
114                    )?);
115                }
116            }
117
118            component_storage_offset = component_storage_offset.checked_add(component_storage_size)
119              .expect("account procedure info constructor should return an error if the addition overflows");
120        }
121
122        // make sure the number of procedures is between 1 and 256 (both inclusive)
123        if procedures.is_empty() {
124            return Err(AccountError::AccountCodeNoProcedures);
125        } else if procedures.len() > Self::MAX_NUM_PROCEDURES {
126            return Err(AccountError::AccountCodeTooManyProcedures(procedures.len()));
127        }
128
129        Ok(Self {
130            commitment: build_procedure_commitment(&procedures),
131            procedures,
132            mast: Arc::new(merged_mast_forest),
133        })
134    }
135
136    /// Returns a new [AccountCode] deserialized from the provided bytes.
137    ///
138    /// # Errors
139    /// Returns an error if account code deserialization fails.
140    pub fn from_bytes(bytes: &[u8]) -> Result<Self, AccountError> {
141        Self::read_from_bytes(bytes).map_err(AccountError::AccountCodeDeserializationError)
142    }
143
144    /// Returns a new definition of an account's interface instantiated from the provided
145    /// [MastForest] and a list of [AccountProcedureInfo]s.
146    ///
147    /// # Panics
148    /// Panics if:
149    /// - The number of procedures is smaller than 1 or greater than 256.
150    /// - If some any of the provided procedures does not have a corresponding root in the provided
151    ///   MAST forest.
152    pub fn from_parts(mast: Arc<MastForest>, procedures: Vec<AccountProcedureInfo>) -> Self {
153        assert!(!procedures.is_empty(), "no account procedures");
154        assert!(procedures.len() <= Self::MAX_NUM_PROCEDURES, "too many account procedures");
155
156        Self {
157            commitment: build_procedure_commitment(&procedures),
158            procedures,
159            mast,
160        }
161    }
162
163    // PUBLIC ACCESSORS
164    // --------------------------------------------------------------------------------------------
165
166    /// Returns a commitment to an account's public interface.
167    pub fn commitment(&self) -> Digest {
168        self.commitment
169    }
170
171    /// Returns a reference to the [MastForest] backing this account code.
172    pub fn mast(&self) -> Arc<MastForest> {
173        self.mast.clone()
174    }
175
176    /// Returns a reference to the account procedures.
177    pub fn procedures(&self) -> &[AccountProcedureInfo] {
178        &self.procedures
179    }
180
181    /// Returns an iterator over the procedure MAST roots of this account code.
182    pub fn procedure_roots(&self) -> impl Iterator<Item = Digest> + '_ {
183        self.procedures().iter().map(|procedure| *procedure.mast_root())
184    }
185
186    /// Returns the number of public interface procedures defined in this account code.
187    pub fn num_procedures(&self) -> usize {
188        self.procedures.len()
189    }
190
191    /// Returns true if a procedure with the specified MAST root is defined in this account code.
192    pub fn has_procedure(&self, mast_root: Digest) -> bool {
193        self.procedures.iter().any(|procedure| procedure.mast_root() == &mast_root)
194    }
195
196    /// Returns information about the procedure at the specified index.
197    ///
198    /// # Panics
199    /// Panics if the provided index is out of bounds.
200    pub fn get_procedure_by_index(&self, index: usize) -> &AccountProcedureInfo {
201        &self.procedures[index]
202    }
203
204    /// Returns the procedure index for the procedure with the specified MAST root or None if such
205    /// procedure is not defined in this [AccountCode].
206    pub fn get_procedure_index_by_root(&self, root: Digest) -> Option<usize> {
207        self.procedures
208            .iter()
209            .map(|procedure| procedure.mast_root())
210            .position(|r| r == &root)
211    }
212
213    /// Converts procedure information in this [AccountCode] into a vector of field elements.
214    ///
215    /// This is done by first converting each procedure into 8 field elements as follows:
216    /// ```text
217    /// [PROCEDURE_MAST_ROOT, storage_offset, storage_size, 0, 0]
218    /// ```
219    /// And then concatenating the resulting elements into a single vector.
220    pub fn as_elements(&self) -> Vec<Felt> {
221        procedures_as_elements(self.procedures())
222    }
223
224    /// Returns an iterator of printable representations for all procedures in this account code.
225    ///
226    /// # Returns
227    /// An iterator yielding [`PrintableProcedure`] instances for all procedures in this account
228    /// code.
229    pub fn printable_procedures(&self) -> impl Iterator<Item = PrintableProcedure> {
230        self.procedures()
231            .iter()
232            .filter_map(move |procedure_info| self.printable_procedure(procedure_info).ok())
233    }
234
235    // HELPER FUNCTIONS
236    // --------------------------------------------------------------------------------------------
237
238    /// Returns a printable representation of the procedure with the specified MAST root.
239    ///
240    /// # Errors
241    /// Returns an error if no procedure with the specified root exists in this account code.
242    fn printable_procedure(
243        &self,
244        proc_info: &AccountProcedureInfo,
245    ) -> Result<PrintableProcedure, AccountError> {
246        let node_id = self
247            .mast
248            .find_procedure_root(*proc_info.mast_root())
249            .expect("procedure root should be present in the mast forest");
250
251        Ok(PrintableProcedure::new(self.mast.clone(), *proc_info, node_id))
252    }
253}
254
255// EQUALITY
256// ================================================================================================
257
258impl PartialEq for AccountCode {
259    fn eq(&self, other: &Self) -> bool {
260        // TODO: consider checking equality based only on the set of procedures
261        self.mast == other.mast && self.procedures == other.procedures
262    }
263}
264
265impl Ord for AccountCode {
266    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
267        self.commitment.cmp(&other.commitment)
268    }
269}
270
271impl PartialOrd for AccountCode {
272    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
273        Some(self.cmp(other))
274    }
275}
276
277impl Eq for AccountCode {}
278
279// SERIALIZATION
280// ================================================================================================
281
282impl Serializable for AccountCode {
283    fn write_into<W: ByteWriter>(&self, target: &mut W) {
284        self.mast.write_into(target);
285        // since the number of procedures is guaranteed to be between 1 and 256, we can store the
286        // number as a single byte - but we do have to subtract 1 to store 256 as 255.
287        target.write_u8((self.procedures.len() - 1) as u8);
288        target.write_many(self.procedures());
289    }
290
291    fn get_size_hint(&self) -> usize {
292        // TODO: Replace with proper calculation.
293        let mut mast_forest_target = Vec::new();
294        self.mast.write_into(&mut mast_forest_target);
295
296        // Size of the serialized procedures length.
297        let u8_size = 0u8.get_size_hint();
298        let mut size = u8_size + mast_forest_target.len();
299
300        for procedure in self.procedures() {
301            size += procedure.get_size_hint();
302        }
303
304        size
305    }
306}
307
308impl Deserializable for AccountCode {
309    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
310        let module = Arc::new(MastForest::read_from(source)?);
311        let num_procedures = (source.read_u8()? as usize) + 1;
312        let procedures = source.read_many::<AccountProcedureInfo>(num_procedures)?;
313
314        Ok(Self::from_parts(module, procedures))
315    }
316}
317
318// PRETTY PRINT
319// ================================================================================================
320
321impl PrettyPrint for AccountCode {
322    fn render(&self) -> vm_core::prettier::Document {
323        use vm_core::prettier::*;
324        let mut partial = Document::Empty;
325        let len_procedures = self.num_procedures();
326
327        for (index, printable_procedure) in self.printable_procedures().enumerate() {
328            partial += indent(
329                0,
330                indent(
331                    4,
332                    text(format!("proc.{}", printable_procedure.mast_root()))
333                        + nl()
334                        + text(format!(
335                            "storage.{}.{}",
336                            printable_procedure.storage_offset(),
337                            printable_procedure.storage_size()
338                        ))
339                        + nl()
340                        + printable_procedure.render(),
341                ) + nl()
342                    + const_text("end"),
343            );
344            if index < len_procedures - 1 {
345                partial += nl();
346            }
347        }
348        partial
349    }
350}
351
352// HELPER FUNCTIONS
353// ================================================================================================
354
355/// Computes the commitment to the given procedures
356pub(crate) fn build_procedure_commitment(procedures: &[AccountProcedureInfo]) -> Digest {
357    let elements = procedures_as_elements(procedures);
358    Hasher::hash_elements(&elements)
359}
360
361/// Converts given procedures into field elements
362pub(crate) fn procedures_as_elements(procedures: &[AccountProcedureInfo]) -> Vec<Felt> {
363    procedures.iter().flat_map(|procedure| <[Felt; 8]>::from(*procedure)).collect()
364}
365
366// TESTS
367// ================================================================================================
368
369#[cfg(test)]
370mod tests {
371
372    use assembly::Assembler;
373    use assert_matches::assert_matches;
374    use vm_core::Word;
375
376    use super::{AccountCode, Deserializable, Serializable};
377    use crate::{
378        AccountError,
379        account::{AccountComponent, AccountType, StorageSlot, code::build_procedure_commitment},
380    };
381
382    #[test]
383    fn test_serde_account_code() {
384        let code = AccountCode::mock();
385        let serialized = code.to_bytes();
386        let deserialized = AccountCode::read_from_bytes(&serialized).unwrap();
387        assert_eq!(deserialized, code)
388    }
389
390    #[test]
391    fn test_account_code_procedure_root() {
392        let code = AccountCode::mock();
393        let procedure_root = build_procedure_commitment(code.procedures());
394        assert_eq!(procedure_root, code.commitment())
395    }
396
397    #[test]
398    fn test_account_code_procedure_offset_out_of_bounds() {
399        let code1 = "export.foo add end";
400        let library1 = Assembler::default().assemble_library([code1]).unwrap();
401        let code2 = "export.bar sub end";
402        let library2 = Assembler::default().assemble_library([code2]).unwrap();
403
404        let component1 =
405            AccountComponent::new(library1, vec![StorageSlot::Value(Word::default()); 250])
406                .unwrap()
407                .with_supports_all_types();
408        let mut component2 =
409            AccountComponent::new(library2, vec![StorageSlot::Value(Word::default()); 5])
410                .unwrap()
411                .with_supports_all_types();
412
413        // This is fine as the offset+size for component 2 is <= 255.
414        AccountCode::from_components(
415            &[component1.clone(), component2.clone()],
416            AccountType::RegularAccountUpdatableCode,
417        )
418        .unwrap();
419
420        // Push one more slot so offset+size exceeds 255.
421        component2.storage_slots.push(StorageSlot::Value(Word::default()));
422
423        let err = AccountCode::from_components(
424            &[component1, component2],
425            AccountType::RegularAccountUpdatableCode,
426        )
427        .unwrap_err();
428
429        assert_matches!(err, AccountError::StorageOffsetPlusSizeOutOfBounds(256))
430    }
431}