use alloc::string::ToString;
use alloc::vec::Vec;
use crate::account::{
Account,
AccountCode,
AccountId,
AccountStorage,
StorageSlot,
StorageSlotType,
};
use crate::asset::AssetVault;
use crate::crypto::SequentialCommit;
use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
use crate::{AccountDeltaError, AccountError, Felt, Word, ZERO};
mod storage;
pub use storage::{AccountStorageDelta, StorageMapDelta};
mod vault;
pub use vault::{
AccountVaultDelta,
FungibleAssetDelta,
NonFungibleAssetDelta,
NonFungibleDeltaAction,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AccountDelta {
account_id: AccountId,
storage: AccountStorageDelta,
vault: AccountVaultDelta,
code: Option<AccountCode>,
nonce_delta: Felt,
}
impl AccountDelta {
pub fn new(
account_id: AccountId,
storage: AccountStorageDelta,
vault: AccountVaultDelta,
nonce_delta: Felt,
) -> Result<Self, AccountDeltaError> {
validate_nonce(nonce_delta, &storage, &vault)?;
Ok(Self {
account_id,
storage,
vault,
code: None,
nonce_delta,
})
}
pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
let new_nonce_delta = self.nonce_delta + other.nonce_delta;
if new_nonce_delta.as_int() < self.nonce_delta.as_int() {
return Err(AccountDeltaError::NonceIncrementOverflow {
current: self.nonce_delta,
increment: other.nonce_delta,
new: new_nonce_delta,
});
}
if self.is_full_state() && other.is_full_state() {
return Err(AccountDeltaError::MergingFullStateDeltas);
}
if let Some(code) = other.code {
self.code = Some(code);
}
self.nonce_delta = new_nonce_delta;
self.storage.merge(other.storage)?;
self.vault.merge(other.vault)
}
pub fn vault_mut(&mut self) -> &mut AccountVaultDelta {
&mut self.vault
}
pub fn with_code(mut self, code: Option<AccountCode>) -> Self {
self.code = code;
self
}
pub fn is_empty(&self) -> bool {
self.storage.is_empty() && self.vault.is_empty() && self.nonce_delta == ZERO
}
pub fn is_full_state(&self) -> bool {
self.code.is_some()
}
pub fn storage(&self) -> &AccountStorageDelta {
&self.storage
}
pub fn vault(&self) -> &AccountVaultDelta {
&self.vault
}
pub fn nonce_delta(&self) -> Felt {
self.nonce_delta
}
pub fn id(&self) -> AccountId {
self.account_id
}
pub fn code(&self) -> Option<&AccountCode> {
self.code.as_ref()
}
pub fn into_parts(self) -> (AccountStorageDelta, AccountVaultDelta, Option<AccountCode>, Felt) {
(self.storage, self.vault, self.code, self.nonce_delta)
}
pub fn to_commitment(&self) -> Word {
<Self as SequentialCommit>::to_commitment(self)
}
}
impl TryFrom<&AccountDelta> for Account {
type Error = AccountError;
fn try_from(delta: &AccountDelta) -> Result<Self, Self::Error> {
if !delta.is_full_state() {
return Err(AccountError::PartialStateDeltaToAccount);
}
let Some(code) = delta.code().cloned() else {
return Err(AccountError::PartialStateDeltaToAccount);
};
let mut vault = AssetVault::default();
vault.apply_delta(delta.vault()).map_err(AccountError::AssetVaultUpdateError)?;
let mut empty_storage_slots = Vec::new();
for slot_idx in 0..u8::MAX {
let slot = match delta.storage().slot_type(slot_idx) {
Some(StorageSlotType::Value) => StorageSlot::empty_value(),
Some(StorageSlotType::Map) => StorageSlot::empty_map(),
None => break,
};
empty_storage_slots.push(slot);
}
let mut storage = AccountStorage::new(empty_storage_slots)
.expect("storage delta should contain a valid number of slots");
storage.apply_delta(delta.storage())?;
let nonce = delta.nonce_delta();
Account::new(delta.id(), vault, storage, code, nonce, None)
}
}
impl SequentialCommit for AccountDelta {
type Commitment = Word;
fn to_elements(&self) -> Vec<Felt> {
if self.is_empty() {
return Vec::new();
}
let mut elements = Vec::with_capacity(24);
elements.extend_from_slice(&[
self.nonce_delta,
ZERO,
self.account_id.suffix(),
self.account_id.prefix().as_felt(),
]);
elements.extend_from_slice(Word::empty().as_elements());
self.vault.append_delta_elements(&mut elements);
self.storage.append_delta_elements(&mut elements);
debug_assert!(
elements.len() % (2 * crate::WORD_SIZE) == 0,
"expected elements to contain an even number of words, but it contained {} elements",
elements.len()
);
elements
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AccountUpdateDetails {
Private,
Delta(AccountDelta),
}
impl AccountUpdateDetails {
pub fn is_private(&self) -> bool {
matches!(self, Self::Private)
}
pub fn merge(self, other: AccountUpdateDetails) -> Result<Self, AccountDeltaError> {
let merged_update = match (self, other) {
(AccountUpdateDetails::Private, AccountUpdateDetails::Private) => {
AccountUpdateDetails::Private
},
(AccountUpdateDetails::Delta(mut delta), AccountUpdateDetails::Delta(new_delta)) => {
delta.merge(new_delta)?;
AccountUpdateDetails::Delta(delta)
},
(left, right) => {
return Err(AccountDeltaError::IncompatibleAccountUpdates {
left_update_type: left.as_tag_str(),
right_update_type: right.as_tag_str(),
});
},
};
Ok(merged_update)
}
pub(crate) const fn as_tag_str(&self) -> &'static str {
match self {
AccountUpdateDetails::Private => "private",
AccountUpdateDetails::Delta(_) => "delta",
}
}
}
impl Serializable for AccountDelta {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.account_id.write_into(target);
self.storage.write_into(target);
self.vault.write_into(target);
self.code.write_into(target);
self.nonce_delta.write_into(target);
}
fn get_size_hint(&self) -> usize {
self.account_id.get_size_hint()
+ self.storage.get_size_hint()
+ self.vault.get_size_hint()
+ self.code.get_size_hint()
+ self.nonce_delta.get_size_hint()
}
}
impl Deserializable for AccountDelta {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let account_id = AccountId::read_from(source)?;
let storage = AccountStorageDelta::read_from(source)?;
let vault = AccountVaultDelta::read_from(source)?;
let code = <Option<AccountCode>>::read_from(source)?;
let nonce_delta = Felt::read_from(source)?;
validate_nonce(nonce_delta, &storage, &vault)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
Ok(Self {
account_id,
storage,
vault,
code,
nonce_delta,
})
}
}
impl Serializable for AccountUpdateDetails {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
match self {
AccountUpdateDetails::Private => {
0_u8.write_into(target);
},
AccountUpdateDetails::Delta(delta) => {
1_u8.write_into(target);
delta.write_into(target);
},
}
}
fn get_size_hint(&self) -> usize {
let u8_size = 0u8.get_size_hint();
match self {
AccountUpdateDetails::Private => u8_size,
AccountUpdateDetails::Delta(account_delta) => u8_size + account_delta.get_size_hint(),
}
}
}
impl Deserializable for AccountUpdateDetails {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
match u8::read_from(source)? {
0 => Ok(Self::Private),
1 => Ok(Self::Delta(AccountDelta::read_from(source)?)),
variant => Err(DeserializationError::InvalidValue(format!(
"Unknown variant {variant} for AccountDetails"
))),
}
}
}
fn validate_nonce(
nonce_delta: Felt,
storage: &AccountStorageDelta,
vault: &AccountVaultDelta,
) -> Result<(), AccountDeltaError> {
if (!storage.is_empty() || !vault.is_empty()) && nonce_delta == ZERO {
return Err(AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta);
}
Ok(())
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use miden_core::utils::Serializable;
use miden_core::{Felt, FieldElement};
use super::{AccountDelta, AccountStorageDelta, AccountVaultDelta};
use crate::account::delta::AccountUpdateDetails;
use crate::account::{
Account,
AccountCode,
AccountId,
AccountStorage,
AccountStorageMode,
AccountType,
StorageMapDelta,
};
use crate::asset::{
Asset,
AssetVault,
FungibleAsset,
NonFungibleAsset,
NonFungibleAssetDetails,
};
use crate::testing::account_id::{
ACCOUNT_ID_PRIVATE_SENDER,
ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
AccountIdBuilder,
};
use crate::{AccountDeltaError, ONE, Word, ZERO};
#[test]
fn account_delta_nonce_validation() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let storage_delta = AccountStorageDelta::new();
let vault_delta = AccountVaultDelta::default();
AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ZERO).unwrap();
AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ONE).unwrap();
let storage_delta = AccountStorageDelta::from_iters([1], [], []);
assert_matches!(
AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ZERO)
.unwrap_err(),
AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta
);
AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ONE).unwrap();
}
#[test]
fn account_delta_nonce_overflow() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let storage_delta = AccountStorageDelta::new();
let vault_delta = AccountVaultDelta::default();
let nonce_delta0 = ONE;
let nonce_delta1 = Felt::try_from(0xffff_ffff_0000_0000u64).unwrap();
let mut delta0 =
AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), nonce_delta0)
.unwrap();
let delta1 =
AccountDelta::new(account_id, storage_delta, vault_delta, nonce_delta1).unwrap();
assert_matches!(delta0.merge(delta1).unwrap_err(), AccountDeltaError::NonceIncrementOverflow {
current, increment, new
} => {
assert_eq!(current, nonce_delta0);
assert_eq!(increment, nonce_delta1);
assert_eq!(new, nonce_delta0 + nonce_delta1);
});
}
#[test]
fn account_update_details_size_hint() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let storage_delta = AccountStorageDelta::new();
let vault_delta = AccountVaultDelta::default();
assert_eq!(storage_delta.to_bytes().len(), storage_delta.get_size_hint());
assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
let account_delta =
AccountDelta::new(account_id, storage_delta, vault_delta, ZERO).unwrap();
assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
let storage_delta = AccountStorageDelta::from_iters(
[1],
[(2, Word::from([1, 1, 1, 1u32])), (3, Word::from([1, 1, 0, 1u32]))],
[(
4,
StorageMapDelta::from_iters(
[Word::from([1, 1, 1, 0u32]), Word::from([0, 1, 1, 1u32])],
[(Word::from([1, 1, 1, 1u32]), Word::from([1, 1, 1, 1u32]))],
),
)],
);
let non_fungible: Asset = NonFungibleAsset::new(
&NonFungibleAssetDetails::new(
AccountIdBuilder::new()
.account_type(AccountType::NonFungibleFaucet)
.storage_mode(AccountStorageMode::Public)
.build_with_rng(&mut rand::rng())
.prefix(),
vec![6],
)
.unwrap(),
)
.unwrap()
.into();
let fungible_2: Asset = FungibleAsset::new(
AccountIdBuilder::new()
.account_type(AccountType::FungibleFaucet)
.storage_mode(AccountStorageMode::Public)
.build_with_rng(&mut rand::rng()),
10,
)
.unwrap()
.into();
let vault_delta = AccountVaultDelta::from_iters([non_fungible], [fungible_2]);
assert_eq!(storage_delta.to_bytes().len(), storage_delta.get_size_hint());
assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
let account_delta = AccountDelta::new(account_id, storage_delta, vault_delta, ONE).unwrap();
assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
let account_id =
AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap();
let asset_vault = AssetVault::mock();
assert_eq!(asset_vault.to_bytes().len(), asset_vault.get_size_hint());
let account_storage = AccountStorage::mock();
assert_eq!(account_storage.to_bytes().len(), account_storage.get_size_hint());
let account_code = AccountCode::mock();
assert_eq!(account_code.to_bytes().len(), account_code.get_size_hint());
let account = Account::new_existing(
account_id,
asset_vault,
account_storage,
account_code,
Felt::ONE,
);
assert_eq!(account.to_bytes().len(), account.get_size_hint());
let update_details_private = AccountUpdateDetails::Private;
assert_eq!(update_details_private.to_bytes().len(), update_details_private.get_size_hint());
let update_details_delta = AccountUpdateDetails::Delta(account_delta);
assert_eq!(update_details_delta.to_bytes().len(), update_details_delta.get_size_hint());
}
}