miden_objects/account/builder/
mod.rs

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