use alloc::collections::BTreeMap;
use alloc::string::ToString;
use miden_core::LexicographicWord;
use crate::asset::{Asset, AssetVault};
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
use crate::{AccountError, Felt, Hasher, Word, ZERO};
mod account_id;
pub use account_id::{
AccountId,
AccountIdPrefix,
AccountIdPrefixV0,
AccountIdV0,
AccountIdVersion,
AccountStorageMode,
AccountType,
};
pub mod auth;
mod builder;
pub use builder::AccountBuilder;
pub mod code;
pub use code::AccountCode;
pub use code::procedure::AccountProcedureInfo;
pub mod component;
pub use component::{
AccountComponent,
AccountComponentMetadata,
AccountComponentTemplate,
FeltRepresentation,
InitStorageData,
MapEntry,
MapRepresentation,
PlaceholderTypeRequirement,
StorageEntry,
StorageValueName,
StorageValueNameError,
TemplateType,
TemplateTypeError,
WordRepresentation,
};
pub mod delta;
pub use delta::{
AccountDelta,
AccountStorageDelta,
AccountVaultDelta,
FungibleAssetDelta,
NonFungibleAssetDelta,
NonFungibleDeltaAction,
StorageMapDelta,
};
mod storage;
pub use storage::{
AccountStorage,
AccountStorageHeader,
PartialStorage,
PartialStorageMap,
SlotName,
StorageMap,
StorageMapWitness,
StorageSlot,
StorageSlotType,
};
mod header;
pub use header::AccountHeader;
mod file;
pub use file::AccountFile;
mod partial;
pub use partial::PartialAccount;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Account {
id: AccountId,
vault: AssetVault,
storage: AccountStorage,
code: AccountCode,
nonce: Felt,
seed: Option<Word>,
}
impl Account {
pub fn new(
id: AccountId,
vault: AssetVault,
storage: AccountStorage,
code: AccountCode,
nonce: Felt,
seed: Option<Word>,
) -> Result<Self, AccountError> {
validate_account_seed(id, code.commitment(), storage.commitment(), seed, nonce)?;
Ok(Self::new_unchecked(id, vault, storage, code, nonce, seed))
}
pub fn new_unchecked(
id: AccountId,
vault: AssetVault,
storage: AccountStorage,
code: AccountCode,
nonce: Felt,
seed: Option<Word>,
) -> Self {
Self { id, vault, storage, code, nonce, seed }
}
pub(super) fn initialize_from_components(
account_type: AccountType,
components: &[AccountComponent],
) -> Result<(AccountCode, AccountStorage), AccountError> {
validate_components_support_account_type(components, account_type)?;
let code = AccountCode::from_components_unchecked(components, account_type)?;
let storage = AccountStorage::from_components(components, account_type)?;
Ok((code, storage))
}
pub fn builder(init_seed: [u8; 32]) -> AccountBuilder {
AccountBuilder::new(init_seed)
}
pub fn commitment(&self) -> Word {
hash_account(
self.id,
self.nonce,
self.vault.root(),
self.storage.commitment(),
self.code.commitment(),
)
}
pub fn initial_commitment(&self) -> Word {
if self.is_new() {
Word::empty()
} else {
self.commitment()
}
}
pub fn id(&self) -> AccountId {
self.id
}
pub fn account_type(&self) -> AccountType {
self.id.account_type()
}
pub fn vault(&self) -> &AssetVault {
&self.vault
}
pub fn storage(&self) -> &AccountStorage {
&self.storage
}
pub fn code(&self) -> &AccountCode {
&self.code
}
pub fn nonce(&self) -> Felt {
self.nonce
}
pub fn seed(&self) -> Option<Word> {
self.seed
}
pub fn is_faucet(&self) -> bool {
self.id.is_faucet()
}
pub fn is_regular_account(&self) -> bool {
self.id.is_regular_account()
}
pub fn has_public_state(&self) -> bool {
self.id().has_public_state()
}
pub fn is_public(&self) -> bool {
self.id().is_public()
}
pub fn is_private(&self) -> bool {
self.id().is_private()
}
pub fn is_network(&self) -> bool {
self.id().is_network()
}
pub fn is_new(&self) -> bool {
self.nonce == ZERO
}
pub fn into_parts(
self,
) -> (AccountId, AssetVault, AccountStorage, AccountCode, Felt, Option<Word>) {
(self.id, self.vault, self.storage, self.code, self.nonce, self.seed)
}
pub fn apply_delta(&mut self, delta: &AccountDelta) -> Result<(), AccountError> {
if delta.is_full_state() {
return Err(AccountError::ApplyFullStateDeltaToAccount);
}
self.vault
.apply_delta(delta.vault())
.map_err(AccountError::AssetVaultUpdateError)?;
self.storage.apply_delta(delta.storage())?;
self.increment_nonce(delta.nonce_delta())?;
Ok(())
}
pub fn increment_nonce(&mut self, nonce_delta: Felt) -> Result<(), AccountError> {
let new_nonce = self.nonce + nonce_delta;
if new_nonce.as_int() < self.nonce.as_int() {
return Err(AccountError::NonceOverflow {
current: self.nonce,
increment: nonce_delta,
new: new_nonce,
});
}
self.nonce = new_nonce;
if !self.is_new() {
self.seed = None;
}
Ok(())
}
#[cfg(any(feature = "testing", test))]
pub fn vault_mut(&mut self) -> &mut AssetVault {
&mut self.vault
}
#[cfg(any(feature = "testing", test))]
pub fn storage_mut(&mut self) -> &mut AccountStorage {
&mut self.storage
}
}
impl TryFrom<Account> for AccountDelta {
type Error = AccountError;
fn try_from(account: Account) -> Result<Self, Self::Error> {
let Account { id, vault, storage, code, nonce, seed } = account;
if seed.is_some() {
return Err(AccountError::DeltaFromAccountWithSeed);
}
let mut value_slots = BTreeMap::new();
let mut map_slots = BTreeMap::new();
for (slot_idx, slot) in (0..u8::MAX).zip(storage.into_slots().into_iter()) {
match slot {
StorageSlot::Value(word) => {
value_slots.insert(slot_idx, word);
},
StorageSlot::Map(storage_map) => {
let map_delta = StorageMapDelta::new(
storage_map
.into_entries()
.into_iter()
.map(|(key, value)| (LexicographicWord::from(key), value))
.collect(),
);
map_slots.insert(slot_idx, map_delta);
},
}
}
let storage_delta = AccountStorageDelta::from_parts(value_slots, map_slots)
.expect("value and map slots from account storage should not overlap");
let mut fungible_delta = FungibleAssetDelta::default();
let mut non_fungible_delta = NonFungibleAssetDelta::default();
for asset in vault.assets() {
match asset {
Asset::Fungible(fungible_asset) => {
fungible_delta
.add(fungible_asset)
.expect("delta should allow representing valid fungible assets");
},
Asset::NonFungible(non_fungible_asset) => {
non_fungible_delta
.add(non_fungible_asset)
.expect("delta should allow representing valid non-fungible assets");
},
}
}
let vault_delta = AccountVaultDelta::new(fungible_delta, non_fungible_delta);
let nonce_delta = nonce;
let delta = AccountDelta::new(id, storage_delta, vault_delta, nonce_delta)
.expect("nonce_delta should be greater than 0")
.with_code(Some(code));
Ok(delta)
}
}
impl Serializable for Account {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
let Account { id, vault, storage, code, nonce, seed } = self;
id.write_into(target);
vault.write_into(target);
storage.write_into(target);
code.write_into(target);
nonce.write_into(target);
seed.write_into(target);
}
fn get_size_hint(&self) -> usize {
self.id.get_size_hint()
+ self.vault.get_size_hint()
+ self.storage.get_size_hint()
+ self.code.get_size_hint()
+ self.nonce.get_size_hint()
+ self.seed.get_size_hint()
}
}
impl Deserializable for Account {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let id = AccountId::read_from(source)?;
let vault = AssetVault::read_from(source)?;
let storage = AccountStorage::read_from(source)?;
let code = AccountCode::read_from(source)?;
let nonce = Felt::read_from(source)?;
let seed = <Option<Word>>::read_from(source)?;
Self::new(id, vault, storage, code, nonce, seed)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
pub fn hash_account(
id: AccountId,
nonce: Felt,
vault_root: Word,
storage_commitment: Word,
code_commitment: Word,
) -> Word {
let mut elements = [ZERO; 16];
elements[0] = id.suffix();
elements[1] = id.prefix().as_felt();
elements[3] = nonce;
elements[4..8].copy_from_slice(&*vault_root);
elements[8..12].copy_from_slice(&*storage_commitment);
elements[12..].copy_from_slice(&*code_commitment);
Hasher::hash_elements(&elements)
}
pub(super) fn validate_account_seed(
id: AccountId,
code_commitment: Word,
storage_commitment: Word,
seed: Option<Word>,
nonce: Felt,
) -> Result<(), AccountError> {
let account_is_new = nonce == ZERO;
match (account_is_new, seed) {
(true, Some(seed)) => {
let account_id =
AccountId::new(seed, id.version(), code_commitment, storage_commitment)
.map_err(AccountError::SeedConvertsToInvalidAccountId)?;
if account_id != id {
return Err(AccountError::AccountIdSeedMismatch {
expected: id,
actual: account_id,
});
}
Ok(())
},
(true, None) => Err(AccountError::NewAccountMissingSeed),
(false, Some(_)) => Err(AccountError::ExistingAccountWithSeed),
(false, None) => Ok(()),
}
}
fn validate_components_support_account_type(
components: &[AccountComponent],
account_type: AccountType,
) -> Result<(), AccountError> {
for (component_index, component) in components.iter().enumerate() {
if !component.supports_type(account_type) {
return Err(AccountError::UnsupportedComponentForAccountType {
account_type,
component_index,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use assert_matches::assert_matches;
use miden_assembly::Assembler;
use miden_core::FieldElement;
use miden_crypto::utils::{Deserializable, Serializable};
use miden_crypto::{Felt, Word};
use super::{
AccountCode,
AccountDelta,
AccountId,
AccountStorage,
AccountStorageDelta,
AccountVaultDelta,
};
use crate::AccountError;
use crate::account::AccountStorageMode::Network;
use crate::account::{
Account,
AccountBuilder,
AccountComponent,
AccountIdVersion,
AccountType,
PartialAccount,
StorageMap,
StorageMapDelta,
StorageSlot,
};
use crate::asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset};
use crate::testing::account_id::{
ACCOUNT_ID_PRIVATE_SENDER,
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
};
use crate::testing::add_component::AddComponent;
use crate::testing::noop_auth_component::NoopAuthComponent;
use crate::testing::storage::AccountStorageDeltaBuilder;
#[test]
fn test_serde_account() {
let init_nonce = Felt::new(1);
let asset_0 = FungibleAsset::mock(99);
let word = Word::from([1, 2, 3, 4u32]);
let storage_slot = StorageSlot::Value(word);
let account = build_account(vec![asset_0], init_nonce, vec![storage_slot]);
let serialized = account.to_bytes();
let deserialized = Account::read_from_bytes(&serialized).unwrap();
assert_eq!(deserialized, account);
}
#[test]
fn test_serde_account_delta() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let nonce_delta = Felt::new(2);
let asset_0 = FungibleAsset::mock(15);
let asset_1 = NonFungibleAsset::mock(&[5, 5, 5]);
let storage_delta = AccountStorageDeltaBuilder::new()
.add_cleared_items([0])
.add_updated_values([(1_u8, Word::from([1, 2, 3, 4u32]))])
.build()
.unwrap();
let account_delta = build_account_delta(
account_id,
vec![asset_1],
vec![asset_0],
nonce_delta,
storage_delta,
);
let serialized = account_delta.to_bytes();
let deserialized = AccountDelta::read_from_bytes(&serialized).unwrap();
assert_eq!(deserialized, account_delta);
}
#[test]
fn valid_account_delta_is_correctly_applied() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let init_nonce = Felt::new(1);
let asset_0 = FungibleAsset::mock(100);
let asset_1 = NonFungibleAsset::mock(&[1, 2, 3]);
let storage_slot_value_0 = StorageSlot::Value(Word::from([1, 2, 3, 4u32]));
let storage_slot_value_1 = StorageSlot::Value(Word::from([5, 6, 7, 8u32]));
let mut storage_map = StorageMap::with_entries([
(
Word::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]),
Word::from([
Felt::new(1_u64),
Felt::new(2_u64),
Felt::new(3_u64),
Felt::new(4_u64),
]),
),
(
Word::new([Felt::new(105), Felt::new(106), Felt::new(107), Felt::new(108)]),
Word::new([Felt::new(5_u64), Felt::new(6_u64), Felt::new(7_u64), Felt::new(8_u64)]),
),
])
.unwrap();
let storage_slot_map = StorageSlot::Map(storage_map.clone());
let mut account = build_account(
vec![asset_0],
init_nonce,
vec![storage_slot_value_0, storage_slot_value_1, storage_slot_map],
);
let new_map_entry = (
Word::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]),
[Felt::new(9_u64), Felt::new(10_u64), Felt::new(11_u64), Felt::new(12_u64)],
);
let updated_map =
StorageMapDelta::from_iters([], [(new_map_entry.0, new_map_entry.1.into())]);
storage_map.insert(new_map_entry.0, new_map_entry.1.into()).unwrap();
let final_nonce = Felt::new(2);
let storage_delta = AccountStorageDeltaBuilder::new()
.add_cleared_items([0])
.add_updated_values([(1, Word::from([1, 2, 3, 4u32]))])
.add_updated_maps([(2, updated_map)])
.build()
.unwrap();
let account_delta = build_account_delta(
account_id,
vec![asset_1],
vec![asset_0],
final_nonce - init_nonce,
storage_delta,
);
account.apply_delta(&account_delta).unwrap();
let final_account = build_account(
vec![asset_1],
final_nonce,
vec![
StorageSlot::Value(Word::empty()),
StorageSlot::Value(Word::from([1, 2, 3, 4u32])),
StorageSlot::Map(storage_map),
],
);
assert_eq!(account, final_account);
}
#[test]
#[should_panic]
fn valid_account_delta_with_unchanged_nonce() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let init_nonce = Felt::new(1);
let asset = FungibleAsset::mock(110);
let mut account =
build_account(vec![asset], init_nonce, vec![StorageSlot::Value(Word::empty())]);
let storage_delta = AccountStorageDeltaBuilder::new()
.add_cleared_items([0])
.add_updated_values([(1_u8, Word::from([1, 2, 3, 4u32]))])
.build()
.unwrap();
let account_delta =
build_account_delta(account_id, vec![], vec![asset], init_nonce, storage_delta);
account.apply_delta(&account_delta).unwrap()
}
#[test]
#[should_panic]
fn valid_account_delta_with_decremented_nonce() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let init_nonce = Felt::new(2);
let asset = FungibleAsset::mock(100);
let mut account =
build_account(vec![asset], init_nonce, vec![StorageSlot::Value(Word::empty())]);
let final_nonce = Felt::new(1);
let storage_delta = AccountStorageDeltaBuilder::new()
.add_cleared_items([0])
.add_updated_values([(1_u8, Word::from([1, 2, 3, 4u32]))])
.build()
.unwrap();
let account_delta =
build_account_delta(account_id, vec![], vec![asset], final_nonce, storage_delta);
account.apply_delta(&account_delta).unwrap()
}
#[test]
fn empty_account_delta_with_incremented_nonce() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let init_nonce = Felt::new(1);
let word = Word::from([1, 2, 3, 4u32]);
let storage_slot = StorageSlot::Value(word);
let mut account = build_account(vec![], init_nonce, vec![storage_slot]);
let nonce_delta = Felt::new(1);
let account_delta = AccountDelta::new(
account_id,
AccountStorageDelta::new(),
AccountVaultDelta::default(),
nonce_delta,
)
.unwrap();
account.apply_delta(&account_delta).unwrap()
}
pub fn build_account_delta(
account_id: AccountId,
added_assets: Vec<Asset>,
removed_assets: Vec<Asset>,
nonce_delta: Felt,
storage_delta: AccountStorageDelta,
) -> AccountDelta {
let vault_delta = AccountVaultDelta::from_iters(added_assets, removed_assets);
AccountDelta::new(account_id, storage_delta, vault_delta, nonce_delta).unwrap()
}
pub fn build_account(assets: Vec<Asset>, nonce: Felt, slots: Vec<StorageSlot>) -> Account {
let id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
let code = AccountCode::mock();
let vault = AssetVault::new(&assets).unwrap();
let storage = AccountStorage::new(slots).unwrap();
Account::new_existing(id, vault, storage, code, nonce)
}
#[test]
fn test_account_unsupported_component_type() {
let code1 = "export.foo add end";
let library1 = Assembler::default().assemble_library([code1]).unwrap();
let component1 = AccountComponent::new(library1, vec![])
.unwrap()
.with_supported_type(AccountType::FungibleFaucet)
.with_supported_type(AccountType::NonFungibleFaucet)
.with_supported_type(AccountType::RegularAccountImmutableCode);
let err = Account::initialize_from_components(
AccountType::RegularAccountUpdatableCode,
&[component1],
)
.unwrap_err();
assert!(matches!(
err,
AccountError::UnsupportedComponentForAccountType {
account_type: AccountType::RegularAccountUpdatableCode,
component_index: 0
}
))
}
#[test]
fn test_account_duplicate_exported_mast_root() {
let code1 = "export.foo add eq.1 end";
let code2 = "export.bar add eq.1 end";
let library1 = Assembler::default().assemble_library([code1]).unwrap();
let library2 = Assembler::default().assemble_library([code2]).unwrap();
let component1 = AccountComponent::new(library1, vec![]).unwrap().with_supports_all_types();
let component2 = AccountComponent::new(library2, vec![]).unwrap().with_supports_all_types();
let err = Account::initialize_from_components(
AccountType::RegularAccountUpdatableCode,
&[NoopAuthComponent.into(), component1, component2],
)
.unwrap_err();
assert_matches!(err, AccountError::AccountComponentDuplicateProcedureRoot(_))
}
#[test]
fn seed_validation() -> anyhow::Result<()> {
let account = AccountBuilder::new([5; 32])
.with_auth_component(NoopAuthComponent)
.with_component(AddComponent)
.build()?;
let (id, vault, storage, code, _nonce, seed) = account.into_parts();
assert!(seed.is_some());
let other_seed = AccountId::compute_account_seed(
[9; 32],
AccountType::FungibleFaucet,
Network,
AccountIdVersion::Version0,
code.commitment(),
storage.commitment(),
)?;
let err = Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ONE, seed)
.unwrap_err();
assert_matches!(err, AccountError::ExistingAccountWithSeed);
let err = Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ZERO, None)
.unwrap_err();
assert_matches!(err, AccountError::NewAccountMissingSeed);
let err = Account::new(
id,
vault.clone(),
storage.clone(),
code.clone(),
Felt::ZERO,
Some(other_seed),
)
.unwrap_err();
assert_matches!(err, AccountError::AccountIdSeedMismatch { .. });
let err = Account::new(
id,
vault.clone(),
storage.clone(),
code.clone(),
Felt::ZERO,
Some(Word::from([1, 2, 3, 4u32])),
)
.unwrap_err();
assert_matches!(err, AccountError::SeedConvertsToInvalidAccountId(_));
Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ONE, None)?;
Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ZERO, seed)?;
Ok(())
}
#[test]
fn incrementing_nonce_should_remove_seed() -> anyhow::Result<()> {
let mut account = AccountBuilder::new([5; 32])
.with_auth_component(NoopAuthComponent)
.with_component(AddComponent)
.build()?;
account.increment_nonce(Felt::ONE)?;
assert_matches!(account.seed(), None);
let _partial_account = PartialAccount::from(&account);
Ok(())
}
}