Skip to main content

miden_protocol/account/code/
mod.rs

1use alloc::collections::BTreeSet;
2use alloc::string::ToString;
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use miden_core::mast::MastForest;
7use miden_core::prettier::PrettyPrint;
8use miden_mast_package::debug_info::PackageDebugInfo;
9use miden_processor::LoadedMastForest;
10
11use super::{
12    AccountError,
13    ByteReader,
14    ByteWriter,
15    Deserializable,
16    DeserializationError,
17    Felt,
18    Hasher,
19    Serializable,
20};
21use crate::Word;
22use crate::account::{AccountCodeInterface, AccountComponent, AccountId};
23use crate::package::{loaded_mast_forest, package_debug_info};
24
25pub mod procedure;
26use procedure::{AccountProcedureRoot, PrintableProcedure};
27
28// ACCOUNT CODE
29// ================================================================================================
30
31/// The public interface of an account.
32///
33/// An account's public interface consists of a set of account procedures, each of which is
34/// identified and committed to by a MAST root. They are represented by [`AccountProcedureRoot`].
35///
36/// The set of procedures has an arbitrary order, i.e. they are not sorted. The only exception is
37/// the authentication procedure of the account, which is always at index 0. This procedure is
38/// automatically called at the end of a transaction to validate an account's state transition.
39///
40/// The code commits to the entire account interface by building a sequential hash of all procedure
41/// MAST roots. Specifically, each procedure contributes exactly 4 field elements to the sequence of
42/// elements to be hashed. Each procedure is represented by its MAST root:
43///
44/// ```text
45/// [PROCEDURE_MAST_ROOT]
46/// ```
47#[derive(Debug, Clone)]
48pub struct AccountCode {
49    mast: Arc<MastForest>,
50    procedures: Vec<AccountProcedureRoot>,
51    commitment: Word,
52    package_debug_info: Option<Arc<PackageDebugInfo>>,
53}
54
55impl AccountCode {
56    // CONSTANTS
57    // --------------------------------------------------------------------------------------------
58
59    /// The minimum number of account interface procedures (one auth and at least one non-auth).
60    pub const MIN_NUM_PROCEDURES: usize = 2;
61
62    /// The maximum number of account interface procedures.
63    pub const MAX_NUM_PROCEDURES: usize = 256;
64
65    // CONSTRUCTORS
66    // --------------------------------------------------------------------------------------------
67
68    /// Returns a new [`AccountCode`] instantiated from the provided [`MastForest`] and a list of
69    /// [`AccountProcedureRoot`]s.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if:
74    /// - The number of procedures is smaller than 2 or greater than 256.
75    /// - The procedure roots are not unique.
76    /// - Any provided procedure root is not in the provided [`MastForest`].
77    pub fn from_parts(
78        mast: Arc<MastForest>,
79        procedures: Vec<AccountProcedureRoot>,
80    ) -> Result<Self, AccountError> {
81        if procedures.len() < Self::MIN_NUM_PROCEDURES {
82            return Err(AccountError::AccountCodeNoProcedures);
83        }
84        if procedures.len() > Self::MAX_NUM_PROCEDURES {
85            return Err(AccountError::AccountCodeTooManyProcedures(procedures.len()));
86        }
87
88        let mut unique_roots = BTreeSet::new();
89        for procedure in &procedures {
90            if !unique_roots.insert(procedure.as_word()) {
91                return Err(AccountError::AccountCodeDuplicateProcedureRoot(procedure.as_word()));
92            }
93        }
94
95        // make sure that all account procedures are in the MAST forest
96        for procedure in procedures.iter() {
97            if mast.find_procedure_root(procedure.as_word()).is_none() {
98                return Err(AccountError::AccountCodeProcedureNotInMastForest(*procedure));
99            }
100        }
101
102        Ok(Self {
103            commitment: build_procedure_commitment(&procedures),
104            procedures,
105            mast,
106            package_debug_info: None,
107        })
108    }
109
110    /// Creates a new [`AccountCode`] from the provided components' packages.
111    ///
112    /// For testing use only.
113    #[cfg(any(feature = "testing", test))]
114    pub fn from_components(components: &[AccountComponent]) -> Result<Self, AccountError> {
115        Self::from_components_unchecked(components)
116    }
117
118    /// Creates a new [`AccountCode`] from the provided components' packages.
119    ///
120    /// # Warning
121    ///
122    /// This does not check whether the provided components are valid when combined.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if:
127    /// - The number of procedures in all merged packages is 0 or exceeds
128    ///   [`AccountCode::MAX_NUM_PROCEDURES`].
129    /// - Two or more packages export a procedure with the same MAST root.
130    /// - The first component doesn't contain exactly one authentication procedure.
131    /// - Other components contain authentication procedures.
132    /// - The number of [`StorageSlot`](crate::account::StorageSlot)s of a component or of all
133    ///   components exceeds 255.
134    /// - [`MastForest::merge`] fails on all packages.
135    pub(super) fn from_components_unchecked(
136        components: &[AccountComponent],
137    ) -> Result<Self, AccountError> {
138        let (merged_mast_forest, root_map) =
139            MastForest::merge(components.iter().map(|component| component.mast_forest()))
140                .map_err(AccountError::AccountComponentMastForestMergeError)?;
141        let package_debug_info = merge_component_debug_info(components, &root_map)?;
142
143        let mut builder = AccountProcedureBuilder::new();
144        let mut num_auth_components = 0;
145
146        for component in components {
147            if component.is_auth_component() {
148                num_auth_components += 1;
149                builder.add_auth_component(component)?
150            } else {
151                builder.add_component(component)?;
152            }
153        }
154
155        if num_auth_components == 0 {
156            return Err(AccountError::AccountCodeNoAuthComponent);
157        } else if num_auth_components > 1 {
158            return Err(AccountError::AccountCodeMultipleAuthComponents);
159        }
160
161        let procedures = builder.build()?;
162
163        Ok(Self {
164            commitment: build_procedure_commitment(&procedures),
165            procedures,
166            mast: Arc::new(merged_mast_forest),
167            package_debug_info,
168        })
169    }
170
171    // PUBLIC ACCESSORS
172    // --------------------------------------------------------------------------------------------
173
174    /// Returns a commitment to an account's public interface.
175    pub fn commitment(&self) -> Word {
176        self.commitment
177    }
178
179    /// Returns a reference to the [MastForest] backing this account code.
180    pub fn mast(&self) -> Arc<MastForest> {
181        self.mast.clone()
182    }
183
184    /// Returns the MAST forest and package-owned debug information backing this account code.
185    pub fn loaded_mast_forest(&self) -> LoadedMastForest {
186        loaded_mast_forest(self.mast.clone(), self.package_debug_info.clone())
187    }
188
189    /// Returns a reference to the account procedure roots.
190    pub fn procedures(&self) -> &[AccountProcedureRoot] {
191        &self.procedures
192    }
193
194    /// Returns an iterator over the procedure MAST roots of this account code.
195    pub fn procedure_roots(&self) -> impl Iterator<Item = Word> + '_ {
196        self.procedures().iter().map(|procedure| *procedure.mast_root())
197    }
198
199    /// Returns the number of public interface procedures defined in this account code.
200    pub fn num_procedures(&self) -> usize {
201        self.procedures.len()
202    }
203
204    /// Returns true if a procedure with the specified MAST root is defined in this account code.
205    pub fn has_procedure(&self, mast_root: Word) -> bool {
206        self.procedures.iter().any(|procedure| procedure.mast_root() == &mast_root)
207    }
208
209    /// Returns the procedure root at the specified index.
210    pub fn get(&self, index: usize) -> Option<&AccountProcedureRoot> {
211        self.procedures.get(index)
212    }
213
214    /// Converts the procedure root in this [`AccountCode`] into a vector of field elements.
215    ///
216    /// This is done by first converting each procedure into 4 field elements as follows:
217    ///
218    /// ```text
219    /// [PROCEDURE_MAST_ROOT]
220    /// ```
221    ///
222    /// And then concatenating the resulting elements into a single vector.
223    pub fn to_elements(&self) -> Vec<Felt> {
224        procedures_as_elements(self.procedures())
225    }
226
227    /// Returns the public interface of this account code: the given account ID and the set of
228    /// procedure roots exposed by this code.
229    pub fn interface(&self, account_id: AccountId) -> AccountCodeInterface {
230        AccountCodeInterface::new(account_id, self.procedures.iter().copied().collect())
231            .expect("account code procedure count is enforced by AccountCode invariants")
232    }
233
234    /// Returns an iterator of printable representations for all procedures in this account code.
235    ///
236    /// # Returns
237    ///
238    /// An iterator yielding [`PrintableProcedure`] instances for all procedures in this account
239    /// code.
240    pub fn printable_procedures(&self) -> impl Iterator<Item = PrintableProcedure> {
241        self.procedures()
242            .iter()
243            .filter_map(move |proc_root| self.printable_procedure(proc_root).ok())
244    }
245
246    // HELPER FUNCTIONS
247    // --------------------------------------------------------------------------------------------
248
249    /// Returns a printable representation of the procedure with the specified MAST root.
250    ///
251    /// # Errors
252    /// Returns an error if no procedure with the specified root exists in this account code.
253    fn printable_procedure(
254        &self,
255        proc_root: &AccountProcedureRoot,
256    ) -> Result<PrintableProcedure, AccountError> {
257        let node_id = self
258            .mast
259            .find_procedure_root(*proc_root.mast_root())
260            .expect("procedure root should be present in the mast forest");
261
262        Ok(PrintableProcedure::new(self.mast.clone(), *proc_root, node_id))
263    }
264}
265
266// EQUALITY
267// ================================================================================================
268
269impl PartialEq for AccountCode {
270    fn eq(&self, other: &Self) -> bool {
271        // TODO: consider checking equality based only on the set of procedures
272        self.mast == other.mast && self.procedures == other.procedures
273    }
274}
275
276impl Ord for AccountCode {
277    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
278        self.commitment.cmp(&other.commitment)
279    }
280}
281
282impl PartialOrd for AccountCode {
283    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
284        Some(self.cmp(other))
285    }
286}
287
288impl Eq for AccountCode {}
289
290// SERIALIZATION
291// ================================================================================================
292
293impl Serializable for AccountCode {
294    fn write_into<W: ByteWriter>(&self, target: &mut W) {
295        self.mast.write_into(target);
296        // since the number of procedures is guaranteed to be between 2 and 256, we can store the
297        // number as a single byte - but we do have to subtract 1 to store 256 as 255.
298        target.write_u8((self.procedures.len() - 1) as u8);
299        target.write_many(self.procedures());
300    }
301
302    fn get_size_hint(&self) -> usize {
303        // TODO: Replace with proper calculation.
304        let mut mast_forest_target = Vec::new();
305        self.mast.write_into(&mut mast_forest_target);
306
307        // Size of the serialized procedures length.
308        let u8_size = 0u8.get_size_hint();
309        let mut size = u8_size + mast_forest_target.len();
310
311        for procedure in self.procedures() {
312            size += procedure.get_size_hint();
313        }
314
315        size
316    }
317}
318
319impl Deserializable for AccountCode {
320    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
321        let mast = Arc::new(MastForest::read_from(source)?);
322        let num_procedures = (source.read_u8()? as usize) + 1;
323
324        let procedures = source
325            .read_many_iter(num_procedures)?
326            .collect::<Result<Vec<AccountProcedureRoot>, _>>()?;
327
328        Self::from_parts(mast, procedures)
329            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
330    }
331}
332
333// PRETTY PRINT
334// ================================================================================================
335
336impl PrettyPrint for AccountCode {
337    fn render(&self) -> miden_core::prettier::Document {
338        use miden_core::prettier::*;
339        let mut partial = Document::Empty;
340        let len_procedures = self.num_procedures();
341
342        for (index, printable_procedure) in self.printable_procedures().enumerate() {
343            partial += indent(
344                0,
345                indent(
346                    4,
347                    text(format!("proc {}", printable_procedure.mast_root()))
348                        + nl()
349                        + printable_procedure.render(),
350                ) + nl()
351                    + const_text("end"),
352            );
353            if index < len_procedures - 1 {
354                partial += nl();
355            }
356        }
357        partial
358    }
359}
360
361// ACCOUNT PROCEDURE BUILDER
362// ================================================================================================
363
364/// A helper type for building the set of account procedures from account components.
365///
366/// In particular, this ensures that the auth procedure ends up at index 0.
367struct AccountProcedureBuilder {
368    procedures: Vec<AccountProcedureRoot>,
369}
370
371impl AccountProcedureBuilder {
372    fn new() -> Self {
373        Self { procedures: Vec::new() }
374    }
375
376    /// This method must be called before add_component is called.
377    fn add_auth_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> {
378        let mut auth_proc_count = 0;
379
380        for (proc_root, is_auth) in component.procedures() {
381            self.add_procedure(proc_root);
382
383            if is_auth {
384                let auth_proc_idx = self.procedures.len() - 1;
385                self.procedures.swap(0, auth_proc_idx);
386                auth_proc_count += 1;
387            }
388        }
389
390        if auth_proc_count == 0 {
391            return Err(AccountError::AccountCodeNoAuthComponent);
392        } else if auth_proc_count > 1 {
393            return Err(AccountError::AccountComponentMultipleAuthProcedures);
394        }
395
396        Ok(())
397    }
398
399    fn add_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> {
400        for (proc_root, is_auth) in component.procedures() {
401            if is_auth {
402                return Err(AccountError::AccountCodeMultipleAuthComponents);
403            }
404            self.add_procedure(proc_root);
405        }
406
407        Ok(())
408    }
409
410    fn add_procedure(&mut self, proc_root: AccountProcedureRoot) {
411        // Allow procedures with the same MAST root from different components, but only add them
412        // once.
413        if !self.procedures.contains(&proc_root) {
414            self.procedures.push(proc_root);
415        }
416    }
417
418    fn build(self) -> Result<Vec<AccountProcedureRoot>, AccountError> {
419        if self.procedures.len() < AccountCode::MIN_NUM_PROCEDURES {
420            Err(AccountError::AccountCodeNoProcedures)
421        } else if self.procedures.len() > AccountCode::MAX_NUM_PROCEDURES {
422            Err(AccountError::AccountCodeTooManyProcedures(self.procedures.len()))
423        } else {
424            Ok(self.procedures)
425        }
426    }
427}
428
429// HELPER FUNCTIONS
430// ================================================================================================
431
432/// Computes the commitment to the given procedures
433fn build_procedure_commitment(procedures: &[AccountProcedureRoot]) -> Word {
434    let elements = procedures_as_elements(procedures);
435    Hasher::hash_elements(&elements)
436}
437
438fn merge_component_debug_info(
439    components: &[AccountComponent],
440    root_map: &miden_core::mast::MastForestRootMap,
441) -> Result<Option<Arc<PackageDebugInfo>>, AccountError> {
442    let component_debug_info = components
443        .iter()
444        .enumerate()
445        .filter_map(|(idx, component)| {
446            package_debug_info(component.component_code().as_package()).map(|debug| (idx, debug))
447        })
448        .collect::<Vec<_>>();
449
450    if component_debug_info.is_empty() {
451        return Ok(None);
452    }
453
454    let debug_info = PackageDebugInfo::merge_source_debug(
455        component_debug_info.iter().map(|(idx, debug)| (*idx, debug.as_ref())),
456        root_map,
457    )
458    .map_err(|err| {
459        AccountError::other_with_source("failed to merge account component debug info", err)
460    })?;
461
462    Ok(Some(Arc::new(debug_info)))
463}
464
465/// Converts given procedures into field elements
466fn procedures_as_elements(procedures: &[AccountProcedureRoot]) -> Vec<Felt> {
467    procedures.iter().flat_map(AccountProcedureRoot::as_elements).copied().collect()
468}
469
470// TESTS
471// ================================================================================================
472
473#[cfg(test)]
474mod tests {
475    use alloc::vec::Vec;
476
477    use assert_matches::assert_matches;
478
479    use super::{AccountCode, ByteWriter, Deserializable, DeserializationError, Serializable};
480    use crate::Word;
481    use crate::account::code::build_procedure_commitment;
482    use crate::account::component::AccountComponentMetadata;
483    use crate::account::{AccountComponent, AccountProcedureRoot};
484    use crate::errors::AccountError;
485    use crate::testing::account_code::CODE;
486    use crate::testing::assembler::assemble_test_package;
487    use crate::testing::noop_auth_component::NoopAuthComponent;
488
489    #[test]
490    fn test_serde_account_code() {
491        let code = AccountCode::mock();
492        let serialized = code.to_bytes();
493        let deserialized = AccountCode::read_from_bytes(&serialized).unwrap();
494        assert_eq!(deserialized, code)
495    }
496
497    #[test]
498    fn test_account_code_procedure_root() {
499        let code = AccountCode::mock();
500        let procedure_root = build_procedure_commitment(code.procedures());
501        assert_eq!(procedure_root, code.commitment())
502    }
503
504    #[test]
505    fn test_account_code_only_auth_component() {
506        let err = AccountCode::from_components(&[NoopAuthComponent.into()]).unwrap_err();
507
508        assert_matches!(err, AccountError::AccountCodeNoProcedures);
509    }
510
511    #[test]
512    fn test_account_code_no_auth_component() {
513        let package =
514            assemble_test_package("test-account-code-no-auth", "test::account_code", CODE);
515        let metadata = AccountComponentMetadata::new("test::no_auth");
516        let component = AccountComponent::new(package, vec![], metadata).unwrap();
517
518        let err = AccountCode::from_components(&[component]).unwrap_err();
519
520        assert_matches!(err, AccountError::AccountCodeNoAuthComponent);
521    }
522
523    #[test]
524    fn test_account_code_preserves_component_debug_info() {
525        let package =
526            assemble_test_package("test-account-code-debug-info", "test::account_code", CODE);
527        let metadata = AccountComponentMetadata::new("test::debug_info");
528        let component = AccountComponent::new(package, vec![], metadata).unwrap();
529
530        let code = AccountCode::from_components(&[NoopAuthComponent.into(), component]).unwrap();
531
532        assert!(code.loaded_mast_forest().package_debug_info().unwrap().is_some());
533    }
534
535    #[test]
536    fn test_account_code_multiple_auth_components() {
537        let err =
538            AccountCode::from_components(&[NoopAuthComponent.into(), NoopAuthComponent.into()])
539                .unwrap_err();
540
541        assert_matches!(err, AccountError::AccountCodeMultipleAuthComponents);
542    }
543
544    #[test]
545    fn test_account_component_multiple_auth_procedures() {
546        let code_with_multiple_auth = "
547            @auth_script
548            pub proc auth_basic
549                push.1 drop
550            end
551
552            @auth_script
553            pub proc auth_secondary
554                push.0 drop
555            end
556        ";
557
558        let package = assemble_test_package(
559            "test-account-code-multiple-auth",
560            "test::account_code_multiple_auth",
561            code_with_multiple_auth,
562        );
563        let metadata = AccountComponentMetadata::new("test::multiple_auth");
564        let component = AccountComponent::new(package, vec![], metadata).unwrap();
565
566        let err = AccountCode::from_components(&[component]).unwrap_err();
567
568        assert_matches!(err, AccountError::AccountComponentMultipleAuthProcedures);
569    }
570
571    #[test]
572    fn test_account_code_from_parts_rejects_duplicate_roots() {
573        let code = AccountCode::mock();
574        let procedures = code.procedures();
575
576        // repeat the non-auth procedure root at a second index
577        let duplicated = vec![procedures[0], procedures[1], procedures[1]];
578        let err = AccountCode::from_parts(code.mast(), duplicated).unwrap_err();
579
580        assert_matches!(
581            err,
582            AccountError::AccountCodeDuplicateProcedureRoot(root) if root == procedures[1].as_word()
583        );
584    }
585
586    #[test]
587    fn test_account_code_from_parts_rejects_missing_root() {
588        let code = AccountCode::mock();
589        let procedures = code.procedures();
590        let non_existent_root = AccountProcedureRoot::from_raw(Word::from([1, 2, 3, 4u32]));
591
592        // provide a procedure root that is not in the mast forest
593        let procedures = vec![procedures[0], non_existent_root];
594        let err = AccountCode::from_parts(code.mast(), procedures).unwrap_err();
595
596        assert_matches!(
597            err,
598            AccountError::AccountCodeProcedureNotInMastForest(root) if root == non_existent_root
599        );
600    }
601
602    #[test]
603    fn test_account_code_deserialization_rejects_duplicate_roots() {
604        let code = AccountCode::mock();
605        let procedures = code.procedures();
606
607        let mut bytes = Vec::new();
608        code.mast().write_into(&mut bytes);
609        bytes.write_u8(3 - 1); // num_procedures is serialized as count - 1
610        procedures[0].write_into(&mut bytes);
611        procedures[1].write_into(&mut bytes);
612        procedures[1].write_into(&mut bytes);
613
614        let err = AccountCode::read_from_bytes(&bytes).unwrap_err();
615
616        assert_matches!(
617            err,
618            DeserializationError::InvalidValue(msg) if msg.contains("duplicate procedure with root")
619        );
620    }
621}