From 5150a97f8e8b597c64198dcdb2d55cfbde58077c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Mar 2025 18:23:37 +0700 Subject: [PATCH 1/4] more work --- .../src/block/finalized_epoch_info/getters.rs | 3 +- .../block/finalized_epoch_info/v0/getters.rs | 5 +- .../src/block/finalized_epoch_info/v0/mod.rs | 3 +- .../accessors/mod.rs | 3 - .../token_configuration_localization/mod.rs | 1 - .../token_distribution_key.rs | 10 +- .../distribution_function/encode.rs | 12 +- .../distribution_function/evaluate.rs | 58 +- .../evaluate_interval.rs | 216 ++-- .../distribution_function/mod.rs | 13 +- .../distribution_function/reward_ratio.rs | 5 + .../distribution_function/validation.rs | 70 +- .../methods/mod.rs | 9 +- .../methods/v0/mod.rs | 5 +- .../reward_distribution_moment/mod.rs | 61 + .../reward_distribution_type/accessors.rs | 1 - .../evaluate_interval.rs | 47 +- .../reward_distribution_type/mod.rs | 102 +- .../v0/methods.rs | 35 +- .../class_methods/try_from_schema/v0/mod.rs | 1 - .../document_type/property/mod.rs | 2 +- .../src/data_contract/v0/methods/schema.rs | 1 - .../src/data_contract/v0/serialization/mod.rs | 1 - .../src/data_contract/v1/methods/schema.rs | 1 - .../src/data_contract/v1/serialization/mod.rs | 1 - packages/rs-dpp/src/errors/consensus/codes.rs | 42 +- .../src/errors/consensus/state/state_error.rs | 8 +- .../invalid_token_claim_no_current_rewards.rs | 77 ++ .../invalid_token_claim_wrong_claimant.rs | 58 + .../src/errors/consensus/state/token/mod.rs | 4 + packages/rs-dpp/src/errors/protocol_error.rs | 3 + .../mod.rs | 3 +- .../v0/mod.rs | 5 +- .../batch/tests/token/distribution/mod.rs | 1 + .../tests/token/distribution/perpetual.rs | 99 -- .../distribution/perpetual/block_based.rs | 500 ++++++++ .../tests/token/distribution/perpetual/mod.rs | 4 + .../distribution/perpetual/time_based.rs | 1074 +++++++++++++++++ .../token/distribution/pre_programmed.rs | 867 +++++++++++++ .../batch/tests/token/mod.rs | 40 + .../state_transition/state_transitions/mod.rs | 5 +- .../contract/insert/insert_contract/v1/mod.rs | 2 - .../src/drive/initialization/v1/mod.rs | 7 +- .../add_perpetual_distribution/mod.rs | 5 - .../add_perpetual_distribution/v0/mod.rs | 54 +- .../add_pre_programmed_distribution/v0/mod.rs | 16 +- .../drive/tokens/distribution/fetch/mod.rs | 1 + .../mod.rs | 91 ++ .../v0/mod.rs | 71 ++ .../mod.rs | 59 +- .../v0/mod.rs | 108 +- .../mod.rs | 6 +- .../v0/mod.rs | 59 +- .../src/drive/tokens/distribution/mod.rs | 4 +- .../for_token_perpetual_distribution/mod.rs | 1 - .../v0/mod.rs | 12 - .../mod.rs | 4 +- .../v0/mod.rs | 32 +- packages/rs-drive/src/drive/tokens/paths.rs | 62 +- .../batch/token/token_claim_transition.rs | 24 +- .../token_claim_transition_action/v0/mod.rs | 2 +- .../v0/transformer.rs | 266 ++-- .../src/util/batch/drive_op_batch/token.rs | 39 +- .../src/util/batch/grovedb_op_batch/mod.rs | 3 - .../drive_token_method_versions/mod.rs | 1 + .../drive_token_method_versions/v1.rs | 1 + .../src/version/mocks/v2_test.rs | 1 + .../src/version/system_limits/mod.rs | 5 + .../src/version/system_limits/v1.rs | 1 + 69 files changed, 3645 insertions(+), 748 deletions(-) create mode 100644 packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/reward_ratio.rs create mode 100644 packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_no_current_rewards.rs create mode 100644 packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_wrong_claimant.rs delete mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/pre_programmed.rs create mode 100644 packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/v0/mod.rs diff --git a/packages/rs-dpp/src/block/finalized_epoch_info/getters.rs b/packages/rs-dpp/src/block/finalized_epoch_info/getters.rs index f48d3298b4c..e1913d0c6db 100644 --- a/packages/rs-dpp/src/block/finalized_epoch_info/getters.rs +++ b/packages/rs-dpp/src/block/finalized_epoch_info/getters.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use crate::block::finalized_epoch_info::v0::getters::FinalizedEpochInfoGettersV0; use crate::block::finalized_epoch_info::FinalizedEpochInfo; use crate::fee::Credits; @@ -59,7 +60,7 @@ impl FinalizedEpochInfoGettersV0 for FinalizedEpochInfo { } } - fn block_proposers(&self) -> &Vec<(Identifier, u64)> { + fn block_proposers(&self) -> &BTreeMap { match self { FinalizedEpochInfo::V0(v0) => v0.block_proposers(), } diff --git a/packages/rs-dpp/src/block/finalized_epoch_info/v0/getters.rs b/packages/rs-dpp/src/block/finalized_epoch_info/v0/getters.rs index 62a575b1187..b5791a43f06 100644 --- a/packages/rs-dpp/src/block/finalized_epoch_info/v0/getters.rs +++ b/packages/rs-dpp/src/block/finalized_epoch_info/v0/getters.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use crate::block::finalized_epoch_info::v0::FinalizedEpochInfoV0; use crate::fee::Credits; use crate::prelude::{BlockHeight, BlockHeightInterval, CoreBlockHeight, TimestampMillis}; @@ -33,7 +34,7 @@ pub trait FinalizedEpochInfoGettersV0 { fn core_block_rewards(&self) -> Credits; /// Returns a reference to the block proposers map. - fn block_proposers(&self) -> &Vec<(Identifier, u64)>; + fn block_proposers(&self) -> &BTreeMap; /// Returns the fee multiplier (permille). fn fee_multiplier_permille(&self) -> u64; @@ -79,7 +80,7 @@ impl FinalizedEpochInfoGettersV0 for FinalizedEpochInfoV0 { self.core_block_rewards } - fn block_proposers(&self) -> &Vec<(Identifier, u64)> { + fn block_proposers(&self) -> &BTreeMap { &self.block_proposers } diff --git a/packages/rs-dpp/src/block/finalized_epoch_info/v0/mod.rs b/packages/rs-dpp/src/block/finalized_epoch_info/v0/mod.rs index 71c8cd99a9e..d9d39ef830e 100644 --- a/packages/rs-dpp/src/block/finalized_epoch_info/v0/mod.rs +++ b/packages/rs-dpp/src/block/finalized_epoch_info/v0/mod.rs @@ -1,5 +1,6 @@ pub mod getters; +use std::collections::BTreeMap; use crate::fee::Credits; use crate::prelude::{BlockHeight, BlockHeightInterval, CoreBlockHeight, TimestampMillis}; use bincode::{Decode, Encode}; @@ -28,7 +29,7 @@ pub struct FinalizedEpochInfoV0 { /// Total rewards given from core subsidy pub core_block_rewards: Credits, /// Block proposers - pub block_proposers: Vec<(Identifier, u64)>, + pub block_proposers: BTreeMap, /// Fee multiplier that you would divide by 1000 to get float value pub fee_multiplier_permille: u64, /// Protocol version diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_convention/accessors/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_convention/accessors/mod.rs index acfbe376d1d..c045ba50fd0 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_convention/accessors/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_convention/accessors/mod.rs @@ -1,8 +1,5 @@ pub mod v0; -use crate::data_contract::associated_token::token_configuration::accessors::v0::{ - TokenConfigurationV0Getters, TokenConfigurationV0Setters, -}; use crate::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters; use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; use crate::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization; diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_localization/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_localization/mod.rs index 6d078d6d2eb..424d64b0115 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_localization/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_localization/mod.rs @@ -1,4 +1,3 @@ -use crate::data_contract::associated_token::token_configuration_localization::accessors::v0::TokenConfigurationLocalizationV0Getters; use crate::data_contract::associated_token::token_configuration_localization::v0::TokenConfigurationLocalizationV0; use bincode::Encode; use derive_more::From; diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs b/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs index 8953531cf6a..14f02965bb1 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs @@ -48,12 +48,12 @@ pub enum TokenDistributionInfo { /// Contains the scheduled timestamp and the recipient’s identifier. PreProgrammed(TimestampMillis, Identifier), - /// A perpetual token distribution with previous and next moments. + /// A perpetual token distribution with moment for distribution. + /// The moment is the beginning of the perpetual distribution cycle /// Includes the last and next distribution times and the resolved recipient. Perpetual( RewardDistributionMoment, - RewardDistributionMoment, - TokenDistributionResolvedRecipient, + TokenDistributionResolvedRecipient ), } @@ -63,7 +63,7 @@ impl From for TokenDistributionTypeWithResolvedRecipient TokenDistributionInfo::PreProgrammed(_, recipient) => { TokenDistributionTypeWithResolvedRecipient::PreProgrammed(recipient) } - TokenDistributionInfo::Perpetual(_, _, recipient) => { + TokenDistributionInfo::Perpetual(_, recipient) => { TokenDistributionTypeWithResolvedRecipient::Perpetual(recipient) } } @@ -76,7 +76,7 @@ impl From<&TokenDistributionInfo> for TokenDistributionTypeWithResolvedRecipient TokenDistributionInfo::PreProgrammed(_, recipient) => { TokenDistributionTypeWithResolvedRecipient::PreProgrammed(*recipient) } - TokenDistributionInfo::Perpetual(_, _, recipient) => { + TokenDistributionInfo::Perpetual(_, recipient) => { TokenDistributionTypeWithResolvedRecipient::Perpetual(recipient.clone()) } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/encode.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/encode.rs index 2d709bc5ed0..6ccd23b14db 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/encode.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/encode.rs @@ -41,8 +41,8 @@ impl Encode for DistributionFunction { DistributionFunction::Linear { a, d, - s, - b, + start_moment: s, + starting_amount: b, min_value, max_value, } => { @@ -192,8 +192,8 @@ impl Decode for DistributionFunction { Ok(Self::Linear { a, d, - s, - b, + start_moment: s, + starting_amount: b, min_value, max_value, }) @@ -338,8 +338,8 @@ impl<'de> BorrowDecode<'de> for DistributionFunction { Ok(Self::Linear { a, d, - s, - b, + start_moment: s, + starting_amount: b, min_value, max_value, }) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs index 4ad0c6107cc..cdf0e0270cf 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs @@ -1,7 +1,6 @@ use crate::balances::credits::TokenAmount; use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; use crate::ProtocolError; -// adjust the import path as needed impl DistributionFunction { /// Evaluates the distribution function at the given period `x`. @@ -94,8 +93,8 @@ impl DistributionFunction { DistributionFunction::Linear { a, d, - s, - b, + start_moment: s, + starting_amount, min_value, max_value, } => { @@ -107,14 +106,39 @@ impl DistributionFunction { // Check that the value at x = 0 is within bounds. let s_val = s.unwrap_or(0); - let diff = x.saturating_sub(s_val) as i128; - let value = (((*a as i128) * diff / (*d as i128)) as i64) - .checked_add(*b as i64) - .ok_or(ProtocolError::Overflow( - "Linear function evaluation overflow or negative", - ))?; - - let value = if value < 0 { 0 } else { value as u64 }; + let diff = x.saturating_sub(s_val); + let value = if *d == 1 { + // very common case + match a.checked_mul(diff as i64) { + None => { + if *a < 0 { + 0 + } else { + if let Some(max_value) = max_value { + *max_value + } else { + return Err(ProtocolError::Overflow( + "Linear function evaluation overflow on multiplication", + )) + } + } + } + Some(mul) => { + let value = mul.checked_add(*starting_amount as i64).ok_or(ProtocolError::Overflow( + "Linear function evaluation overflow or negative", + ))?; + if value < 0 { 0 } else { value as u64 } + } + } + } else { + let value = (((*a as i128) * (diff as i128) / (*d as i128)) as i64) + .checked_add(*starting_amount as i64) + .ok_or(ProtocolError::Overflow( + "Linear function evaluation overflow or negative", + ))?; + if value < 0 { 0 } else { value as u64 } + }; + if let Some(min_value) = min_value { if value < *min_value { return Ok(*min_value); @@ -570,8 +594,8 @@ mod tests { let distribution = DistributionFunction::Linear { a: 10, d: 2, - s: Some(0), - b: 50, + start_moment: Some(0), + starting_amount: 50, min_value: None, max_value: None, }; @@ -587,8 +611,8 @@ mod tests { let distribution = DistributionFunction::Linear { a: -5, d: 1, - s: Some(0), - b: 100, + start_moment: Some(0), + starting_amount: 100, min_value: Some(10), max_value: None, }; @@ -603,8 +627,8 @@ mod tests { let distribution = DistributionFunction::Linear { a: 10, d: 0, // Invalid denominator - s: Some(0), - b: 50, + start_moment: Some(0), + starting_amount: 50, min_value: None, max_value: None, }; diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs index ce0d595ab0c..3f838966167 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs @@ -1,186 +1,106 @@ use crate::balances::credits::TokenAmount; +use crate::block::epoch::EpochIndex; use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::reward_ratio::RewardRatio; use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; use crate::ProtocolError; impl DistributionFunction { - /// Evaluates the total token emission over a specified interval. + /// Evaluates the total amount of tokens emitted over a specified interval. /// - /// This function calculates the sum of token emissions at discrete points between - /// `start_not_included` (exclusive) and `end_included` (inclusive), using `step` as the - /// interval between evaluations. Each evaluation is performed by calling `self.evaluate(x)` - /// at the appropriate step values. + /// This function calculates the cumulative emission of tokens by evaluating the distribution + /// function at discrete intervals between two specified moments. The interval calculation begins + /// after the `start_not_included` moment and includes the `end_included` moment, stepping forward by + /// the specified `step` interval. /// /// # Parameters /// - /// - `start_not_included` (`u64`): - /// The block height after which emissions are considered (exclusive). - /// - `step` (`u64`): - /// The interval in blocks at which emissions occur (e.g., every 5 blocks). - /// **Must be greater than zero** to avoid division-by-zero errors. - /// - `end_included` (`u64`): - /// The final block height at which emissions should be considered (inclusive). - /// - /// # Behavior - /// - /// Given that emissions occur at regular block intervals, this function iterates through - /// block heights that are spaced by `step`, beginning at `start_not_included + step` and - /// stopping at or before `end_included`. - /// The token emissions at each step are retrieved using `self.evaluate(x)`, and their sum - /// is returned. - /// - /// # Returns - /// - /// - `Ok(TokenAmount)`: The total sum of emissions over the interval. - /// - `Err(ProtocolError)`: If an evaluation results in an error, such as an overflow or - /// invalid operation. - /// - /// # Errors - /// - /// - `ProtocolError::DivideByZero`: If `step` is zero. - /// - `ProtocolError::Overflow`: If the accumulated sum exceeds the maximum allowable value. - /// - Any error that `self.evaluate(x)` might return. - /// - pub fn evaluate_interval( - &self, - start_not_included: u64, - step: u64, - end_included: u64, - ) -> Result { - if step == 0 { - return Err(ProtocolError::DivideByZero( - "evaluate_interval: step cannot be zero".into(), - )); - } - if end_included <= start_not_included { - return Ok(0); - } - - let mut total: u64 = 0; - // Begin at the first period after start_not_included by adding 'step'. - let mut x = start_not_included + step; - while x <= end_included { - // Call evaluate(x) and accumulate the result. - total = total.checked_add(self.evaluate(x)?).ok_or_else(|| { - ProtocolError::Overflow("Total evaluation overflow in evaluate_interval".into()) - })?; - x += step; - } - Ok(total) - } - - /// Evaluates the total token emission over a specified interval, clamped within additional bounds. - /// - /// This function calculates the sum of token emissions by invoking `self.evaluate(x)` at discrete - /// points that lie between `start_not_included` (exclusive) and `end_included` (inclusive), stepping by - /// `step`. In addition, only evaluation points that also fall within the optional bounds `start_bounds_included` - /// (inclusive) and `end_bounds_included` (inclusive) are considered. - /// - /// # Parameters - /// - /// - `start_not_included` (`RewardDistributionMoment`): - /// The moment after which emissions are considered (exclusive). - /// - `step` (`RewardDistributionMoment`): - /// The interval step between evaluations. **Must be greater than zero**. - /// - `end_included` (`RewardDistributionMoment`): - /// The final moment at which emissions are considered (inclusive). - /// - `start_bounds_included` (`Option`): - /// An optional lower bound for evaluation. Only evaluation points ≥ this value will be included. - /// - `end_bounds_included` (`Option`): - /// An optional upper bound for evaluation. Only evaluation points ≤ this value will be included. - /// - /// # Type Consistency - /// - /// This function **requires all input values to be of the same variant** (`BlockBasedMoment`, `TimeBasedMoment`, or `EpochBasedMoment`). - /// If a mismatch occurs, the function returns an error. + /// - `start_not_included`: The starting moment (exclusive). + /// - `end_included`: The end moment (inclusive). + /// - `step`: The interval between each emission evaluation; must be greater than zero. + /// - `get_epoch_reward_ratio`: Optional function providing a reward ratio for epoch-based distributions. /// /// # Returns - /// - `Ok(TokenAmount)`: The total sum of token emissions over all valid evaluation points. - /// - `Err(ProtocolError)`: If any evaluation fails (e.g., type mismatch, overflow, division-by-zero, or if - /// `step` is zero). - /// - /// # Behavior - /// The function computes the effective start point as the larger of: - /// - `start_not_included + step` (i.e., the first natural evaluation point). - /// - `start_bounds_included` (if provided). /// - /// Similarly, the effective end point is computed as the smaller of: - /// - `end_included`. - /// - `end_bounds_included` (if provided). - /// - /// It then iterates over these evaluation points, accumulating the token amounts. - pub fn evaluate_interval_in_bounds( + /// - `Ok(TokenAmount)` total emitted tokens. + /// - `Err(ProtocolError)` on mismatched types, zero steps, or overflow. + pub fn evaluate_interval( &self, start_not_included: RewardDistributionMoment, - step: RewardDistributionMoment, end_included: RewardDistributionMoment, - start_bounds_included: Option, - end_bounds_included: Option, - ) -> Result { - // Ensure that all moments are of the same type. - if !(start_not_included.same_type(&step) - && start_not_included.same_type(&end_included) - && start_bounds_included - .as_ref() - .map_or(true, |b| start_not_included.same_type(b)) - && end_bounds_included - .as_ref() - .map_or(true, |b| start_not_included.same_type(b))) - { + step: RewardDistributionMoment, + get_epoch_reward_ratio: Option, + ) -> Result + where + F: Fn(EpochIndex) -> Option, + { + // Ensure moments are the same type. + if !(start_not_included.same_type(&step) && start_not_included.same_type(&end_included)) { return Err(ProtocolError::AddingDifferentTypes( - "Mismatched RewardDistributionMoment types".to_string(), + "Mismatched RewardDistributionMoment types".into(), )); } if step == 0u64 { return Err(ProtocolError::InvalidDistributionStep( - "evaluate_interval_in_bounds: step cannot be zero".into(), + "evaluate_interval: step cannot be zero".into(), )); } + if end_included <= start_not_included { return Ok(0); } - // The first natural evaluation point is start_not_included + step. let first_point = (start_not_included + step)?; - // Determine the effective starting point: the larger of first_point and start_bounds_included (if provided). - let effective_start = if let Some(lb) = start_bounds_included { - if lb > first_point { - lb - } else { - first_point - } - } else { - first_point - }; - // Determine the effective ending point: the smallest of end_included and end_bounds_included (if provided). - let effective_end = if let Some(ub) = end_bounds_included { - if ub < end_included { - ub - } else { - end_included - } - } else { - end_included - }; - - if effective_end < effective_start { + if end_included < first_point { return Ok(0); } + // Optimization for FixedAmount + if let DistributionFunction::FixedAmount { amount: fixed_amount } = self { + let steps_count = first_point.steps_till(&end_included, &step)?; + return fixed_amount.checked_mul(steps_count).ok_or_else(|| { + ProtocolError::Overflow("Overflow in FixedAmount evaluation".into()) + }); + } + let mut total: u64 = 0; - let mut x = effective_start; - while x <= effective_end { - total = total - .checked_add(self.evaluate(x.to_u64())?) - .ok_or_else(|| { - ProtocolError::Overflow( - "Total evaluation overflow in evaluate_interval_in_bounds".into(), - ) - })?; - x = (x + step)?; + let mut current_point = first_point; + + while current_point <= end_included { + let base_amount = self.evaluate(current_point.to_u64())?; + + let amount = if let ( + RewardDistributionMoment::EpochBasedMoment(epoch_index), + Some(ref get_ratio_fn), + ) = (current_point, get_epoch_reward_ratio.as_ref()) + { + if let Some(ratio) = get_ratio_fn(epoch_index) { + base_amount + .checked_mul(ratio.numerator) + .and_then(|v| v.checked_div(ratio.denominator)) + .ok_or_else(|| { + ProtocolError::Overflow( + "Overflow applying reward ratio in evaluate_interval".into(), + ) + })? + } else { + return Err(ProtocolError::MissingEpochInfo(format!("missing epoch info for epoch {}", epoch_index))); + } + } else { + base_amount + }; + + total = total.checked_add(amount).ok_or_else(|| { + ProtocolError::Overflow( + "Overflow in token interval evaluation" + ) + })?; + + current_point = (current_point + step)?; } + Ok(total) } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs index 26c0b3d5098..3e00a81341c 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs @@ -7,9 +7,12 @@ mod encode; mod evaluate; mod evaluate_interval; mod validation; +pub mod reward_ratio; pub const MAX_DISTRIBUTION_PARAM: u64 = 281_474_976_710_655; //u48::Max 2^48 - 1 +pub const MAX_LINEAR_SLOPE_PARAM: u64 = 256; + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)] pub enum DistributionFunction { /// Emits a constant (fixed) number of tokens for every period. @@ -126,7 +129,7 @@ pub enum DistributionFunction { /// The emission at period `x` is given by: /// /// ```text - /// f(x) = (a * (x - s) / d) + b + /// f(x) = (a * (x - start_moment) / d) + starting_amount /// ``` /// /// # Parameters @@ -233,8 +236,8 @@ pub enum DistributionFunction { Linear { a: i64, d: u64, - s: Option, - b: TokenAmount, + start_moment: Option, + starting_amount: TokenAmount, min_value: Option, max_value: Option, }, @@ -560,8 +563,8 @@ impl fmt::Display for DistributionFunction { DistributionFunction::Linear { a, d, - s, - b, + start_moment: s, + starting_amount: b, min_value, max_value, } => { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/reward_ratio.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/reward_ratio.rs new file mode 100644 index 00000000000..ce1574b2e7e --- /dev/null +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/reward_ratio.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Copy, Clone)] +pub struct RewardRatio { + pub numerator: u64, + pub denominator: u64, +} \ No newline at end of file diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs index bc720767b46..3897d4dc4e5 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs @@ -4,9 +4,7 @@ use crate::consensus::basic::data_contract::{ InvalidTokenDistributionFunctionInvalidParameterError, InvalidTokenDistributionFunctionInvalidParameterTupleError, }; -use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::{ - DistributionFunction, MAX_DISTRIBUTION_PARAM, -}; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::{DistributionFunction, MAX_DISTRIBUTION_PARAM, MAX_LINEAR_SLOPE_PARAM}; use crate::validation::SimpleConsensusValidationResult; use crate::ProtocolError; impl DistributionFunction { @@ -142,8 +140,8 @@ impl DistributionFunction { DistributionFunction::Linear { a, d, - s, - b, + start_moment: s, + starting_amount: b, min_value, max_value, } => { @@ -164,12 +162,12 @@ impl DistributionFunction { )); } - if *a > MAX_DISTRIBUTION_PARAM as i64 || *a < -(MAX_DISTRIBUTION_PARAM as i64) { + if *a > MAX_LINEAR_SLOPE_PARAM as i64 || *a < -(MAX_LINEAR_SLOPE_PARAM as i64) { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( "a".to_string(), - -(MAX_DISTRIBUTION_PARAM as i64), - MAX_DISTRIBUTION_PARAM as i64, + -(MAX_LINEAR_SLOPE_PARAM as i64), + MAX_LINEAR_SLOPE_PARAM as i64, None, ) .into(), @@ -220,8 +218,8 @@ impl DistributionFunction { let start_token_amount = DistributionFunction::Linear { a: *a, d: *d, - s: Some(s.unwrap_or(start_moment)), - b: *b, + start_moment: Some(s.unwrap_or(start_moment)), + starting_amount: *b, min_value: *min_value, max_value: *max_value, } @@ -1017,8 +1015,8 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - s: Some(3800), - b: 100, + start_moment: Some(3800), + starting_amount: 100, min_value: Some(50), max_value: Some(150), }; @@ -1040,8 +1038,8 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 0, - s: Some(0), - b: 100, + start_moment: Some(0), + starting_amount: 100, min_value: Some(50), max_value: Some(150), }; @@ -1057,8 +1055,8 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - s: Some(MAX_DISTRIBUTION_PARAM + 1), - b: 100, + start_moment: Some(MAX_DISTRIBUTION_PARAM + 1), + starting_amount: 100, min_value: Some(50), max_value: Some(150), }; @@ -1074,8 +1072,8 @@ mod tests { let dist = DistributionFunction::Linear { a: 0, // Invalid: a cannot be zero d: 10, - s: Some(0), - b: 100, + start_moment: Some(0), + starting_amount: 100, min_value: Some(50), max_value: Some(150), }; @@ -1094,8 +1092,8 @@ mod tests { let dist = DistributionFunction::Linear { a: MAX_DISTRIBUTION_PARAM as i64 + 1, // Invalid: a exceeds allowed range d: 10, - s: Some(0), - b: 100, + start_moment: Some(0), + starting_amount: 100, min_value: Some(50), max_value: Some(150), }; @@ -1114,8 +1112,8 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - s: Some(0), - b: 100, + start_moment: Some(0), + starting_amount: 100, min_value: Some(200), // Invalid: min > max max_value: Some(150), }; @@ -1134,8 +1132,8 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - s: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: s exceeds allowed range - b: 100, + start_moment: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: s exceeds allowed range + starting_amount: 100, min_value: Some(50), max_value: Some(150), }; @@ -1154,8 +1152,8 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - s: Some(0), - b: 100, + start_moment: Some(0), + starting_amount: 100, min_value: Some(50), max_value: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: max_value exceeds max allowed range }; @@ -1174,8 +1172,8 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - s: Some(0), - b: 150, // Starts at max value + start_moment: Some(0), + starting_amount: 150, // Starts at max value min_value: Some(50), max_value: Some(150), }; @@ -1194,8 +1192,8 @@ mod tests { let dist = DistributionFunction::Linear { a: -1, // Negative slope (decreasing function) d: 10, - s: Some(0), - b: 50, // Starts at min value + start_moment: Some(0), + starting_amount: 50, // Starts at min value min_value: Some(50), max_value: Some(150), }; @@ -1214,8 +1212,8 @@ mod tests { let dist = DistributionFunction::Linear { a: -5, // Valid decreasing function d: 10, - s: Some(START_MOMENT), - b: 200, + start_moment: Some(START_MOMENT), + starting_amount: 200, min_value: Some(50), max_value: Some(250), }; @@ -1244,8 +1242,8 @@ mod tests { let dist = DistributionFunction::Linear { a: -3, d: 5, - s: Some(START_MOMENT), - b: 100, + start_moment: Some(START_MOMENT), + starting_amount: 100, min_value: Some(10), // Valid min boundary max_value: Some(150), }; @@ -1261,8 +1259,8 @@ mod tests { let dist = DistributionFunction::Linear { a: 3, d: 5, - s: Some(START_MOMENT), - b: 50, + start_moment: Some(START_MOMENT), + starting_amount: 50, min_value: Some(10), max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary }; diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/methods/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/methods/mod.rs index 027fc1ccf13..8c716450028 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/methods/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/methods/mod.rs @@ -2,6 +2,7 @@ use v0::TokenPerpetualDistributionV0Methods; use crate::block::block_info::BlockInfo; use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; use crate::data_contract::associated_token::token_perpetual_distribution::methods::v0::TokenPerpetualDistributionV0Accessors; +use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; use crate::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; @@ -36,9 +37,15 @@ impl TokenPerpetualDistributionV0Accessors for TokenPerpetualDistribution { } impl TokenPerpetualDistributionV0Methods for TokenPerpetualDistribution { - fn next_interval(&self, block_info: &BlockInfo) -> u64 { + fn next_interval(&self, block_info: &BlockInfo) -> RewardDistributionMoment { match self { TokenPerpetualDistribution::V0(v0) => v0.next_interval(block_info), } } + + fn current_interval(&self, block_info: &BlockInfo) -> RewardDistributionMoment { + match self { + TokenPerpetualDistribution::V0(v0) => v0.current_interval(block_info), + } + } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/methods/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/methods/v0/mod.rs index 367f5c4a9d7..158c63018a3 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/methods/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/methods/v0/mod.rs @@ -1,5 +1,6 @@ use crate::block::block_info::BlockInfo; use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; +use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; /// Accessor trait for `TokenPerpetualDistribution`, providing getter and setter methods @@ -21,5 +22,7 @@ pub trait TokenPerpetualDistributionV0Accessors { /// Methods trait for `TokenPerpetualDistribution` pub trait TokenPerpetualDistributionV0Methods { /// we use u64 as a catch-all for any type of interval we might have, eg time, block or epoch - fn next_interval(&self, block_info: &BlockInfo) -> u64; + fn next_interval(&self, block_info: &BlockInfo) -> RewardDistributionMoment; + + fn current_interval(&self, block_info: &BlockInfo) -> RewardDistributionMoment; } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs index a4dff02e160..a3e1f6df488 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs @@ -40,6 +40,67 @@ impl RewardDistributionMoment { RewardDistributionMoment::EpochBasedMoment(epoch) => *epoch as u64, } } + + /// Calculates the number of steps from `self` to `other`, using `step` as the increment. + /// + /// This function computes how many `step` intervals are needed to go from `self` + /// to `other`. If `self >= other`, it returns `0` since no steps are needed. + /// + /// # Parameters + /// + /// - `other`: The target moment. + /// - `step`: The step interval. + /// + /// # Returns + /// + /// - `Ok(u64)`: The number of steps needed. + /// - `Err(ProtocolError)`: If `step` is zero or types are mismatched. + pub fn steps_till(&self, other: &Self, step: &Self) -> Result { + if !self.same_type(other) || !self.same_type(step) { + return Err(ProtocolError::AddingDifferentTypes( + "Cannot compute steps between different RewardDistributionMoment types".to_string(), + )); + } + + match (self, other, step) { + ( + RewardDistributionMoment::BlockBasedMoment(start), + RewardDistributionMoment::BlockBasedMoment(end), + RewardDistributionMoment::BlockBasedMoment(step_size), + ) + | ( + RewardDistributionMoment::TimeBasedMoment(start), + RewardDistributionMoment::TimeBasedMoment(end), + RewardDistributionMoment::TimeBasedMoment(step_size), + ) => { + if *step_size == 0 { + return Err(ProtocolError::InvalidDistributionStep( + "Step value cannot be zero", + )); + } + let start_index = *start / step_size; + let end_index = *end / step_size; + Ok(end_index.saturating_sub(start_index)) + } + ( + RewardDistributionMoment::EpochBasedMoment(start), + RewardDistributionMoment::EpochBasedMoment(end), + RewardDistributionMoment::EpochBasedMoment(step_size), + ) => { + if *step_size == 0 { + return Err(ProtocolError::InvalidDistributionStep( + "Step value cannot be zero", + )); + } + let start_index = *start / step_size; + let end_index = *end / step_size; + Ok(end_index.saturating_sub(start_index) as u64) + } + _ => Err(ProtocolError::AddingDifferentTypes( + "Cannot compute steps with mismatched types".to_string(), + )), + } + } } impl From for u64 { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/accessors.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/accessors.rs index 87a6c83fe63..38f7ae97b86 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/accessors.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/accessors.rs @@ -1,4 +1,3 @@ -use crate::balances::credits::TokenAmount; use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs index 7f52fe80841..8a7eaa2872c 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs @@ -1,5 +1,7 @@ use crate::balances::credits::TokenAmount; use crate::block::block_info::BlockInfo; +use crate::block::epoch::EpochIndex; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::reward_ratio::RewardRatio; use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; use crate::ProtocolError; @@ -8,33 +10,56 @@ impl RewardDistributionType { /// Computes the total rewards emitted in a given interval based on the provided distribution moments. /// /// This function determines the emission amounts within the range from `start_at_excluded` (exclusive) - /// up to `end_at_moment_included` (inclusive). The evaluation depends on the specific type of + /// up to `current_moment_included` (inclusive). The evaluation depends on the specific type of /// distribution (Block-Based, Time-Based, or Epoch-Based) and the associated interval. + /// If the distribution type has a start moment after the provided start moment, it uses the later start moment. + /// If the distribution type has an end moment before the provided current moment, it uses the earlier end moment. /// /// # Parameters /// /// - `start_at_moment_excluded` (`RewardDistributionMoment`): /// The last known point after which rewards should be counted (exclusive). - /// - `end_at_moment_included` (`RewardDistributionMoment`): + /// - `current_moment_included` (`RewardDistributionMoment`): /// The latest point up to which rewards should be counted (inclusive). + /// - `get_epoch_reward_ratio`: Optional function providing a reward ratio for epoch-based distributions. /// /// # Returns /// /// - `Ok(TokenAmount)`: The total sum of emitted rewards in the interval. /// - `Err(ProtocolError)`: If any evaluation fails (e.g., overflow, invalid configuration). /// - pub fn rewards_in_interval( + pub fn rewards_in_interval( &self, start_at_moment_excluded: RewardDistributionMoment, - block_info: &BlockInfo, - ) -> Result { - let end_reward_moment = RewardDistributionMoment::from_block_info(block_info, self); - self.function().evaluate_interval_in_bounds( - start_at_moment_excluded, + current_moment_included: RewardDistributionMoment, + get_epoch_reward_ratio: Option, + ) -> Result + where + F: Fn(EpochIndex) -> Option { + let mut effective_start = start_at_moment_excluded; + let mut effective_end = current_moment_included; + + if let Some(distribution_start) = self.start_at() { + if distribution_start > effective_start { + effective_start = distribution_start; + } + } + + if let Some(distribution_end) = self.end_at() { + if distribution_end < effective_end { + effective_end = distribution_end; + } + } + + if effective_end <= effective_start { + return Ok(0); + } + + self.function().evaluate_interval( + effective_start, + effective_end, self.interval(), - end_reward_moment, - self.start(), - self.end(), + get_epoch_reward_ratio, ) } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs index 5fa6c3e9f8b..9014d6ab7b4 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs @@ -13,24 +13,31 @@ use crate::ProtocolError; #[derive(Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq, PartialOrd)] pub enum RewardDistributionType { - /// An amount of tokens is emitted every n blocks - /// The start and end are included if set + /// An amount of tokens is emitted every n blocks. + /// The start and end are included if set. + /// If start is not set then it will start at the height of the block when the data contract + /// is registered. BlockBasedDistribution { interval: BlockHeightInterval, function: DistributionFunction, start: Option, end: Option, }, - /// An amount of tokens is emitted every amount of time given - /// The start and end are included if set + /// An amount of tokens is emitted every amount of time given. + /// The start and end are included if set. + /// If start is not set then it will start at the time of the block when the data contract + /// is registered. TimeBasedDistribution { interval: TimestampMillisInterval, function: DistributionFunction, start: Option, end: Option, }, - /// An amount of tokens is emitted every amount of epochs - /// The start and end are included if set + /// An amount of tokens is emitted every amount of epochs. + /// The start and end are included if set. + /// If start is not set then it will start at the epoch of the block when the data contract + /// is registered. A distribution would happen at the start of the following epoch, even if it + /// is just 1 block later. EpochBasedDistribution { interval: EpochInterval, function: DistributionFunction, @@ -135,6 +142,89 @@ impl RewardDistributionType { } } } + + /// Returns the start distribution moment as an Option. + pub fn start_at(&self) -> Option { + match self { + RewardDistributionType::BlockBasedDistribution { start, .. } => { + start.map(RewardDistributionMoment::BlockBasedMoment) + } + RewardDistributionType::TimeBasedDistribution { start, .. } => { + start.map(RewardDistributionMoment::TimeBasedMoment) + } + RewardDistributionType::EpochBasedDistribution { start, .. } => { + start.map(RewardDistributionMoment::EpochBasedMoment) + } + } + } + + /// Returns the end distribution moment as an Option. + pub fn end_at(&self) -> Option { + match self { + RewardDistributionType::BlockBasedDistribution { end, .. } => { + end.map(RewardDistributionMoment::BlockBasedMoment) + } + RewardDistributionType::TimeBasedDistribution { end, .. } => { + end.map(RewardDistributionMoment::TimeBasedMoment) + } + RewardDistributionType::EpochBasedDistribution { end, .. } => { + end.map(RewardDistributionMoment::EpochBasedMoment) + } + } + } + + /// Determines the maximum cycle moment allowed based on the last paid moment, + /// the current cycle moment, and the maximum allowed token redemption cycles. + /// + /// This function calculates a capped distribution moment (`RewardDistributionMoment`) by limiting + /// the range between the `last_paid_moment` (or start) and the `current_cycle_moment` to the + /// maximum allowed number of redemption cycles (`max_cycles`). + /// + /// # Arguments + /// - `last_paid_moment`: Optional last moment at which tokens were claimed. + /// - `current_cycle_moment`: The current cycle moment as of the current block. + /// - `max_cycles`: The maximum number of redemption cycles permitted per claim. + /// + /// # Returns + /// - `RewardDistributionMoment`: The maximum allowed cycle moment capped by `max_cycles`. + pub fn max_cycle_moment( + &self, + start_moment: RewardDistributionMoment, + current_cycle_moment: RewardDistributionMoment, + max_cycles: u32, + ) -> Result { + if matches!(self.function(), DistributionFunction::FixedAmount {..}) { + // This is much easier to calculate as it's always fixed, so we can have an unlimited amount of cycles + return Ok(current_cycle_moment); + } + let interval = self.interval(); + + // Calculate maximum allowed moment based on distribution type + match (start_moment, interval, current_cycle_moment) { + ( + RewardDistributionMoment::BlockBasedMoment(start), + RewardDistributionMoment::BlockBasedMoment(step), + RewardDistributionMoment::BlockBasedMoment(current), + ) => Ok(RewardDistributionMoment::BlockBasedMoment( + (start + step.saturating_mul(max_cycles as u64)).min(current), + )), + ( + RewardDistributionMoment::TimeBasedMoment(start), + RewardDistributionMoment::TimeBasedMoment(step), + RewardDistributionMoment::TimeBasedMoment(current), + ) => Ok(RewardDistributionMoment::TimeBasedMoment( + (start + step.saturating_mul(max_cycles as u64)).min(current), + )), + ( + RewardDistributionMoment::EpochBasedMoment(start), + RewardDistributionMoment::EpochBasedMoment(step), + RewardDistributionMoment::EpochBasedMoment(current), + ) => Ok(RewardDistributionMoment::EpochBasedMoment( + (start + (step as u16).saturating_mul(max_cycles as u16)).min(current), + )), + _ => Err(ProtocolError::CorruptedCodeExecution("Mismatch moment types".to_string())), + } + } } impl fmt::Display for RewardDistributionType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/v0/methods.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/v0/methods.rs index e1e749390bb..4a4e9651b18 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/v0/methods.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/v0/methods.rs @@ -1,25 +1,50 @@ use crate::block::block_info::BlockInfo; use crate::data_contract::associated_token::token_perpetual_distribution::methods::v0::TokenPerpetualDistributionV0Methods; +use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; +use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment::{BlockBasedMoment, EpochBasedMoment, TimeBasedMoment}; use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; use crate::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; impl TokenPerpetualDistributionV0Methods for TokenPerpetualDistributionV0 { - fn next_interval(&self, block_info: &BlockInfo) -> u64 { + fn next_interval(&self, block_info: &BlockInfo) -> RewardDistributionMoment { match self.distribution_type { // If the distribution is based on block height, return the next height where emissions occur. RewardDistributionType::BlockBasedDistribution { interval, .. } => { - (block_info.height - block_info.height % interval).saturating_add(interval) + BlockBasedMoment((block_info.height - block_info.height % interval).saturating_add(interval)) } // If the distribution is based on time, return the next timestamp in milliseconds. RewardDistributionType::TimeBasedDistribution { interval, .. } => { - (block_info.time_ms - block_info.time_ms % interval).saturating_add(interval) + TimeBasedMoment((block_info.time_ms - block_info.time_ms % interval).saturating_add(interval)) } // If the distribution is based on epochs, return the next epoch index. RewardDistributionType::EpochBasedDistribution { interval, .. } => { - (block_info.epoch.index - block_info.epoch.index % interval) - .saturating_add(interval) as u64 + EpochBasedMoment((block_info.epoch.index - block_info.epoch.index % interval) + .saturating_add(interval)) + } + } + } + + fn current_interval(&self, block_info: &BlockInfo) -> RewardDistributionMoment { + match self.distribution_type { + // If the distribution is based on block height, return the next height where emissions occur. + RewardDistributionType::BlockBasedDistribution { interval, .. } => { + BlockBasedMoment(block_info.height - block_info.height % interval) + } + + // If the distribution is based on time, return the next timestamp in milliseconds. + RewardDistributionType::TimeBasedDistribution { interval, .. } => { + TimeBasedMoment(block_info.time_ms - block_info.time_ms % interval) + } + + // If the distribution is based on epochs, return the next epoch index. + RewardDistributionType::EpochBasedDistribution { interval, .. } => { + if interval == 1 { + EpochBasedMoment(block_info.epoch.index) + } else { + EpochBasedMoment(block_info.epoch.index - block_info.epoch.index % interval) + } } } } diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs index eb725d311e3..1eea1fa1235 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v0/mod.rs @@ -31,7 +31,6 @@ use crate::consensus::basic::document::MissingPositionsInDocumentTypePropertiesE #[cfg(feature = "validation")] use crate::consensus::basic::BasicError; use crate::data_contract::config::v0::DataContractConfigGettersV0; -use crate::data_contract::config::v1::DataContractConfigGettersV1; use crate::data_contract::config::DataContractConfig; use crate::data_contract::document_type::class_methods::{ consensus_or_protocol_data_contract_error, consensus_or_protocol_value_error, diff --git a/packages/rs-dpp/src/data_contract/document_type/property/mod.rs b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs index 22595b09d87..817139f4e85 100644 --- a/packages/rs-dpp/src/data_contract/document_type/property/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs @@ -16,7 +16,7 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use indexmap::IndexMap; use integer_encoding::{VarInt, VarIntReader}; use itertools::Itertools; -use platform_value::btreemap_extensions::{BTreeValueMapHelper, BTreeValueMapPathHelper}; +use platform_value::btreemap_extensions::BTreeValueMapHelper; use platform_value::{Identifier, Value}; use platform_version::version::PlatformVersion; use rand::distributions::{Alphanumeric, Standard}; diff --git a/packages/rs-dpp/src/data_contract/v0/methods/schema.rs b/packages/rs-dpp/src/data_contract/v0/methods/schema.rs index ea8c5649233..c8a0ce31e95 100644 --- a/packages/rs-dpp/src/data_contract/v0/methods/schema.rs +++ b/packages/rs-dpp/src/data_contract/v0/methods/schema.rs @@ -1,4 +1,3 @@ -use crate::data_contract::config::v0::DataContractConfigGettersV0; use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; use crate::data_contract::document_type::DocumentType; use crate::data_contract::schema::DataContractSchemaMethodsV0; diff --git a/packages/rs-dpp/src/data_contract/v0/serialization/mod.rs b/packages/rs-dpp/src/data_contract/v0/serialization/mod.rs index eb773037191..3128ab44da8 100644 --- a/packages/rs-dpp/src/data_contract/v0/serialization/mod.rs +++ b/packages/rs-dpp/src/data_contract/v0/serialization/mod.rs @@ -1,4 +1,3 @@ -use crate::data_contract::config::v0::DataContractConfigGettersV0; use crate::data_contract::document_type::DocumentType; use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0; use crate::data_contract::serialized_version::DataContractInSerializationFormat; diff --git a/packages/rs-dpp/src/data_contract/v1/methods/schema.rs b/packages/rs-dpp/src/data_contract/v1/methods/schema.rs index ae44171cb39..f66e69b5b83 100644 --- a/packages/rs-dpp/src/data_contract/v1/methods/schema.rs +++ b/packages/rs-dpp/src/data_contract/v1/methods/schema.rs @@ -1,4 +1,3 @@ -use crate::data_contract::config::v0::DataContractConfigGettersV0; use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; use crate::data_contract::document_type::DocumentType; use crate::data_contract::schema::DataContractSchemaMethodsV0; diff --git a/packages/rs-dpp/src/data_contract/v1/serialization/mod.rs b/packages/rs-dpp/src/data_contract/v1/serialization/mod.rs index 49b94bd059c..7f8976516be 100644 --- a/packages/rs-dpp/src/data_contract/v1/serialization/mod.rs +++ b/packages/rs-dpp/src/data_contract/v1/serialization/mod.rs @@ -1,4 +1,3 @@ -use crate::data_contract::config::v0::DataContractConfigGettersV0; use crate::data_contract::document_type::DocumentType; use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0; use crate::data_contract::serialized_version::DataContractInSerializationFormat; diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index 3088afdf325..daa5cdca865 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -233,7 +233,7 @@ impl ErrorWithCode for StateError { Self::DataContractUpdatePermissionError(_) => 40003, Self::DataContractUpdateActionNotAllowedError(_) => 40004, - // Document Errors: 40100-40149 + // Document Errors: 40100-40199 Self::DocumentAlreadyPresentError { .. } => 40100, Self::DocumentNotFoundError { .. } => 40101, Self::DocumentOwnerIdMismatchError { .. } => 40102, @@ -250,24 +250,6 @@ impl ErrorWithCode for StateError { Self::DocumentContestDocumentWithSameIdAlreadyPresentError(_) => 40113, Self::DocumentContestNotPaidForError(_) => 40114, - // Token errors: 40150-40199 - Self::IdentityDoesNotHaveEnoughTokenBalanceError(_) => 40150, - Self::UnauthorizedTokenActionError(_) => 40151, - Self::IdentityTokenAccountFrozenError(_) => 40152, - Self::IdentityTokenAccountNotFrozenError(_) => 40153, - Self::TokenSettingMaxSupplyToLessThanCurrentSupplyError(_) => 40154, - Self::TokenMintPastMaxSupplyError(_) => 40155, - Self::NewTokensDestinationIdentityDoesNotExistError(_) => 40156, - Self::NewAuthorizedActionTakerIdentityDoesNotExistError(_) => 40157, - Self::NewAuthorizedActionTakerGroupDoesNotExistError(_) => 40158, - Self::NewAuthorizedActionTakerMainGroupNotSetError(_) => 40159, - Self::InvalidGroupPositionError(_) => 40160, - Self::TokenIsPausedError(_) => 40161, - Self::IdentityTokenAccountAlreadyFrozenError(_) => 40162, - Self::TokenAlreadyPausedError(_) => 40163, - Self::TokenNotPausedError(_) => 40164, - Self::InvalidTokenClaimPropertyMismatch(_) => 40165, - // Identity Errors: 40200-40299 Self::IdentityAlreadyExistsError(_) => 40200, Self::IdentityPublicKeyIsReadOnlyError { .. } => 40201, @@ -299,10 +281,30 @@ impl ErrorWithCode for StateError { Self::PrefundedSpecializedBalanceInsufficientError(_) => 40400, Self::PrefundedSpecializedBalanceNotFoundError(_) => 40401, - // Data trigger errors: 40500-40799 + // Data trigger errors: 40500-40699 #[cfg(feature = "state-transition-validation")] Self::DataTriggerError(ref e) => e.code(), + // Token errors: 40700-40799 + Self::IdentityDoesNotHaveEnoughTokenBalanceError(_) => 40700, + Self::UnauthorizedTokenActionError(_) => 40701, + Self::IdentityTokenAccountFrozenError(_) => 40702, + Self::IdentityTokenAccountNotFrozenError(_) => 40703, + Self::TokenSettingMaxSupplyToLessThanCurrentSupplyError(_) => 40704, + Self::TokenMintPastMaxSupplyError(_) => 40705, + Self::NewTokensDestinationIdentityDoesNotExistError(_) => 40706, + Self::NewAuthorizedActionTakerIdentityDoesNotExistError(_) => 40707, + Self::NewAuthorizedActionTakerGroupDoesNotExistError(_) => 40708, + Self::NewAuthorizedActionTakerMainGroupNotSetError(_) => 40709, + Self::InvalidGroupPositionError(_) => 40710, + Self::TokenIsPausedError(_) => 40711, + Self::IdentityTokenAccountAlreadyFrozenError(_) => 40712, + Self::TokenAlreadyPausedError(_) => 40713, + Self::TokenNotPausedError(_) => 40714, + Self::InvalidTokenClaimPropertyMismatch(_) => 40715, + Self::InvalidTokenClaimNoCurrentRewards(_) => 40716, + Self::InvalidTokenClaimWrongClaimant(_) => 40717, + // Group errors: 40800-40899 Self::IdentityNotMemberOfGroupError(_) => 40800, Self::GroupActionDoesNotExistError(_) => 40801, diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index 168d57bea5e..b66b45d4651 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -42,7 +42,7 @@ use crate::consensus::state::identity::missing_transfer_key_error::MissingTransf use crate::consensus::state::identity::no_transfer_key_for_core_withdrawal_available_error::NoTransferKeyForCoreWithdrawalAvailableError; use crate::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_insufficient_error::PrefundedSpecializedBalanceInsufficientError; use crate::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_not_found_error::PrefundedSpecializedBalanceNotFoundError; -use crate::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountFrozenError, IdentityTokenAccountNotFrozenError, InvalidGroupPositionError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, NewTokensDestinationIdentityDoesNotExistError, TokenMintPastMaxSupplyError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, UnauthorizedTokenActionError, IdentityTokenAccountAlreadyFrozenError, TokenAlreadyPausedError, TokenIsPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch}; +use crate::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountFrozenError, IdentityTokenAccountNotFrozenError, InvalidGroupPositionError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, NewTokensDestinationIdentityDoesNotExistError, TokenMintPastMaxSupplyError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, UnauthorizedTokenActionError, IdentityTokenAccountAlreadyFrozenError, TokenAlreadyPausedError, TokenIsPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant}; use crate::consensus::state::voting::masternode_incorrect_voter_identity_id_error::MasternodeIncorrectVoterIdentityIdError; use crate::consensus::state::voting::masternode_incorrect_voting_address_error::MasternodeIncorrectVotingAddressError; use crate::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; @@ -242,6 +242,12 @@ pub enum StateError { #[error(transparent)] InvalidTokenClaimPropertyMismatch(InvalidTokenClaimPropertyMismatch), + #[error(transparent)] + InvalidTokenClaimNoCurrentRewards(InvalidTokenClaimNoCurrentRewards), + + #[error(transparent)] + InvalidTokenClaimWrongClaimant(InvalidTokenClaimWrongClaimant), + #[error(transparent)] NewTokensDestinationIdentityDoesNotExistError(NewTokensDestinationIdentityDoesNotExistError), diff --git a/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_no_current_rewards.rs b/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_no_current_rewards.rs new file mode 100644 index 00000000000..dfa49070ac5 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_no_current_rewards.rs @@ -0,0 +1,77 @@ +use crate::prelude::Identifier; +use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; +use crate::consensus::state::state_error::StateError; +use crate::consensus::ConsensusError; +use crate::ProtocolError; +use bincode::{Decode, Encode}; +use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; +use thiserror::Error; + +#[derive( + Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize, +)] +#[error( + "No current rewards available for recipient '{}' on token ID '{}' at moment '{}'. Last claimed moment: '{}'", + recipient_id, + token_id, + current_moment, + last_claimed_moment.as_ref().map_or("Never claimed before".to_string(), |moment| moment.to_string()) +)] +#[platform_serialize(unversioned)] +pub struct InvalidTokenClaimNoCurrentRewards { + token_id: Identifier, + recipient_id: Identifier, + current_moment: RewardDistributionMoment, + last_claimed_moment: Option, +} + +impl InvalidTokenClaimNoCurrentRewards { + /// Creates a new `InvalidTokenClaimNoCurrentRewards` error. + pub fn new( + token_id: Identifier, + recipient_id: Identifier, + current_moment: RewardDistributionMoment, + last_claimed_moment: Option, + ) -> Self { + Self { + token_id, + recipient_id, + current_moment, + last_claimed_moment, + } + } + + /// Returns the token ID associated with the error. + pub fn token_id(&self) -> Identifier { + self.token_id + } + + /// Returns the recipient ID associated with the error. + pub fn recipient_id(&self) -> Identifier { + self.recipient_id + } + + /// Returns the current moment of attempted claim. + pub fn current_moment(&self) -> RewardDistributionMoment { + self.current_moment + } + + /// Returns the last claimed moment, if available. + pub fn last_claimed_moment(&self) -> Option { + self.last_claimed_moment + } + + /// Returns a formatted display string for the last claimed moment. + fn last_claimed_moment_display(&self) -> String { + self.last_claimed_moment + .map(|moment| moment.to_string()) + .unwrap_or_else(|| "Never claimed before".to_string()) + } +} + +/// Implement conversion from `InvalidTokenClaimNoCurrentRewards` to `ConsensusError`. +impl From for ConsensusError { + fn from(err: InvalidTokenClaimNoCurrentRewards) -> Self { + Self::StateError(StateError::InvalidTokenClaimNoCurrentRewards(err)) + } +} \ No newline at end of file diff --git a/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_wrong_claimant.rs b/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_wrong_claimant.rs new file mode 100644 index 00000000000..251e3867b65 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_wrong_claimant.rs @@ -0,0 +1,58 @@ +use crate::prelude::Identifier; +use crate::consensus::state::state_error::StateError; +use crate::consensus::ConsensusError; +use crate::ProtocolError; +use bincode::{Decode, Encode}; +use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; +use thiserror::Error; + +#[derive( + Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize, +)] +#[error( + "Token claim error: expected claimant '{}' for token ID '{}', but received claim from '{}'", + expected_claimant_id, token_id, claimant_id +)] +#[platform_serialize(unversioned)] +pub struct InvalidTokenClaimWrongClaimant { + token_id: Identifier, + expected_claimant_id: Identifier, + claimant_id: Identifier, +} + +impl InvalidTokenClaimWrongClaimant { + /// Creates a new `InvalidTokenClaimWrongClaimant` error. + pub fn new( + token_id: Identifier, + expected_claimant_id: Identifier, + claimant_id: Identifier, + ) -> Self { + Self { + token_id, + expected_claimant_id, + claimant_id, + } + } + + /// Returns the token ID associated with the error. + pub fn token_id(&self) -> Identifier { + self.token_id + } + + /// Returns the expected claimant ID. + pub fn expected_claimant_id(&self) -> Identifier { + self.expected_claimant_id + } + + /// Returns the actual claimant ID. + pub fn claimant_id(&self) -> Identifier { + self.claimant_id + } +} + +/// Implement conversion from `InvalidTokenClaimWrongClaimant` to `ConsensusError`. +impl From for ConsensusError { + fn from(err: InvalidTokenClaimWrongClaimant) -> Self { + Self::StateError(StateError::InvalidTokenClaimWrongClaimant(err)) + } +} \ No newline at end of file diff --git a/packages/rs-dpp/src/errors/consensus/state/token/mod.rs b/packages/rs-dpp/src/errors/consensus/state/token/mod.rs index bf66a2ae1f4..c37d4f85297 100644 --- a/packages/rs-dpp/src/errors/consensus/state/token/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/state/token/mod.rs @@ -14,6 +14,8 @@ mod token_mint_past_max_supply_error; mod token_not_paused_error; mod token_setting_max_supply_to_less_than_current_supply_error; mod unauthorized_token_action_error; +mod invalid_token_claim_no_current_rewards; +mod invalid_token_claim_wrong_claimant; pub use identity_does_not_have_enough_token_balance_error::*; pub use identity_token_account_already_frozen_error::*; @@ -31,3 +33,5 @@ pub use token_mint_past_max_supply_error::*; pub use token_not_paused_error::*; pub use token_setting_max_supply_to_less_than_current_supply_error::*; pub use unauthorized_token_action_error::*; +pub use invalid_token_claim_no_current_rewards::*; +pub use invalid_token_claim_wrong_claimant::*; diff --git a/packages/rs-dpp/src/errors/protocol_error.rs b/packages/rs-dpp/src/errors/protocol_error.rs index da040d1e69b..9e358de2df7 100644 --- a/packages/rs-dpp/src/errors/protocol_error.rs +++ b/packages/rs-dpp/src/errors/protocol_error.rs @@ -281,6 +281,9 @@ pub enum ProtocolError { #[error("invalid distribution step error: {0}")] InvalidDistributionStep(&'static str), + + #[error("missing epoch info: {0}")] + MissingEpochInfo(String), } impl From<&str> for ProtocolError { diff --git a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/mod.rs index 4fd3827a6a6..971f7164296 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/mod.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::unpaid_epoch::UnpaidEpoch; @@ -38,7 +39,7 @@ impl Platform { transaction: &Transaction, batch: &mut Vec, platform_version: &PlatformVersion, - ) -> Result<(StorageAndProcessingPoolCredits, Vec<(Identifier, u64)>), Error> { + ) -> Result<(StorageAndProcessingPoolCredits, BTreeMap), Error> { match platform_version .drive_abci .methods diff --git a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/v0/mod.rs index 7d095a8bf05..5ac0ca44cbb 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/v0/mod.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use crate::error::execution::ExecutionError; use crate::error::Error; @@ -32,7 +33,7 @@ impl Platform { transaction: &Transaction, batch: &mut Vec, platform_version: &PlatformVersion, - ) -> Result<(StorageAndProcessingPoolCredits, Vec<(Identifier, u64)>), Error> { + ) -> Result<(StorageAndProcessingPoolCredits, BTreeMap), Error> { let mut drive_operations = vec![]; let unpaid_epoch_tree = Epoch::new(unpaid_epoch.epoch_index())?; @@ -165,7 +166,7 @@ impl Platform { batch.push(DriveOperation::GroveDBOpBatch(operations)); - Ok((storage_and_processing_fees, proposers)) + Ok((storage_and_processing_fees, proposers.into_iter().collect())) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/mod.rs index 8401d81d89e..d88d2ae282c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/mod.rs @@ -1,3 +1,4 @@ mod perpetual; +mod pre_programmed; use super::*; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual.rs deleted file mode 100644 index 251fd6929b1..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual.rs +++ /dev/null @@ -1,99 +0,0 @@ -use super::*; -mod perpetual_distribution { - use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; - use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; - use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; - use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; - use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; - use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; - use super::*; - #[test] - #[ignore] - fn test_token_perpetual_distribution_block_claim_linear() { - let platform_version = PlatformVersion::latest(); - let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() - .build_with_mock_rpc() - .set_genesis_state(); - - let mut rng = StdRng::seed_from_u64(49853); - - let platform_state = platform.state.load(); - - let (identity, signer, key) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); - - let (contract, token_id) = create_token_contract_with_owner_identity( - &mut platform, - identity.id(), - Some(|token_configuration: &mut TokenConfiguration| { - token_configuration - .distribution_rules_mut() - .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( - TokenPerpetualDistributionV0 { - distribution_type: RewardDistributionType::BlockBasedDistribution { - interval: 10, - function: DistributionFunction::FixedAmount { amount: 50 }, - start: None, - end: None, - }, - distribution_recipient: Default::default(), - }, - ))); - }), - None, - platform_version, - ); - - fast_forward_to_block(&platform, 10_200_000_000, 40, 42, 1, false); //25 years later - - let claim_transition = BatchTransition::new_token_claim_transition( - token_id, - identity.id(), - contract.id(), - 0, - TokenDistributionType::Perpetual, - None, - &key, - 2, - 0, - &signer, - platform_version, - None, - None, - None, - ) - .expect("expect to create documents batch transition"); - - let claim_serialized_transition = claim_transition - .serialize_to_bytes() - .expect("expected documents batch serialized state transition"); - - let transaction = platform.drive.grove.start_transaction(); - - let processing_result = platform - .platform - .process_raw_state_transitions( - &vec![claim_serialized_transition.clone()], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - - assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution(_, _)] - ); - - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); - } -} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs new file mode 100644 index 00000000000..f19268b9bbb --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs @@ -0,0 +1,500 @@ +use rand::prelude::StdRng; +use dpp::dash_to_credits; +use dpp::data_contract::TokenConfiguration; +use dpp::state_transition::batch_transition::BatchTransition; +use platform_version::version::PlatformVersion; +use crate::execution::validation::state_transition::tests::{create_token_contract_with_owner_identity, setup_identity}; +use crate::test::helpers::setup::TestPlatformBuilder; +use super::*; +mod perpetual_distribution_block { + use dpp::block::epoch::Epoch; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; + use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; + use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; + use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; + use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; + use super::*; + #[test] + fn test_token_perpetual_distribution_block_claim_linear_and_claim_again() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::FixedAmount { amount: 50 }, + start: None, + end: None, + }, + distribution_recipient: TokenDistributionRecipient::ContractOwner, + }, + ))); + }), + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 10_200_000_000, 40, 42, 1, false); //25 years later + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 10_200_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // Since height is 42 we had 4 events * 50 (+ 100000 which was data contract owner base). + assert_eq!(token_balance, Some(100200)); + + fast_forward_to_block(&platform, 10_200_000_000, 45, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 10_200_100_000, + height: 46, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); + + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(100200)); + + fast_forward_to_block(&platform, 10_200_000_000, 49, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key, + 4, + 0, + &signer, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 10_200_100_000, + height: 50, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // An extra event + assert_eq!(token_balance, Some(100250)); + } + + #[test] + fn test_token_perpetual_distribution_not_claimant() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::FixedAmount { amount: 50 }, + start: None, + end: None, + }, + // we give to identity 2 + distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + }, + ))); + }), + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 10_200_000_000, 40, 42, 1, false); //25 years later + + // claiming for identity 1 (contract owner) + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 10_200_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimWrongClaimant(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(100000)); + + let token_balance_2 = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance_2, None); + } + + #[test] + fn test_token_perpetual_distribution_block_claim_linear_given_to_specific_identity() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::FixedAmount { amount: 50 }, + start: None, + end: None, + }, + distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + }, + ))); + }), + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 10_200_000_000, 40, 42, 1, false); //25 years later + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 10_200_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // Since height is 42 we had 4 events x 5. + assert_eq!(token_balance, Some(200)); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/mod.rs new file mode 100644 index 00000000000..f4243241076 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/mod.rs @@ -0,0 +1,4 @@ +use super::*; +mod block_based; +mod time_based; + diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs new file mode 100644 index 00000000000..99293b608a1 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs @@ -0,0 +1,1074 @@ +use rand::prelude::StdRng; +use dpp::dash_to_credits; +use dpp::data_contract::TokenConfiguration; +use dpp::state_transition::batch_transition::BatchTransition; +use platform_version::version::PlatformVersion; +use crate::execution::validation::state_transition::tests::{create_token_contract_with_owner_identity, setup_identity}; +use crate::test::helpers::setup::TestPlatformBuilder; +use super::*; +mod perpetual_distribution_time { + use dpp::block::epoch::Epoch; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::{DistributionFunction, MAX_DISTRIBUTION_PARAM, MAX_LINEAR_SLOPE_PARAM}; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; + use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; + use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; + use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; + use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; + use super::*; + #[test] + fn test_token_perpetual_distribution_time_claim_linear_and_claim_again() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::TimeBasedDistribution { + // every 3600 seconds, meaning every hour + interval: 3_600_000, + function: DistributionFunction::FixedAmount { amount: 50 }, + start: None, + end: None, + }, + distribution_recipient: TokenDistributionRecipient::ContractOwner, + }, + ))); + }), + None, + None, + platform_version, + ); + + // 5 hours later + fast_forward_to_block(&platform, 18_000_000, 40, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_000_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // Since height is 42 we had 5 events * 50 (+ 100000 which was data contract owner base). + assert_eq!(token_balance, Some(100250)); + + fast_forward_to_block(&platform, 19_000_000, 45, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 19_100_000, + height: 46, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); + + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(100250)); + + fast_forward_to_block(&platform, 22_000_000, 49, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key, + 4, + 0, + &signer, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 22_100_000, + height: 50, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // An extra event + assert_eq!(token_balance, Some(100300)); + } + + #[test] + fn test_token_perpetual_distribution_not_claimant() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::TimeBasedDistribution { + // every 3600 seconds, meaning every hour + interval: 3_600_000, + function: DistributionFunction::FixedAmount { amount: 50 }, + start: None, + end: None, + }, + // we give to identity 2 + distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + }, + ))); + }), + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 10_200_000_000, 40, 42, 1, false); //25 years later + + // claiming for identity 1 (contract owner) + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 10_200_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimWrongClaimant(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(100000)); + + let token_balance_2 = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance_2, None); + } + + #[test] + fn test_token_perpetual_distribution_time_claim_linear_given_to_specific_identity() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::TimeBasedDistribution { + // every 3600 seconds, meaning every hour + interval: 3_600_000, + function: DistributionFunction::FixedAmount { amount: 50 }, + start: None, + end: None, + }, + distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + }, + ))); + }), + None, + None, + platform_version, + ); + + // 5 hours later + fast_forward_to_block(&platform, 18_000_000, 40, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // Since time is slightly over 5 hours we had 5 events x 5. + assert_eq!(token_balance, Some(250)); + } + + #[test] + fn test_token_perpetual_distribution_time_linear() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(4981); + + let platform_state = platform.state.load(); + + let (identity, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::TimeBasedDistribution { + // every 1 millisecond + interval: 1, + function: DistributionFunction::Linear { + a: 1, // Increase in slope y = x + d: 1, // No division, simple emission + start_moment: None, + starting_amount: 0, + min_value: None, + max_value: None, + }, + start: None, + end: None, + }, + distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + }, + ))); + }), + None, + None, + platform_version, + ); + + // 5 hours later + fast_forward_to_block(&platform, 18_000_000, 40, 42, 1, false); + + // We are only claiming for 256 cycles + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // Since time is slightly over 5 hours we had 5 events x 5. + let redemption_cycles = platform_version.system_limits.max_token_redemption_cycles; + let balance = redemption_cycles * (redemption_cycles +1) / 2; // Euler's theorem + // This works because we are adding 1 + 2 + 3 + 4 etc + assert_eq!(token_balance, Some(balance as u64)); + + // We are only claiming for 128 more cycles + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 3, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + let redemption_cycles = redemption_cycles * 2; + let balance = redemption_cycles * (redemption_cycles +1) / 2; // Euler's theorem + assert_eq!(token_balance, Some(balance as u64)); + } + + #[test] + fn test_token_perpetual_distribution_time_linear_verify_contract_start() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(4981); + + let platform_state = platform.state.load(); + + let (identity, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::TimeBasedDistribution { + // every hour + interval: 3_600_000, + function: DistributionFunction::Linear { + a: 1, // Increase in slope y = x + d: 1, // No division, simple emission + start_moment: None, + starting_amount: 0, + min_value: None, + max_value: None, + }, + start: None, + end: None, + }, + distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + }, + ))); + }), + Some(10), + None, + platform_version, + ); + + // 5 hours later + fast_forward_to_block(&platform, 18_000_000, 40, 42, 1, false); + + // We are only claiming for 256 cycles + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // Since time is slightly over 5 hours we had 5 events x 5. + let redemption_cycles = platform_version.system_limits.max_token_redemption_cycles; + let balance = redemption_cycles * (redemption_cycles +1) / 2; // Euler's theorem + // This works because we are adding 1 + 2 + 3 + 4 etc + assert_eq!(token_balance, Some(balance as u64)); + + // We are only claiming for 128 more cycles + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 3, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + let redemption_cycles = redemption_cycles * 2; + let balance = redemption_cycles * (redemption_cycles +1) / 2; // Euler's theorem + assert_eq!(token_balance, Some(balance as u64)); + } + + #[test] + fn test_token_perpetual_distribution_time_linear_high_values() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(4981); + + let platform_state = platform.state.load(); + + let (identity, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::TimeBasedDistribution { + // every 1 millisecond + interval: 1, + function: DistributionFunction::Linear { + a: MAX_LINEAR_SLOPE_PARAM as i64, // Strongest slope + d: 1, // No division + start_moment: None, + starting_amount: MAX_DISTRIBUTION_PARAM, + min_value: None, + max_value: None, + }, + start: None, + end: None, + }, + distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + }, + ))); + }), + None, + None, + platform_version, + ); + + // 5 hours later + fast_forward_to_block(&platform, 18_000_000, 40, 42, 1, false); + + // We are only claiming for 256 cycles + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // Since time is slightly over 5 hours we had 5 events x 5. + assert_eq!(token_balance, Some(100205091725260956)); + + // We are only claiming for 256 more cycles + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 3, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // Since time is slightly over 5 hours we had 5 events x 5. + assert_eq!(token_balance, Some(100205091725260956)); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/pre_programmed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/pre_programmed.rs new file mode 100644 index 00000000000..e11be12d060 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/pre_programmed.rs @@ -0,0 +1,867 @@ +use super::*; +mod pre_programmed_distribution { + use dpp::block::epoch::Epoch; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; + use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; + use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; + use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; + use dpp::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; + use dpp::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; + use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; + use super::*; + #[test] + fn test_token_pre_programmed_distribution_two_claims() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + // At time 100 we give identity 2 445 as part of the block execution + distributions: [(100, [(identity_2.id(), 445)].into()), (500000, [(identity_2.id(), 600)].into())].into(), + }, + ))); + }), + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(445)); + + fast_forward_to_block(&platform, 600000, 45, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + &key_2, + 3, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 700000, + height: 46, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(1045)); + } + + #[test] + fn test_token_pre_programmed_distribution_claim_again_when_none_left() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + // At time 100 we give identity 2 445 as part of the block execution + distributions: [(100, [(identity_2.id(), 445)].into())].into(), + }, + ))); + }), + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(445)); + + fast_forward_to_block(&platform, 600000, 45, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + &key_2, + 3, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 700000, + height: 46, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(445)); + } + + #[test] + fn test_token_pre_programmed_distribution_claim_again_when_none_ready() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + // At time 100 we give identity 2 445 as part of the block execution + distributions: [(20000000, [(identity_2.id(), 1337)].into())].into(), + }, + ))); + }), + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, None); + } + + #[test] + fn test_token_pre_programmed_distribution_claim_again_when_none_ready_after_a_claim() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + // At time 100 we give identity 2 445 as part of the block execution + distributions: [(100, [(identity_2.id(), 445)].into()), (20000000, [(identity_2.id(), 1337)].into())].into(), + }, + ))); + }), + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(445)); + + fast_forward_to_block(&platform, 600000, 45, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + &key_2, + 3, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 700000, + height: 46, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(445)); + } + + #[test] + fn test_token_pre_programmed_distribution_claim_no_rewards_for_recipient() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + // We give rewards to identity 1, not 2 + distributions: [(100, [(identity.id(), 445)].into())].into(), + }, + ))); + }), + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, None); + } + + #[test] + fn test_token_pre_programmed_distribution_claim_no_pre_programmed_rewards_for_recipient_when_they_have_perpetual() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, _, _) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + // We give rewards to identity 1, not 2 + distributions: [(100, [(identity.id(), 445)].into())].into(), + }, + ))); + + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::FixedAmount { amount: 50 }, + start: None, + end: None, + }, + distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + }, + ))); + }), + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, None); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs index ce0ca0b86c8..a3f69f66894 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs @@ -58,6 +58,7 @@ mod token_tests { identity.id(), None::, None, + None, platform_version, ); @@ -147,6 +148,7 @@ mod token_tests { token_configuration.set_max_supply(Some(1000000)); }), None, + None, platform_version, ); @@ -240,6 +242,7 @@ mod token_tests { identity.id(), None::, None, + None, platform_version, ); @@ -329,6 +332,7 @@ mod token_tests { identity.id(), None::, None, + None, platform_version, ); @@ -421,6 +425,7 @@ mod token_tests { identity.id(), None::, None, + None, platform_version, ); @@ -510,6 +515,7 @@ mod token_tests { .set_minting_allow_choosing_destination(false); }), None, + None, platform_version, ); @@ -609,6 +615,7 @@ mod token_tests { .set_minting_allow_choosing_destination(false); }), None, + None, platform_version, ); @@ -705,6 +712,7 @@ mod token_tests { .set_minting_allow_choosing_destination(false); }), None, + None, platform_version, ); @@ -797,6 +805,7 @@ mod token_tests { .set_new_tokens_destination_identity(Some(identity.id())); }), None, + None, platform_version, ); @@ -899,6 +908,7 @@ mod token_tests { .set_new_tokens_destination_identity(Some(identity.id())); }), None, + None, platform_version, ); @@ -998,6 +1008,7 @@ mod token_tests { .set_new_tokens_destination_identity(Some(identity.id())); }), None, + None, platform_version, ); @@ -1106,6 +1117,7 @@ mod token_tests { )); }), None, + None, platform_version, ); @@ -1208,6 +1220,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -1322,6 +1335,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -1432,6 +1446,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -1629,6 +1644,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -1835,6 +1851,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -2128,6 +2145,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -2418,6 +2436,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -2533,6 +2552,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -2733,6 +2753,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -2863,6 +2884,7 @@ mod token_tests { identity.id(), None::, None, + None, platform_version, ); @@ -2949,6 +2971,7 @@ mod token_tests { identity.id(), None::, None, + None, platform_version, ); @@ -3042,6 +3065,7 @@ mod token_tests { contract_owner_identity.id(), None::, None, + None, platform_version, ); @@ -3161,6 +3185,7 @@ mod token_tests { identity.id(), None::, None, + None, platform_version, ); @@ -3261,6 +3286,7 @@ mod token_tests { identity.id(), None::, None, + None, platform_version, ); @@ -3352,6 +3378,7 @@ mod token_tests { identity.id(), None::, None, + None, platform_version, ); @@ -3469,6 +3496,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -3684,6 +3712,7 @@ mod token_tests { )); }), None, + None, platform_version, ); @@ -3792,6 +3821,7 @@ mod token_tests { )); }), None, + None, platform_version, ); @@ -3962,6 +3992,7 @@ mod token_tests { )); }), None, + None, platform_version, ); @@ -4364,6 +4395,7 @@ mod token_tests { )); }), None, + None, platform_version, ); @@ -4467,6 +4499,7 @@ mod token_tests { )); }), None, + None, platform_version, ); @@ -4577,6 +4610,7 @@ mod token_tests { )); }), None, + None, platform_version, ); @@ -4733,6 +4767,7 @@ mod token_tests { )); }), None, + None, platform_version, ); @@ -4824,6 +4859,7 @@ mod token_tests { )); }), None, + None, platform_version, ); @@ -4915,6 +4951,7 @@ mod token_tests { )); }), None, + None, platform_version, ); @@ -5014,6 +5051,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -5131,6 +5169,7 @@ mod token_tests { }, )); }), + None, Some( [( 0, @@ -5337,6 +5376,7 @@ mod token_tests { }, )); }), + None, Some( [ ( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs index 5959bbc5465..f23d3725f8f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs @@ -56,7 +56,7 @@ pub(in crate::execution) mod tests { use crate::test::helpers::setup::TempPlatform; use dpp::block::block_info::BlockInfo; use dpp::fee::Credits; - use dpp::identity::{Identity, IdentityPublicKey, IdentityV0, KeyID, KeyType, Purpose, SecurityLevel}; + use dpp::identity::{Identity, IdentityPublicKey, IdentityV0, KeyID, KeyType, Purpose, SecurityLevel, TimestampMillis}; use dpp::prelude::{Identifier, IdentityNonce}; use platform_version::version::PlatformVersion; use rand::prelude::StdRng; @@ -2292,6 +2292,7 @@ pub(in crate::execution) mod tests { platform: &mut TempPlatform, identity_id: Identifier, token_configuration_modification: Option, + contract_start_time: Option, add_groups: Option>, platform_version: &PlatformVersion, ) -> (DataContract, Identifier) { @@ -2304,7 +2305,7 @@ pub(in crate::execution) mod tests { Some(identity_id.to_buffer()), Some(|data_contract: &mut DataContract| { data_contract.set_created_at_epoch(Some(0)); - data_contract.set_created_at(Some(0)); + data_contract.set_created_at(Some(contract_start_time.unwrap_or_default())); data_contract.set_created_at_block_height(Some(0)); if let Some(token_configuration_modification) = token_configuration_modification { let token_configuration = data_contract diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs index 93e6707a2c0..b19effc5349 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs @@ -220,9 +220,7 @@ impl Drive { { self.add_perpetual_distribution( token_id.to_buffer(), - contract.owner_id().to_buffer(), perpetual_distribution, - block_info, estimated_costs_only_with_layer_info, &mut batch_operations, transaction, diff --git a/packages/rs-drive/src/drive/initialization/v1/mod.rs b/packages/rs-drive/src/drive/initialization/v1/mod.rs index f7d1ad8aae0..7f76cfd3064 100644 --- a/packages/rs-drive/src/drive/initialization/v1/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v1/mod.rs @@ -5,8 +5,7 @@ use crate::util::batch::GroveDbOpBatch; use crate::drive::system::misc_path_vec; use crate::drive::tokens::paths::{ - token_distributions_root_path_vec, token_perpetual_distributions_path_vec, - token_root_perpetual_distributions_path_vec, token_timed_distributions_path_vec, + token_distributions_root_path_vec, token_timed_distributions_path_vec, tokens_root_path_vec, TOKEN_BALANCES_KEY, TOKEN_BLOCK_TIMED_DISTRIBUTIONS_KEY, TOKEN_DISTRIBUTIONS_KEY, TOKEN_EPOCH_TIMED_DISTRIBUTIONS_KEY, TOKEN_IDENTITY_INFO_KEY, TOKEN_MS_TIMED_DISTRIBUTIONS_KEY, TOKEN_PERPETUAL_DISTRIBUTIONS_KEY, @@ -15,10 +14,8 @@ use crate::drive::tokens::paths::{ use crate::drive::{Drive, RootTree}; use crate::error::Error; use crate::util::batch::grovedb_op_batch::GroveDbOpBatchV0Methods; -use crate::util::grove_operations::BatchInsertTreeApplyType; -use crate::util::object_size_info::PathKeyInfo; use dpp::version::PlatformVersion; -use grovedb::{Element, TransactionArg, TreeType}; +use grovedb::{Element, TransactionArg}; use grovedb_path::SubtreePath; impl Drive { diff --git a/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/mod.rs index 90e65e7abd7..0e2136179df 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/mod.rs @@ -4,7 +4,6 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::block_info::BlockInfo; use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; @@ -16,9 +15,7 @@ impl Drive { pub fn add_perpetual_distribution( &self, token_id: [u8; 32], - owner_id: [u8; 32], distribution: &TokenPerpetualDistribution, - block_info: &BlockInfo, estimated_costs_only_with_layer_info: &mut Option< HashMap, >, @@ -35,9 +32,7 @@ impl Drive { { 0 => self.add_perpetual_distribution_v0( token_id, - owner_id, distribution, - block_info, estimated_costs_only_with_layer_info, batch_operations, transaction, diff --git a/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/v0/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/v0/mod.rs index b70d752e55f..08cc68b427f 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/v0/mod.rs @@ -1,27 +1,14 @@ -use crate::drive::tokens::paths::{ - token_perpetual_distributions_path_vec, token_root_perpetual_distributions_path_vec, - TokenPerpetualDistributionPaths, TOKEN_PERPETUAL_DISTRIBUTIONS_FIRST_EVENT_KEY, - TOKEN_PERPETUAL_DISTRIBUTIONS_INFO_KEY, TOKEN_PERPETUAL_DISTRIBUTIONS_KEY, -}; +use crate::drive::tokens::paths::{token_perpetual_distributions_path_vec, token_root_perpetual_distributions_path_vec, TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, TOKEN_PERPETUAL_DISTRIBUTIONS_INFO_KEY}; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::util::grove_operations::BatchInsertTreeApplyType; use crate::util::object_size_info::{PathKeyElementInfo, PathKeyInfo}; -use crate::util::storage_flags::StorageFlags; -use dpp::block::block_info::BlockInfo; -use dpp::data_contract::associated_token::token_distribution_key::{ - TokenDistributionKey, TokenDistributionType, -}; -use dpp::data_contract::associated_token::token_perpetual_distribution::methods::v0::{ - TokenPerpetualDistributionV0Accessors, TokenPerpetualDistributionV0Methods, -}; use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; use dpp::serialization::PlatformSerializable; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; -use grovedb::reference_path::ReferencePathType; use grovedb::{Element, EstimatedLayerInformation, TransactionArg, TreeType}; use std::collections::HashMap; @@ -30,9 +17,7 @@ impl Drive { pub(super) fn add_perpetual_distribution_v0( &self, token_id: [u8; 32], - owner_id: [u8; 32], distribution: &TokenPerpetualDistribution, - block_info: &BlockInfo, estimated_costs_only_with_layer_info: &mut Option< HashMap, >, @@ -49,9 +34,6 @@ impl Drive { } let serialized_distribution = distribution.serialize_to_bytes()?; - // Storage flags for cleanup logic - let storage_flags = StorageFlags::new_single_epoch(block_info.epoch.index, Some(owner_id)); - let root_perpetual_distributions_path = token_root_perpetual_distributions_path_vec(); let perpetual_distributions_path = token_perpetual_distributions_path_vec(token_id); @@ -91,44 +73,16 @@ impl Drive { &platform_version.drive, )?; - let next_interval = distribution.next_interval(block_info); - self.batch_insert( PathKeyElementInfo::<0>::PathKeyElement(( - perpetual_distributions_path, - vec![TOKEN_PERPETUAL_DISTRIBUTIONS_FIRST_EVENT_KEY], - Element::new_item(next_interval.to_be_bytes().to_vec()), + perpetual_distributions_path.clone(), + vec![TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY], + Element::empty_tree(), )), batch_operations, &platform_version.drive, )?; - // We will distribute for the first time on the next interval - let distribution_path_for_next_interval = - distribution.distribution_path_for_next_interval_from_block_info(block_info); - - let distribution_key = TokenDistributionKey { - token_id: token_id.into(), - recipient: distribution.distribution_recipient(), - distribution_type: TokenDistributionType::Perpetual, - }; - - let serialized_key = distribution_key.serialize_consume_to_bytes()?; - - let remaining_reference = vec![vec![TOKEN_PERPETUAL_DISTRIBUTIONS_KEY], token_id.to_vec()]; - - let reference = ReferencePathType::UpstreamRootHeightReference(2, remaining_reference); - - // Now we create the reference - self.batch_insert( - PathKeyElementInfo::<0>::PathKeyElement(( - distribution_path_for_next_interval, - serialized_key, - Element::new_reference_with_flags(reference, storage_flags.to_some_element_flags()), - )), - batch_operations, - &platform_version.drive, - )?; Ok(()) } diff --git a/packages/rs-drive/src/drive/tokens/distribution/add_pre_programmed_distribution/v0/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/add_pre_programmed_distribution/v0/mod.rs index 8423dbc4de9..db6ccf0dae5 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/add_pre_programmed_distribution/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/add_pre_programmed_distribution/v0/mod.rs @@ -1,8 +1,4 @@ -use crate::drive::tokens::paths::{ - token_ms_timed_at_time_distributions_path_vec, token_ms_timed_distributions_path_vec, - token_pre_programmed_at_time_distribution_path_vec, token_pre_programmed_distributions_path, - token_root_pre_programmed_distributions_path, TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY, -}; +use crate::drive::tokens::paths::{token_ms_timed_at_time_distributions_path_vec, token_ms_timed_distributions_path_vec, token_pre_programmed_at_time_distribution_path_vec, token_pre_programmed_distributions_path, token_root_pre_programmed_distributions_path, TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY}; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -131,7 +127,7 @@ impl Drive { if let Some(estimated_costs_only_with_layer_info) = estimated_costs_only_with_layer_info { Drive::add_estimation_costs_for_token_pre_programmed_distribution( token_id, - distribution.distributions().keys(), + Some(distribution.distributions().keys()), estimated_costs_only_with_layer_info, &platform_version.drive, )?; @@ -188,6 +184,14 @@ impl Drive { } let pre_programmed_distributions_path = token_pre_programmed_distributions_path(&token_id); + self.batch_insert_empty_tree( + pre_programmed_distributions_path, + DriveKeyInfo::Key(vec![TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY]), + None, // we will never clean this part up + batch_operations, + &platform_version.drive, + )?; + for (time, distribution) in distribution.distributions() { self.batch_insert_empty_sum_tree( pre_programmed_distributions_path, diff --git a/packages/rs-drive/src/drive/tokens/distribution/fetch/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/fetch/mod.rs index c5158306db1..da143a9582a 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/fetch/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/fetch/mod.rs @@ -1,2 +1,3 @@ mod perpetual_distribution_last_paid_moment; mod pre_programmed_distributions; +mod pre_programmed_distribution_last_paid_time_ms; diff --git a/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/mod.rs new file mode 100644 index 00000000000..0f456d45719 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/mod.rs @@ -0,0 +1,91 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::prelude::Identifier; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; +use dpp::identity::TimestampMillis; + +impl Drive { + /// Fetches the last paid timestamp for a pre-programmed distribution for a given identity, + /// using the appropriate versioned method. + /// + /// This method queries the pre-programmed distributions tree at the path + /// `pre_programmed_distribution_last_paid_time_path_vec(token_id, identity_id)`. + /// + /// # Parameters + /// + /// - `token_id`: The 32‑byte identifier for the token. + /// - `identity_id`: The identifier of the identity whose last paid time is being queried. + /// - `transaction`: The current GroveDB transaction. + /// - `platform_version`: The platform version to determine the method variant. + /// + /// # Returns + /// + /// A `Result` containing the last paid `RewardDistributionMoment` on success or an `Error` on failure. + pub fn fetch_pre_programmed_distribution_last_paid_time_ms( + &self, + token_id: [u8; 32], + identity_id: Identifier, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + self.fetch_pre_programmed_distribution_last_paid_time_ms_operations( + token_id, + identity_id, + &mut vec![], + transaction, + platform_version, + ) + } + + /// Fetches the last paid timestamp for a pre-programmed distribution for a given identity, + /// using the appropriate versioned method. + /// + /// This method queries the pre-programmed distributions tree at the path + /// `pre_programmed_distribution_last_paid_time_path_vec(token_id, identity_id)`. + /// + /// # Parameters + /// + /// - `token_id`: The 32‑byte identifier for the token. + /// - `identity_id`: The identifier of the identity whose last paid time is being queried. + /// - `drive_operations`: A mutable vector to accumulate low-level drive operations. + /// - `transaction`: The current GroveDB transaction. + /// - `platform_version`: The platform version to determine the method variant. + /// + /// # Returns + /// + /// A `Result` containing the last paid `RewardDistributionMoment` on success or an `Error` on failure. + pub(crate) fn fetch_pre_programmed_distribution_last_paid_time_ms_operations( + &self, + token_id: [u8; 32], + identity_id: Identifier, + drive_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .token + .fetch + .pre_programmed_distribution_last_paid_time + { + 0 => self.fetch_pre_programmed_distribution_last_paid_time_ms_operations_v0( + token_id, + identity_id, + drive_operations, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "fetch_pre_programmed_distribution_last_paid_time_ms_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/v0/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/v0/mod.rs new file mode 100644 index 00000000000..e0a25f62112 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/v0/mod.rs @@ -0,0 +1,71 @@ +use crate::drive::tokens::paths::token_pre_programmed_distributions_identity_last_claimed_time_path; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::DirectQueryType; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::Element::Item; +use grovedb::TransactionArg; +use dpp::prelude::TimestampMillis; + +impl Drive { + /// Fetches the last paid timestamp for a pre_programmed distribution for a given identity. + /// + /// This method queries the `token_pre_programmed_distributions_path_vec(token_id)` tree and + /// retrieves the last recorded payment timestamp (`TimestampMillis`) associated with + /// `identity_id`. The timestamp is expected to be stored as an 8-byte big-endian value. + /// + /// # Parameters + /// + /// - `token_id`: The 32‑byte identifier for the token. + /// - `identity_id`: The identifier of the identity whose last paid time is being queried. + /// - `drive_operations`: A mutable vector to accumulate low-level drive operations. + /// - `transaction`: The current GroveDB transaction. + /// - `platform_version`: The platform version to determine the method variant. + /// + /// # Returns + /// + /// A `Result` containing `Some(RewardDistributionMoment)` if a record exists, `None` if no record is found, + /// or an `Error` if retrieval fails. + pub(super) fn fetch_pre_programmed_distribution_last_paid_time_ms_operations_v0( + &self, + token_id: [u8; 32], + identity_id: Identifier, + drive_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let direct_query_type = DirectQueryType::StatefulDirectQuery; + + let pre_programmed_distributions_path = + token_pre_programmed_distributions_identity_last_claimed_time_path(&token_id); + + match self.grove_get_raw_optional( + (&pre_programmed_distributions_path).into(), + identity_id.as_slice(), + direct_query_type, + transaction, + drive_operations, + &platform_version.drive, + ) { + Ok(Some(Item(value, _))) => { + if value.len() != 8 { + return Err(Error::Drive(DriveError::CorruptedDriveState("Pre programmed last claimed time should be encoded on 8 bytes".to_string()))); + } + let mut array = [0u8; 8]; + array.copy_from_slice(&value); + Ok(Some(u64::from_be_bytes(array))) + } + + Ok(None) | Err(Error::GroveDB(grovedb::Error::PathKeyNotFound(_))) => Ok(None), + + Ok(Some(_)) => Err(Error::Drive(DriveError::CorruptedElementType( + "Last moment was present but was not an item", + ))), + + Err(e) => Err(e), + } + } +} diff --git a/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/mod.rs index 86c8b28ecf7..af6749e3900 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/mod.rs @@ -4,58 +4,43 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::block_info::BlockInfo; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; -use grovedb::{EstimatedLayerInformation, TransactionArg}; +use grovedb::EstimatedLayerInformation; use std::collections::HashMap; -use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; impl Drive { - /// Marks a perpetual token release as distributed in the state tree. + /// Marks a perpetual token distribution as "distributed" by updating the last claimed moment for an identity. /// - /// This function updates the perpetual distribution record by: - /// - Removing the previous distribution moment. - /// - Setting the new distribution moment. - /// - Associating the new distribution with the correct recipient. + /// This function records the current distribution moment in the GroveDB tree, indicating that the recipient + /// identified by `owner_id` has claimed the distribution for the specified perpetual token. + /// + /// # Actions Performed + /// - Inserts or updates the distribution record for the recipient (`owner_id`) at the specified token's perpetual distribution path. + /// - If provided, updates estimated costs for storage layers involved. /// /// # Parameters - /// - `token_id`: The unique identifier of the token. - /// - `owner_id`: The unique identifier of the owner who initiated the distribution. - /// - `previous_moment`: The previous moment when the reward was last distributed. - /// - `next_moment`: The next moment when the reward should be distributed. - /// - `distribution_recipient`: The recipient of the distributed reward. - /// - `block_info`: Metadata about the current block, including epoch details. - /// - `estimated_costs_only_with_layer_info`: Optional storage layer information for cost estimation. - /// - `batch_operations`: A mutable reference to the batch operation queue. - /// - `transaction`: The transaction context. - /// - `platform_version`: The current platform version. + /// - `token_id`: A 32-byte identifier uniquely representing the token. + /// - `recipient_id`: A 32-byte identifier uniquely representing the recipient (identity) claiming the distribution. + /// - `current_moment`: The moment (`RewardDistributionMoment`) representing when the distribution occurred. + /// - `estimated_costs_only_with_layer_info`: Optional mutable reference to a hashmap for estimating storage costs. If `Some`, cost estimation is performed without executing database writes. + /// - `platform_version`: A reference to the current `PlatformVersion` to determine correct version-specific behavior. /// /// # Returns - /// - `Ok(())` if the operation succeeds. - /// - `Err(Error::Drive(DriveError::UnknownVersionMismatch))` if an unsupported version is encountered. - /// - /// # Behavior - /// - If `estimated_costs_only_with_layer_info` is `Some`, the function only estimates costs. - /// - The previous distribution entry is deleted from the tree. - /// - The new distribution entry is inserted with a reference to the corresponding recipient. + /// - `Ok(Vec)`: Batch operations to perform the storage update if successful. + /// - `Err(Error::Drive(DriveError::UnknownVersionMismatch))`: If an unsupported `platform_version` is encountered. /// /// # Versioning - /// - Uses version 0 of `mark_perpetual_release_as_distributed_operations_v0` if supported. - /// - Returns an error if an unknown version is received. + /// - Currently supports version `0`. If an unknown version is specified, an error is returned. pub fn mark_perpetual_release_as_distributed_operations( &self, token_id: [u8; 32], - owner_id: [u8; 32], - previous_moment: RewardDistributionMoment, - next_moment: RewardDistributionMoment, - distribution_recipient: TokenDistributionRecipient, - block_info: &BlockInfo, + recipient_id: [u8; 32], + current_moment: RewardDistributionMoment, estimated_costs_only_with_layer_info: &mut Option< HashMap, >, - transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { match platform_version @@ -67,13 +52,9 @@ impl Drive { { 0 => self.mark_perpetual_release_as_distributed_operations_v0( token_id, - owner_id, - previous_moment, - next_moment, - distribution_recipient, - block_info, + recipient_id, + current_moment, estimated_costs_only_with_layer_info, - transaction, platform_version, ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { diff --git a/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/v0/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/v0/mod.rs index 91ee22b4282..7c3873b2c5e 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/v0/mod.rs @@ -1,116 +1,60 @@ -use crate::drive::tokens::paths::{token_perpetual_distributions_path_vec, TokenPerpetualDistributionMomentPaths, TOKEN_PERPETUAL_DISTRIBUTIONS_KEY, TOKEN_PERPETUAL_DISTRIBUTIONS_FIRST_EVENT_KEY}; +use crate::drive::tokens::paths::{token_perpetual_distributions_identity_last_claimed_by_identity_path_vec, token_perpetual_distributions_identity_last_claimed_time_path_vec, TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY}; use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::util::object_size_info::{PathKeyElementInfo}; -use crate::util::storage_flags::StorageFlags; -use dpp::block::block_info::BlockInfo; -use dpp::data_contract::associated_token::token_distribution_key::{ - TokenDistributionType, TokenDistributionKey, -}; -use dpp::serialization::PlatformSerializable; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; -use grovedb::reference_path::ReferencePathType; -use grovedb::{Element, EstimatedLayerInformation, MaybeTree, TransactionArg, TreeType}; +use grovedb::{Element, EstimatedLayerInformation, TreeType}; use std::collections::HashMap; -use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; +use grovedb::EstimatedLayerCount::EstimatedLevel; +use grovedb::EstimatedLayerSizes::AllItems; use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; -use crate::util::grove_operations::BatchDeleteApplyType::{StatefulBatchDelete, StatelessBatchDelete}; -use crate::util::type_constants::DEFAULT_HASH_SIZE_U32; impl Drive { /// Version 0 of `mark_perpetual_release_as_distributed_v0` pub(super) fn mark_perpetual_release_as_distributed_operations_v0( &self, token_id: [u8; 32], - owner_id: [u8; 32], - previous_moment: RewardDistributionMoment, - next_moment: RewardDistributionMoment, - distribution_recipient: TokenDistributionRecipient, - block_info: &BlockInfo, + recipient_id: [u8; 32], + current_moment: RewardDistributionMoment, estimated_costs_only_with_layer_info: &mut Option< HashMap, >, - - transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { let mut batch_operations = vec![]; + + let perpetual_distributions_path = token_perpetual_distributions_identity_last_claimed_time_path_vec(token_id); + if let Some(estimated_costs_only_with_layer_info) = estimated_costs_only_with_layer_info { Drive::add_estimation_costs_for_token_perpetual_distribution( Some(token_id), estimated_costs_only_with_layer_info, &platform_version.drive, )?; + + let estimated_layer_count = match current_moment { + RewardDistributionMoment::BlockBasedMoment(_) + | RewardDistributionMoment::TimeBasedMoment(_) => EstimatedLevel(0, false), + RewardDistributionMoment::EpochBasedMoment(_) => EstimatedLevel(10, false), + }; + + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_owned_path(perpetual_distributions_path.clone()), + EstimatedLayerInformation { + tree_type: TreeType::NormalTree, + estimated_layer_count, + estimated_layer_sizes: AllItems(1, 8, None), + }, + ); } - // Storage flags for cleanup logic - let storage_flags = StorageFlags::new_single_epoch(block_info.epoch.index, Some(owner_id)); - - let perpetual_distributions_path = token_perpetual_distributions_path_vec(token_id); - self.batch_insert( PathKeyElementInfo::<0>::PathKeyElement(( perpetual_distributions_path, - vec![TOKEN_PERPETUAL_DISTRIBUTIONS_FIRST_EVENT_KEY], - Element::new_item(next_moment.to_be_bytes_vec()), - )), - &mut batch_operations, - &platform_version.drive, - )?; - - // We will distribute for the first time on the next interval - - let distribution_path_for_previous_moment = previous_moment.distribution_path(); - - let distribution_path_for_next_moment = next_moment.distribution_path(); - - let distribution_key = TokenDistributionKey { - token_id: token_id.into(), - recipient: distribution_recipient, - distribution_type: TokenDistributionType::Perpetual, - }; - - let serialized_distribution_key = distribution_key.serialize_consume_to_bytes()?; - - let remaining_reference = vec![vec![TOKEN_PERPETUAL_DISTRIBUTIONS_KEY], token_id.to_vec()]; - - let reference = ReferencePathType::UpstreamRootHeightReference(2, remaining_reference); - - let delete_apply_type = if estimated_costs_only_with_layer_info.is_some() { - StatelessBatchDelete { - in_tree_type: TreeType::NormalTree, - estimated_key_size: DEFAULT_HASH_SIZE_U32, - estimated_value_size: reference.serialized_size() as u32 - + storage_flags.serialized_size(), - } - } else { - // we know we are not deleting a subtree - StatefulBatchDelete { - is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), - } - }; - - let new_element = - Element::new_reference_with_flags(reference, storage_flags.to_some_element_flags()); - - // We delete the old one - self.batch_delete( - distribution_path_for_previous_moment.as_slice().into(), - &serialized_distribution_key, - delete_apply_type, - transaction, - &mut batch_operations, - &platform_version.drive, - )?; - - // Now we add the new one - self.batch_insert( - PathKeyElementInfo::<0>::PathKeyElement(( - distribution_path_for_next_moment, - serialized_distribution_key, - new_element, + recipient_id.to_vec(), + Element::new_item(current_moment.to_be_bytes_vec()), )), &mut batch_operations, &platform_version.drive, diff --git a/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/mod.rs index 4a4c6a92ad1..4ff258d90bf 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/mod.rs @@ -38,8 +38,7 @@ impl Drive { pub fn mark_pre_programmed_release_as_distributed_operations( &self, token_id: [u8; 32], - owner_id: [u8; 32], - identity_id: [u8; 32], + recipient_id: [u8; 32], release_time: TimestampMillis, block_info: &BlockInfo, estimated_costs_only_with_layer_info: &mut Option< @@ -57,8 +56,7 @@ impl Drive { { 0 => self.mark_pre_programmed_release_as_distributed_operations_v0( token_id, - owner_id, - identity_id, + recipient_id, release_time, block_info, estimated_costs_only_with_layer_info, diff --git a/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/v0/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/v0/mod.rs index 2f20e852d8c..c219d3b4335 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/v0/mod.rs @@ -1,6 +1,4 @@ -use crate::drive::tokens::paths::{ - token_ms_timed_at_time_distributions_path_vec, TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY, -}; +use crate::drive::tokens::paths::{token_distributions_root_path_vec, token_ms_timed_at_time_distributions_path_vec, token_pre_programmed_distributions_identity_last_claimed_time_path, token_pre_programmed_distributions_identity_last_claimed_time_path_vec, TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY}; use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; @@ -8,7 +6,7 @@ use crate::util::grove_operations::BatchDeleteApplyType::{ StatefulBatchDelete, StatelessBatchDelete, }; use crate::util::storage_flags::StorageFlags; -use crate::util::type_constants::DEFAULT_HASH_SIZE_U32; +use crate::util::type_constants::{DEFAULT_HASH_SIZE_U32, U8_SIZE_U8}; use dpp::block::block_info::BlockInfo; use dpp::data_contract::associated_token::token_distribution_key::{ TokenDistributionKey, TokenDistributionType, @@ -19,8 +17,12 @@ use dpp::serialization::PlatformSerializable; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; use grovedb::reference_path::ReferencePathType; -use grovedb::{EstimatedLayerInformation, TransactionArg, TreeType}; +use grovedb::{Element, EstimatedLayerInformation, TransactionArg, TreeType}; use std::collections::HashMap; +use grovedb::EstimatedLayerCount::EstimatedLevel; +use grovedb::EstimatedLayerSizes::{AllItems, AllSubtrees}; +use grovedb::EstimatedSumTrees::NoSumTrees; +use crate::util::object_size_info::PathKeyElementInfo; /// Marks the pre-programmed release as distributed. /// @@ -49,8 +51,7 @@ impl Drive { pub(super) fn mark_pre_programmed_release_as_distributed_operations_v0( &self, token_id: [u8; 32], - owner_id: [u8; 32], - identity_id: [u8; 32], + recipient_id: [u8; 32], release_time: TimestampMillis, // TimestampMillis represented as a 32-bit unsigned integer block_info: &BlockInfo, estimated_costs_only_with_layer_info: &mut Option< @@ -59,18 +60,46 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { + let pre_programmed_distributions_path = + token_pre_programmed_distributions_identity_last_claimed_time_path_vec(token_id); + if let Some(estimated_costs_only_with_layer_info) = estimated_costs_only_with_layer_info { + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_owned_path(token_distributions_root_path_vec()), + EstimatedLayerInformation { + tree_type: TreeType::NormalTree, + estimated_layer_count: EstimatedLevel(1, false), // We should be on the first level + estimated_layer_sizes: AllSubtrees(U8_SIZE_U8, NoSumTrees, None), + }, + ); + + Drive::add_estimation_costs_for_token_pre_programmed_distribution::>( + token_id, + None, + estimated_costs_only_with_layer_info, + &platform_version.drive, + )?; + Drive::add_estimation_costs_for_root_token_ms_interval_distribution( [&release_time], estimated_costs_only_with_layer_info, &platform_version.drive, )?; + + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_owned_path(pre_programmed_distributions_path.clone()), + EstimatedLayerInformation { + tree_type: TreeType::NormalTree, + estimated_layer_count: EstimatedLevel(5, false), + estimated_layer_sizes: AllItems(1, 8, None), + }, + ); } // Initialize an empty batch of operations let mut batch_operations = vec![]; // Create storage flags for cleanup logic; these flags are attached to inserted elements. - let storage_flags = StorageFlags::new_single_epoch(block_info.epoch.index, Some(owner_id)); + let storage_flags = StorageFlags::new_single_epoch(block_info.epoch.index, Some(recipient_id)); // The pre-programmed distribution was scheduled by inserting a reference in the // millisecond-timed distributions tree at a key corresponding to the release time. @@ -80,7 +109,7 @@ impl Drive { // Build the distribution key used when the pre-programmed release was scheduled. let distribution_key = TokenDistributionKey { token_id: token_id.into(), - recipient: TokenDistributionRecipient::Identity(identity_id.into()), + recipient: TokenDistributionRecipient::Identity(recipient_id.into()), distribution_type: TokenDistributionType::PreProgrammed, }; @@ -92,7 +121,7 @@ impl Drive { vec![TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY], token_id.to_vec(), release_time.to_be_bytes().to_vec(), - identity_id.to_vec(), + recipient_id.to_vec(), ]; let reference = ReferencePathType::UpstreamRootHeightReference(2, remaining_reference); @@ -122,6 +151,16 @@ impl Drive { &platform_version.drive, )?; + self.batch_insert( + PathKeyElementInfo::<0>::PathKeyElement(( + pre_programmed_distributions_path, + recipient_id.to_vec(), + Element::new_item(release_time.to_be_bytes().to_vec()), + )), + &mut batch_operations, + &platform_version.drive, + )?; + Ok(batch_operations) } } diff --git a/packages/rs-drive/src/drive/tokens/distribution/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/mod.rs index c3c9e07a7bc..121855ec180 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/mod.rs @@ -12,5 +12,5 @@ mod mark_pre_programmed_release_as_distributed; mod prove; /// Token distribution queries pub mod queries; -#[cfg(feature = "server")] -mod set_perpetual_distribution_next_event_for_identity_id; +// #[cfg(feature = "server")] +// mod set_perpetual_distribution_next_event_for_identity_id; diff --git a/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_perpetual_distribution/mod.rs b/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_perpetual_distribution/mod.rs index a789a4929e6..e025ea514d3 100644 --- a/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_perpetual_distribution/mod.rs +++ b/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_perpetual_distribution/mod.rs @@ -3,7 +3,6 @@ mod v0; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; -use dpp::prelude::TimestampMillis; use dpp::version::drive_versions::DriveVersion; use grovedb::batch::KeyInfoPath; use grovedb::EstimatedLayerInformation; diff --git a/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_perpetual_distribution/v0/mod.rs b/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_perpetual_distribution/v0/mod.rs index 2b14ee3f14d..45c6380a279 100644 --- a/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_perpetual_distribution/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_perpetual_distribution/v0/mod.rs @@ -59,18 +59,6 @@ impl Drive { estimated_layer_sizes: AllSubtrees(U8_SIZE_U8, NoSumTrees, None), }, ); - - // 4. Add estimation for identities' last claim subtree - estimated_costs_only_with_layer_info.insert( - KeyInfoPath::from_known_owned_path( - token_perpetual_distributions_identity_last_claimed_time_path_vec(token_id), - ), - EstimatedLayerInformation { - tree_type: TreeType::NormalTree, - estimated_layer_count: ApproximateElements(1000), // Example size, adjust as needed - estimated_layer_sizes: AllItems(DEFAULT_HASH_SIZE_U8, U64_SIZE_U32, None), - }, - ); } } } diff --git a/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/mod.rs b/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/mod.rs index b83b6c58961..97bfd007159 100644 --- a/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/mod.rs +++ b/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/mod.rs @@ -25,7 +25,7 @@ impl Drive { /// # Parameters /// /// - `token_id`: The 32-byte identifier for the token whose pre-programmed distribution tree is being estimated. - /// - `times`: A vector of timestamps (in milliseconds) for which pre-programmed distributions exist. + /// - `times`: An optional vector of timestamps (in milliseconds) for which pre-programmed distributions exist. /// - `estimated_costs_only_with_layer_info`: A mutable hashmap that maps `KeyInfoPath` to `EstimatedLayerInformation`. /// This cache is used by Grovedb to track the estimated storage costs for each layer in the tree. /// - `drive_version`: The drive version that determines which estimation logic to use. @@ -41,7 +41,7 @@ impl Drive { /// implementations are applied. pub(crate) fn add_estimation_costs_for_token_pre_programmed_distribution<'a, I>( token_id: [u8; 32], - times: I, + times: Option, estimated_costs_only_with_layer_info: &mut HashMap, drive_version: &DriveVersion, ) -> Result<(), Error> diff --git a/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/v0/mod.rs b/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/v0/mod.rs index 43590bf1d4f..9f1f64f28e5 100644 --- a/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/v0/mod.rs @@ -26,7 +26,7 @@ impl Drive { /// - `estimated_costs_only_with_layer_info`: A mutable hashmap that holds estimated layer information. pub(crate) fn add_estimation_costs_for_token_pre_programmed_distribution_v0<'a, I>( token_id: [u8; 32], - times: I, + times: Option, estimated_costs_only_with_layer_info: &mut HashMap, ) where I: IntoIterator + ExactSizeIterator, @@ -59,23 +59,25 @@ impl Drive { EstimatedLayerInformation { tree_type: TreeType::NormalTree, // At this level, expect as many children as there are time entries. - estimated_layer_count: ApproximateElements(times.len() as u32), + estimated_layer_count: ApproximateElements(times.as_ref().map(|times| times.len()).unwrap_or(128) as u32), estimated_layer_sizes: AllSubtrees(U64_SIZE_U8, AllSumTrees, None), }, ); - - // 4. For each provided timestamp, add an estimation for the at-time sum tree. - for time in times { - estimated_costs_only_with_layer_info.insert( - KeyInfoPath::from_known_owned_path( - token_pre_programmed_at_time_distribution_path_vec(token_id, *time), - ), - EstimatedLayerInformation { - tree_type: TreeType::SumTree, - estimated_layer_count: EstimatedLevel(3, false), // probably not that many - estimated_layer_sizes: AllItems(DEFAULT_HASH_SIZE_U8, U64_SIZE_U32, None), - }, - ); + + if let Some(times) = times { + // 4. For each provided timestamp, add an estimation for the at-time sum tree. + for time in times { + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_owned_path( + token_pre_programmed_at_time_distribution_path_vec(token_id, *time), + ), + EstimatedLayerInformation { + tree_type: TreeType::SumTree, + estimated_layer_count: EstimatedLevel(3, false), // probably not that many + estimated_layer_sizes: AllItems(DEFAULT_HASH_SIZE_U8, U64_SIZE_U32, None), + }, + ); + } } } } diff --git a/packages/rs-drive/src/drive/tokens/paths.rs b/packages/rs-drive/src/drive/tokens/paths.rs index 6c072c56225..a5587fda4d1 100644 --- a/packages/rs-drive/src/drive/tokens/paths.rs +++ b/packages/rs-drive/src/drive/tokens/paths.rs @@ -56,12 +56,12 @@ pub const TOKEN_EPOCH_TIMED_DISTRIBUTIONS_KEY: u8 = 192; /// Key for the perpetual distribution info. pub const TOKEN_PERPETUAL_DISTRIBUTIONS_INFO_KEY: u8 = 128; -/// Key for the perpetual distribution first event. -pub const TOKEN_PERPETUAL_DISTRIBUTIONS_FIRST_EVENT_KEY: u8 = 64; - /// Key for the perpetual distribution last claim for identities key. pub const TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY: u8 = 192; +/// Key for the perpetual distribution last claim for identities key. +pub const TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY: u8 = 192; + /// The path for the balances tree pub fn tokens_root_path() -> [&'static [u8]; 1] { @@ -214,6 +214,62 @@ pub fn token_perpetual_distributions_identity_last_claimed_time_path_vec( ] } +/// The path for the token perpetual distributions tree for a token +pub fn token_pre_programmed_distributions_identity_last_claimed_time_path( + token_id: &[u8; 32], +) -> [&[u8]; 5] { + [ + Into::<&[u8; 1]>::into(RootTree::Tokens), + &[TOKEN_DISTRIBUTIONS_KEY], + &[TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY], + token_id, + &[TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY], + ] +} + +/// The path for the token perpetual distributions tree for a token as a vector +pub fn token_pre_programmed_distributions_identity_last_claimed_time_path_vec( + token_id: [u8; 32], +) -> Vec> { + vec![ + vec![RootTree::Tokens as u8], + vec![TOKEN_DISTRIBUTIONS_KEY], + vec![TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY], + token_id.to_vec(), + vec![TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY], + ] +} + +/// The path for the token perpetual distributions tree for a token +pub fn token_perpetual_distributions_identity_last_claimed_by_identity_path<'a>( + token_id: &'a [u8; 32], + identity_id: &'a [u8; 32], +) -> [&'a [u8]; 6] { + [ + Into::<&[u8; 1]>::into(RootTree::Tokens), + &[TOKEN_DISTRIBUTIONS_KEY], + &[TOKEN_PERPETUAL_DISTRIBUTIONS_KEY], + token_id, + &[TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY], + identity_id, + ] +} + +/// The path for the token perpetual distributions tree for a token as a vector +pub fn token_perpetual_distributions_identity_last_claimed_by_identity_path_vec( + token_id: [u8; 32], + identity_id: [u8; 32], +) -> Vec> { + vec![ + vec![RootTree::Tokens as u8], + vec![TOKEN_DISTRIBUTIONS_KEY], + vec![TOKEN_PERPETUAL_DISTRIBUTIONS_KEY], + token_id.to_vec(), + vec![TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY], + identity_id.to_vec() + ] +} + /// The path for the token pre-programmed distributions tree pub fn token_root_pre_programmed_distributions_path() -> [&'static [u8]; 3] { [ diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs index 628c27ec748..5ff11401929 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs @@ -42,19 +42,16 @@ impl DriveHighLevelBatchOperationConverter for TokenClaimTransitionAction { match self.distribution_info() { TokenDistributionInfo::Perpetual( - _, - _, + _, TokenDistributionResolvedRecipient::ContractOwnerIdentity(identity), ) | TokenDistributionInfo::PreProgrammed(_, identity) | TokenDistributionInfo::Perpetual( - _, - _, + _, TokenDistributionResolvedRecipient::Identity(identity), ) | TokenDistributionInfo::Perpetual( - _, - _, + _, TokenDistributionResolvedRecipient::Evonode(identity), ) => { ops.push(TokenOperation(TokenOperationType::TokenMint { @@ -71,24 +68,17 @@ impl DriveHighLevelBatchOperationConverter for TokenClaimTransitionAction { ops.push(TokenOperation( TokenOperationType::TokenMarkPreProgrammedReleaseAsDistributed { token_id: self.token_id(), - owner_id, - identity_id: *recipient, + recipient_id: *recipient, release_time: *release_time, }, )); } - TokenDistributionInfo::Perpetual( - last_release_moment, - next_release_moment, - _, - ) => { + TokenDistributionInfo::Perpetual(claim_moment, _) => { ops.push(TokenOperation( TokenOperationType::TokenMarkPerpetualReleaseAsDistributed { token_id: self.token_id(), - owner_id, - last_release_moment: *last_release_moment, - next_release_moment: *next_release_moment, - recipient: self.recipient(), + recipient_id: owner_id, + cycle_start_moment: *claim_moment, }, )); } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs index e18b7882496..22d695a29ce 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs @@ -103,7 +103,7 @@ impl TokenClaimTransitionActionAccessorsV0 for TokenClaimTransitionActionV0 { TokenDistributionInfo::PreProgrammed(_, identifier) => { TokenDistributionRecipient::Identity(*identifier) } - TokenDistributionInfo::Perpetual(_, _, resolved_recipient) => resolved_recipient.into(), + TokenDistributionInfo::Perpetual(_, resolved_recipient) => resolved_recipient.into(), } } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index b01a110e727..6bdf54dc1f3 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -1,19 +1,24 @@ use std::collections::BTreeMap; +use std::process::id; use std::sync::Arc; use grovedb::TransactionArg; +use dpp::balances::credits::TokenAmount; use dpp::block::block_info::BlockInfo; use dpp::block::epoch::EpochIndex; use dpp::block::finalized_epoch_info::FinalizedEpochInfo; +use dpp::block::finalized_epoch_info::v0::getters::FinalizedEpochInfoGettersV0; use dpp::consensus::ConsensusError; use dpp::consensus::state::state_error::StateError; -use dpp::consensus::state::token::InvalidTokenClaimPropertyMismatch; +use dpp::consensus::state::token::{InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimWrongClaimant}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; use dpp::data_contract::associated_token::token_distribution_key::{TokenDistributionInfo, TokenDistributionType}; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; +use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::reward_ratio::RewardRatio; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::{TokenDistributionRecipient, TokenDistributionResolvedRecipient}; -use dpp::data_contract::associated_token::token_perpetual_distribution::methods::v0::TokenPerpetualDistributionV0Accessors; +use dpp::data_contract::associated_token::token_perpetual_distribution::methods::v0::{TokenPerpetualDistributionV0Accessors, TokenPerpetualDistributionV0Methods}; use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; +use dpp::data_contract::associated_token::token_pre_programmed_distribution::methods::v0::TokenPreProgrammedDistributionV0Methods; use dpp::identifier::Identifier; use dpp::state_transition::batch_transition::token_claim_transition::v0::TokenClaimTransitionV0; use dpp::ProtocolError; @@ -21,7 +26,7 @@ use crate::drive::contract::DataContractFetchInfo; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_transition_action::v0::TokenClaimTransitionActionV0; use dpp::fee::fee_result::FeeResult; -use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::prelude::{ConsensusValidationResult, TimestampMillis, UserFeeIncrease}; use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; use platform_version::version::PlatformVersion; @@ -73,69 +78,7 @@ impl TokenClaimTransitionActionV0 { ), Error, > { - let TokenClaimTransitionV0 { - base, - distribution_type, - public_note, - } = value; - - let mut drive_operations = vec![]; - - let base_action_validation_result = - TokenBaseTransitionAction::try_from_borrowed_base_transition_with_contract_lookup( - drive, - owner_id, - &base, - approximate_without_state_for_costs, - transaction, - &mut drive_operations, - get_data_contract, - platform_version, - )?; - - let fee_result = Drive::calculate_fee( - None, - Some(drive_operations), - &block_info.epoch, - drive.config.epochs_per_era, - platform_version, - None, - )?; - - let base_action = match base_action_validation_result.is_valid() { - true => base_action_validation_result.into_data()?, - false => { - let bump_action = BumpIdentityDataContractNonceAction::from_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action.into(), - base_action_validation_result.errors, - ), - fee_result, - )); - } - }; - - Ok(( - BatchedTransitionAction::TokenAction(TokenTransitionAction::ClaimAction( - TokenClaimTransitionActionV0 { - base: base_action, - amount: 0, //todo - distribution_info: todo!(), - public_note, - } - .into(), - )) - .into(), - fee_result, - )) + Self::try_from_borrowed_token_claim_transition_with_contract_lookup(drive, owner_id, &value, approximate_without_state_for_costs, transaction, block_info, user_fee_increase, get_data_contract, platform_version) } /// Converts a borrowed `TokenClaimTransitionV0` into a `TokenClaimTransitionActionV0` using the provided contract lookup. @@ -268,11 +211,86 @@ impl TokenClaimTransitionActionV0 { // We need to find the oldest pre-programmed distribution that wasn't yet claimed // for this identity - let oldest_time = 0; + + let times = pre_programmed_distribution.distributions(); + + let mut last_paid_time_operations = vec![]; + + let last_paid_moment = drive + .fetch_pre_programmed_distribution_last_paid_time_ms_operations( + base.token_id().to_buffer(), + owner_id, + &mut last_paid_time_operations, + transaction, + platform_version, + )?; + + let last_paid_time_fee_result = Drive::calculate_fee( + None, + Some(last_paid_time_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; + + fee_result.checked_add_assign(last_paid_time_fee_result)?; - let amount = 0; + let mut distributions_in_past_for_owner: BTreeMap = times + .iter() + .filter_map(|(timestamp, distribution)| { + if timestamp > &block_info.time_ms { + // Don't get the ones in the future + None + } else { + distribution.get(&owner_id).map(|amount| (*timestamp, *amount)) + } + }) + .collect(); - (amount, TokenDistributionInfo::PreProgrammed(0, owner_id)) + let distribution_after_last_paid: Option<(TimestampMillis, TokenAmount)> = + if let Some(last_paid) = last_paid_moment { + // Find the earliest distribution after the last paid moment + distributions_in_past_for_owner + .range((last_paid + 1)..) + .next() + .map(|(timestamp, amount)| (*timestamp, *amount)) + } else { + // If never paid, take the earliest available distribution + distributions_in_past_for_owner + .first_key_value() + .map(|(timestamp, amount)| (*timestamp, *amount)) + }; + + let Some((payout_time, amount)) = distribution_after_last_paid else { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action.into(), + vec![ConsensusError::StateError( + StateError::InvalidTokenClaimNoCurrentRewards( + InvalidTokenClaimNoCurrentRewards::new( + value.base().token_id(), + owner_id, + RewardDistributionMoment::TimeBasedMoment(block_info.time_ms), + last_paid_moment.map(RewardDistributionMoment::TimeBasedMoment), + ), + ), + )], + ), + fee_result, + )); + }; + + (amount, TokenDistributionInfo::PreProgrammed(payout_time, owner_id)) } TokenDistributionType::Perpetual => { // we need to validate that we have a perpetual distribution @@ -304,6 +322,44 @@ impl TokenClaimTransitionActionV0 { )); }; + // Let's start by checking that we have a valid claimant + let wrong_claimant_error = match perpetual_distribution.distribution_recipient() { + TokenDistributionRecipient::ContractOwner if base_action.data_contract_fetch_info().contract.owner_id() != owner_id => { + Some(base_action.data_contract_fetch_info().contract.owner_id()) + } + TokenDistributionRecipient::Identity(identifier) if identifier != owner_id => { + Some(identifier) + } + _ => None + }; + + if let Some(expected_claimant) = wrong_claimant_error { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action.into(), + vec![ConsensusError::StateError( + StateError::InvalidTokenClaimWrongClaimant( + InvalidTokenClaimWrongClaimant::new( + value.base().token_id(), + expected_claimant, + owner_id, + ), + ), + )], + ), + fee_result, + )); + } + let mut last_paid_time_operations = vec![]; let last_paid_moment = drive @@ -336,24 +392,33 @@ impl TokenClaimTransitionActionV0 { )?; fee_result.checked_add_assign(last_paid_time_fee_result)?; + + let current_cycle_moment = perpetual_distribution.current_interval(block_info); + + // We need to get the max cycles allowed + let max_cycles = platform_version.system_limits.max_token_redemption_cycles; + let max_cycle_moment = perpetual_distribution.distribution_type().max_cycle_moment(start_from_moment_for_distribution, current_cycle_moment, max_cycles)?; - let recipient = match perpetual_distribution.distribution_recipient() { + let (recipient, amount) = match perpetual_distribution.distribution_recipient() { TokenDistributionRecipient::ContractOwner => { - TokenDistributionResolvedRecipient::ContractOwnerIdentity( + (TokenDistributionResolvedRecipient::ContractOwnerIdentity( base_action.data_contract_fetch_info().contract.owner_id(), - ) + ), perpetual_distribution + .distribution_type() + .rewards_in_interval:: Option>(start_from_moment_for_distribution, max_cycle_moment, None)?) } TokenDistributionRecipient::Identity(identifier) => { - TokenDistributionResolvedRecipient::Identity(identifier) + (TokenDistributionResolvedRecipient::Identity(identifier), perpetual_distribution + .distribution_type() + .rewards_in_interval:: Option>(start_from_moment_for_distribution, max_cycle_moment, None)?) } TokenDistributionRecipient::EvonodesByParticipation => { - let RewardDistributionMoment::EpochBasedMoment(epoch_index) = - start_from_moment_for_distribution - else { + let RewardDistributionMoment::EpochBasedMoment(epoch_index) = start_from_moment_for_distribution else { return Err(Error::Drive(DriveError::NotSupported( "evonodes by participation can only use epoch based distribution", ))); }; + let epochs: BTreeMap = drive .get_finalized_epoch_infos( epoch_index, @@ -363,19 +428,56 @@ impl TokenClaimTransitionActionV0 { transaction, platform_version, )?; - TokenDistributionResolvedRecipient::Evonode(owner_id) + + let rewards = perpetual_distribution + .distribution_type() + .rewards_in_interval( + start_from_moment_for_distribution, + max_cycle_moment, + Some(|epoch_index| { + epochs.get(&epoch_index).map(|epoch_info| RewardRatio { + numerator: epoch_info.block_proposers().get(&owner_id).copied().unwrap_or_default(), + denominator: epoch_info.total_blocks_in_epoch(), + }) + }), + )?; + + (TokenDistributionResolvedRecipient::Evonode(owner_id), rewards) } }; - let amount = perpetual_distribution - .distribution_type() - .rewards_in_interval(start_from_moment_for_distribution, block_info)?; + if amount == 0 { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action.into(), + vec![ConsensusError::StateError( + StateError::InvalidTokenClaimNoCurrentRewards( + InvalidTokenClaimNoCurrentRewards::new( + value.base().token_id(), + owner_id, + current_cycle_moment, + last_paid_moment, + ), + ), + )], + ), + fee_result, + )); + } ( amount, TokenDistributionInfo::Perpetual( - RewardDistributionMoment::TimeBasedMoment(0), - RewardDistributionMoment::TimeBasedMoment(0), + max_cycle_moment, recipient, ), ) diff --git a/packages/rs-drive/src/util/batch/drive_op_batch/token.rs b/packages/rs-drive/src/util/batch/drive_op_batch/token.rs index ce5aac2c7eb..b1e551d7d9b 100644 --- a/packages/rs-drive/src/util/batch/drive_op_batch/token.rs +++ b/packages/rs-drive/src/util/batch/drive_op_batch/token.rs @@ -54,24 +54,19 @@ pub enum TokenOperationType { TokenMarkPerpetualReleaseAsDistributed { /// The token id token_id: Identifier, - /// The owner of this operation, generally the person making the state transition - owner_id: Identifier, - /// The last release time, block or epoch - last_release_moment: RewardDistributionMoment, - /// The next known release time, block or epoch - next_release_moment: RewardDistributionMoment, - /// The recipient - recipient: TokenDistributionRecipient, + /// The recipient of this operation, generally the person making the claim state transition + recipient_id: Identifier, + /// The beginning of the current perpetual release cycle. + /// For example if we pay every 10 blocks, and we are on block 54, this would be 50. + cycle_start_moment: RewardDistributionMoment, }, /// Marks the pre-programmed release as distributed /// This removes the references in the queue TokenMarkPreProgrammedReleaseAsDistributed { /// The token id token_id: Identifier, - /// The owner of this operation, generally the person making the state transition - owner_id: Identifier, - /// The identity that had their pre-programmed release set - identity_id: Identifier, + /// The recipient of this operation, generally the person making the state transition + recipient_id: Identifier, /// The last release time, block or epoch release_time: TimestampMillis, }, @@ -261,35 +256,27 @@ impl DriveLowLevelOperationConverter for TokenOperationType { } TokenOperationType::TokenMarkPerpetualReleaseAsDistributed { token_id, - owner_id, - last_release_moment, - next_release_moment, - recipient, + recipient_id, + cycle_start_moment, } => { let batch_operations = drive.mark_perpetual_release_as_distributed_operations( token_id.to_buffer(), - owner_id.to_buffer(), - last_release_moment, - next_release_moment, - recipient, - block_info, + recipient_id.to_buffer(), + cycle_start_moment, estimated_costs_only_with_layer_info, - transaction, platform_version, )?; Ok(batch_operations) } TokenOperationType::TokenMarkPreProgrammedReleaseAsDistributed { token_id, - owner_id, - identity_id, + recipient_id, release_time, } => { let batch_operations = drive .mark_pre_programmed_release_as_distributed_operations( token_id.to_buffer(), - owner_id.to_buffer(), - identity_id.to_buffer(), + recipient_id.to_buffer(), release_time, block_info, estimated_costs_only_with_layer_info, diff --git a/packages/rs-drive/src/util/batch/grovedb_op_batch/mod.rs b/packages/rs-drive/src/util/batch/grovedb_op_batch/mod.rs index 4939acfd9a7..9d2339cc966 100644 --- a/packages/rs-drive/src/util/batch/grovedb_op_batch/mod.rs +++ b/packages/rs-drive/src/util/batch/grovedb_op_batch/mod.rs @@ -278,9 +278,6 @@ fn readable_key_info(known_path: KnownPath, key_info: &KeyInfo) -> (String, Opti tokens::paths::TOKEN_PERPETUAL_DISTRIBUTIONS_INFO_KEY => { (format!("PerpetualDistributionInfo({})", tokens::paths::TOKEN_PERPETUAL_DISTRIBUTIONS_INFO_KEY), None) } - tokens::paths::TOKEN_PERPETUAL_DISTRIBUTIONS_FIRST_EVENT_KEY => { - (format!("PerpetualDistributionFirstEvent({})", tokens::paths::TOKEN_PERPETUAL_DISTRIBUTIONS_FIRST_EVENT_KEY), None) - } tokens::paths::TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY => { (format!("PerpetualDistributionLastClaim({})", tokens::paths::TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY), None) } diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs index 0a94075e0a3..d6489b93774 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs @@ -33,6 +33,7 @@ pub struct DriveTokenFetchMethodVersions { pub token_total_aggregated_identity_balances: FeatureVersion, pub pre_programmed_distributions: FeatureVersion, pub perpetual_distribution_last_paid_time: FeatureVersion, + pub pre_programmed_distribution_last_paid_time: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs index 8ef1820b92d..e18655ffc3f 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs @@ -17,6 +17,7 @@ pub const DRIVE_TOKEN_METHOD_VERSIONS_V1: DriveTokenMethodVersions = DriveTokenM token_total_aggregated_identity_balances: 0, pre_programmed_distributions: 0, perpetual_distribution_last_paid_time: 0, + pre_programmed_distribution_last_paid_time: 0, }, prove: DriveTokenProveMethodVersions { identity_token_balance: 0, diff --git a/packages/rs-platform-version/src/version/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs index 9a11e69178a..3b8d73f2735 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -392,6 +392,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { retry_signing_expired_withdrawal_documents_per_block_limit: 1, max_withdrawal_amount: 50_000_000_000_000, max_contract_group_size: 256, + max_token_redemption_cycles: 128, }, consensus: ConsensusVersions { tenderdash_consensus_version: 0, diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index 4460d29d13a..67f3fb83718 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -10,4 +10,9 @@ pub struct SystemLimits { pub retry_signing_expired_withdrawal_documents_per_block_limit: u16, pub max_withdrawal_amount: u64, pub max_contract_group_size: u32, + // This the max redemption cycles we can process if we don't use a constant distribution + // For a constant perpetual distribution this is very cheap since it's just a multiplication + // For other distributions we much calculate at each cycle the rewards, so we don't want to + // do this that much + pub max_token_redemption_cycles: u32, } diff --git a/packages/rs-platform-version/src/version/system_limits/v1.rs b/packages/rs-platform-version/src/version/system_limits/v1.rs index b62f7bfcd0c..2379b62d6c6 100644 --- a/packages/rs-platform-version/src/version/system_limits/v1.rs +++ b/packages/rs-platform-version/src/version/system_limits/v1.rs @@ -9,4 +9,5 @@ pub const SYSTEM_LIMITS_V1: SystemLimits = SystemLimits { retry_signing_expired_withdrawal_documents_per_block_limit: 1, max_withdrawal_amount: 50_000_000_000_000, //500 Dash max_contract_group_size: 256, + max_token_redemption_cycles: 128, }; From b0bf2f3d00f887b0e6f65c08f745c50a3b9746e0 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Mar 2025 20:41:55 +0700 Subject: [PATCH 2/4] a lot more work --- .../src/block/finalized_epoch_info/getters.rs | 2 +- .../block/finalized_epoch_info/v0/getters.rs | 2 +- .../src/block/finalized_epoch_info/v0/mod.rs | 2 +- .../token_distribution_key.rs | 5 +- .../distribution_function/encode.rs | 30 +- .../distribution_function/evaluate.rs | 312 ++++++------- .../evaluate_interval.rs | 59 ++- .../distribution_function/mod.rs | 22 +- .../distribution_function/reward_ratio.rs | 2 +- .../distribution_function/validation.rs | 158 +++---- .../reward_distribution_moment/mod.rs | 416 +++++++++++++++++- .../reward_distribution_type/accessors.rs | 40 -- .../evaluate_interval.rs | 30 +- .../reward_distribution_type/mod.rs | 90 +--- .../v0/methods.rs | 20 +- packages/rs-dpp/src/errors/consensus/codes.rs | 2 +- .../src/errors/consensus/state/state_error.rs | 2 +- .../invalid_token_claim_no_current_rewards.rs | 2 +- .../invalid_token_claim_wrong_claimant.rs | 8 +- .../src/errors/consensus/state/token/mod.rs | 8 +- .../mod.rs | 2 +- .../v0/mod.rs | 2 +- .../v0/mod.rs | 2 - .../distribution/perpetual/block_based.rs | 63 ++- .../tests/token/distribution/perpetual/mod.rs | 1 - .../distribution/perpetual/time_based.rs | 341 ++++++++++---- .../token/distribution/pre_programmed.rs | 117 ++--- .../data_contract_create/state/v0/mod.rs | 1 - .../data_contract_update/state/v0/mod.rs | 1 - .../src/drive/initialization/v1/mod.rs | 10 +- .../add_perpetual_distribution/v0/mod.rs | 7 +- .../add_pre_programmed_distribution/v0/mod.rs | 13 +- .../drive/tokens/distribution/fetch/mod.rs | 2 +- .../mod.rs | 5 +- .../v0/mod.rs | 6 +- .../v0/mod.rs | 7 +- .../v0/mod.rs | 24 +- .../v0/mod.rs | 6 +- packages/rs-drive/src/drive/tokens/paths.rs | 2 +- .../batch/token/token_claim_transition.rs | 6 +- .../v0/transformer.rs | 136 ++++-- 41 files changed, 1260 insertions(+), 706 deletions(-) diff --git a/packages/rs-dpp/src/block/finalized_epoch_info/getters.rs b/packages/rs-dpp/src/block/finalized_epoch_info/getters.rs index e1913d0c6db..f34decfda1f 100644 --- a/packages/rs-dpp/src/block/finalized_epoch_info/getters.rs +++ b/packages/rs-dpp/src/block/finalized_epoch_info/getters.rs @@ -1,9 +1,9 @@ -use std::collections::BTreeMap; use crate::block::finalized_epoch_info::v0::getters::FinalizedEpochInfoGettersV0; use crate::block::finalized_epoch_info::FinalizedEpochInfo; use crate::fee::Credits; use crate::prelude::{BlockHeight, BlockHeightInterval, CoreBlockHeight, TimestampMillis}; use platform_value::Identifier; +use std::collections::BTreeMap; impl FinalizedEpochInfoGettersV0 for FinalizedEpochInfo { fn first_block_time(&self) -> TimestampMillis { diff --git a/packages/rs-dpp/src/block/finalized_epoch_info/v0/getters.rs b/packages/rs-dpp/src/block/finalized_epoch_info/v0/getters.rs index b5791a43f06..131b286dd81 100644 --- a/packages/rs-dpp/src/block/finalized_epoch_info/v0/getters.rs +++ b/packages/rs-dpp/src/block/finalized_epoch_info/v0/getters.rs @@ -1,8 +1,8 @@ -use std::collections::BTreeMap; use crate::block::finalized_epoch_info::v0::FinalizedEpochInfoV0; use crate::fee::Credits; use crate::prelude::{BlockHeight, BlockHeightInterval, CoreBlockHeight, TimestampMillis}; use platform_value::Identifier; +use std::collections::BTreeMap; /// Trait for accessing fields of `FinalizedEpochInfoV0`. pub trait FinalizedEpochInfoGettersV0 { diff --git a/packages/rs-dpp/src/block/finalized_epoch_info/v0/mod.rs b/packages/rs-dpp/src/block/finalized_epoch_info/v0/mod.rs index d9d39ef830e..f9fe0b89c74 100644 --- a/packages/rs-dpp/src/block/finalized_epoch_info/v0/mod.rs +++ b/packages/rs-dpp/src/block/finalized_epoch_info/v0/mod.rs @@ -1,11 +1,11 @@ pub mod getters; -use std::collections::BTreeMap; use crate::fee::Credits; use crate::prelude::{BlockHeight, BlockHeightInterval, CoreBlockHeight, TimestampMillis}; use bincode::{Decode, Encode}; use platform_value::Identifier; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; /// Finalized Epoch information #[derive(Clone, Debug, PartialEq, Encode, Decode, Serialize, Deserialize)] diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs b/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs index 14f02965bb1..d355b062634 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs @@ -51,10 +51,7 @@ pub enum TokenDistributionInfo { /// A perpetual token distribution with moment for distribution. /// The moment is the beginning of the perpetual distribution cycle /// Includes the last and next distribution times and the resolved recipient. - Perpetual( - RewardDistributionMoment, - TokenDistributionResolvedRecipient - ), + Perpetual(RewardDistributionMoment, TokenDistributionResolvedRecipient), } impl From for TokenDistributionTypeWithResolvedRecipient { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/encode.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/encode.rs index 6ccd23b14db..da38b539f0e 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/encode.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/encode.rs @@ -41,7 +41,7 @@ impl Encode for DistributionFunction { DistributionFunction::Linear { a, d, - start_moment: s, + start_step: s, starting_amount: b, min_value, max_value, @@ -60,7 +60,7 @@ impl Encode for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -82,7 +82,7 @@ impl Encode for DistributionFunction { m, n, o, - s, + start_moment: s, c, min_value, max_value, @@ -104,7 +104,7 @@ impl Encode for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -126,7 +126,7 @@ impl Encode for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -192,7 +192,7 @@ impl Decode for DistributionFunction { Ok(Self::Linear { a, d, - start_moment: s, + start_step: s, starting_amount: b, min_value, max_value, @@ -214,7 +214,7 @@ impl Decode for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -236,7 +236,7 @@ impl Decode for DistributionFunction { m, n, o, - s, + start_moment: s, c, min_value, max_value, @@ -258,7 +258,7 @@ impl Decode for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -280,7 +280,7 @@ impl Decode for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -338,7 +338,7 @@ impl<'de> BorrowDecode<'de> for DistributionFunction { Ok(Self::Linear { a, d, - start_moment: s, + start_step: s, starting_amount: b, min_value, max_value, @@ -360,7 +360,7 @@ impl<'de> BorrowDecode<'de> for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -382,7 +382,7 @@ impl<'de> BorrowDecode<'de> for DistributionFunction { m, n, o, - s, + start_moment: s, c, min_value, max_value, @@ -404,7 +404,7 @@ impl<'de> BorrowDecode<'de> for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -426,7 +426,7 @@ impl<'de> BorrowDecode<'de> for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs index cdf0e0270cf..d1f511e800e 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs @@ -6,11 +6,17 @@ impl DistributionFunction { /// Evaluates the distribution function at the given period `x`. /// /// If an optional start period (`s`) is not provided, it defaults to 0. + /// The contract registration step is the contract registration moment divided + /// by the step /// /// # Returns /// A `Result` with the computed token amount or a `ProtocolError` in case of a /// divide-by-zero, undefined operation (e.g. log of non-positive number), or overflow. - pub fn evaluate(&self, x: u64) -> Result { + pub fn evaluate( + &self, + contract_registration_step: u64, + x: u64, + ) -> Result { match self { DistributionFunction::FixedAmount { amount: n } => { // For fixed amount, simply return n. @@ -55,7 +61,7 @@ impl DistributionFunction { "StepDecreasingAmount: denominator is 0", )); } - let s_val = s.unwrap_or(0); + let s_val = s.unwrap_or(contract_registration_step); // Compute the number of steps passed. let steps = if x > s_val { (x - s_val) / (*step_count as u64) @@ -93,7 +99,7 @@ impl DistributionFunction { DistributionFunction::Linear { a, d, - start_moment: s, + start_step, starting_amount, min_value, max_value, @@ -104,7 +110,7 @@ impl DistributionFunction { )); } // Check that the value at x = 0 is within bounds. - let s_val = s.unwrap_or(0); + let s_val = start_step.unwrap_or(contract_registration_step); let diff = x.saturating_sub(s_val); let value = if *d == 1 { @@ -119,15 +125,21 @@ impl DistributionFunction { } else { return Err(ProtocolError::Overflow( "Linear function evaluation overflow on multiplication", - )) + )); } } } Some(mul) => { - let value = mul.checked_add(*starting_amount as i64).ok_or(ProtocolError::Overflow( - "Linear function evaluation overflow or negative", - ))?; - if value < 0 { 0 } else { value as u64 } + let value = mul.checked_add(*starting_amount as i64).ok_or( + ProtocolError::Overflow( + "Linear function evaluation overflow or negative", + ), + )?; + if value < 0 { + 0 + } else { + value as u64 + } } } } else { @@ -136,9 +148,13 @@ impl DistributionFunction { .ok_or(ProtocolError::Overflow( "Linear function evaluation overflow or negative", ))?; - if value < 0 { 0 } else { value as u64 } + if value < 0 { + 0 + } else { + value as u64 + } }; - + if let Some(min_value) = min_value { if value < *min_value { return Ok(*min_value); @@ -159,7 +175,7 @@ impl DistributionFunction { m, n, o, - s, + start_moment, b, min_value, max_value, @@ -174,7 +190,7 @@ impl DistributionFunction { "Polynomial function: exponent denominator n is 0", )); } - let s_val = s.unwrap_or(0); + let s_val = start_moment.unwrap_or(contract_registration_step); let exponent = (*m as f64) / (*n as f64); let diff = x as i128 - s_val as i128 + *o as i128; @@ -227,7 +243,7 @@ impl DistributionFunction { m, n, o, - s, + start_moment, c, min_value, max_value, @@ -242,7 +258,7 @@ impl DistributionFunction { "Exponential function: exponent denominator n is 0", )); } - let s_val = s.unwrap_or(0); + let s_val = start_moment.unwrap_or(contract_registration_step); let diff = x as i128 - s_val as i128 + *o as i128; if diff < -(u64::MAX as i128) { @@ -295,7 +311,7 @@ impl DistributionFunction { m, n, o, - s, + start_moment, b, min_value, max_value, @@ -308,7 +324,7 @@ impl DistributionFunction { if *n == 0 { return Err(ProtocolError::DivideByZero("Logarithmic function: n is 0")); } - let s_val = s.unwrap_or(0); + let s_val = start_moment.unwrap_or(contract_registration_step); let diff = x as i128 - s_val as i128 + *o as i128; if diff <= 0 { @@ -357,7 +373,7 @@ impl DistributionFunction { m, n, o, - s, + start_moment, b, min_value, max_value, @@ -380,7 +396,7 @@ impl DistributionFunction { } // Use the provided start period or default to 0. - let s_val = s.unwrap_or(0); + let s_val = start_moment.unwrap_or(contract_registration_step); // Compute the adjusted time difference: (x - s + o). // We use i128 to prevent overflow issues. @@ -461,9 +477,9 @@ mod tests { #[test] fn test_fixed_amount() { let distribution = DistributionFunction::FixedAmount { amount: 100 }; - assert_eq!(distribution.evaluate(0).unwrap(), 100); - assert_eq!(distribution.evaluate(50).unwrap(), 100); - assert_eq!(distribution.evaluate(1000).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 50).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 1000).unwrap(), 100); } #[test] @@ -474,12 +490,12 @@ mod tests { steps.insert(20, 25); let distribution = DistributionFunction::Stepwise(steps); - assert_eq!(distribution.evaluate(0).unwrap(), 100); - assert_eq!(distribution.evaluate(5).unwrap(), 100); - assert_eq!(distribution.evaluate(10).unwrap(), 50); - assert_eq!(distribution.evaluate(15).unwrap(), 50); - assert_eq!(distribution.evaluate(20).unwrap(), 25); - assert_eq!(distribution.evaluate(30).unwrap(), 25); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 5).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 10).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 15).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 20).unwrap(), 25); + assert_eq!(distribution.evaluate(0, 30).unwrap(), 25); } #[test] @@ -493,12 +509,12 @@ mod tests { min_value: Some(10), }; - assert_eq!(distribution.evaluate(0).unwrap(), 100); - assert_eq!(distribution.evaluate(9).unwrap(), 100); - assert_eq!(distribution.evaluate(10).unwrap(), 50); - assert_eq!(distribution.evaluate(20).unwrap(), 25); - assert_eq!(distribution.evaluate(30).unwrap(), 12); - assert_eq!(distribution.evaluate(40).unwrap(), 10); // Should not go below min_value + assert_eq!(distribution.evaluate(0, 0).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 9).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 10).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 20).unwrap(), 25); + assert_eq!(distribution.evaluate(0, 30).unwrap(), 12); + assert_eq!(distribution.evaluate(0, 40).unwrap(), 10); // Should not go below min_value } #[test] @@ -513,7 +529,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(10), + distribution.evaluate(0, 10), Err(ProtocolError::DivideByZero(_)) )); } @@ -525,7 +541,7 @@ mod tests { let distribution = DistributionFunction::Random { min: 10, max: 100 }; for x in 0..100 { - let result = distribution.evaluate(x).unwrap(); + let result = distribution.evaluate(0, x).unwrap(); assert!( (10..=100).contains(&result), "Random value {} is out of range for x = {}", @@ -540,7 +556,7 @@ mod tests { let distribution = DistributionFunction::Random { min: 42, max: 42 }; for x in 0..10 { - let result = distribution.evaluate(x).unwrap(); + let result = distribution.evaluate(0, x).unwrap(); assert_eq!( result, 42, "Expected fixed output 42, got {} for x = {}", @@ -553,7 +569,7 @@ mod tests { fn test_random_distribution_invalid_range() { let distribution = DistributionFunction::Random { min: 50, max: 40 }; - let result = distribution.evaluate(0); + let result = distribution.evaluate(0, 0); assert!( matches!(result, Err(ProtocolError::Overflow(_))), "Expected ProtocolError::Overflow but got {:?}", @@ -565,8 +581,8 @@ mod tests { fn test_random_distribution_deterministic_for_same_x() { let distribution = DistributionFunction::Random { min: 10, max: 100 }; - let value1 = distribution.evaluate(42).unwrap(); - let value2 = distribution.evaluate(42).unwrap(); + let value1 = distribution.evaluate(0, 42).unwrap(); + let value2 = distribution.evaluate(0, 42).unwrap(); assert_eq!( value1, value2, @@ -578,8 +594,8 @@ mod tests { fn test_random_distribution_varies_for_different_x() { let distribution = DistributionFunction::Random { min: 10, max: 100 }; - let value1 = distribution.evaluate(1).unwrap(); - let value2 = distribution.evaluate(2).unwrap(); + let value1 = distribution.evaluate(0, 1).unwrap(); + let value2 = distribution.evaluate(0, 2).unwrap(); assert_ne!( value1, value2, @@ -594,16 +610,16 @@ mod tests { let distribution = DistributionFunction::Linear { a: 10, d: 2, - start_moment: Some(0), + start_step: Some(0), starting_amount: 50, min_value: None, max_value: None, }; - assert_eq!(distribution.evaluate(0).unwrap(), 50); - assert_eq!(distribution.evaluate(2).unwrap(), 60); - assert_eq!(distribution.evaluate(4).unwrap(), 70); - assert_eq!(distribution.evaluate(6).unwrap(), 80); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 2).unwrap(), 60); + assert_eq!(distribution.evaluate(0, 4).unwrap(), 70); + assert_eq!(distribution.evaluate(0, 6).unwrap(), 80); } #[test] @@ -611,15 +627,15 @@ mod tests { let distribution = DistributionFunction::Linear { a: -5, d: 1, - start_moment: Some(0), + start_step: Some(0), starting_amount: 100, min_value: Some(10), max_value: None, }; - assert_eq!(distribution.evaluate(0).unwrap(), 100); - assert_eq!(distribution.evaluate(10).unwrap(), 50); - assert_eq!(distribution.evaluate(20).unwrap(), 10); // Should not go below min_value + assert_eq!(distribution.evaluate(0, 0).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 10).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 20).unwrap(), 10); // Should not go below min_value } #[test] @@ -627,14 +643,14 @@ mod tests { let distribution = DistributionFunction::Linear { a: 10, d: 0, // Invalid denominator - start_moment: Some(0), + start_step: Some(0), starting_amount: 50, min_value: None, max_value: None, }; assert!(matches!( - distribution.evaluate(10), + distribution.evaluate(0, 10), Err(ProtocolError::DivideByZero(_)) )); } @@ -649,16 +665,16 @@ mod tests { m: 2, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), b: 10, min_value: None, max_value: None, }; - assert_eq!(distribution.evaluate(0).unwrap(), 10); - assert_eq!(distribution.evaluate(2).unwrap(), 18); - assert_eq!(distribution.evaluate(3).unwrap(), 28); - assert_eq!(distribution.evaluate(4).unwrap(), 42); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 10); + assert_eq!(distribution.evaluate(0, 2).unwrap(), 18); + assert_eq!(distribution.evaluate(0, 3).unwrap(), 28); + assert_eq!(distribution.evaluate(0, 4).unwrap(), 42); } #[test] @@ -669,13 +685,13 @@ mod tests { m: 2, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), b: 10, min_value: None, max_value: None, }; - let result = distribution.evaluate(1); + let result = distribution.evaluate(0, 1); assert!( matches!(result, Err(ProtocolError::Overflow(_))), "Expected overflow but got {:?}", @@ -692,13 +708,13 @@ mod tests { m: 3, // exponent is 3/2 n: 2, o: 0, - s: Some(0), + start_moment: Some(0), b: 0, min_value: None, max_value: None, }; // (4 - 0 + 0)^(3/2) = 4^(3/2) = (sqrt(4))^3 = 2^3 = 8. - assert_eq!(distribution.evaluate(4).unwrap(), 8); + assert_eq!(distribution.evaluate(0, 4).unwrap(), 8); } // Test: Negative coefficient a (should flip the sign) @@ -710,13 +726,13 @@ mod tests { m: 2, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), b: 0, min_value: None, max_value: None, }; // f(x) = -1 * (x^2). For x = 3: -1 * (3^2) = -9. - assert_eq!(distribution.evaluate(3).unwrap(), 0); + assert_eq!(distribution.evaluate(0, 3).unwrap(), 0); } // Test: Non-zero shift parameter s (shifting the x coordinate) @@ -728,16 +744,16 @@ mod tests { m: 2, n: 1, o: 0, - s: Some(2), + start_moment: Some(2), b: 10, min_value: None, max_value: None, }; // f(x) = 2 * ((x - 2)^2) + 10. // At x = 2: (0)^2 = 0, f(2) = 10. - assert_eq!(distribution.evaluate(2).unwrap(), 10); + assert_eq!(distribution.evaluate(0, 2).unwrap(), 10); // At x = 3: (3 - 2)^2 = 1, f(3) = 2*1 + 10 = 12. - assert_eq!(distribution.evaluate(3).unwrap(), 12); + assert_eq!(distribution.evaluate(0, 3).unwrap(), 12); } // Test: Non-zero offset o (shifting the base of the power) @@ -749,14 +765,14 @@ mod tests { m: 2, n: 1, o: 3, - s: Some(0), + start_moment: Some(0), b: 10, min_value: None, max_value: None, }; // f(x) = 2 * ((x - 0 + 3)^2) + 10. // At x = 1: (1 + 3) = 4, 4^2 = 16, then 2*16 + 10 = 42. - assert_eq!(distribution.evaluate(1).unwrap(), 42); + assert_eq!(distribution.evaluate(0, 1).unwrap(), 42); } // Test: Constant function when m = 0 (should ignore x) @@ -768,14 +784,14 @@ mod tests { m: 0, // exponent 0 => (x-s+o)^0 = 1 (for any x where x-s+o ≠ 0) n: 1, o: 0, - s: Some(0), + start_moment: Some(0), b: 3, min_value: None, max_value: None, }; // f(x) = 5*1 + 3 = 8 for any x. for x in [0, 10, 100].iter() { - assert_eq!(distribution.evaluate(*x).unwrap(), 8); + assert_eq!(distribution.evaluate(0, *x).unwrap(), 8); } } @@ -788,13 +804,13 @@ mod tests { m: 1, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), b: 5, min_value: None, max_value: None, }; // f(x) = 3*x + 5. At x = 10, f(10) = 30 + 5 = 35. - assert_eq!(distribution.evaluate(10).unwrap(), 35); + assert_eq!(distribution.evaluate(0, 10).unwrap(), 35); } // Test: Cubic function (m = 3, n = 1) @@ -806,13 +822,13 @@ mod tests { m: 3, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), b: 0, min_value: None, max_value: None, }; // f(x) = x^3. At x = 4, f(4) = 64. - assert_eq!(distribution.evaluate(4).unwrap(), 64); + assert_eq!(distribution.evaluate(0, 4).unwrap(), 64); } // Test: Combination of non-zero offset and shift @@ -824,14 +840,14 @@ mod tests { m: 2, n: 1, o: 2, - s: Some(1), + start_moment: Some(1), b: 0, min_value: None, max_value: None, }; // f(x) = ( (x - 1 + 2)^2 ). // At x = 3: (3 - 1 + 2) = 4, and 4^2 = 16. - assert_eq!(distribution.evaluate(3).unwrap(), 16); + assert_eq!(distribution.evaluate(0, 3).unwrap(), 16); } } mod exp { @@ -844,14 +860,14 @@ mod tests { m: 1, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), c: 10, min_value: None, max_value: None, }; - assert_eq!(distribution.evaluate(0).unwrap(), 11); - assert!(distribution.evaluate(10).unwrap() > 20); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 11); + assert!(distribution.evaluate(0, 10).unwrap() > 20); } #[test] @@ -862,14 +878,14 @@ mod tests { m: 1, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), c: 10, min_value: None, max_value: None, }; assert!(matches!( - distribution.evaluate(10), + distribution.evaluate(0, 10), Err(ProtocolError::DivideByZero(_)) )); } @@ -882,15 +898,15 @@ mod tests { m: 1, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), c: 5, min_value: None, max_value: None, }; - assert_eq!(distribution.evaluate(0).unwrap(), 7); - assert_eq!(distribution.evaluate(5).unwrap(), 301); - assert_eq!(distribution.evaluate(10).unwrap(), 44057); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 7); + assert_eq!(distribution.evaluate(0, 5).unwrap(), 301); + assert_eq!(distribution.evaluate(0, 10).unwrap(), 44057); } #[test] @@ -901,15 +917,15 @@ mod tests { m: 1, n: 10, o: 0, - s: Some(0), + start_moment: Some(0), c: 0, min_value: None, max_value: None, }; - assert_eq!(distribution.evaluate(0).unwrap(), 0); - assert_eq!(distribution.evaluate(50).unwrap(), 14); - assert_eq!(distribution.evaluate(100).unwrap(), 2202); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 0); + assert_eq!(distribution.evaluate(0, 50).unwrap(), 14); + assert_eq!(distribution.evaluate(0, 100).unwrap(), 2202); } #[test] @@ -920,17 +936,17 @@ mod tests { m: 4, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), c: 0, min_value: None, max_value: Some(100000000), }; - assert_eq!(distribution.evaluate(0).unwrap(), 1); - assert_eq!(distribution.evaluate(2).unwrap(), 2980); - assert_eq!(distribution.evaluate(4).unwrap(), 8886110); - assert_eq!(distribution.evaluate(10).unwrap(), 100000000); - assert_eq!(distribution.evaluate(100000).unwrap(), 100000000); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 1); + assert_eq!(distribution.evaluate(0, 2).unwrap(), 2980); + assert_eq!(distribution.evaluate(0, 4).unwrap(), 8886110); + assert_eq!(distribution.evaluate(0, 10).unwrap(), 100000000); + assert_eq!(distribution.evaluate(0, 100000).unwrap(), 100000000); } #[test] @@ -941,15 +957,15 @@ mod tests { m: -1, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), c: 10, min_value: None, max_value: None, }; - assert_eq!(distribution.evaluate(0).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 - assert_eq!(distribution.evaluate(5).unwrap(), 10); - assert_eq!(distribution.evaluate(10000).unwrap(), 10); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 + assert_eq!(distribution.evaluate(0, 5).unwrap(), 10); + assert_eq!(distribution.evaluate(0, 10000).unwrap(), 10); } #[test] @@ -960,15 +976,15 @@ mod tests { m: -1, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(11), max_value: None, }; - assert_eq!(distribution.evaluate(0).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 - assert_eq!(distribution.evaluate(5).unwrap(), 11); - assert_eq!(distribution.evaluate(100).unwrap(), 11); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 + assert_eq!(distribution.evaluate(0, 5).unwrap(), 11); + assert_eq!(distribution.evaluate(0, 100).unwrap(), 11); } #[test] @@ -979,19 +995,19 @@ mod tests { m: 1, n: 2, o: 0, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(1), max_value: Some(11), // Set max at the starting value }; assert_eq!( - distribution.evaluate(0).unwrap(), + distribution.evaluate(0, 0).unwrap(), 11, "Function should start at the max value" ); assert_eq!( - distribution.evaluate(5).unwrap(), + distribution.evaluate(0, 5).unwrap(), 11, "Function should be clamped at max value" ); @@ -1005,13 +1021,13 @@ mod tests { m: 1, n: 10, o: 0, - s: Some(0), + start_moment: Some(0), c: 5, min_value: None, max_value: None, }; - let result = distribution.evaluate(100000); + let result = distribution.evaluate(0, 100000); assert!( matches!(result, Err(ProtocolError::Overflow(_))), "Expected overflow but got {:?}", @@ -1028,15 +1044,15 @@ mod tests { d: 1, m: 1, n: 1, - o: 1, // Offset ensures (x - s + o) > 0 - s: Some(1), // Start at x=1 to avoid log(0) + o: 1, // Offset ensures (x - s + o) > 0 + start_moment: Some(1), // Start at x=1 to avoid log(0) b: 5, min_value: None, max_value: None, }; - assert_eq!(distribution.evaluate(1).unwrap(), 5); - assert!(distribution.evaluate(10).unwrap() > 5); + assert_eq!(distribution.evaluate(0, 1).unwrap(), 5); + assert!(distribution.evaluate(0, 10).unwrap() > 5); } #[test] @@ -1047,14 +1063,14 @@ mod tests { m: 1, n: 1, o: 1, - s: Some(1), + start_moment: Some(1), b: 5, min_value: Some(7), // Minimum bound should be enforced max_value: Some(20), // Maximum bound should be enforced }; - assert_eq!(distribution.evaluate(1).unwrap(), 7); // Clamped to min_value - assert!(distribution.evaluate(10).unwrap() <= 20); // Should not exceed max_value + assert_eq!(distribution.evaluate(0, 1).unwrap(), 7); // Clamped to min_value + assert!(distribution.evaluate(0, 10).unwrap() <= 20); // Should not exceed max_value } #[test] @@ -1065,14 +1081,14 @@ mod tests { m: 1, n: 1, o: -1, // Invalid offset causing log(0) - s: Some(1), + start_moment: Some(1), b: 5, min_value: None, max_value: None, }; assert!(matches!( - distribution.evaluate(1), + distribution.evaluate(0, 1), Err(ProtocolError::Overflow(_)) )); } @@ -1085,13 +1101,13 @@ mod tests { m: 1, n: 1, o: 5, - s: Some(10), + start_moment: Some(10), b: 10, min_value: None, max_value: None, }; - let result = distribution.evaluate(100); + let result = distribution.evaluate(0, 100); assert!(result.is_ok()); assert!(result.unwrap() > 10); // Function should increase over time } @@ -1104,14 +1120,14 @@ mod tests { m: 1, n: 1, o: 1, - s: Some(5), + start_moment: Some(5), b: 5, min_value: None, max_value: None, }; assert!(matches!( - distribution.evaluate(10), + distribution.evaluate(0, 10), Err(ProtocolError::DivideByZero(_)) )); } @@ -1124,14 +1140,14 @@ mod tests { m: 1, n: 0, // Invalid: Division by zero in log denominator o: 1, - s: Some(5), + start_moment: Some(5), b: 5, min_value: None, max_value: None, }; assert!(matches!( - distribution.evaluate(10), + distribution.evaluate(0, 10), Err(ProtocolError::DivideByZero(_)) )); } @@ -1146,14 +1162,14 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: None, max_value: None, }; - assert!(distribution.evaluate(1).unwrap() > distribution.evaluate(5).unwrap()); - assert!(distribution.evaluate(5).unwrap() > distribution.evaluate(10).unwrap()); + assert!(distribution.evaluate(0, 1).unwrap() > distribution.evaluate(0, 5).unwrap()); + assert!(distribution.evaluate(0, 5).unwrap() > distribution.evaluate(0, 10).unwrap()); } #[test] @@ -1165,15 +1181,15 @@ mod tests { m: 1, n: 1000, o: 10, - s: Some(0), + start_moment: Some(0), b: 5, min_value: None, max_value: None, }; - let val1000 = distribution.evaluate(1000).unwrap(); - let val2000 = distribution.evaluate(2000).unwrap(); - let val3000 = distribution.evaluate(3000).unwrap(); + let val1000 = distribution.evaluate(0, 1000).unwrap(); + let val2000 = distribution.evaluate(0, 2000).unwrap(); + let val3000 = distribution.evaluate(0, 3000).unwrap(); assert!(val1000 < val2000, "Function should be increasing"); assert!(val2000 < val3000, "Function should be increasing"); @@ -1187,13 +1203,13 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: None, max_value: None, }; - assert_eq!(distribution.evaluate(1).unwrap(), 0); // Should be clamped to 0 + assert_eq!(distribution.evaluate(0, 1).unwrap(), 0); // Should be clamped to 0 } #[test] @@ -1204,13 +1220,13 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(7), max_value: None, }; - assert_eq!(distribution.evaluate(1000).unwrap(), 7); // Should be clamped to min_value + assert_eq!(distribution.evaluate(0, 1000).unwrap(), 7); // Should be clamped to min_value } #[test] @@ -1222,13 +1238,13 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: None, max_value: Some(20), }; - assert_eq!(distribution.evaluate(500).unwrap(), 20); // Should be clamped to max_value + assert_eq!(distribution.evaluate(0, 500).unwrap(), 20); // Should be clamped to max_value } #[test] @@ -1239,14 +1255,14 @@ mod tests { m: 1, n: 100, o: -1, - s: Some(1), + start_moment: Some(1), b: 5, min_value: None, max_value: None, }; assert!(matches!( - distribution.evaluate(1), + distribution.evaluate(0, 1), Err(ProtocolError::Overflow(_)) )); } @@ -1259,14 +1275,14 @@ mod tests { m: 1, n: 0, // Invalid: n must not be zero o: 1, - s: Some(5), + start_moment: Some(5), b: 5, min_value: None, max_value: None, }; assert!(matches!( - distribution.evaluate(10), + distribution.evaluate(0, 10), Err(ProtocolError::DivideByZero(_)) )); } @@ -1279,14 +1295,14 @@ mod tests { m: 1, n: 1, o: 1, - s: Some(5), + start_moment: Some(5), b: 5, min_value: None, max_value: None, }; assert!(matches!( - distribution.evaluate(10), + distribution.evaluate(0, 10), Err(ProtocolError::DivideByZero(_)) )); } @@ -1299,19 +1315,19 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(10), // Max value set at the starting point }; assert_eq!( - distribution.evaluate(0).unwrap(), + distribution.evaluate(0, 0).unwrap(), 1, "Function should start at the max value" ); assert_eq!( - distribution.evaluate(200).unwrap(), + distribution.evaluate(0, 200).unwrap(), 10, "Function should remain clamped at max value" ); @@ -1325,14 +1341,14 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(3), max_value: None, }; assert_eq!( - distribution.evaluate(1000).unwrap(), + distribution.evaluate(0, 1000).unwrap(), 3, "Function should remain clamped at min value" ); diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs index 3f838966167..edde12f0087 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs @@ -1,3 +1,4 @@ +use std::ops::Div; use crate::balances::credits::TokenAmount; use crate::block::epoch::EpochIndex; use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; @@ -26,8 +27,9 @@ impl DistributionFunction { /// - `Err(ProtocolError)` on mismatched types, zero steps, or overflow. pub fn evaluate_interval( &self, - start_not_included: RewardDistributionMoment, - end_included: RewardDistributionMoment, + distribution_start: RewardDistributionMoment, + interval_start_excluded: RewardDistributionMoment, + interval_end_included: RewardDistributionMoment, step: RewardDistributionMoment, get_epoch_reward_ratio: Option, ) -> Result @@ -35,7 +37,9 @@ impl DistributionFunction { F: Fn(EpochIndex) -> Option, { // Ensure moments are the same type. - if !(start_not_included.same_type(&step) && start_not_included.same_type(&end_included)) { + if !(interval_start_excluded.same_type(&step) + && interval_start_excluded.same_type(&interval_end_included)) + { return Err(ProtocolError::AddingDifferentTypes( "Mismatched RewardDistributionMoment types".into(), )); @@ -47,29 +51,39 @@ impl DistributionFunction { )); } - if end_included <= start_not_included { - return Ok(0); - } - - let first_point = (start_not_included + step)?; - - if end_included < first_point { + if interval_start_excluded >= interval_end_included { return Ok(0); } // Optimization for FixedAmount - if let DistributionFunction::FixedAmount { amount: fixed_amount } = self { - let steps_count = first_point.steps_till(&end_included, &step)?; + if let DistributionFunction::FixedAmount { + amount: fixed_amount, + } = self + { + let steps_count = + interval_start_excluded.steps_till(&interval_end_included, &step, false, true)?; return fixed_amount.checked_mul(steps_count).ok_or_else(|| { ProtocolError::Overflow("Overflow in FixedAmount evaluation".into()) }); } + // Let's say you have a step 10 going from 10 to 20, the first index would be 2 + // If we are at 10 + let first_step = ((interval_start_excluded / step)? + 1)?; + let last_step = (interval_end_included / step)?; + + if first_step > last_step { + return Ok(0); + } + + let distribution_start_step = distribution_start.div(step)?; + let mut total: u64 = 0; - let mut current_point = first_point; + let mut current_point = first_step; - while current_point <= end_included { - let base_amount = self.evaluate(current_point.to_u64())?; + while current_point <= last_step { + let base_amount = + self.evaluate(distribution_start_step.to_u64(), current_point.to_u64())?; let amount = if let ( RewardDistributionMoment::EpochBasedMoment(epoch_index), @@ -86,19 +100,20 @@ impl DistributionFunction { ) })? } else { - return Err(ProtocolError::MissingEpochInfo(format!("missing epoch info for epoch {}", epoch_index))); + return Err(ProtocolError::MissingEpochInfo(format!( + "missing epoch info for epoch {}", + epoch_index + ))); } } else { base_amount }; - total = total.checked_add(amount).ok_or_else(|| { - ProtocolError::Overflow( - "Overflow in token interval evaluation" - ) - })?; + total = total + .checked_add(amount) + .ok_or_else(|| ProtocolError::Overflow("Overflow in token interval evaluation"))?; - current_point = (current_point + step)?; + current_point = (current_point + 1)?; } Ok(total) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs index 3e00a81341c..2397d93f18a 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs @@ -6,8 +6,8 @@ use std::fmt; mod encode; mod evaluate; mod evaluate_interval; -mod validation; pub mod reward_ratio; +mod validation; pub const MAX_DISTRIBUTION_PARAM: u64 = 281_474_976_710_655; //u48::Max 2^48 - 1 @@ -236,7 +236,7 @@ pub enum DistributionFunction { Linear { a: i64, d: u64, - start_moment: Option, + start_step: Option, starting_amount: TokenAmount, min_value: Option, max_value: Option, @@ -321,7 +321,7 @@ pub enum DistributionFunction { m: i64, n: u64, o: i64, - s: Option, + start_moment: Option, b: TokenAmount, min_value: Option, max_value: Option, @@ -380,7 +380,7 @@ pub enum DistributionFunction { m: i64, n: u64, o: i64, - s: Option, + start_moment: Option, c: TokenAmount, min_value: Option, max_value: Option, @@ -449,7 +449,7 @@ pub enum DistributionFunction { m: u64, n: u64, o: i64, - s: Option, + start_moment: Option, b: TokenAmount, min_value: Option, max_value: Option, @@ -508,7 +508,7 @@ pub enum DistributionFunction { m: u64, n: u64, o: i64, - s: Option, + start_moment: Option, b: TokenAmount, min_value: Option, max_value: Option, @@ -563,7 +563,7 @@ impl fmt::Display for DistributionFunction { DistributionFunction::Linear { a, d, - start_moment: s, + start_step: s, starting_amount: b, min_value, max_value, @@ -589,7 +589,7 @@ impl fmt::Display for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -615,7 +615,7 @@ impl fmt::Display for DistributionFunction { m, n, o, - s, + start_moment: s, c, min_value, max_value, @@ -641,7 +641,7 @@ impl fmt::Display for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -667,7 +667,7 @@ impl fmt::Display for DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/reward_ratio.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/reward_ratio.rs index ce1574b2e7e..1d122cdad86 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/reward_ratio.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/reward_ratio.rs @@ -2,4 +2,4 @@ pub struct RewardRatio { pub numerator: u64, pub denominator: u64, -} \ No newline at end of file +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs index 3897d4dc4e5..6c287538533 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs @@ -4,7 +4,9 @@ use crate::consensus::basic::data_contract::{ InvalidTokenDistributionFunctionInvalidParameterError, InvalidTokenDistributionFunctionInvalidParameterTupleError, }; -use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::{DistributionFunction, MAX_DISTRIBUTION_PARAM, MAX_LINEAR_SLOPE_PARAM}; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::{ + DistributionFunction, MAX_DISTRIBUTION_PARAM, MAX_LINEAR_SLOPE_PARAM, +}; use crate::validation::SimpleConsensusValidationResult; use crate::ProtocolError; impl DistributionFunction { @@ -140,7 +142,7 @@ impl DistributionFunction { DistributionFunction::Linear { a, d, - start_moment: s, + start_step: s, starting_amount: b, min_value, max_value, @@ -218,12 +220,12 @@ impl DistributionFunction { let start_token_amount = DistributionFunction::Linear { a: *a, d: *d, - start_moment: Some(s.unwrap_or(start_moment)), + start_step: Some(s.unwrap_or(start_moment)), starting_amount: *b, min_value: *min_value, max_value: *max_value, } - .evaluate(start_moment)?; + .evaluate(0, start_moment)?; if *a > 0 { // we want to put in the max value to see if we are starting off at the max @@ -262,7 +264,7 @@ impl DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -349,12 +351,12 @@ impl DistributionFunction { m: *m, n: *n, o: *o, - s: Some(s.unwrap_or(start_moment)), + start_moment: Some(s.unwrap_or(start_moment)), b: *b, min_value: *min_value, max_value: *max_value, } - .evaluate(start_moment)?; + .evaluate(0, start_moment)?; // Now, based on the monotonicity implied by (*a) * (*m), // check for incoherence: @@ -391,7 +393,7 @@ impl DistributionFunction { m, n, o, - s, + start_moment: s, c, min_value, max_value, @@ -526,12 +528,12 @@ impl DistributionFunction { m: *m, n: *n, o: *o, - s: Some(s.unwrap_or(start_moment)), + start_moment: Some(s.unwrap_or(start_moment)), c: *c, min_value: *min_value, max_value: *max_value, } - .evaluate(start_moment)?; + .evaluate(0, start_moment)?; if *m > 0 { // we want to put in the max value to see if we are starting off at the max @@ -569,7 +571,7 @@ impl DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -691,12 +693,12 @@ impl DistributionFunction { m: *m, n: *n, o: *o, - s: Some(s.unwrap_or(start_moment)), + start_moment: Some(s.unwrap_or(start_moment)), b: *b, min_value: *min_value, max_value: *max_value, } - .evaluate(start_moment)?; + .evaluate(0, start_moment)?; if let Some(max) = max_value { if start_token_amount == *max { @@ -716,7 +718,7 @@ impl DistributionFunction { m, n, o, - s, + start_moment: s, b, min_value, max_value, @@ -830,12 +832,12 @@ impl DistributionFunction { m: *m, n: *n, o: *o, - s: Some(start_s), + start_moment: Some(start_s), b: *b, min_value: *min_value, max_value: *max_value, } - .evaluate(start_moment)?; + .evaluate(0, start_moment)?; // Determine the function's monotonicity. // For InvertedLogarithmic, f'(x) = -a / (d * (x - s + o)). @@ -1015,7 +1017,7 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - start_moment: Some(3800), + start_step: Some(3800), starting_amount: 100, min_value: Some(50), max_value: Some(150), @@ -1038,7 +1040,7 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 0, - start_moment: Some(0), + start_step: Some(0), starting_amount: 100, min_value: Some(50), max_value: Some(150), @@ -1055,7 +1057,7 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - start_moment: Some(MAX_DISTRIBUTION_PARAM + 1), + start_step: Some(MAX_DISTRIBUTION_PARAM + 1), starting_amount: 100, min_value: Some(50), max_value: Some(150), @@ -1072,7 +1074,7 @@ mod tests { let dist = DistributionFunction::Linear { a: 0, // Invalid: a cannot be zero d: 10, - start_moment: Some(0), + start_step: Some(0), starting_amount: 100, min_value: Some(50), max_value: Some(150), @@ -1092,7 +1094,7 @@ mod tests { let dist = DistributionFunction::Linear { a: MAX_DISTRIBUTION_PARAM as i64 + 1, // Invalid: a exceeds allowed range d: 10, - start_moment: Some(0), + start_step: Some(0), starting_amount: 100, min_value: Some(50), max_value: Some(150), @@ -1112,7 +1114,7 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - start_moment: Some(0), + start_step: Some(0), starting_amount: 100, min_value: Some(200), // Invalid: min > max max_value: Some(150), @@ -1132,7 +1134,7 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - start_moment: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: s exceeds allowed range + start_step: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: s exceeds allowed range starting_amount: 100, min_value: Some(50), max_value: Some(150), @@ -1152,7 +1154,7 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - start_moment: Some(0), + start_step: Some(0), starting_amount: 100, min_value: Some(50), max_value: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: max_value exceeds max allowed range @@ -1172,7 +1174,7 @@ mod tests { let dist = DistributionFunction::Linear { a: 1, d: 10, - start_moment: Some(0), + start_step: Some(0), starting_amount: 150, // Starts at max value min_value: Some(50), max_value: Some(150), @@ -1192,7 +1194,7 @@ mod tests { let dist = DistributionFunction::Linear { a: -1, // Negative slope (decreasing function) d: 10, - start_moment: Some(0), + start_step: Some(0), starting_amount: 50, // Starts at min value min_value: Some(50), max_value: Some(150), @@ -1212,7 +1214,7 @@ mod tests { let dist = DistributionFunction::Linear { a: -5, // Valid decreasing function d: 10, - start_moment: Some(START_MOMENT), + start_step: Some(START_MOMENT), starting_amount: 200, min_value: Some(50), max_value: Some(250), @@ -1242,7 +1244,7 @@ mod tests { let dist = DistributionFunction::Linear { a: -3, d: 5, - start_moment: Some(START_MOMENT), + start_step: Some(START_MOMENT), starting_amount: 100, min_value: Some(10), // Valid min boundary max_value: Some(150), @@ -1259,7 +1261,7 @@ mod tests { let dist = DistributionFunction::Linear { a: 3, d: 5, - start_moment: Some(START_MOMENT), + start_step: Some(START_MOMENT), starting_amount: 50, min_value: Some(10), max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary @@ -1282,7 +1284,7 @@ mod tests { m: 2, n: 3, o: 0, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(80), @@ -1308,7 +1310,7 @@ mod tests { m: 2, n: 3, o: 0, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(50), @@ -1329,7 +1331,7 @@ mod tests { m: 2, n: 0, // Invalid: n == 0 o: 0, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(50), @@ -1350,7 +1352,7 @@ mod tests { m: 2, n: 3, o: 0, - s: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: s too large + start_moment: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: s too large b: 5, min_value: Some(1), max_value: Some(50), @@ -1371,7 +1373,7 @@ mod tests { m: 2, n: 3, o: MAX_DISTRIBUTION_PARAM as i64 + 1, // Invalid: o too high - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(50), @@ -1392,7 +1394,7 @@ mod tests { m: 2, n: 3, o: -(MAX_DISTRIBUTION_PARAM as i64) - 1, // Invalid: o too low - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(50), @@ -1413,7 +1415,7 @@ mod tests { m: 2, n: 3, o: 0, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: max_value too high @@ -1434,7 +1436,7 @@ mod tests { m: 2, n: 3, o: 0, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(60), // min_value > max_value max_value: Some(50), @@ -1457,7 +1459,7 @@ mod tests { m: 2, // positive n: 1, o: 0, - s: Some(0), + start_moment: Some(0), b: 100, // f(0) = 100 min_value: Some(1), max_value: Some(100), // Starting at max_value @@ -1480,7 +1482,7 @@ mod tests { m: 2, // positive => a*m is negative (decreasing) n: 1, o: 0, - s: Some(0), + start_moment: Some(0), b: 50, // f(0) = 50 min_value: Some(50), // Starting at min_value max_value: Some(100), @@ -1501,7 +1503,7 @@ mod tests { m: 2, n: 1, o: 0, - s: Some(0), + start_moment: Some(0), b: 20, min_value: None, max_value: None, @@ -1525,12 +1527,12 @@ mod tests { m: 3, n: 2, o: 0, - s: Some(0), + start_moment: Some(0), b: 0, min_value: Some(0), max_value: Some(100), }; - let eval_result = dist.evaluate(4); + let eval_result = dist.evaluate(0, 4); assert_eq!( eval_result.unwrap(), 8, @@ -1556,7 +1558,7 @@ mod tests { m: 1, n: 2, o: -3999, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(1), max_value: Some(1000000), @@ -1581,7 +1583,7 @@ mod tests { m: 1, n: 0, o: 1, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(1), max_value: Some(100), @@ -1601,7 +1603,7 @@ mod tests { m: 0, // Invalid: `m` should not be zero n: 2, o: 1, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(1), max_value: Some(100), @@ -1624,7 +1626,7 @@ mod tests { m: 1, n: 2, o: 1, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(1), max_value: Some(100), @@ -1647,7 +1649,7 @@ mod tests { m: 1, // `m > 0`, so `max_value` must be set n: 2, o: 1, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(1), max_value: None, // Invalid: max_value must be set @@ -1670,7 +1672,7 @@ mod tests { m: 1, n: 2, o: MAX_DISTRIBUTION_PARAM as i64 + 1, // Invalid: `o` exceeds allowed range - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(1), max_value: Some(100), @@ -1693,7 +1695,7 @@ mod tests { m: -1, n: 2, o: 1, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(50), // Invalid: min > max max_value: Some(30), @@ -1716,7 +1718,7 @@ mod tests { m: -2, // Valid: Decay function (exponential decrease) n: 4, o: 2, - s: Some(START_MOMENT), + start_moment: Some(START_MOMENT), c: 8, min_value: Some(2), max_value: Some(50), @@ -1736,7 +1738,7 @@ mod tests { m: 2, n: 4, o: 1, - s: Some(START_MOMENT), + start_moment: Some(START_MOMENT), c: 8, min_value: Some(2), max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max @@ -1756,7 +1758,7 @@ mod tests { m: 1, n: 1, o: 1, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM), @@ -1779,7 +1781,7 @@ mod tests { m: 2, // Increasing n: 1, o: 0, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(1), max_value: Some(1000), // Small `max_value` @@ -1802,7 +1804,7 @@ mod tests { m: -3, // Decreasing n: 2, o: 0, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(10), // Function starts at `min_value` max_value: Some(1000), @@ -1825,7 +1827,7 @@ mod tests { m: 3, // Increasing n: 2, o: 1, - s: Some(0), + start_moment: Some(0), c: 5, min_value: Some(1), max_value: None, // Should fail @@ -1848,7 +1850,7 @@ mod tests { m: 1, n: 1, o: i64::MAX / 2, // Large `o` - s: Some(0), + start_moment: Some(0), c: 5, min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM), @@ -1871,7 +1873,7 @@ mod tests { m: -2, // Decreasing n: 2, o: 0, - s: Some(0), + start_moment: Some(0), c: 10, min_value: Some(10), max_value: Some(100), @@ -1894,7 +1896,7 @@ mod tests { m: 1, // Small positive `m` n: 10, o: -3, - s: Some(0), + start_moment: Some(0), c: 5, min_value: Some(10), max_value: Some(1000), @@ -1925,7 +1927,7 @@ mod tests { m: -1, // Small negative `m` n: 4, o: 2, - s: Some(START_MOMENT), + start_moment: Some(START_MOMENT), c: 8, min_value: Some(5), max_value: Some(100), @@ -1945,7 +1947,7 @@ mod tests { m: -2, // Decreasing n: 3, o: 5, // Shift start - s: Some(START_MOMENT), + start_moment: Some(START_MOMENT), c: 10, min_value: Some(5), max_value: Some(100), @@ -1967,7 +1969,7 @@ mod tests { m: 1, n: 2, o: 1, - s: None, + start_moment: None, b: 10, min_value: Some(1), max_value: Some(100), @@ -1987,7 +1989,7 @@ mod tests { m: 1, n: 2, o: 1, - s: Some(0), + start_moment: Some(0), b: 10, min_value: Some(1), max_value: Some(100), @@ -2010,7 +2012,7 @@ mod tests { m: 1, n: 0, // Invalid: Division by zero in log denominator o: 1, - s: Some(0), + start_moment: Some(0), b: 10, min_value: Some(1), max_value: Some(100), @@ -2033,7 +2035,7 @@ mod tests { m: 1, n: 2, o: -5, // Causes (x - s + o) to be <= 0 - s: Some(START_MOMENT), + start_moment: Some(START_MOMENT), b: 10, min_value: Some(1), max_value: Some(100), @@ -2056,7 +2058,7 @@ mod tests { m: 1, n: 2, o: 1, - s: Some(0), + start_moment: Some(0), b: 10, min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: max_value too large @@ -2079,7 +2081,7 @@ mod tests { m: 1, n: 2, o: 1, - s: Some(0), + start_moment: Some(0), b: 10, min_value: Some(50), // Invalid: min > max max_value: Some(30), @@ -2102,7 +2104,7 @@ mod tests { m: 2, n: 4, o: 3, // Offset ensures (x - s + o) > 0 - s: Some(START_MOMENT - 2), + start_moment: Some(START_MOMENT - 2), b: 8, min_value: Some(2), max_value: Some(50), @@ -2122,7 +2124,7 @@ mod tests { m: 2, n: 4, o: 1, - s: Some(START_MOMENT), + start_moment: Some(START_MOMENT), b: 8, min_value: Some(2), max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max @@ -2144,7 +2146,7 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(50), @@ -2167,7 +2169,7 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(50), @@ -2187,7 +2189,7 @@ mod tests { m: 1, n: 0, // Invalid: n = 0 causes division by zero in log argument o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(50), @@ -2207,7 +2209,7 @@ mod tests { m: 0, // Invalid: m = 0 causes invalid log argument n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(50), @@ -2227,7 +2229,7 @@ mod tests { m: 1, n: 100, o: -10, // Causes log argument to be non-positive - s: Some(START_MOMENT), + start_moment: Some(START_MOMENT), b: 5, min_value: Some(1), max_value: Some(50), @@ -2247,7 +2249,7 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: s exceeds max + start_moment: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: s exceeds max b: 5, min_value: Some(1), max_value: Some(50), @@ -2267,7 +2269,7 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(60), // Invalid: min > max max_value: Some(50), @@ -2287,7 +2289,7 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 5, min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary @@ -2307,7 +2309,7 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 50, // Starts at max_value min_value: Some(1), max_value: Some(50), // Function already at max @@ -2327,7 +2329,7 @@ mod tests { m: 1, n: 100, o: 1, - s: Some(0), + start_moment: Some(0), b: 1, // Starts at min_value min_value: Some(1), max_value: Some(50), // Function already at min diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs index a3e1f6df488..a6dd169ed6c 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs @@ -3,7 +3,7 @@ use crate::prelude::{BlockHeight, TimestampMillis}; use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; use std::fmt; -use std::ops::Add; +use std::ops::{Add, Div}; use crate::block::block_info::BlockInfo; use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; use crate::ProtocolError; @@ -41,6 +41,70 @@ impl RewardDistributionMoment { } } + /// Computes the cycle start for the given moment, aligned with the `step` boundary. + /// + /// "Cycle start" is defined here as the greatest multiple of `step` not greater than `self`. + /// + /// # Parameters + /// - `step`: The step interval (must be of the same variant, non-zero). + /// + /// # Returns + /// - `Ok(RewardDistributionMoment)`: The moment snapped down to the nearest multiple of `step`. + /// - `Err(ProtocolError)`: If `step` is zero or if the types are mismatched. + pub fn cycle_start( + &self, + step: RewardDistributionMoment, + ) -> Result { + match (self, step) { + ( + RewardDistributionMoment::BlockBasedMoment(start), + RewardDistributionMoment::BlockBasedMoment(step_size), + ) => { + if step_size == 0 { + return Err(ProtocolError::InvalidDistributionStep( + "Step value cannot be zero", + )); + } + // Greatest multiple of step_size <= start + let remainder = start % step_size; + let cycle_start = start.saturating_sub(remainder); + Ok(RewardDistributionMoment::BlockBasedMoment(cycle_start)) + } + ( + RewardDistributionMoment::TimeBasedMoment(timestamp), + RewardDistributionMoment::TimeBasedMoment(step_size), + ) => { + if step_size == 0 { + return Err(ProtocolError::InvalidDistributionStep( + "Step value cannot be zero", + )); + } + // Greatest multiple of step_size <= timestamp + let remainder = timestamp % step_size; + let cycle_start = timestamp.saturating_sub(remainder); + Ok(RewardDistributionMoment::TimeBasedMoment(cycle_start)) + } + ( + RewardDistributionMoment::EpochBasedMoment(epoch), + RewardDistributionMoment::EpochBasedMoment(step_size), + ) => { + if step_size == 0 { + return Err(ProtocolError::InvalidDistributionStep( + "Step value cannot be zero", + )); + } + // Greatest multiple of step_size <= epoch + let remainder = epoch % step_size; + let cycle_start = epoch.saturating_sub(remainder); + Ok(RewardDistributionMoment::EpochBasedMoment(cycle_start)) + } + // Fallback for completeness—should not occur because we already did a type check + _ => Err(ProtocolError::AddingDifferentTypes( + "Cannot compute cycle_start with mismatched types".into(), + )), + } + } + /// Calculates the number of steps from `self` to `other`, using `step` as the increment. /// /// This function computes how many `step` intervals are needed to go from `self` @@ -50,18 +114,22 @@ impl RewardDistributionMoment { /// /// - `other`: The target moment. /// - `step`: The step interval. + /// - `start_included`: Whether the starting boundary is included. + /// - `end_included`: Whether the ending boundary is included. /// /// # Returns /// /// - `Ok(u64)`: The number of steps needed. /// - `Err(ProtocolError)`: If `step` is zero or types are mismatched. - pub fn steps_till(&self, other: &Self, step: &Self) -> Result { - if !self.same_type(other) || !self.same_type(step) { - return Err(ProtocolError::AddingDifferentTypes( - "Cannot compute steps between different RewardDistributionMoment types".to_string(), - )); - } - + pub fn steps_till( + &self, + other: &Self, + step: &Self, + start_included: bool, + end_included: bool, + ) -> Result { + // Depending on the variant, calculate the needed steps the same way, + // but adjust for inclusivity at the end. match (self, other, step) { ( RewardDistributionMoment::BlockBasedMoment(start), @@ -73,28 +141,68 @@ impl RewardDistributionMoment { RewardDistributionMoment::TimeBasedMoment(end), RewardDistributionMoment::TimeBasedMoment(step_size), ) => { + // If start >= end, by spec we return 0 + if *start >= *end { + return Ok(0); + } + if *step_size == 0 { return Err(ProtocolError::InvalidDistributionStep( "Step value cannot be zero", )); } - let start_index = *start / step_size; - let end_index = *end / step_size; - Ok(end_index.saturating_sub(start_index)) + + // Convert to "indexes" + let start_index = start / step_size; + let end_index = end / step_size; + + // Base count is the difference of those indexes + let mut steps = end_index.saturating_sub(start_index); + + // Adjust if we're *not* including the start and the start is exactly on a boundary + if !start_included && (start % step_size == 0) { + steps = steps.saturating_sub(1); + } + + // Adjust if we *are* including the end and the end is exactly on a boundary + if end_included && (end % step_size == 0) { + steps = steps.saturating_add(1); + } + + Ok(steps) } ( RewardDistributionMoment::EpochBasedMoment(start), RewardDistributionMoment::EpochBasedMoment(end), RewardDistributionMoment::EpochBasedMoment(step_size), ) => { + // If start >= end, by spec we return 0 + if *start >= *end { + return Ok(0); + } + if *step_size == 0 { return Err(ProtocolError::InvalidDistributionStep( "Step value cannot be zero", )); } - let start_index = *start / step_size; - let end_index = *end / step_size; - Ok(end_index.saturating_sub(start_index) as u64) + + let start_index = *start / *step_size; + let end_index = *end / *step_size; + + let mut steps = end_index.saturating_sub(start_index) as u64; + + // Adjust if not including start but it's on a boundary + if !start_included && (start % step_size == 0) { + steps = steps.saturating_sub(1); + } + + // Adjust if end is included and exactly on a boundary + if end_included && (end % step_size == 0) { + steps = steps.saturating_add(1); + } + + Ok(steps) } _ => Err(ProtocolError::AddingDifferentTypes( "Cannot compute steps with mismatched types".to_string(), @@ -111,6 +219,37 @@ impl From for u64 { moment.to_u64() } } +impl Add for RewardDistributionMoment { + type Output = Result; + + fn add(self, rhs: u64) -> Self::Output { + match self { + RewardDistributionMoment::BlockBasedMoment(a) => a + .checked_add(rhs) + .map(RewardDistributionMoment::BlockBasedMoment) + .ok_or(ProtocolError::Overflow("Block height addition overflow")), + + RewardDistributionMoment::TimeBasedMoment(a) => a + .checked_add(rhs) + .map(RewardDistributionMoment::TimeBasedMoment) + .ok_or(ProtocolError::Overflow("Timestamp addition overflow")), + + RewardDistributionMoment::EpochBasedMoment(a) => { + // Ensure `rhs` fits within `u16` before performing addition + if rhs > u16::MAX as u64 { + return Err(ProtocolError::Overflow( + "Epoch index addition overflow: value exceeds u16 max", + )); + } + + a.checked_add(rhs as u16) + .map(RewardDistributionMoment::EpochBasedMoment) + .ok_or(ProtocolError::Overflow("Epoch index addition overflow")) + } + } + } +} + impl Add for RewardDistributionMoment { type Output = Result; @@ -144,6 +283,81 @@ impl Add for RewardDistributionMoment { } } +impl Div for RewardDistributionMoment { + type Output = Result; + + fn div(self, rhs: u64) -> Self::Output { + if rhs == 0 { + return Err(ProtocolError::DivideByZero( + "Cannot divide RewardDistributionMoment by zero".into(), + )); + } + + match self { + RewardDistributionMoment::BlockBasedMoment(a) => { + Ok(RewardDistributionMoment::BlockBasedMoment(a / rhs)) + } + RewardDistributionMoment::TimeBasedMoment(a) => { + Ok(RewardDistributionMoment::TimeBasedMoment(a / rhs)) + } + RewardDistributionMoment::EpochBasedMoment(a) => { + // Ensure `rhs` fits within `u16` before performing addition + if rhs > u16::MAX as u64 { + return Err(ProtocolError::Overflow( + "Epoch index addition overflow: value exceeds u16 max", + )); + } + Ok(RewardDistributionMoment::EpochBasedMoment(a / rhs as u16)) + } + } + } +} + +impl Div for RewardDistributionMoment { + type Output = Result; + + fn div(self, rhs: Self) -> Self::Output { + match (self, rhs) { + ( + RewardDistributionMoment::BlockBasedMoment(a), + RewardDistributionMoment::BlockBasedMoment(b), + ) => { + if b == 0 { + return Err(ProtocolError::DivideByZero( + "Cannot divide by zero block height".into(), + )); + } + Ok(RewardDistributionMoment::BlockBasedMoment(a / b)) + } + ( + RewardDistributionMoment::TimeBasedMoment(a), + RewardDistributionMoment::TimeBasedMoment(b), + ) => { + if b == 0 { + return Err(ProtocolError::DivideByZero( + "Cannot divide by zero timestamp".into(), + )); + } + Ok(RewardDistributionMoment::TimeBasedMoment(a / b)) + } + ( + RewardDistributionMoment::EpochBasedMoment(a), + RewardDistributionMoment::EpochBasedMoment(b), + ) => { + if b == 0 { + return Err(ProtocolError::DivideByZero( + "Cannot divide by zero epoch index".into(), + )); + } + Ok(RewardDistributionMoment::EpochBasedMoment(a / b)) + } + _ => Err(ProtocolError::AddingDifferentTypes( + "Cannot divide different types of RewardDistributionMoment".to_string(), + )), + } + } +} + impl PartialEq<&u64> for RewardDistributionMoment { fn eq(&self, other: &&u64) -> bool { match self { @@ -275,3 +489,177 @@ impl fmt::Display for RewardDistributionMoment { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_type_mismatch_block_vs_time() { + let start = RewardDistributionMoment::BlockBasedMoment(10); + let end = RewardDistributionMoment::TimeBasedMoment(50); + let step = RewardDistributionMoment::BlockBasedMoment(5); + + // Mismatched type => Err(ProtocolError::AddingDifferentTypes) + let result = start.steps_till(&end, &step, true, true); + assert!( + matches!(result, Err(ProtocolError::AddingDifferentTypes(_))), + "Expected Err(AddingDifferentTypes), got: {:?}", + result + ); + } + + #[test] + fn test_type_mismatch_block_vs_epoch() { + let start = RewardDistributionMoment::BlockBasedMoment(10); + let end = RewardDistributionMoment::EpochBasedMoment(50); + let step = RewardDistributionMoment::BlockBasedMoment(5); + + let result = start.steps_till(&end, &step, true, true); + assert!( + matches!(result, Err(ProtocolError::AddingDifferentTypes(_))), + "Expected Err(AddingDifferentTypes), got: {:?}", + result + ); + } + + #[test] + fn test_zero_step_block_based() { + let start = RewardDistributionMoment::BlockBasedMoment(10); + let end = RewardDistributionMoment::BlockBasedMoment(50); + let step = RewardDistributionMoment::BlockBasedMoment(0); + + let result = start.steps_till(&end, &step, true, true); + assert!( + matches!(result, Err(ProtocolError::InvalidDistributionStep(_))), + "Expected Err(InvalidDistributionStep), got: {:?}", + result + ); + } + + #[test] + fn test_start_greater_than_end_returns_zero() { + let start = RewardDistributionMoment::TimeBasedMoment(100); + let end = RewardDistributionMoment::TimeBasedMoment(50); + let step = RewardDistributionMoment::TimeBasedMoment(10); + + // By spec, start >= end => 0 + let result = start.steps_till(&end, &step, true, true).unwrap(); + assert_eq!(result, 0, "Expected 0 steps when start >= end"); + } + + #[test] + fn test_block_basic_inclusive() { + let start = RewardDistributionMoment::BlockBasedMoment(0); + let end = RewardDistributionMoment::BlockBasedMoment(100); + let step = RewardDistributionMoment::BlockBasedMoment(10); + + // start_included = true, end_included = true + // The multiples in [0..=100] are 0,10,20,30,40,50,60,70,80,90,100 + // We expect 11 intervals if we are counting from 0 to 100 inclusively by 10s. + let result = start.steps_till(&end, &step, true, true).unwrap(); + assert_eq!( + result, 11, + "Expected 11 steps for [0..=100] in increments of 10" + ); + } + + #[test] + fn test_block_basic_exclusive() { + let start = RewardDistributionMoment::BlockBasedMoment(0); + let end = RewardDistributionMoment::BlockBasedMoment(100); + let step = RewardDistributionMoment::BlockBasedMoment(10); + + // start_included = false, end_included = false + // The multiples from 0..=100 by 10 are: 0,10,20,30,40,50,60,70,80,90,100 + // Excluding the start boundary (0) => skip that one + // Excluding the end boundary (100) => skip that one + // That leaves: 10,20,30,40,50,60,70,80,90 => 9 total + let result = start.steps_till(&end, &step, false, false).unwrap(); + assert_eq!( + result, 9, + "Expected 9 steps for (0..100) in increments of 10" + ); + } + + #[test] + fn test_block_mixed_inclusive() { + let start = RewardDistributionMoment::BlockBasedMoment(0); + let end = RewardDistributionMoment::BlockBasedMoment(100); + let step = RewardDistributionMoment::BlockBasedMoment(10); + + // start_included = false, end_included = true + // Multiples are 0,10,20,30,40,50,60,70,80,90,100 + // Excluding start=0 => skip that boundary + // Including end=100 => keep that boundary + // That leaves: 10,20,30,40,50,60,70,80,90,100 => 10 total + let result = start.steps_till(&end, &step, false, true).unwrap(); + assert_eq!(result, 10); + } + + #[test] + fn test_time_mixed_inclusive_with_non_multiple_bounds() { + // Start and end are not multiples of the step + let start = RewardDistributionMoment::TimeBasedMoment(3); + let end = RewardDistributionMoment::TimeBasedMoment(27); + let step = RewardDistributionMoment::TimeBasedMoment(5); + + // Multiples of 5 in the range 0..=27 are: 0,5,10,15,20,25 + // Our actual range is start=3 to end=27. + // - The multiples in [3..=27] are 5,10,15,20,25. + // + // start_included = true => but 3 is not a multiple, so that doesn't add a step + // end_included = true => 27 is not a multiple, so that doesn't add a step + // + // So we only have the steps at 5,10,15,20,25 => that's 5 steps. + let result = start.steps_till(&end, &step, true, true).unwrap(); + assert_eq!(result, 5); + } + + #[test] + fn test_epoch_inclusive_boundaries() { + // Now test an epoch-based moment + // Start=1, End=10, Step=1 + let start = RewardDistributionMoment::EpochBasedMoment(1); + let end = RewardDistributionMoment::EpochBasedMoment(10); + let step = RewardDistributionMoment::EpochBasedMoment(1); + + // start_included=true, end_included=true + // If we’re counting steps at each integer from 1..=10, that’s 10 steps + // Because each integer point is a boundary. + let result = start.steps_till(&end, &step, true, true).unwrap(); + assert_eq!(result, 10, "Expected 10 steps for [1..=10] with step=1"); + } + + #[test] + fn test_epoch_exclusive_boundaries() { + // Start=1, End=10, Step=1 + let start = RewardDistributionMoment::EpochBasedMoment(1); + let end = RewardDistributionMoment::EpochBasedMoment(10); + let step = RewardDistributionMoment::EpochBasedMoment(1); + + // start_included=false, end_included=false + // If we exclude the start boundary=1 and the end boundary=10, + // we are left with steps at 2,3,4,5,6,7,8,9 => total 8 + let result = start.steps_till(&end, &step, false, false).unwrap(); + assert_eq!(result, 8, "Expected 8 steps for (1..10) with step=1"); + } + + #[test] + fn test_epoch_start_between_boundaries() { + // Start=2, End=10, Step=3 + let start = RewardDistributionMoment::EpochBasedMoment(2); + let end = RewardDistributionMoment::EpochBasedMoment(10); + let step = RewardDistributionMoment::EpochBasedMoment(3); + + // Multiples of 3 up to 10 are: 0,3,6,9 (12 is beyond 10). + // In the range [2..10], the valid multiples are: 3,6,9 + // + // start_included = true => 2 is not a multiple, so it doesn’t add a boundary + // end_included = true => 10 is not a multiple, so it doesn’t add a boundary + // + // So steps are at 3,6,9 => 3 total + let result = start.steps_till(&end, &step, true, true).unwrap(); + assert_eq!(result, 3); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/accessors.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/accessors.rs index 38f7ae97b86..4a746324d53 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/accessors.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/accessors.rs @@ -32,44 +32,4 @@ impl RewardDistributionType { RewardDistributionType::EpochBasedDistribution { function, .. } => function, } } - - /// Returns the optional start moment of the distribution. - /// - /// # Returns - /// - `Some(RewardDistributionMoment::BlockBasedMoment)`, `Some(RewardDistributionMoment::TimeBasedMoment)`, - /// or `Some(RewardDistributionMoment::EpochBasedMoment)`, depending on the distribution type. - /// - `None` if the start moment is not set. - pub fn start(&self) -> Option { - match self { - RewardDistributionType::BlockBasedDistribution { start, .. } => { - start.map(RewardDistributionMoment::BlockBasedMoment) - } - RewardDistributionType::TimeBasedDistribution { start, .. } => { - start.map(RewardDistributionMoment::TimeBasedMoment) - } - RewardDistributionType::EpochBasedDistribution { start, .. } => { - start.map(RewardDistributionMoment::EpochBasedMoment) - } - } - } - - /// Returns the optional end moment of the distribution. - /// - /// # Returns - /// - `Some(RewardDistributionMoment::BlockBasedMoment)`, `Some(RewardDistributionMoment::TimeBasedMoment)`, - /// or `Some(RewardDistributionMoment::EpochBasedMoment)`, depending on the distribution type. - /// - `None` if the end moment is not set. - pub fn end(&self) -> Option { - match self { - RewardDistributionType::BlockBasedDistribution { end, .. } => { - end.map(RewardDistributionMoment::BlockBasedMoment) - } - RewardDistributionType::TimeBasedDistribution { end, .. } => { - end.map(RewardDistributionMoment::TimeBasedMoment) - } - RewardDistributionType::EpochBasedDistribution { end, .. } => { - end.map(RewardDistributionMoment::EpochBasedMoment) - } - } - } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs index 8a7eaa2872c..e5b805b6655 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs @@ -30,34 +30,18 @@ impl RewardDistributionType { /// pub fn rewards_in_interval( &self, - start_at_moment_excluded: RewardDistributionMoment, + distribution_start: RewardDistributionMoment, + start_at_moment: RewardDistributionMoment, current_moment_included: RewardDistributionMoment, get_epoch_reward_ratio: Option, ) -> Result where - F: Fn(EpochIndex) -> Option { - let mut effective_start = start_at_moment_excluded; - let mut effective_end = current_moment_included; - - if let Some(distribution_start) = self.start_at() { - if distribution_start > effective_start { - effective_start = distribution_start; - } - } - - if let Some(distribution_end) = self.end_at() { - if distribution_end < effective_end { - effective_end = distribution_end; - } - } - - if effective_end <= effective_start { - return Ok(0); - } - + F: Fn(EpochIndex) -> Option, + { self.function().evaluate_interval( - effective_start, - effective_end, + distribution_start, + start_at_moment, + current_moment_included, self.interval(), get_epoch_reward_ratio, ) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs index 9014d6ab7b4..276312a1504 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs @@ -1,9 +1,8 @@ mod accessors; mod evaluate_interval; -use crate::block::epoch::EpochIndex; use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; -use crate::prelude::{BlockHeight, BlockHeightInterval, DataContract, EpochInterval, TimestampMillis, TimestampMillisInterval}; +use crate::prelude::{BlockHeightInterval, DataContract, EpochInterval, TimestampMillisInterval}; use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; use std::fmt; @@ -15,34 +14,28 @@ use crate::ProtocolError; pub enum RewardDistributionType { /// An amount of tokens is emitted every n blocks. /// The start and end are included if set. - /// If start is not set then it will start at the height of the block when the data contract + /// If start is not set then it will start at the height of the block when the data contract /// is registered. BlockBasedDistribution { interval: BlockHeightInterval, function: DistributionFunction, - start: Option, - end: Option, }, /// An amount of tokens is emitted every amount of time given. /// The start and end are included if set. - /// If start is not set then it will start at the time of the block when the data contract + /// If start is not set then it will start at the time of the block when the data contract /// is registered. TimeBasedDistribution { interval: TimestampMillisInterval, function: DistributionFunction, - start: Option, - end: Option, }, /// An amount of tokens is emitted every amount of epochs. /// The start and end are included if set. - /// If start is not set then it will start at the epoch of the block when the data contract + /// If start is not set then it will start at the epoch of the block when the data contract /// is registered. A distribution would happen at the start of the following epoch, even if it /// is just 1 block later. EpochBasedDistribution { interval: EpochInterval, function: DistributionFunction, - start: Option, - end: Option, }, } @@ -143,36 +136,6 @@ impl RewardDistributionType { } } - /// Returns the start distribution moment as an Option. - pub fn start_at(&self) -> Option { - match self { - RewardDistributionType::BlockBasedDistribution { start, .. } => { - start.map(RewardDistributionMoment::BlockBasedMoment) - } - RewardDistributionType::TimeBasedDistribution { start, .. } => { - start.map(RewardDistributionMoment::TimeBasedMoment) - } - RewardDistributionType::EpochBasedDistribution { start, .. } => { - start.map(RewardDistributionMoment::EpochBasedMoment) - } - } - } - - /// Returns the end distribution moment as an Option. - pub fn end_at(&self) -> Option { - match self { - RewardDistributionType::BlockBasedDistribution { end, .. } => { - end.map(RewardDistributionMoment::BlockBasedMoment) - } - RewardDistributionType::TimeBasedDistribution { end, .. } => { - end.map(RewardDistributionMoment::TimeBasedMoment) - } - RewardDistributionType::EpochBasedDistribution { end, .. } => { - end.map(RewardDistributionMoment::EpochBasedMoment) - } - } - } - /// Determines the maximum cycle moment allowed based on the last paid moment, /// the current cycle moment, and the maximum allowed token redemption cycles. /// @@ -193,7 +156,7 @@ impl RewardDistributionType { current_cycle_moment: RewardDistributionMoment, max_cycles: u32, ) -> Result { - if matches!(self.function(), DistributionFunction::FixedAmount {..}) { + if matches!(self.function(), DistributionFunction::FixedAmount { .. }) { // This is much easier to calculate as it's always fixed, so we can have an unlimited amount of cycles return Ok(current_cycle_moment); } @@ -222,68 +185,37 @@ impl RewardDistributionType { ) => Ok(RewardDistributionMoment::EpochBasedMoment( (start + (step as u16).saturating_mul(max_cycles as u16)).min(current), )), - _ => Err(ProtocolError::CorruptedCodeExecution("Mismatch moment types".to_string())), + _ => Err(ProtocolError::CorruptedCodeExecution( + "Mismatch moment types".to_string(), + )), } } } impl fmt::Display for RewardDistributionType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - RewardDistributionType::BlockBasedDistribution { - interval, - function, - start, - end, - } => { + RewardDistributionType::BlockBasedDistribution { interval, function } => { write!( f, "BlockBasedDistribution: every {} blocks using {}", interval, function )?; - if let Some(start) = start { - write!(f, ", starting at block {}", start)?; - } - if let Some(end) = end { - write!(f, ", ending at block {}", end)?; - } Ok(()) } - RewardDistributionType::TimeBasedDistribution { - interval, - function, - start, - end, - } => { + RewardDistributionType::TimeBasedDistribution { interval, function } => { write!( f, "TimeBasedDistribution: every {} milliseconds using {}", interval, function )?; - if let Some(start) = start { - write!(f, ", starting at timestamp {}", start)?; - } - if let Some(end) = end { - write!(f, ", ending at timestamp {}", end)?; - } Ok(()) } - RewardDistributionType::EpochBasedDistribution { - interval, - function, - start, - end, - } => { + RewardDistributionType::EpochBasedDistribution { interval, function } => { write!( f, "EpochBasedDistribution: every {} epochs using {}", interval, function )?; - if let Some(start) = start { - write!(f, ", starting at epoch {}", start)?; - } - if let Some(end) = end { - write!(f, ", ending at epoch {}", end)?; - } Ok(()) } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/v0/methods.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/v0/methods.rs index 4a4e9651b18..be932ecc8e0 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/v0/methods.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/v0/methods.rs @@ -9,20 +9,20 @@ impl TokenPerpetualDistributionV0Methods for TokenPerpetualDistributionV0 { fn next_interval(&self, block_info: &BlockInfo) -> RewardDistributionMoment { match self.distribution_type { // If the distribution is based on block height, return the next height where emissions occur. - RewardDistributionType::BlockBasedDistribution { interval, .. } => { - BlockBasedMoment((block_info.height - block_info.height % interval).saturating_add(interval)) - } + RewardDistributionType::BlockBasedDistribution { interval, .. } => BlockBasedMoment( + (block_info.height - block_info.height % interval).saturating_add(interval), + ), // If the distribution is based on time, return the next timestamp in milliseconds. - RewardDistributionType::TimeBasedDistribution { interval, .. } => { - TimeBasedMoment((block_info.time_ms - block_info.time_ms % interval).saturating_add(interval)) - } + RewardDistributionType::TimeBasedDistribution { interval, .. } => TimeBasedMoment( + (block_info.time_ms - block_info.time_ms % interval).saturating_add(interval), + ), // If the distribution is based on epochs, return the next epoch index. - RewardDistributionType::EpochBasedDistribution { interval, .. } => { - EpochBasedMoment((block_info.epoch.index - block_info.epoch.index % interval) - .saturating_add(interval)) - } + RewardDistributionType::EpochBasedDistribution { interval, .. } => EpochBasedMoment( + (block_info.epoch.index - block_info.epoch.index % interval) + .saturating_add(interval), + ), } } diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index daa5cdca865..e768aee5803 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -304,7 +304,7 @@ impl ErrorWithCode for StateError { Self::InvalidTokenClaimPropertyMismatch(_) => 40715, Self::InvalidTokenClaimNoCurrentRewards(_) => 40716, Self::InvalidTokenClaimWrongClaimant(_) => 40717, - + // Group errors: 40800-40899 Self::IdentityNotMemberOfGroupError(_) => 40800, Self::GroupActionDoesNotExistError(_) => 40801, diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index b66b45d4651..39d696bd6b7 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -247,7 +247,7 @@ pub enum StateError { #[error(transparent)] InvalidTokenClaimWrongClaimant(InvalidTokenClaimWrongClaimant), - + #[error(transparent)] NewTokensDestinationIdentityDoesNotExistError(NewTokensDestinationIdentityDoesNotExistError), diff --git a/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_no_current_rewards.rs b/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_no_current_rewards.rs index dfa49070ac5..19e133903c0 100644 --- a/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_no_current_rewards.rs +++ b/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_no_current_rewards.rs @@ -74,4 +74,4 @@ impl From for ConsensusError { fn from(err: InvalidTokenClaimNoCurrentRewards) -> Self { Self::StateError(StateError::InvalidTokenClaimNoCurrentRewards(err)) } -} \ No newline at end of file +} diff --git a/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_wrong_claimant.rs b/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_wrong_claimant.rs index 251e3867b65..b4b4bf7abfa 100644 --- a/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_wrong_claimant.rs +++ b/packages/rs-dpp/src/errors/consensus/state/token/invalid_token_claim_wrong_claimant.rs @@ -1,6 +1,6 @@ -use crate::prelude::Identifier; use crate::consensus::state::state_error::StateError; use crate::consensus::ConsensusError; +use crate::prelude::Identifier; use crate::ProtocolError; use bincode::{Decode, Encode}; use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; @@ -11,7 +11,9 @@ use thiserror::Error; )] #[error( "Token claim error: expected claimant '{}' for token ID '{}', but received claim from '{}'", - expected_claimant_id, token_id, claimant_id + expected_claimant_id, + token_id, + claimant_id )] #[platform_serialize(unversioned)] pub struct InvalidTokenClaimWrongClaimant { @@ -55,4 +57,4 @@ impl From for ConsensusError { fn from(err: InvalidTokenClaimWrongClaimant) -> Self { Self::StateError(StateError::InvalidTokenClaimWrongClaimant(err)) } -} \ No newline at end of file +} diff --git a/packages/rs-dpp/src/errors/consensus/state/token/mod.rs b/packages/rs-dpp/src/errors/consensus/state/token/mod.rs index c37d4f85297..a7bd5f9dcdd 100644 --- a/packages/rs-dpp/src/errors/consensus/state/token/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/state/token/mod.rs @@ -3,7 +3,9 @@ mod identity_token_account_already_frozen_error; mod identity_token_account_frozen_error; mod identity_token_account_not_frozen_error; mod invalid_group_position_error; +mod invalid_token_claim_no_current_rewards; mod invalid_token_claim_property_mismatch; +mod invalid_token_claim_wrong_claimant; mod new_authorized_action_taker_group_does_not_exist_error; mod new_authorized_action_taker_identity_does_not_exist_error; mod new_authorized_action_taker_main_group_not_set_error; @@ -14,15 +16,15 @@ mod token_mint_past_max_supply_error; mod token_not_paused_error; mod token_setting_max_supply_to_less_than_current_supply_error; mod unauthorized_token_action_error; -mod invalid_token_claim_no_current_rewards; -mod invalid_token_claim_wrong_claimant; pub use identity_does_not_have_enough_token_balance_error::*; pub use identity_token_account_already_frozen_error::*; pub use identity_token_account_frozen_error::*; pub use identity_token_account_not_frozen_error::*; pub use invalid_group_position_error::*; +pub use invalid_token_claim_no_current_rewards::*; pub use invalid_token_claim_property_mismatch::*; +pub use invalid_token_claim_wrong_claimant::*; pub use new_authorized_action_taker_group_does_not_exist_error::*; pub use new_authorized_action_taker_identity_does_not_exist_error::*; pub use new_authorized_action_taker_main_group_not_set_error::*; @@ -33,5 +35,3 @@ pub use token_mint_past_max_supply_error::*; pub use token_not_paused_error::*; pub use token_setting_max_supply_to_less_than_current_supply_error::*; pub use unauthorized_token_action_error::*; -pub use invalid_token_claim_no_current_rewards::*; -pub use invalid_token_claim_wrong_claimant::*; diff --git a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/mod.rs index 971f7164296..2fba3a87f3d 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/mod.rs @@ -1,9 +1,9 @@ -use std::collections::BTreeMap; use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::unpaid_epoch::UnpaidEpoch; use crate::platform_types::platform::Platform; use dpp::block::pool_credits::StorageAndProcessingPoolCredits; +use std::collections::BTreeMap; use dpp::fee::Credits; use dpp::identifier::Identifier; diff --git a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/v0/mod.rs index 5ac0ca44cbb..5dcb08f2684 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/add_epoch_pool_to_proposers_payout_operations/v0/mod.rs @@ -1,6 +1,6 @@ -use std::collections::BTreeMap; use crate::error::execution::ExecutionError; use crate::error::Error; +use std::collections::BTreeMap; use crate::execution::types::unpaid_epoch::v0::{UnpaidEpochV0Getters, UnpaidEpochV0Methods}; use crate::execution::types::unpaid_epoch::UnpaidEpoch; diff --git a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/find_oldest_epoch_needing_payment/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/find_oldest_epoch_needing_payment/v0/mod.rs index 9ad2e8a604c..29cccf57b02 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/find_oldest_epoch_needing_payment/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/find_oldest_epoch_needing_payment/v0/mod.rs @@ -6,8 +6,6 @@ use dpp::block::epoch::Epoch; use dpp::fee::epoch::GENESIS_EPOCH_INDEX; use dpp::version::PlatformVersion; use drive::drive::credit_pools::epochs::start_block::StartBlockInfo; -use drive::error; -use drive::error::drive::DriveError; use drive::grovedb::TransactionArg; impl Platform { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index f19268b9bbb..52b53bb5ef5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs @@ -1,11 +1,13 @@ -use rand::prelude::StdRng; +use super::*; +use crate::execution::validation::state_transition::tests::{ + create_token_contract_with_owner_identity, setup_identity, +}; +use crate::test::helpers::setup::TestPlatformBuilder; use dpp::dash_to_credits; use dpp::data_contract::TokenConfiguration; use dpp::state_transition::batch_transition::BatchTransition; use platform_version::version::PlatformVersion; -use crate::execution::validation::state_transition::tests::{create_token_contract_with_owner_identity, setup_identity}; -use crate::test::helpers::setup::TestPlatformBuilder; -use super::*; +use rand::prelude::StdRng; mod perpetual_distribution_block { use dpp::block::epoch::Epoch; use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; @@ -42,8 +44,6 @@ mod perpetual_distribution_block { distribution_type: RewardDistributionType::BlockBasedDistribution { interval: 10, function: DistributionFunction::FixedAmount { amount: 50 }, - start: None, - end: None, }, distribution_recipient: TokenDistributionRecipient::ContractOwner, }, @@ -72,7 +72,7 @@ mod perpetual_distribution_block { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -140,7 +140,7 @@ mod perpetual_distribution_block { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -167,13 +167,12 @@ mod perpetual_distribution_block { .expect("expected to process state transition"); assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( - ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), - _ - )] - ); - + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); platform .drive @@ -211,7 +210,7 @@ mod perpetual_distribution_block { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -237,7 +236,6 @@ mod perpetual_distribution_block { ) .expect("expected to process state transition"); - assert_matches!( processing_result.execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution(_, _)] @@ -278,8 +276,7 @@ mod perpetual_distribution_block { let (identity, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); - let (identity_2, _, _) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity_2, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (contract, token_id) = create_token_contract_with_owner_identity( &mut platform, @@ -292,11 +289,11 @@ mod perpetual_distribution_block { distribution_type: RewardDistributionType::BlockBasedDistribution { interval: 10, function: DistributionFunction::FixedAmount { amount: 50 }, - start: None, - end: None, }, // we give to identity 2 - distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + distribution_recipient: TokenDistributionRecipient::Identity( + identity_2.id(), + ), }, ))); }), @@ -324,7 +321,7 @@ mod perpetual_distribution_block { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -351,12 +348,12 @@ mod perpetual_distribution_block { .expect("expected to process state transition"); assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( - ConsensusError::StateError(StateError::InvalidTokenClaimWrongClaimant(_)), - _ - )] - ); + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimWrongClaimant(_)), + _ + )] + ); platform .drive @@ -417,10 +414,10 @@ mod perpetual_distribution_block { distribution_type: RewardDistributionType::BlockBasedDistribution { interval: 10, function: DistributionFunction::FixedAmount { amount: 50 }, - start: None, - end: None, }, - distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + distribution_recipient: TokenDistributionRecipient::Identity( + identity_2.id(), + ), }, ))); }), @@ -447,7 +444,7 @@ mod perpetual_distribution_block { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/mod.rs index f4243241076..c3a359f9916 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/mod.rs @@ -1,4 +1,3 @@ use super::*; mod block_based; mod time_based; - diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs index 99293b608a1..15d95c4c515 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs @@ -1,11 +1,13 @@ -use rand::prelude::StdRng; +use super::*; +use crate::execution::validation::state_transition::tests::{ + create_token_contract_with_owner_identity, setup_identity, +}; +use crate::test::helpers::setup::TestPlatformBuilder; use dpp::dash_to_credits; use dpp::data_contract::TokenConfiguration; use dpp::state_transition::batch_transition::BatchTransition; use platform_version::version::PlatformVersion; -use crate::execution::validation::state_transition::tests::{create_token_contract_with_owner_identity, setup_identity}; -use crate::test::helpers::setup::TestPlatformBuilder; -use super::*; +use rand::prelude::StdRng; mod perpetual_distribution_time { use dpp::block::epoch::Epoch; use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; @@ -43,8 +45,6 @@ mod perpetual_distribution_time { // every 3600 seconds, meaning every hour interval: 3_600_000, function: DistributionFunction::FixedAmount { amount: 50 }, - start: None, - end: None, }, distribution_recipient: TokenDistributionRecipient::ContractOwner, }, @@ -74,7 +74,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -142,7 +142,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -169,13 +169,12 @@ mod perpetual_distribution_time { .expect("expected to process state transition"); assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( - ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), - _ - )] - ); - + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); platform .drive @@ -213,7 +212,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -239,7 +238,6 @@ mod perpetual_distribution_time { ) .expect("expected to process state transition"); - assert_matches!( processing_result.execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution(_, _)] @@ -280,8 +278,7 @@ mod perpetual_distribution_time { let (identity, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); - let (identity_2, _, _) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity_2, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (contract, token_id) = create_token_contract_with_owner_identity( &mut platform, @@ -295,11 +292,11 @@ mod perpetual_distribution_time { // every 3600 seconds, meaning every hour interval: 3_600_000, function: DistributionFunction::FixedAmount { amount: 50 }, - start: None, - end: None, }, // we give to identity 2 - distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + distribution_recipient: TokenDistributionRecipient::Identity( + identity_2.id(), + ), }, ))); }), @@ -327,7 +324,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -354,12 +351,12 @@ mod perpetual_distribution_time { .expect("expected to process state transition"); assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( - ConsensusError::StateError(StateError::InvalidTokenClaimWrongClaimant(_)), - _ - )] - ); + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimWrongClaimant(_)), + _ + )] + ); platform .drive @@ -403,8 +400,7 @@ mod perpetual_distribution_time { let platform_state = platform.state.load(); - let (identity, signer, key) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -421,10 +417,10 @@ mod perpetual_distribution_time { // every 3600 seconds, meaning every hour interval: 3_600_000, function: DistributionFunction::FixedAmount { amount: 50 }, - start: None, - end: None, }, - distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + distribution_recipient: TokenDistributionRecipient::Identity( + identity_2.id(), + ), }, ))); }), @@ -452,7 +448,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -515,8 +511,7 @@ mod perpetual_distribution_time { let platform_state = platform.state.load(); - let (identity, _, _) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -535,15 +530,15 @@ mod perpetual_distribution_time { function: DistributionFunction::Linear { a: 1, // Increase in slope y = x d: 1, // No division, simple emission - start_moment: None, + start_step: None, starting_amount: 0, min_value: None, max_value: None, }, - start: None, - end: None, }, - distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + distribution_recipient: TokenDistributionRecipient::Identity( + identity_2.id(), + ), }, ))); }), @@ -572,7 +567,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -621,8 +616,8 @@ mod perpetual_distribution_time { .expect("expected to fetch token balance"); // Since time is slightly over 5 hours we had 5 events x 5. let redemption_cycles = platform_version.system_limits.max_token_redemption_cycles; - let balance = redemption_cycles * (redemption_cycles +1) / 2; // Euler's theorem - // This works because we are adding 1 + 2 + 3 + 4 etc + let balance = redemption_cycles * (redemption_cycles + 1) / 2; // Euler's theorem + // This works because we are adding 1 + 2 + 3 + 4 etc assert_eq!(token_balance, Some(balance as u64)); // We are only claiming for 128 more cycles @@ -642,7 +637,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -690,12 +685,12 @@ mod perpetual_distribution_time { ) .expect("expected to fetch token balance"); let redemption_cycles = redemption_cycles * 2; - let balance = redemption_cycles * (redemption_cycles +1) / 2; // Euler's theorem + let balance = redemption_cycles * (redemption_cycles + 1) / 2; // Euler's theorem assert_eq!(token_balance, Some(balance as u64)); } #[test] - fn test_token_perpetual_distribution_time_linear_verify_contract_start() { + fn test_token_perpetual_distribution_time_linear_every_hour() { let platform_version = PlatformVersion::latest(); let mut platform = TestPlatformBuilder::new() .with_latest_protocol_version() @@ -706,9 +701,201 @@ mod perpetual_distribution_time { let platform_state = platform.state.load(); - let (identity, _, _) = + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::TimeBasedDistribution { + // every hour + interval: 3_600_000, + function: DistributionFunction::Linear { + a: 1, // Increase in slope y = x + d: 1, // No division, simple emission + start_step: None, + starting_amount: 0, + min_value: None, + max_value: None, + }, + }, + distribution_recipient: TokenDistributionRecipient::Identity( + identity_2.id(), + ), + }, + ))); + }), + None, + None, + platform_version, + ); + + // 5 hours later + fast_forward_to_block(&platform, 18_000_000, 40, 42, 1, false); + + // We are only claiming for 256 cycles + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 2, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // 1 at hour 1 = 36_000 + // 2 at hour 2 = 72_000 + // 3 at hour 3 = 108_000 + // 4 at hour 4 = 144_000 + // 5 at hour 5 = 180_000 + // Sum of 1 + 2 + 3 + 4 + 5 = 15 + assert_eq!(token_balance, Some(15)); + + // We are claiming again shortly later, we should have nothing + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 3, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(15)); + } + + #[test] + fn test_token_perpetual_distribution_time_linear_verify_contract_start() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(4981); + + let platform_state = platform.state.load(); + + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -726,19 +913,19 @@ mod perpetual_distribution_time { function: DistributionFunction::Linear { a: 1, // Increase in slope y = x d: 1, // No division, simple emission - start_moment: None, + start_step: None, starting_amount: 0, min_value: None, max_value: None, }, - start: None, - end: None, }, - distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + distribution_recipient: TokenDistributionRecipient::Identity( + identity_2.id(), + ), }, ))); }), - Some(10), + Some(9_000_000), None, platform_version, ); @@ -763,7 +950,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -810,13 +997,13 @@ mod perpetual_distribution_time { platform_version, ) .expect("expected to fetch token balance"); - // Since time is slightly over 5 hours we had 5 events x 5. - let redemption_cycles = platform_version.system_limits.max_token_redemption_cycles; - let balance = redemption_cycles * (redemption_cycles +1) / 2; // Euler's theorem - // This works because we are adding 1 + 2 + 3 + 4 etc - assert_eq!(token_balance, Some(balance as u64)); + // 1 at hour 3 = 108_000 + // 2 at hour 4 = 144_000 + // 3 at hour 5 = 180_000 + // Sum of 1 + 2 + 3 = 6 + assert_eq!(token_balance, Some(6)); - // We are only claiming for 128 more cycles + // We are claiming again, we should have nothing let claim_transition = BatchTransition::new_token_claim_transition( token_id, identity_2.id(), @@ -833,7 +1020,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -861,7 +1048,10 @@ mod perpetual_distribution_time { assert_matches!( processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] ); platform @@ -880,9 +1070,7 @@ mod perpetual_distribution_time { platform_version, ) .expect("expected to fetch token balance"); - let redemption_cycles = redemption_cycles * 2; - let balance = redemption_cycles * (redemption_cycles +1) / 2; // Euler's theorem - assert_eq!(token_balance, Some(balance as u64)); + assert_eq!(token_balance, Some(6)); } #[test] @@ -897,8 +1085,7 @@ mod perpetual_distribution_time { let platform_state = platform.state.load(); - let (identity, _, _) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -916,16 +1103,16 @@ mod perpetual_distribution_time { interval: 1, function: DistributionFunction::Linear { a: MAX_LINEAR_SLOPE_PARAM as i64, // Strongest slope - d: 1, // No division - start_moment: None, + d: 1, // No division + start_step: None, starting_amount: MAX_DISTRIBUTION_PARAM, min_value: None, max_value: None, }, - start: None, - end: None, }, - distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + distribution_recipient: TokenDistributionRecipient::Identity( + identity_2.id(), + ), }, ))); }), @@ -954,7 +1141,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -1001,8 +1188,7 @@ mod perpetual_distribution_time { platform_version, ) .expect("expected to fetch token balance"); - // Since time is slightly over 5 hours we had 5 events x 5. - assert_eq!(token_balance, Some(100205091725260956)); + assert_eq!(token_balance, Some(36028797021077376)); // We are only claiming for 256 more cycles let claim_transition = BatchTransition::new_token_claim_transition( @@ -1021,7 +1207,7 @@ mod perpetual_distribution_time { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -1068,7 +1254,6 @@ mod perpetual_distribution_time { platform_version, ) .expect("expected to fetch token balance"); - // Since time is slightly over 5 hours we had 5 events x 5. - assert_eq!(token_balance, Some(100205091725260956)); + assert_eq!(token_balance, Some(72057594046349056)); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/pre_programmed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/pre_programmed.rs index e11be12d060..70df806caf2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/pre_programmed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/pre_programmed.rs @@ -23,8 +23,7 @@ mod pre_programmed_distribution { let platform_state = platform.state.load(); - let (identity, _, _) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -38,7 +37,11 @@ mod pre_programmed_distribution { .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( TokenPreProgrammedDistributionV0 { // At time 100 we give identity 2 445 as part of the block execution - distributions: [(100, [(identity_2.id(), 445)].into()), (500000, [(identity_2.id(), 600)].into())].into(), + distributions: [ + (100, [(identity_2.id(), 445)].into()), + (500000, [(identity_2.id(), 600)].into()), + ] + .into(), }, ))); }), @@ -65,7 +68,7 @@ mod pre_programmed_distribution { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -132,7 +135,7 @@ mod pre_programmed_distribution { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -194,8 +197,7 @@ mod pre_programmed_distribution { let platform_state = platform.state.load(); - let (identity, _, _) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -236,7 +238,7 @@ mod pre_programmed_distribution { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -303,7 +305,7 @@ mod pre_programmed_distribution { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -330,12 +332,12 @@ mod pre_programmed_distribution { .expect("expected to process state transition"); assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( - ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), - _ - )] - ); + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); platform .drive @@ -368,8 +370,7 @@ mod pre_programmed_distribution { let platform_state = platform.state.load(); - let (identity, _, _) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -410,7 +411,7 @@ mod pre_programmed_distribution { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -437,12 +438,12 @@ mod pre_programmed_distribution { .expect("expected to process state transition"); assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( - ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), - _ - )] - ); + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); platform .drive @@ -475,8 +476,7 @@ mod pre_programmed_distribution { let platform_state = platform.state.load(); - let (identity, _, _) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -490,7 +490,11 @@ mod pre_programmed_distribution { .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( TokenPreProgrammedDistributionV0 { // At time 100 we give identity 2 445 as part of the block execution - distributions: [(100, [(identity_2.id(), 445)].into()), (20000000, [(identity_2.id(), 1337)].into())].into(), + distributions: [ + (100, [(identity_2.id(), 445)].into()), + (20000000, [(identity_2.id(), 1337)].into()), + ] + .into(), }, ))); }), @@ -517,7 +521,7 @@ mod pre_programmed_distribution { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -584,7 +588,7 @@ mod pre_programmed_distribution { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -611,12 +615,12 @@ mod pre_programmed_distribution { .expect("expected to process state transition"); assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( - ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), - _ - )] - ); + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); platform .drive @@ -649,8 +653,7 @@ mod pre_programmed_distribution { let platform_state = platform.state.load(); - let (identity, _, _) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -691,7 +694,7 @@ mod pre_programmed_distribution { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -718,12 +721,12 @@ mod pre_programmed_distribution { .expect("expected to process state transition"); assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( - ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), - _ - )] - ); + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); platform .drive @@ -745,7 +748,8 @@ mod pre_programmed_distribution { } #[test] - fn test_token_pre_programmed_distribution_claim_no_pre_programmed_rewards_for_recipient_when_they_have_perpetual() { + fn test_token_pre_programmed_distribution_claim_no_pre_programmed_rewards_for_recipient_when_they_have_perpetual( + ) { let platform_version = PlatformVersion::latest(); let mut platform = TestPlatformBuilder::new() .with_latest_protocol_version() @@ -756,8 +760,7 @@ mod pre_programmed_distribution { let platform_state = platform.state.load(); - let (identity, _, _) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -782,10 +785,10 @@ mod pre_programmed_distribution { distribution_type: RewardDistributionType::BlockBasedDistribution { interval: 10, function: DistributionFunction::FixedAmount { amount: 50 }, - start: None, - end: None, }, - distribution_recipient: TokenDistributionRecipient::Identity(identity_2.id()), + distribution_recipient: TokenDistributionRecipient::Identity( + identity_2.id(), + ), }, ))); }), @@ -812,7 +815,7 @@ mod pre_programmed_distribution { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -839,12 +842,12 @@ mod pre_programmed_distribution { .expect("expected to process state transition"); assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( - ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), - _ - )] - ); + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + _ + )] + ); platform .drive diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v0/mod.rs index 7534edc0c3d..e22a92ffe12 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/state/v0/mod.rs @@ -2,7 +2,6 @@ use crate::error::Error; use crate::platform_types::platform::PlatformRef; use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; -use dpp::block::epoch::Epoch; use dpp::consensus::state::data_contract::data_contract_already_present_error::DataContractAlreadyPresentError; use dpp::consensus::state::state_error::StateError; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v0/mod.rs index c44713240fb..10d3e2b1850 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v0/mod.rs @@ -2,7 +2,6 @@ use crate::error::Error; use crate::platform_types::platform::PlatformRef; use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; -use dpp::block::epoch::Epoch; use dpp::consensus::basic::document::DataContractNotPresentError; use dpp::consensus::basic::BasicError; diff --git a/packages/rs-drive/src/drive/initialization/v1/mod.rs b/packages/rs-drive/src/drive/initialization/v1/mod.rs index 7f76cfd3064..9b0b8fad1d8 100644 --- a/packages/rs-drive/src/drive/initialization/v1/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v1/mod.rs @@ -5,11 +5,11 @@ use crate::util::batch::GroveDbOpBatch; use crate::drive::system::misc_path_vec; use crate::drive::tokens::paths::{ - token_distributions_root_path_vec, token_timed_distributions_path_vec, - tokens_root_path_vec, TOKEN_BALANCES_KEY, TOKEN_BLOCK_TIMED_DISTRIBUTIONS_KEY, - TOKEN_DISTRIBUTIONS_KEY, TOKEN_EPOCH_TIMED_DISTRIBUTIONS_KEY, TOKEN_IDENTITY_INFO_KEY, - TOKEN_MS_TIMED_DISTRIBUTIONS_KEY, TOKEN_PERPETUAL_DISTRIBUTIONS_KEY, - TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY, TOKEN_STATUS_INFO_KEY, TOKEN_TIMED_DISTRIBUTIONS_KEY, + token_distributions_root_path_vec, token_timed_distributions_path_vec, tokens_root_path_vec, + TOKEN_BALANCES_KEY, TOKEN_BLOCK_TIMED_DISTRIBUTIONS_KEY, TOKEN_DISTRIBUTIONS_KEY, + TOKEN_EPOCH_TIMED_DISTRIBUTIONS_KEY, TOKEN_IDENTITY_INFO_KEY, TOKEN_MS_TIMED_DISTRIBUTIONS_KEY, + TOKEN_PERPETUAL_DISTRIBUTIONS_KEY, TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY, + TOKEN_STATUS_INFO_KEY, TOKEN_TIMED_DISTRIBUTIONS_KEY, }; use crate::drive::{Drive, RootTree}; use crate::error::Error; diff --git a/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/v0/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/v0/mod.rs index 08cc68b427f..4724161d76f 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/add_perpetual_distribution/v0/mod.rs @@ -1,4 +1,8 @@ -use crate::drive::tokens::paths::{token_perpetual_distributions_path_vec, token_root_perpetual_distributions_path_vec, TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, TOKEN_PERPETUAL_DISTRIBUTIONS_INFO_KEY}; +use crate::drive::tokens::paths::{ + token_perpetual_distributions_path_vec, token_root_perpetual_distributions_path_vec, + TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, + TOKEN_PERPETUAL_DISTRIBUTIONS_INFO_KEY, +}; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -83,7 +87,6 @@ impl Drive { &platform_version.drive, )?; - Ok(()) } } diff --git a/packages/rs-drive/src/drive/tokens/distribution/add_pre_programmed_distribution/v0/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/add_pre_programmed_distribution/v0/mod.rs index db6ccf0dae5..9ff30fe2798 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/add_pre_programmed_distribution/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/add_pre_programmed_distribution/v0/mod.rs @@ -1,4 +1,11 @@ -use crate::drive::tokens::paths::{token_ms_timed_at_time_distributions_path_vec, token_ms_timed_distributions_path_vec, token_pre_programmed_at_time_distribution_path_vec, token_pre_programmed_distributions_path, token_root_pre_programmed_distributions_path, TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY}; +use crate::drive::tokens::paths::{ + token_ms_timed_at_time_distributions_path_vec, token_ms_timed_distributions_path_vec, + token_pre_programmed_at_time_distribution_path_vec, token_pre_programmed_distributions_path, + token_root_pre_programmed_distributions_path, + TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, + TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, + TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY, +}; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -186,7 +193,9 @@ impl Drive { self.batch_insert_empty_tree( pre_programmed_distributions_path, - DriveKeyInfo::Key(vec![TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY]), + DriveKeyInfo::Key(vec![ + TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, + ]), None, // we will never clean this part up batch_operations, &platform_version.drive, diff --git a/packages/rs-drive/src/drive/tokens/distribution/fetch/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/fetch/mod.rs index da143a9582a..cc75c81d7b6 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/fetch/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/fetch/mod.rs @@ -1,3 +1,3 @@ mod perpetual_distribution_last_paid_moment; -mod pre_programmed_distributions; mod pre_programmed_distribution_last_paid_time_ms; +mod pre_programmed_distributions; diff --git a/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/mod.rs index 0f456d45719..3322dcb3f7d 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/mod.rs @@ -4,10 +4,10 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::identity::TimestampMillis; use dpp::prelude::Identifier; use dpp::version::PlatformVersion; use grovedb::TransactionArg; -use dpp::identity::TimestampMillis; impl Drive { /// Fetches the last paid timestamp for a pre-programmed distribution for a given identity, @@ -82,7 +82,8 @@ impl Drive { platform_version, ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { - method: "fetch_pre_programmed_distribution_last_paid_time_ms_operations".to_string(), + method: "fetch_pre_programmed_distribution_last_paid_time_ms_operations" + .to_string(), known_versions: vec![0], received: version, })), diff --git a/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/v0/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/v0/mod.rs index e0a25f62112..68abd8a4bf1 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/fetch/pre_programmed_distribution_last_paid_time_ms/v0/mod.rs @@ -5,10 +5,10 @@ use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::util::grove_operations::DirectQueryType; use dpp::identifier::Identifier; +use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; use grovedb::Element::Item; use grovedb::TransactionArg; -use dpp::prelude::TimestampMillis; impl Drive { /// Fetches the last paid timestamp for a pre_programmed distribution for a given identity. @@ -52,7 +52,9 @@ impl Drive { ) { Ok(Some(Item(value, _))) => { if value.len() != 8 { - return Err(Error::Drive(DriveError::CorruptedDriveState("Pre programmed last claimed time should be encoded on 8 bytes".to_string()))); + return Err(Error::Drive(DriveError::CorruptedDriveState( + "Pre programmed last claimed time should be encoded on 8 bytes".to_string(), + ))); } let mut array = [0u8; 8]; array.copy_from_slice(&value); diff --git a/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/v0/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/v0/mod.rs index 7c3873b2c5e..7cde1c102b6 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/mark_perpetual_release_as_distributed/v0/mod.rs @@ -25,15 +25,16 @@ impl Drive { ) -> Result, Error> { let mut batch_operations = vec![]; - let perpetual_distributions_path = token_perpetual_distributions_identity_last_claimed_time_path_vec(token_id); - + let perpetual_distributions_path = + token_perpetual_distributions_identity_last_claimed_time_path_vec(token_id); + if let Some(estimated_costs_only_with_layer_info) = estimated_costs_only_with_layer_info { Drive::add_estimation_costs_for_token_perpetual_distribution( Some(token_id), estimated_costs_only_with_layer_info, &platform_version.drive, )?; - + let estimated_layer_count = match current_moment { RewardDistributionMoment::BlockBasedMoment(_) | RewardDistributionMoment::TimeBasedMoment(_) => EstimatedLevel(0, false), diff --git a/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/v0/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/v0/mod.rs index c219d3b4335..fc481553d7e 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/mark_pre_programmed_release_as_distributed/v0/mod.rs @@ -1,10 +1,16 @@ -use crate::drive::tokens::paths::{token_distributions_root_path_vec, token_ms_timed_at_time_distributions_path_vec, token_pre_programmed_distributions_identity_last_claimed_time_path, token_pre_programmed_distributions_identity_last_claimed_time_path_vec, TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY}; +use crate::drive::tokens::paths::{ + token_distributions_root_path_vec, token_ms_timed_at_time_distributions_path_vec, + token_pre_programmed_distributions_identity_last_claimed_time_path, + token_pre_programmed_distributions_identity_last_claimed_time_path_vec, + TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY, +}; use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::util::grove_operations::BatchDeleteApplyType::{ StatefulBatchDelete, StatelessBatchDelete, }; +use crate::util::object_size_info::PathKeyElementInfo; use crate::util::storage_flags::StorageFlags; use crate::util::type_constants::{DEFAULT_HASH_SIZE_U32, U8_SIZE_U8}; use dpp::block::block_info::BlockInfo; @@ -17,12 +23,11 @@ use dpp::serialization::PlatformSerializable; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; use grovedb::reference_path::ReferencePathType; -use grovedb::{Element, EstimatedLayerInformation, TransactionArg, TreeType}; -use std::collections::HashMap; use grovedb::EstimatedLayerCount::EstimatedLevel; use grovedb::EstimatedLayerSizes::{AllItems, AllSubtrees}; use grovedb::EstimatedSumTrees::NoSumTrees; -use crate::util::object_size_info::PathKeyElementInfo; +use grovedb::{Element, EstimatedLayerInformation, TransactionArg, TreeType}; +use std::collections::HashMap; /// Marks the pre-programmed release as distributed. /// @@ -62,7 +67,7 @@ impl Drive { ) -> Result, Error> { let pre_programmed_distributions_path = token_pre_programmed_distributions_identity_last_claimed_time_path_vec(token_id); - + if let Some(estimated_costs_only_with_layer_info) = estimated_costs_only_with_layer_info { estimated_costs_only_with_layer_info.insert( KeyInfoPath::from_known_owned_path(token_distributions_root_path_vec()), @@ -73,13 +78,15 @@ impl Drive { }, ); - Drive::add_estimation_costs_for_token_pre_programmed_distribution::>( + Drive::add_estimation_costs_for_token_pre_programmed_distribution::< + std::iter::Empty<&TimestampMillis>, + >( token_id, None, estimated_costs_only_with_layer_info, &platform_version.drive, )?; - + Drive::add_estimation_costs_for_root_token_ms_interval_distribution( [&release_time], estimated_costs_only_with_layer_info, @@ -99,7 +106,8 @@ impl Drive { let mut batch_operations = vec![]; // Create storage flags for cleanup logic; these flags are attached to inserted elements. - let storage_flags = StorageFlags::new_single_epoch(block_info.epoch.index, Some(recipient_id)); + let storage_flags = + StorageFlags::new_single_epoch(block_info.epoch.index, Some(recipient_id)); // The pre-programmed distribution was scheduled by inserting a reference in the // millisecond-timed distributions tree at a key corresponding to the release time. diff --git a/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/v0/mod.rs b/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/v0/mod.rs index 9f1f64f28e5..e61cb383097 100644 --- a/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/estimated_costs/for_token_pre_programmed_distribution/v0/mod.rs @@ -59,11 +59,13 @@ impl Drive { EstimatedLayerInformation { tree_type: TreeType::NormalTree, // At this level, expect as many children as there are time entries. - estimated_layer_count: ApproximateElements(times.as_ref().map(|times| times.len()).unwrap_or(128) as u32), + estimated_layer_count: ApproximateElements( + times.as_ref().map(|times| times.len()).unwrap_or(128) as u32, + ), estimated_layer_sizes: AllSubtrees(U64_SIZE_U8, AllSumTrees, None), }, ); - + if let Some(times) = times { // 4. For each provided timestamp, add an estimation for the at-time sum tree. for time in times { diff --git a/packages/rs-drive/src/drive/tokens/paths.rs b/packages/rs-drive/src/drive/tokens/paths.rs index a5587fda4d1..7068b6880ea 100644 --- a/packages/rs-drive/src/drive/tokens/paths.rs +++ b/packages/rs-drive/src/drive/tokens/paths.rs @@ -266,7 +266,7 @@ pub fn token_perpetual_distributions_identity_last_claimed_by_identity_path_vec( vec![TOKEN_PERPETUAL_DISTRIBUTIONS_KEY], token_id.to_vec(), vec![TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY], - identity_id.to_vec() + identity_id.to_vec(), ] } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs index 5ff11401929..1a658b48ecf 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs @@ -42,16 +42,16 @@ impl DriveHighLevelBatchOperationConverter for TokenClaimTransitionAction { match self.distribution_info() { TokenDistributionInfo::Perpetual( - _, + _, TokenDistributionResolvedRecipient::ContractOwnerIdentity(identity), ) | TokenDistributionInfo::PreProgrammed(_, identity) | TokenDistributionInfo::Perpetual( - _, + _, TokenDistributionResolvedRecipient::Identity(identity), ) | TokenDistributionInfo::Perpetual( - _, + _, TokenDistributionResolvedRecipient::Evonode(identity), ) => { ops.push(TokenOperation(TokenOperationType::TokenMint { diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index 6bdf54dc1f3..17f672757e1 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -78,7 +78,17 @@ impl TokenClaimTransitionActionV0 { ), Error, > { - Self::try_from_borrowed_token_claim_transition_with_contract_lookup(drive, owner_id, &value, approximate_without_state_for_costs, transaction, block_info, user_fee_increase, get_data_contract, platform_version) + Self::try_from_borrowed_token_claim_transition_with_contract_lookup( + drive, + owner_id, + &value, + approximate_without_state_for_costs, + transaction, + block_info, + user_fee_increase, + get_data_contract, + platform_version, + ) } /// Converts a borrowed `TokenClaimTransitionV0` into a `TokenClaimTransitionActionV0` using the provided contract lookup. @@ -211,7 +221,7 @@ impl TokenClaimTransitionActionV0 { // We need to find the oldest pre-programmed distribution that wasn't yet claimed // for this identity - + let times = pre_programmed_distribution.distributions(); let mut last_paid_time_operations = vec![]; @@ -236,17 +246,20 @@ impl TokenClaimTransitionActionV0 { fee_result.checked_add_assign(last_paid_time_fee_result)?; - let mut distributions_in_past_for_owner: BTreeMap = times - .iter() - .filter_map(|(timestamp, distribution)| { - if timestamp > &block_info.time_ms { - // Don't get the ones in the future - None - } else { - distribution.get(&owner_id).map(|amount| (*timestamp, *amount)) - } - }) - .collect(); + let mut distributions_in_past_for_owner: BTreeMap = + times + .iter() + .filter_map(|(timestamp, distribution)| { + if timestamp > &block_info.time_ms { + // Don't get the ones in the future + None + } else { + distribution + .get(&owner_id) + .map(|amount| (*timestamp, *amount)) + } + }) + .collect(); let distribution_after_last_paid: Option<(TimestampMillis, TokenAmount)> = if let Some(last_paid) = last_paid_moment { @@ -261,7 +274,7 @@ impl TokenClaimTransitionActionV0 { .first_key_value() .map(|(timestamp, amount)| (*timestamp, *amount)) }; - + let Some((payout_time, amount)) = distribution_after_last_paid else { let bump_action = BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( @@ -280,8 +293,11 @@ impl TokenClaimTransitionActionV0 { InvalidTokenClaimNoCurrentRewards::new( value.base().token_id(), owner_id, - RewardDistributionMoment::TimeBasedMoment(block_info.time_ms), - last_paid_moment.map(RewardDistributionMoment::TimeBasedMoment), + RewardDistributionMoment::TimeBasedMoment( + block_info.time_ms, + ), + last_paid_moment + .map(RewardDistributionMoment::TimeBasedMoment), ), ), )], @@ -290,7 +306,10 @@ impl TokenClaimTransitionActionV0 { )); }; - (amount, TokenDistributionInfo::PreProgrammed(payout_time, owner_id)) + ( + amount, + TokenDistributionInfo::PreProgrammed(payout_time, owner_id), + ) } TokenDistributionType::Perpetual => { // we need to validate that we have a perpetual distribution @@ -324,15 +343,18 @@ impl TokenClaimTransitionActionV0 { // Let's start by checking that we have a valid claimant let wrong_claimant_error = match perpetual_distribution.distribution_recipient() { - TokenDistributionRecipient::ContractOwner if base_action.data_contract_fetch_info().contract.owner_id() != owner_id => { + TokenDistributionRecipient::ContractOwner + if base_action.data_contract_fetch_info().contract.owner_id() + != owner_id => + { Some(base_action.data_contract_fetch_info().contract.owner_id()) } TokenDistributionRecipient::Identity(identifier) if identifier != owner_id => { Some(identifier) } - _ => None + _ => None, }; - + if let Some(expected_claimant) = wrong_claimant_error { let bump_action = BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( @@ -374,14 +396,19 @@ impl TokenClaimTransitionActionV0 { // if the token has never been paid then we use the token creation - let start_from_moment_for_distribution = last_paid_moment - .or(perpetual_distribution - .distribution_type() - .contract_creation_moment(&base_action.data_contract_fetch_info().contract)) + let contract_creation_moment = perpetual_distribution + .distribution_type() + .contract_creation_moment(&base_action.data_contract_fetch_info().contract) .ok_or(Error::Drive(DriveError::ContractDoesNotHaveAStartMoment( base_action.data_contract_fetch_info().contract.id(), )))?; + let contract_creation_cycle_start = contract_creation_moment + .cycle_start(perpetual_distribution.distribution_type().interval())?; + + let start_from_moment_for_distribution = + last_paid_moment.unwrap_or(contract_creation_cycle_start); + let last_paid_time_fee_result = Drive::calculate_fee( None, Some(last_paid_time_operations), @@ -392,28 +419,48 @@ impl TokenClaimTransitionActionV0 { )?; fee_result.checked_add_assign(last_paid_time_fee_result)?; - + let current_cycle_moment = perpetual_distribution.current_interval(block_info); // We need to get the max cycles allowed let max_cycles = platform_version.system_limits.max_token_redemption_cycles; - let max_cycle_moment = perpetual_distribution.distribution_type().max_cycle_moment(start_from_moment_for_distribution, current_cycle_moment, max_cycles)?; + let max_cycle_moment = perpetual_distribution + .distribution_type() + .max_cycle_moment( + start_from_moment_for_distribution, + current_cycle_moment, + max_cycles, + )?; let (recipient, amount) = match perpetual_distribution.distribution_recipient() { - TokenDistributionRecipient::ContractOwner => { - (TokenDistributionResolvedRecipient::ContractOwnerIdentity( + TokenDistributionRecipient::ContractOwner => ( + TokenDistributionResolvedRecipient::ContractOwnerIdentity( base_action.data_contract_fetch_info().contract.owner_id(), - ), perpetual_distribution + ), + perpetual_distribution .distribution_type() - .rewards_in_interval:: Option>(start_from_moment_for_distribution, max_cycle_moment, None)?) - } - TokenDistributionRecipient::Identity(identifier) => { - (TokenDistributionResolvedRecipient::Identity(identifier), perpetual_distribution + .rewards_in_interval:: Option>( + contract_creation_cycle_start, + start_from_moment_for_distribution, + max_cycle_moment, + None, + )?, + ), + TokenDistributionRecipient::Identity(identifier) => ( + TokenDistributionResolvedRecipient::Identity(identifier), + perpetual_distribution .distribution_type() - .rewards_in_interval:: Option>(start_from_moment_for_distribution, max_cycle_moment, None)?) - } + .rewards_in_interval:: Option>( + contract_creation_cycle_start, + start_from_moment_for_distribution, + max_cycle_moment, + None, + )?, + ), TokenDistributionRecipient::EvonodesByParticipation => { - let RewardDistributionMoment::EpochBasedMoment(epoch_index) = start_from_moment_for_distribution else { + let RewardDistributionMoment::EpochBasedMoment(epoch_index) = + start_from_moment_for_distribution + else { return Err(Error::Drive(DriveError::NotSupported( "evonodes by participation can only use epoch based distribution", ))); @@ -432,17 +479,25 @@ impl TokenClaimTransitionActionV0 { let rewards = perpetual_distribution .distribution_type() .rewards_in_interval( + contract_creation_cycle_start, start_from_moment_for_distribution, max_cycle_moment, Some(|epoch_index| { epochs.get(&epoch_index).map(|epoch_info| RewardRatio { - numerator: epoch_info.block_proposers().get(&owner_id).copied().unwrap_or_default(), + numerator: epoch_info + .block_proposers() + .get(&owner_id) + .copied() + .unwrap_or_default(), denominator: epoch_info.total_blocks_in_epoch(), }) }), )?; - (TokenDistributionResolvedRecipient::Evonode(owner_id), rewards) + ( + TokenDistributionResolvedRecipient::Evonode(owner_id), + rewards, + ) } }; @@ -476,10 +531,7 @@ impl TokenClaimTransitionActionV0 { ( amount, - TokenDistributionInfo::Perpetual( - max_cycle_moment, - recipient, - ), + TokenDistributionInfo::Perpetual(max_cycle_moment, recipient), ) } }; From ee41999eb51629c721a5755525306044f10088b8 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Mar 2025 21:59:02 +0700 Subject: [PATCH 3/4] more work --- .../distribution/perpetual/block_based.rs | 3 +- .../distribution/perpetual/time_based.rs | 122 ++++++++++++++++++ .../rs-drive/src/drive/tokens/mint/mod.rs | 6 + .../rs-drive/src/drive/tokens/mint/v0/mod.rs | 29 +++-- .../src/drive/tokens/mint_many/v0/mod.rs | 20 +-- .../system/add_to_token_total_supply/mod.rs | 19 ++- .../add_to_token_total_supply/v0/mod.rs | 86 ++++++------ .../batch/token/token_claim_transition.rs | 1 + .../batch/token/token_mint_transition.rs | 1 + .../src/util/batch/drive_op_batch/token.rs | 5 + .../src/errors/consensus/consensus_error.rs | 8 +- 11 files changed, 238 insertions(+), 62 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 52b53bb5ef5..72d9ea6b700 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs @@ -397,8 +397,7 @@ mod perpetual_distribution_block { let platform_state = platform.state.load(); - let (identity, signer, key) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (identity_2, signer_2, key_2) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs index 15d95c4c515..90e0372bfc2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/time_based.rs @@ -1256,4 +1256,126 @@ mod perpetual_distribution_time { .expect("expected to fetch token balance"); assert_eq!(token_balance, Some(72057594046349056)); } + + #[test] + fn test_token_perpetual_distribution_time_linear_high_values_old_contract_should_handle_overflow( + ) { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(4981); + + let platform_state = platform.state.load(); + + let (identity, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (identity_2, signer_2, key_2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::TimeBasedDistribution { + // every 1 millisecond + interval: 1, + function: DistributionFunction::Linear { + a: MAX_LINEAR_SLOPE_PARAM as i64, // Strongest slope + d: 1, // No division + start_step: None, + starting_amount: MAX_DISTRIBUTION_PARAM, + min_value: None, + max_value: None, + }, + }, + distribution_recipient: TokenDistributionRecipient::Identity( + identity_2.id(), + ), + }, + ))); + }), + None, + None, + platform_version, + ); + + // 5 hours later + fast_forward_to_block(&platform, 18_000_000, 40, 42, 1, false); + + for i in 0..256 { + // We are only claiming for 256 cycles + let claim_transition = BatchTransition::new_token_claim_transition( + token_id, + identity_2.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + &key_2, + 2 + i, + 0, + &signer_2, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let claim_serialized_transition = claim_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![claim_serialized_transition.clone()], + &platform_state, + &BlockInfo { + time_ms: 18_100_000, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }, + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + } + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + // This is i64::Max + assert_eq!(token_balance, Some(9223372036854675807)); + } } diff --git a/packages/rs-drive/src/drive/tokens/mint/mod.rs b/packages/rs-drive/src/drive/tokens/mint/mod.rs index ddcab0a8560..45df917d18d 100644 --- a/packages/rs-drive/src/drive/tokens/mint/mod.rs +++ b/packages/rs-drive/src/drive/tokens/mint/mod.rs @@ -18,6 +18,7 @@ impl Drive { identity_id: [u8; 32], issuance_amount: u64, allow_first_mint: bool, + allow_saturation: bool, block_info: &BlockInfo, apply: bool, transaction: TransactionArg, @@ -29,6 +30,7 @@ impl Drive { identity_id, issuance_amount, allow_first_mint, + allow_saturation, block_info, apply, transaction, @@ -49,6 +51,7 @@ impl Drive { identity_id: [u8; 32], issuance_amount: u64, allow_first_mint: bool, + allow_saturation: bool, apply: bool, transaction: TransactionArg, drive_operations: &mut Vec, @@ -60,6 +63,7 @@ impl Drive { identity_id, issuance_amount, allow_first_mint, + allow_saturation, apply, transaction, drive_operations, @@ -80,6 +84,7 @@ impl Drive { identity_id: [u8; 32], issuance_amount: u64, allow_first_mint: bool, + allow_saturation: bool, estimated_costs_only_with_layer_info: &mut Option< HashMap, >, @@ -92,6 +97,7 @@ impl Drive { identity_id, issuance_amount, allow_first_mint, + allow_saturation, estimated_costs_only_with_layer_info, transaction, platform_version, diff --git a/packages/rs-drive/src/drive/tokens/mint/v0/mod.rs b/packages/rs-drive/src/drive/tokens/mint/v0/mod.rs index f0d737455a0..0b60eb36302 100644 --- a/packages/rs-drive/src/drive/tokens/mint/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/mint/v0/mod.rs @@ -14,6 +14,7 @@ impl Drive { identity_id: [u8; 32], issuance_amount: u64, allow_first_mint: bool, + allow_saturation: bool, block_info: &BlockInfo, apply: bool, transaction: TransactionArg, @@ -26,6 +27,7 @@ impl Drive { identity_id, issuance_amount, allow_first_mint, + allow_saturation, apply, transaction, &mut drive_operations, @@ -50,6 +52,7 @@ impl Drive { identity_id: [u8; 32], issuance_amount: u64, allow_first_mint: bool, + allow_saturation: bool, apply: bool, transaction: TransactionArg, drive_operations: &mut Vec, @@ -63,6 +66,7 @@ impl Drive { identity_id, issuance_amount, allow_first_mint, + allow_saturation, &mut estimated_costs_only_with_layer_info, transaction, platform_version, @@ -83,6 +87,7 @@ impl Drive { identity_id: [u8; 32], issuance_amount: u64, allow_first_mint: bool, + allow_saturation: bool, estimated_costs_only_with_layer_info: &mut Option< HashMap, >, @@ -91,24 +96,30 @@ impl Drive { ) -> Result, Error> { let mut drive_operations = vec![]; + let (add_to_supply_operations, actual_issuance_amount) = self + .add_to_token_total_supply_operations( + token_id, + issuance_amount, + allow_first_mint, + allow_saturation, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + + // There is a chance that we can't add more to the supply because it would overflow, in that case we issue what can be issued if allow saturation is set to true + // Update identity balance drive_operations.extend(self.add_to_identity_token_balance_operations( token_id, identity_id, - issuance_amount, + actual_issuance_amount, estimated_costs_only_with_layer_info, transaction, platform_version, )?); - drive_operations.extend(self.add_to_token_total_supply_operations( - token_id, - issuance_amount, - allow_first_mint, - estimated_costs_only_with_layer_info, - transaction, - platform_version, - )?); + drive_operations.extend(add_to_supply_operations); Ok(drive_operations) } diff --git a/packages/rs-drive/src/drive/tokens/mint_many/v0/mod.rs b/packages/rs-drive/src/drive/tokens/mint_many/v0/mod.rs index 077980e444a..81608dceeb9 100644 --- a/packages/rs-drive/src/drive/tokens/mint_many/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/mint_many/v0/mod.rs @@ -129,14 +129,18 @@ impl Drive { // Update total supply - drive_operations.extend(self.add_to_token_total_supply_operations( - token_id.to_buffer(), - total_mint_amount, - allow_first_mint, - estimated_costs_only_with_layer_info, - transaction, - platform_version, - )?); + drive_operations.extend( + self.add_to_token_total_supply_operations( + token_id.to_buffer(), + total_mint_amount, + allow_first_mint, + false, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )? + .0, + ); Ok(drive_operations) } diff --git a/packages/rs-drive/src/drive/tokens/system/add_to_token_total_supply/mod.rs b/packages/rs-drive/src/drive/tokens/system/add_to_token_total_supply/mod.rs index 4daa069ec8d..a2b7430d0ba 100644 --- a/packages/rs-drive/src/drive/tokens/system/add_to_token_total_supply/mod.rs +++ b/packages/rs-drive/src/drive/tokens/system/add_to_token_total_supply/mod.rs @@ -4,6 +4,7 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::balances::credits::TokenAmount; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; @@ -16,13 +17,14 @@ impl Drive { pub fn add_to_token_total_supply( &self, token_id: [u8; 32], - amount: u64, + amount: TokenAmount, allow_first_mint: bool, + allow_saturation: bool, block_info: &BlockInfo, apply: bool, transaction: TransactionArg, platform_version: &PlatformVersion, - ) -> Result { + ) -> Result<(FeeResult, TokenAmount), Error> { match platform_version .drive .methods @@ -34,6 +36,7 @@ impl Drive { token_id, amount, allow_first_mint, + allow_saturation, block_info, apply, transaction, @@ -51,13 +54,14 @@ impl Drive { pub fn add_to_token_total_supply_add_to_operations( &self, token_id: [u8; 32], - amount: u64, + amount: TokenAmount, allow_first_mint: bool, + allow_saturation: bool, apply: bool, transaction: TransactionArg, drive_operations: &mut Vec, platform_version: &PlatformVersion, - ) -> Result<(), Error> { + ) -> Result { match platform_version .drive .methods @@ -69,6 +73,7 @@ impl Drive { token_id, amount, allow_first_mint, + allow_saturation, apply, transaction, drive_operations, @@ -86,14 +91,15 @@ impl Drive { pub fn add_to_token_total_supply_operations( &self, token_id: [u8; 32], - amount: u64, + amount: TokenAmount, allow_first_mint: bool, + allow_saturation: bool, estimated_costs_only_with_layer_info: &mut Option< HashMap, >, transaction: TransactionArg, platform_version: &PlatformVersion, - ) -> Result, Error> { + ) -> Result<(Vec, TokenAmount), Error> { match platform_version .drive .methods @@ -105,6 +111,7 @@ impl Drive { token_id, amount, allow_first_mint, + allow_saturation, estimated_costs_only_with_layer_info, transaction, platform_version, diff --git a/packages/rs-drive/src/drive/tokens/system/add_to_token_total_supply/v0/mod.rs b/packages/rs-drive/src/drive/tokens/system/add_to_token_total_supply/v0/mod.rs index 686591211a8..df354270cf3 100644 --- a/packages/rs-drive/src/drive/tokens/system/add_to_token_total_supply/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/system/add_to_token_total_supply/v0/mod.rs @@ -5,6 +5,7 @@ use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::fees::op::LowLevelDriveOperation::GroveOperation; use crate::util::grove_operations::DirectQueryType; +use dpp::balances::credits::TokenAmount; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; @@ -19,18 +20,20 @@ impl Drive { token_id: [u8; 32], amount: u64, allow_first_mint: bool, + allow_saturation: bool, block_info: &BlockInfo, apply: bool, transaction: TransactionArg, platform_version: &PlatformVersion, - ) -> Result { + ) -> Result<(FeeResult, TokenAmount), Error> { let mut drive_operations = vec![]; - self.add_to_token_total_supply_add_to_operations_v0( + let token_amount = self.add_to_token_total_supply_add_to_operations_v0( token_id, amount, apply, allow_first_mint, + allow_saturation, transaction, &mut drive_operations, platform_version, @@ -45,26 +48,28 @@ impl Drive { None, )?; - Ok(fees) + Ok((fees, token_amount)) } pub(super) fn add_to_token_total_supply_add_to_operations_v0( &self, token_id: [u8; 32], - amount: u64, + amount: TokenAmount, allow_first_mint: bool, + allow_saturation: bool, apply: bool, transaction: TransactionArg, drive_operations: &mut Vec, platform_version: &PlatformVersion, - ) -> Result<(), Error> { + ) -> Result { let mut estimated_costs_only_with_layer_info = if apply { None } else { Some(HashMap::new()) }; - let batch_operations = self.add_to_token_total_supply_operations_v0( + let (batch_operations, token_amount) = self.add_to_token_total_supply_operations_v0( token_id, amount, allow_first_mint, + allow_saturation, &mut estimated_costs_only_with_layer_info, transaction, platform_version, @@ -76,7 +81,8 @@ impl Drive { batch_operations, drive_operations, &platform_version.drive, - ) + )?; + Ok(token_amount) } pub(super) fn add_to_token_total_supply_operations_v0( @@ -84,12 +90,13 @@ impl Drive { token_id: [u8; 32], amount: u64, allow_first_mint: bool, + allow_saturation: bool, estimated_costs_only_with_layer_info: &mut Option< HashMap, >, transaction: TransactionArg, platform_version: &PlatformVersion, - ) -> Result, Error> { + ) -> Result<(Vec, TokenAmount), Error> { let mut drive_operations = vec![]; // If we only estimate, add estimation costs @@ -113,36 +120,43 @@ impl Drive { &platform_version.drive, )?; - if let Some(total_token_supply_in_platform) = total_token_supply_in_platform { - let new_total = (total_token_supply_in_platform as i64) - .checked_add(amount as i64) - .ok_or(Error::Drive(DriveError::CriticalCorruptedState( - "trying to add an amount that would underflow total supply", - )))?; - let replace_op = QualifiedGroveDbOp::replace_op( - path_holding_total_token_supply_vec, - token_id.to_vec(), - SumItem(new_total, None), - ); - drive_operations.push(GroveOperation(replace_op)); - } else if allow_first_mint { - if amount > i64::MAX as u64 { + let added_amount = + if let Some(total_token_supply_in_platform) = total_token_supply_in_platform { + let new_total = if allow_saturation { + (total_token_supply_in_platform as i64).saturating_add(amount as i64) + } else { + (total_token_supply_in_platform as i64) + .checked_add(amount as i64) + .ok_or(Error::Drive(DriveError::CorruptedCodeExecution( + "trying to add an amount that would overflow total supply", + )))? + }; + let replace_op = QualifiedGroveDbOp::replace_op( + path_holding_total_token_supply_vec, + token_id.to_vec(), + SumItem(new_total, None), + ); + drive_operations.push(GroveOperation(replace_op)); + new_total as u64 - total_token_supply_in_platform + } else if allow_first_mint { + if amount > i64::MAX as u64 { + return Err(Error::Drive(DriveError::CriticalCorruptedState( + "amount is over max allowed in Sum Item (i64::Max)", + ))); + } + let insert_op = QualifiedGroveDbOp::insert_only_op( + path_holding_total_token_supply_vec, + token_id.to_vec(), + SumItem(amount as i64, None), + ); + drive_operations.push(GroveOperation(insert_op)); + amount + } else { return Err(Error::Drive(DriveError::CriticalCorruptedState( - "amount is over max allowed in Sum Item (i64::Max)", + "Total supply for token not found in Platform", ))); - } - let insert_op = QualifiedGroveDbOp::insert_only_op( - path_holding_total_token_supply_vec, - token_id.to_vec(), - SumItem(amount as i64, None), - ); - drive_operations.push(GroveOperation(insert_op)); - } else { - return Err(Error::Drive(DriveError::CriticalCorruptedState( - "Total supply for token not found in Platform", - ))); - } + }; - Ok(drive_operations) + Ok((drive_operations, added_amount)) } } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs index 1a658b48ecf..820a71c8227 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_transition.rs @@ -59,6 +59,7 @@ impl DriveHighLevelBatchOperationConverter for TokenClaimTransitionAction { identity_balance_holder_id: *identity, mint_amount: self.amount(), allow_first_mint: false, + allow_saturation: true, })); } } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_mint_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_mint_transition.rs index 1306088c33b..abc006dae2d 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_mint_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_mint_transition.rs @@ -82,6 +82,7 @@ impl DriveHighLevelBatchOperationConverter for TokenMintTransitionAction { identity_balance_holder_id: self.identity_balance_holder_id(), mint_amount: self.mint_amount(), allow_first_mint: false, + allow_saturation: false, })); let token_configuration = self.base().token_configuration()?; diff --git a/packages/rs-drive/src/util/batch/drive_op_batch/token.rs b/packages/rs-drive/src/util/batch/drive_op_batch/token.rs index b1e551d7d9b..d67500a16a4 100644 --- a/packages/rs-drive/src/util/batch/drive_op_batch/token.rs +++ b/packages/rs-drive/src/util/batch/drive_op_batch/token.rs @@ -37,6 +37,9 @@ pub enum TokenOperationType { mint_amount: TokenAmount, /// Should we allow this to be the first ever mint allow_first_mint: bool, + /// Should we allow a mint to saturate the upper bounds instead of giving an error? + /// For example if we were to add 10 to i64::Max - 5 we would get i64::Max + allow_saturation: bool, }, /// Mints tokens to many recipients TokenMintMany { @@ -149,6 +152,7 @@ impl DriveLowLevelOperationConverter for TokenOperationType { identity_balance_holder_id, mint_amount, allow_first_mint, + allow_saturation, } => { let token_id_bytes: [u8; 32] = token_id.to_buffer(); let identity_id_bytes: [u8; 32] = identity_balance_holder_id.to_buffer(); @@ -157,6 +161,7 @@ impl DriveLowLevelOperationConverter for TokenOperationType { identity_id_bytes, mint_amount, allow_first_mint, + allow_saturation, estimated_costs_only_with_layer_info, transaction, platform_version, diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index 302145b4e89..1a77988f810 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -84,7 +84,7 @@ use dpp::consensus::state::identity::no_transfer_key_for_core_withdrawal_availab use dpp::consensus::state::identity::RecipientIdentityDoesNotExistError; use dpp::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_insufficient_error::PrefundedSpecializedBalanceInsufficientError; use dpp::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_not_found_error::PrefundedSpecializedBalanceNotFoundError; -use dpp::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountNotFrozenError, IdentityTokenAccountFrozenError, TokenIsPausedError, IdentityTokenAccountAlreadyFrozenError, UnauthorizedTokenActionError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, TokenMintPastMaxSupplyError, NewTokensDestinationIdentityDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, InvalidGroupPositionError, TokenAlreadyPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch}; +use dpp::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountNotFrozenError, IdentityTokenAccountFrozenError, TokenIsPausedError, IdentityTokenAccountAlreadyFrozenError, UnauthorizedTokenActionError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, TokenMintPastMaxSupplyError, NewTokensDestinationIdentityDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, InvalidGroupPositionError, TokenAlreadyPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant}; use dpp::consensus::state::voting::masternode_incorrect_voter_identity_id_error::MasternodeIncorrectVoterIdentityIdError; use dpp::consensus::state::voting::masternode_incorrect_voting_address_error::MasternodeIncorrectVotingAddressError; use dpp::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; @@ -378,6 +378,12 @@ pub fn from_state_error(state_error: &StateError) -> JsValue { StateError::InvalidTokenClaimPropertyMismatch(e) => { generic_consensus_error!(InvalidTokenClaimPropertyMismatch, e).into() } + StateError::InvalidTokenClaimNoCurrentRewards(e) => { + generic_consensus_error!(InvalidTokenClaimNoCurrentRewards, e).into() + } + StateError::InvalidTokenClaimWrongClaimant(e) => { + generic_consensus_error!(InvalidTokenClaimWrongClaimant, e).into() + } } } From 3df98ab063e2952a29fa9ef944ac8f6802965ed0 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Mar 2025 22:11:51 +0700 Subject: [PATCH 4/4] fixes --- .../src/query/group_queries/group_actions/v0/mod.rs | 3 +-- .../tokens/balance/prove_identities_token_balances/v0/mod.rs | 2 ++ .../tokens/balance/prove_identity_token_balances/v0/mod.rs | 2 ++ .../v0/mod.rs | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs b/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs index d714a52b8c8..226742cd154 100644 --- a/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs @@ -7,8 +7,7 @@ use dapi_grpc::platform::v0::get_group_actions_request::GetGroupActionsRequestV0 use dapi_grpc::platform::v0::get_group_actions_response::get_group_actions_response_v0::{ emergency_action_event, group_action_event, token_event, BurnEvent, DestroyFrozenFundsEvent, EmergencyActionEvent, FreezeEvent, GroupActionEntry, GroupActionEvent, GroupActions, MintEvent, - PersonalEncryptedNote, SharedEncryptedNote, TokenConfigUpdateEvent, - TokenEvent as TokenEventResponse, UnfreezeEvent, + TokenConfigUpdateEvent, TokenEvent as TokenEventResponse, UnfreezeEvent, }; use dapi_grpc::platform::v0::get_group_actions_response::{ get_group_actions_response_v0, GetGroupActionsResponseV0, diff --git a/packages/rs-drive/src/drive/tokens/balance/prove_identities_token_balances/v0/mod.rs b/packages/rs-drive/src/drive/tokens/balance/prove_identities_token_balances/v0/mod.rs index 44c1ba25cc4..ba017cdf53d 100644 --- a/packages/rs-drive/src/drive/tokens/balance/prove_identities_token_balances/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/balance/prove_identities_token_balances/v0/mod.rs @@ -126,6 +126,7 @@ mod tests { identity.id().to_buffer(), 10000, true, + false, &BlockInfo::default(), true, None, @@ -327,6 +328,7 @@ mod tests { identity_1.id().to_buffer(), 100000, true, + false, &BlockInfo::default(), true, None, diff --git a/packages/rs-drive/src/drive/tokens/balance/prove_identity_token_balances/v0/mod.rs b/packages/rs-drive/src/drive/tokens/balance/prove_identity_token_balances/v0/mod.rs index 56031f6d3d9..9ee2f26b2a0 100644 --- a/packages/rs-drive/src/drive/tokens/balance/prove_identity_token_balances/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/balance/prove_identity_token_balances/v0/mod.rs @@ -134,6 +134,7 @@ mod tests { identity.id().to_buffer(), 5000, true, + false, &BlockInfo::default(), true, None, @@ -158,6 +159,7 @@ mod tests { identity.id().to_buffer(), 10000, true, + false, &BlockInfo::default(), true, None, diff --git a/packages/rs-drive/src/drive/tokens/system/prove_token_total_supply_and_aggregated_identity_balances/v0/mod.rs b/packages/rs-drive/src/drive/tokens/system/prove_token_total_supply_and_aggregated_identity_balances/v0/mod.rs index c20e544fa00..5cba606590d 100644 --- a/packages/rs-drive/src/drive/tokens/system/prove_token_total_supply_and_aggregated_identity_balances/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/system/prove_token_total_supply_and_aggregated_identity_balances/v0/mod.rs @@ -113,6 +113,7 @@ mod tests { identity_1, 50_000, true, + false, &BlockInfo::default(), true, None, @@ -126,6 +127,7 @@ mod tests { identity_2, 50_000, true, + false, &BlockInfo::default(), true, None,