diff --git a/sdk/src/types/block/mana/parameters.rs b/sdk/src/types/block/mana/parameters.rs index 127e0b2320..e9e8469489 100644 --- a/sdk/src/types/block/mana/parameters.rs +++ b/sdk/src/types/block/mana/parameters.rs @@ -53,6 +53,11 @@ impl ManaParameters { self.decay_factors.get(*epoch_index.into() as usize).copied() } + /// Returns the annual decay factor. + pub fn annual_decay_factor(&self) -> f64 { + self.annual_decay_factor_percentage() as f64 / 100.0 + } + /// Returns the max mana that can exist with the mana bits defined. pub fn max_mana(&self) -> u64 { (1 << self.bits_count) - 1 @@ -155,7 +160,7 @@ impl ProtocolParameters { /// Calculates the potential mana that is generated by holding `amount` tokens from `slot_index_created` to /// `slot_index_target` and applies the decay to the result - pub fn potential_mana( + pub fn generate_mana_with_decay( &self, amount: u64, slot_index_created: impl Into, @@ -182,30 +187,29 @@ impl ProtocolParameters { Ok(if epoch_index_created == epoch_index_target { mana_parameters.generate_mana(amount, slot_index_target.0 - slot_index_created.0) } else if epoch_index_target == epoch_index_created + 1 { - 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 mana_decayed = - mana_parameters.decay(mana_parameters.generate_mana(amount, slots_before_next_epoch.0), 1); - let mana_generated = mana_parameters.generate_mana(amount, slots_since_epoch_start.0); - mana_decayed + mana_generated + let mana_generated_first_epoch = + mana_parameters.generate_mana(amount, self.slots_before_next_epoch(slot_index_created)); + let mana_decayed_first_epoch = mana_parameters.decay(mana_generated_first_epoch, 1); + let mana_generated_second_epoch = + mana_parameters.generate_mana(amount, self.slots_since_epoch_start(slot_index_target)); + mana_decayed_first_epoch + mana_generated_second_epoch } else { + let mana_generated_first_epoch = + mana_parameters.generate_mana(amount, self.slots_before_next_epoch(slot_index_created)); + let mana_decayed_first_epoch = + mana_parameters.decay(mana_generated_first_epoch, epoch_index_target.0 - epoch_index_created.0); let c = fixed_point_multiply( amount, mana_parameters.decay_factor_epochs_sum() * mana_parameters.generation_rate() as u32, mana_parameters.decay_factor_epochs_sum_exponent() + mana_parameters.generation_rate_exponent() - self.slots_per_epoch_exponent(), ); - 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_diff, - ); - 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 + let mana_decayed_intermediate_epochs = + c - mana_parameters.decay(c, epoch_index_target.0 - epoch_index_created.0 - 1); + let mana_generated_last_epoch = + mana_parameters.generate_mana(amount, self.slots_since_epoch_start(slot_index_target)); + mana_decayed_intermediate_epochs + mana_generated_last_epoch + mana_decayed_first_epoch + - (c >> mana_parameters.decay_factors_exponent()) }) } } @@ -223,32 +227,33 @@ mod test { // Tests from https://github.com/iotaledger/iota.go/blob/develop/mana_decay_provider_test.go - const BETA_PER_YEAR: f64 = 1. / 3.; - fn params() -> &'static ProtocolParameters { use once_cell::sync::Lazy; static PARAMS: Lazy = Lazy::new(|| { let mut params = ProtocolParameters { + genesis_slot: 0, + genesis_unix_timestamp: time::OffsetDateTime::now_utc().unix_timestamp() as _, slots_per_epoch_exponent: 13, slot_duration_in_seconds: 10, + token_supply: 1813620509061365, mana_parameters: ManaParameters { bits_count: 63, generation_rate: 1, - generation_rate_exponent: 27, + generation_rate_exponent: 17, decay_factors_exponent: 32, - decay_factor_epochs_sum_exponent: 20, + decay_factor_epochs_sum_exponent: 21, + annual_decay_factor_percentage: 70, ..Default::default() }, ..Default::default() }; params.mana_parameters.decay_factors = { - let epochs_per_year = ((365_u64 * 24 * 60 * 60) as f64 / params.slot_duration_in_seconds() as f64) - / params.slots_per_epoch() as f64; - let beta_per_epoch_index = BETA_PER_YEAR / epochs_per_year; - (1..=epochs_per_year.floor() as usize) + let epochs_in_table = (u16::MAX as usize).min(params.epochs_per_year().floor() as usize); + let decay_per_epoch = params.decay_per_epoch(); + (1..=epochs_in_table) .map(|epoch| { - ((-beta_per_epoch_index * epoch as f64).exp() - * (params.mana_parameters().decay_factors_exponent() as f64).exp2()) + (decay_per_epoch.powi(epoch as _) + * 2f64.powi(params.mana_parameters().decay_factors_exponent() as _)) .floor() as u32 }) .collect::>() @@ -256,11 +261,11 @@ mod test { .try_into() .unwrap(); params.mana_parameters.decay_factor_epochs_sum = { - let delta = params.slots_per_epoch() as f64 * params.slot_duration_in_seconds() as f64 - / (365_u64 * 24 * 60 * 60) as f64; - (((-BETA_PER_YEAR * delta).exp() / (1. - (-BETA_PER_YEAR * delta).exp())) - * (params.mana_parameters().decay_factor_epochs_sum_exponent() as f64).exp2()) - .floor() as u32 + let delta = params.epochs_per_year().recip(); + let annual_decay_factor = params.mana_parameters().annual_decay_factor(); + (annual_decay_factor.powf(delta) / (1.0 - annual_decay_factor.powf(delta)) + * (2f64.powi(params.mana_parameters().decay_factor_epochs_sum_exponent() as _))) + .floor() as _ }; params }); @@ -276,145 +281,271 @@ mod test { assert_eq!(mana_parameters.decay(100, 100), 100); } - #[test] - fn mana_decay_no_delta() { - assert_eq!( - params().mana_with_decay(100, params().first_slot_of(1), params().first_slot_of(1)), - Ok(100) - ); - } - - #[test] - fn mana_decay_no_mana() { - assert_eq!( - params().mana_with_decay(0, params().first_slot_of(1), params().first_slot_of(400)), - Ok(0) - ); - } - - #[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::InvalidEpochDiff { - created: 2.into(), - target: 1.into() - }) - ); - } - - #[test] - fn mana_decay_lookup_len_delta() { - assert_eq!( - params().mana_with_decay( - u64::MAX, - params().first_slot_of(1), - params().first_slot_of(params().mana_parameters().decay_factors().len() as u32 + 1) - ), - Ok(13228672242897911807) - ); - } - - #[test] - fn mana_decay_lookup_len_delta_multiple() { - assert_eq!( - params().mana_with_decay( - u64::MAX, - params().first_slot_of(1), - params().first_slot_of(3 * params().mana_parameters().decay_factors().len() as u32 + 1) - ), - Ok(6803138682699798504) - ); - } - - #[test] - fn mana_decay_max_mana() { - assert_eq!( - params().mana_with_decay(u64::MAX, params().first_slot_of(1), params().first_slot_of(401)), - Ok(13046663022640287317) - ); + struct ManaDecayTest { + name: &'static str, + stored_mana: u64, + created_slot: SlotIndex, + target_slot: SlotIndex, + err: Option, } #[test] - fn potential_mana_no_delta() { - assert_eq!( - params().potential_mana(100, params().first_slot_of(1), params().first_slot_of(1)), - Ok(0) - ); + fn mana_decay() { + let tests = [ + ManaDecayTest { + name: "check if mana decay works for 0 mana values", + stored_mana: 0, + created_slot: params().first_slot_of(1), + target_slot: params().first_slot_of(400), + err: None, + }, + ManaDecayTest { + name: "check if mana decay works for 0 slot index diffs", + stored_mana: u64::MAX, + created_slot: params().first_slot_of(1), + target_slot: params().first_slot_of(1), + err: None, + }, + ManaDecayTest { + name: "check for error if target index is lower than created index", + stored_mana: 0, + created_slot: params().first_slot_of(2), + target_slot: params().first_slot_of(1), + err: Some(Error::InvalidEpochDiff { + created: 2.into(), + target: 1.into(), + }), + }, + ManaDecayTest { + name: "check if mana decay works for exactly the amount of epochs in the lookup table", + stored_mana: u64::MAX, + created_slot: params().first_slot_of(1), + target_slot: params().first_slot_of(params().mana_parameters().decay_factors().len() as u32 + 1), + err: None, + }, + ManaDecayTest { + name: "check if mana decay works for multiples of the available epochs in the lookup table", + stored_mana: u64::MAX, + created_slot: params().first_slot_of(1), + target_slot: params().first_slot_of(3 * params().mana_parameters().decay_factors().len() as u32 + 1), + err: None, + }, + ManaDecayTest { + name: "even with the highest possible uint64 number, the calculation should not overflow", + stored_mana: u64::MAX, + created_slot: params().first_slot_of(1), + target_slot: params().first_slot_of(401), + err: None, + }, + ]; + + for ManaDecayTest { + name, + stored_mana, + created_slot, + target_slot, + err, + } in tests + { + let result = params().mana_with_decay(stored_mana, created_slot, target_slot); + if let Some(err) = err { + assert_eq!(result, Err(err), "{name}"); + } else { + let result = result.map_err(|e| format!("{name}: {e}")).unwrap(); + let upper_bound = upper_bound_mana_decay(stored_mana, created_slot, target_slot); + let lower_bound = lower_bound_mana_decay(stored_mana, created_slot, target_slot); + + assert!( + result as f64 <= upper_bound, + "{name}: result {result} above upper bound {upper_bound}", + ); + assert!( + result as f64 >= lower_bound, + "{name}: result {result} below lower bound {upper_bound}", + ); + } + } } - #[test] - fn potential_mana_no_mana() { - assert_eq!( - params().potential_mana(0, params().first_slot_of(1), params().first_slot_of(400)), - Ok(0) - ); + struct ManaGenerationTest { + name: &'static str, + amount: u64, + created_slot: SlotIndex, + target_slot: SlotIndex, + err: Option, } #[test] - fn potential_mana_negative_delta() { - assert_eq!( - params().potential_mana(100, params().first_slot_of(2), params().first_slot_of(1)), - Err(Error::InvalidEpochDiff { - created: 2.into(), - target: 1.into() - }) - ); + fn mana_generation() { + let tests = [ + ManaGenerationTest { + name: "check if mana generation works for 0 mana values", + amount: 0, + created_slot: params().first_slot_of(1), + target_slot: params().first_slot_of(400), + err: None, + }, + ManaGenerationTest { + name: "check if mana generation works for 0 slot index diffs", + amount: i64::MAX as _, + created_slot: params().first_slot_of(1), + target_slot: params().first_slot_of(1), + err: None, + }, + ManaGenerationTest { + name: "check for error if target index is lower than created index", + amount: 0, + created_slot: params().first_slot_of(2), + target_slot: params().first_slot_of(1), + err: Some(Error::InvalidEpochDiff { + created: 2.into(), + target: 1.into(), + }), + }, + ManaGenerationTest { + name: "check if mana generation works for exactly the amount of epochs in the lookup table", + amount: params().token_supply(), + created_slot: params().first_slot_of(1), + target_slot: params().first_slot_of(params().mana_parameters().decay_factors().len() as u32 + 1), + err: None, + }, + ManaGenerationTest { + name: "check if mana generation works for multiples of the available epochs in the lookup table", + amount: params().token_supply(), + created_slot: params().first_slot_of(1), + target_slot: params().first_slot_of(3 * params().mana_parameters().decay_factors().len() as u32 + 1), + err: None, + }, + ManaGenerationTest { + name: "check if mana generation works for 0 epoch diffs", + amount: params().token_supply(), + created_slot: params().first_slot_of(1), + target_slot: params().last_slot_of(1), + err: None, + }, + ManaGenerationTest { + name: "check if mana generation works for 1 epoch diffs", + amount: params().token_supply(), + created_slot: params().first_slot_of(1), + target_slot: params().last_slot_of(2), + err: None, + }, + ManaGenerationTest { + name: "check if mana generation works for >=2 epoch diffs", + amount: params().token_supply(), + created_slot: params().first_slot_of(1), + target_slot: params().last_slot_of(3), + err: None, + }, + ]; + + for ManaGenerationTest { + name, + amount, + created_slot, + target_slot, + err, + } in tests + { + let result = params().generate_mana_with_decay(amount, created_slot, target_slot); + if let Some(err) = err { + assert_eq!(result, Err(err), "{name}"); + } else { + let result = result.map_err(|e| format!("{name}: {e}")).unwrap(); + let upper_bound = upper_bound_mana_generation(amount, created_slot, target_slot); + let lower_bound = lower_bound_mana_generation(amount, created_slot, target_slot); + + assert!( + result as f64 <= upper_bound, + "{name}: result {result} above upper bound {upper_bound}", + ); + assert!( + result as f64 >= lower_bound, + "{name}: result {result} below lower bound {upper_bound}", + ); + + if result != 0 { + let float_res = mana_generation_with_decay_float(amount as _, created_slot, target_slot); + let epsilon = 0.001; + let allowed_delta = float_res.abs().min(result as f64) * epsilon; + let dt = float_res - result as f64; + assert!( + dt >= -allowed_delta && dt <= allowed_delta, + "{name}: fixed point result varies too greatly from float result" + ); + } + } + } } - #[test] - fn potential_mana_lookup_len_delta() { - assert_eq!( - params().potential_mana( - i64::MAX as u64, - params().first_slot_of(1), - params().first_slot_of(params().mana_parameters().decay_factors().len() as u32 + 1) - ), - Ok(183827295065703076) + fn mana_decay_float(mana: f64, creation_slot: SlotIndex, target_slot: SlotIndex) -> f64 { + let (creation_epoch, target_epoch) = ( + params().epoch_index_of(creation_slot), + params().epoch_index_of(target_slot), ); + mana * params() + .decay_per_epoch() + .powi((target_epoch.0 - creation_epoch.0) as _) } - #[test] - fn potential_mana_lookup_len_delta_multiple() { - assert_eq!( - params().potential_mana( - i64::MAX as u64, - params().first_slot_of(1), - params().first_slot_of(3 * params().mana_parameters().decay_factors().len() as u32 + 1) - ), - Ok(410192223115924783) + fn mana_generation_with_decay_float(amount: f64, creation_slot: SlotIndex, target_slot: SlotIndex) -> f64 { + let (creation_epoch, target_epoch) = ( + params().epoch_index_of(creation_slot), + params().epoch_index_of(target_slot), ); + let decay_per_epoch = params().decay_per_epoch(); + let generation_rate = params().mana_parameters().generation_rate() as f64 + * 2f64.powi(-(params().mana_parameters().generation_rate_exponent() as i32)); + + if creation_epoch == target_epoch { + (target_slot.0 - creation_slot.0) as f64 * amount * generation_rate + } else if target_epoch == creation_epoch + 1 { + let slots_before_next_epoch = params().slots_before_next_epoch(creation_slot); + let slots_since_epoch_start = params().slots_since_epoch_start(target_slot); + let mana_decayed = slots_before_next_epoch as f64 * amount * generation_rate * decay_per_epoch; + let mana_generated = slots_since_epoch_start as f64 * amount * generation_rate; + mana_decayed + mana_generated + } else { + let slots_before_next_epoch = params().slots_before_next_epoch(creation_slot); + let slots_since_epoch_start = params().slots_since_epoch_start(target_slot); + let c = decay_per_epoch * (1.0 - decay_per_epoch.powi((target_epoch.0 - creation_epoch.0) as i32 - 1)) + / (1.0 - decay_per_epoch); + let potential_mana_n = slots_before_next_epoch as f64 + * amount + * generation_rate + * decay_per_epoch.powi((target_epoch.0 - creation_epoch.0) as i32); + let potential_mana_n_1 = c * amount * generation_rate * params().slots_per_epoch() as f64; + let potential_mana_0 = slots_since_epoch_start as f64 * amount * generation_rate; + potential_mana_n + potential_mana_n_1 + potential_mana_0 + } } - #[test] - fn potential_mana_same_epoch() { - assert_eq!( - params().potential_mana(i64::MAX as u64, params().first_slot_of(1), params().last_slot_of(1)), - Ok(562881233944575) - ); + fn upper_bound_mana_decay(mana: u64, creation_slot: SlotIndex, target_slot: SlotIndex) -> f64 { + mana_decay_float(mana as _, creation_slot, target_slot) } - #[test] - fn potential_mana_one_epoch() { - assert_eq!( - params().potential_mana(i64::MAX as u64, params().first_slot_of(1), params().last_slot_of(2)), - Ok(1125343946211326) - ); + fn lower_bound_mana_decay(mana: u64, creation_slot: SlotIndex, target_slot: SlotIndex) -> f64 { + mana_decay_float(mana as _, creation_slot, target_slot) + - (mana as f64 * 2f64.powi(-(params().mana_parameters().decay_factors_exponent() as i32)) + 1.0) } - #[test] - fn potential_mana_several_epochs() { - assert_eq!( - params().potential_mana(i64::MAX as u64, params().first_slot_of(1), params().last_slot_of(3)), - Ok(1687319824887185) - ); + fn upper_bound_mana_generation(amount: u64, creation_slot: SlotIndex, target_slot: SlotIndex) -> f64 { + mana_generation_with_decay_float(amount as _, creation_slot, target_slot) + 2.0 + - 2f64.powi(-(params().mana_parameters().decay_factors_exponent() as i32) - 1) } - #[test] - fn potential_mana_max_mana() { - assert_eq!( - params().potential_mana(i64::MAX as u64, params().first_slot_of(1), params().first_slot_of(401)), - Ok(190239292388858706) - ); + fn lower_bound_mana_generation(amount: u64, creation_slot: SlotIndex, target_slot: SlotIndex) -> f64 { + let decay_per_epoch = params().decay_per_epoch(); + let c = decay_per_epoch / (1.0 - decay_per_epoch); + + mana_generation_with_decay_float(amount as _, creation_slot, target_slot) + - (4.0 + + amount as f64 + * params().mana_parameters().generation_rate() as f64 + * 2f64.powi( + params().slots_per_epoch_exponent() as i32 + - params().mana_parameters().generation_rate_exponent() as i32, + ) + * (1.0 + c * 2f64.powi(-(params().mana_parameters().decay_factors_exponent() as i32)))) } } diff --git a/sdk/src/types/block/protocol/mod.rs b/sdk/src/types/block/protocol/mod.rs index adc9b1e9c7..d2ee4ca4b5 100644 --- a/sdk/src/types/block/protocol/mod.rs +++ b/sdk/src/types/block/protocol/mod.rs @@ -202,11 +202,51 @@ impl ProtocolParameters { epoch_index.into().last_slot_index(self.slots_per_epoch_exponent()) } + /// Calculates the number of slots before the next epoch. + pub fn slots_before_next_epoch(&self, slot_index: impl Into) -> u32 { + let slot_index = slot_index.into(); + + if slot_index.0 < self.genesis_slot() { + 0 + } else { + self.genesis_slot() + self.first_slot_of(self.epoch_index_of(slot_index) + 1).0 - slot_index.0 + } + } + + /// Calculates the number of slots since the start of the epoch. + pub fn slots_since_epoch_start(&self, slot_index: impl Into) -> u32 { + let slot_index = slot_index.into(); + + if slot_index.0 < self.genesis_slot() { + 0 + } else { + self.genesis_slot() + slot_index.0 - self.first_slot_of(self.epoch_index_of(slot_index)).0 + } + } + /// Gets the [`EpochIndex`] of a given [`SlotIndex`]. pub fn epoch_index_of(&self, slot_index: impl Into) -> EpochIndex { EpochIndex::from_slot_index(slot_index.into(), self.slots_per_epoch_exponent()) } + /// Calculates the duration of an epoch in seconds. + pub fn epoch_duration_in_seconds(&self) -> u64 { + self.slot_duration_in_seconds() as u64 * self.slots_per_epoch() as u64 + } + + /// Calculates the number of epochs per year. + pub fn epochs_per_year(&self) -> f64 { + (365_u64 * 24 * 60 * 60) as f64 / self.epoch_duration_in_seconds() as f64 + } + + /// Calculates the decay per epoch based on the annual decay factor and number of epochs in a year. + #[cfg(feature = "std")] + pub fn decay_per_epoch(&self) -> f64 { + self.mana_parameters() + .annual_decay_factor() + .powf(self.epochs_per_year().recip()) + } + /// Returns the hash of the [`ProtocolParameters`]. pub fn hash(&self) -> ProtocolParametersHash { ProtocolParametersHash::new(Blake2b256::digest(self.pack_to_vec()).into()) diff --git a/sdk/src/types/block/semantic/mod.rs b/sdk/src/types/block/semantic/mod.rs index e9bf091ef3..0c7a7e8c11 100644 --- a/sdk/src/types/block/semantic/mod.rs +++ b/sdk/src/types/block/semantic/mod.rs @@ -167,7 +167,7 @@ impl<'a> SemanticValidationContext<'a> { 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( + self.protocol_parameters.generate_mana_with_decay( generation_amount, output_id.transaction_id().slot_index(), self.transaction.creation_slot(),