Skip to content

Commit

Permalink
Add mana semantic validation (#1700)
Browse files Browse the repository at this point in the history
* validate some mana semantics

* Cleanup

* delta->diff

* Nit

* Allow passing an index for test inputs IDs

* Fix tests

* rand nit

* Simplify tests

* Add link to issue

* Update sdk/src/types/block/semantic/mod.rs

Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com>

* Format

---------

Co-authored-by: Thibault Martinez <thibault@iota.org>
Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com>
Co-authored-by: /alex/ <alexander.schmidt@iota.org>
  • Loading branch information
4 people authored Jan 11, 2024
1 parent 5ac148f commit 019d348
Show file tree
Hide file tree
Showing 18 changed files with 2,987 additions and 2,468 deletions.
2 changes: 1 addition & 1 deletion bindings/core/src/method/secret_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ pub enum SecretManagerMethod {
/// Expected response: [`Ok`](crate::Response::Ok)
#[cfg(feature = "stronghold")]
#[cfg_attr(docsrs, doc(cfg(feature = "stronghold")))]
ClearStrongholdPassword
ClearStrongholdPassword,
}

#[cfg(test)]
Expand Down
6 changes: 3 additions & 3 deletions sdk/src/types/block/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ pub enum Error {
DuplicateOutputChain(ChainId),
InvalidField(&'static str),
NullDelegationValidatorId,
InvalidEpochDelta {
InvalidEpochDiff {
created: EpochIndex,
target: EpochIndex,
},
Expand Down Expand Up @@ -429,8 +429,8 @@ impl fmt::Display for Error {
Self::DuplicateOutputChain(chain_id) => write!(f, "duplicate output chain {chain_id}"),
Self::InvalidField(field) => write!(f, "invalid field: {field}"),
Self::NullDelegationValidatorId => write!(f, "null delegation validator ID"),
Self::InvalidEpochDelta { created, target } => {
write!(f, "invalid epoch delta: created {created}, target {target}")
Self::InvalidEpochDiff { created, target } => {
write!(f, "invalid epoch diff: created {created}, target {target}")
}
Self::TrailingCapabilityBytes => write!(f, "capability bytes have trailing zeroes"),
Self::RestrictedAddressCapability(cap) => write!(f, "restricted address capability: {cap:?}"),
Expand Down
36 changes: 21 additions & 15 deletions sdk/src/types/block/mana/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ impl ManaParameters {
(1 << self.bits_count) - 1
}

fn decay(&self, mut mana: u64, epoch_delta: u32) -> u64 {
if mana == 0 || epoch_delta == 0 || self.decay_factors().is_empty() {
fn decay(&self, mut mana: u64, epoch_diff: u32) -> u64 {
if mana == 0 || epoch_diff == 0 || self.decay_factors().is_empty() {
return mana;
}

// we keep applying the lookup table factors as long as n epochs are left
let mut remaining_epochs = epoch_delta;
let mut remaining_epochs = epoch_diff;

while remaining_epochs > 0 {
let epochs_to_decay = remaining_epochs.min(self.decay_factors().len() as u32);
Expand All @@ -80,13 +80,14 @@ impl ManaParameters {
mana
}

fn generate_mana(&self, amount: u64, slot_delta: u32) -> u64 {
if self.generation_rate() == 0 || slot_delta == 0 {
fn generate_mana(&self, amount: u64, slot_diff: u32) -> u64 {
if self.generation_rate() == 0 || slot_diff == 0 {
return 0;
}

fixed_point_multiply(
amount,
slot_delta * self.generation_rate() as u32,
slot_diff * self.generation_rate() as u32,
self.generation_rate_exponent(),
)
}
Expand Down Expand Up @@ -116,17 +117,18 @@ impl ProtocolParameters {
slot_index_created: impl Into<SlotIndex>,
slot_index_target: impl Into<SlotIndex>,
) -> Result<u64, Error> {
let (slot_index_created, slot_index_target) = (slot_index_created.into(), slot_index_target.into());
let (epoch_index_created, epoch_index_target) = (
self.epoch_index_of(slot_index_created),
self.epoch_index_of(slot_index_target),
);

if epoch_index_created > epoch_index_target {
return Err(Error::InvalidEpochDelta {
return Err(Error::InvalidEpochDiff {
created: epoch_index_created,
target: epoch_index_target,
});
}

Ok(self
.mana_parameters()
.decay(mana, epoch_index_target.0 - epoch_index_created.0))
Expand All @@ -140,12 +142,14 @@ impl ProtocolParameters {
claimed_epoch: impl Into<EpochIndex>,
) -> Result<u64, Error> {
let (reward_epoch, claimed_epoch) = (reward_epoch.into(), claimed_epoch.into());

if reward_epoch > claimed_epoch {
return Err(Error::InvalidEpochDelta {
return Err(Error::InvalidEpochDiff {
created: reward_epoch,
target: claimed_epoch,
});
}

Ok(self.mana_parameters().decay(reward, claimed_epoch.0 - reward_epoch.0))
}

Expand All @@ -162,15 +166,17 @@ impl ProtocolParameters {
self.epoch_index_of(slot_index_created),
self.epoch_index_of(slot_index_target),
);

if epoch_index_created > epoch_index_target {
return Err(Error::InvalidEpochDelta {
return Err(Error::InvalidEpochDiff {
created: epoch_index_created,
target: epoch_index_target,
});
}
if slot_index_created >= slot_index_target {
return Ok(0);
}

let mana_parameters = self.mana_parameters();

Ok(if epoch_index_created == epoch_index_target {
Expand All @@ -189,14 +195,14 @@ impl ProtocolParameters {
mana_parameters.decay_factor_epochs_sum_exponent() + mana_parameters.generation_rate_exponent()
- self.slots_per_epoch_exponent(),
);
let epoch_delta = epoch_index_target.0 - epoch_index_created.0;
let epoch_diff = epoch_index_target.0 - epoch_index_created.0;
let slots_before_next_epoch = self.first_slot_of(epoch_index_created + 1) - slot_index_created;
let slots_since_epoch_start = slot_index_target - self.first_slot_of(epoch_index_target);
let potential_mana_n = mana_parameters.decay(
mana_parameters.generate_mana(amount, slots_before_next_epoch.0),
epoch_delta,
epoch_diff,
);
let potential_mana_n_1 = mana_parameters.decay(c, epoch_delta - 1);
let potential_mana_n_1 = mana_parameters.decay(c, epoch_diff - 1);
let potential_mana_0 = c + mana_parameters.generate_mana(amount, slots_since_epoch_start.0)
- (c >> mana_parameters.decay_factors_exponent());
potential_mana_0 - potential_mana_n_1 + potential_mana_n
Expand Down Expand Up @@ -290,7 +296,7 @@ mod test {
fn mana_decay_negative_delta() {
assert_eq!(
params().mana_with_decay(100, params().first_slot_of(2), params().first_slot_of(1)),
Err(Error::InvalidEpochDelta {
Err(Error::InvalidEpochDiff {
created: 2.into(),
target: 1.into()
})
Expand Down Expand Up @@ -349,7 +355,7 @@ mod test {
fn potential_mana_negative_delta() {
assert_eq!(
params().potential_mana(100, params().first_slot_of(2), params().first_slot_of(1)),
Err(Error::InvalidEpochDelta {
Err(Error::InvalidEpochDiff {
created: 2.into(),
target: 1.into()
})
Expand Down
13 changes: 11 additions & 2 deletions sdk/src/types/block/rand/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
// Copyright 2020-2021 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use crate::types::block::{payload::signed_transaction::TransactionId, rand::bytes::rand_bytes_array};
use crate::types::block::{
payload::signed_transaction::{TransactionHash, TransactionId},
rand::{bytes::rand_bytes_array, number::rand_number},
slot::SlotIndex,
};

/// Generates a random transaction id with a given slot index.
pub fn rand_transaction_id_with_slot_index(slot_index: impl Into<SlotIndex>) -> TransactionId {
TransactionHash::new(rand_bytes_array()).into_transaction_id(slot_index.into())
}

/// Generates a random transaction id.
pub fn rand_transaction_id() -> TransactionId {
TransactionId::new(rand_bytes_array())
rand_transaction_id_with_slot_index(rand_number::<u32>())
}
61 changes: 54 additions & 7 deletions sdk/src/types/block/semantic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ pub use self::{
};
use crate::types::block::{
address::Address,
output::{AccountId, AnchorOutput, ChainId, FoundryId, NativeTokens, Output, OutputId, TokenId},
output::{
AccountId, AnchorOutput, ChainId, FoundryId, MinimumOutputAmount, NativeTokens, Output, OutputId, TokenId,
},
payload::signed_transaction::{Transaction, TransactionCapabilityFlag, TransactionSigningHash},
protocol::ProtocolParameters,
unlock::Unlock,
Expand Down Expand Up @@ -160,7 +162,37 @@ impl<'a> SemanticValidationContext<'a> {
.checked_add(amount)
.ok_or(Error::ConsumedAmountOverflow)?;

self.input_mana = self.input_mana.checked_add(mana).ok_or(Error::ConsumedManaOverflow)?;
let potential_mana = {
// Deposit amount doesn't generate mana
let min_deposit = consumed_output.minimum_amount(self.protocol_parameters.storage_score_parameters());
let generation_amount = consumed_output.amount().saturating_sub(min_deposit);

self.protocol_parameters.potential_mana(
generation_amount,
output_id.transaction_id().slot_index(),
self.transaction.creation_slot(),
)
}?;

// Add potential mana
self.input_mana = self
.input_mana
.checked_add(potential_mana)
.ok_or(Error::ConsumedManaOverflow)?;

let stored_mana = self.protocol_parameters.mana_with_decay(
mana,
output_id.transaction_id().slot_index(),
self.transaction.creation_slot(),
)?;

// Add stored mana
self.input_mana = self
.input_mana
.checked_add(stored_mana)
.ok_or(Error::ConsumedManaOverflow)?;

// TODO: Add reward mana https://github.com/iotaledger/iota-sdk/issues/1310

if let Some(consumed_native_token) = consumed_native_token {
let native_token_amount = self
Expand Down Expand Up @@ -221,8 +253,17 @@ impl<'a> SemanticValidationContext<'a> {
.checked_add(amount)
.ok_or(Error::CreatedAmountOverflow)?;

// Add stored mana
self.output_mana = self.output_mana.checked_add(mana).ok_or(Error::CreatedManaOverflow)?;

// Add allotted mana
for mana_allotment in self.transaction.allotments() {
self.output_mana = self
.output_mana
.checked_add(mana_allotment.mana())
.ok_or(Error::CreatedManaOverflow)?;
}

if let Some(created_native_token) = created_native_token {
let native_token_amount = self
.output_native_tokens
Expand Down Expand Up @@ -251,11 +292,17 @@ impl<'a> SemanticValidationContext<'a> {
return Ok(Some(TransactionFailureReason::SumInputsOutputsAmountMismatch));
}

// TODO re-enable with https://github.com/iotaledger/iota-sdk/issues/1692
// if self.input_mana > self.output_mana &&
// !self.transaction.has_capability(TransactionCapabilityFlag::BurnMana) { // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430
// return Ok(Some(TransactionFailureReason::SemanticValidationFailed));
// }
if self.input_mana != self.output_mana {
if self.input_mana > self.output_mana {
if !self.transaction.has_capability(TransactionCapabilityFlag::BurnMana) {
return Ok(Some(
TransactionFailureReason::TransactionCapabilityManaBurningNotAllowed,
));
}
} else {
return Ok(Some(TransactionFailureReason::InvalidManaAmount));
}
}

// Validation of input native tokens.
let mut native_token_ids = self.input_native_tokens.keys().collect::<HashSet<_>>();
Expand Down
Loading

0 comments on commit 019d348

Please sign in to comment.