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

feat(types): add mana allotment struct #969

Merged
Merged
Show file tree
Hide file tree
Changes from 8 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
13 changes: 11 additions & 2 deletions sdk/src/types/block/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crypto::Error as CryptoError;
use prefix_hex::Error as HexError;
use primitive_types::U256;

use super::mana::AllotmentCount;
use crate::types::block::{
input::UtxoInput,
output::{
Expand All @@ -28,6 +29,7 @@ pub enum Error {
CreatedAmountOverflow,
CreatedNativeTokensAmountOverflow,
Crypto(CryptoError),
DuplicateAllotment(AccountId),
DuplicateSignatureUnlock(u16),
DuplicateUtxo(UtxoInput),
ExpirationUnlockConditionZero,
Expand All @@ -53,13 +55,15 @@ pub enum Error {
InvalidBech32Hrp(String),
InvalidBlockLength(usize),
InvalidStateMetadataLength(<StateMetadataLength as TryFrom<usize>>::Error),
InvalidManaValue(u64),
InvalidMetadataFeatureLength(<MetadataFeatureLength as TryFrom<usize>>::Error),
InvalidNativeTokenCount(<NativeTokenCount as TryFrom<usize>>::Error),
InvalidNetworkName(FromUtf8Error),
InvalidNftIndex(<UnlockIndex as TryFrom<u16>>::Error),
InvalidOutputAmount(u64),
InvalidOutputCount(<OutputCount as TryFrom<usize>>::Error),
InvalidOutputKind(u8),
InvalidAllotmentCount(<AllotmentCount as TryFrom<usize>>::Error),
// TODO this would now need to be generic, not sure if possible.
// https://github.com/iotaledger/iota-sdk/issues/647
// InvalidParentCount(<BoundedU8 as TryFrom<usize>>::Error),
Expand All @@ -78,6 +82,7 @@ pub enum Error {
InvalidTokenSchemeKind(u8),
InvalidTransactionAmountSum(u128),
InvalidTransactionNativeTokensCount(u16),
InvalidAllotmentManaSum(u128),
InvalidUnlockCount(<UnlockCount as TryFrom<usize>>::Error),
InvalidUnlockKind(u8),
InvalidUnlockReference(u16),
Expand Down Expand Up @@ -122,6 +127,7 @@ impl fmt::Display for Error {
Self::CreatedAmountOverflow => write!(f, "created amount overflow"),
Self::CreatedNativeTokensAmountOverflow => write!(f, "created native tokens amount overflow"),
Self::Crypto(e) => write!(f, "cryptographic error: {e}"),
Self::DuplicateAllotment(id) => write!(f, "duplicate allotment, account ID: {id}"),
Self::DuplicateSignatureUnlock(index) => {
write!(f, "duplicate signature unlock at index: {index}")
}
Expand Down Expand Up @@ -178,16 +184,18 @@ impl fmt::Display for Error {
Self::InvalidInputCount(count) => write!(f, "invalid input count: {count}"),
Self::InvalidInputOutputIndex(index) => write!(f, "invalid input or output index: {index}"),
Self::InvalidBlockLength(length) => write!(f, "invalid block length {length}"),
Self::InvalidStateMetadataLength(length) => write!(f, "invalid state metadata length {length}"),
Self::InvalidStateMetadataLength(length) => write!(f, "invalid state metadata length: {length}"),
Self::InvalidManaValue(mana) => write!(f, "invalid mana value: {mana}"),
Self::InvalidMetadataFeatureLength(length) => {
write!(f, "invalid metadata feature length {length}")
write!(f, "invalid metadata feature length: {length}")
}
Self::InvalidNativeTokenCount(count) => write!(f, "invalid native token count: {count}"),
Self::InvalidNetworkName(err) => write!(f, "invalid network name: {err}"),
Self::InvalidNftIndex(index) => write!(f, "invalid nft index: {index}"),
Self::InvalidOutputAmount(amount) => write!(f, "invalid output amount: {amount}"),
Self::InvalidOutputCount(count) => write!(f, "invalid output count: {count}"),
Self::InvalidOutputKind(k) => write!(f, "invalid output kind: {k}"),
Self::InvalidAllotmentCount(count) => write!(f, "invalid allotment count: {count}"),
Self::InvalidParentCount => {
write!(f, "invalid parents count")
}
Expand Down Expand Up @@ -215,6 +223,7 @@ impl fmt::Display for Error {
Self::InvalidTransactionNativeTokensCount(count) => {
write!(f, "invalid transaction native tokens count: {count}")
}
Self::InvalidAllotmentManaSum(value) => write!(f, "invalid allotment mana sum: {value}"),
Self::InvalidUnlockCount(count) => write!(f, "invalid unlock count: {count}"),
Self::InvalidUnlockKind(k) => write!(f, "invalid unlock kind: {k}"),
Self::InvalidUnlockReference(index) => {
Expand Down
94 changes: 94 additions & 0 deletions sdk/src/types/block/mana/allotment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use packable::{
error::{UnpackError, UnpackErrorExt},
packer::Packer,
unpacker::Unpacker,
Packable,
};

use super::MAX_THEORETICAL_MANA;
use crate::types::block::{output::AccountId, Error};

/// An allotment of Mana which will be added upon commitment of the slot in which the containing transaction was issued,
/// in the form of Block Issuance Credits to the account.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(rename_all = "camelCase"))]
pub struct Allotment {
pub(crate) account_id: AccountId,
#[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::string"))]
pub(crate) mana: u64,
}

impl Allotment {
pub fn new(account_id: AccountId, mana: u64) -> Result<Self, Error> {
if mana > MAX_THEORETICAL_MANA {
return Err(Error::InvalidManaValue(mana));
}
Ok(Self { account_id, mana })
}

pub fn account_id(&self) -> &AccountId {
&self.account_id
}

pub fn mana(&self) -> u64 {
self.mana
}
}

impl Packable for Allotment {
type UnpackError = Error;
type UnpackVisitor = ();

fn pack<P: Packer>(&self, packer: &mut P) -> Result<(), P::Error> {
self.account_id.pack(packer)?;
self.mana.pack(packer)?;
Ok(())
}

fn unpack<U: Unpacker, const VERIFY: bool>(
unpacker: &mut U,
visitor: &Self::UnpackVisitor,
) -> Result<Self, UnpackError<Self::UnpackError, U::Error>> {
let account_id = AccountId::unpack::<_, VERIFY>(unpacker, visitor).coerce()?;
let mana = u64::unpack::<_, VERIFY>(unpacker, visitor).coerce()?;
Ok(Self { account_id, mana })
}
}

#[cfg(feature = "serde")]
mod dto {
use serde::Deserialize;

use super::*;
use crate::utils::serde::string;

impl<'de> Deserialize<'de> for Allotment {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AllotmentDto {
account_id: AccountId,
#[cfg_attr(feature = "serde", serde(with = "string"))]
kwek20 marked this conversation as resolved.
Show resolved Hide resolved
mana: u64,
}

impl TryFrom<AllotmentDto> for Allotment {
type Error = Error;

fn try_from(value: AllotmentDto) -> Result<Self, Self::Error> {
Self::new(value.account_id, value.mana)
}
}

AllotmentDto::deserialize(d)?
.try_into()
.map_err(serde::de::Error::custom)
}
}
}
95 changes: 95 additions & 0 deletions sdk/src/types/block/mana/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

mod allotment;

use alloc::{boxed::Box, vec::Vec};
use core::ops::RangeInclusive;

use derive_more::Deref;
use hashbrown::HashSet;
use packable::{bounded::BoundedU16, prefix::BoxedSlicePrefix, Packable};

pub use self::allotment::Allotment;
use super::{output::AccountId, Error};

/// The number of bits that a given mana value can use, excluding the sign bit.
pub const MANA_BITS: u64 = 63;
/// Equivalent to `2^MANA_BITS - 1`
pub const MAX_THEORETICAL_MANA: u64 = u64::MAX >> 1;
DaughterOfMars marked this conversation as resolved.
Show resolved Hide resolved

pub(crate) type AllotmentCount = BoundedU16<{ *Allotments::COUNT_RANGE.start() }, { *Allotments::COUNT_RANGE.end() }>;

/// A list of [`Allotment`]s with unique [`AccountId`]s.
#[derive(Clone, Debug, Eq, PartialEq, Deref, Packable)]
#[packable(unpack_error = Error, with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidAllotmentCount(p.into())))]
pub struct Allotments(#[packable(verify_with = verify_allotments)] BoxedSlicePrefix<Allotment, AllotmentCount>);

fn verify_allotments<const VERIFY: bool>(allotments: &[Allotment], _visitor: &()) -> Result<(), Error> {
if VERIFY {
let mut mana_sum: u64 = 0;
let mut unique_ids = HashSet::with_capacity(allotments.len());
for Allotment { account_id, mana } in allotments.iter() {
mana_sum = mana_sum
.checked_add(*mana)
.ok_or(Error::InvalidAllotmentManaSum(mana_sum as u128 + *mana as u128))?;

if mana_sum > MAX_THEORETICAL_MANA {
return Err(Error::InvalidAllotmentManaSum(mana_sum as u128));
}

if !unique_ids.insert(account_id) {
return Err(Error::DuplicateAllotment(*account_id));
}
}
}

Ok(())
}

impl TryFrom<Vec<Allotment>> for Allotments {
type Error = Error;

#[inline(always)]
fn try_from(allotments: Vec<Allotment>) -> Result<Self, Self::Error> {
Self::from_vec(allotments)
}
}

impl IntoIterator for Allotments {
type Item = Allotment;
type IntoIter = alloc::vec::IntoIter<Self::Item>;

fn into_iter(self) -> Self::IntoIter {
Vec::from(Into::<Box<[Allotment]>>::into(self.0)).into_iter()
}
}

impl Allotments {
/// The minimum number of allotments of a transaction.
pub const COUNT_MIN: u16 = 1;
kwek20 marked this conversation as resolved.
Show resolved Hide resolved
/// The maximum number of allotments of a transaction.
pub const COUNT_MAX: u16 = 128;
/// The range of valid numbers of allotments of a transaction.
pub const COUNT_RANGE: RangeInclusive<u16> = Self::COUNT_MIN..=Self::COUNT_MAX; // [1..128]

/// Creates a new [`Allotments`] from a vec.
pub fn from_vec(allotments: Vec<Allotment>) -> Result<Self, Error> {
let allotments = BoxedSlicePrefix::<Allotment, AllotmentCount>::try_from(allotments.into_boxed_slice())
.map_err(Error::InvalidAllotmentCount)?;

verify_allotments::<true>(&allotments, &())?;

Ok(Self(allotments))
}

/// Gets a reference to an [`Allotment`], if one exists, using an [`AccountId`].
#[inline(always)]
pub fn get(&self, account_id: &AccountId) -> Option<&Allotment> {
self.0
.iter()
.position(|a| a.account_id() == account_id)
// PANIC: indexation is fine since the index has been found.
.map(|index| &self.0[index])
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
}
}
2 changes: 2 additions & 0 deletions sdk/src/types/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub mod core;
pub mod helper;
/// A module that provides types and syntactic validations of inputs.
pub mod input;
/// A module that provides types and syntactic validations of mana.
pub mod mana;
/// A module that provides types and syntactic validations of outputs.
pub mod output;
/// A module that provides types and syntactic validations of parents.
Expand Down
4 changes: 2 additions & 2 deletions sdk/src/types/block/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ use crate::types::{

/// The maximum number of outputs of a transaction.
pub const OUTPUT_COUNT_MAX: u16 = 128;
/// The range of valid numbers of outputs of a transaction .
/// The range of valid numbers of outputs of a transaction.
pub const OUTPUT_COUNT_RANGE: RangeInclusive<u16> = 1..=OUTPUT_COUNT_MAX; // [1..128]
/// The maximum index of outputs of a transaction.
pub const OUTPUT_INDEX_MAX: u16 = OUTPUT_COUNT_MAX - 1; // 127
/// The range of valid indices of outputs of a transaction .
/// The range of valid indices of outputs of a transaction.
pub const OUTPUT_INDEX_RANGE: RangeInclusive<u16> = 0..=OUTPUT_INDEX_MAX; // [0..127]

#[derive(Copy, Clone)]
Expand Down
Loading
Loading