Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Serializable for FungibleAsset #907

Merged
merged 3 commits into from
Oct 14, 2024
Merged
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 @@ -25,6 +25,7 @@
- [BREAKING] Moved `MAX_NUM_FOREIGN_ACCOUNTS` into `miden-objects` (#904).
- Implemented `storage_size`, updated storage bounds (#886).
- [BREAKING] Auto-generate `KERNEL_ERRORS` list from the transaction kernel's MASM files and rework error constant names (#906).
- Implement `Serializable` for `FungibleAsset` (#907).

## 0.5.1 (2024-08-28) - `miden-objects` crate only

Expand Down
101 changes: 80 additions & 21 deletions objects/src/assets/fungible.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use alloc::string::ToString;
use core::fmt;

use vm_core::FieldElement;
use vm_core::{
utils::{ByteReader, ByteWriter, Deserializable, Serializable},
FieldElement,
};
use vm_processor::DeserializationError;

use super::{
is_not_a_non_fungible_asset, parse_word, AccountId, AccountType, Asset, AssetError, Felt, Word,
ZERO,
is_not_a_non_fungible_asset, AccountId, AccountType, Asset, AssetError, Felt, Word, ZERO,
};

// FUNGIBLE ASSET
Expand Down Expand Up @@ -145,16 +148,6 @@ impl From<FungibleAsset> for Word {
}
}

impl From<FungibleAsset> for [u8; 32] {
fn from(asset: FungibleAsset) -> Self {
let mut result = [0_u8; 32];
let id_bytes: [u8; 8] = asset.faucet_id.into();
result[..8].copy_from_slice(&asset.amount.to_le_bytes());
result[24..].copy_from_slice(&id_bytes);
result
}
}

impl From<FungibleAsset> for Asset {
fn from(asset: FungibleAsset) -> Self {
Asset::Fungible(asset)
Expand All @@ -175,17 +168,83 @@ impl TryFrom<Word> for FungibleAsset {
}
}

impl TryFrom<[u8; 32]> for FungibleAsset {
type Error = AssetError;
impl fmt::Display for FungibleAsset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}

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

impl Serializable for FungibleAsset {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
// All assets should serialize their faucet ID at the first position to allow them to be
// easily distinguishable during deserialization.
target.write(self.faucet_id);
target.write(self.amount);
}

fn try_from(value: [u8; 32]) -> Result<Self, Self::Error> {
let word = parse_word(value)?;
Self::try_from(word)
fn get_size_hint(&self) -> usize {
self.faucet_id.get_size_hint() + self.amount.get_size_hint()
}
}

impl fmt::Display for FungibleAsset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
impl Deserializable for FungibleAsset {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let faucet_id: AccountId = source.read()?;
FungibleAsset::deserialize_with_account_id(faucet_id, source)
}
}

impl FungibleAsset {
/// Deserializes a [`FungibleAsset`] from an [`AccountId`] and the remaining data from the given
/// `source`.
pub(super) fn deserialize_with_account_id<R: ByteReader>(
faucet_id: AccountId,
source: &mut R,
) -> Result<Self, DeserializationError> {
let amount: u64 = source.read()?;
FungibleAsset::new(faucet_id, amount)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}

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

#[cfg(test)]
mod tests {
use super::*;
use crate::accounts::account_id::testing::{
ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN,
};

#[test]
fn test_fungible_asset_serde() {
for fungible_account_id in [
ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3,
] {
let account_id = AccountId::try_from(fungible_account_id).unwrap();
let fungible_asset = FungibleAsset::new(account_id, 10).unwrap();
assert_eq!(
fungible_asset,
FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
);
}

let account_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3).unwrap();
let asset = FungibleAsset::new(account_id, 50).unwrap();
let mut asset_bytes = asset.to_bytes();
// Set invalid Faucet ID.
asset_bytes[0..8].copy_from_slice(&ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN.to_le_bytes());
let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
assert!(matches!(err, DeserializationError::InvalidValue(_)));
}
}
89 changes: 27 additions & 62 deletions objects/src/assets/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use alloc::string::ToString;

use super::{
accounts::{AccountId, AccountType, ACCOUNT_ISFAUCET_MASK},
utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
Expand Down Expand Up @@ -70,12 +68,6 @@ pub enum Asset {
}

impl Asset {
// CONSTANTS
// --------------------------------------------------------------------------------------------

/// The serialized size of an [`Asset`] in bytes.
pub const SERIALIZED_SIZE: usize = 32;

/// Creates a new [Asset] without checking its validity.
pub(crate) fn new_unchecked(value: Word) -> Asset {
if is_not_a_non_fungible_asset(value) {
Expand Down Expand Up @@ -152,21 +144,6 @@ impl From<&Asset> for Word {
}
}

impl From<Asset> for [u8; Asset::SERIALIZED_SIZE] {
fn from(asset: Asset) -> Self {
match asset {
Asset::Fungible(asset) => asset.into(),
Asset::NonFungible(asset) => asset.into(),
}
}
}

impl From<&Asset> for [u8; Asset::SERIALIZED_SIZE] {
fn from(value: &Asset) -> Self {
(*value).into()
}
}

impl TryFrom<&Word> for Asset {
type Error = AssetError;

Expand All @@ -187,64 +164,51 @@ impl TryFrom<Word> for Asset {
}
}

impl TryFrom<[u8; Asset::SERIALIZED_SIZE]> for Asset {
type Error = AssetError;

fn try_from(value: [u8; Asset::SERIALIZED_SIZE]) -> Result<Self, Self::Error> {
parse_word(value)?.try_into()
}
}

impl TryFrom<&[u8; Asset::SERIALIZED_SIZE]> for Asset {
type Error = AssetError;

fn try_from(value: &[u8; Asset::SERIALIZED_SIZE]) -> Result<Self, Self::Error> {
(*value).try_into()
}
}

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

impl Serializable for Asset {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
let data: [u8; Asset::SERIALIZED_SIZE] = self.into();
target.write_bytes(&data);
match self {
Asset::Fungible(fungible_asset) => fungible_asset.write_into(target),
Asset::NonFungible(non_fungible_asset) => non_fungible_asset.write_into(target),
}
}

fn get_size_hint(&self) -> usize {
Asset::SERIALIZED_SIZE
match self {
Asset::Fungible(fungible_asset) => fungible_asset.get_size_hint(),
Asset::NonFungible(non_fungible_asset) => non_fungible_asset.get_size_hint(),
}
}
}

impl Deserializable for Asset {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let data_vec = source.read_vec(Asset::SERIALIZED_SIZE)?;
let data_array: [u8; Asset::SERIALIZED_SIZE] =
data_vec.try_into().expect("Vec must be of size 32");

let asset = Asset::try_from(&data_array)
.map_err(|error| DeserializationError::InvalidValue(format!("{error}")))?;
Ok(asset)
// Both asset types have their faucet ID as the first element, so we can use it to inspect
// what type of asset it is.
let account_id: AccountId = source.read()?;
let account_type = account_id.account_type();

match account_type {
AccountType::FungibleFaucet => {
FungibleAsset::deserialize_with_account_id(account_id, source).map(Asset::from)
},
AccountType::NonFungibleFaucet => {
NonFungibleAsset::deserialize_with_account_id(account_id, source).map(Asset::from)
},
other_type => {
Err(DeserializationError::InvalidValue(format!(
"failed to deserialize asset: expected an account ID of type faucet, found {other_type:?}"
)))
},
}
}
}

// HELPER FUNCTIONS
// ================================================================================================

fn parse_word(bytes: [u8; Asset::SERIALIZED_SIZE]) -> Result<Word, AssetError> {
Ok([
parse_felt(&bytes[..8])?,
parse_felt(&bytes[8..16])?,
parse_felt(&bytes[16..24])?,
parse_felt(&bytes[24..])?,
])
}

fn parse_felt(bytes: &[u8]) -> Result<Felt, AssetError> {
Felt::try_from(bytes).map_err(|err| AssetError::InvalidFieldElement(err.to_string()))
}

/// Returns `true` if asset in [Word] is not a non-fungible asset.
///
/// Note: this does not mean that the word is a fungible asset as the word may contain an value
Expand All @@ -260,6 +224,7 @@ fn is_not_a_non_fungible_asset(asset: Word) -> bool {

#[cfg(test)]
mod tests {

use miden_crypto::{
utils::{Deserializable, Serializable},
Word,
Expand Down
Loading
Loading