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