miden_objects/account/
mod.rs

1use crate::{
2    AccountError, Digest, Felt, Hasher, Word, ZERO,
3    asset::AssetVault,
4    utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
5};
6
7mod account_id;
8pub use account_id::{
9    AccountId, AccountIdPrefix, AccountIdPrefixV0, AccountIdV0, AccountIdVersion,
10    AccountStorageMode, AccountType, AddressType, NetworkId,
11};
12
13pub mod auth;
14
15pub use auth::AuthSecretKey;
16
17mod builder;
18pub use builder::AccountBuilder;
19
20pub mod code;
21pub use code::{AccountCode, procedure::AccountProcedureInfo};
22
23pub mod component;
24pub use component::{
25    AccountComponent, AccountComponentMetadata, AccountComponentTemplate, FeltRepresentation,
26    InitStorageData, MapEntry, MapRepresentation, PlaceholderTypeRequirement, StorageEntry,
27    StorageValueName, StorageValueNameError, TemplateType, TemplateTypeError, WordRepresentation,
28};
29
30pub mod delta;
31pub use delta::{
32    AccountDelta, AccountStorageDelta, AccountVaultDelta, FungibleAssetDelta,
33    NonFungibleAssetDelta, NonFungibleDeltaAction, StorageMapDelta,
34};
35
36mod storage;
37pub use storage::{
38    AccountStorage, AccountStorageHeader, PartialStorage, StorageMap, StorageSlot, StorageSlotType,
39};
40
41mod header;
42pub use header::AccountHeader;
43
44mod file;
45pub use file::AccountFile;
46
47mod partial;
48pub use partial::PartialAccount;
49
50// ACCOUNT
51// ================================================================================================
52
53/// An account which can store assets and define rules for manipulating them.
54///
55/// An account consists of the following components:
56/// - Account ID, which uniquely identifies the account and also defines basic properties of the
57///   account.
58/// - Account vault, which stores assets owned by the account.
59/// - Account storage, which is a key-value map (both keys and values are words) used to store
60///   arbitrary user-defined data.
61/// - Account code, which is a set of Miden VM programs defining the public interface of the
62///   account.
63/// - Account nonce, a value which is incremented whenever account state is updated.
64///
65/// Out of the above components account ID is always immutable (once defined it can never be
66/// changed). Other components may be mutated throughout the lifetime of the account. However,
67/// account state can be changed only by invoking one of account interface methods.
68///
69/// The recommended way to build an account is through an [`AccountBuilder`], which can be
70/// instantiated through [`Account::builder`]. See the type's documentation for details.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Account {
73    id: AccountId,
74    vault: AssetVault,
75    storage: AccountStorage,
76    code: AccountCode,
77    nonce: Felt,
78}
79
80impl Account {
81    // CONSTRUCTORS
82    // --------------------------------------------------------------------------------------------
83
84    /// Returns an [Account] instantiated with the provided components.
85    pub fn from_parts(
86        id: AccountId,
87        vault: AssetVault,
88        storage: AccountStorage,
89        code: AccountCode,
90        nonce: Felt,
91    ) -> Self {
92        Self { id, vault, storage, code, nonce }
93    }
94
95    /// Creates an account's [`AccountCode`] and [`AccountStorage`] from the provided components.
96    ///
97    /// This merges all libraries of the components into a single
98    /// [`MastForest`](vm_processor::MastForest) to produce the [`AccountCode`]. For each
99    /// procedure in the resulting forest, the storage offset and size are set so that the
100    /// procedure can only access the storage slots of the component in which it was defined and
101    /// each component's storage offset is the total number of slots in the previous components.
102    /// To illustrate, given two components with one and two storage slots respectively:
103    ///
104    /// - RpoFalcon512 Component: Component slot 0 stores the public key.
105    /// - Custom Component: Component slot 0 stores a custom [`StorageSlot::Value`] and component
106    ///   slot 1 stores a custom [`StorageSlot::Map`].
107    ///
108    /// When combined, their assigned slots in the [`AccountStorage`] would be:
109    ///
110    /// - The RpoFalcon512 Component has offset 0 and size 1: Account slot 0 stores the public key.
111    /// - The Custom Component has offset 1 and size 2: Account slot 1 stores the value and account
112    ///   slot 2 stores the map.
113    ///
114    /// The resulting commitments from code and storage can then be used to construct an
115    /// [`AccountId`]. Finally, a new account can then be instantiated from those parts using
116    /// [`Account::new`].
117    ///
118    /// If the account type is faucet the reserved slot (slot 0) will be initialized.
119    /// - For Fungible Faucets the value is [`StorageSlot::empty_value`].
120    /// - For Non-Fungible Faucets the value is [`StorageSlot::empty_map`].
121    ///
122    /// If the storage needs to be initialized with certain values in that slot, those can be added
123    /// after construction with the standard set methods for items and maps.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if:
128    /// - Any of the components does not support `account_type`.
129    /// - The number of procedures in all merged libraries is 0 or exceeds
130    ///   [`AccountCode::MAX_NUM_PROCEDURES`].
131    /// - Two or more libraries export a procedure with the same MAST root.
132    /// - The first component doesn't contain exactly one authentication procedure.
133    /// - Other components contain authentication procedures.
134    /// - The number of [`StorageSlot`]s of all components exceeds 255.
135    /// - [`MastForest::merge`](vm_processor::MastForest::merge) fails on all libraries.
136    pub(super) fn initialize_from_components(
137        account_type: AccountType,
138        components: &[AccountComponent],
139    ) -> Result<(AccountCode, AccountStorage), AccountError> {
140        validate_components_support_account_type(components, account_type)?;
141
142        let code = AccountCode::from_components_unchecked(components, account_type)?;
143        let storage = AccountStorage::from_components(components, account_type)?;
144
145        Ok((code, storage))
146    }
147
148    /// Creates a new [`AccountBuilder`] for an account and sets the initial seed from which the
149    /// grinding process for that account's [`AccountId`] will start.
150    ///
151    /// This initial seed should come from a cryptographic random number generator.
152    pub fn builder(init_seed: [u8; 32]) -> AccountBuilder {
153        AccountBuilder::new(init_seed)
154    }
155
156    // PUBLIC ACCESSORS
157    // --------------------------------------------------------------------------------------------
158
159    /// Returns the commitment of this account.
160    ///
161    /// The commitment of an account is computed as hash(id, nonce, vault_root, storage_commitment,
162    /// code_commitment). Computing the account commitment requires 2 permutations of the hash
163    /// function.
164    pub fn commitment(&self) -> Digest {
165        hash_account(
166            self.id,
167            self.nonce,
168            self.vault.root(),
169            self.storage.commitment(),
170            self.code.commitment(),
171        )
172    }
173
174    /// Returns the commitment of this account as used for the initial account state commitment in
175    /// transaction proofs.
176    ///
177    /// For existing accounts, this is exactly the same as [Account::commitment()], however, for new
178    /// accounts this value is set to [crate::EMPTY_WORD]. This is because when a transaction is
179    /// executed against a new account, public input for the initial account state is set to
180    /// [crate::EMPTY_WORD] to distinguish new accounts from existing accounts. The actual
181    /// commitment of the initial account state (and the initial state itself), are provided to
182    /// the VM via the advice provider.
183    pub fn init_commitment(&self) -> Digest {
184        if self.is_new() {
185            Digest::default()
186        } else {
187            self.commitment()
188        }
189    }
190
191    /// Returns unique identifier of this account.
192    pub fn id(&self) -> AccountId {
193        self.id
194    }
195
196    /// Returns the account type
197    pub fn account_type(&self) -> AccountType {
198        self.id.account_type()
199    }
200
201    /// Returns a reference to the vault of this account.
202    pub fn vault(&self) -> &AssetVault {
203        &self.vault
204    }
205
206    /// Returns a reference to the storage of this account.
207    pub fn storage(&self) -> &AccountStorage {
208        &self.storage
209    }
210
211    /// Returns a reference to the code of this account.
212    pub fn code(&self) -> &AccountCode {
213        &self.code
214    }
215
216    /// Returns nonce for this account.
217    pub fn nonce(&self) -> Felt {
218        self.nonce
219    }
220
221    /// Returns true if this account can issue assets.
222    pub fn is_faucet(&self) -> bool {
223        self.id.is_faucet()
224    }
225
226    /// Returns true if this is a regular account.
227    pub fn is_regular_account(&self) -> bool {
228        self.id.is_regular_account()
229    }
230
231    /// Returns `true` if the full state of the account is on chain, i.e. if the storage modes are
232    /// [`AccountStorageMode::Public`] or [`AccountStorageMode::Network`], `false` otherwise.
233    pub fn is_onchain(&self) -> bool {
234        self.id().is_onchain()
235    }
236
237    /// Returns `true` if the storage mode is [`AccountStorageMode::Public`], `false` otherwise.
238    pub fn is_public(&self) -> bool {
239        self.id().is_public()
240    }
241
242    /// Returns `true` if the storage mode is [`AccountStorageMode::Private`], `false` otherwise.
243    pub fn is_private(&self) -> bool {
244        self.id().is_private()
245    }
246
247    /// Returns `true` if the storage mode is [`AccountStorageMode::Network`], `false` otherwise.
248    pub fn is_network(&self) -> bool {
249        self.id().is_network()
250    }
251
252    /// Returns true if the account is new (i.e. it has not been initialized yet).
253    pub fn is_new(&self) -> bool {
254        self.nonce == ZERO
255    }
256
257    /// Decomposes the account into the underlying account components.
258    pub fn into_parts(self) -> (AccountId, AssetVault, AccountStorage, AccountCode, Felt) {
259        (self.id, self.vault, self.storage, self.code, self.nonce)
260    }
261
262    // DATA MUTATORS
263    // --------------------------------------------------------------------------------------------
264
265    /// Applies the provided delta to this account. This updates account vault, storage, and nonce
266    /// to the values specified by the delta.
267    ///
268    /// # Errors
269    /// Returns an error if:
270    /// - Applying vault sub-delta to the vault of this account fails.
271    /// - Applying storage sub-delta to the storage of this account fails.
272    /// - The nonce specified in the provided delta smaller than or equal to the current account
273    ///   nonce.
274    pub fn apply_delta(&mut self, delta: &AccountDelta) -> Result<(), AccountError> {
275        // update vault; we don't check vault delta validity here because `AccountDelta` can contain
276        // only valid vault deltas
277        self.vault
278            .apply_delta(delta.vault())
279            .map_err(AccountError::AssetVaultUpdateError)?;
280
281        // update storage
282        self.storage.apply_delta(delta.storage())?;
283
284        // update nonce
285        self.increment_nonce(delta.nonce_delta())?;
286
287        Ok(())
288    }
289
290    /// Increments the nonce of this account by the provided increment.
291    ///
292    /// # Errors
293    ///
294    /// Returns an error if:
295    /// - Incrementing the nonce overflows a [`Felt`].
296    pub fn increment_nonce(&mut self, nonce_delta: Felt) -> Result<(), AccountError> {
297        let new_nonce = self.nonce + nonce_delta;
298
299        if new_nonce.as_int() < self.nonce.as_int() {
300            return Err(AccountError::NonceOverflow {
301                current: self.nonce,
302                increment: nonce_delta,
303                new: new_nonce,
304            });
305        }
306
307        self.nonce = new_nonce;
308
309        Ok(())
310    }
311
312    // TEST HELPERS
313    // --------------------------------------------------------------------------------------------
314
315    #[cfg(any(feature = "testing", test))]
316    /// Returns a mutable reference to the vault of this account.
317    pub fn vault_mut(&mut self) -> &mut AssetVault {
318        &mut self.vault
319    }
320
321    #[cfg(any(feature = "testing", test))]
322    /// Returns a mutable reference to the storage of this account.
323    pub fn storage_mut(&mut self) -> &mut AccountStorage {
324        &mut self.storage
325    }
326}
327
328// SERIALIZATION
329// ================================================================================================
330
331impl Serializable for Account {
332    fn write_into<W: ByteWriter>(&self, target: &mut W) {
333        let Account { id, vault, storage, code, nonce } = self;
334
335        id.write_into(target);
336        vault.write_into(target);
337        storage.write_into(target);
338        code.write_into(target);
339        nonce.write_into(target);
340    }
341
342    fn get_size_hint(&self) -> usize {
343        self.id.get_size_hint()
344            + self.vault.get_size_hint()
345            + self.storage.get_size_hint()
346            + self.code.get_size_hint()
347            + self.nonce.get_size_hint()
348    }
349}
350
351impl Deserializable for Account {
352    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
353        let id = AccountId::read_from(source)?;
354        let vault = AssetVault::read_from(source)?;
355        let storage = AccountStorage::read_from(source)?;
356        let code = AccountCode::read_from(source)?;
357        let nonce = Felt::read_from(source)?;
358
359        Ok(Self::from_parts(id, vault, storage, code, nonce))
360    }
361}
362
363// HELPERS
364// ================================================================================================
365
366/// Returns hash of an account with the specified ID, nonce, vault root, storage commitment, and
367/// code commitment.
368///
369/// Hash of an account is computed as hash(id, nonce, vault_root, storage_commitment,
370/// code_commitment). Computing the account commitment requires 2 permutations of the hash function.
371pub fn hash_account(
372    id: AccountId,
373    nonce: Felt,
374    vault_root: Digest,
375    storage_commitment: Digest,
376    code_commitment: Digest,
377) -> Digest {
378    let mut elements = [ZERO; 16];
379    elements[0] = id.suffix();
380    elements[1] = id.prefix().as_felt();
381    elements[3] = nonce;
382    elements[4..8].copy_from_slice(&*vault_root);
383    elements[8..12].copy_from_slice(&*storage_commitment);
384    elements[12..].copy_from_slice(&*code_commitment);
385    Hasher::hash_elements(&elements)
386}
387
388/// Validates that all `components` support the given `account_type`.
389fn validate_components_support_account_type(
390    components: &[AccountComponent],
391    account_type: AccountType,
392) -> Result<(), AccountError> {
393    for (component_index, component) in components.iter().enumerate() {
394        if !component.supports_type(account_type) {
395            return Err(AccountError::UnsupportedComponentForAccountType {
396                account_type,
397                component_index,
398            });
399        }
400    }
401
402    Ok(())
403}
404
405// TESTS
406// ================================================================================================
407
408#[cfg(test)]
409mod tests {
410    use alloc::vec::Vec;
411
412    use assembly::Assembler;
413    use assert_matches::assert_matches;
414    use miden_crypto::{
415        Felt, Word,
416        utils::{Deserializable, Serializable},
417    };
418    use vm_processor::Digest;
419
420    use super::{
421        AccountCode, AccountDelta, AccountId, AccountStorage, AccountStorageDelta,
422        AccountVaultDelta,
423    };
424    use crate::{
425        AccountError,
426        account::{
427            Account, AccountComponent, AccountType, StorageMap, StorageMapDelta, StorageSlot,
428        },
429        asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset},
430        testing::{
431            account_component::NoopAuthComponent,
432            account_id::{
433                ACCOUNT_ID_PRIVATE_SENDER, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
434            },
435            storage::AccountStorageDeltaBuilder,
436        },
437    };
438
439    #[test]
440    fn test_serde_account() {
441        let init_nonce = Felt::new(1);
442        let asset_0 = FungibleAsset::mock(99);
443        let word = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)];
444        let storage_slot = StorageSlot::Value(word);
445        let account = build_account(vec![asset_0], init_nonce, vec![storage_slot]);
446
447        let serialized = account.to_bytes();
448        let deserialized = Account::read_from_bytes(&serialized).unwrap();
449        assert_eq!(deserialized, account);
450    }
451
452    #[test]
453    fn test_serde_account_delta() {
454        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
455        let nonce_delta = Felt::new(2);
456        let asset_0 = FungibleAsset::mock(15);
457        let asset_1 = NonFungibleAsset::mock(&[5, 5, 5]);
458        let storage_delta = AccountStorageDeltaBuilder::new()
459            .add_cleared_items([0])
460            .add_updated_values([(1_u8, [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)])])
461            .build()
462            .unwrap();
463        let account_delta = build_account_delta(
464            account_id,
465            vec![asset_1],
466            vec![asset_0],
467            nonce_delta,
468            storage_delta,
469        );
470
471        let serialized = account_delta.to_bytes();
472        let deserialized = AccountDelta::read_from_bytes(&serialized).unwrap();
473        assert_eq!(deserialized, account_delta);
474    }
475
476    #[test]
477    fn valid_account_delta_is_correctly_applied() {
478        // build account
479        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
480        let init_nonce = Felt::new(1);
481        let asset_0 = FungibleAsset::mock(100);
482        let asset_1 = NonFungibleAsset::mock(&[1, 2, 3]);
483
484        // build storage slots
485        let storage_slot_value_0 =
486            StorageSlot::Value([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]);
487        let storage_slot_value_1 =
488            StorageSlot::Value([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]);
489        let mut storage_map = StorageMap::with_entries([
490            (
491                Digest::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]),
492                [Felt::new(1_u64), Felt::new(2_u64), Felt::new(3_u64), Felt::new(4_u64)],
493            ),
494            (
495                Digest::new([Felt::new(105), Felt::new(106), Felt::new(107), Felt::new(108)]),
496                [Felt::new(5_u64), Felt::new(6_u64), Felt::new(7_u64), Felt::new(8_u64)],
497            ),
498        ])
499        .unwrap();
500        let storage_slot_map = StorageSlot::Map(storage_map.clone());
501
502        let mut account = build_account(
503            vec![asset_0],
504            init_nonce,
505            vec![storage_slot_value_0, storage_slot_value_1, storage_slot_map],
506        );
507
508        // update storage map
509        let new_map_entry = (
510            Digest::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]),
511            [Felt::new(9_u64), Felt::new(10_u64), Felt::new(11_u64), Felt::new(12_u64)],
512        );
513
514        let updated_map =
515            StorageMapDelta::from_iters([], [(new_map_entry.0.into(), new_map_entry.1)]);
516        storage_map.insert(new_map_entry.0, new_map_entry.1);
517
518        // build account delta
519        let final_nonce = Felt::new(2);
520        let storage_delta = AccountStorageDeltaBuilder::new()
521            .add_cleared_items([0])
522            .add_updated_values([(1, [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)])])
523            .add_updated_maps([(2, updated_map)])
524            .build()
525            .unwrap();
526        let account_delta = build_account_delta(
527            account_id,
528            vec![asset_1],
529            vec![asset_0],
530            final_nonce - init_nonce,
531            storage_delta,
532        );
533
534        // apply delta and create final_account
535        account.apply_delta(&account_delta).unwrap();
536
537        let final_account = build_account(
538            vec![asset_1],
539            final_nonce,
540            vec![
541                StorageSlot::Value(Word::default()),
542                StorageSlot::Value([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]),
543                StorageSlot::Map(storage_map),
544            ],
545        );
546
547        // assert account is what it should be
548        assert_eq!(account, final_account);
549    }
550
551    #[test]
552    #[should_panic]
553    fn valid_account_delta_with_unchanged_nonce() {
554        // build account
555        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
556        let init_nonce = Felt::new(1);
557        let asset = FungibleAsset::mock(110);
558        let mut account =
559            build_account(vec![asset], init_nonce, vec![StorageSlot::Value(Word::default())]);
560
561        // build account delta
562        let storage_delta = AccountStorageDeltaBuilder::new()
563            .add_cleared_items([0])
564            .add_updated_values([(1_u8, [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)])])
565            .build()
566            .unwrap();
567        let account_delta =
568            build_account_delta(account_id, vec![], vec![asset], init_nonce, storage_delta);
569
570        // apply delta
571        account.apply_delta(&account_delta).unwrap()
572    }
573
574    #[test]
575    #[should_panic]
576    fn valid_account_delta_with_decremented_nonce() {
577        // build account
578        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
579        let init_nonce = Felt::new(2);
580        let asset = FungibleAsset::mock(100);
581        let mut account =
582            build_account(vec![asset], init_nonce, vec![StorageSlot::Value(Word::default())]);
583
584        // build account delta
585        let final_nonce = Felt::new(1);
586        let storage_delta = AccountStorageDeltaBuilder::new()
587            .add_cleared_items([0])
588            .add_updated_values([(1_u8, [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)])])
589            .build()
590            .unwrap();
591        let account_delta =
592            build_account_delta(account_id, vec![], vec![asset], final_nonce, storage_delta);
593
594        // apply delta
595        account.apply_delta(&account_delta).unwrap()
596    }
597
598    #[test]
599    fn empty_account_delta_with_incremented_nonce() {
600        // build account
601        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
602        let init_nonce = Felt::new(1);
603        let word = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)];
604        let storage_slot = StorageSlot::Value(word);
605        let mut account = build_account(vec![], init_nonce, vec![storage_slot]);
606
607        // build account delta
608        let nonce_delta = Felt::new(1);
609        let account_delta = AccountDelta::new(
610            account_id,
611            AccountStorageDelta::new(),
612            AccountVaultDelta::default(),
613            nonce_delta,
614        )
615        .unwrap();
616
617        // apply delta
618        account.apply_delta(&account_delta).unwrap()
619    }
620
621    pub fn build_account_delta(
622        account_id: AccountId,
623        added_assets: Vec<Asset>,
624        removed_assets: Vec<Asset>,
625        nonce_delta: Felt,
626        storage_delta: AccountStorageDelta,
627    ) -> AccountDelta {
628        let vault_delta = AccountVaultDelta::from_iters(added_assets, removed_assets);
629        AccountDelta::new(account_id, storage_delta, vault_delta, nonce_delta).unwrap()
630    }
631
632    pub fn build_account(assets: Vec<Asset>, nonce: Felt, slots: Vec<StorageSlot>) -> Account {
633        let id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
634        let code = AccountCode::mock();
635
636        let vault = AssetVault::new(&assets).unwrap();
637
638        let storage = AccountStorage::new(slots).unwrap();
639
640        Account::from_parts(id, vault, storage, code, nonce)
641    }
642
643    /// Tests that initializing code and storage from a component which does not support the given
644    /// account type returns an error.
645    #[test]
646    fn test_account_unsupported_component_type() {
647        let code1 = "export.foo add end";
648        let library1 = Assembler::default().assemble_library([code1]).unwrap();
649
650        // This component support all account types except the regular account with updatable code.
651        let component1 = AccountComponent::new(library1, vec![])
652            .unwrap()
653            .with_supported_type(AccountType::FungibleFaucet)
654            .with_supported_type(AccountType::NonFungibleFaucet)
655            .with_supported_type(AccountType::RegularAccountImmutableCode);
656
657        let err = Account::initialize_from_components(
658            AccountType::RegularAccountUpdatableCode,
659            &[component1],
660        )
661        .unwrap_err();
662
663        assert!(matches!(
664            err,
665            AccountError::UnsupportedComponentForAccountType {
666                account_type: AccountType::RegularAccountUpdatableCode,
667                component_index: 0
668            }
669        ))
670    }
671
672    /// Two components who export a procedure with the same MAST root should fail to convert into
673    /// code and storage.
674    #[test]
675    fn test_account_duplicate_exported_mast_root() {
676        let code1 = "export.foo add eq.1 end";
677        let code2 = "export.bar add eq.1 end";
678
679        let library1 = Assembler::default().assemble_library([code1]).unwrap();
680        let library2 = Assembler::default().assemble_library([code2]).unwrap();
681
682        let auth_component: AccountComponent =
683            NoopAuthComponent::new(Assembler::default()).unwrap().into();
684        let component1 = AccountComponent::new(library1, vec![]).unwrap().with_supports_all_types();
685        let component2 = AccountComponent::new(library2, vec![]).unwrap().with_supports_all_types();
686
687        let err = Account::initialize_from_components(
688            AccountType::RegularAccountUpdatableCode,
689            &[auth_component, component1, component2],
690        )
691        .unwrap_err();
692
693        assert_matches!(err, AccountError::AccountComponentDuplicateProcedureRoot(_))
694    }
695}