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 1 commit
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
9 changes: 6 additions & 3 deletions sdk/src/types/block/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ 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::{
feature::FeatureCount, unlock_condition::UnlockConditionCount, AccountId, ChainId, MetadataFeatureLength,
NativeTokenCount, NftId, OutputIndex, StateMetadataLength, TagFeatureLength,
},
payload::{AllotmentCount, InputCount, OutputCount, TagLength, TaggedDataLength},
payload::{InputCount, OutputCount, TagLength, TaggedDataLength},
unlock::{UnlockCount, UnlockIndex},
};

Expand Down Expand Up @@ -53,6 +54,7 @@ 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),
Expand Down Expand Up @@ -180,9 +182,10 @@ 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}"),
Expand Down
75 changes: 38 additions & 37 deletions sdk/src/types/block/mana/allotment.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
// Copyright 2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use core::ops::RangeInclusive;

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

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

/// The maximum number of allotments of a transaction.
pub const ALLOTMENT_COUNT_MAX: u16 = 128;
/// The range of valid numbers of allotments of a transaction.
pub const ALLOTMENT_COUNT_RANGE: RangeInclusive<u16> = 1..=ALLOTMENT_COUNT_MAX; // [1..128]

/// TODO
#[derive(Clone, Debug, Eq, PartialEq)]
/// 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 {
account_id: AccountId,
mana: u64,
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) -> Self {
Self { account_id, mana }
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 {
Expand Down Expand Up @@ -58,36 +58,37 @@ impl Packable for Allotment {
}
}

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

use super::*;
use crate::types::{block::Error, TryFromDto, ValidationParams};
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,
}

/// TODO
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AllotmentDto {
pub account_id: AccountId,
#[serde(with = "crate::utils::serde::string")]
pub mana: u64,
}
impl TryFrom<AllotmentDto> for Allotment {
type Error = Error;

impl From<&Allotment> for AllotmentDto {
fn from(value: &Allotment) -> Self {
Self {
account_id: value.account_id,
mana: value.mana,
fn try_from(value: AllotmentDto) -> Result<Self, Self::Error> {
Self::new(value.account_id, value.mana)
}
}
}
}

impl TryFromDto for Allotment {
type Dto = AllotmentDto;
type Error = Error;

fn try_from_dto_with_params_inner(dto: Self::Dto, _params: ValidationParams<'_>) -> Result<Self, Self::Error> {
// TODO: we may want to validate the mana amount?
Ok(Self::new(dto.account_id, dto.mana))
AllotmentDto::deserialize(d)?
.try_into()
.map_err(serde::de::Error::custom)
}
}
}
90 changes: 89 additions & 1 deletion sdk/src/types/block/mana/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,92 @@

mod allotment;

pub use self::allotment::{dto, Allotment, ALLOTMENT_COUNT_MAX, ALLOTMENT_COUNT_RANGE};
/// 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;

use core::ops::RangeInclusive;
use std::collections::HashSet;

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

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

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
.binary_search_by(|a| a.account_id().cmp(account_id))
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
// PANIC: indexation is fine since the index has been found.
.map(|index| &self.0[index])
.ok()
}
}
2 changes: 1 addition & 1 deletion sdk/src/types/block/payload/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use packable::{
pub use self::{tagged_data::TaggedDataPayload, transaction::TransactionPayload};
pub(crate) use self::{
tagged_data::{TagLength, TaggedDataLength},
transaction::{AllotmentCount, InputCount, OutputCount},
transaction::{InputCount, OutputCount},
};
use crate::types::block::{protocol::ProtocolParameters, Error};

Expand Down
2 changes: 1 addition & 1 deletion sdk/src/types/block/payload/transaction/essence/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crypto::hashes::{blake2b::Blake2b256, Digest};
use derive_more::From;
use packable::PackableExt;

pub(crate) use self::regular::{AllotmentCount, InputCount, OutputCount};
pub(crate) use self::regular::{InputCount, OutputCount};
pub use self::regular::{RegularTransactionEssence, RegularTransactionEssenceBuilder};
use crate::types::block::Error;

Expand Down
61 changes: 9 additions & 52 deletions sdk/src/types/block/payload/transaction/essence/regular.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use packable::{bounded::BoundedU16, prefix::BoxedSlicePrefix, Packable};
use crate::types::{
block::{
input::{Input, INPUT_COUNT_RANGE},
mana::{Allotment, ALLOTMENT_COUNT_RANGE},
mana::{Allotment, Allotments},
output::{InputsCommitment, NativeTokens, Output, OUTPUT_COUNT_RANGE},
payload::{OptionalPayload, Payload},
protocol::ProtocolParameters,
Expand Down Expand Up @@ -70,8 +70,8 @@ impl RegularTransactionEssenceBuilder {
}

/// Add allotments to a [`RegularTransactionEssenceBuilder`].
pub fn with_allotments(mut self, allotments: impl Into<Vec<Allotment>>) -> Self {
self.allotments = allotments.into();
pub fn with_allotments(mut self, allotments: impl IntoIterator<Item = Allotment>) -> Self {
self.allotments = allotments.into_iter().collect();
self
}

Expand Down Expand Up @@ -126,15 +126,7 @@ impl RegularTransactionEssenceBuilder {
verify_outputs::<true>(&outputs, protocol_parameters)?;
}

let allotments: BoxedSlicePrefix<Allotment, AllotmentCount> = self
.allotments
.into_boxed_slice()
.try_into()
.map_err(Error::InvalidAllotmentCount)?;

if let Some(protocol_parameters) = params.protocol_parameters() {
verify_allotments::<true>(&allotments, protocol_parameters)?;
}
let allotments = Allotments::from_vec(self.allotments)?;

verify_payload::<true>(&self.payload)?;

Expand Down Expand Up @@ -172,7 +164,6 @@ impl RegularTransactionEssenceBuilder {

pub(crate) type InputCount = BoundedU16<{ *INPUT_COUNT_RANGE.start() }, { *INPUT_COUNT_RANGE.end() }>;
pub(crate) type OutputCount = BoundedU16<{ *OUTPUT_COUNT_RANGE.start() }, { *OUTPUT_COUNT_RANGE.end() }>;
pub(crate) type AllotmentCount = BoundedU16<{ *ALLOTMENT_COUNT_RANGE.start() }, { *ALLOTMENT_COUNT_RANGE.end() }>;

/// A transaction regular essence consuming inputs, creating outputs and carrying an optional payload.
#[derive(Clone, Debug, Eq, PartialEq, Packable)]
Expand All @@ -192,9 +183,7 @@ pub struct RegularTransactionEssence {
#[packable(verify_with = verify_outputs)]
#[packable(unpack_error_with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidOutputCount(p.into())))]
outputs: BoxedSlicePrefix<Output, OutputCount>,
#[packable(verify_with = verify_allotments)]
#[packable(unpack_error_with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidAllotmentCount(p.into())))]
allotments: BoxedSlicePrefix<Allotment, AllotmentCount>,
allotments: Allotments,
#[packable(verify_with = verify_payload_packable)]
payload: OptionalPayload,
}
Expand Down Expand Up @@ -325,31 +314,6 @@ fn verify_outputs<const VERIFY: bool>(outputs: &[Output], visitor: &ProtocolPara
Ok(())
}

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

// TODO: compare with `max_mana_supply` from visitor once available
// if mana_sum > visitor.max_mana_supply() {
// return Err(Error::InvalidAllotmentManaSum(mana_sum as u128));
// }

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

Ok(())
}

fn verify_payload<const VERIFY: bool>(payload: &OptionalPayload) -> Result<(), Error> {
if VERIFY {
match &payload.0 {
Expand Down Expand Up @@ -379,9 +343,7 @@ pub(crate) mod dto {

use super::*;
use crate::types::{
block::{
input::dto::InputDto, mana::dto::AllotmentDto, output::dto::OutputDto, payload::dto::PayloadDto, Error,
},
block::{input::dto::InputDto, output::dto::OutputDto, payload::dto::PayloadDto, Error},
TryFromDto,
};

Expand All @@ -396,7 +358,7 @@ pub(crate) mod dto {
pub inputs: Vec<InputDto>,
pub inputs_commitment: String,
pub outputs: Vec<OutputDto>,
pub allotments: Vec<AllotmentDto>,
pub allotments: Vec<Allotment>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload: Option<PayloadDto>,
}
Expand All @@ -410,7 +372,7 @@ pub(crate) mod dto {
inputs: value.inputs().iter().map(Into::into).collect::<Vec<_>>(),
inputs_commitment: value.inputs_commitment().to_string(),
outputs: value.outputs().iter().map(Into::into).collect::<Vec<_>>(),
allotments: value.allotments().iter().map(Into::into).collect::<Vec<_>>(),
allotments: value.allotments().iter().copied().collect(),
payload: match value.payload() {
Some(Payload::TaggedData(i)) => Some(PayloadDto::TaggedData(Box::new(i.as_ref().into()))),
Some(_) => unimplemented!(),
Expand Down Expand Up @@ -439,17 +401,12 @@ pub(crate) mod dto {
.into_iter()
.map(|o| Output::try_from_dto_with_params(o, &params))
.collect::<Result<Vec<Output>, Error>>()?;
let allotments = dto
.allotments
.into_iter()
.map(|a| Allotment::try_from_dto_with_params(a, &params))
.collect::<Result<Vec<Allotment>, Error>>()?;

let mut builder = Self::builder(network_id, InputsCommitment::from_str(&dto.inputs_commitment)?)
.with_creation_time(dto.creation_time)
.with_inputs(inputs)
.with_outputs(outputs)
.with_allotments(allotments);
.with_allotments(dto.allotments);

builder = if let Some(p) = dto.payload {
if let PayloadDto::TaggedData(i) = p {
Expand Down
Loading