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