miden_objects/account/builder/
mod.rs

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