miden_objects/account/code/
mod.rs

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