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