diff --git a/Cargo.lock b/Cargo.lock index efa07cbcc35..1f413a68fe2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1650,6 +1650,7 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", + "serial_test", "simple-signer", "strategy-tests", "tempfile", @@ -4371,6 +4372,15 @@ dependencies = [ "regex", ] +[[package]] +name = "scc" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea091f6cac2595aa38993f04f4ee692ed43757035c36e67c180b6828356385b1" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.23" @@ -4386,6 +4396,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21" + [[package]] name = "seahash" version = "4.1.0" @@ -4627,6 +4643,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "sha1" version = "0.10.6" 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 da38b539f0e..0b6c6393d2c 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 @@ -22,8 +22,10 @@ impl Encode for DistributionFunction { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, - n, + start_decreasing_offset: s, + max_interval_count, + distribution_start_amount: n, + trailing_distribution_interval_amount, min_value, } => { 2u8.encode(encoder)?; @@ -31,7 +33,9 @@ impl Encode for DistributionFunction { decrease_per_interval_numerator.encode(encoder)?; decrease_per_interval_denominator.encode(encoder)?; s.encode(encoder)?; + max_interval_count.encode(encoder)?; n.encode(encoder)?; + trailing_distribution_interval_amount.encode(encoder)?; min_value.encode(encoder)?; } DistributionFunction::Stepwise(steps) => { @@ -60,7 +64,7 @@ impl Encode for DistributionFunction { m, n, o, - start_moment: s, + start_moment, b, min_value, max_value, @@ -71,7 +75,7 @@ impl Encode for DistributionFunction { m.encode(encoder)?; n.encode(encoder)?; o.encode(encoder)?; - s.encode(encoder)?; + start_moment.encode(encoder)?; b.encode(encoder)?; min_value.encode(encoder)?; max_value.encode(encoder)?; @@ -83,7 +87,7 @@ impl Encode for DistributionFunction { n, o, start_moment: s, - c, + b, min_value, max_value, } => { @@ -94,7 +98,7 @@ impl Encode for DistributionFunction { n.encode(encoder)?; o.encode(encoder)?; s.encode(encoder)?; - c.encode(encoder)?; + b.encode(encoder)?; min_value.encode(encoder)?; max_value.encode(encoder)?; } @@ -167,15 +171,19 @@ impl Decode for DistributionFunction { let decrease_per_interval_numerator = u16::decode(decoder)?; let decrease_per_interval_denominator = u16::decode(decoder)?; let s = Option::::decode(decoder)?; + let max_interval_count = Option::::decode(decoder)?; let n = TokenAmount::decode(decoder)?; + let trailing_distribution_interval_amount = TokenAmount::decode(decoder)?; let min_value = Option::::decode(decoder)?; Ok(Self::StepDecreasingAmount { - s, + start_decreasing_offset: s, decrease_per_interval_numerator, decrease_per_interval_denominator, step_count, - n, + distribution_start_amount: n, + max_interval_count, min_value, + trailing_distribution_interval_amount, }) } 3 => { @@ -226,8 +234,8 @@ impl Decode for DistributionFunction { let m = i64::decode(decoder)?; let n = u64::decode(decoder)?; let o = i64::decode(decoder)?; - let s = Option::::decode(decoder)?; - let c = TokenAmount::decode(decoder)?; + let start_moment = Option::::decode(decoder)?; + let b = TokenAmount::decode(decoder)?; let min_value = Option::::decode(decoder)?; let max_value = Option::::decode(decoder)?; Ok(Self::Exponential { @@ -236,8 +244,8 @@ impl Decode for DistributionFunction { m, n, o, - start_moment: s, - c, + start_moment, + b, min_value, max_value, }) @@ -313,14 +321,18 @@ impl<'de> BorrowDecode<'de> for DistributionFunction { let decrease_per_interval_numerator = u16::borrow_decode(decoder)?; let decrease_per_interval_denominator = u16::borrow_decode(decoder)?; let s = Option::::borrow_decode(decoder)?; + let max_interval_count = Option::::borrow_decode(decoder)?; let n = TokenAmount::borrow_decode(decoder)?; + let trailing_distribution_interval_amount = TokenAmount::borrow_decode(decoder)?; let min_value = Option::::borrow_decode(decoder)?; Ok(Self::StepDecreasingAmount { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, - n, + start_decreasing_offset: s, + max_interval_count, + distribution_start_amount: n, + trailing_distribution_interval_amount, min_value, }) } @@ -372,8 +384,8 @@ impl<'de> BorrowDecode<'de> for DistributionFunction { let m = i64::borrow_decode(decoder)?; let n = u64::borrow_decode(decoder)?; let o = i64::borrow_decode(decoder)?; - let s = Option::::borrow_decode(decoder)?; - let c = TokenAmount::borrow_decode(decoder)?; + let start_moment = Option::::borrow_decode(decoder)?; + let b = TokenAmount::borrow_decode(decoder)?; let min_value = Option::::borrow_decode(decoder)?; let max_value = Option::::borrow_decode(decoder)?; Ok(Self::Exponential { @@ -382,8 +394,8 @@ impl<'de> BorrowDecode<'de> for DistributionFunction { m, n, o, - start_moment: s, - c, + start_moment, + 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 833c3574456..78a2dde2c73 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,5 +1,8 @@ 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::distribution_function::{ + DistributionFunction, DEFAULT_STEP_DECREASING_AMOUNT_MAX_CYCLES_BEFORE_TRAILING_DISTRIBUTION, + MAX_DISTRIBUTION_PARAM, +}; use crate::ProtocolError; impl DistributionFunction { @@ -39,7 +42,7 @@ impl DistributionFunction { z = z ^ (z >> 31); // Calculate the range size: (max - min + 1) - let range = max.wrapping_sub(*min).wrapping_add(1); + let range = max.saturating_sub(*min).saturating_add(1); // Map the pseudorandom number into the desired range. let value = min.wrapping_add(z % range); @@ -51,42 +54,52 @@ impl DistributionFunction { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, - n, + start_decreasing_offset, + max_interval_count, + distribution_start_amount, + trailing_distribution_interval_amount, min_value, } => { - // Check for division by zero in the denominator: if *decrease_per_interval_denominator == 0 { return Err(ProtocolError::DivideByZero( "StepDecreasingAmount: denominator is 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) - } else { - 0 - }; - let reduction = 1.0 - - ((*decrease_per_interval_numerator as f64) - / (*decrease_per_interval_denominator as f64)); - let factor = reduction.powf(steps as f64); - let result = (*n as f64) * factor; - // Clamp to min_value if provided. - let clamped = if let Some(min) = min_value { - result.max(*min as f64) - } else { - result - }; - if !clamped.is_finite() || clamped > (u64::MAX as f64) || clamped < 0.0 { - return Err(ProtocolError::Overflow( - "StepDecreasingAmount evaluation overflow or negative", - )); + + let s_val = start_decreasing_offset.unwrap_or(contract_registration_step); + + if x <= s_val { + return Ok(*distribution_start_amount); } - Ok(clamped as TokenAmount) - } + let steps_passed = (x - s_val) / (*step_count as u64); + let max_intervals = max_interval_count.unwrap_or( + DEFAULT_STEP_DECREASING_AMOUNT_MAX_CYCLES_BEFORE_TRAILING_DISTRIBUTION, + ) as u64; + + if steps_passed > max_intervals { + return Ok(*trailing_distribution_interval_amount); + } + + let mut numerator = *distribution_start_amount; + let denominator = *decrease_per_interval_denominator as u64; + let reduction_numerator = + denominator.saturating_sub(*decrease_per_interval_numerator as u64); + + for _ in 0..steps_passed { + numerator = numerator * reduction_numerator / denominator; + } + + let mut result = numerator; + + if let Some(min) = min_value { + if result < *min { + result = *min; + } + } + + Ok(result) + } DistributionFunction::Stepwise(steps) => { // Return the emission corresponding to the greatest key <= x. Ok(steps @@ -192,10 +205,12 @@ impl DistributionFunction { let exponent = (*m as f64) / (*n as f64); let diff = x as i128 - s_val as i128 + *o as i128; - if diff < 0 { - return Err(ProtocolError::Overflow( - "Polynomial function: argument is non-positive", - )); + if diff <= 0 { + return if let Some(min_value) = min_value { + Ok(*min_value) + } else { + Ok(0) + }; } if diff > u64::MAX as i128 { @@ -206,19 +221,50 @@ impl DistributionFunction { let diff_exp = (diff as f64).powf(exponent); - if !diff_exp.is_finite() || diff_exp.abs() > (u64::MAX as f64) { - return Err(ProtocolError::Overflow( - "Polynomial function evaluation overflow or negative", - )); + if !diff_exp.is_finite() { + return if diff_exp.is_sign_positive() { + if let Some(max_value) = max_value { + Ok(*max_value) + } else { + Ok(MAX_DISTRIBUTION_PARAM) + } + } else if let Some(min_value) = min_value { + Ok(*min_value) + } else { + Ok(0) + }; } let pol = diff_exp as i128; - let value = (((*a as i128) * pol / (*d as i128)) as i64) - .checked_add(*b as i64) - .ok_or(ProtocolError::Overflow( - "Polynomial function evaluation overflow or negative", - ))?; + let intermediate = if *d == 1 { + (*a as i128).saturating_mul(pol) + } else { + ((*a as i128).saturating_mul(pol)) / *d as i128 + }; + + if intermediate > MAX_DISTRIBUTION_PARAM as i128 + || intermediate < -(MAX_DISTRIBUTION_PARAM as i128) + { + return if intermediate > 0 { + if let Some(max_value) = max_value { + Ok(*max_value) + } else { + Ok(MAX_DISTRIBUTION_PARAM) + } + } else if let Some(min_value) = min_value { + Ok(*min_value) + } else { + Ok(0) + }; + } + + let value = + (intermediate as i64) + .checked_add(*b as i64) + .ok_or(ProtocolError::Overflow( + "Polynomial function evaluation overflow", + ))?; let value = if value < 0 { 0 } else { value as u64 }; @@ -232,7 +278,12 @@ impl DistributionFunction { return Ok(*max_value); } } - Ok(value) + + if value > MAX_DISTRIBUTION_PARAM { + Ok(MAX_DISTRIBUTION_PARAM) + } else { + Ok(value) + } } DistributionFunction::Exponential { @@ -242,7 +293,7 @@ impl DistributionFunction { n, o, start_moment, - c, + b, min_value, max_value, } => { @@ -272,7 +323,7 @@ impl DistributionFunction { } let exponent = (*m as f64) * (diff as f64) / (*n as f64); - let value = ((*a as f64) * exponent.exp() / (*d as f64)) + (*c as f64); + let value = ((*a as f64) * exponent.exp() / (*d as f64)) + (*b as f64); if let Some(max_value) = max_value { if value.is_infinite() && value.is_sign_positive() || value > *max_value as f64 { @@ -334,22 +385,73 @@ impl DistributionFunction { return Err(ProtocolError::Overflow("Logarithmic function: argument for log is too big (max should be u64::MAX)")); } - let argument = (*m as f64) * (diff as f64) / (*n as f64); + let argument = if *m == 1 { + if *n == 1 { + diff as f64 + } else { + (diff as f64) / (*n as f64) + } + } else if *n == 1 { + (*m as f64) * (diff as f64) + } else { + (*m as f64) * (diff as f64) / (*n as f64) + }; let log_val = argument.ln(); - let value = ((*a as f64) * log_val / (*d as f64)) + (*b as f64); - if let Some(max_value) = max_value { - if value.is_infinite() && value.is_sign_positive() || value > *max_value as f64 - { - return Ok(*max_value); - } - } - if !value.is_finite() || value > (u64::MAX as f64) { + + // Ensure the computed value is finite and within the u64 range. + if !log_val.is_finite() || log_val > (u64::MAX as f64) { return Err(ProtocolError::Overflow( - "Logarithmic function evaluation overflow or negative", + "InvertedLogarithmic: evaluation overflow", )); } - if value < 0.0 { + + let intermediate = if *a == 1 { + log_val + } else if *a == -1 { + -log_val + } else { + (*a as f64) * log_val + }; + + let value = if d == &1 { + if !intermediate.is_finite() || intermediate > (i64::MAX as f64) { + if let Some(max_value) = max_value { + if intermediate.is_sign_positive() { + *max_value as i64 + } else { + return Err(ProtocolError::Overflow( + "InvertedLogarithmic: evaluation overflow intermediate bigger than i64::max", + )); + } + } else { + return Err(ProtocolError::Overflow( + "InvertedLogarithmic: evaluation overflow intermediate bigger than i64::max", + )); + } + } else { + (intermediate.floor() as i64) + .checked_add(*b as i64) + .or(max_value.map(|max| max as i64)) + .ok_or(ProtocolError::Overflow( + "InvertedLogarithmic: evaluation overflow when adding b", + ))? + } + } else { + if !intermediate.is_finite() || intermediate > (i64::MAX as f64) { + return Err(ProtocolError::Overflow( + "InvertedLogarithmic: evaluation overflow intermediate bigger than i64::max", + )); + } + ((intermediate / (*d as f64)).floor() as i64) + .checked_add(*b as i64) + .or(max_value.map(|max| max as i64)) + .ok_or(ProtocolError::Overflow( + "InvertedLogarithmic: evaluation overflow when adding b", + ))? + }; + + if value < 0 { return if let Some(min_value) = min_value { Ok(*min_value) } else { @@ -362,6 +464,12 @@ impl DistributionFunction { return Ok(*min_value); } } + + if let Some(max_value) = max_value { + if value_u64 > *max_value { + return Ok(*max_value); + } + } Ok(value_u64) } DistributionFunction::InvertedLogarithmic { @@ -408,7 +516,11 @@ impl DistributionFunction { } // Calculate the denominator for the logarithm: m * (x - s + o) - let denom_f = (*m as f64) * (diff as f64); + let denom_f = if *m == 1 { + diff as f64 + } else { + (*m as f64) * (diff as f64) + }; if denom_f <= 0.0 { return Err(ProtocolError::Overflow( "InvertedLogarithmic: computed denominator is non-positive", @@ -425,26 +537,48 @@ impl DistributionFunction { let log_val = argument.ln(); - // Compute the final value: (a * ln(...)) / d + b. - let value = ((*a as f64) * log_val / (*d as f64)) + (*b as f64); + // Ensure the computed value is finite and within the u64 range. + if !log_val.is_finite() || log_val > (u64::MAX as f64) { + return Err(ProtocolError::Overflow( + "InvertedLogarithmic: evaluation overflow", + )); + } + + let intermediate = if *a == 1 { + log_val + } else if *a == -1 { + -log_val + } else { + (*a as f64) * log_val + }; + if !intermediate.is_finite() || intermediate > (i64::MAX as f64) { + return Err(ProtocolError::Overflow( + "InvertedLogarithmic: evaluation overflow intermediate bigger than i64::max", + )); + } + + let value = if d == &1 { + (intermediate.floor() as i64).checked_add(*b as i64).ok_or( + ProtocolError::Overflow( + "InvertedLogarithmic: evaluation overflow when adding b", + ), + )? + } else { + ((intermediate / (*d as f64)).floor() as i64) + .checked_add(*b as i64) + .ok_or(ProtocolError::Overflow( + "InvertedLogarithmic: evaluation overflow when adding b", + ))? + }; // Clamp to max_value if provided. if let Some(max_value) = max_value { - if value > *max_value as f64 - || (value.is_infinite() && value.is_sign_positive()) - { + if value > *max_value as i64 { return Ok(*max_value); } } - // Ensure the computed value is finite and within the u64 range. - if !value.is_finite() || value > (u64::MAX as f64) { - return Err(ProtocolError::Overflow( - "InvertedLogarithmic: evaluation overflow", - )); - } - - if value < 0.0 { + if value < 0 { return if let Some(min_value) = min_value { Ok(*min_value) } else { @@ -501,8 +635,10 @@ mod tests { step_count: 10, decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, // 50% reduction per step - s: Some(0), - n: 100, + start_decreasing_offset: Some(0), + max_interval_count: None, + distribution_start_amount: 100, + trailing_distribution_interval_amount: 0, min_value: Some(10), }; @@ -520,8 +656,10 @@ mod tests { step_count: 10, decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 0, // Invalid denominator - s: Some(0), - n: 100, + start_decreasing_offset: Some(0), + max_interval_count: None, + distribution_start_amount: 100, + trailing_distribution_interval_amount: 0, min_value: Some(10), }; @@ -653,6 +791,7 @@ mod tests { } } mod polynomial { + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::{MAX_POL_A_PARAM, MAX_POL_M_PARAM}; use super::*; #[test] fn test_polynomial_function() { @@ -668,18 +807,18 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 10); + assert_eq!(distribution.evaluate(0, 0).unwrap(), 0); 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] - fn test_polynomial_function_overflow() { + fn test_polynomial_function_should_not_overflow() { let distribution = DistributionFunction::Polynomial { - a: i64::MAX, + a: MAX_POL_A_PARAM, d: 1, - m: 2, + m: MAX_POL_M_PARAM, n: 1, o: 0, start_moment: Some(0), @@ -688,12 +827,8 @@ mod tests { max_value: None, }; - let result = distribution.evaluate(0, 1); - assert!( - matches!(result, Err(ProtocolError::Overflow(_))), - "Expected overflow but got {:?}", - result - ); + let result = distribution.evaluate(0, 100000).expect("expected value"); + assert_eq!(result, MAX_DISTRIBUTION_PARAM); } // Test: Fractional exponent (exponent = 3/2) @@ -746,9 +881,8 @@ mod tests { 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(0, 2).unwrap(), 10); + // since it starts at 2 (that's like the contract registration at 2, so we should get 0 + assert_eq!(distribution.evaluate(0, 2).unwrap(), 0); // At x = 3: (3 - 2)^2 = 1, f(3) = 2*1 + 10 = 12. assert_eq!(distribution.evaluate(0, 3).unwrap(), 12); } @@ -772,26 +906,6 @@ mod tests { assert_eq!(distribution.evaluate(0, 1).unwrap(), 42); } - // Test: Constant function when m = 0 (should ignore x) - #[test] - fn test_polynomial_function_constant() { - let distribution = DistributionFunction::Polynomial { - a: 5, - d: 1, - m: 0, // exponent 0 => (x-s+o)^0 = 1 (for any x where x-s+o ≠ 0) - n: 1, - o: 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(0, *x).unwrap(), 8); - } - } - // Test: Linear function when exponent is 1 (m = 1, n = 1) #[test] fn test_polynomial_function_linear() { @@ -858,7 +972,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: None, max_value: None, }; @@ -876,7 +990,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: None, max_value: None, }; @@ -896,7 +1010,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 5, + b: 5, min_value: None, max_value: None, }; @@ -915,7 +1029,7 @@ mod tests { n: 10, o: 0, start_moment: Some(0), - c: 0, + b: 0, min_value: None, max_value: None, }; @@ -934,7 +1048,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 0, + b: 0, min_value: None, max_value: Some(100000000), }; @@ -955,7 +1069,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: None, max_value: None, }; @@ -974,7 +1088,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(11), max_value: None, }; @@ -993,7 +1107,7 @@ mod tests { n: 2, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(11), // Set max at the starting value }; @@ -1019,7 +1133,7 @@ mod tests { n: 10, o: 0, start_moment: Some(0), - c: 5, + b: 5, min_value: None, max_value: None, }; 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 2397d93f18a..3ea95c7db8b 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 @@ -10,8 +10,36 @@ pub mod reward_ratio; mod validation; pub const MAX_DISTRIBUTION_PARAM: u64 = 281_474_976_710_655; //u48::Max 2^48 - 1 +/// The max cycles param is the upper limit of cycles the system can ever support +/// This is applied to linear distribution. +/// For all other distributions we use a versioned max cycles contained in the platform version. +/// That other version is much lower because the calculations for other distributions are more +/// complex. +pub const MAX_DISTRIBUTION_CYCLES_PARAM: u64 = 32_767; //u15::Max 2^(63 - 48) - 1 -pub const MAX_LINEAR_SLOPE_PARAM: u64 = 256; +pub const DEFAULT_STEP_DECREASING_AMOUNT_MAX_CYCLES_BEFORE_TRAILING_DISTRIBUTION: u16 = 128; + +pub const MAX_LINEAR_SLOPE_A_PARAM: u64 = 256; + +pub const MIN_LINEAR_SLOPE_A_PARAM: i64 = -255; + +pub const MIN_POL_M_PARAM: i64 = -8; +pub const MAX_POL_M_PARAM: i64 = 8; + +pub const MAX_POL_N_PARAM: u64 = 32; + +pub const MIN_LOG_A_PARAM: i64 = -32_766; +pub const MAX_LOG_A_PARAM: i64 = 32_767; +pub const MAX_EXP_A_PARAM: u64 = 256; + +pub const MAX_EXP_M_PARAM: u64 = 8; + +pub const MIN_EXP_M_PARAM: i64 = -8; + +pub const MAX_EXP_N_PARAM: u64 = 32; + +pub const MIN_POL_A_PARAM: i64 = -255; +pub const MAX_POL_A_PARAM: i64 = 256; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)] pub enum DistributionFunction { @@ -85,11 +113,18 @@ pub enum DistributionFunction { /// f(x) = n * (1 - (decrease_per_interval_numerator / decrease_per_interval_denominator))^((x - s) / step_count) /// ``` /// + /// For `x <= s`, `f(x) = n` + /// /// # Parameters /// - `step_count`: The number of periods between each step. /// - `decrease_per_interval_numerator` and `decrease_per_interval_denominator`: Define the reduction factor per step. - /// - `s`: Optional start period offset (e.g., start block or time). If not provided, the contract creation start is used. - /// - `n`: The initial token emission. + /// - `start_decreasing_offset`: Optional start period offset (e.g., start block or time). If not provided, the contract creation start is used. + /// If this is provided before this number we give out the distribution start amount every interval. + /// - `max_interval_count`: The maximum amount of intervals there can be. Can be up to 1024. + /// !!!Very important!!! -> This will default to 128 is default if not set. + /// This means that after 128 cycles we will be distributing trailing_distribution_interval_amount per interval. + /// - `distribution_start_amount`: The initial token emission. + /// - `trailing_distribution_interval_amount`: The token emission after all decreasing intervals. /// - `min_value`: Optional minimum emission value. /// /// # Use Case @@ -103,8 +138,10 @@ pub enum DistributionFunction { step_count: u32, decrease_per_interval_numerator: u16, decrease_per_interval_denominator: u16, - s: Option, - n: TokenAmount, + start_decreasing_offset: Option, + max_interval_count: Option, + distribution_start_amount: TokenAmount, + trailing_distribution_interval_amount: TokenAmount, min_value: Option, }, @@ -114,6 +151,8 @@ pub enum DistributionFunction { /// - Within each step, the emission remains constant. /// - The keys in the `BTreeMap` represent the starting period for each interval, /// and the corresponding values are the fixed token amounts to emit during that interval. + /// - VERY IMPORTANT: the steps are the amount of intervals, not the time or the block count. + /// So if you have step 5 with interval 10 using blocks that's 50 blocks. /// /// # Use Case /// - Adjusting rewards at specific milestones or time intervals. @@ -333,7 +372,7 @@ pub enum DistributionFunction { /// The emission at period `x` is given by: /// /// ```text - /// f(x) = (a * e^(m * (x - s) / n)) / d + c + /// f(x) = (a * e^(m * (x - s) / n)) / d + b /// ``` /// /// # Parameters @@ -342,7 +381,7 @@ pub enum DistributionFunction { /// - `d`: A divisor used to scale the exponential term. /// - `s`: Optional start period offset. If not set, the contract creation start is assumed. /// - `o`: An offset for the exp function, this is useful if s is in None. - /// - `c`: An offset added to the result. + /// - `b`: An offset added to the result. /// - `min_value` / `max_value`: Optional constraints on the emitted tokens. /// /// # Use Cases @@ -368,7 +407,7 @@ pub enum DistributionFunction { /// /// ## **Example 2: Exponential Decay (`m < 0`)** /// - **Use Case**: A deflationary model where emissions start high and gradually decrease to ensure scarcity. - /// - **Parameters**: `a = 500`, `m = -3`, `n = 100`, `d = 20`, `c = 10` + /// - **Parameters**: `a = 500`, `m = -3`, `n = 100`, `d = 20`, `b = 10` /// - **Formula**: /// ```text /// f(x) = (500 * e^(-3 * (x - s) / 100)) / 20 + 10 @@ -381,12 +420,12 @@ pub enum DistributionFunction { n: u64, o: i64, start_moment: Option, - c: TokenAmount, + b: TokenAmount, min_value: Option, max_value: Option, }, - /// Emits tokens following a logarithmic function. + /// Emits tokens following a natural logarithmic (ln) function. /// /// # Formula /// The emission at period `x` is computed as: @@ -418,7 +457,7 @@ pub enum DistributionFunction { /// /// - Given the formula: /// ```text - /// f(x) = (a * log(m * (x - s + o) / n)) / d + b + /// f(x) = (a * ln(m * (x - s + o) / n)) / d + b /// ``` /// /// - Let’s assume the following parameters: @@ -430,7 +469,7 @@ pub enum DistributionFunction { /// /// - This results in: /// ```text - /// f(x) = (100 * log(2 * (x + 1) / 1)) / 10 + 50 + /// f(x) = (100 * ln(2 * (x + 1) / 1)) / 10 + 50 /// ``` /// /// - **Expected Behavior:** @@ -454,13 +493,13 @@ pub enum DistributionFunction { min_value: Option, max_value: Option, }, - /// Emits tokens following an inverted logarithmic function. + /// Emits tokens following an inverted natural logarithmic function. /// /// # Formula /// The emission at period `x` is given by: /// /// ```text - /// f(x) = (a * log( n / (m * (x - s + o)) )) / d + b + /// f(x) = (a * ln( n / (m * (x - s + o)) )) / d + b /// ``` /// /// # Parameters @@ -481,22 +520,24 @@ pub enum DistributionFunction { /// claimants receive diminishing rewards. /// /// # Example - /// - Suppose a system starts with **500 tokens per period** and gradually reduces over time: - /// /// ```text - /// f(x) = (1000 * log(5000 / (5 * (x - 1000)))) / 10 + 10 + /// f(x) = 10000 * ln(5000 / x) /// ``` - /// - /// Example values: - /// - /// | Period (x) | Emission (f(x)) | - /// |------------|----------------| - /// | 1000 | 500 tokens | - /// | 1500 | 230 tokens | - /// | 2000 | 150 tokens | - /// | 5000 | 50 tokens | - /// | 10,000 | 20 tokens | - /// | 50,000 | 10 tokens | + /// - Values: a = 10000 n = 5000 m = 1 o = 0 b = 0 d = 0 + /// y + /// ↑ + /// 10000 |* + /// 9000 | * + /// 8000 | * + /// 7000 | * + /// 6000 | * + /// 5000 | * + /// 4000 | * + /// 3000 | * + /// 2000 | * + /// 1000 | * + /// 0 +-------------------*----------→ x + /// 0 2000 4000 6000 8000 /// /// - The emission **starts high** and **gradually decreases**, ensuring early adopters receive /// more tokens while later participants still get rewards. @@ -528,23 +569,35 @@ impl fmt::Display for DistributionFunction { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, - n, + start_decreasing_offset: s, + max_interval_count, + distribution_start_amount, + trailing_distribution_interval_amount, min_value, } => { write!( f, "StepDecreasingAmount: {} tokens, decreasing by {}/{} every {} steps", - n, + distribution_start_amount, decrease_per_interval_numerator, decrease_per_interval_denominator, step_count )?; if let Some(start) = s { - write!(f, " starting at period {}", start)?; + write!(f, ", starting at period {}", start)?; } + if let Some(max_intervals) = max_interval_count { + write!(f, ", with a maximum of {} intervals", max_intervals)?; + } else { + write!(f, ", with a maximum of 128 intervals (default)")?; + } + write!( + f, + ", trailing distribution amount {} tokens", + trailing_distribution_interval_amount + )?; if let Some(min) = min_value { - write!(f, ", with a minimum emission of {}", min)?; + write!(f, ", minimum emission {} tokens", min)?; } Ok(()) } @@ -615,18 +668,18 @@ impl fmt::Display for DistributionFunction { m, n, o, - start_moment: s, - c, + start_moment, + b, min_value, max_value, } => { write!(f, "Exponential: f(x) = {} * e^( {} * (x", a, m)?; - if let Some(start) = s { + if let Some(start) = start_moment { write!(f, " - {} + {})", start, o)?; } else { write!(f, " + {})", o)?; } - write!(f, " / {} ) / {} + {}", n, d, c)?; + write!(f, " / {} ) / {} + {}", n, d, b)?; if let Some(min) = min_value { write!(f, ", min: {}", min)?; } 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 5f0df6bf152..c9738c71f1f 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,83 +4,135 @@ use crate::consensus::basic::data_contract::{ InvalidTokenDistributionFunctionInvalidParameterError, InvalidTokenDistributionFunctionInvalidParameterTupleError, }; +use crate::consensus::basic::UnsupportedFeatureError; use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::{ - DistributionFunction, MAX_DISTRIBUTION_PARAM, MAX_LINEAR_SLOPE_PARAM, + DistributionFunction, MAX_DISTRIBUTION_PARAM, MAX_EXP_A_PARAM, MAX_EXP_M_PARAM, + MAX_EXP_N_PARAM, MAX_LINEAR_SLOPE_A_PARAM, MAX_LOG_A_PARAM, MAX_POL_M_PARAM, MAX_POL_N_PARAM, + MIN_EXP_M_PARAM, MIN_LINEAR_SLOPE_A_PARAM, MIN_LOG_A_PARAM, MIN_POL_M_PARAM, }; use crate::validation::SimpleConsensusValidationResult; use crate::ProtocolError; +use platform_version::version::PlatformVersion; impl DistributionFunction { pub fn validate( &self, start_moment: u64, + platform_version: &PlatformVersion, ) -> Result { match self { DistributionFunction::FixedAmount { amount: n } => { // Validate that n is > 0 and does not exceed u32::MAX. - if *n == 0 || *n > u32::MAX as u64 { + if *n == 0 || *n > MAX_DISTRIBUTION_PARAM { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( "n".to_string(), 1, - u32::MAX as i64, + MAX_DISTRIBUTION_PARAM as i64, None, ) .into(), )); } } - DistributionFunction::Random { min, max } => { + DistributionFunction::Random { .. } => { + return Ok(SimpleConsensusValidationResult::new_with_error( + UnsupportedFeatureError::new( + "token random distribution".to_string(), + platform_version.protocol_version, + ) + .into(), + )); // Ensure that `min` is not greater than `max` - if *min > *max { - return Ok(SimpleConsensusValidationResult::new_with_error( - InvalidTokenDistributionFunctionInvalidParameterTupleError::new( - "min".to_string(), - "max".to_string(), - "smaller than or equal to".to_string(), - ) - .into(), - )); - } - - // Ensure that `max` is within valid bounds - if *max > MAX_DISTRIBUTION_PARAM { - return Ok(SimpleConsensusValidationResult::new_with_error( - InvalidTokenDistributionFunctionInvalidParameterError::new( - "max".to_string(), - 0, - MAX_DISTRIBUTION_PARAM as i64, - None, - ) - .into(), - )); - } + // if *min > *max { + // return Ok(SimpleConsensusValidationResult::new_with_error( + // InvalidTokenDistributionFunctionInvalidParameterTupleError::new( + // "min".to_string(), + // "max".to_string(), + // "smaller than or equal to".to_string(), + // ) + // .into(), + // )); + // } + // + // // Ensure that `max` is within valid bounds + // if *max > MAX_DISTRIBUTION_PARAM { + // return Ok(SimpleConsensusValidationResult::new_with_error( + // InvalidTokenDistributionFunctionInvalidParameterError::new( + // "max".to_string(), + // 0, + // MAX_DISTRIBUTION_PARAM as i64, + // None, + // ) + // .into(), + // )); + // } } DistributionFunction::StepDecreasingAmount { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, - n, + start_decreasing_offset, + max_interval_count, + distribution_start_amount, + trailing_distribution_interval_amount, min_value, } => { // Validate n. - if *n == 0 || *n > u32::MAX as u64 { + if *distribution_start_amount == 0 + || *distribution_start_amount > MAX_DISTRIBUTION_PARAM + { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( "n".to_string(), 1, - u32::MAX as i64, + MAX_DISTRIBUTION_PARAM as i64, None, ) .into(), )); } + + // Ensure trailing amount does not exceed the initial amount + if *trailing_distribution_interval_amount > *distribution_start_amount { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterTupleError::new( + "trailing_distribution_interval_amount".to_string(), + "distribution_start_amount".to_string(), + "smaller than or equal to".to_string(), + ) + .into(), + )); + } + if let Some(max_interval_count) = max_interval_count { + if *max_interval_count < 2 || *max_interval_count > 1024 { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "max_interval_count".to_string(), + 2, + 1024, + None, + ) + .into(), + )); + } + } if *step_count == 0 { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionDivideByZeroError::new(self.clone()).into(), )); } + if *decrease_per_interval_numerator == 0 { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "decrease_per_interval_numerator".to_string(), + 1, + u16::MAX as i64, + None, + ) + .into(), + )); + } if *decrease_per_interval_denominator == 0 { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionDivideByZeroError::new(self.clone()).into(), @@ -97,7 +149,7 @@ impl DistributionFunction { )); } if let Some(min) = min_value { - if *n < *min { + if *distribution_start_amount < *min { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterTupleError::new( "n".to_string(), @@ -109,7 +161,7 @@ impl DistributionFunction { } } - if let Some(s) = s { + if let Some(s) = start_decreasing_offset { if *s > MAX_DISTRIBUTION_PARAM { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( @@ -143,7 +195,7 @@ impl DistributionFunction { a, d, start_step: s, - starting_amount: b, + starting_amount, min_value, max_value, } => { @@ -152,30 +204,19 @@ impl DistributionFunction { InvalidTokenDistributionFunctionDivideByZeroError::new(self.clone()).into(), )); } - if *a == 0 { + if *a == 0 || *a > MAX_LINEAR_SLOPE_A_PARAM as i64 || *a < MIN_LINEAR_SLOPE_A_PARAM + { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( "a".to_string(), - -(MAX_DISTRIBUTION_PARAM as i64), - MAX_DISTRIBUTION_PARAM as i64, + MIN_LINEAR_SLOPE_A_PARAM, + MAX_LINEAR_SLOPE_A_PARAM as i64, Some(0), ) .into(), )); } - 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_LINEAR_SLOPE_PARAM as i64), - MAX_LINEAR_SLOPE_PARAM as i64, - None, - ) - .into(), - )); - } - if let (Some(min), Some(max)) = (min_value, max_value) { if min > max { return Ok(SimpleConsensusValidationResult::new_with_error( @@ -221,7 +262,7 @@ impl DistributionFunction { a: *a, d: *d, start_step: Some(s.unwrap_or(start_moment)), - starting_amount: *b, + starting_amount: *starting_amount, min_value: *min_value, max_value: *max_value, } @@ -280,6 +321,53 @@ impl DistributionFunction { )); } + if *m > 0 && *n == m.unsigned_abs() { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterTupleError::new( + "m".to_string(), + "n".to_string(), + "different than".to_string(), + ) + .into(), + )); + } + + if *a == 0 || *a < MIN_LOG_A_PARAM || *a > MAX_LOG_A_PARAM { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "a".to_string(), + MIN_LOG_A_PARAM, + MAX_LOG_A_PARAM, + Some(0), + ) + .into(), + )); + } + + if *m == 0 || *m < MIN_POL_M_PARAM || *m > MAX_POL_M_PARAM { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "m".to_string(), + MIN_POL_M_PARAM, + MAX_POL_M_PARAM, + Some(0), + ) + .into(), + )); + } + + if *n > MAX_POL_N_PARAM { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "n".to_string(), + 1, + MAX_POL_N_PARAM as i64, + None, + ) + .into(), + )); + } + if let Some(s) = s { if *s > MAX_DISTRIBUTION_PARAM { return Ok(SimpleConsensusValidationResult::new_with_error( @@ -387,7 +475,7 @@ impl DistributionFunction { } } } - // f(x) = (a * e^(m * (x - s + o) / n)) / d + c + // f(x) = (a * e^(m * (x - s + o) / n)) / d + b DistributionFunction::Exponential { a, d, @@ -395,7 +483,7 @@ impl DistributionFunction { n, o, start_moment: s, - c, + b, min_value, max_value, } => { @@ -409,23 +497,35 @@ impl DistributionFunction { InvalidTokenDistributionFunctionDivideByZeroError::new(self.clone()).into(), )); } - if *m == 0 { + if *n > MAX_EXP_N_PARAM { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "n".to_string(), + 1, + MAX_EXP_N_PARAM as i64, + None, + ) + .into(), + )); + } + if *m == 0 || *m > MAX_EXP_M_PARAM as i64 || *m < MIN_EXP_M_PARAM { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( "m".to_string(), - -(MAX_DISTRIBUTION_PARAM as i64), - MAX_DISTRIBUTION_PARAM as i64, + MIN_EXP_M_PARAM, + MAX_EXP_M_PARAM as i64, Some(0), ) .into(), )); } - if *a == 0 { + // Check valid a values + if *a == 0 || *a > MAX_EXP_A_PARAM { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( "a".to_string(), 1, - MAX_DISTRIBUTION_PARAM as i64, + MAX_EXP_A_PARAM as i64, None, ) .into(), @@ -472,10 +572,10 @@ impl DistributionFunction { )); } - if *a > MAX_DISTRIBUTION_PARAM { + if *b > MAX_DISTRIBUTION_PARAM { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( - "a".to_string(), + "b".to_string(), 0, MAX_DISTRIBUTION_PARAM as i64, None, @@ -530,7 +630,7 @@ impl DistributionFunction { n: *n, o: *o, start_moment: Some(s.unwrap_or(start_moment)), - c: *c, + b: *b, min_value: *min_value, max_value: *max_value, } @@ -565,7 +665,7 @@ impl DistributionFunction { start_token_amount }; } - // f(x) = (a * log(m * (x - s + o) / n)) / d + b + // f(x) = (a * ln(m * (x - s + o) / n)) / d + b DistributionFunction::Logarithmic { a, d, @@ -587,7 +687,7 @@ impl DistributionFunction { InvalidTokenDistributionFunctionDivideByZeroError::new(self.clone()).into(), )); } - if *m == 0 { + if *m == 0 || *m > MAX_DISTRIBUTION_PARAM { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( "m".to_string(), @@ -598,11 +698,24 @@ impl DistributionFunction { .into(), )); } - if *a == 0 { + // Check valid a values + if *a == 0 || *a < MIN_LOG_A_PARAM || *a > MAX_LOG_A_PARAM { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( "a".to_string(), - 1, + MIN_LOG_A_PARAM, + MAX_LOG_A_PARAM, + Some(0), + ) + .into(), + )); + } + + if *b > MAX_DISTRIBUTION_PARAM { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "b".to_string(), + 0, MAX_DISTRIBUTION_PARAM as i64, None, ) @@ -724,6 +837,31 @@ impl DistributionFunction { min_value, max_value, } => { + // Check valid a values + if *a == 0 || *a < MIN_LOG_A_PARAM || *a > MAX_LOG_A_PARAM { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "a".to_string(), + MIN_LOG_A_PARAM, + MAX_LOG_A_PARAM, + Some(0), + ) + .into(), + )); + } + + if *b > MAX_DISTRIBUTION_PARAM { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "b".to_string(), + 0, + MAX_DISTRIBUTION_PARAM as i64, + None, + ) + .into(), + )); + } + // Check for division by zero. if *d == 0 { return Ok(SimpleConsensusValidationResult::new_with_error( @@ -889,7 +1027,7 @@ mod tests { #[test] fn test_fixed_amount_valid() { let dist = DistributionFunction::FixedAmount { amount: 100 }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_fixed_amount_valid") .first_error() @@ -899,7 +1037,7 @@ mod tests { #[test] fn test_fixed_amount_zero_invalid() { let dist = DistributionFunction::FixedAmount { amount: 0 }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_fixed_amount_zero_invalid") .first_error() @@ -911,7 +1049,7 @@ mod tests { let dist = DistributionFunction::FixedAmount { amount: u32::MAX as u64, }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_fixed_amount_max_valid") .first_error() @@ -921,9 +1059,9 @@ mod tests { #[test] fn test_fixed_amount_exceeds_max_invalid() { let dist = DistributionFunction::FixedAmount { - amount: u32::MAX as u64 + 1, + amount: MAX_DISTRIBUTION_PARAM + 1, }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_fixed_amount_exceeds_max_invalid") .first_error() @@ -939,11 +1077,13 @@ mod tests { step_count: 10, decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, - s: Some(0), - n: 100, + start_decreasing_offset: Some(0), + max_interval_count: None, + distribution_start_amount: 100, + trailing_distribution_interval_amount: 0, min_value: Some(10), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_step_decreasing_amount_valid") .first_error() @@ -956,11 +1096,13 @@ mod tests { step_count: 0, decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, - s: Some(0), - n: 100, + start_decreasing_offset: Some(0), + max_interval_count: None, + distribution_start_amount: 100, + trailing_distribution_interval_amount: 0, min_value: Some(10), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_step_decreasing_amount_invalid_zero_step_count") .first_error() @@ -973,11 +1115,13 @@ mod tests { step_count: 10, decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 0, - s: Some(0), - n: 100, + start_decreasing_offset: Some(0), + max_interval_count: None, + distribution_start_amount: 100, + trailing_distribution_interval_amount: 0, min_value: Some(10), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_step_decreasing_amount_invalid_zero_denominator") .first_error() @@ -993,7 +1137,7 @@ mod tests { steps.insert(10, 50); steps.insert(20, 25); let dist = DistributionFunction::Stepwise(steps); - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_stepwise_valid") .first_error() @@ -1005,7 +1149,7 @@ mod tests { let mut steps = BTreeMap::new(); steps.insert(0, 100); let dist = DistributionFunction::Stepwise(steps); - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_stepwise_invalid_single_step") .first_error() @@ -1025,7 +1169,7 @@ mod tests { max_value: Some(150), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); // If the test fails, print the exact error message. if let Err(err) = &result { @@ -1047,7 +1191,7 @@ mod tests { min_value: Some(50), max_value: Some(150), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_linear_invalid_divide_by_zero") .first_error() @@ -1064,7 +1208,7 @@ mod tests { min_value: Some(50), max_value: Some(150), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_linear_invalid_s_exceeds_max") .first_error() @@ -1081,7 +1225,7 @@ mod tests { min_value: Some(50), max_value: Some(150), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_linear_invalid_a_zero") @@ -1101,7 +1245,7 @@ mod tests { min_value: Some(50), max_value: Some(150), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_linear_invalid_a_too_large") @@ -1121,7 +1265,7 @@ mod tests { min_value: Some(200), // Invalid: min > max max_value: Some(150), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_linear_invalid_min_greater_than_max") @@ -1141,7 +1285,7 @@ mod tests { min_value: Some(50), max_value: Some(150), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_linear_invalid_s_greater_than_max") @@ -1161,7 +1305,7 @@ mod tests { min_value: Some(50), max_value: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: max_value exceeds max allowed range }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_linear_invalid_max_exceeds_max_distribution_param") @@ -1181,7 +1325,7 @@ mod tests { min_value: Some(50), max_value: Some(150), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_linear_invalid_starting_at_max_value") @@ -1201,7 +1345,7 @@ mod tests { min_value: Some(50), max_value: Some(150), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_linear_invalid_starting_at_min_value") @@ -1221,7 +1365,7 @@ mod tests { min_value: Some(50), max_value: Some(250), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); match result { Ok(validation_result) => { @@ -1251,7 +1395,7 @@ mod tests { min_value: Some(10), // Valid min boundary max_value: Some(150), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_linear_valid_with_min_boundary") .first_error() @@ -1268,7 +1412,7 @@ mod tests { min_value: Some(10), max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_linear_valid_with_max_boundary") .first_error() @@ -1291,7 +1435,7 @@ mod tests { min_value: Some(1), max_value: Some(80), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); match &result { Ok(validation_result) => { @@ -1304,6 +1448,30 @@ mod tests { } } } + + #[test] + fn test_polynomial_invalid_zero_a() { + let dist = DistributionFunction::Polynomial { + a: 0, + d: 1, + m: 2, + n: 3, + o: 0, + start_moment: Some(0), + b: 5, + min_value: Some(1), + max_value: Some(50), + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert!( + result + .expect("no error on test_exponential_invalid_zero_a") + .first_error() + .is_some(), + "Expected error: a cannot be zero" + ); + } + #[test] fn test_polynomial_invalid_divide_by_zero() { let dist = DistributionFunction::Polynomial { @@ -1317,7 +1485,7 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_polynomial_invalid_divide_by_zero") .first_error() @@ -1338,7 +1506,7 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected an error when n is zero" @@ -1359,7 +1527,7 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected an error when s exceeds MAX_DISTRIBUTION_PARAM" @@ -1380,7 +1548,7 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected an error when o is above the allowed maximum" @@ -1401,13 +1569,133 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected an error when o is below the allowed minimum" ); } + #[test] + fn test_polynomial_invalid_a_below_min() { + let dist = DistributionFunction::Polynomial { + a: MIN_LOG_A_PARAM - 1, + d: 1, + m: 2, + n: 3, + o: 0, + start_moment: Some(0), + b: 5, + min_value: Some(1), + max_value: Some(50), + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert!( + result.expect("expected result").first_error().is_some(), + "Expected error: a is below minimum" + ); + } + + #[test] + fn test_polynomial_invalid_m_equal_n() { + let dist = DistributionFunction::Polynomial { + a: 1, + d: 1, + m: 3, + n: 3, + o: 0, + start_moment: Some(0), + b: 5, + min_value: None, + max_value: None, + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert!( + result.expect("expected result").first_error().is_some(), + "Expected error: a is below minimum" + ); + } + + #[test] + fn test_polynomial_invalid_a_above_max() { + let dist = DistributionFunction::Polynomial { + a: MAX_LOG_A_PARAM + 1, + d: 1, + m: 2, + n: 3, + o: 0, + start_moment: Some(0), + b: 5, + min_value: Some(1), + max_value: Some(50), + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert!( + result.expect("expected result").first_error().is_some(), + "Expected error: a is above maximum" + ); + } + + #[test] + fn test_polynomial_invalid_m_below_min() { + let dist = DistributionFunction::Polynomial { + a: 2, + d: 1, + m: MIN_POL_M_PARAM - 1, + n: 3, + o: 0, + start_moment: Some(0), + b: 5, + min_value: Some(1), + max_value: Some(50), + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert!( + result.expect("expected result").first_error().is_some(), + "Expected error: m is below minimum" + ); + } + + #[test] + fn test_polynomial_invalid_m_above_max() { + let dist = DistributionFunction::Polynomial { + a: 2, + d: 1, + m: MAX_POL_M_PARAM + 1, + n: 3, + o: 0, + start_moment: Some(0), + b: 5, + min_value: Some(1), + max_value: Some(50), + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert!( + result.expect("expected result").first_error().is_some(), + "Expected error: m is above maximum" + ); + } + + #[test] + fn test_polynomial_invalid_n_above_max() { + let dist = DistributionFunction::Polynomial { + a: 2, + d: 1, + m: 3, + n: MAX_POL_N_PARAM + 1, + o: 0, + start_moment: Some(0), + b: 5, + min_value: Some(1), + max_value: Some(50), + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert!( + result.expect("expected result").first_error().is_some(), + "Expected error: n is above maximum" + ); + } + // 5. Test invalid: max_value exceeds MAX_DISTRIBUTION_PARAM. #[test] fn test_polynomial_invalid_max_exceeds_max_distribution() { @@ -1422,7 +1710,7 @@ mod tests { min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: max_value too high }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected an error when max_value exceeds MAX_DISTRIBUTION_PARAM" @@ -1443,7 +1731,7 @@ mod tests { min_value: Some(60), // min_value > max_value max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected an error when min_value is greater than max_value" @@ -1466,7 +1754,7 @@ mod tests { min_value: Some(1), max_value: Some(100), // Starting at max_value }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected an incoherence error when an increasing function starts at max_value" @@ -1489,7 +1777,7 @@ mod tests { min_value: Some(50), // Starting at min_value max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected an incoherence error when a decreasing function starts at min_value" @@ -1510,7 +1798,7 @@ mod tests { min_value: None, max_value: None, }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected valid").first_error().is_none(), "Expected no validation errors when boundaries are omitted" @@ -1540,7 +1828,7 @@ mod tests { 8, "Expected f(4) to be 8 for a fractional exponent of 3/2" ); - let validation_result = dist.validate(4); + let validation_result = dist.validate(4, PlatformVersion::latest()); assert!( validation_result .expect("expected valid") @@ -1561,11 +1849,11 @@ mod tests { n: 2, o: -3999, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(1000000), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); if let Err(err) = &result { panic!("Test failed: unexpected error: {:?}", err); } @@ -1586,11 +1874,11 @@ mod tests { n: 0, o: 1, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_exponential_invalid_zero_n") .first_error() @@ -1606,11 +1894,11 @@ mod tests { n: 2, o: 1, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_zero_m") @@ -1629,11 +1917,11 @@ mod tests { n: 2, o: 1, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_zero_a") @@ -1652,11 +1940,11 @@ mod tests { n: 2, o: 1, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: None, // Invalid: max_value must be set }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_max_missing_when_m_positive") @@ -1675,11 +1963,11 @@ mod tests { n: 2, o: MAX_DISTRIBUTION_PARAM as i64 + 1, // Invalid: `o` exceeds allowed range start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_o_too_large") @@ -1698,11 +1986,11 @@ mod tests { n: 2, o: 1, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(50), // Invalid: min > max max_value: Some(30), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_min_greater_than_max") @@ -1721,11 +2009,11 @@ mod tests { n: 4, o: 2, start_moment: Some(START_MOMENT), - c: 8, + b: 8, min_value: Some(2), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_exponential_valid_with_negative_m") .first_error() @@ -1741,11 +2029,11 @@ mod tests { n: 4, o: 1, start_moment: Some(START_MOMENT), - c: 8, + b: 8, min_value: Some(2), max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_exponential_valid_with_max_boundary") .first_error() @@ -1761,11 +2049,11 @@ mod tests { n: 1, o: 1, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_large_start_token_amount") @@ -1784,11 +2072,11 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(1000), // Small `max_value` }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_a_too_large_for_max") @@ -1807,11 +2095,11 @@ mod tests { n: 2, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(10), // Function starts at `min_value` max_value: Some(1000), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_starts_at_min") @@ -1830,11 +2118,11 @@ mod tests { n: 2, o: 1, start_moment: Some(0), - c: 5, + b: 5, min_value: Some(1), max_value: None, // Should fail }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_missing_max_for_positive_m") @@ -1853,11 +2141,11 @@ mod tests { n: 1, o: i64::MAX / 2, // Large `o` start_moment: Some(0), - c: 5, + b: 5, min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_large_o_overflow") @@ -1876,11 +2164,11 @@ mod tests { n: 2, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(10), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_exponential_invalid_a_too_small") @@ -1899,12 +2187,12 @@ mod tests { n: 10, o: -3, start_moment: Some(0), - c: 5, + b: 5, min_value: Some(10), max_value: Some(1000), }; - let result = dist.validate(5); + let result = dist.validate(5, PlatformVersion::latest()); match result { Ok(validation_result) => { @@ -1930,11 +2218,11 @@ mod tests { n: 4, o: 2, start_moment: Some(START_MOMENT), - c: 8, + b: 8, min_value: Some(5), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_exponential_valid_gentle_decay") .first_error() @@ -1950,11 +2238,11 @@ mod tests { n: 3, o: 5, // Shift start start_moment: Some(START_MOMENT), - c: 10, + b: 10, min_value: Some(5), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_exponential_valid_negative_m_with_o_offset") .first_error() @@ -1976,7 +2264,7 @@ mod tests { min_value: Some(1), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_logarithmic_valid") .first_error() @@ -1996,7 +2284,7 @@ mod tests { min_value: Some(1), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_logarithmic_invalid_zero_d") @@ -2019,7 +2307,7 @@ mod tests { min_value: Some(1), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_logarithmic_invalid_zero_n") @@ -2029,6 +2317,29 @@ mod tests { ); } + #[test] + fn test_logarithmic_invalid_zero_m() { + let dist = DistributionFunction::Logarithmic { + a: 4, + d: 10, + m: 0, // Invalid: this would make it a constant + n: 1, + o: 1, + start_moment: Some(0), + b: 10, + min_value: Some(1), + max_value: Some(100), + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert!( + result + .expect("no error on test_logarithmic_invalid_zero_m") + .first_error() + .is_some(), + "Expected m == 0 error" + ); + } + #[test] fn test_logarithmic_invalid_x_s_o_non_positive() { let dist = DistributionFunction::Logarithmic { @@ -2042,7 +2353,7 @@ mod tests { min_value: Some(1), max_value: Some(100), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_logarithmic_invalid_x_s_o_non_positive") @@ -2065,7 +2376,7 @@ mod tests { min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM + 1), // Invalid: max_value too large }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_logarithmic_invalid_max_greater_than_max_param") @@ -2088,7 +2399,7 @@ mod tests { min_value: Some(50), // Invalid: min > max max_value: Some(30), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_logarithmic_invalid_min_greater_than_max") @@ -2111,7 +2422,7 @@ mod tests { min_value: Some(2), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_logarithmic_valid_with_s_and_o") .first_error() @@ -2131,7 +2442,7 @@ mod tests { min_value: Some(2), max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!(result .expect("no error on test_logarithmic_valid_edge_case_max") .first_error() @@ -2153,7 +2464,7 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result .expect("no error on test_inverted_logarithmic_valid") @@ -2163,6 +2474,66 @@ mod tests { ); } + #[test] + fn test_inverted_logarithmic_invalid_zero_a() { + let dist = DistributionFunction::InvertedLogarithmic { + a: 0, + d: 1, + m: 1, + n: 100, + o: 1, + start_moment: Some(0), + b: 5, + min_value: Some(1), + max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert_eq!( + result.expect("expected valid").first_error().expect("expected error").to_string(), + "Invalid parameter `a` in token distribution function. Expected range: -32766 to 32767 except 0 (which we got)" + ); + } + + #[test] + fn test_inverted_logarithmic_invalid_too_low_a() { + let dist = DistributionFunction::InvertedLogarithmic { + a: -50000, + d: 1, + m: 1, + n: 100, + o: 1, + start_moment: Some(0), + b: 5, + min_value: Some(1), + max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert_eq!( + result.expect("expected valid").first_error().expect("expected error").to_string(), + "Invalid parameter `a` in token distribution function. Expected range: -32766 to 32767 except 0 (which we got)" + ); + } + + #[test] + fn test_inverted_logarithmic_invalid_too_high_a() { + let dist = DistributionFunction::InvertedLogarithmic { + a: 50000, + d: 1, + m: 1, + n: 100, + o: 1, + start_moment: Some(0), + b: 5, + min_value: Some(1), + max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert_eq!( + result.expect("expected valid").first_error().expect("expected error").to_string(), + "Invalid parameter `a` in token distribution function. Expected range: -32766 to 32767 except 0 (which we got)" + ); + } + #[test] fn test_inverted_logarithmic_invalid_divide_by_zero_d() { let dist = DistributionFunction::InvertedLogarithmic { @@ -2176,7 +2547,7 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected error: division by zero (d = 0)" @@ -2196,7 +2567,7 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected error: division by zero (n = 0)" @@ -2216,7 +2587,7 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected error: division by zero (m = 0)" @@ -2236,7 +2607,7 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected error: log argument must be positive" @@ -2256,7 +2627,7 @@ mod tests { min_value: Some(1), max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected error: s exceeds MAX_DISTRIBUTION_PARAM" @@ -2276,7 +2647,7 @@ mod tests { min_value: Some(60), // Invalid: min > max max_value: Some(50), }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected error: min_value > max_value" @@ -2296,13 +2667,35 @@ mod tests { min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected valid").first_error().is_none(), "Expected valid function with max boundary" ); } + #[test] + fn test_inverted_logarithmic_valid_with_min_a() { + // Since `a` is negative, the inverted logarithmic function is increasing, + // but it starts at the maximum value already, so it will never produce a higher value. + let dist = DistributionFunction::InvertedLogarithmic { + a: i64::MIN, + d: 1, + m: 1, + n: 100, + o: 1, + start_moment: Some(0), + b: 5, + min_value: Some(1), + max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary + }; + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); + assert_eq!( + result.expect("expected valid").first_error().expect("expected error").to_string(), + "Invalid parameter `a` in token distribution function. Expected range: -32766 to 32767 except 0 (which we got)" + ); + } + #[test] fn test_inverted_logarithmic_invalid_starting_at_max_for_increasing() { let dist = DistributionFunction::InvertedLogarithmic { @@ -2316,7 +2709,7 @@ mod tests { min_value: Some(1), max_value: Some(50), // Function already at max }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected error: increasing function starts at max_value" @@ -2336,7 +2729,7 @@ mod tests { min_value: Some(1), max_value: Some(50), // Function already at min }; - let result = dist.validate(START_MOMENT); + let result = dist.validate(START_MOMENT, PlatformVersion::latest()); assert!( result.expect("expected error").first_error().is_some(), "Expected error: decreasing function starts at min_value" 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 407f77b20c8..d7873c3d2bf 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,7 +1,7 @@ mod accessors; mod evaluate_interval; -use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::{DistributionFunction, MAX_DISTRIBUTION_CYCLES_PARAM}; use crate::prelude::{BlockHeightInterval, DataContract, EpochInterval, TimestampMillisInterval}; use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; @@ -154,12 +154,15 @@ impl RewardDistributionType { &self, start_moment: RewardDistributionMoment, current_cycle_moment: RewardDistributionMoment, - max_cycles: u32, + max_non_fixed_amount_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 max_cycles = if matches!(self.function(), DistributionFunction::FixedAmount { .. }) { + // This is much easier to calculate as it's always fixed, so we can have a near unlimited amount of cycles + // + MAX_DISTRIBUTION_CYCLES_PARAM + } else { + max_non_fixed_amount_cycles as u64 + }; let interval = self.interval(); // Calculate maximum allowed moment based on distribution type @@ -169,14 +172,14 @@ impl RewardDistributionType { RewardDistributionMoment::BlockBasedMoment(step), RewardDistributionMoment::BlockBasedMoment(current), ) => Ok(RewardDistributionMoment::BlockBasedMoment( - (start + step.saturating_mul(max_cycles as u64)).min(current), + (start + step.saturating_mul(max_cycles)).min(current), )), ( RewardDistributionMoment::TimeBasedMoment(start), RewardDistributionMoment::TimeBasedMoment(step), RewardDistributionMoment::TimeBasedMoment(current), ) => Ok(RewardDistributionMoment::TimeBasedMoment( - (start + step.saturating_mul(max_cycles as u64)).min(current), + (start + step.saturating_mul(max_cycles)).min(current), )), ( RewardDistributionMoment::EpochBasedMoment(start), diff --git a/packages/rs-dpp/src/errors/consensus/basic/data_contract/invalid_token_distribution_function_incoherence_error.rs b/packages/rs-dpp/src/errors/consensus/basic/data_contract/invalid_token_distribution_function_incoherence_error.rs index da87ea8a346..3224b76e8df 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/data_contract/invalid_token_distribution_function_incoherence_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/data_contract/invalid_token_distribution_function_incoherence_error.rs @@ -10,7 +10,7 @@ use bincode::{Decode, Encode}; #[derive( Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize, )] -#[error("Incoherent parameters in token distribution function: {}.", message)] +#[error("Incoherent parameters in token distribution function: {}", message)] #[platform_serialize(unversioned)] pub struct InvalidTokenDistributionFunctionIncoherenceError { /* diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index 538fe4c336b..0c568ede8bb 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -103,10 +103,10 @@ assert_matches = "1.5.0" drive-abci = { path = ".", features = ["testing-config", "mocks"] } bls-signatures = { git = "https://github.com/dashpay/bls-signatures", tag = "1.3.3" } mockall = { version = "0.13" } - # For tests of grovedb verify rocksdb = { version = "0.23.0" } integer-encoding = { version = "4.0.0" } +serial_test = { version = "3.2.0" } [features] default = [] 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 72d9ea6b700..0a356690b5d 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 @@ -8,6 +8,10 @@ use dpp::data_contract::TokenConfiguration; use dpp::state_transition::batch_transition::BatchTransition; use platform_version::version::PlatformVersion; use rand::prelude::StdRng; + +/// Initial contract balance, as hardcoded in the contract definition (JSON file). +const INITIAL_BALANCE: u64 = 100_000; + mod perpetual_distribution_block { use dpp::block::epoch::Epoch; use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; @@ -18,6 +22,7 @@ mod perpetual_distribution_block { 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(); @@ -83,7 +88,7 @@ mod perpetual_distribution_block { let processing_result = platform .platform .process_raw_state_transitions( - &vec![claim_serialized_transition.clone()], + &[claim_serialized_transition.clone()], &platform_state, &BlockInfo { time_ms: 10_200_100_000, @@ -151,7 +156,7 @@ mod perpetual_distribution_block { let processing_result = platform .platform .process_raw_state_transitions( - &vec![claim_serialized_transition.clone()], + &[claim_serialized_transition.clone()], &platform_state, &BlockInfo { time_ms: 10_200_100_000, @@ -168,7 +173,7 @@ mod perpetual_distribution_block { assert_matches!( processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( + [PaidConsensusError( ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), _ )] @@ -221,7 +226,7 @@ mod perpetual_distribution_block { let processing_result = platform .platform .process_raw_state_transitions( - &vec![claim_serialized_transition.clone()], + &[claim_serialized_transition.clone()], &platform_state, &BlockInfo { time_ms: 10_200_100_000, @@ -332,7 +337,7 @@ mod perpetual_distribution_block { let processing_result = platform .platform .process_raw_state_transitions( - &vec![claim_serialized_transition.clone()], + &[claim_serialized_transition.clone()], &platform_state, &BlockInfo { time_ms: 10_200_100_000, @@ -349,7 +354,7 @@ mod perpetual_distribution_block { assert_matches!( processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( + [PaidConsensusError( ConsensusError::StateError(StateError::InvalidTokenClaimWrongClaimant(_)), _ )] @@ -454,7 +459,7 @@ mod perpetual_distribution_block { let processing_result = platform .platform .process_raw_state_transitions( - &vec![claim_serialized_transition.clone()], + &[claim_serialized_transition.clone()], &platform_state, &BlockInfo { time_ms: 10_200_100_000, @@ -494,3 +499,2561 @@ mod perpetual_distribution_block { assert_eq!(token_balance, Some(200)); } } + +#[cfg(test)] +mod fixed_amount { + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; + + use super::{test_suite::*, INITIAL_BALANCE}; + use dpp::{ + consensus::{state::state_error::StateError, ConsensusError}, + data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction, + }; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::{MAX_DISTRIBUTION_CYCLES_PARAM, MAX_DISTRIBUTION_PARAM}; + + #[test] + fn fixed_amount_1_interval_1() -> Result<(), String> { + check_heights( + DistributionFunction::FixedAmount { amount: 1 }, + &[ + TestStep::new(1, 100_001, true), + TestStep::new(2, 100_002, true), + TestStep::new(3, 100_003, true), + TestStep::new(50, 100_050, true), + ], + None, + 1, + None, + ) + } + + // Given some token configuration, + // When a claim is made at block 41 and 50, + // Then the claim should be successful. + // If we claim again in the interval it should not be successful. + #[test] + fn fixed_amount_50_interval_10() { + check_heights( + DistributionFunction::FixedAmount { amount: 50 }, + &[ + TestStep::new(1, 100_000, false), + TestStep::new(41, 100_200, true), + TestStep::new(46, 100_200, false), + TestStep::new(50, 100_250, true), + TestStep::new(51, 100_250, false), + ], + None, + 10, + None, + ) + .expect("\n-> fixed amount should pass"); + } + + /// Test case for overflow error. + /// + /// + /// claim at height 1000000000000: claim failed: assertion 0 failed: expected SuccessfulExecution, + /// got [InternalError(\"storage: protocol: overflow error: Overflow in FixedAmount evaluation\")]" + #[test] + fn fixed_amount_at_trillionth_block() { + check_heights( + DistributionFunction::FixedAmount { + amount: 1_000_000_000, + }, + &[ + TestStep::new(41, INITIAL_BALANCE + 4 * 1_000_000_000, true), + TestStep::new(46, INITIAL_BALANCE + 4 * 1_000_000_000, false), + TestStep::new(50, INITIAL_BALANCE + 5 * 1_000_000_000, true), + TestStep::new(51, INITIAL_BALANCE + 5 * 1_000_000_000, false), + // We will be getting MAX_DISTRIBUTION_CYCLES_PARAM intervals of 1_000_000_000 tokens, and we already had 5 + TestStep::new( + 1_000_000_000_000, + INITIAL_BALANCE + (MAX_DISTRIBUTION_CYCLES_PARAM + 5) * 1_000_000_000, + true, + ), + // We will be getting another MAX_DISTRIBUTION_CYCLES_PARAM intervals of 1_000_000_000 tokens, and we already had 5 + MAX_DISTRIBUTION_CYCLES_PARAM + TestStep::new( + 1_000_000_000_000, + INITIAL_BALANCE + (MAX_DISTRIBUTION_CYCLES_PARAM * 2 + 5) * 1_000_000_000, + true, + ), + ], + None, + 10, + None, + ) + .expect("\n-> fixed amount should pass"); + } + + #[test] + /// Given a fixed amount distribution with value of 0, + /// When we try to claim, + /// Then we always fail and the balance remains unchanged. + fn fixed_amount_0() { + check_heights( + DistributionFunction::FixedAmount { amount: 0 }, + &[(41, 100000, false)], + None, + 10, + None, + ) + .expect_err("\namount should not be 0\n"); + } + + #[test] + /// Given a fixed amount distribution with value of 1_000_000 and max_supply of 200_000, + /// When we try to claim, + /// Then we always fail and the balance remains unchanged. + fn fixed_amount_gt_max_supply() { + let test = TestStep { + name: "test_fixed_amount_above_max_supply".to_string(), + base_height: 41, + base_time_ms: Default::default(), + expected_balance: None, + claim_transition_assertions: vec![|v| match v { + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError(_)), + _, + )] => Ok(()), + _ => Err(format!("expected TokenMintPastMaxSupplyError, got {:?}", v)), + }], + }; + check_heights( + DistributionFunction::FixedAmount { amount: 1_000_000 }, + &[test], + None, + 10, + Some(Some(200_000)), + ) + .expect("\nfixed amount zero increase\n"); + } + + /// Given a fixed amount distribution with value of u64::MAX, + /// When I claim tokens, + /// Then I don't get an InternalError. + #[test] + fn test_block_based_perpetual_fixed_amount_u64_max_should_error_at_validation() { + check_heights( + DistributionFunction::FixedAmount { amount: u64::MAX }, + &[TestStep::new(41, 100_000, false)], + None, + 10, + None, + ) + .expect_err("u64::Max is too much for DistributionFunction::FixedAmount"); + } + + /// Given a fixed amount distribution with value of u64::MAX, + /// When I claim tokens, + /// Then I don't get an InternalError. + #[test] + fn test_block_based_perpetual_fixed_amount_max_distribution() { + check_heights( + DistributionFunction::FixedAmount { + amount: MAX_DISTRIBUTION_PARAM, + }, + &[TestStep::new( + 41, + 4 * MAX_DISTRIBUTION_PARAM + 100_000, + true, + )], + None, + 10, + None, + ) + .expect("MAX_DISTRIBUTION_PARAM should be valid DistributionFunction::FixedAmount"); + } +} +mod random { + use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, + }; + + use crate::execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::test_suite::TestSuite; + + use super::{ + test_suite::{check_heights, TestStep}, + INITIAL_BALANCE, + }; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::MAX_DISTRIBUTION_PARAM; + use dpp::data_contract::{ + associated_token::{ + token_configuration::accessors::v0::TokenConfigurationV0Getters, + token_distribution_key::TokenDistributionType, + token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters, + token_perpetual_distribution::{ + distribution_function::DistributionFunction, + distribution_recipient::TokenDistributionRecipient, + reward_distribution_type::RewardDistributionType, v0::TokenPerpetualDistributionV0, + TokenPerpetualDistribution, + }, + }, + TokenConfiguration, + }; + + /// Given a random distribution function with min=0, max=100, + /// When I claim tokens at various heights, + /// Then I get deterministic balances at those heights. + #[test] + #[ignore] + fn test_random_max_supply() -> Result<(), String> { + let steps = [ + TestStep::new(41, 100_192, true), + TestStep::new(46, 100_192, false), + TestStep::new(50, 100_263, true), + TestStep::new(59, 100_263, false), + TestStep::new(60, 100_310, true), + ]; + + for max_supply in [None, Some(1_000_000)] { + check_heights( + DistributionFunction::Random { min: 0, max: 100 }, + &steps, + None, + 10, + Some(max_supply), + )?; + } + Ok(()) + } + + /// Given a random distribution function with min=0, max=0, + /// When I claim tokens at various heights, + /// Then claim fails and I get the same balance at those heights. + #[test] + #[ignore] + fn test_block_based_perpetual_random_0_0() { + check_heights( + DistributionFunction::Random { min: 0, max: 0 }, + &[ + TestStep::new(41, INITIAL_BALANCE, false), + TestStep::new(50, INITIAL_BALANCE, false), + TestStep::new(100, INITIAL_BALANCE, false), + ], + None, + 10, + None, + ) + .expect("no rewards"); + } + #[test] + #[ignore] + fn test_block_based_perpetual_random_0_u64_max_should_error_at_validation() { + check_heights( + DistributionFunction::Random { + min: 0, + max: u64::MAX, + }, + &[TestStep::new(41, INITIAL_BALANCE, false)], + None, + 10, + None, + ) + .expect_err("max is too much for DistributionFunction::Random"); + } + + #[test] + #[ignore] + fn test_block_based_perpetual_random_0_MAX_distribution_param() { + check_heights( + DistributionFunction::Random { + min: 0, + max: MAX_DISTRIBUTION_PARAM, + }, + &[ + TestStep::new(41, 382777733174502, true), + TestStep::new(50, 447703202535488, true), + TestStep::new(100, 1080112432401531, true), + ], + None, + 10, + None, + ) + .expect("no rewards"); + } + + /// Given a random distribution function with min=10, max=30, + /// When I claim tokens at various heights, + /// Then I get a distribution of balances that is close to the maximum entropy. + #[test] + #[ignore] + fn test_block_based_perpetual_random_10_30_entropy() { + const N: u64 = 200; + const MIN: u64 = 10; + const MAX: u64 = 30; + let tests: Vec<_> = (1..=N) + .map(|i| TestStep { + name: format!("test_{}", i), + base_height: i - 1, + base_time_ms: Default::default(), + + expected_balance: None, + claim_transition_assertions: Default::default(), + }) + .collect(); + + let balances = Arc::new(Mutex::new(Vec::new())); + let balances_result = balances.clone(); + + let mut suite = TestSuite::new( + 10_200_000_000, + 0, + TokenDistributionType::Perpetual, + Some(move |token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 1, + function: DistributionFunction::Random { min: MIN, max: MAX }, + }, + distribution_recipient: TokenDistributionRecipient::ContractOwner, + }, + ))); + }), + ) + .with_step_success_fn(move |balance: u64| { + balances.lock().unwrap().push(balance); + }); + + suite.execute(&tests).expect("should execute"); + + let data = balances_result.lock().unwrap(); + // subtract balance from previous step (for first step, subtract initial balance of 100_000) + let diffs: Vec = data + .iter() + .scan(INITIAL_BALANCE, |prev, &x| { + let diff = x - *prev; + *prev = x; + Some(diff) + }) + .collect(); + + let entropy = calculate_entropy(&diffs); + let max_entropy: f64 = ((MAX - MIN) as f64).log2(); + let entropy_diff = (max_entropy - entropy).abs() / max_entropy; + + tracing::debug!("Data: {:?}", diffs); + tracing::info!( + "Entropy: {}, max entropy: {}, difference: {}%", + entropy, + max_entropy, + entropy_diff * 100.0 + ); + + // assert that the entropy is close to the maximum entropy + assert!( + entropy_diff < 0.05, + "Entropy is not close to maximum entropy" + ); + } + + // HELPERS // + + fn calculate_entropy(data: &[u64]) -> f64 { + let mut counts = BTreeMap::new(); + let len = data.len() as f64; + + // Count the occurrences of each value + for &value in data { + *counts.entry(value).or_insert(0) += 1; + } + + // Calculate the probability of each value and apply the Shannon entropy formula + let mut entropy = 0.0; + for &count in counts.values() { + let probability = count as f64 / len; + entropy -= probability * probability.log2(); + } + + entropy + } +} + +mod step_decreasing { + use dpp::balances::credits::TokenAmount; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::{DistributionFunction, MAX_DISTRIBUTION_PARAM}; + use dpp::prelude::{BlockHeight, BlockHeightInterval}; + use crate::{execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::test_suite::check_heights}; + use crate::execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::INITIAL_BALANCE; + + const DECREASING_ONE_PERCENT_100K: [TokenAmount; 500] = [ + 100000, 99000, 98010, 97029, 96058, 95097, 94146, 93204, 92271, 91348, 90434, 89529, 88633, + 87746, 86868, 85999, 85139, 84287, 83444, 82609, 81782, 80964, 80154, 79352, 78558, 77772, + 76994, 76224, 75461, 74706, 73958, 73218, 72485, 71760, 71042, 70331, 69627, 68930, 68240, + 67557, 66881, 66212, 65549, 64893, 64244, 63601, 62964, 62334, 61710, 61092, 60481, 59876, + 59277, 58684, 58097, 57516, 56940, 56370, 55806, 55247, 54694, 54147, 53605, 53068, 52537, + 52011, 51490, 50975, 50465, 49960, 49460, 48965, 48475, 47990, 47510, 47034, 46563, 46097, + 45636, 45179, 44727, 44279, 43836, 43397, 42963, 42533, 42107, 41685, 41268, 40855, 40446, + 40041, 39640, 39243, 38850, 38461, 38076, 37695, 37318, 36944, 36574, 36208, 35845, 35486, + 35131, 34779, 34431, 34086, 33745, 33407, 33072, 32741, 32413, 32088, 31767, 31449, 31134, + 30822, 30513, 30207, 29904, 29604, 29307, 29013, 28722, 28434, 28149, 27867, 27588, 27312, + 27038, 26767, 26499, 26234, 25971, 25711, 25453, 25198, 24946, 24696, 24449, 24204, 23961, + 23721, 23483, 23248, 23015, 22784, 22556, 22330, 22106, 21884, 21665, 21448, 21233, 21020, + 20809, 20600, 20394, 20190, 19988, 19788, 19590, 19394, 19200, 19008, 18817, 18628, 18441, + 18256, 18073, 17892, 17713, 17535, 17359, 17185, 17013, 16842, 16673, 16506, 16340, 16176, + 16014, 15853, 15694, 15537, 15381, 15227, 15074, 14923, 14773, 14625, 14478, 14333, 14189, + 14047, 13906, 13766, 13628, 13491, 13356, 13222, 13089, 12958, 12828, 12699, 12572, 12446, + 12321, 12197, 12075, 11954, 11834, 11715, 11597, 11481, 11366, 11252, 11139, 11027, 10916, + 10806, 10697, 10590, 10484, 10379, 10275, 10172, 10070, 9969, 9869, 9770, 9672, 9575, 9479, + 9384, 9290, 9197, 9105, 9013, 8922, 8832, 8743, 8655, 8568, 8482, 8397, 8313, 8229, 8146, + 8064, 7983, 7903, 7823, 7744, 7666, 7589, 7513, 7437, 7362, 7288, 7215, 7142, 7070, 6999, + 6929, 6859, 6790, 6722, 6654, 6587, 6521, 6455, 6390, 6326, 6262, 6199, 6137, 6075, 6014, + 5953, 5893, 5834, 5775, 5717, 5659, 5602, 5545, 5489, 5434, 5379, 5325, 5271, 5218, 5165, + 5113, 5061, 5010, 4959, 4909, 4859, 4810, 4761, 4713, 4665, 4618, 4571, 4525, 4479, 4434, + 4389, 4345, 4301, 4257, 4214, 4171, 4129, 4087, 4046, 4005, 3964, 3924, 3884, 3845, 3806, + 3767, 3729, 3691, 3654, 3617, 3580, 3544, 3508, 3472, 3437, 3402, 3367, 3333, 3299, 3266, + 3233, 3200, 3168, 3136, 3104, 3072, 3041, 3010, 2979, 2949, 2919, 2889, 2860, 2831, 2802, + 2773, 2745, 2717, 2689, 2662, 2635, 2608, 2581, 2555, 2529, 2503, 2477, 2452, 2427, 2402, + 2377, 2353, 2329, 2305, 2281, 2258, 2235, 2212, 2189, 2167, 2145, 2123, 2101, 2079, 2058, + 2037, 2016, 1995, 1975, 1955, 1935, 1915, 1895, 1876, 1857, 1838, 1819, 1800, 1782, 1764, + 1746, 1728, 1710, 1692, 1675, 1658, 1641, 1624, 1607, 1590, 1574, 1558, 1542, 1526, 1510, + 1494, 1479, 1464, 1449, 1434, 1419, 1404, 1389, 1375, 1361, 1347, 1333, 1319, 1305, 1291, + 1278, 1265, 1252, 1239, 1226, 1213, 1200, 1188, 1176, 1164, 1152, 1140, 1128, 1116, 1104, + 1092, 1081, 1070, 1059, 1048, 1037, 1026, 1015, 1004, 993, 983, 973, 963, 953, 943, 933, + 923, 913, 903, 893, 884, 875, 866, 857, 848, 839, 830, 821, 812, 803, 794, 786, 778, 770, + 762, 754, 746, 738, 730, 722, 714, 706, 698, 691, 684, 677, 670, 663, 656, 649, 642, 635, + 628, 621, 614, + ]; + + fn sum_till_for_100k_step_1_interval_1( + distribution_heights: Vec, + ) -> Vec { + distribution_heights + .into_iter() + .map(|height| { + (1..=height) + .map(|height| DECREASING_ONE_PERCENT_100K[height as usize]) + .sum::() + + INITIAL_BALANCE + }) + .collect() + } + + const DECREASING_HALF_100K: [TokenAmount; 20] = [ + 100000, 50000, 25000, 12500, 6250, 3125, 1562, 781, 390, 195, 97, 48, 24, 12, 6, 3, 1, 0, + 0, 0, + ]; + + fn sum_till_for_100k_halving( + distribution_heights: Vec, + reduce_every_block_count: u32, + interval: BlockHeightInterval, + start_decreasing_step: u64, + ) -> Vec { + distribution_heights + .into_iter() + .map(|height| { + // How many full intervals have passed by `height`? + let end = height / interval; + + // If not even 1 interval, return the initial balance + if end < 1 { + return INITIAL_BALANCE; + } + + // Sum each interval’s distribution + let sum_halved = (1..=end) + .map(|i| { + if i < start_decreasing_step { + // Before start offset => always distribute the first entry + DECREASING_HALF_100K[0] + } else { + // After offset => normal indexing + let offset_index = ((i - start_decreasing_step) as usize) + / (reduce_every_block_count as usize); + + DECREASING_HALF_100K.get(offset_index).copied().unwrap_or(0) + } + }) + .sum::(); + + INITIAL_BALANCE + sum_halved + }) + .collect() + } + + #[test] + fn claim_every_block() { + run_test( + 1, + 1, + 100, + None, + None, + 10_000, + 0, + Some(1), + (1..5).step_by(1).collect(), + 1, + vec![ + INITIAL_BALANCE + 9_900, + INITIAL_BALANCE + 9_900 + 9_801, + INITIAL_BALANCE + 9_900 + 9_801 + 9_702, + INITIAL_BALANCE + 9_900 + 9_801 + 9_702 + 9_604, + ], + ) + .expect("expected to succeed"); + } + + #[test] + fn claim_every_5_blocks() { + run_test( + 1, + 1, + 100, + None, + None, + 10_000, + 0, + Some(1), + vec![1, 6, 11], + 1, + vec![ + INITIAL_BALANCE + 9_900, + INITIAL_BALANCE + 9_900 + 9_801 + 9_702 + 9_604 + 9_507 + 9_411, + INITIAL_BALANCE + + 9_900 + + 9_801 + + 9_702 + + 9_604 + + 9_507 + + 9_411 + + 9_316 + + 9_222 + + 9_129 + + 9_037 + + 8_946, + ], + ) + .expect("expected to succeed"); + } + + #[test] + fn claim_with_1_percent_increase_should_fail() { + let result_str = run_test( + 1, + 101, + 100, + None, + None, + 100_000, + 0, + Some(1), + (1..1000).step_by(100).collect(), + 1, + vec![], + ) + .expect_err("should not allow to increase"); + assert!( + result_str.contains("Invalid parameter tuple in token distribution function: `decrease_per_interval_numerator` must be smaller than `decrease_per_interval_denominator`"), + "Unexpected panic message: {result_str}" + ); + } + + #[test] + fn claim_with_no_decrease_should_fail() { + let result_str = run_test( + 1, + 0, + 100, + None, + None, + 100_000, + 0, + Some(1), + (1..1000).step_by(100).collect(), + 1, + vec![], + ) + .expect_err("should not allow to increase"); + assert!( + result_str.contains("Invalid parameter `decrease_per_interval_numerator` in token distribution function. Expected range: 1 to 65535"), + "Unexpected panic message: {result_str}" + ); + } + + #[test] + fn claim_every_10_blocks_on_100k() { + let steps = (1..500).step_by(10).collect::>(); + run_test( + 1, + 1, + 100, + None, + Some(1024), + 100_000, + 0, + Some(1), + steps.clone(), + 1, + sum_till_for_100k_step_1_interval_1(steps), + ) + .expect("should pass"); + } + + #[test] + fn claim_every_block_on_100k_128_default_steps() { + let steps = (1..140).step_by(1).collect::>(); + let start_steps = (1..129).step_by(1).collect::>(); + let start_steps_expected_amounts = sum_till_for_100k_step_1_interval_1(start_steps.clone()); + let later_steps = (129..140).step_by(1).collect::>(); + let later_steps_expected_amounts = later_steps + .iter() + .map(|_| *start_steps_expected_amounts.last().unwrap()) + .collect::>(); + let mut expected_amounts = start_steps_expected_amounts; + expected_amounts.extend(later_steps_expected_amounts); + run_test( + 1, + 1, + 100, + None, + None, + 100_000, + 0, + Some(1), + steps.clone(), + 1, + expected_amounts, + ) + .expect("should pass"); + } + + #[test] + fn claim_every_block_on_100k_128_default_steps_with_trailing_distribution() { + let steps = (1..200).step_by(1).collect::>(); + let start_steps = (1..129).step_by(1).collect::>(); + let start_steps_expected_amounts = sum_till_for_100k_step_1_interval_1(start_steps.clone()); + let later_steps = (129..200).step_by(1).collect::>(); + let later_steps_expected_amounts = later_steps + .iter() + .map(|&i| *start_steps_expected_amounts.last().unwrap() + (i - 128) * 10) + .collect::>(); + let mut expected_amounts = start_steps_expected_amounts; + expected_amounts.extend(later_steps_expected_amounts); + run_test( + 1, + 1, + 100, + None, + None, + 100_000, + // 10 credits per step afterward + 10, + Some(1), + steps.clone(), + 1, + expected_amounts, + ) + .expect("should pass"); + } + + #[test] + fn claim_every_10_blocks_on_100k_128_default_steps() { + let steps = (1..500).step_by(10).collect::>(); + let start_steps = (1..128).step_by(10).collect::>(); + let start_steps_expected_amounts = sum_till_for_100k_step_1_interval_1(start_steps); + let step_128_amount = sum_till_for_100k_step_1_interval_1(vec![128]).remove(0); + let later_steps = (141..500).step_by(10).collect::>(); + let later_steps_expected_amounts = later_steps + .iter() + .map(|_| step_128_amount) + .collect::>(); + let mut expected_amounts = start_steps_expected_amounts; + expected_amounts.push(step_128_amount); // at 131. + expected_amounts.extend(later_steps_expected_amounts); + run_test( + 1, + 1, + 100, + None, + None, + 100_000, + 0, + Some(1), + steps.clone(), + 1, + expected_amounts, + ) + .expect("should pass"); + } + + #[test] + fn claim_128_default_steps_480_max_token_redemption_cycles() { + // We can only claim 128 events at a time. + // The step_wise distribution stops after 500 from the start. + let claim_heights = vec![1, 400, 400, 400, 400, 401, 450, 500]; + // 129 is the first claim for 400 because we can only do 128 cycles at a time + // Then 257 because we are doing 128 cycles and 129 + 128 = 257 + // The last one is 480 because our max steps is 480 + let expected_amounts = + sum_till_for_100k_step_1_interval_1(vec![1, 129, 257, 385, 400, 401, 450, 480]); + run_test( + 1, + 1, + 100, + None, + Some(480), + 100_000, + 0, + Some(1), + // This will give us 1, 151, 301, 400, 401, 450 for result values + claim_heights, + 1, + expected_amounts, + ) + .expect("should pass"); + } + + #[test] + fn decrease_where_min_would_not_matter_min_1_100() { + let claim_heights = vec![1, 2, 3, 10, 100]; + let expected_amounts = sum_till_for_100k_step_1_interval_1(claim_heights.clone()); + for min in [1, 100] { + run_test( + 1, + 1, + 100, + None, + None, + 100_000, + 0, + Some(min), + claim_heights.clone(), + 1, + expected_amounts.clone(), + ) + .map_err(|e| format!("failed with min {}: {}", min, e)) + .expect("should pass"); + } + } + + #[test] + fn heavy_decrease_to_min_with_min_various_values() { + let claim_heights = vec![1, 2, 3, 10, 100]; + for min in [1, 10] { + let expected_amounts = vec![ + INITIAL_BALANCE + min, + INITIAL_BALANCE + 2 * min, + INITIAL_BALANCE + 3 * min, + INITIAL_BALANCE + 10 * min, + INITIAL_BALANCE + 100 * min, + ]; + run_test( + 1, + u16::MAX - 1, + u16::MAX, + None, + None, + 100_000, + 0, + Some(min), + claim_heights.clone(), + 1, + expected_amounts, + ) + .map_err(|e| format!("failed with min {}: {}", min, e)) + .expect("should pass"); + } + } + + #[test] + fn full_decrease_min_eq_u64_max() { + let result_str = run_test( + 1, + u16::MAX - 1, + u16::MAX, + None, + None, + MAX_DISTRIBUTION_PARAM, + 0, + Some(u64::MAX), + vec![1, 2, 3, 10, 100], + 1, + vec![], + ) + .expect_err("should fail"); + assert!( + result_str.contains("Invalid parameter tuple in token distribution function: `n` must be greater than or equal to `min_value`"), + "Unexpected panic message: {result_str}" + ); + } + #[test] + fn full_decrease_min_eq_max_distribution() { + run_test( + 1, + u16::MAX - 1, + u16::MAX, + None, + None, + MAX_DISTRIBUTION_PARAM, + 0, + Some(MAX_DISTRIBUTION_PARAM), + vec![1, 2, 10], + 1, + vec![ + MAX_DISTRIBUTION_PARAM + INITIAL_BALANCE, + MAX_DISTRIBUTION_PARAM * 2 + INITIAL_BALANCE, + MAX_DISTRIBUTION_PARAM * 10 + INITIAL_BALANCE, + ], + ) + .expect("should succeed"); + } + + #[test] + fn distribute_max_distribution_param_every_step() { + let claim_heights = (1..65_536).step_by(128).collect::>(); + let expected_balances = claim_heights + .iter() + .map(|&height| { + MAX_DISTRIBUTION_PARAM + .saturating_mul(height) + .saturating_add(INITIAL_BALANCE) + .min(i64::MAX as u64) + }) + .collect(); + run_test( + 1, + u16::MAX - 1, + u16::MAX, + None, + None, + MAX_DISTRIBUTION_PARAM, + MAX_DISTRIBUTION_PARAM, + Some(MAX_DISTRIBUTION_PARAM), + claim_heights, + 1, + expected_balances, + ) + .expect("should succeed"); + } + + #[test] + fn start_over_max_distribution_param_should_fail() { + let result_str = run_test( + 1, + 1, + u16::MAX, + None, + None, + MAX_DISTRIBUTION_PARAM + 1, + 0, + None, + vec![1, 2, 10], + 1, + vec![], + ) + .expect_err("should fail"); + assert!( + result_str.contains("Invalid parameter `n` in token distribution function. Expected range: 1 to 281474976710655"), + "Unexpected panic message: {result_str}" + ); + } + + #[test] + fn half_decrease_changing_step_5_distribution_interval_1() { + let step = 5; // Every 5 blocks the amount divides by 1/2 + let distribution_interval = 1; // The payout happens every block + let claim_heights = vec![5, 10, 18, 22, 100]; + let expected_balances = + sum_till_for_100k_halving(claim_heights.clone(), step, distribution_interval, 0); + run_test( + step, + 1, + 2, + None, + None, + 100_000, + 0, + None, + claim_heights, + distribution_interval, + expected_balances, + ) + .expect("should pass"); + } + + #[test] + fn half_decrease_changing_step_5_distribution_interval_5() { + let step = 5; // Every 25 blocks (5 x distribution interval) the amount divides by 1/2 + let distribution_interval = 5; // The payout happens every 5 blocks + let claim_heights = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 18, 22, 25, 26, 51, 100]; + let expected_balances = + sum_till_for_100k_halving(claim_heights.clone(), step, distribution_interval, 0); + run_test( + step, + 1, + 2, + None, + None, + 100_000, + 0, + None, + claim_heights, + distribution_interval, + expected_balances, + ) + .expect("should pass"); + } + + #[test] + fn half_decrease_changing_step_24_distribution_interval_1000() { + let step = 24; // Every 24000 blocks (24 x distribution interval) the amount divides by 1/2 + let distribution_interval = 1000; // The payout happens every 400 blocks + let claim_heights = vec![3000, 45000, 60000, 300000, 300000]; + let value_heights = vec![3000, 45000, 60000, 60000 + 128 * 1000, 300000]; + let expected_balances = + sum_till_for_100k_halving(value_heights, step, distribution_interval, 0); + run_test( + step, + 1, + 2, + None, + None, + 100_000, + 0, + None, + claim_heights, + distribution_interval, + expected_balances, + ) + .expect("should pass"); + } + + #[test] + fn half_decrease_changing_step_24_distribution_interval_1000_start_height_2000() { + let step = 24; // Every 24000 blocks (24 x distribution interval) the amount divides by 1/2 + let distribution_interval = 1000; // The payout happens every 400 blocks + let claim_heights = vec![3000, 23000, 24000, 25000, 43000, 44000, 300000, 300000]; + let start_height = 2000; + let value_heights = vec![ + 3000, + 23000, + 24000, + 25000, + 43000, + 44000, + 44000 + 128 * 1000, + 300000, + ]; + let expected_balances = sum_till_for_100k_halving( + value_heights, + step, + distribution_interval, + start_height / distribution_interval, + ); + run_test( + step, + 1, + 2, + Some(start_height / distribution_interval), + None, + 100_000, + 0, + None, + claim_heights, + distribution_interval, + expected_balances, + ) + .expect("should pass"); + } + + /// Test various combinations of [DistributionFunction::StepDecreasingAmount] distribution. + #[allow(clippy::too_many_arguments)] + fn run_test( + step_count: u32, + decrease_per_interval_numerator: u16, + decrease_per_interval_denominator: u16, + start_decreasing_offset: Option, + max_interval_count: Option, + distribution_start_amount: TokenAmount, + trailing_distribution_interval_amount: TokenAmount, + min_value: Option, + claim_heights: Vec, + distribution_interval: BlockHeightInterval, + mut expected_balances: Vec, + ) -> Result<(), String> { + let dist = DistributionFunction::StepDecreasingAmount { + step_count, + decrease_per_interval_numerator, + decrease_per_interval_denominator, + start_decreasing_offset, + max_interval_count, + distribution_start_amount, + trailing_distribution_interval_amount, + min_value, + }; + + if claim_heights.len() != expected_balances.len() { + expected_balances = (0..claim_heights.len()).map(|_| 0u64).collect(); + } + + let mut prev = None; + let claims = claim_heights + .iter() + .zip(expected_balances.iter()) + .map(|(&h, &b)| { + let is_increase = match prev { + Some(p) => b > p || b == i64::MAX as u64, + None => b > INITIAL_BALANCE, + }; + prev = Some(b); + (h, b, is_increase) + }) + .collect::>(); + + // we return Err(()) to make result comparison easier in test_case + check_heights( + dist, + &claims, + None, //Some(S), + distribution_interval, + None, + ) + .inspect_err(|e| { + tracing::error!(e); + }) + } +} + +mod stepwise { + use super::test_suite::check_heights; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + use std::collections::BTreeMap; + + #[test] + fn distribution_stepwise_correct() { + let distribution_interval = 10; + let periods = BTreeMap::from([ + (0, 10_000), // h 1-20 + (2, 20_000), // h 20+ + (45, 30_000), + (50, 40_000), + (70, 50_000), + ]); + + let dist = DistributionFunction::Stepwise(periods); + + // claims: height, balance, expect_pass + let steps = [ + (1, 100_000, false), + (9, 100_000, false), + (10, 110_000, true), + (11, 110_000, false), + (19, 110_000, false), + (20, 130_000, true), + (21, 130_000, false), + (24, 130_000, false), + (35, 150_000, true), + (39, 150_000, false), + (46, 170_000, true), + (49, 170_000, false), + (51, 190_000, true), + (200, 490_000, true), + (300, 690_000, true), + ( + 1_000_000, 6_370_000, // because we only do 128 steps at a time. + true, + ), + ]; + + check_heights( + dist, + &steps, + None, //Some(S), + distribution_interval, + None, + ) + .inspect_err(|e| { + tracing::error!("{}", e); + }) + .expect("stepwise should pass"); + } +} + +mod linear { + use super::test_suite::check_heights; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::{DistributionFunction, MAX_LINEAR_SLOPE_A_PARAM, MIN_LINEAR_SLOPE_A_PARAM}; + + #[test] + fn linear_distribution_divide_by_max() -> Result<(), String> { + // Given linear distribution with d=MAX and starting amount of 1, + // We expect no claim rewards + test_linear( + 1, // a + u64::MAX, // d + None, // start_step + 0, // starting_amount + Some(0), // min_value + None, // max_value + &[(1, 100_000, false), (20, 100_000, false)], // heights + 1, + ) + } + + #[test] + fn linear_distribution_x_matrix() -> Result<(), String> { + let steps = [ + (1, 100_001, true), + (2, 100_003, true), + (3, 100_006, true), + (10, 100_055, true), + ]; + + for start_step in [None, Some(0)] { + for min_value in [None, Some(0), Some(1)] { + for max_value in [None, Some(1000)] { + test_linear(1, 1, start_step, 0, min_value, max_value, &steps, 1)?; + } + } + } + Ok(()) + } + #[test] + fn linear_distribution_slopes() -> Result<(), String> { + for (a, steps) in [ + (-1, [(1, 100_000, false), (20, 100_000, false)]), + (1, [(1, 100_001, true), (20, 100_210, true)]), + ( + MIN_LINEAR_SLOPE_A_PARAM, + [(1, 100_000, false), (20, 100_000, false)], + ), + ( + MAX_LINEAR_SLOPE_A_PARAM as i64, + [(1, 100_256, true), (20, 153_760, true)], + ), + ] { + test_linear(a, 1, None, 0, None, None, &steps, 1)?; + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn test_linear( + a: i64, + d: u64, + start_step: Option, + starting_amount: u64, + min_value: Option, + max_value: Option, + steps: &[(u64, u64, bool)], // height, expected balance, expect pass + distribution_interval: u64, + ) -> Result<(), String> { + // Linear distribution function + // + // # Formula + // The formula for the linear distribution function is: + + // ```text + // f(x) = (a * (x - start_moment) / d) + starting_amount + // ``` + // + let dist = DistributionFunction::Linear { + a, + d, + start_step, + starting_amount, + min_value, + max_value, + }; + + check_heights( + dist, + steps, + None, //Some(S), + distribution_interval, + None, + ) + .inspect_err(|e| { + tracing::error!("{}", e); + }) + } +} + +#[cfg(test)] +mod exponential { + use super::test_suite::{check_heights, TestStep, TestSuite}; + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; + use dpp::data_contract::{ + associated_token::{ + token_configuration::accessors::v0::TokenConfigurationV0Getters, + token_distribution_key::TokenDistributionType, + token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters, + token_perpetual_distribution::{ + distribution_function::DistributionFunction::{self, Exponential}, + distribution_recipient::TokenDistributionRecipient, + reward_distribution_type::RewardDistributionType, + v0::TokenPerpetualDistributionV0, + TokenPerpetualDistribution, + }, + }, + TokenConfiguration, + }; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::{MAX_DISTRIBUTION_PARAM, MAX_EXP_A_PARAM, MAX_EXP_M_PARAM, MAX_EXP_N_PARAM, MIN_EXP_M_PARAM}; + + // ───────────────────────────────────────────────────────────────────────── + // helper – one‑liner wrapper around `check_heights` (same as polynomial) + // ───────────────────────────────────────────────────────────────────────── + fn test_exponential( + dist: DistributionFunction, + steps: &[(u64, u64, bool)], // (height, expected balance, expect‑pass) + distribution_interval: u64, + ) -> Result<(), String> { + check_heights(dist, steps, None, distribution_interval, None) + .inspect_err(|e| tracing::error!("{e}")) + } + + // ───────────────────────────────────────────────────────────────────────── + // 1. Basic positive‑growth example (m > 0) + // ───────────────────────────────────────────────────────────────────────── + #[test] + fn exponential_distribution_growth_basic() -> Result<(), String> { + test_exponential( + Exponential { + a: 1, + d: 1, + m: 1, // positive ⇒ growth + n: 1, + o: 0, + start_moment: Some(1), + b: 0, + min_value: None, + max_value: Some(1_000_000), + }, + // heights 10 and 20 should both succeed – balances are illustrative + &[(10, 112_814, true), (20, 6_799_881, true)], + 1, + ) + } + + // ───────────────────────────────────────────────────────────────────────── + // 2. Basic negative‑decay example (m < 0) + // ───────────────────────────────────────────────────────────────────────── + #[test] + fn exponential_distribution_decay_basic() -> Result<(), String> { + test_exponential( + Exponential { + a: 5, + d: 1, + m: -1, // negative ⇒ decay + n: 1, + o: 0, + start_moment: Some(1), + b: 100_000, + min_value: Some(50_000), + max_value: None, + }, + &[(1, 200_005, true), (4, 500_006, true)], + 1, + ) + } + + // ───────────────────────────────────────────────────────────────────────── + // 3. o at −MAX_DISTRIBUTION_PARAM ⇒ argument very negative ▶ min / 0 + // ───────────────────────────────────────────────────────────────────────── + #[test] + fn exponential_distribution_o_min() -> Result<(), String> { + test_exponential( + Exponential { + a: 1, + d: 1, + m: 1, + n: 1, + o: -(MAX_DISTRIBUTION_PARAM as i64), + start_moment: Some(1), + b: 0, + min_value: None, + max_value: Some(MAX_DISTRIBUTION_PARAM), + }, + &[(1, 100_000, false), (4, 100_000, false)], + 1, + ) + } + + // ───────────────────────────────────────────────────────────────────────── + // 4. o at +MAX_DISTRIBUTION_PARAM (huge positive shift) + // ───────────────────────────────────────────────────────────────────────── + #[test] + fn exponential_distribution_o_max() -> Result<(), String> { + test_exponential( + Exponential { + a: MAX_EXP_A_PARAM, + d: 1, + m: -1, + n: 32, + o: MAX_DISTRIBUTION_PARAM as i64, + start_moment: Some(1), + b: 10, + min_value: None, + max_value: Some(MAX_DISTRIBUTION_PARAM), + }, + &[(1, 100010, true), (10, 100100, true)], + 1, + ) + } + + // ───────────────────────────────────────────────────────────────────────── + // 5. Exhaustive combination of extreme parameter values + // ‑ ensure no `InternalError` + // ───────────────────────────────────────────────────────────────────────── + #[test] + fn exponential_distribution_extreme_values() -> Result<(), String> { + for m in [MIN_EXP_M_PARAM, -1, 1, MAX_EXP_M_PARAM as i64] { + for n in [1, MAX_EXP_N_PARAM] { + for a in [1, MAX_EXP_A_PARAM] { + let dist = Exponential { + a, + d: 1, + m, + n, + o: 0, + start_moment: Some(1), + b: 0, + min_value: None, + max_value: Some(MAX_DISTRIBUTION_PARAM), + }; + + let mut suite = TestSuite::new( + 10_200_000_000, // initial balance + 0, // owner balance + TokenDistributionType::Perpetual, + Some(move |cfg: &mut TokenConfiguration| { + cfg.distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: + RewardDistributionType::BlockBasedDistribution { + interval: 1, + function: dist, + }, + distribution_recipient: + TokenDistributionRecipient::ContractOwner, + }, + ))); + }), + ); + + suite = suite.with_contract_start_time(1); + + let step = TestStep { + base_height: 10, + base_time_ms: Default::default(), + expected_balance: None, + claim_transition_assertions: vec![ + |results: &[StateTransitionExecutionResult]| -> Result<(), String> { + let err = results + .iter() + .find(|r| format!("{:?}", r).contains("InternalError")); + + if let Some(e) = err { + Err(format!("InternalError: {:?}", e)) + } else { + Ok(()) + } + }, + ], + name: "extreme".into(), + }; + + suite + .execute(&[step]) + .map_err(|e| format!("failed with a {a} m {m} n {n}: {e}"))?; + } + } + } + Ok(()) + } +} + +mod polynomial { + use super::test_suite::{check_heights, TestStep, TestSuite}; + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; + use dpp::data_contract::{ + associated_token::{ + token_configuration::accessors::v0::TokenConfigurationV0Getters, + token_distribution_key::TokenDistributionType, + token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters, + token_perpetual_distribution::{ + distribution_function::DistributionFunction::{self, Polynomial}, + distribution_recipient::TokenDistributionRecipient, + reward_distribution_type::RewardDistributionType, + v0::TokenPerpetualDistributionV0, + TokenPerpetualDistribution, + }, + }, + TokenConfiguration, + }; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::{MAX_DISTRIBUTION_PARAM, MAX_POL_A_PARAM, MAX_POL_M_PARAM, MAX_POL_N_PARAM, MIN_POL_A_PARAM, MIN_POL_M_PARAM}; + + #[test] + fn polynomial_distribution_basic() -> Result<(), String> { + test_polynomial( + Polynomial { + a: 1, + d: 1, + m: 2, + n: 1, + o: 1, + start_moment: Some(1), + b: 0, + min_value: None, + max_value: None, + }, + &[(10, 100_385, true), (20, 102_870, true)], + 1, + ) + } + + #[test] + fn polynomial_distribution_negative_a() -> Result<(), String> { + test_polynomial( + Polynomial { + a: -1, + d: 1, + m: 3, + n: 1, + o: 1, + start_moment: Some(1), + b: 100_000, + min_value: None, + max_value: None, + }, + &[(1, 199_999, true), (4, 499_900, true)], + 1, + ) + } + + #[test] + fn polynomial_distribution_a_minus_1_b_0() -> Result<(), String> { + test_polynomial( + Polynomial { + a: -1, + d: 1, + m: 2, + n: 1, + o: 1, + start_moment: Some(1), + b: 0, + min_value: None, + max_value: None, + }, + &[(1, 100_000, false), (4, 100_000, false)], + 1, + ) + } + + /// Given a polynomial distribution function with o=-MAX_DISTRIBUTION_PARAM, we should + /// have no rewards + #[test] + fn polynomial_distribution_o_min() -> Result<(), String> { + test_polynomial( + Polynomial { + a: 1, + d: 1, + m: 2, + n: 1, + o: -(MAX_DISTRIBUTION_PARAM as i64), + start_moment: Some(1), + b: 0, + min_value: None, + max_value: None, + }, + &[(1, 100_000, false), (4, 100_000, false)], + 1, + ) + } + + #[test] + fn polynomial_distribution_pow_minus_1_at_h_2() -> Result<(), String> { + test_polynomial( + Polynomial { + a: 1, + d: 1, + m: 1, + n: 2, + o: 0, + start_moment: Some(1), + b: 0, + min_value: None, + max_value: None, + }, + &[ + (1, 100_000, false), // this should fail, 0.pow(-1) is unspecified + (2, 100_001, true), // it's 1.pow(1/2) == 1 + (3, 100_002, true), // 2.pow(1/2) == 1.41 - should round to 1 + (4, 100_003, true), // 3.pow(1/2) == 1.73 - should round to 1 + (5, 100_005, true), // 4.pow(1/2) == 2 + (6, 100_007, true), // 5.pow(1/2) == 2.23 - should round to 2 + ], + 1, + ) + } + + #[test] + fn polynomial_distribution_o_max() -> Result<(), String> { + test_polynomial( + Polynomial { + a: 1, + d: 1, + m: 2, + n: 1, + o: MAX_DISTRIBUTION_PARAM as i64, + start_moment: Some(1), + b: 0, + min_value: None, + max_value: None, + }, + &[(1, 281474976810655, true), (10, 2814749767206550, true)], + 1, + ) + } + /// Test polynomial distribution function. + /// + /// `f(x) = (a * (x - s + o)^(m/n)) / d + b` + fn test_polynomial( + dist: DistributionFunction, + steps: &[(u64, u64, bool)], // height, expected balance, expect pass + distribution_interval: u64, + ) -> Result<(), String> { + check_heights( + dist, + steps, + None, //Some(S), + distribution_interval, + None, + ) + .inspect_err(|e| { + tracing::error!("{}", e); + }) + } + + /// Test various combinations of `m/n` in `[DistributionFunction::Polynomial]` distribution. + /// + /// We expect this test not to end with InternalError. + #[test] + fn polynomial_distribution_power_extreme_values() -> Result<(), String> { + for m in [MIN_POL_M_PARAM, MAX_POL_M_PARAM] { + for n in [1, MAX_POL_N_PARAM] { + for a in [MIN_POL_A_PARAM, MAX_POL_A_PARAM] { + for b in [0, MAX_DISTRIBUTION_PARAM] { + for o in [ + -(MAX_DISTRIBUTION_PARAM as i64), + 0, + MAX_DISTRIBUTION_PARAM as i64, + ] { + let dist = Polynomial { + a, + d: 1, + m, + n, + o, + start_moment: Some(1), + b, + min_value: None, + max_value: None, + }; + + let mut suite = TestSuite::new( + 10_200_000_000, + 0, + TokenDistributionType::Perpetual, + Some(move |token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution( + Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: + RewardDistributionType::BlockBasedDistribution { + interval: 1, + function: dist, + }, + distribution_recipient: + TokenDistributionRecipient::ContractOwner, + }, + )), + ); + }), + ); + + suite = suite.with_contract_start_time(1); + + let step = TestStep { + base_height: 10, + base_time_ms: Default::default(), + expected_balance: None, + claim_transition_assertions: vec![ + |results: &[StateTransitionExecutionResult]| -> Result<(), String> { + let err = results + .iter() + .find(|r| format!("{:?}", r).contains("InternalError")); + + if let Some(e) = err { + Err(format!("InternalError: {:?}", e)) + } else { + Ok(()) + } + }, + ], + name: "test".to_string(), + }; + + suite + .execute(&[step]) + .inspect_err(|e| { + tracing::error!("{}", e); + }) + .map_err(|e| format!("failed with m {} n {}: {}", m, n, e))?; + } + } + } + } + } + + Ok(()) + } +} + +mod logarithmic { + + use super::test_suite::check_heights; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,Logarithmic}; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::{MAX_DISTRIBUTION_PARAM, MAX_LOG_A_PARAM, MIN_LOG_A_PARAM}; + + #[test] + fn log_distribution_basic() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 1, // n: u64, + o: 1, // o: i64, + start_moment: Some(1), // start_moment: Option, + b: 1, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }, + &[ + (1, 100_001, true), // ln(0)+1 = 1 + (2, 100_002, true), // ln(1)+1 = 1 + (3, 100_004, true), // ln(3)+1 = 2 + (4, 100_006, true), // ln(4)+1 = 2 + ], + 1, + ) + } + + #[test] + fn log_distribution_1_div_u64_max() -> Result<(), String> { + // n is very big here, so we would expect to get 0 + test_logarithmic( + Logarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: u64::MAX, // n: u64, + o: 0, // o: i64, + start_moment: Some(0), // start_moment: Option, + b: 0, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }, + &[(1, 100_000, false), (5, 100_000, false)], + 1, + ) + } + + #[test] + fn log_distribution_neg_1_div_u64_max() -> Result<(), String> { + // n is very big here, so we would expect to get 0 + test_logarithmic( + Logarithmic { + a: -1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: u64::MAX, // n: u64, + o: 0, // o: i64, + start_moment: Some(0), // start_moment: Option, + b: 0, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }, + &[(1, 100_044, true), (5, 100_214, true)], + 1, + ) + } + + #[test] + fn log_distribution_a_min() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: MIN_LOG_A_PARAM, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 1, // n: u64, + o: 1, // o: i64, + start_moment: Some(1), // start_moment: Option, + b: 1, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }, + // f(x) = (a * log(m * (x - s + o) / n)) / d + b + &[ + (1, 100_001, true), + (2, 100_001, false), + (9, 100_001, false), + (10, 100_001, false), + ], + 1, + ) + } + + #[test] + fn log_distribution_max_amounts() { + test_logarithmic( + Logarithmic { + a: MAX_LOG_A_PARAM, // a: i64, + d: 1, // d: u64, + m: MAX_DISTRIBUTION_PARAM, // m: u64, + n: 1, // n: u64, + o: MAX_DISTRIBUTION_PARAM as i64, // o: i64, + start_moment: Some(0), // start_moment: Option, + b: MAX_DISTRIBUTION_PARAM, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }, + &[ + (1, 281474978991040, true), + (9, 2533274810119360, true), + (10, 2814749789010400, true), + (200, 38843547087063520, true), + ], + 1, + ) + .expect("expect to pass"); + } + + #[test] + fn log_distribution_with_b_max() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 1, // n: u64, + o: 1, // o: i64, + start_moment: Some(1), // start_moment: Option, + b: MAX_DISTRIBUTION_PARAM, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }, + &[ + (1, 281474976810655, true), // We start at 1 + (9, 2533274790495904, true), + (10, 2814749767206561, true), + ], + 1, + ) + } + /// f(x) = (a * log(m * (x - s + o) / n)) / d + b + fn test_logarithmic( + dist: DistributionFunction, + steps: &[(u64, u64, bool)], // height, expected balance, expect pass + distribution_interval: u64, + ) -> Result<(), String> { + check_heights( + dist, + steps, + None, //Some(S), + distribution_interval, + None, + ) + .inspect_err(|e| { + tracing::error!("{}", e); + }) + } +} + +mod inverted_logarithmic { + use super::test_suite::check_heights; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,InvertedLogarithmic}; + + #[test] + fn inv_log_distribution_very_low_emission() -> Result<(), String> { + // At block 2 no more can ever be claimed because the function is decreasing + let dist = InvertedLogarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 1, // n: u64, + o: 1, // o: i64, + start_moment: Some(1), // start_moment: Option, + b: 1, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }; + let steps = [ + (1, 100_001, true), + (2, 100_001, false), + (50000, 100_001, false), + ]; + let x_1 = dist.evaluate(0, 1).expect("expected to evaluate"); + assert_eq!(x_1, 1); // This is ln (1/ (1 - 1 + 1)), or basically ln(1) = 1 + let x_2 = dist.evaluate(0, 2).expect("expected to evaluate"); + assert_eq!(x_2, 0); // This is ln (1/ (1 - 1 + 2)), or basically ln(1/2) = 0 + run_test(dist, &steps, 1) + } + + #[test] + fn inv_log_distribution_reduced_emission() -> Result<(), String> { + // y + // ↑ + // 10000 |* + // 9000 | * + // 8000 | * + // 7000 | * + // 6000 | * + // 5000 | * + // 4000 | * + // 3000 | * + // 2000 | * + // 1000 | * + // 0 +-------------------*----------→ x + // 0 2000 4000 6000 8000 + let dist = InvertedLogarithmic { + a: 10000, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 5000, // n: u64, + o: 0, // o: i64, + start_moment: Some(0), // start_moment: Option, + b: 0, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }; + let x_1 = dist.evaluate(0, 1).expect("expected to evaluate"); + let x_2 = dist.evaluate(0, 2).expect("expected to evaluate"); + let x_1000 = dist.evaluate(0, 1000).expect("expected to evaluate"); + let x_4000 = dist.evaluate(0, 4000).expect("expected to evaluate"); + let x_5000 = dist.evaluate(0, 5000).expect("expected to evaluate"); + let x_6000 = dist.evaluate(0, 6000).expect("expected to evaluate"); + assert_eq!(x_1, 85171); + assert_eq!(x_2, 78240); + assert_eq!(x_1000, 16094); + assert_eq!(x_4000, 2231); + assert_eq!(x_5000, 0); + assert_eq!(x_6000, 0); + let steps = [ + (1, 185_171, true), + (2, 263_411, true), + (1000, 6_110_958, true), + ]; + + run_test(dist, &steps, 1) + } + + #[test] + fn inv_log_distribution_reduced_emission_passing_0() -> Result<(), String> { + // y + // ↑ + // 350 |* + // 300 | * + // 250 | * + // 200 | * + // 150 | * + // 100 | * + // 50 | * + // 0 +-------------*--------------→ x + // 0 100 200 300 400 + let dist = InvertedLogarithmic { + a: 100, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 200, // n: u64, + o: 0, // o: i64, + start_moment: Some(0), // start_moment: Option, + b: 0, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }; + let steps = [ + (1, 100529, true), + (2, 100989, true), + (100, 116559, true), + (210, 119546, true), + (300, 119546, false), // past 200 we won't get any more + ]; + + run_test(dist, &steps, 1) + } + + #[test] + fn inv_log_distribution_negative_a_increase_emission() -> Result<(), String> { + // y + // ↑ + // 10000 | + // 9000 | + // 8000 | + // 7000 | * + // 6000 | * + // 5000 | * + // 4000 | * + // 3000 | * + // 2000 | * + // 1000 * + // 0 +-------------------------------------------→ x + // 0 5k 10k 15k 20k 25k 30k + let dist = InvertedLogarithmic { + a: -2200, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 10000, // n: u64, + o: 3000, // o: i64, + start_moment: Some(0), // start_moment: Option, + b: 4000, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }; + let x_1 = dist.evaluate(0, 1).expect("expected to evaluate"); + let x_2 = dist.evaluate(0, 2).expect("expected to evaluate"); + let x_1000 = dist.evaluate(0, 1000).expect("expected to evaluate"); + let x_4000 = dist.evaluate(0, 4000).expect("expected to evaluate"); + assert_eq!(x_1, 1351); + assert_eq!(x_2, 1352); + assert_eq!(x_1000, 1984); + assert_eq!(x_4000, 3215); + let steps = [ + (1, 101351, true), + (2, 102703, true), + (100, 238739, true), + (210, 399539, true), + (300, 537282, true), + ]; + + run_test(dist, &steps, 1) + } + + /// f(x) = (a * log( n / (m * (x - s + o)) )) / d + b + fn run_test( + dist: DistributionFunction, + steps: &[(u64, u64, bool)], // height, expected balance, expect pass + distribution_interval: u64, + ) -> Result<(), String> { + check_heights( + dist, + steps, + None, //Some(S), + distribution_interval, + None, + ) + .inspect_err(|e| { + tracing::error!("{}", e); + }) + } +} + +mod test_suite { + use super::*; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; + use crate::test::helpers::setup::TempPlatform; + use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Getters; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; + use dpp::data_contract::associated_token::token_distribution_rules::TokenDistributionRules; + 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::v0::TokenPerpetualDistributionV0; + use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; + use dpp::prelude::{DataContract, IdentityPublicKey, TimestampMillis}; + use simple_signer::signer::SimpleSigner; + + const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(60); + /// Run provided closure with timeout. + /// TODO: Check if it works with sync code + fn with_timeout( + duration: tokio::time::Duration, + f: impl FnOnce() -> Result<(), String> + Send + 'static, + ) -> Result<(), String> { + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + .unwrap(); + // thread executing our code + let worker = rt.spawn_blocking(f); + + rt.block_on(async move { tokio::time::timeout(duration, worker).await }) + .map_err(|_| format!("test timed out after {:?}", TIMEOUT))? + .map_err(|e| format!("join error: {:?}", e))? + } + + /// Check that claim results at provided heights are as expected, and that balances match expectations. + /// + /// Note we take i128 into expected_balances, as we want to be able to detect overflows. + /// + /// # Arguments + /// + /// * `distribution_function` - configured distribution function to test + /// * `claims` - heights at which claims will be made; they will see balance from previous height + /// * `contract_start_time` - optional start time of the contract + /// * `distribution_interval` - interval between distributions + /// * `max_supply` - optional max supply of the token; if Some(), it will override max supply in contract JSON definition + /// + /// Note that for convenience, you can provide `steps` as a [`TestStep`] or a slice of tuples, where each tuple contains: + /// * `height` - height at which claim will be made + /// * `expected_balance` - expected balance after claim was made + /// * `expect_pass` - whether we expect the claim to pass or not + /// + pub(super) fn check_heights + Clone>( + distribution_function: DistributionFunction, + steps: &[C], + contract_start_time: Option, + distribution_interval: u64, + max_supply: Option>, + ) -> Result<(), String> { + let mut suite = TestSuite::new( + 10_200_000_000, + 0, + TokenDistributionType::Perpetual, + Some(move |token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: distribution_interval, + function: distribution_function, + }, + distribution_recipient: TokenDistributionRecipient::ContractOwner, + }, + ))); + }), + ); + if let Some(max_supply) = max_supply { + suite = suite.with_max_supply(max_supply); + } + + suite = suite.with_contract_start_time(contract_start_time.unwrap_or(1)); + + let steps = steps + .iter() + .map(|item| item.clone().into()) + .collect::>(); + + with_timeout(TIMEOUT, move || suite.execute(&steps)) + } + + pub(super) type TokenConfigFn = dyn FnOnce(&mut TokenConfiguration) + Send + Sync; + /// Test engine to run tests for different token distribution functions. + pub(crate) struct TestSuite { + platform: TempPlatform, + platform_version: &'static PlatformVersion, + identity: dpp::prelude::Identity, + signer: SimpleSigner, + identity_public_key: IdentityPublicKey, + token_id: Option, + contract: Option, + start_time: Option, + token_distribution_type: TokenDistributionType, + token_configuration_modification: Option>, + epoch_index: u16, + nonce: u64, + time_between_blocks: u64, + + /// function that will be called after successful claim. + /// + /// ## Arguments + /// + /// * `u64` - balance after claim + on_step_success: Box, + } + + impl TestSuite { + /// Create new test suite that will start at provided genesis time and create token contract with provided + /// configuration. + pub(crate) fn new( + genesis_time_ms: u64, + time_between_blocks: u64, + token_distribution_type: TokenDistributionType, + token_configuration_modification: Option, + ) -> Self { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + Self::setup_logs(); + + let mut rng = StdRng::seed_from_u64(49853); + + let (identity, signer, identity_public_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let me = Self { + platform, + platform_version, + identity, + signer, + identity_public_key, + token_id: None, // lazy initialization in get_contract/get_token_id + contract: None, // lazy initialization in get_contract/get_token_id + start_time: None, // optional, configured using with_contract_start_time + token_distribution_type, + epoch_index: 1, + nonce: 1, + time_between_blocks, + token_configuration_modification: None, // setup later + on_step_success: Box::new(|_| {}), + } + .with_genesis(1, genesis_time_ms); + + if let Some(token_configuration_modification) = token_configuration_modification { + me.with_token_configuration_modification_fn(token_configuration_modification) + } else { + me + } + } + + /// Appends new token configuration modification function after existing ones. + pub(crate) fn with_token_configuration_modification_fn( + mut self, + token_configuration_modification: impl FnOnce(&mut TokenConfiguration) + + Send + + Sync + + 'static, + ) -> Self { + if let Some(previous) = self.token_configuration_modification.take() { + let f = Box::new(move |token_configuration: &mut TokenConfiguration| { + previous(token_configuration); + token_configuration_modification(token_configuration); + }); + + self.token_configuration_modification = Some(f); + } else { + // no previous modifications + let f = Box::new(token_configuration_modification); + self.token_configuration_modification = Some(f); + }; + + self + } + /// Appends a token configuration modification that will change max supply. + pub(crate) fn with_max_supply(self, max_supply: Option) -> Self { + self.with_token_configuration_modification_fn( + move |token_configuration: &mut TokenConfiguration| { + token_configuration.set_max_supply(max_supply); + }, + ) + } + + /// Enable logging for tests + fn setup_logs() { + tracing_subscriber::fmt::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::new( + "info,dash_sdk=trace,dash_sdk::platform::fetch=debug,drive_proof_verifier=debug,main=debug,h2=info,drive_abci::execution=trace", + )) + .pretty() + .with_ansi(true) + .with_writer(std::io::stdout) + .try_init() + .ok(); + } + + /// Lazily initialize and return token contract. Also sets token id. + fn get_contract(&mut self) -> DataContract { + if let Some(ref contract) = self.contract { + return contract.clone(); + } + // we `take()` to avoid moving from reference; this means subsequent calls will fail, but we will already have + // the contract and token id initialized so it should never happen + let token_config_fn = if let Some(tc) = self.token_configuration_modification.take() { + let closure = |token_configuration: &mut TokenConfiguration| { + // call previous token configuration modification + tc(token_configuration); + + // execute distribution function validation + if let Err(e) = validate_distribution_function( + token_configuration, + self.start_time.unwrap_or(0), + ) { + panic!("{}", e); + }; + + tracing::trace!("token configuration validated"); + }; + Some(closure) + } else { + None + }; + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut self.platform, + self.identity.id(), + token_config_fn, + self.start_time, + None, + self.platform_version, + ); + + self.token_id = Some(token_id); + self.contract = Some(contract.clone()); + + contract + } + + /// Get token ID or create if needed. + fn get_token_id(&mut self) -> Identifier { + if self.token_id.is_none() { + self.get_contract(); // lazy initialization of token id and contract + } + + self.token_id + .expect("expected token id to be initialized in get_contract") + } + + fn next_identity_nonce(&mut self) -> u64 { + self.nonce += 1; + + self.nonce + } + + /// Submit a claim transition and assert the results + pub(crate) fn claim(&mut self, assertions: Vec) -> Result<(), String> { + let committed_block_info = self.block_info(); + let nonce = self.next_identity_nonce(); + // next block config + let new_block_info = BlockInfo { + time_ms: committed_block_info.time_ms + self.time_between_blocks, + height: committed_block_info.height + 1, + // no change here + core_height: committed_block_info.core_height, + ..committed_block_info + }; + + let claim_transition = BatchTransition::new_token_claim_transition( + self.get_token_id(), + self.identity.id(), + self.get_contract().id(), + 0, + self.token_distribution_type, + None, + &self.identity_public_key, + nonce, + 0, + &self.signer, + self.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 = self.platform.drive.grove.start_transaction(); + let platform_state = self.platform.state.load(); + + let processing_result = self + .platform + .platform + .process_raw_state_transitions( + &[claim_serialized_transition.clone()], + &platform_state, + &new_block_info, + &transaction, + self.platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + for (i, assertion) in assertions.iter().enumerate() { + if let Err(e) = assertion(processing_result.execution_results().as_slice()) { + return Err(format!("assertion {} failed: {}", i, e)); + } + } + + self.platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + Ok(()) + } + + /// Retrieve token balance for the identity and assert it matches expected value. + pub(crate) fn get_balance(&mut self) -> Result, String> { + let token_id = self.get_token_id().to_buffer(); + + let balance = self + .platform + .drive + .fetch_identity_token_balance( + token_id, + self.identity.id().to_buffer(), + None, + self.platform_version, + ) + .map_err(|e| format!("failed to fetch token balance: {}", e)); + + tracing::trace!("retrieved balance: {:?}", balance); + balance + } + + /// Retrieve token balance for the identity and assert it matches expected value. + pub(crate) fn assert_balance( + &mut self, + expected_balance: Option, + ) -> Result<(), String> { + let token_balance = self.get_balance()?; + + if token_balance != expected_balance { + return Err(format!( + "expected balance {:?} but got {:?}", + expected_balance, token_balance + )); + } + + Ok(()) + } + + fn block_info(&self) -> BlockInfo { + *self + .platform + .state + .load() + .last_committed_block_info() + .as_ref() + .expect("expected last committed block info") + .basic_info() + } + /// initialize genesis state + fn with_genesis(self, genesis_core_height: u32, genesis_time_ms: u64) -> Self { + fast_forward_to_block( + &self.platform, + genesis_time_ms, + 1, + genesis_core_height, + self.epoch_index, + false, + ); + + self + } + + /// Configure custom contract start time; must be called before contract is + /// initialized. + pub(super) fn with_contract_start_time(mut self, start_time: TimestampMillis) -> Self { + if self.contract.is_some() { + panic!("with_contract_start_time must be called before contract is initialized"); + } + self.start_time = Some(start_time); + self + } + + pub(super) fn with_step_success_fn<'a>( + mut self, + step_success_fn: impl Fn(u64) + Send + Sync + 'static, + ) -> Self + where + Self: 'a, + { + // fn f(s: TestSuite) { + // step_success_fn(s); + // }; + self.on_step_success = Box::new(step_success_fn); + self + } + + /// execute test steps, one by one + pub(super) fn execute(&mut self, tests: &[TestStep]) -> Result<(), String> { + let mut errors = String::new(); + for test_case in tests { + let result = self.execute_step(test_case); + if let Err(e) = result { + errors += format!("\n--> {}: {}\n", test_case.name, e).as_str(); + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } + + /// Execute a single test step. It fasts forwards to the block height of the test case, + /// executes the claim and checks the balance. + pub(super) fn execute_step(&mut self, test_case: &TestStep) -> Result<(), String> { + let current_height = self.block_info().height; + let current_core_height = self.block_info().core_height; + + let block_time = if test_case.base_height >= current_height { + test_case.base_time_ms + + self.time_between_blocks * (test_case.base_height - current_height) + } else { + // workaround for fast_forward_to_block not allowing to go back in time + test_case.base_time_ms + }; + + fast_forward_to_block( + &self.platform, + block_time, + test_case.base_height, + current_core_height, + self.epoch_index, + false, + ); + let mut result = Vec::new(); + if let Err(e) = self.claim(test_case.claim_transition_assertions.clone()) { + result.push(format!("claim failed: {}", e)) + } + + let balance = self + .get_balance() + .map_err(|e| format!("failed to get balance: {}", e))? + .ok_or("expected balance to be present, but got None".to_string())?; + + if test_case + .expected_balance + .is_some_and(|expected_balance| expected_balance != balance) + { + result.push(format!( + "expected balance {:?} but got {:?}", + test_case.expected_balance, balance + )); + } + + if result.is_empty() { + tracing::trace!( + "step successful, base height: {}, balance: {}", + test_case.base_height, + balance + ); + (self.on_step_success)(balance); + Ok(()) + } else { + Err(result.join("\n")) + } + } + } + + /// dyn FnOnce(&mut TokenConfiguration) + Send + Sync; + fn validate_distribution_function( + token_configuration: &mut TokenConfiguration, + contract_start_time: u64, + ) -> Result<(), String> { + let TokenConfiguration::V0(token_config) = token_configuration; + + let TokenDistributionRules::V0(dist_rules) = token_config.distribution_rules(); + + let TokenPerpetualDistribution::V0(perpetual_distribution) = dist_rules + .perpetual_distribution() + .expect("expected perpetual distribution"); + + let consensus_result = perpetual_distribution + .distribution_type + .function() + .validate(contract_start_time, PlatformVersion::latest()) + .map_err(|e| format!("invalid distribution function: {:?}", e))?; + + if let Some(error) = consensus_result.first_error() { + return Err(error.to_string()); + } + + Ok(()) + } + + pub(crate) type AssertionFn = fn(&[StateTransitionExecutionResult]) -> Result<(), String>; + + /// Individual step of a test case. + #[derive(Clone, Debug)] + pub(crate) struct TestStep { + pub(crate) name: String, + /// height of block just before the claim + pub(crate) base_height: u64, + /// time of block before the claim + pub(crate) base_time_ms: u64, + /// expected balance is a function that should return the expected balance after committing block + /// at provided height and time + pub(crate) expected_balance: Option, + /// assertion functions that must be met after executing the claim state transition + pub(crate) claim_transition_assertions: Vec, + } + + impl TestStep { + /// Create a new test step with provided claim height and expected balance. + /// If expect_success is true, we expect the claim to be successful. + /// If false, we expect the claim to fail. + /// + /// If expected_balance is None, we don't check the balance. + pub(super) fn new(claim_height: u64, expected_balance: u64, expect_success: bool) -> Self { + let trace_assertion: AssertionFn = |processing_results: &[_]| { + tracing::trace!( + "transaction assertion check for processing results: {:?}", + processing_results + ); + Ok(()) + }; + let assertions: Vec = if expect_success { + vec![ + |processing_results: &[_]| { + tracing::trace!(?processing_results, "expect success"); + Ok(()) + }, + |processing_results: &[_]| match processing_results { + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => Ok(()), + _ => Err(format!( + "expected SuccessfulExecution, got {:?}", + processing_results + )), + }, + trace_assertion, + ] + } else { + vec![ + |processing_results: &[_]| { + tracing::trace!(?processing_results, "expect failure"); + Ok(()) + }, + |processing_results: &[_]| match processing_results { + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => { + Err("expected error, got SuccessfulExecution".into()) + } + [StateTransitionExecutionResult::InternalError(e)] => { + Err(format!("expected normal error, got InternalError: {}", e)) + } + _ => Ok(()), + }, + trace_assertion, + ] + }; + Self { + name: format!("claim at height {}", claim_height), + base_height: claim_height - 1, + base_time_ms: 10_200_000_000, + expected_balance: Some(expected_balance), + claim_transition_assertions: assertions, + } + } + } + + impl From<(u64, u64, bool)> for TestStep { + fn from( + (claim_height, expected_balance, expect_claim_successful): (u64, u64, bool), + ) -> Self { + Self::new(claim_height, expected_balance, expect_claim_successful) + } + } +} 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 90e0372bfc2..f5433c5fb0f 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 @@ -11,7 +11,7 @@ use rand::prelude::StdRng; 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_function::{DistributionFunction, MAX_DISTRIBUTION_PARAM, MAX_LINEAR_SLOPE_A_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; @@ -1102,8 +1102,8 @@ mod perpetual_distribution_time { // every 1 millisecond interval: 1, function: DistributionFunction::Linear { - a: MAX_LINEAR_SLOPE_PARAM as i64, // Strongest slope - d: 1, // No division + a: MAX_LINEAR_SLOPE_A_PARAM as i64, // Strongest slope + d: 1, // No division start_step: None, starting_amount: MAX_DISTRIBUTION_PARAM, min_value: None, @@ -1287,8 +1287,8 @@ mod perpetual_distribution_time { // every 1 millisecond interval: 1, function: DistributionFunction::Linear { - a: MAX_LINEAR_SLOPE_PARAM as i64, // Strongest slope - d: 1, // No division + a: MAX_LINEAR_SLOPE_A_PARAM as i64, // Strongest slope + d: 1, // No division start_step: None, starting_amount: MAX_DISTRIBUTION_PARAM, min_value: None, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs index 121e9cf348c..f0456a99aa9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs @@ -7,6 +7,8 @@ use dpp::consensus::basic::data_contract::{ use dpp::consensus::basic::BasicError; use dpp::consensus::ConsensusError; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; +use dpp::data_contract::associated_token::token_perpetual_distribution::methods::v0::TokenPerpetualDistributionV0Accessors; use dpp::data_contract::{TokenContractPosition, INITIAL_DATA_CONTRACT_VERSION}; use dpp::prelude::DataContract; use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; @@ -80,6 +82,21 @@ impl DataContractCreateStateTransitionBasicStructureValidationV0 for DataContrac if !validation_result.is_valid() { return Ok(validation_result); } + + if let Some(perpetual_distribution) = token_configuration + .distribution_rules() + .perpetual_distribution() + { + // We use 0 as the start moment to show that we are starting now with no offset + let validation_result = perpetual_distribution + .distribution_type() + .function() + .validate(0, platform_version)?; + + if !validation_result.is_valid() { + return Ok(validation_result); + } + } } // Validate there are no more than 20 keywords diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs index 073da6f9db0..dfc2f50980c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs @@ -412,6 +412,11 @@ mod tests { use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters; mod basic_creation { + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::{DistributionFunction, MAX_DISTRIBUTION_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 super::*; #[test] fn test_data_contract_creation_with_single_token() { @@ -1062,6 +1067,119 @@ mod tests { .unwrap() .expect("expected to commit transaction"); } + + #[test] + fn test_data_contract_creation_with_single_token_with_valid_perpetual_distribution() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + + let mut data_contract = json_document_to_contract_with_ids( + "tests/supporting_files/contract/basic-token/basic-token.json", + None, + None, + false, //no need to validate the data contracts in tests for drive + platform_version, + ) + .expect("expected to get json based contract"); + + { + let token_config = data_contract + .tokens_mut() + .expect("expected tokens") + .get_mut(&0) + .expect("expected first token"); + token_config + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::Exponential { + a: 1, + d: 1, + m: 1, + n: 1, + o: 0, + start_moment: None, + b: 10, + min_value: None, + max_value: Some(MAX_DISTRIBUTION_PARAM), + }, + }, + // we give to identity 2 + distribution_recipient: TokenDistributionRecipient::Identity( + identity.id(), + ), + }, + ))); + } + + let identity_id = identity.id(); + + let data_contract_id = DataContract::generate_data_contract_id_v0(identity_id, 1); + + let data_contract_create_transition = + DataContractCreateTransition::new_from_data_contract( + data_contract, + 1, + &identity.into_partial_identity_info(), + key.id(), + &signer, + platform_version, + None, + ) + .expect("expect to create documents batch transition"); + + let token_id = calculate_token_id(data_contract_id.as_bytes(), 0); + + let data_contract_create_serialized_transition = data_contract_create_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( + &[data_contract_create_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"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id, + identity_id.to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(100_000)); + } } mod pre_programmed_distribution { @@ -1243,6 +1361,11 @@ mod tests { } mod token_errors { + 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 super::*; #[test] fn test_data_contract_creation_with_single_token_with_starting_balance_over_limit_should_cause_error( @@ -1691,6 +1814,230 @@ mod tests { .unwrap() .expect("expected to commit transaction"); } + + #[test] + fn test_data_contract_creation_with_single_token_with_invalid_perpetual_distribution_should_cause_error( + ) { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + + let mut data_contract = json_document_to_contract_with_ids( + "tests/supporting_files/contract/basic-token/basic-token.json", + None, + None, + false, //no need to validate the data contracts in tests for drive + platform_version, + ) + .expect("expected to get json based contract"); + + { + let token_config = data_contract + .tokens_mut() + .expect("expected tokens") + .get_mut(&0) + .expect("expected first token"); + token_config + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::Exponential { + a: 0, + d: 0, + m: 0, + n: 0, + o: 0, + start_moment: None, + b: 0, + min_value: None, + max_value: None, + }, + }, + // we give to identity 2 + distribution_recipient: TokenDistributionRecipient::Identity( + identity.id(), + ), + }, + ))); + } + + let identity_id = identity.id(); + + let data_contract_id = DataContract::generate_data_contract_id_v0(identity_id, 1); + + let data_contract_create_transition = + DataContractCreateTransition::new_from_data_contract( + data_contract, + 1, + &identity.into_partial_identity_info(), + key.id(), + &signer, + platform_version, + None, + ) + .expect("expect to create documents batch transition"); + + let token_id = calculate_token_id(data_contract_id.as_bytes(), 0); + + let data_contract_create_serialized_transition = data_contract_create_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( + &[data_contract_create_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::UnpaidConsensusError( + ConsensusError::BasicError( + BasicError::InvalidTokenDistributionFunctionDivideByZeroError(_) + ), + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id, + identity_id.to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, None); + } + + #[test] + fn test_data_contract_creation_with_single_token_with_random_perpetual_distribution_should_cause_error( + ) { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + + let mut data_contract = json_document_to_contract_with_ids( + "tests/supporting_files/contract/basic-token/basic-token.json", + None, + None, + false, //no need to validate the data contracts in tests for drive + platform_version, + ) + .expect("expected to get json based contract"); + + { + let token_config = data_contract + .tokens_mut() + .expect("expected tokens") + .get_mut(&0) + .expect("expected first token"); + token_config + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::Random { min: 0, max: 10 }, + }, + // we give to identity 2 + distribution_recipient: TokenDistributionRecipient::Identity( + identity.id(), + ), + }, + ))); + } + + let identity_id = identity.id(); + + let data_contract_id = DataContract::generate_data_contract_id_v0(identity_id, 1); + + let data_contract_create_transition = + DataContractCreateTransition::new_from_data_contract( + data_contract, + 1, + &identity.into_partial_identity_info(), + key.id(), + &signer, + platform_version, + None, + ) + .expect("expect to create documents batch transition"); + + let token_id = calculate_token_id(data_contract_id.as_bytes(), 0); + + let data_contract_create_serialized_transition = data_contract_create_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( + &[data_contract_create_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::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::UnsupportedFeatureError(_)), + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id, + identity_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/data_contract_update/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs index d59cee74b55..61d36ee95ea 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs @@ -3,6 +3,8 @@ use dpp::consensus::basic::data_contract::{ InvalidTokenBaseSupplyError, NonContiguousContractTokenPositionsError, }; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; +use dpp::data_contract::associated_token::token_perpetual_distribution::methods::v0::TokenPerpetualDistributionV0Accessors; use dpp::data_contract::TokenContractPosition; use dpp::prelude::DataContract; use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; @@ -65,6 +67,21 @@ impl DataContractUpdateStateTransitionBasicStructureValidationV0 for DataContrac if !validation_result.is_valid() { return Ok(validation_result); } + + if let Some(perpetual_distribution) = token_configuration + .distribution_rules() + .perpetual_distribution() + { + // We use 0 as the start moment to show that we are starting now with no offset + let validation_result = perpetual_distribution + .distribution_type() + .function() + .validate(0, platform_version)?; + + if !validation_result.is_valid() { + return Ok(validation_result); + } + } } Ok(SimpleConsensusValidationResult::new()) diff --git a/packages/rs-drive-abci/src/logging/logger.rs b/packages/rs-drive-abci/src/logging/logger.rs index 6646a783c83..b35d4d41132 100644 --- a/packages/rs-drive-abci/src/logging/logger.rs +++ b/packages/rs-drive-abci/src/logging/logger.rs @@ -53,6 +53,12 @@ pub struct LogBuilder { loggers: HashMap, } +use std::sync::OnceLock; +use tracing::Dispatch; +// std, no external crate + +static LOGGING_INSTALLED: OnceLock<()> = OnceLock::new(); + impl LogBuilder { /// Creates a new `LogBuilder` instance with default settings. pub fn new() -> Self { @@ -164,6 +170,12 @@ impl Loggers { self.0.get(id) } + /// Build a subscriber containing all layers from these loggers. + pub fn as_subscriber(&self) -> Result { + let layers = self.tracing_subscriber_layers()?; + Ok(Dispatch::new(Registry::default().with(layers))) + } + /// Installs loggers prepared in the [LogBuilder] as a global tracing handler. /// /// Same as [Loggers::install()], but returns error if the logging subsystem is already initialized. @@ -177,14 +189,22 @@ impl Loggers { /// drive_abci::logging::Loggers::default().try_install().ok(); /// ``` pub fn try_install(&self) -> Result<(), Error> { + // Fast path: somebody already installed – just return Ok(()) + if LOGGING_INSTALLED.get().is_some() { + return Ok(()); // <- second and later calls are ignored + } + let layers = self.tracing_subscriber_layers()?; registry() .with(layers) .try_init() - .map_err(Error::TryInitError) - } + .map_err(Error::TryInitError)?; + // Mark as installed + let _ = LOGGING_INSTALLED.set(()); + Ok(()) + } /// Returns tracing subscriber layers pub fn tracing_subscriber_layers(&self) -> Result>>, Error> { // Based on examples from https://docs.rs/tracing-subscriber/0.3.17/tracing_subscriber/layer/index.html diff --git a/packages/rs-drive-abci/src/logging/mod.rs b/packages/rs-drive-abci/src/logging/mod.rs index 9687e9c9cef..cd0f24cf273 100644 --- a/packages/rs-drive-abci/src/logging/mod.rs +++ b/packages/rs-drive-abci/src/logging/mod.rs @@ -106,7 +106,9 @@ mod tests { .with_config("file_v4", &logger_file_v4) .unwrap() .build(); - loggers.install(); + + let dispatch = loggers.as_subscriber().expect("subscriber failed"); + let _guard = tracing::dispatcher::set_default(&dispatch); const TEST_STRING_DEBUG: &str = "testing debug trace"; const TEST_STRING_ERROR: &str = "testing error trace";