Skip to main content

miden_protocol/account/builder/
mod.rs

1use alloc::boxed::Box;
2use alloc::vec::Vec;
3
4use crate::account::component::StorageSchema;
5use crate::account::{
6    Account,
7    AccountCode,
8    AccountComponent,
9    AccountId,
10    AccountIdV1,
11    AccountIdVersion,
12    AccountStorage,
13    AccountType,
14    AssetCallbackFlag,
15};
16use crate::asset::AssetVault;
17use crate::errors::AccountError;
18use crate::{Felt, Word};
19
20/// A convenient builder for an [`Account`] allowing for safe construction of an account by
21/// combining multiple [`AccountComponent`]s.
22///
23/// This will build a valid new account with these properties:
24/// - An empty [`AssetVault`].
25/// - The nonce set to [`Felt::ZERO`].
26/// - A seed which results in an [`AccountId`] valid for the configured account type.
27///
28/// By default, the builder is initialized with:
29/// - The `account_type` set to [`AccountType::Private`].
30/// - The `version` set to [`AccountIdVersion::Version1`].
31///
32/// **Asset Callbacks**
33///
34/// The [`AssetCallbackFlag`] determines whether the tx kernel dispatches asset callbacks for assets
35/// issued by the account (if any) and is encoded into the resulting [`AccountId`] at creation. Note
36/// that the flag only enables the dispatch: whether a callback actually runs additionally depends
37/// on the corresponding callback slot being present and holding a non-empty procedure root, so an
38/// enabled flag means callbacks may be invoked, not that the account has any.
39///
40/// The flag is derived from the account's storage: it is [`AssetCallbackFlag::Enabled`] if any
41/// component installs one of the protocol-reserved asset callback slots (see
42/// [`AccountStorage::has_callback_slots`]) and [`AssetCallbackFlag::Disabled`] otherwise. There is
43/// deliberately no way to disable the flag for an account that does install such a slot, since the
44/// tx kernel gates callback invocation on the flag alone and the flag cannot be changed after the
45/// ID is ground, so such a callback could never be invoked.
46///
47/// The converse is allowed: [`AccountBuilder::enable_asset_callbacks`] enables the flag without
48/// installing a callback slot, so that the account retains the ability to add a callback slot via
49/// an account upgrade later. This is particularly useful if new types of callbacks are introduced.
50///
51/// An enabled flag makes the account's state a required input of every transaction that moves one
52/// of its assets: dispatching a callback starts a foreign context against the issuing account, and
53/// the foreign state is loaded before the callback slot is looked up, so the load happens even when
54/// no callback procedure root is registered. An [`AccountType::Private`] account publishes only its
55/// commitment, so its holders have to obtain that state out of band. Both the account type and the
56/// flag are immutable parts of the [`AccountId`], so this is settled at creation.
57///
58/// [`AccountBuilder::with_component`] (or [`AccountBuilder::with_components`]) must be called at
59/// least once, and exactly one of the added components must be an authentication component (i.e. a
60/// component exporting a procedure marked with the `@auth_script` attribute). The auth component is
61/// identified and extracted automatically when [`AccountBuilder::build`] is called.
62///
63/// # Security
64///
65/// The builder only enforces the structural requirement of exactly one auth component; it does not
66/// check that the auth component is a sensible choice for the other components on the account. In
67/// particular, an auth component that performs no authentication makes the account permissionless:
68/// every state-changing procedure it exposes can be called by anyone. This is especially dangerous
69/// when combined with components that rely on the auth component as their sole access gate (such as
70/// authority-controlled setters), which then become permissionless as well. Higher-level factory
71/// functions vet these combinations; when building an account directly, the caller is responsible
72/// for pairing a suitable auth component with the account's other components.
73///
74/// Under the `testing` feature, it is possible to:
75/// - Build an existing account using `AccountBuilder::build_existing`, which will set the account's
76///   nonce to `1` by default, or to the configured value.
77/// - Add assets to the account's vault; this only succeeds when using
78///   `AccountBuilder::build_existing`.
79///
80/// **Account Procedure Order**
81///
82/// Note that the auth procedure is always moved to the first position, since the tx kernel assumes
83/// procedure index 0 is the auth procedure within an [`AccountCode`]. The procedures of all other
84/// components are merged and sorted, so the order in which `with_component` is called does not
85/// affect the resulting account code commitment.
86#[derive(Debug, Clone)]
87pub struct AccountBuilder {
88    #[cfg(any(feature = "testing", test))]
89    assets: Vec<crate::asset::Asset>,
90    #[cfg(any(feature = "testing", test))]
91    nonce: Option<Felt>,
92    components: Vec<AccountComponent>,
93    account_type: AccountType,
94    asset_callbacks: AssetCallbackFlag,
95    init_seed: [u8; 32],
96    id_version: AccountIdVersion,
97}
98
99impl AccountBuilder {
100    /// Creates a new builder for an account and sets the initial seed from which the grinding
101    /// process for that account's [`AccountId`] will start.
102    ///
103    /// This initial seed should come from a cryptographic random number generator.
104    pub fn new(init_seed: [u8; 32]) -> Self {
105        Self {
106            #[cfg(any(feature = "testing", test))]
107            assets: vec![],
108            #[cfg(any(feature = "testing", test))]
109            nonce: None,
110            components: vec![],
111            init_seed,
112            account_type: AccountType::Private,
113            asset_callbacks: AssetCallbackFlag::Disabled,
114            id_version: AccountIdVersion::Version1,
115        }
116    }
117
118    /// Sets the [`AccountIdVersion`] of the account ID.
119    pub fn version(mut self, version: AccountIdVersion) -> Self {
120        self.id_version = version;
121        self
122    }
123
124    /// Sets the account type of the account.
125    pub fn account_type(mut self, account_type: AccountType) -> Self {
126        self.account_type = account_type;
127        self
128    }
129
130    /// Enables the immutable [`AssetCallbackFlag`] of the account even if none of its components
131    /// install an asset callback slot.
132    ///
133    /// See the [type-level docs](AccountBuilder#asset-callbacks) for details.
134    pub fn enable_asset_callbacks(mut self) -> Self {
135        self.asset_callbacks = AssetCallbackFlag::Enabled;
136        self
137    }
138
139    /// Adds an [`AccountComponent`] to the builder. This method can be called multiple times and
140    /// **must be called at least once** since an account must export at least one procedure.
141    ///
142    /// All components will be merged to form the final code and storage of the built account.
143    /// Exactly one of the added components must be an authentication component (see
144    /// [`AccountComponent::is_auth_component`]); it is identified and moved to the front of the
145    /// procedure list automatically when [`Self::build`] is called, while all other procedures are
146    /// sorted.
147    ///
148    /// For composite configurations that expand into multiple components (such as
149    /// `AccessControl` or `TokenPolicyManager`), use [`Self::with_components`].
150    pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
151        self.components.push(account_component.into());
152        self
153    }
154
155    /// Adds the components yielded by `components` to the builder.
156    ///
157    /// This is a convenience wrapper around repeated [`Self::with_component`] calls. It is
158    /// most useful for installing the variable number of components produced by composite
159    /// configurations whose component count is not known at the call site (for example, a
160    /// configuration value that expands into one or several components depending on its
161    /// variant).
162    pub fn with_components(
163        mut self,
164        components: impl IntoIterator<Item = impl Into<AccountComponent>>,
165    ) -> Self {
166        for component in components {
167            self = self.with_component(component);
168        }
169        self
170    }
171
172    /// Returns an iterator of storage schemas attached to the builder's components.
173    pub fn storage_schemas(&self) -> impl Iterator<Item = &StorageSchema> + '_ {
174        self.components.iter().map(|component| component.storage_schema())
175    }
176
177    /// Builds the common parts of testing and non-testing code.
178    fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
179        #[cfg(any(feature = "testing", test))]
180        let vault = AssetVault::new(&self.assets).map_err(|err| {
181            AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
182        })?;
183
184        #[cfg(all(not(feature = "testing"), not(test)))]
185        let vault = AssetVault::default();
186
187        // The build method does not access components, so it is safe to `take` them out.
188        let components = core::mem::take(&mut self.components);
189        let (code, storage) = Account::initialize_from_components(components).map_err(|err| {
190            AccountError::BuildError(
191                "account components failed to build".into(),
192                Some(Box::new(err)),
193            )
194        })?;
195
196        Ok((vault, code, storage))
197    }
198
199    /// Derives the account's [`AssetCallbackFlag`] from the asset callback slots installed by its
200    /// components.
201    ///
202    /// See the [type-level docs](AccountBuilder#asset-callbacks) for details.
203    fn derive_asset_callbacks(&self, storage: &AccountStorage) -> AssetCallbackFlag {
204        AssetCallbackFlag::from(self.asset_callbacks.is_enabled() || storage.has_callback_slots())
205    }
206
207    /// Grinds a new [`AccountId`] using the `init_seed` as a starting point.
208    fn grind_account_id(
209        &self,
210        init_seed: [u8; 32],
211        version: AccountIdVersion,
212        asset_callbacks: AssetCallbackFlag,
213        code_commitment: Word,
214        storage_commitment: Word,
215    ) -> Result<Word, AccountError> {
216        let seed = AccountIdV1::compute_account_seed(
217            init_seed,
218            self.account_type,
219            asset_callbacks,
220            version,
221            code_commitment,
222            storage_commitment,
223        )
224        .map_err(|err| {
225            AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
226        })?;
227
228        Ok(seed)
229    }
230
231    /// Builds an [`Account`] out of the configured builder.
232    ///
233    /// # Errors
234    ///
235    /// Returns an error if:
236    /// - The init seed is not set.
237    /// - The number of procedures in all merged components is 0 or exceeds
238    ///   [`AccountCode::MAX_NUM_PROCEDURES`](crate::account::AccountCode::MAX_NUM_PROCEDURES).
239    /// - Two or more packages export a procedure with the same MAST root.
240    /// - Authentication component is missing.
241    /// - Multiple authentication procedures are found.
242    /// - The number of [`StorageSlot`](crate::account::StorageSlot)s of all components exceeds 255.
243    /// - [`MastForest::merge`](miden_processor::mast::MastForest::merge) fails on the given
244    ///   components.
245    /// - If duplicate assets were added to the builder (only under the `testing` feature).
246    /// - If the vault is not empty on new accounts (only under the `testing` feature).
247    pub fn build(mut self) -> Result<Account, AccountError> {
248        let (vault, code, storage) = self.build_inner()?;
249
250        #[cfg(any(feature = "testing", test))]
251        if !vault.is_empty() {
252            return Err(AccountError::BuildError(
253                "account asset vault must be empty on new accounts".into(),
254                None,
255            ));
256        }
257
258        let asset_callbacks = self.derive_asset_callbacks(&storage);
259
260        let seed = self.grind_account_id(
261            self.init_seed,
262            self.id_version,
263            asset_callbacks,
264            code.commitment(),
265            storage.to_commitment(),
266        )?;
267
268        let account_id = AccountId::new(
269            seed,
270            AccountIdVersion::Version1,
271            code.commitment(),
272            storage.to_commitment(),
273        )
274        .expect("get_account_seed should provide a suitable seed");
275
276        debug_assert_eq!(account_id.account_type(), self.account_type);
277        debug_assert_eq!(account_id.asset_callback_flag(), asset_callbacks);
278
279        // SAFETY: The account ID was derived from the seed and the seed is provided, so it is safe
280        // to bypass the checks of `Account::new`.
281        let account =
282            Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed));
283
284        Ok(account)
285    }
286}
287
288#[cfg(any(feature = "testing", test))]
289impl AccountBuilder {
290    /// Adds all the assets to the account's [`AssetVault`]. This method is optional.
291    ///
292    /// Must only be used when using [`Self::build_existing`] instead of [`Self::build`] since new
293    /// accounts must have an empty vault.
294    pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
295        self.assets.extend(assets);
296        self
297    }
298
299    /// Sets the nonce of an existing account.
300    ///
301    /// This method is optional. It must only be used when using [`Self::build_existing`]
302    /// instead of [`Self::build`] since new accounts must have a nonce of `0`.
303    pub fn nonce(mut self, nonce: Felt) -> Self {
304        self.nonce = Some(nonce);
305        self
306    }
307
308    /// Builds the account as an existing account, that is, with the nonce set to [`Felt::ONE`].
309    ///
310    /// The [`AccountId`] is constructed by slightly modifying `init_seed[0..8]` to be a valid ID.
311    ///
312    /// For possible errors, see the documentation of [`Self::build`].
313    pub fn build_existing(mut self) -> Result<Account, AccountError> {
314        let (vault, code, storage) = self.build_inner()?;
315
316        let account_id = {
317            let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
318                .expect("we should have sliced exactly 15 bytes off");
319            AccountId::dummy(
320                bytes,
321                AccountIdVersion::Version1,
322                self.account_type,
323                self.derive_asset_callbacks(&storage),
324            )
325        };
326
327        // Use the nonce value set by the Self::nonce method or Felt::ONE as a default.
328        let nonce = self.nonce.unwrap_or(Felt::ONE);
329
330        Ok(Account::new_existing(account_id, vault, storage, code, nonce))
331    }
332}
333
334// TESTS
335// ================================================================================================
336
337#[cfg(test)]
338mod tests {
339    use std::sync::LazyLock;
340
341    use assert_matches::assert_matches;
342    use miden_core::mast::MastNodeExt;
343    use miden_mast_package::Package;
344
345    use super::*;
346    use crate::account::component::AccountComponentMetadata;
347    use crate::account::{AccountProcedureRoot, StorageSlot, StorageSlotName};
348    use crate::asset::AssetCallbacks;
349    use crate::testing::assembler::assemble_test_package;
350    use crate::testing::noop_auth_component::NoopAuthComponent;
351
352    const CUSTOM_CODE1: &str = "
353          @account_procedure
354          pub proc foo
355            push.2.2 add eq.4
356          end
357        ";
358    const CUSTOM_CODE2: &str = "
359            @account_procedure
360            pub proc bar
361              push.4.4 add eq.8
362            end
363          ";
364
365    static CUSTOM_PACKAGE1: LazyLock<Package> = LazyLock::new(|| {
366        assemble_test_package("custom-package-1", "custom::component1", CUSTOM_CODE1)
367    });
368    static CUSTOM_PACKAGE2: LazyLock<Package> = LazyLock::new(|| {
369        assemble_test_package("custom-package-2", "custom::component2", CUSTOM_CODE2)
370    });
371
372    static CUSTOM_COMPONENT1_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
373        StorageSlotName::new("custom::component1::slot0")
374            .expect("storage slot name should be valid")
375    });
376    static CUSTOM_COMPONENT2_SLOT_NAME0: LazyLock<StorageSlotName> = LazyLock::new(|| {
377        StorageSlotName::new("custom::component2::slot0")
378            .expect("storage slot name should be valid")
379    });
380    static CUSTOM_COMPONENT2_SLOT_NAME1: LazyLock<StorageSlotName> = LazyLock::new(|| {
381        StorageSlotName::new("custom::component2::slot1")
382            .expect("storage slot name should be valid")
383    });
384
385    struct CustomComponent1 {
386        slot0: u32,
387    }
388    impl From<CustomComponent1> for AccountComponent {
389        fn from(custom: CustomComponent1) -> Self {
390            let mut value = Word::empty();
391            value[0] = Felt::from(custom.slot0);
392
393            let metadata = AccountComponentMetadata::new("test::custom_component1");
394            AccountComponent::new(
395                CUSTOM_PACKAGE1.clone(),
396                vec![StorageSlot::with_value(CUSTOM_COMPONENT1_SLOT_NAME.clone(), value)],
397                metadata,
398            )
399            .expect("component should be valid")
400        }
401    }
402
403    struct CustomComponent2 {
404        slot0: u32,
405        slot1: u32,
406    }
407    impl From<CustomComponent2> for AccountComponent {
408        fn from(custom: CustomComponent2) -> Self {
409            let mut value0 = Word::empty();
410            value0[3] = Felt::from(custom.slot0);
411            let mut value1 = Word::empty();
412            value1[3] = Felt::from(custom.slot1);
413
414            let metadata = AccountComponentMetadata::new("test::custom_component2");
415            AccountComponent::new(
416                CUSTOM_PACKAGE2.clone(),
417                vec![
418                    StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME0.clone(), value0),
419                    StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME1.clone(), value1),
420                ],
421                metadata,
422            )
423            .expect("component should be valid")
424        }
425    }
426
427    #[test]
428    fn account_builder() {
429        let storage_slot0 = 25;
430        let storage_slot1 = 12;
431        let storage_slot2 = 42;
432
433        let account = Account::builder([5; 32])
434            .with_component(NoopAuthComponent)
435            .with_component(CustomComponent1 { slot0: storage_slot0 })
436            .with_component(CustomComponent2 {
437                slot0: storage_slot1,
438                slot1: storage_slot2,
439            })
440            .build()
441            .unwrap();
442
443        // Account should be new, i.e. nonce = zero.
444        assert_eq!(account.nonce(), Felt::ZERO);
445
446        let computed_id = AccountId::new(
447            account.seed().unwrap(),
448            AccountIdVersion::Version1,
449            account.code.commitment(),
450            account.storage.to_commitment(),
451        )
452        .unwrap();
453        assert_eq!(account.id(), computed_id);
454
455        // The merged code should have one procedure from each package.
456        assert_eq!(account.code.procedure_roots().count(), 3);
457
458        let foo_root = CUSTOM_PACKAGE1.mast_forest()[CUSTOM_PACKAGE1
459            .get_export_node_id(CUSTOM_PACKAGE1.manifest.exports().next().unwrap().path())]
460        .digest();
461        let bar_root = CUSTOM_PACKAGE2.mast_forest()[CUSTOM_PACKAGE2
462            .get_export_node_id(CUSTOM_PACKAGE2.manifest.exports().next().unwrap().path())]
463        .digest();
464
465        assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(foo_root)));
466        assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(bar_root)));
467
468        assert_eq!(
469            account.storage().get_item(&CUSTOM_COMPONENT1_SLOT_NAME).unwrap(),
470            Word::from([Felt::from(storage_slot0), Felt::ZERO, Felt::ZERO, Felt::ZERO])
471        );
472        assert_eq!(
473            account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME0).unwrap(),
474            Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(storage_slot1)])
475        );
476        assert_eq!(
477            account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME1).unwrap(),
478            Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(storage_slot2)])
479        );
480    }
481
482    #[test]
483    fn account_builder_with_components() {
484        let storage_slot0 = 25;
485        let storage_slot1 = 12;
486        let storage_slot2 = 42;
487
488        let components: Vec<AccountComponent> = vec![
489            CustomComponent1 { slot0: storage_slot0 }.into(),
490            CustomComponent2 {
491                slot0: storage_slot1,
492                slot1: storage_slot2,
493            }
494            .into(),
495        ];
496
497        let account = Account::builder([5; 32])
498            .with_component(NoopAuthComponent)
499            .with_components(components)
500            .build()
501            .unwrap();
502
503        // The account built via `with_components` should be identical to one built via
504        // chained `with_component` calls in the same order.
505        let expected = Account::builder([5; 32])
506            .with_component(NoopAuthComponent)
507            .with_component(CustomComponent1 { slot0: storage_slot0 })
508            .with_component(CustomComponent2 {
509                slot0: storage_slot1,
510                slot1: storage_slot2,
511            })
512            .build()
513            .unwrap();
514
515        assert_eq!(account.id(), expected.id());
516        assert_eq!(account.code().commitment(), expected.code().commitment());
517        assert_eq!(account.storage().to_commitment(), expected.storage().to_commitment());
518
519        // Empty iterators are accepted and behave as a no-op.
520        let account_no_extra = Account::builder([6; 32])
521            .with_component(NoopAuthComponent)
522            .with_component(CustomComponent1 { slot0: storage_slot0 })
523            .with_components(core::iter::empty::<CustomComponent2>())
524            .build()
525            .unwrap();
526
527        let expected_no_extra = Account::builder([6; 32])
528            .with_component(NoopAuthComponent)
529            .with_component(CustomComponent1 { slot0: storage_slot0 })
530            .build()
531            .unwrap();
532
533        assert_eq!(account_no_extra.id(), expected_no_extra.id());
534    }
535
536    #[test]
537    fn account_builder_auth_component_position_is_irrelevant() {
538        let component1 = CustomComponent1 { slot0: 25 };
539        let component2 = CustomComponent2 { slot0: 12, slot1: 42 };
540        let common_components =
541            vec![AccountComponent::from(component1), AccountComponent::from(component2)];
542
543        let mut components_auth_1st = common_components.clone();
544        components_auth_1st.insert(0, AccountComponent::from(NoopAuthComponent));
545
546        let mut components_auth_2nd = common_components.clone();
547        components_auth_2nd.insert(1, AccountComponent::from(NoopAuthComponent));
548
549        let seed = [5; 32];
550        let auth_1st = Account::builder(seed).with_components(components_auth_1st).build().unwrap();
551        let auth_2nd = Account::builder(seed).with_components(components_auth_2nd).build().unwrap();
552
553        assert_eq!(auth_1st.id(), auth_2nd.id());
554        assert_eq!(auth_1st.code().commitment(), auth_2nd.code().commitment());
555        assert_eq!(auth_1st.storage().to_commitment(), auth_2nd.storage().to_commitment());
556    }
557
558    #[test]
559    fn account_builder_without_auth_component_fails() {
560        let build_error = Account::builder([5; 32])
561            .with_component(CustomComponent1 { slot0: 25 })
562            .build()
563            .unwrap_err();
564
565        assert_matches!(build_error, AccountError::BuildError(_, Some(source)) => {
566            assert_matches!(*source, AccountError::AccountCodeNoAuthComponent);
567        });
568    }
569
570    #[test]
571    fn account_builder_with_multiple_auth_components_fails() {
572        let build_error = Account::builder([5; 32])
573            .with_component(NoopAuthComponent)
574            .with_component(NoopAuthComponent)
575            .with_component(CustomComponent1 { slot0: 25 })
576            .build()
577            .unwrap_err();
578
579        assert_matches!(build_error, AccountError::BuildError(_, Some(source)) => {
580            assert_matches!(*source, AccountError::AccountCodeMultipleAuthComponents);
581        });
582    }
583
584    #[test]
585    fn account_builder_non_empty_vault_on_new_account() {
586        let storage_slot0 = 25;
587
588        let build_error = Account::builder([0xff; 32])
589            .with_component(NoopAuthComponent)
590            .with_component(CustomComponent1 { slot0: storage_slot0 })
591            .with_assets(AssetVault::mock().assets())
592            .build()
593            .unwrap_err();
594
595        assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
596    }
597
598    /// The [`AssetCallbackFlag`] is derived from the installed asset callback slots: the kernel
599    /// gates callback invocation on that flag alone and the flag is immutable once the ID is
600    /// ground, so an account that installs a callback slot must have callbacks enabled or whatever
601    /// the callback enforces would be silently and permanently bypassed.
602    #[test]
603    fn account_builder_derives_asset_callback_flag_from_callback_slots() {
604        let callback_component = |slots| {
605            AccountComponent::new(
606                CUSTOM_PACKAGE1.clone(),
607                slots,
608                AccountComponentMetadata::new("test::callback_component"),
609            )
610            .expect("component should be valid")
611        };
612
613        for slots in [
614            AssetCallbacks::new()
615                .on_before_asset_added_to_note(Word::from([1u32, 2, 3, 4]))
616                .into_storage_slots(),
617            AssetCallbacks::new()
618                .on_before_asset_added_to_account(Word::from([1u32, 2, 3, 4]))
619                .into_storage_slots(),
620        ] {
621            let account = Account::builder([7; 32])
622                .with_component(NoopAuthComponent)
623                .with_component(callback_component(slots))
624                .build()
625                .unwrap();
626
627            assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Enabled);
628        }
629    }
630
631    /// Without an installed callback slot the flag is disabled, unless callbacks are explicitly
632    /// enabled to reserve the capability for the account's lifetime.
633    #[test]
634    fn account_builder_derives_disabled_asset_callback_flag_without_callback_slots() {
635        let builder = Account::builder([7; 32])
636            .with_component(NoopAuthComponent)
637            .with_component(CustomComponent1 { slot0: 25 });
638
639        let account = builder.clone().build().unwrap();
640        assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Disabled);
641
642        let account = builder.enable_asset_callbacks().build().unwrap();
643        assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Enabled);
644    }
645
646    // TODO: Test that a BlockHeader with a number which is not a multiple of 2^16 returns an error.
647}