miden_objects/account/builder/
mod.rsuse alloc::{boxed::Box, vec::Vec};
use vm_core::FieldElement;
use vm_processor::Digest;
use crate::{
account::{
Account, AccountCode, AccountComponent, AccountId, AccountIdAnchor, AccountIdV0,
AccountIdVersion, AccountStorage, AccountStorageMode, AccountType,
},
asset::AssetVault,
AccountError, Felt, Word,
};
#[derive(Debug, Clone)]
pub struct AccountBuilder {
#[cfg(any(feature = "testing", test))]
assets: Vec<crate::asset::Asset>,
components: Vec<AccountComponent>,
account_type: AccountType,
storage_mode: AccountStorageMode,
id_anchor: Option<AccountIdAnchor>,
init_seed: [u8; 32],
id_version: AccountIdVersion,
}
impl AccountBuilder {
pub fn new(init_seed: [u8; 32]) -> Self {
Self {
#[cfg(any(feature = "testing", test))]
assets: vec![],
components: vec![],
id_anchor: None,
init_seed,
account_type: AccountType::RegularAccountUpdatableCode,
storage_mode: AccountStorageMode::Private,
id_version: AccountIdVersion::Version0,
}
}
pub fn anchor(mut self, anchor: AccountIdAnchor) -> Self {
self.id_anchor = Some(anchor);
self
}
pub fn version(mut self, version: AccountIdVersion) -> Self {
self.id_version = version;
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<(AssetVault, AccountCode, AccountStorage), AccountError> {
#[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();
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((vault, code, storage))
}
fn grind_account_id(
&self,
init_seed: [u8; 32],
version: AccountIdVersion,
code_commitment: Digest,
storage_commitment: Digest,
block_hash: Digest,
) -> Result<Word, AccountError> {
let seed = AccountIdV0::compute_account_seed(
init_seed,
self.account_type,
self.storage_mode,
version,
code_commitment,
storage_commitment,
block_hash,
)
.map_err(|err| {
AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
})?;
Ok(seed)
}
pub fn build(self) -> Result<(Account, Word), AccountError> {
let (vault, code, storage) = self.build_inner()?;
let id_anchor = self
.id_anchor
.ok_or_else(|| AccountError::BuildError("anchor must be set".into(), None))?;
#[cfg(any(feature = "testing", test))]
if !vault.is_empty() {
return Err(AccountError::BuildError(
"account asset vault must be empty on new accounts".into(),
None,
));
}
let seed = self.grind_account_id(
self.init_seed,
self.id_version,
code.commitment(),
storage.commitment(),
id_anchor.block_hash(),
)?;
let account_id = AccountId::new(
seed,
id_anchor,
AccountIdVersion::Version0,
code.commitment(),
storage.commitment(),
)
.expect("get_account_seed should provide a suitable seed");
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, Felt::ZERO);
Ok((account, seed))
}
}
#[cfg(any(feature = "testing", test))]
impl AccountBuilder {
pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
self.assets.extend(assets);
self
}
pub fn build_existing(self) -> Result<Account, AccountError> {
let (vault, code, storage) = self.build_inner()?;
let account_id = {
let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
.expect("we should have sliced exactly 15 bytes off");
AccountId::dummy(
bytes,
AccountIdVersion::Version0,
self.account_type,
self.storage_mode,
)
};
Ok(Account::from_parts(account_id, vault, storage, code, Felt::ONE))
}
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use assembly::{Assembler, Library};
use assert_matches::assert_matches;
use vm_core::FieldElement;
use super::*;
use crate::{account::StorageSlot, block::BlockNumber};
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 anchor_block_hash = Digest::new([Felt::new(42); 4]);
let anchor_block_number = 1 << 16;
let id_anchor =
AccountIdAnchor::new(BlockNumber::from(anchor_block_number), anchor_block_hash)
.unwrap();
let (account, seed) = Account::builder([5; 32])
.anchor(id_anchor)
.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,
id_anchor,
AccountIdVersion::Version0,
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 anchor = AccountIdAnchor::new_unchecked(5, Digest::default());
let build_error = Account::builder([0xff; 32])
.anchor(anchor)
.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")
}
}