Skip to main content

miden_protocol/account/code/
mod.rs

1use alloc::string::ToString;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use core::cmp::Ordering;
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 authentication procedure of the account is always at index 0. It is automatically called at
37/// the end of a transaction to validate an account's state transition. The remaining procedures are
38/// sorted in ascending order, which makes the code commitment independent of the order in which the
39/// account's components were provided.
40///
41/// The code commits to the entire account interface by building a sequential hash of all procedure
42/// MAST roots. Specifically, each procedure contributes exactly 4 field elements to the sequence of
43/// elements to be hashed. Each procedure is represented by its MAST root:
44///
45/// ```text
46/// [PROCEDURE_MAST_ROOT]
47/// ```
48#[derive(Debug, Clone)]
49pub struct AccountCode {
50    mast: Arc<MastForest>,
51    procedures: Vec<AccountProcedureRoot>,
52    commitment: Word,
53    package_debug_info: Option<Arc<PackageDebugInfo>>,
54}
55
56impl AccountCode {
57    // CONSTANTS
58    // --------------------------------------------------------------------------------------------
59
60    /// The minimum number of account interface procedures (one auth and at least one non-auth).
61    pub const MIN_NUM_PROCEDURES: usize = 2;
62
63    /// The maximum number of account interface procedures.
64    pub const MAX_NUM_PROCEDURES: usize = 256;
65
66    // CONSTRUCTORS
67    // --------------------------------------------------------------------------------------------
68
69    /// Returns a new [`AccountCode`] instantiated from the provided [`MastForest`] and a list of
70    /// [`AccountProcedureRoot`]s.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if:
75    /// - The number of procedures is smaller than 2 or greater than 256.
76    /// - The procedures after the authentication procedure at index 0 are not sorted in ascending
77    ///   order.
78    /// - The procedure roots are not unique.
79    /// - Any provided procedure root is not in the provided [`MastForest`].
80    pub fn from_parts(
81        mast: Arc<MastForest>,
82        procedures: Vec<AccountProcedureRoot>,
83    ) -> Result<Self, AccountError> {
84        if procedures.len() < Self::MIN_NUM_PROCEDURES {
85            return Err(AccountError::AccountCodeNoProcedures);
86        }
87        if procedures.len() > Self::MAX_NUM_PROCEDURES {
88            return Err(AccountError::AccountCodeTooManyProcedures(procedures.len()));
89        }
90
91        // The authentication procedure at index 0 is exempt from the ordering invariant, so the
92        // remaining procedures are checked to be strictly increasing, which also makes them unique.
93        // Each of them is also compared against the authentication procedure, which must not appear
94        // a second time.
95        let (auth_proc, other_procs) = procedures
96            .split_first()
97            .expect("account code should contain at least two procedures");
98
99        let mut previous_proc: Option<&AccountProcedureRoot> = None;
100        for procedure in other_procs {
101            if procedure == auth_proc {
102                return Err(AccountError::AccountCodeDuplicateProcedureRoot(*procedure));
103            }
104
105            if let Some(previous_proc) = previous_proc {
106                match previous_proc.cmp(procedure) {
107                    Ordering::Less => {},
108                    Ordering::Equal => {
109                        return Err(AccountError::AccountCodeDuplicateProcedureRoot(*procedure));
110                    },
111                    Ordering::Greater => return Err(AccountError::AccountCodeProceduresUnsorted),
112                }
113            }
114
115            previous_proc = Some(procedure);
116        }
117
118        // make sure that all account procedures are in the MAST forest
119        for procedure in procedures.iter() {
120            if mast.find_procedure_root(procedure.as_word()).is_none() {
121                return Err(AccountError::AccountCodeProcedureNotInMastForest(*procedure));
122            }
123        }
124
125        Ok(Self {
126            commitment: build_procedure_commitment(&procedures),
127            procedures,
128            mast,
129            package_debug_info: None,
130        })
131    }
132
133    /// Creates a new [`AccountCode`] from the provided components' packages.
134    ///
135    /// For testing use only.
136    #[cfg(any(feature = "testing", test))]
137    pub fn from_components(components: &[AccountComponent]) -> Result<Self, AccountError> {
138        Self::from_components_unchecked(components)
139    }
140
141    /// Creates a new [`AccountCode`] from the provided components' packages.
142    ///
143    /// # Warning
144    ///
145    /// This does not check whether the provided components are valid when combined.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if:
150    /// - The number of procedures in all merged packages is 0 or exceeds
151    ///   [`AccountCode::MAX_NUM_PROCEDURES`].
152    /// - The components don't contain exactly one authentication component with exactly one
153    ///   authentication procedure.
154    /// - The number of [`StorageSlot`](crate::account::StorageSlot)s of a component or of all
155    ///   components exceeds 255.
156    /// - [`MastForest::merge`] fails on all packages.
157    pub(super) fn from_components_unchecked(
158        components: &[AccountComponent],
159    ) -> Result<Self, AccountError> {
160        let (merged_mast_forest, root_map) =
161            MastForest::merge(components.iter().map(|component| component.mast_forest()))
162                .map_err(AccountError::AccountComponentMastForestMergeError)?;
163        let package_debug_info = merge_component_debug_info(components, &root_map)?;
164
165        let mut builder = AccountProcedureBuilder::new();
166        let mut num_auth_components = 0;
167
168        for component in components {
169            if component.is_auth_component() {
170                num_auth_components += 1;
171                builder.add_auth_component(component)?
172            } else {
173                builder.add_component(component)?;
174            }
175        }
176
177        if num_auth_components == 0 {
178            return Err(AccountError::AccountCodeNoAuthComponent);
179        } else if num_auth_components > 1 {
180            return Err(AccountError::AccountCodeMultipleAuthComponents);
181        }
182
183        let procedures = builder.build()?;
184
185        Self::from_parts(Arc::new(merged_mast_forest), procedures).map(|mut code| {
186            code.package_debug_info = package_debug_info;
187            code
188        })
189    }
190
191    // PUBLIC ACCESSORS
192    // --------------------------------------------------------------------------------------------
193
194    /// Returns a commitment to an account's public interface.
195    pub fn commitment(&self) -> Word {
196        self.commitment
197    }
198
199    /// Returns a reference to the [MastForest] backing this account code.
200    pub fn mast(&self) -> Arc<MastForest> {
201        self.mast.clone()
202    }
203
204    /// Returns the MAST forest and package-owned debug information backing this account code.
205    pub fn loaded_mast_forest(&self) -> LoadedMastForest {
206        loaded_mast_forest(self.mast.clone(), self.package_debug_info.clone())
207    }
208
209    /// Returns a reference to the account procedure roots.
210    pub fn procedures(&self) -> &[AccountProcedureRoot] {
211        &self.procedures
212    }
213
214    /// Returns an iterator over the procedure MAST roots of this account code.
215    pub fn procedure_roots(&self) -> impl Iterator<Item = Word> + '_ {
216        self.procedures().iter().map(|procedure| *procedure.mast_root())
217    }
218
219    /// Returns the number of public interface procedures defined in this account code.
220    pub fn num_procedures(&self) -> usize {
221        self.procedures.len()
222    }
223
224    /// Returns true if a procedure with the specified MAST root is defined in this account code.
225    pub fn has_procedure(&self, mast_root: Word) -> bool {
226        self.procedures.iter().any(|procedure| procedure.mast_root() == &mast_root)
227    }
228
229    /// Returns the procedure root at the specified index.
230    pub fn get(&self, index: usize) -> Option<&AccountProcedureRoot> {
231        self.procedures.get(index)
232    }
233
234    /// Converts the procedure root in this [`AccountCode`] into a vector of field elements.
235    ///
236    /// This is done by first converting each procedure into 4 field elements as follows:
237    ///
238    /// ```text
239    /// [PROCEDURE_MAST_ROOT]
240    /// ```
241    ///
242    /// And then concatenating the resulting elements into a single vector.
243    pub fn to_elements(&self) -> Vec<Felt> {
244        procedures_as_elements(self.procedures())
245    }
246
247    /// Returns the public interface of this account code: the given account ID and the set of
248    /// procedure roots exposed by this code.
249    pub fn interface(&self, account_id: AccountId) -> AccountCodeInterface {
250        AccountCodeInterface::new(account_id, self.procedures.iter().copied().collect())
251            .expect("account code procedure count is enforced by AccountCode invariants")
252    }
253
254    /// Returns an iterator of printable representations for all procedures in this account code.
255    ///
256    /// # Returns
257    ///
258    /// An iterator yielding [`PrintableProcedure`] instances for all procedures in this account
259    /// code.
260    pub fn printable_procedures(&self) -> impl Iterator<Item = PrintableProcedure> {
261        self.procedures()
262            .iter()
263            .filter_map(move |proc_root| self.printable_procedure(proc_root).ok())
264    }
265
266    // HELPER FUNCTIONS
267    // --------------------------------------------------------------------------------------------
268
269    /// Returns a printable representation of the procedure with the specified MAST root.
270    ///
271    /// # Errors
272    /// Returns an error if no procedure with the specified root exists in this account code.
273    fn printable_procedure(
274        &self,
275        proc_root: &AccountProcedureRoot,
276    ) -> Result<PrintableProcedure, AccountError> {
277        let node_id = self
278            .mast
279            .find_procedure_root(*proc_root.mast_root())
280            .expect("procedure root should be present in the mast forest");
281
282        Ok(PrintableProcedure::new(self.mast.clone(), *proc_root, node_id))
283    }
284}
285
286// EQUALITY
287// ================================================================================================
288
289impl PartialEq for AccountCode {
290    fn eq(&self, other: &Self) -> bool {
291        // TODO: consider checking equality based only on the set of procedures
292        self.mast == other.mast && self.procedures == other.procedures
293    }
294}
295
296impl Ord for AccountCode {
297    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
298        self.commitment.cmp(&other.commitment)
299    }
300}
301
302impl PartialOrd for AccountCode {
303    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
304        Some(self.cmp(other))
305    }
306}
307
308impl Eq for AccountCode {}
309
310// SERIALIZATION
311// ================================================================================================
312
313impl Serializable for AccountCode {
314    fn write_into<W: ByteWriter>(&self, target: &mut W) {
315        self.mast.write_into(target);
316        // since the number of procedures is guaranteed to be between 2 and 256, we can store the
317        // number as a single byte - but we do have to subtract 1 to store 256 as 255.
318        target.write_u8((self.procedures.len() - 1) as u8);
319        target.write_many(self.procedures());
320    }
321
322    fn get_size_hint(&self) -> usize {
323        // TODO: Replace with proper calculation.
324        let mut mast_forest_target = Vec::new();
325        self.mast.write_into(&mut mast_forest_target);
326
327        // Size of the serialized procedures length.
328        let u8_size = 0u8.get_size_hint();
329        let mut size = u8_size + mast_forest_target.len();
330
331        for procedure in self.procedures() {
332            size += procedure.get_size_hint();
333        }
334
335        size
336    }
337}
338
339impl Deserializable for AccountCode {
340    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
341        let mast = Arc::new(MastForest::read_from(source)?);
342        let num_procedures = (source.read_u8()? as usize) + 1;
343
344        let procedures = source
345            .read_many_iter(num_procedures)?
346            .collect::<Result<Vec<AccountProcedureRoot>, _>>()?;
347
348        Self::from_parts(mast, procedures)
349            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
350    }
351}
352
353// PRETTY PRINT
354// ================================================================================================
355
356impl PrettyPrint for AccountCode {
357    fn render(&self) -> miden_core::prettier::Document {
358        use miden_core::prettier::*;
359        let mut partial = Document::Empty;
360        let len_procedures = self.num_procedures();
361
362        for (index, printable_procedure) in self.printable_procedures().enumerate() {
363            partial += indent(
364                0,
365                indent(
366                    4,
367                    text(format!("proc {}", printable_procedure.mast_root()))
368                        + nl()
369                        + printable_procedure.render(),
370                ) + nl()
371                    + const_text("end"),
372            );
373            if index < len_procedures - 1 {
374                partial += nl();
375            }
376        }
377        partial
378    }
379}
380
381// ACCOUNT PROCEDURE BUILDER
382// ================================================================================================
383
384/// A helper type for building the set of account procedures from account components.
385///
386/// In particular, this ensures that the auth procedure ends up at index 0 and that the remaining
387/// procedures are sorted.
388struct AccountProcedureBuilder {
389    procedures: Vec<AccountProcedureRoot>,
390}
391
392impl AccountProcedureBuilder {
393    fn new() -> Self {
394        Self { procedures: Vec::new() }
395    }
396
397    fn add_auth_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> {
398        let mut auth_proc_count = 0;
399
400        for (proc_root, is_auth) in component.procedures() {
401            let proc_idx = self.add_procedure(proc_root);
402
403            if is_auth {
404                self.procedures.swap(0, proc_idx);
405                auth_proc_count += 1;
406            }
407        }
408
409        if auth_proc_count == 0 {
410            return Err(AccountError::AccountCodeNoAuthComponent);
411        } else if auth_proc_count > 1 {
412            return Err(AccountError::AccountComponentMultipleAuthProcedures);
413        }
414
415        Ok(())
416    }
417
418    fn add_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> {
419        for (proc_root, is_auth) in component.procedures() {
420            if is_auth {
421                return Err(AccountError::AccountCodeMultipleAuthComponents);
422            }
423            self.add_procedure(proc_root);
424        }
425
426        Ok(())
427    }
428
429    /// Adds the procedure and returns its index, which is the index of the existing entry if the
430    /// procedure was added before.
431    ///
432    /// Different components may export procedures with the same MAST root, but the set of
433    /// procedures must not contain duplicates.
434    fn add_procedure(&mut self, proc_root: AccountProcedureRoot) -> usize {
435        match self.procedures.iter().position(|existing_root| existing_root == &proc_root) {
436            Some(existing_idx) => existing_idx,
437            None => {
438                self.procedures.push(proc_root);
439                self.procedures.len() - 1
440            },
441        }
442    }
443
444    fn build(mut self) -> Result<Vec<AccountProcedureRoot>, AccountError> {
445        if self.procedures.len() < AccountCode::MIN_NUM_PROCEDURES {
446            return Err(AccountError::AccountCodeNoProcedures);
447        } else if self.procedures.len() > AccountCode::MAX_NUM_PROCEDURES {
448            return Err(AccountError::AccountCodeTooManyProcedures(self.procedures.len()));
449        }
450
451        // Sorting makes the account code commitment independent of the order in which components
452        // were provided. The auth procedure at index 0 is excluded from the sort so it keeps the
453        // position the transaction kernel expects.
454        self.procedures[1..].sort_unstable();
455
456        Ok(self.procedures)
457    }
458}
459
460// HELPER FUNCTIONS
461// ================================================================================================
462
463/// Computes the commitment to the given procedures
464fn build_procedure_commitment(procedures: &[AccountProcedureRoot]) -> Word {
465    let elements = procedures_as_elements(procedures);
466    Hasher::hash_elements(&elements)
467}
468
469fn merge_component_debug_info(
470    components: &[AccountComponent],
471    root_map: &miden_core::mast::MastForestRootMap,
472) -> Result<Option<Arc<PackageDebugInfo>>, AccountError> {
473    let component_debug_info = components
474        .iter()
475        .enumerate()
476        .filter_map(|(idx, component)| {
477            package_debug_info(component.component_code().as_package()).map(|debug| (idx, debug))
478        })
479        .collect::<Vec<_>>();
480
481    if component_debug_info.is_empty() {
482        return Ok(None);
483    }
484
485    let debug_info = PackageDebugInfo::merge_source_debug(
486        component_debug_info.iter().map(|(idx, debug)| (*idx, debug.as_ref())),
487        root_map,
488    )
489    .map_err(|err| {
490        AccountError::other_with_source("failed to merge account component debug info", err)
491    })?;
492
493    Ok(Some(Arc::new(debug_info)))
494}
495
496/// Converts given procedures into field elements
497fn procedures_as_elements(procedures: &[AccountProcedureRoot]) -> Vec<Felt> {
498    procedures.iter().flat_map(AccountProcedureRoot::as_elements).copied().collect()
499}
500
501// TESTS
502// ================================================================================================
503
504#[cfg(test)]
505mod tests {
506    use alloc::vec::Vec;
507
508    use anyhow::Context;
509    use assert_matches::assert_matches;
510    use rstest::rstest;
511
512    use super::{AccountCode, ByteWriter, Deserializable, DeserializationError, Serializable};
513    use crate::Word;
514    use crate::account::code::build_procedure_commitment;
515    use crate::account::component::AccountComponentMetadata;
516    use crate::account::{AccountComponent, AccountProcedureRoot};
517    use crate::errors::AccountError;
518    use crate::testing::account_code::CODE;
519    use crate::testing::assembler::assemble_test_package;
520    use crate::testing::noop_auth_component::NoopAuthComponent;
521
522    #[test]
523    fn test_serde_account_code() {
524        let code = AccountCode::mock();
525        let serialized = code.to_bytes();
526        let deserialized = AccountCode::read_from_bytes(&serialized).unwrap();
527        assert_eq!(deserialized, code)
528    }
529
530    #[test]
531    fn test_account_code_procedure_root() {
532        let code = AccountCode::mock();
533        let procedure_root = build_procedure_commitment(code.procedures());
534        assert_eq!(procedure_root, code.commitment())
535    }
536
537    #[test]
538    fn test_account_code_only_auth_component() {
539        let err = AccountCode::from_components(&[NoopAuthComponent.into()]).unwrap_err();
540
541        assert_matches!(err, AccountError::AccountCodeNoProcedures);
542    }
543
544    #[test]
545    fn test_account_code_no_auth_component() {
546        let package =
547            assemble_test_package("test-account-code-no-auth", "test::account_code", CODE);
548        let metadata = AccountComponentMetadata::new("test::no_auth");
549        let component = AccountComponent::new(package, vec![], metadata).unwrap();
550
551        let err = AccountCode::from_components(&[component]).unwrap_err();
552
553        assert_matches!(err, AccountError::AccountCodeNoAuthComponent);
554    }
555
556    #[test]
557    fn test_account_code_preserves_component_debug_info() {
558        let package =
559            assemble_test_package("test-account-code-debug-info", "test::account_code", CODE);
560        let metadata = AccountComponentMetadata::new("test::debug_info");
561        let component = AccountComponent::new(package, vec![], metadata).unwrap();
562
563        let code = AccountCode::from_components(&[NoopAuthComponent.into(), component]).unwrap();
564
565        assert!(code.loaded_mast_forest().package_debug_info().unwrap().is_some());
566    }
567
568    #[test]
569    fn test_account_code_multiple_auth_components() {
570        let err =
571            AccountCode::from_components(&[NoopAuthComponent.into(), NoopAuthComponent.into()])
572                .unwrap_err();
573
574        assert_matches!(err, AccountError::AccountCodeMultipleAuthComponents);
575    }
576
577    #[test]
578    fn test_account_component_multiple_auth_procedures() {
579        let code_with_multiple_auth = "
580            @auth_script
581            pub proc auth_basic
582                push.1 drop
583            end
584
585            @auth_script
586            pub proc auth_secondary
587                push.0 drop
588            end
589        ";
590
591        let package = assemble_test_package(
592            "test-account-code-multiple-auth",
593            "test::account_code_multiple_auth",
594            code_with_multiple_auth,
595        );
596        let metadata = AccountComponentMetadata::new("test::multiple_auth");
597        let component = AccountComponent::new(package, vec![], metadata).unwrap();
598
599        let err = AccountCode::from_components(&[component]).unwrap_err();
600
601        assert_matches!(err, AccountError::AccountComponentMultipleAuthProcedures);
602    }
603
604    /// Tests that the auth procedure is at index 0 even if its MAST root was already added by a
605    /// previously processed non-auth component, no matter at which index that component sits.
606    #[rstest]
607    #[case::duplicate_first(true)]
608    #[case::duplicate_second(false)]
609    fn test_account_code_auth_procedure_at_index_zero_on_duplicate_root(
610        #[case] duplicate_first: bool,
611    ) -> anyhow::Result<()> {
612        // Same body as the auth procedure of NoopAuthComponent, so it has the same MAST root.
613        let duplicate_of_auth = "
614            @account_procedure
615            pub proc noop
616                push.0 drop
617            end
618        ";
619        let duplicate_component = AccountComponent::new(
620            assemble_test_package(
621                "test-account-code-duplicate-auth-root",
622                "test::duplicate_auth_root",
623                duplicate_of_auth,
624            ),
625            vec![],
626            AccountComponentMetadata::new("test::duplicate_auth_root"),
627        )?;
628
629        let other_component = AccountComponent::new(
630            assemble_test_package("test-account-code-other", "test::other", CODE),
631            vec![],
632            AccountComponentMetadata::new("test::other"),
633        )?;
634
635        let auth_component = AccountComponent::from(NoopAuthComponent);
636        let auth_proc_root = auth_component
637            .procedures()
638            .find_map(|(proc_root, is_auth)| is_auth.then_some(proc_root))
639            .context("auth component should export an auth procedure")?;
640
641        // Without this the test would not cover the deduplication path it guards.
642        let duplicate_proc_root = duplicate_component
643            .procedures()
644            .next()
645            .context("duplicate component should export a procedure")?
646            .0;
647        assert_eq!(duplicate_proc_root, auth_proc_root);
648
649        let components = if duplicate_first {
650            [duplicate_component, other_component, auth_component]
651        } else {
652            [other_component, duplicate_component, auth_component]
653        };
654
655        let code = AccountCode::from_components(&components)?;
656
657        assert_eq!(code.procedures()[0], auth_proc_root);
658        // The procedure displaced by moving the auth procedure to index 0 must be retained.
659        assert_eq!(code.num_procedures(), 3);
660
661        Ok(())
662    }
663
664    #[test]
665    fn test_account_code_from_parts_rejects_duplicate_roots() {
666        let code = AccountCode::mock();
667        let procedures = code.procedures();
668
669        // repeat the non-auth procedure root at a second index
670        let duplicated = vec![procedures[0], procedures[1], procedures[1]];
671        let err = AccountCode::from_parts(code.mast(), duplicated).unwrap_err();
672
673        assert_matches!(
674            err,
675            AccountError::AccountCodeDuplicateProcedureRoot(root) if root == procedures[1]
676        );
677    }
678
679    #[test]
680    fn test_account_code_from_parts_rejects_missing_root() {
681        let code = AccountCode::mock();
682        let procedures = code.procedures();
683        let non_existent_root = AccountProcedureRoot::from_raw(Word::from([1, 2, 3, 4u32]));
684
685        // provide a procedure root that is not in the mast forest
686        let procedures = vec![procedures[0], non_existent_root];
687        let err = AccountCode::from_parts(code.mast(), procedures).unwrap_err();
688
689        assert_matches!(
690            err,
691            AccountError::AccountCodeProcedureNotInMastForest(root) if root == non_existent_root
692        );
693    }
694
695    #[test]
696    fn test_account_code_deserialization_rejects_duplicate_roots() {
697        let code = AccountCode::mock();
698        let procedures = code.procedures();
699
700        let mut bytes = Vec::new();
701        code.mast().write_into(&mut bytes);
702        bytes.write_u8(3 - 1); // num_procedures is serialized as count - 1
703        procedures[0].write_into(&mut bytes);
704        procedures[1].write_into(&mut bytes);
705        procedures[1].write_into(&mut bytes);
706
707        let err = AccountCode::read_from_bytes(&bytes).unwrap_err();
708
709        assert_matches!(
710            err,
711            DeserializationError::InvalidValue(msg) if msg.contains("duplicate procedure with root")
712        );
713    }
714
715    #[test]
716    fn account_code_procedures_are_sorted_after_the_auth_procedure() {
717        let code = AccountCode::mock();
718
719        assert!(code.procedures()[1..].is_sorted());
720    }
721
722    #[test]
723    fn account_code_commitment_is_independent_of_component_order() -> anyhow::Result<()> {
724        let first = mock_component("test-account-code-first", "test::first", 1);
725        let second = mock_component("test-account-code-second", "test::second", 2);
726
727        let mut components = vec![NoopAuthComponent.into(), first.clone(), second.clone()];
728
729        let code = AccountCode::from_components(&components)?;
730        components.reverse();
731        let reversed_code = AccountCode::from_components(&components)?;
732
733        assert_eq!(code.commitment(), reversed_code.commitment());
734        assert_eq!(
735            code.procedures()[0],
736            reversed_code.procedures()[0],
737            "the auth procedure should stay at index 0"
738        );
739
740        Ok(())
741    }
742
743    #[test]
744    fn account_code_from_parts_rejects_unsorted_procedures() -> anyhow::Result<()> {
745        let code = AccountCode::mock();
746        let procedures = code.procedures();
747
748        // the procedures of the mock code are sorted, so swapping two of them breaks the invariant
749        let unsorted = vec![procedures[0], procedures[2], procedures[1]];
750        let err = AccountCode::from_parts(code.mast(), unsorted).unwrap_err();
751
752        assert_matches!(err, AccountError::AccountCodeProceduresUnsorted);
753
754        Ok(())
755    }
756
757    #[test]
758    fn account_code_from_parts_rejects_duplicated_auth_procedure() -> anyhow::Result<()> {
759        let code = AccountCode::mock();
760        let procedures = code.procedures();
761
762        // the auth procedure must not reappear among the sorted procedures
763        let mut duplicated_auth = vec![procedures[0], procedures[1], procedures[0]];
764        duplicated_auth[1..].sort_unstable();
765        let err = AccountCode::from_parts(code.mast(), duplicated_auth).unwrap_err();
766
767        assert_matches!(
768            err,
769            AccountError::AccountCodeDuplicateProcedureRoot(root) if root == procedures[0]
770        );
771
772        Ok(())
773    }
774
775    /// Creates a component exporting a single account procedure whose MAST root is made unique by
776    /// the provided value.
777    fn mock_component(
778        package_name: &str,
779        module_path: &str,
780        unique_value: u32,
781    ) -> AccountComponent {
782        let code = format!(
783            "
784            @account_procedure
785            pub proc account_procedure
786                push.{unique_value} drop
787            end
788            "
789        );
790        let package = assemble_test_package(package_name, module_path, &code);
791        let metadata = AccountComponentMetadata::new(module_path);
792
793        AccountComponent::new(package, vec![], metadata).expect("component should be valid")
794    }
795}