miden_objects/accounts/builder/
mod.rsuse alloc::{boxed::Box, vec::Vec};
use vm_processor::Digest;
use crate::{
accounts::{
Account, AccountCode, AccountComponent, AccountId, AccountStorage, AccountStorageMode,
AccountType,
},
assets::{Asset, AssetVault},
AccountError, Felt, Word, ZERO,
};
#[derive(Debug, Clone)]
pub struct AccountBuilder {
nonce: Felt,
#[cfg(any(feature = "testing", test))]
assets: Vec<Asset>,
components: Vec<AccountComponent>,
account_type: AccountType,
storage_mode: AccountStorageMode,
init_seed: Option<[u8; 32]>,
}
impl AccountBuilder {
pub fn new() -> Self {
Self {
nonce: ZERO,
#[cfg(any(feature = "testing", test))]
assets: vec![],
components: vec![],
init_seed: None,
account_type: AccountType::RegularAccountUpdatableCode,
storage_mode: AccountStorageMode::Private,
}
}
pub fn init_seed(mut self, init_seed: [u8; 32]) -> Self {
self.init_seed = Some(init_seed);
self
}
pub fn account_type(mut self, account_type: AccountType) -> Self {
self.account_type = account_type;
self
}
pub fn storage_mode(mut self, storage_mode: AccountStorageMode) -> Self {
self.storage_mode = storage_mode;
self
}
pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
self.components.push(account_component.into());
self
}
fn build_inner(
&self,
) -> Result<([u8; 32], AssetVault, AccountCode, AccountStorage), AccountError> {
let init_seed = self.init_seed.ok_or(AccountError::BuildError(
"init_seed must be set on the account builder".into(),
None,
))?;
#[cfg(any(feature = "testing", test))]
let vault = AssetVault::new(&self.assets).map_err(|err| {
AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
})?;
#[cfg(all(not(feature = "testing"), not(test)))]
let vault = AssetVault::default();
#[cfg(any(feature = "testing", test))]
if self.nonce == ZERO && !vault.is_empty() {
return Err(AccountError::BuildError(
"account asset vault must be empty on new accounts".into(),
None,
));
}
let (code, storage) =
Account::initialize_from_components(self.account_type, &self.components).map_err(
|err| {
AccountError::BuildError(
"account components failed to build".into(),
Some(Box::new(err)),
)
},
)?;
Ok((init_seed, vault, code, storage))
}
fn grind_account_id(
&self,
init_seed: [u8; 32],
code_commitment: Digest,
storage_commitment: Digest,
) -> Result<(AccountId, Word), AccountError> {
let seed = AccountId::get_account_seed(
init_seed,
self.account_type,
self.storage_mode,
code_commitment,
storage_commitment,
)
.map_err(|err| {
AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
})?;
let account_id = AccountId::new(seed, code_commitment, storage_commitment)
.expect("get_account_seed should provide a suitable seed");
Ok((account_id, seed))
}
pub fn build(self) -> Result<(Account, Word), AccountError> {
let (init_seed, vault, code, storage) = self.build_inner()?;
let (account_id, seed) =
self.grind_account_id(init_seed, code.commitment(), storage.commitment())?;
debug_assert_eq!(account_id.account_type(), self.account_type);
debug_assert_eq!(account_id.storage_mode(), self.storage_mode);
let account = Account::from_parts(account_id, vault, storage, code, self.nonce);
Ok((account, seed))
}
}
#[cfg(any(feature = "testing", test))]
impl AccountBuilder {
pub fn nonce(mut self, nonce: Felt) -> Self {
self.nonce = nonce;
self
}
pub fn with_assets<I: IntoIterator<Item = Asset>>(mut self, assets: I) -> Self {
self.assets.extend(assets);
self
}
pub fn build_testing(self) -> Result<(Account, Option<Word>), AccountError> {
let (init_seed, vault, code, storage) = self.build_inner()?;
let (account_id, seed) = if self.nonce == ZERO {
let (account_id, seed) =
self.grind_account_id(init_seed, code.commitment(), storage.commitment())?;
(account_id, Some(seed))
} else {
let bytes = <[u8; 8]>::try_from(&init_seed[0..8])
.expect("we should have sliced exactly 8 bytes off");
let account_id =
AccountId::new_with_type_and_mode(bytes, self.account_type, self.storage_mode);
(account_id, None)
};
let account = Account::from_parts(account_id, vault, storage, code, self.nonce);
Ok((account, seed))
}
}
impl Default for AccountBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use assembly::{Assembler, Library};
use vm_core::FieldElement;
use super::*;
use crate::accounts::StorageSlot;
const CUSTOM_CODE1: &str = "
export.foo
push.2.2 add eq.4
end
";
const CUSTOM_CODE2: &str = "
export.bar
push.4.4 add eq.8
end
";
static CUSTOM_LIBRARY1: LazyLock<Library> = LazyLock::new(|| {
Assembler::default()
.assemble_library([CUSTOM_CODE1])
.expect("code should be valid")
});
static CUSTOM_LIBRARY2: LazyLock<Library> = LazyLock::new(|| {
Assembler::default()
.assemble_library([CUSTOM_CODE2])
.expect("code should be valid")
});
struct CustomComponent1 {
slot0: u64,
}
impl From<CustomComponent1> for AccountComponent {
fn from(custom: CustomComponent1) -> Self {
let mut value = Word::default();
value[0] = Felt::new(custom.slot0);
AccountComponent::new(CUSTOM_LIBRARY1.clone(), vec![StorageSlot::Value(value)])
.expect("component should be valid")
.with_supports_all_types()
}
}
struct CustomComponent2 {
slot0: u64,
slot1: u64,
}
impl From<CustomComponent2> for AccountComponent {
fn from(custom: CustomComponent2) -> Self {
let mut value0 = Word::default();
value0[3] = Felt::new(custom.slot0);
let mut value1 = Word::default();
value1[3] = Felt::new(custom.slot1);
AccountComponent::new(
CUSTOM_LIBRARY2.clone(),
vec![StorageSlot::Value(value0), StorageSlot::Value(value1)],
)
.expect("component should be valid")
.with_supports_all_types()
}
}
#[test]
fn account_builder() {
let storage_slot0 = 25;
let storage_slot1 = 12;
let storage_slot2 = 42;
let (account, seed) = Account::builder()
.init_seed([5; 32])
.with_component(CustomComponent1 { slot0: storage_slot0 })
.with_component(CustomComponent2 {
slot0: storage_slot1,
slot1: storage_slot2,
})
.build()
.unwrap();
assert_eq!(account.nonce(), Felt::ZERO);
let computed_id =
AccountId::new(seed, account.code.commitment(), account.storage.commitment()).unwrap();
assert_eq!(account.id(), computed_id);
assert_eq!(account.code.procedure_roots().count(), 2);
let foo_root = CUSTOM_LIBRARY1.mast_forest()
[CUSTOM_LIBRARY1.get_export_node_id(CUSTOM_LIBRARY1.exports().next().unwrap())]
.digest();
let bar_root = CUSTOM_LIBRARY2.mast_forest()
[CUSTOM_LIBRARY2.get_export_node_id(CUSTOM_LIBRARY2.exports().next().unwrap())]
.digest();
let foo_procedure_info = &account
.code()
.procedures()
.iter()
.find(|info| info.mast_root() == &foo_root)
.unwrap();
assert_eq!(foo_procedure_info.storage_offset(), 0);
assert_eq!(foo_procedure_info.storage_size(), 1);
let bar_procedure_info = &account
.code()
.procedures()
.iter()
.find(|info| info.mast_root() == &bar_root)
.unwrap();
assert_eq!(bar_procedure_info.storage_offset(), 1);
assert_eq!(bar_procedure_info.storage_size(), 2);
assert_eq!(
account.storage().get_item(0).unwrap(),
[Felt::new(storage_slot0), Felt::new(0), Felt::new(0), Felt::new(0)].into()
);
assert_eq!(
account.storage().get_item(1).unwrap(),
[Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot1)].into()
);
assert_eq!(
account.storage().get_item(2).unwrap(),
[Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot2)].into()
);
}
#[test]
fn account_builder_non_empty_vault_on_new_account() {
let storage_slot0 = 25;
let build_error = Account::builder()
.init_seed([0xff; 32])
.with_component(CustomComponent1 { slot0: storage_slot0 })
.with_assets(AssetVault::mock().assets())
.build()
.unwrap_err();
assert!(
matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
)
}
}