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