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' libraries.
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' libraries.
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 libraries is 0 or exceeds
128    ///   [`AccountCode::MAX_NUM_PROCEDURES`].
129    /// - Two or more libraries 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 libraries.
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 components_iter = components.iter();
145
146        let first_component =
147            components_iter.next().ok_or(AccountError::AccountCodeNoAuthComponent)?;
148        builder.add_auth_component(first_component)?;
149
150        for component in components_iter {
151            builder.add_component(component)?;
152        }
153
154        let procedures = builder.build()?;
155
156        Ok(Self {
157            commitment: build_procedure_commitment(&procedures),
158            procedures,
159            mast: Arc::new(merged_mast_forest),
160            package_debug_info,
161        })
162    }
163
164    // PUBLIC ACCESSORS
165    // --------------------------------------------------------------------------------------------
166
167    /// Returns a commitment to an account's public interface.
168    pub fn commitment(&self) -> Word {
169        self.commitment
170    }
171
172    /// Returns a reference to the [MastForest] backing this account code.
173    pub fn mast(&self) -> Arc<MastForest> {
174        self.mast.clone()
175    }
176
177    /// Returns the MAST forest and package-owned debug information backing this account code.
178    pub fn loaded_mast_forest(&self) -> LoadedMastForest {
179        loaded_mast_forest(self.mast.clone(), self.package_debug_info.clone())
180    }
181
182    /// Returns a reference to the account procedure roots.
183    pub fn procedures(&self) -> &[AccountProcedureRoot] {
184        &self.procedures
185    }
186
187    /// Returns an iterator over the procedure MAST roots of this account code.
188    pub fn procedure_roots(&self) -> impl Iterator<Item = Word> + '_ {
189        self.procedures().iter().map(|procedure| *procedure.mast_root())
190    }
191
192    /// Returns the number of public interface procedures defined in this account code.
193    pub fn num_procedures(&self) -> usize {
194        self.procedures.len()
195    }
196
197    /// Returns true if a procedure with the specified MAST root is defined in this account code.
198    pub fn has_procedure(&self, mast_root: Word) -> bool {
199        self.procedures.iter().any(|procedure| procedure.mast_root() == &mast_root)
200    }
201
202    /// Returns the procedure root at the specified index.
203    pub fn get(&self, index: usize) -> Option<&AccountProcedureRoot> {
204        self.procedures.get(index)
205    }
206
207    /// Converts the procedure root in this [`AccountCode`] into a vector of field elements.
208    ///
209    /// This is done by first converting each procedure into 4 field elements as follows:
210    ///
211    /// ```text
212    /// [PROCEDURE_MAST_ROOT]
213    /// ```
214    ///
215    /// And then concatenating the resulting elements into a single vector.
216    pub fn to_elements(&self) -> Vec<Felt> {
217        procedures_as_elements(self.procedures())
218    }
219
220    /// Returns the public interface of this account code: the given account ID and the set of
221    /// procedure roots exposed by this code.
222    pub fn interface(&self, account_id: AccountId) -> AccountCodeInterface {
223        AccountCodeInterface::new(account_id, self.procedures.iter().copied().collect())
224            .expect("account code procedure count is enforced by AccountCode invariants")
225    }
226
227    /// Returns an iterator of printable representations for all procedures in this account code.
228    ///
229    /// # Returns
230    ///
231    /// An iterator yielding [`PrintableProcedure`] instances for all procedures in this account
232    /// code.
233    pub fn printable_procedures(&self) -> impl Iterator<Item = PrintableProcedure> {
234        self.procedures()
235            .iter()
236            .filter_map(move |proc_root| self.printable_procedure(proc_root).ok())
237    }
238
239    // HELPER FUNCTIONS
240    // --------------------------------------------------------------------------------------------
241
242    /// Returns a printable representation of the procedure with the specified MAST root.
243    ///
244    /// # Errors
245    /// Returns an error if no procedure with the specified root exists in this account code.
246    fn printable_procedure(
247        &self,
248        proc_root: &AccountProcedureRoot,
249    ) -> Result<PrintableProcedure, AccountError> {
250        let node_id = self
251            .mast
252            .find_procedure_root(*proc_root.mast_root())
253            .expect("procedure root should be present in the mast forest");
254
255        Ok(PrintableProcedure::new(self.mast.clone(), *proc_root, node_id))
256    }
257}
258
259// EQUALITY
260// ================================================================================================
261
262impl PartialEq for AccountCode {
263    fn eq(&self, other: &Self) -> bool {
264        // TODO: consider checking equality based only on the set of procedures
265        self.mast == other.mast && self.procedures == other.procedures
266    }
267}
268
269impl Ord for AccountCode {
270    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
271        self.commitment.cmp(&other.commitment)
272    }
273}
274
275impl PartialOrd for AccountCode {
276    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
277        Some(self.cmp(other))
278    }
279}
280
281impl Eq for AccountCode {}
282
283// SERIALIZATION
284// ================================================================================================
285
286impl Serializable for AccountCode {
287    fn write_into<W: ByteWriter>(&self, target: &mut W) {
288        self.mast.write_into(target);
289        // since the number of procedures is guaranteed to be between 2 and 256, we can store the
290        // number as a single byte - but we do have to subtract 1 to store 256 as 255.
291        target.write_u8((self.procedures.len() - 1) as u8);
292        target.write_many(self.procedures());
293    }
294
295    fn get_size_hint(&self) -> usize {
296        // TODO: Replace with proper calculation.
297        let mut mast_forest_target = Vec::new();
298        self.mast.write_into(&mut mast_forest_target);
299
300        // Size of the serialized procedures length.
301        let u8_size = 0u8.get_size_hint();
302        let mut size = u8_size + mast_forest_target.len();
303
304        for procedure in self.procedures() {
305            size += procedure.get_size_hint();
306        }
307
308        size
309    }
310}
311
312impl Deserializable for AccountCode {
313    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
314        let mast = Arc::new(MastForest::read_from(source)?);
315        let num_procedures = (source.read_u8()? as usize) + 1;
316
317        let procedures = source
318            .read_many_iter(num_procedures)?
319            .collect::<Result<Vec<AccountProcedureRoot>, _>>()?;
320
321        Self::from_parts(mast, procedures)
322            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
323    }
324}
325
326// PRETTY PRINT
327// ================================================================================================
328
329impl PrettyPrint for AccountCode {
330    fn render(&self) -> miden_core::prettier::Document {
331        use miden_core::prettier::*;
332        let mut partial = Document::Empty;
333        let len_procedures = self.num_procedures();
334
335        for (index, printable_procedure) in self.printable_procedures().enumerate() {
336            partial += indent(
337                0,
338                indent(
339                    4,
340                    text(format!("proc {}", printable_procedure.mast_root()))
341                        + nl()
342                        + printable_procedure.render(),
343                ) + nl()
344                    + const_text("end"),
345            );
346            if index < len_procedures - 1 {
347                partial += nl();
348            }
349        }
350        partial
351    }
352}
353
354// ACCOUNT PROCEDURE BUILDER
355// ================================================================================================
356
357/// A helper type for building the set of account procedures from account components.
358///
359/// In particular, this ensures that the auth procedure ends up at index 0.
360struct AccountProcedureBuilder {
361    procedures: Vec<AccountProcedureRoot>,
362}
363
364impl AccountProcedureBuilder {
365    fn new() -> Self {
366        Self { procedures: Vec::new() }
367    }
368
369    /// This method must be called before add_component is called.
370    fn add_auth_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> {
371        let mut auth_proc_count = 0;
372
373        for (proc_root, is_auth) in component.procedures() {
374            self.add_procedure(proc_root);
375
376            if is_auth {
377                let auth_proc_idx = self.procedures.len() - 1;
378                self.procedures.swap(0, auth_proc_idx);
379                auth_proc_count += 1;
380            }
381        }
382
383        if auth_proc_count == 0 {
384            return Err(AccountError::AccountCodeNoAuthComponent);
385        } else if auth_proc_count > 1 {
386            return Err(AccountError::AccountComponentMultipleAuthProcedures);
387        }
388
389        Ok(())
390    }
391
392    fn add_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> {
393        for (proc_root, is_auth) in component.procedures() {
394            if is_auth {
395                return Err(AccountError::AccountCodeMultipleAuthComponents);
396            }
397            self.add_procedure(proc_root);
398        }
399
400        Ok(())
401    }
402
403    fn add_procedure(&mut self, proc_root: AccountProcedureRoot) {
404        // Allow procedures with the same MAST root from different components, but only add them
405        // once.
406        if !self.procedures.contains(&proc_root) {
407            self.procedures.push(proc_root);
408        }
409    }
410
411    fn build(self) -> Result<Vec<AccountProcedureRoot>, 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
426fn build_procedure_commitment(procedures: &[AccountProcedureRoot]) -> Word {
427    let elements = procedures_as_elements(procedures);
428    Hasher::hash_elements(&elements)
429}
430
431fn merge_component_debug_info(
432    components: &[AccountComponent],
433    root_map: &miden_core::mast::MastForestRootMap,
434) -> Result<Option<Arc<PackageDebugInfo>>, AccountError> {
435    let component_debug_info = components
436        .iter()
437        .enumerate()
438        .filter_map(|(idx, component)| {
439            package_debug_info(component.component_code().as_package()).map(|debug| (idx, debug))
440        })
441        .collect::<Vec<_>>();
442
443    if component_debug_info.is_empty() {
444        return Ok(None);
445    }
446
447    let debug_info = PackageDebugInfo::merge_source_debug(
448        component_debug_info.iter().map(|(idx, debug)| (*idx, debug.as_ref())),
449        root_map,
450    )
451    .map_err(|err| {
452        AccountError::other_with_source("failed to merge account component debug info", err)
453    })?;
454
455    Ok(Some(Arc::new(debug_info)))
456}
457
458/// Converts given procedures into field elements
459fn procedures_as_elements(procedures: &[AccountProcedureRoot]) -> Vec<Felt> {
460    procedures.iter().flat_map(AccountProcedureRoot::as_elements).copied().collect()
461}
462
463// TESTS
464// ================================================================================================
465
466#[cfg(test)]
467mod tests {
468    use alloc::vec::Vec;
469
470    use assert_matches::assert_matches;
471
472    use super::{AccountCode, ByteWriter, Deserializable, DeserializationError, Serializable};
473    use crate::Word;
474    use crate::account::code::build_procedure_commitment;
475    use crate::account::component::AccountComponentMetadata;
476    use crate::account::{AccountComponent, AccountProcedureRoot};
477    use crate::errors::AccountError;
478    use crate::testing::account_code::CODE;
479    use crate::testing::assembler::assemble_test_library;
480    use crate::testing::noop_auth_component::NoopAuthComponent;
481
482    #[test]
483    fn test_serde_account_code() {
484        let code = AccountCode::mock();
485        let serialized = code.to_bytes();
486        let deserialized = AccountCode::read_from_bytes(&serialized).unwrap();
487        assert_eq!(deserialized, code)
488    }
489
490    #[test]
491    fn test_account_code_procedure_root() {
492        let code = AccountCode::mock();
493        let procedure_root = build_procedure_commitment(code.procedures());
494        assert_eq!(procedure_root, code.commitment())
495    }
496
497    #[test]
498    fn test_account_code_only_auth_component() {
499        let err = AccountCode::from_components(&[NoopAuthComponent.into()]).unwrap_err();
500
501        assert_matches!(err, AccountError::AccountCodeNoProcedures);
502    }
503
504    #[test]
505    fn test_account_code_no_auth_component() {
506        let library =
507            assemble_test_library("test-account-code-no-auth", "test::account_code", CODE);
508        let metadata = AccountComponentMetadata::new("test::no_auth");
509        let component = AccountComponent::new(library, vec![], metadata).unwrap();
510
511        let err = AccountCode::from_components(&[component]).unwrap_err();
512
513        assert_matches!(err, AccountError::AccountCodeNoAuthComponent);
514    }
515
516    #[test]
517    fn test_account_code_preserves_component_debug_info() {
518        let library =
519            assemble_test_library("test-account-code-debug-info", "test::account_code", CODE);
520        let metadata = AccountComponentMetadata::new("test::debug_info");
521        let component = AccountComponent::new(library, vec![], metadata).unwrap();
522
523        let code = AccountCode::from_components(&[NoopAuthComponent.into(), component]).unwrap();
524
525        assert!(code.loaded_mast_forest().package_debug_info().unwrap().is_some());
526    }
527
528    #[test]
529    fn test_account_code_multiple_auth_components() {
530        let err =
531            AccountCode::from_components(&[NoopAuthComponent.into(), NoopAuthComponent.into()])
532                .unwrap_err();
533
534        assert_matches!(err, AccountError::AccountCodeMultipleAuthComponents);
535    }
536
537    #[test]
538    fn test_account_component_multiple_auth_procedures() {
539        let code_with_multiple_auth = "
540            @auth_script
541            pub proc auth_basic
542                push.1 drop
543            end
544
545            @auth_script
546            pub proc auth_secondary
547                push.0 drop
548            end
549        ";
550
551        let library = assemble_test_library(
552            "test-account-code-multiple-auth",
553            "test::account_code_multiple_auth",
554            code_with_multiple_auth,
555        );
556        let metadata = AccountComponentMetadata::new("test::multiple_auth");
557        let component = AccountComponent::new(library, vec![], metadata).unwrap();
558
559        let err = AccountCode::from_components(&[component]).unwrap_err();
560
561        assert_matches!(err, AccountError::AccountComponentMultipleAuthProcedures);
562    }
563
564    #[test]
565    fn test_account_code_from_parts_rejects_duplicate_roots() {
566        let code = AccountCode::mock();
567        let procedures = code.procedures();
568
569        // repeat the non-auth procedure root at a second index
570        let duplicated = vec![procedures[0], procedures[1], procedures[1]];
571        let err = AccountCode::from_parts(code.mast(), duplicated).unwrap_err();
572
573        assert_matches!(
574            err,
575            AccountError::AccountCodeDuplicateProcedureRoot(root) if root == procedures[1].as_word()
576        );
577    }
578
579    #[test]
580    fn test_account_code_from_parts_rejects_missing_root() {
581        let code = AccountCode::mock();
582        let procedures = code.procedures();
583        let non_existent_root = AccountProcedureRoot::from_raw(Word::from([1, 2, 3, 4u32]));
584
585        // provide a procedure root that is not in the mast forest
586        let procedures = vec![procedures[0], non_existent_root];
587        let err = AccountCode::from_parts(code.mast(), procedures).unwrap_err();
588
589        assert_matches!(
590            err,
591            AccountError::AccountCodeProcedureNotInMastForest(root) if root == non_existent_root
592        );
593    }
594
595    #[test]
596    fn test_account_code_deserialization_rejects_duplicate_roots() {
597        let code = AccountCode::mock();
598        let procedures = code.procedures();
599
600        let mut bytes = Vec::new();
601        code.mast().write_into(&mut bytes);
602        bytes.write_u8(3 - 1); // num_procedures is serialized as count - 1
603        procedures[0].write_into(&mut bytes);
604        procedures[1].write_into(&mut bytes);
605        procedures[1].write_into(&mut bytes);
606
607        let err = AccountCode::read_from_bytes(&bytes).unwrap_err();
608
609        assert_matches!(
610            err,
611            DeserializationError::InvalidValue(msg) if msg.contains("duplicate procedure with root")
612        );
613    }
614}