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