Skip to main content

miden_protocol/account/builder/
mod.rs

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