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