miden_objects/account/code/
mod.rs

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