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