Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- Introduced `AssetAmount` wrapper type for fungible asset amounts with validated construction and a maximum value of `2^63 - 2^31` ([#2658](https://github.com/0xMiden/protocol/pull/2658)).
- Made `NoteMetadataHeader` and `NoteMetadata::to_header()` public, added `NoteMetadata::from_header()` constructor, and exported `NoteMetadataHeader` from the `note` module ([#2561](https://github.com/0xMiden/protocol/pull/2561)).
- Introduce NOTE_MAX_SIZE (256 KiB) and enforce it on individual output notes ([#2205](https://github.com/0xMiden/miden-base/pull/2205), [#2651](https://github.com/0xMiden/miden-base/pull/2651))
- Added AggLayer faucet registry to bridge account with conversion metadata, `CONFIG_AGG_BRIDGE` note for faucet registration, and FPI-based asset conversion in `bridge_out` ([#2426](https://github.com/0xMiden/miden-base/pull/2426)).
Expand Down
4 changes: 2 additions & 2 deletions crates/miden-protocol/src/account/delta/vault.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ impl FungibleAssetDelta {
/// # Errors
/// Returns an error if the delta would overflow.
pub fn add(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
let amount: i64 = asset.amount().try_into().expect("Amount it too high");
let amount: i64 = asset.amount().inner().try_into().expect("Amount it too high");
self.add_delta(asset.vault_key(), amount)
}

Expand All @@ -225,7 +225,7 @@ impl FungibleAssetDelta {
/// # Errors
/// Returns an error if the delta would overflow.
pub fn remove(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
let amount: i64 = asset.amount().try_into().expect("Amount it too high");
let amount: i64 = asset.amount().inner().try_into().expect("Amount it too high");
self.add_delta(asset.vault_key(), -amount)
}

Expand Down
200 changes: 200 additions & 0 deletions crates/miden-protocol/src/asset/asset_amount.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
use alloc::string::ToString;
use core::fmt;

use super::AssetError;
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};

// ASSET AMOUNT
// ================================================================================================

/// A validated amount for a [`super::FungibleAsset`].
///
/// The inner value is guaranteed to be at most [`AssetAmount::MAX`].
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AssetAmount(u64);

impl AssetAmount {
/// The maximum amount a fungible asset can represent.
///
/// This number was chosen so that it can be represented as a positive and negative number in a
/// field element. See `account_delta.masm` for more details on how this number was chosen.
pub const MAX: Self = Self(2u64.pow(63) - 2u64.pow(31));

/// An amount of zero.
pub const ZERO: Self = Self(0);

/// Creates a new [`AssetAmount`] after validating that `amount` does not exceed
/// [`Self::MAX`].
///
/// # Errors
///
/// Returns [`AssetError::FungibleAssetAmountTooBig`] if `amount` exceeds [`Self::MAX`].
pub fn new(amount: u64) -> Result<Self, AssetError> {
if amount > Self::MAX.0 {
return Err(AssetError::FungibleAssetAmountTooBig(amount));
}
Ok(Self(amount))
}

/// Returns the inner `u64` value.
pub const fn inner(&self) -> u64 {
self.0
}

/// Creates a new [`AssetAmount`] without validating bounds.
///
/// # Safety
///
/// The caller must ensure that `amount <= AssetAmount::MAX`.
pub(crate) const fn new_unchecked(amount: u64) -> Self {
Self(amount)
}
}

// TRAIT IMPLEMENTATIONS
// ================================================================================================

impl From<u8> for AssetAmount {
fn from(amount: u8) -> Self {
Self(amount as u64)
}
}

impl From<u16> for AssetAmount {
fn from(amount: u16) -> Self {
Self(amount as u64)
}
}

impl From<u32> for AssetAmount {
fn from(amount: u32) -> Self {
Self(amount as u64)
}
}

impl TryFrom<u64> for AssetAmount {
type Error = AssetError;

fn try_from(amount: u64) -> Result<Self, Self::Error> {
Self::new(amount)
}
}

impl From<AssetAmount> for u64 {
fn from(amount: AssetAmount) -> Self {
amount.0
}
}

impl fmt::Display for AssetAmount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}

// SERIALIZATION
// ================================================================================================

impl Serializable for AssetAmount {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write(self.0);
}

fn get_size_hint(&self) -> usize {
self.0.get_size_hint()
}
}

impl Deserializable for AssetAmount {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let amount: u64 = source.read()?;
Self::new(amount).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}

// TESTS
// ================================================================================================

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn asset_amount_max_value() {
let max = AssetAmount::MAX;
assert_eq!(max.inner(), 2u64.pow(63) - 2u64.pow(31));
}

#[test]
fn asset_amount_new_valid() {
assert!(AssetAmount::new(0).is_ok());
assert!(AssetAmount::new(100).is_ok());
assert!(AssetAmount::new(AssetAmount::MAX.inner()).is_ok());
}

#[test]
fn asset_amount_new_exceeds_max() {
assert!(AssetAmount::new(AssetAmount::MAX.inner() + 1).is_err());
assert!(AssetAmount::new(u64::MAX).is_err());
}

#[test]
fn asset_amount_from_small_types() {
let a: AssetAmount = 42u8.into();
assert_eq!(a.inner(), 42);

let b: AssetAmount = 1000u16.into();
assert_eq!(b.inner(), 1000);

let c: AssetAmount = 1_000_000u32.into();
assert_eq!(c.inner(), 1_000_000);
}

#[test]
fn asset_amount_try_from_u64() {
assert!(AssetAmount::try_from(100u64).is_ok());
assert!(AssetAmount::try_from(AssetAmount::MAX.inner() + 1).is_err());
}

#[test]
fn asset_amount_into_u64() {
let amount = AssetAmount::new(42).unwrap();
let val: u64 = amount.into();
assert_eq!(val, 42);
}

#[test]
fn asset_amount_display() {
let amount = AssetAmount::new(12345).unwrap();
assert_eq!(format!("{amount}"), "12345");
}

#[test]
fn asset_amount_ordering() {
let a = AssetAmount::new(10).unwrap();
let b = AssetAmount::new(20).unwrap();
assert!(a < b);
assert!(b > a);
assert_eq!(a, AssetAmount::new(10).unwrap());
}

#[test]
fn asset_amount_default_is_zero() {
assert_eq!(AssetAmount::default(), AssetAmount::ZERO);
assert_eq!(AssetAmount::default().inner(), 0);
}

#[test]
fn asset_amount_serde_roundtrip() {
let amount = AssetAmount::new(999).unwrap();
let bytes = amount.to_bytes();
let restored = AssetAmount::read_from_bytes(&bytes).unwrap();
assert_eq!(amount, restored);
}
}
38 changes: 18 additions & 20 deletions crates/miden-protocol/src/asset/fungible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use alloc::string::ToString;
use core::fmt;

use super::vault::AssetVaultKey;
use super::{AccountType, Asset, AssetCallbackFlag, AssetError, Word};
use super::{AccountType, Asset, AssetAmount, AssetCallbackFlag, AssetError, Word};
use crate::Felt;
use crate::account::AccountId;
use crate::asset::AssetId;
Expand All @@ -26,7 +26,7 @@ use crate::utils::serde::{
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct FungibleAsset {
faucet_id: AccountId,
amount: u64,
amount: AssetAmount,
callbacks: AssetCallbackFlag,
}

Expand All @@ -35,15 +35,14 @@ impl FungibleAsset {
// --------------------------------------------------------------------------------------------
/// Specifies the maximum amount a fungible asset can represent.
///
/// This number was chosen so that it can be represented as a positive and negative number in a
/// field element. See `account_delta.masm` for more details on how this number was chosen.
pub const MAX_AMOUNT: u64 = 2u64.pow(63) - 2u64.pow(31);
/// Use [`AssetAmount::MAX`] instead for the validated wrapper type.
pub const MAX_AMOUNT: u64 = AssetAmount::MAX.inner();

/// The serialized size of a [`FungibleAsset`] in bytes.
///
/// An account ID (15 bytes) plus an amount (u64) plus a callbacks flag (u8).
pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE
+ core::mem::size_of::<u64>()
+ core::mem::size_of::<AssetAmount>()
+ AssetCallbackFlag::SERIALIZED_SIZE;

// CONSTRUCTOR
Expand All @@ -55,15 +54,13 @@ impl FungibleAsset {
///
/// Returns an error if:
/// - The faucet ID is not a valid fungible faucet ID.
/// - The provided amount is greater than [`FungibleAsset::MAX_AMOUNT`].
/// - The provided amount is greater than [`AssetAmount::MAX`].
pub fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) {
return Err(AssetError::FungibleFaucetIdTypeMismatch(faucet_id));
}

if amount > Self::MAX_AMOUNT {
return Err(AssetError::FungibleAssetAmountTooBig(amount));
}
let amount = AssetAmount::new(amount)?;

Ok(Self {
faucet_id,
Expand Down Expand Up @@ -126,7 +123,7 @@ impl FungibleAsset {
}

/// Returns the amount of this asset.
pub fn amount(&self) -> u64 {
pub fn amount(&self) -> AssetAmount {
self.amount
}

Expand Down Expand Up @@ -154,7 +151,7 @@ impl FungibleAsset {
/// Returns the asset's value encoded to a [`Word`].
pub fn to_value_word(&self) -> Word {
Word::new([
Felt::try_from(self.amount)
Felt::try_from(self.amount.inner())
.expect("fungible asset should only allow amounts that fit into a felt"),
Felt::ZERO,
Felt::ZERO,
Expand All @@ -180,13 +177,12 @@ impl FungibleAsset {
});
}

let amount = self
let raw_amount = self
.amount
.checked_add(other.amount)
.inner()
.checked_add(other.amount.inner())
.expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64");
if amount > Self::MAX_AMOUNT {
return Err(AssetError::FungibleAssetAmountTooBig(amount));
}
let amount = AssetAmount::new(raw_amount)?;

Ok(Self {
faucet_id: self.faucet_id,
Expand All @@ -210,12 +206,14 @@ impl FungibleAsset {
});
}

let amount = self.amount.checked_sub(other.amount).ok_or(
let raw_amount = self.amount.inner().checked_sub(other.amount.inner()).ok_or(
AssetError::FungibleAssetAmountNotSufficient {
minuend: self.amount,
subtrahend: other.amount,
minuend: self.amount.inner(),
subtrahend: other.amount.inner(),
},
)?;
// SAFETY: subtraction of two valid amounts always produces a valid amount.
let amount = AssetAmount::new_unchecked(raw_amount);

Ok(FungibleAsset {
faucet_id: self.faucet_id,
Expand Down
3 changes: 3 additions & 0 deletions crates/miden-protocol/src/asset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use super::utils::serde::{
use super::{Felt, Word};
use crate::account::AccountId;

mod asset_amount;
pub use asset_amount::AssetAmount;

mod fungible;

pub use fungible::FungibleAsset;
Expand Down
7 changes: 4 additions & 3 deletions crates/miden-protocol/src/asset/vault/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use miden_crypto::merkle::InnerNodeInfo;
use super::{
AccountType,
Asset,
AssetAmount,
ByteReader,
ByteWriter,
Deserializable,
Expand Down Expand Up @@ -118,7 +119,7 @@ impl AssetVault {
let asset = FungibleAsset::from_key_value(vault_key, asset_value)
.expect("asset vault should only store valid assets");

Ok(asset.amount())
Ok(asset.amount().inner())
}

/// Returns an iterator over the assets stored in the vault.
Expand Down Expand Up @@ -312,7 +313,7 @@ impl AssetVault {
.expect("asset vault should store valid assets");

// If the asset's amount is 0, we consider it absent from the vault.
if current_asset.amount() == 0 {
if current_asset.amount() == AssetAmount::ZERO {
return Err(AssetVaultError::FungibleAssetNotFound(other_asset));
}

Expand All @@ -325,7 +326,7 @@ impl AssetVault {
// leaf.
#[cfg(debug_assertions)]
{
if new_asset.amount() == 0 {
if new_asset.amount() == AssetAmount::ZERO {
assert!(new_asset.to_value_word().is_empty())
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/miden-protocol/src/transaction/kernel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ impl TransactionKernel {
outputs.extend(account_update_commitment);
outputs.push(fee.faucet_id().suffix());
outputs.push(fee.faucet_id().prefix().as_felt());
outputs.push(Felt::try_from(fee.amount()).expect("amount should fit into felt"));
outputs.push(Felt::try_from(fee.amount().inner()).expect("amount should fit into felt"));
outputs.push(Felt::from(expiration_block_num));

StackOutputs::new(&outputs).expect("number of stack inputs should be <= 16")
Expand Down
Loading
Loading