Skip to content

Commit

Permalink
ISA: add mana requirement (#1762)
Browse files Browse the repository at this point in the history
* Add ManaAllotments to InputSelection

* Add fulfill_mana_requirement

* Add allotments to requirement

* Already fulfilled check

* Select inputs for mana

* Cleanup allotments

* Remove ManaAllotment

* Pass mana allotments to ISA

* Build tw with mana allotments

* Add allot_mana and prepare_allot_mana

* Add allot_mana_command to CLI

* Add CLI log

* Fix deadlock

* Temporarily disable mana semantic check

* fallback to implicit accounts

* ManaAllotment camelCase

* Tmp mana burn

* Move mana burn cap to a single place

* Some clippy

* Nits

* Copyright date

* Add TODO

* Cleanup TODO

* Add mana_allotments to ISA output count check

* Nit

* Remove check

* Fix no_std

* move dto.outputs

* Nit

---------

Co-authored-by: /alex/ <[email protected]>
  • Loading branch information
thibault-martinez and Alex6323 authored Jan 10, 2024
1 parent 656f20f commit 679b8fa
Show file tree
Hide file tree
Showing 20 changed files with 337 additions and 222 deletions.
1 change: 1 addition & 0 deletions cli/src/wallet_cli/completer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use rustyline::{
const WALLET_COMMANDS: &[&str] = &[
"accounts",
"address",
"allot-mana",
"balance",
"burn-native-token",
"burn-nft",
Expand Down
34 changes: 33 additions & 1 deletion cli/src/wallet_cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use iota_sdk::{
api::plugins::participation::types::ParticipationEventId,
block::{
address::{Bech32Address, ToBech32Ext},
mana::ManaAllotment,
output::{
unlock_condition::AddressUnlockCondition, AccountId, BasicOutputBuilder, FoundryId, NativeToken,
NativeTokensBuilder, NftId, Output, OutputId, TokenId,
Expand Down Expand Up @@ -60,6 +61,8 @@ pub enum WalletCommand {
Accounts,
/// Print the wallet address.
Address,
/// Allots mana to an account.
AllotMana { mana: u64, account_id: Option<AccountId> },
/// Print the wallet balance.
Balance,
/// Burn an amount of native token.
Expand Down Expand Up @@ -344,6 +347,32 @@ pub async fn address_command(wallet: &Wallet) -> Result<(), Error> {
Ok(())
}

// `allot-mana` command
pub async fn allot_mana_command(wallet: &Wallet, mana: u64, account_id: Option<AccountId>) -> Result<(), Error> {
let wallet_data = wallet.data().await;
let account_id = account_id
.or(wallet_data
.accounts()
.next()
.map(|o| o.output.as_account().account_id_non_null(&o.output_id)))
.or(wallet_data
.implicit_accounts()
.next()
.map(|o| AccountId::from(&o.output_id)))
.ok_or(WalletError::AccountNotFound)?;
drop(wallet_data);

let transaction = wallet.allot_mana([ManaAllotment::new(account_id, mana)?], None).await?;

println_log_info!(
"Mana allotment transaction sent:\n{:?}\n{:?}",
transaction.transaction_id,
transaction.block_id
);

Ok(())
}

// `balance` command
pub async fn balance_command(wallet: &Wallet) -> Result<(), Error> {
let balance = wallet.balance().await?;
Expand Down Expand Up @@ -497,7 +526,7 @@ pub async fn congestion_command(wallet: &Wallet, account_id: Option<AccountId>)
.next()
.map(|o| AccountId::from(&o.output_id))
})
.ok_or(WalletError::NoAccountToIssueBlock)?
.ok_or(WalletError::AccountNotFound)?
};

let congestion = wallet.client().get_account_congestion(&account_id).await?;
Expand Down Expand Up @@ -1173,6 +1202,9 @@ pub async fn prompt_internal(
match protocol_cli.command {
WalletCommand::Accounts => accounts_command(wallet).await,
WalletCommand::Address => address_command(wallet).await,
WalletCommand::AllotMana { mana, account_id } => {
allot_mana_command(wallet, mana, account_id).await
}
WalletCommand::Balance => balance_command(wallet).await,
WalletCommand::BurnNativeToken { token_id, amount } => {
burn_native_token_command(wallet, token_id, amount).await
Expand Down
15 changes: 13 additions & 2 deletions sdk/src/client/api/block_builder/input_selection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::{
types::block::{
address::{AccountAddress, Address, NftAddress},
input::INPUT_COUNT_RANGE,
mana::ManaAllotment,
output::{
AccountOutput, ChainId, FoundryOutput, NativeTokensBuilder, NftOutput, Output, OutputId, OUTPUT_COUNT_RANGE,
},
Expand All @@ -45,6 +46,7 @@ pub struct InputSelection {
slot_index: SlotIndex,
requirements: Vec<Requirement>,
automatically_transitioned: HashSet<ChainId>,
mana_allotments: u64,
}

/// Result of the input selection algorithm.
Expand Down Expand Up @@ -101,6 +103,8 @@ impl InputSelection {
}

fn init(&mut self) -> Result<(), Error> {
// Adds an initial mana requirement.
self.requirements.push(Requirement::Mana(self.mana_allotments));
// Adds an initial amount requirement.
self.requirements.push(Requirement::Amount);
// Adds an initial native tokens requirement.
Expand Down Expand Up @@ -190,6 +194,7 @@ impl InputSelection {
slot_index: SlotIndex::from(0),
requirements: Vec::new(),
automatically_transitioned: HashSet::new(),
mana_allotments: 0,
}
}

Expand Down Expand Up @@ -223,6 +228,12 @@ impl InputSelection {
self
}

/// Sets the mana allotments sum of an [`InputSelection`].
pub fn with_mana_allotments<'a>(mut self, mana_allotments: impl Iterator<Item = &'a ManaAllotment>) -> Self {
self.mana_allotments = mana_allotments.map(ManaAllotment::mana).sum();
self
}

fn filter_inputs(&mut self) {
self.available_inputs.retain(|input| {
// TODO what about other kinds?
Expand Down Expand Up @@ -373,8 +384,8 @@ impl InputSelection {
/// transaction. Also creates a remainder output and chain transition outputs if required.
pub fn select(mut self) -> Result<Selected, Error> {
if !OUTPUT_COUNT_RANGE.contains(&(self.outputs.len() as u16)) {
// If burn is provided, outputs will be added later
if !(self.outputs.is_empty() && self.burn.is_some()) {
// If burn or mana allotments are provided, outputs will be added later.
if !(self.outputs.is_empty() && (self.burn.is_some() || self.mana_allotments != 0)) {
return Err(Error::InvalidOutputCount(self.outputs.len()));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use super::{Error, InputSelection};
use crate::client::secret::types::InputSigningData;

impl InputSelection {
pub(crate) fn fulfill_mana_requirement(&mut self, allotments: u64) -> Result<Vec<InputSigningData>, Error> {
let required_mana = self.outputs.iter().map(|o| o.mana()).sum::<u64>() + allotments;
let mut selected_mana = self.selected_inputs.iter().map(|o| o.output.mana()).sum::<u64>();

if selected_mana >= required_mana {
log::debug!("Mana requirement already fulfilled");
Ok(Vec::new())
} else {
let mut inputs = Vec::new();

// TODO we should do as for the amount and have preferences on which inputs to pick.
while let Some(input) = self.available_inputs.pop() {
selected_mana += input.output.mana();
inputs.push(input);

if selected_mana >= required_mana {
break;
}
}

Ok(inputs)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) mod amount;
pub(crate) mod ed25519;
pub(crate) mod foundry;
pub(crate) mod issuer;
pub(crate) mod mana;
pub(crate) mod native_tokens;
pub(crate) mod nft;
pub(crate) mod sender;
Expand Down Expand Up @@ -39,6 +40,8 @@ pub enum Requirement {
NativeTokens,
/// Amount requirement.
Amount,
/// Mana requirement.
Mana(u64),
}

impl InputSelection {
Expand All @@ -56,6 +59,7 @@ impl InputSelection {
Requirement::Nft(nft_id) => self.fulfill_nft_requirement(nft_id),
Requirement::NativeTokens => self.fulfill_native_tokens_requirement(),
Requirement::Amount => self.fulfill_amount_requirement(),
Requirement::Mana(allotments) => self.fulfill_mana_requirement(allotments),
}
}

Expand Down
166 changes: 127 additions & 39 deletions sdk/src/types/block/mana/allotment.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// Copyright 2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use packable::Packable;
use alloc::{boxed::Box, collections::BTreeSet, vec::Vec};
use core::ops::RangeInclusive;

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

use crate::types::block::{
output::AccountId,
Expand All @@ -13,16 +18,21 @@ use crate::types::block::{
/// in the form of Block Issuance Credits to the account.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Packable)]
#[packable(unpack_error = Error)]
#[packable(unpack_visitor = ProtocolParameters)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
pub struct ManaAllotment {
pub(crate) account_id: AccountId,
#[packable(verify_with = verify_mana)]
#[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::string"))]
pub(crate) mana: u64,
}

impl ManaAllotment {
pub fn new(account_id: AccountId, mana: u64, protocol_params: &ProtocolParameters) -> Result<Self, Error> {
verify_mana::<true>(&mana, protocol_params)?;
pub fn new(account_id: AccountId, mana: u64) -> Result<Self, Error> {
verify_mana::<true>(&mana)?;

Ok(Self { account_id, mana })
}
Expand Down Expand Up @@ -54,53 +64,131 @@ impl WorkScore for ManaAllotment {
}
}

fn verify_mana<const VERIFY: bool>(mana: &u64, params: &ProtocolParameters) -> Result<(), Error> {
if VERIFY && *mana > params.mana_parameters().max_mana() {
fn verify_mana<const VERIFY: bool>(mana: &u64) -> Result<(), Error> {
if VERIFY && *mana == 0 {
return Err(Error::InvalidManaValue(*mana));
}

Ok(())
}

#[cfg(feature = "serde")]
pub(super) mod dto {
use serde::{Deserialize, Serialize};
pub(crate) type ManaAllotmentCount =
BoundedU16<{ *ManaAllotments::COUNT_RANGE.start() }, { *ManaAllotments::COUNT_RANGE.end() }>;

/// A list of [`ManaAllotment`]s with unique [`AccountId`]s.
#[derive(Clone, Debug, Eq, PartialEq, Deref, Packable)]
#[packable(unpack_visitor = ProtocolParameters)]
#[packable(unpack_error = Error, with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidManaAllotmentCount(p.into())))]
pub struct ManaAllotments(
#[packable(verify_with = verify_mana_allotments)] BoxedSlicePrefix<ManaAllotment, ManaAllotmentCount>,
);

impl ManaAllotments {
/// The minimum number of mana allotments of a transaction.
pub const COUNT_MIN: u16 = 0;
/// The maximum number of mana allotments of a transaction.
pub const COUNT_MAX: u16 = 128;
/// The range of valid numbers of mana allotments of a transaction.
pub const COUNT_RANGE: RangeInclusive<u16> = Self::COUNT_MIN..=Self::COUNT_MAX; // [0..128]

/// Creates a new [`ManaAllotments`] from a vec.
pub fn from_vec(allotments: Vec<ManaAllotment>) -> Result<Self, Error> {
verify_mana_allotments_unique_sorted(&allotments)?;

Ok(Self(
allotments
.into_boxed_slice()
.try_into()
.map_err(Error::InvalidManaAllotmentCount)?,
))
}

use super::*;
use crate::{types::TryFromDto, utils::serde::string};
/// Creates a new [`ManaAllotments`] from an ordered set.
pub fn from_set(allotments: BTreeSet<ManaAllotment>) -> Result<Self, Error> {
Ok(Self(
allotments
.into_iter()
.collect::<Box<[_]>>()
.try_into()
.map_err(Error::InvalidManaAllotmentCount)?,
))
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManaAllotmentDto {
pub account_id: AccountId,
#[serde(with = "string")]
pub mana: u64,
/// Gets a reference to an [`ManaAllotment`], if one exists, using an [`AccountId`].
#[inline(always)]
pub fn get(&self, account_id: &AccountId) -> Option<&ManaAllotment> {
self.0.iter().find(|a| a.account_id() == account_id)
}
}

impl From<&ManaAllotment> for ManaAllotmentDto {
fn from(value: &ManaAllotment) -> Self {
Self {
account_id: value.account_id,
mana: value.mana,
}
}
fn verify_mana_allotments<const VERIFY: bool>(
allotments: &[ManaAllotment],
protocol_params: &ProtocolParameters,
) -> Result<(), Error> {
if VERIFY {
verify_mana_allotments_unique_sorted(allotments)?;
verify_mana_allotments_sum(allotments, protocol_params)?;
}

impl TryFromDto<ManaAllotmentDto> for ManaAllotment {
type Error = Error;

fn try_from_dto_with_params_inner(
dto: ManaAllotmentDto,
params: Option<&ProtocolParameters>,
) -> Result<Self, Self::Error> {
Ok(if let Some(params) = params {
Self::new(dto.account_id, dto.mana, params)?
} else {
Self {
account_id: dto.account_id,
mana: dto.mana,
}
})
Ok(())
}

fn verify_mana_allotments_unique_sorted<'a>(
allotments: impl IntoIterator<Item = &'a ManaAllotment>,
) -> Result<(), Error> {
if !is_unique_sorted(allotments.into_iter()) {
return Err(Error::ManaAllotmentsNotUniqueSorted);
}
Ok(())
}

pub(crate) fn verify_mana_allotments_sum<'a>(
allotments: impl IntoIterator<Item = &'a ManaAllotment>,
protocol_params: &ProtocolParameters,
) -> Result<(), Error> {
let mut mana_sum: u64 = 0;
let max_mana = protocol_params.mana_parameters().max_mana();

for ManaAllotment { mana, .. } in allotments {
mana_sum = mana_sum.checked_add(*mana).ok_or(Error::InvalidManaAllotmentSum {
sum: mana_sum as u128 + *mana as u128,
max: max_mana,
})?;

if mana_sum > max_mana {
return Err(Error::InvalidManaAllotmentSum {
sum: mana_sum as u128,
max: max_mana,
});
}
}

Ok(())
}

impl TryFrom<Vec<ManaAllotment>> for ManaAllotments {
type Error = Error;

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

impl TryFrom<BTreeSet<ManaAllotment>> for ManaAllotments {
type Error = Error;

#[inline(always)]
fn try_from(allotments: BTreeSet<ManaAllotment>) -> Result<Self, Self::Error> {
Self::from_set(allotments)
}
}

impl IntoIterator for ManaAllotments {
type Item = ManaAllotment;
type IntoIter = alloc::vec::IntoIter<Self::Item>;

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

0 comments on commit 679b8fa

Please sign in to comment.