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