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