From 4e38431343d930f4787b389794d3975106016ccd Mon Sep 17 00:00:00 2001 From: lklimek <842586+lklimek@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:18:55 +0200 Subject: [PATCH 1/6] test(drive): test various token distribution algorithms (#2511) --- .../distribution_function/evaluate.rs | 2 +- .../distribution_function/mod.rs | 2 + packages/rs-drive-abci/Cargo.toml | 1 - .../distribution/perpetual/block_based.rs | 2428 ++++++++++++++++- 4 files changed, 2426 insertions(+), 7 deletions(-) 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..97316e441cd 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 @@ -39,7 +39,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); 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..c04a5b3521d 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 @@ -85,6 +85,8 @@ 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. diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index 9696eab8838..49ea54b19d4 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -103,7 +103,6 @@ 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" } 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..13919b9e88d 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, @@ -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, @@ -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,2416 @@ 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, + }; + + #[test] + fn fixed_amount_1_interval_1() -> Result<(), String> { + super::test_suite::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 42, + // Then the claim should be successful. + #[test] + fn fixed_amount_50_interval_10() { + super::test_suite::check_heights( + DistributionFunction::FixedAmount { amount: 50 }, + &[ + TestStep::new(1, 100_000, true), + 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. + /// + /// TODO: Fails, please fix. + /// + /// claim at height 1000000000000: claim failed: assertion 0 failed: expected SuccessfulExecution, + /// got [InternalError(\"storage: protocol: overflow error: Overflow in FixedAmount evaluation\")]" + #[test] + fn fail_fixed_amount_1_000_000_000() { + 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), + TestStep::new( + 1_000_000_000_000, + INITIAL_BALANCE + 5 * 1_000_000_000, + false, + ), + ], + 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), + (46, 100000, false), + (50, 100000, false), + (1000, 100000, false), + ], + None, + 10, + None, + ) + .expect("\nfixed amount zero increase\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 fail_test_block_based_perpetual_fixed_amount_u64_max() { + check_heights( + DistributionFunction::FixedAmount { amount: u64::MAX }, + &[TestStep::new(41, 100_000, false)], + None, + 10, + None, + ) + .expect("\nfixed amount u64::MAX should pass\n"); + } +} +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_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] + 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] + 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] + fn fails_test_block_based_perpetual_random_0_max() { + check_heights( + DistributionFunction::Random { + min: 0, + max: u64::MAX, + }, + &[ + TestStep::new(41, INITIAL_BALANCE, false), + TestStep::new(50, INITIAL_BALANCE, false), + TestStep::new(100, INITIAL_BALANCE, false), + ], + 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] + 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(); + // substract balance from previous step (for first step, substract 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_configuration::accessors::v0::TokenConfigurationV0Getters; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters; + 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::data_contract::TokenConfiguration; + use rust_decimal::prelude::ToPrimitive; + use crate::{execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::test_suite::check_heights, platform_types::state_transitions_processing_result::StateTransitionExecutionResult}; + use crate::execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::INITIAL_BALANCE; + + use super::test_suite::{TestStep, TestSuite}; + + #[test] + fn claim_every_100_blocks() -> Result<(), String> { + run_test( + 1, + 1, + 100, + None, + 100_000, + Some(1), + Some((1..1000).step_by(100).collect()), + 1, + ) + } + + #[test] + fn claim_every_100_blocks_with_1_percent_increase() -> Result<(), String> { + run_test( + 1, + 101, + 100, + None, + 100_000, + Some(1), + Some((1..1000).step_by(100).collect()), + 1, + ) + } + + #[test] + fn claim_every_500_blocks_fails_due_to_max_token_redemption_cycles() -> Result<(), String> { + let result = run_test( + 1, + 101, + 100, + None, + 100_000, + Some(1), + Some((1..1000).step_by(500).collect()), + 1, + ); + assert!(result.is_err_and( + |s| s.contains("claim at height 501: expected balance Some(100510) but got 100138") + )); + Ok(()) + } + + #[test] + fn fails_with_1000x_increase_overflow() -> Result<(), String> { + run_test(1, 1000, 1, None, 100_000, Some(1), Some(vec![1, 7]), 1) + } + + #[test] + fn full_decrease_min_1_100() -> Result<(), String> { + for min in [1, 100] { + run_test( + 1, + 1, + 1, + None, + 100_000, + Some(min), + Some(vec![1, 2, 3, 10, 100]), + 1, + ) + .map_err(|e| format!("failed with min {}: {}", min, e))?; + } + + Ok(()) + } + + #[test] + fn fails_full_decrease_min_eq_u64_max() -> Result<(), String> { + run_test( + 1, + 1, + 1, + None, + 100_000, + Some(u64::MAX), + Some(vec![1, 2, 3, 10, 100]), + 1, + ) + } + #[test] + fn no_decrease_changing_min() -> Result<(), String> { + for min in [None, Some(0), Some(1), Some(100)] { + run_test(1, 0, 1, None, 100_000, min, Some(vec![1, 2, 3, 10, 100]), 1) + .map_err(|e| format!("failed with min {:?}: {}", min, e))?; + } + Ok(()) + } + + #[test] + fn full_decrease_step_10_interval_1() -> Result<(), String> { + run_test(10, 1, 1, None, 100_000, None, Some(vec![2, 7, 9]), 1) + } + + #[test] + fn full_decrease_start_5_step_10_interval_1() -> Result<(), String> { + run_test( + 10, + 1, + 1, + Some(5), + 100_000, + None, + Some(vec![2, 7, 9, 13, 14]), + 1, + ) + } + + #[test] + fn full_decrease_start_5_step_10_interval_1_err_at_15() -> Result<(), String> { + let result = run_test(10, 1, 1, Some(5), 100_000, None, Some(vec![14, 15]), 1); + assert!(result.is_err_and(|s| s.contains("claim at height 15: claim failed"))); + Ok(()) + } + + #[test] + fn fails_half_decrease_changing_step_and_interval() -> Result<(), String> { + for step in [5, 10] { + for distribution_interval in [1, 5] { + run_test( + step, + 1, + 2, + None, + 100_000, + None, + Some(vec![5, 10, 18, 22, 100]), + distribution_interval, + ) + .map_err(|e| { + format!( + "failed with step {} interval {}: {}", + step, distribution_interval, e + ) + })?; + } + } + + Ok(()) + } + + #[test] + fn half_decrease_chainging_s() -> Result<(), String> { + for s in [None, Some(1), Some(5)] { + run_test(1, 10, 100, s, 100_000, None, Some(vec![5, 10, 15, 20]), 1) + .map_err(|e| format!("failed with s {:?}: {}", s, e))?; + } + Ok(()) + } + + /// 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, + s: Option, + n: TokenAmount, + min_value: Option, + claim_heights: Option>, + distribution_interval: u64, + ) -> Result<(), String> { + let dist = DistributionFunction::StepDecreasingAmount { + step_count, + decrease_per_interval_numerator, + decrease_per_interval_denominator, + s, + n, + min_value, + }; + let claim_heights = + claim_heights.unwrap_or(vec![1, 2, 3, 4, 5, 10, 20, 30, 50, 100, 1_000_000]); + + let expected_balances = claim_heights + .iter() + .map(|&h| { + // initial balance, defined in contract js + let mut expected_balance: i128 = INITIAL_BALANCE as i128; + // loop over blocks, starting with S, with step PERPETUAL_DISTRIBUTION_INTERVAL + for i in (1..=h).step_by(distribution_interval as usize) { + expected_balance += expected_emission(i, &dist); + } + tracing::debug!("expected balance at height {}: {}", h, expected_balance); + expected_balance.to_u64().unwrap_or_else(|| { + tracing::error!("overflow in expected balance at height {}", h); + 0 + }) // to handle tests that overflow + }) + .collect::>(); + // we expect all tests to pass + let claims = claim_heights + .iter() + .zip(expected_balances.iter()) + .map(|(&h, &b)| (h, b, true)) + .collect::>(); + + // we return Err(()) to make result comparision easier in test_case + check_heights( + dist, + &claims, + None, //Some(S), + distribution_interval, + None, + ) + .inspect_err(|e| { + tracing::error!(e); + }) + } + + /// Given that we have a distribution function distributing some tokens, + /// When I claim tokens with delay bigger than [platform_version.system_limits.max_token_redemption_cycles], + /// Then I need to run the claim more than once to get correct balance. + #[test] + fn test_claim_more_than_max_token_redemption_cycles() { + let dist = DistributionFunction::StepDecreasingAmount { + step_count: 1, + decrease_per_interval_numerator: 101, + decrease_per_interval_denominator: 100, + s: None, + n: 100_000, + min_value: Some(1), + }; + + let dist_clone = dist.clone(); + let mut suite = TestSuite::new( + 10_200_000_000, + 1, + TokenDistributionType::Perpetual, + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 1, + function: dist_clone, + }, + distribution_recipient: TokenDistributionRecipient::ContractOwner, + }, + ))); + }), + ); + + for (height, balance) in [(1, 100_001), (101, 100_110), (501, 100_510)] { + // claim at height 500;loop until we have no more coins + let step = TestStep { + name: format!("height {}", height), + base_height: height - 1, + base_time_ms: 10_200_000_000, + expected_balance: None, + claim_transition_assertions: vec![|v| match v { + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => Ok(()), + _ => Err(format!("got {:?}", v)), + }], + }; + + let mut loops = 0; + let err = loop { + if let Err(err) = suite.execute_step(&step) { + break err; + } + loops += 1; + }; + + // max_token_redemption_cycles is 128 + if height == 501 { + assert_eq!(loops, (501 - 101) / 128 + 1); + } else { + assert_eq!(loops, 1); + } + + assert!( + err.contains("InvalidTokenClaimNoCurrentRewards"), + "expected InvalidTokenClaimNoCurrentRewards error, got {}", + err + ); + + assert_eq!( + suite + .get_balance() + .expect("get balance") + .unwrap_or_default(), + balance, + "expected balance at height {}: {}", + height, + balance + ); + } + } + + // ===== HELPER FUNCTIONS ===== // + + /// Calculate expected emission at provided height. + /// + /// We use [i128] to ensure we handle overflows better than the original code. + /// + // f(x) = n * (1 - (decrease_per_interval_numerator / decrease_per_interval_denominator))^((x - s) / step_count) + pub(super) fn expected_emission(x: u64, dist: &DistributionFunction) -> i128 { + let x = x as i128; + let ( + step_count, + decrease_per_interval_numerator, + decrease_per_interval_denominator, + s, + n, + min_value, + ) = match dist { + DistributionFunction::StepDecreasingAmount { + step_count, + decrease_per_interval_numerator, + decrease_per_interval_denominator, + s, + n, + min_value, + } => ( + *step_count as i128, + *decrease_per_interval_numerator as i128, + *decrease_per_interval_denominator as i128, + s.unwrap_or_default() as i128, + *n as i128, + min_value.unwrap_or(1) as i128, + ), + _ => panic!("expected StepDecreasingAmount"), + }; + + if x < s { + n + } else { + // let's simplify it to a form like: + // f(x) = N * a ^ b + let a = 1f64 + - (decrease_per_interval_numerator as f64 + / decrease_per_interval_denominator as f64); + let b = (x - s) / step_count; // integer by purpose, we want to round down + let f_x = n as f64 * a.powi(b.to_i32().expect("overflow")); + f_x.to_i128() + .unwrap_or_else(|| { + tracing::error!("overflow in expected_emission({})", f_x); + 0 + }) + .max(min_value) + } + } +} + +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 fails_stepwise_correct() { + let distribution_interval = 10; + let periods = BTreeMap::from([ + (0, 10_000), // h 1-30 + (2, 20_000), // h 31+ + (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, 120_000, true), + (21, 120_000, false), + (24, 120_000, false), + (35, 140_000, true), // since 20, we should get one more distribution of 20k at height 30 + (39, 140_000, false), + (46, 160_000, true), + (49, 160_000, false), + (51, 180_000, true), + (52, 180_000, false), + (70, 270_000, true), + ( + 1_000_000, + 270_000 + 50_000 * (1_000_000 - 70_000) / distribution_interval, + 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; + + /// Given linear distribution with d=0, + /// When I create a token, + /// Then I get an error. + #[test] + fn fails_divide_by_0() -> Result<(), String> { + test_linear( + 1, // a + 0, // d + None, // start_step + 100_000, // starting_amount + None, // min_value + None, // max_value + &[(10, 100_000, false)], // heights + 1, // distribution_interval + ) + } + /// Given linear distribution with d=MAX and starting amount of 1, + /// When I claim tokens, + /// Then I have only one success, and subsequent claims fail because the calculated distribution is lower than 1 + #[test] + fn divide_my_max() -> Result<(), String> { + 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 min_eq_max() -> Result<(), String> { + test_linear( + 1, + 1, + None, + 0, + Some(10), + Some(10), + &[(1, 100_010, true), (2, 100_020, true)], + 1, + ) + } + + #[test] + fn fx_eq_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 negative_a() -> Result<(), String> { + for a in [-1, -100_000, i64::MIN] { + test_linear( + a, + 1, + None, + 0, + None, + None, + &[(1, 100_000, false), (20, 100_000, false)], + 1, + )?; + } + Ok(()) + } + + #[test] + fn fails_max_lt_min() -> Result<(), String> { + for max in [0, 99] { + test_linear( + 1, + 1, + None, + 0, + Some(100), + Some(max), + &[(1, 100_000, false), (20, 100_000, false)], + 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); + }) + } +} + +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, + }; + + #[test] + fn ones() -> Result<(), String> { + test_polynomial( + Polynomial { + a: 1, + d: 1, + m: 1, + n: 1, + o: 1, + start_moment: Some(1), + b: 100_000, + min_value: None, + max_value: None, + }, + &[(10, 1_100_055, true), (20, 2_100_210, true)], + 1, + ) + } + + /// Divide by 0 + /// claim at height 10: claim failed: assertion 1 failed: expected SuccessfulExecution, got + /// [InternalError(\"storage: protocol: divide by zero error: Polynomial function: divisor d is 0\")]\nexpected balance Some(1100055) but got 100000\n\n--> + #[test] + fn fails_divide_by_0() -> Result<(), String> { + test_polynomial( + Polynomial { + a: 1, + d: 0, + m: 1, + n: 1, + o: 1, + start_moment: Some(1), + b: 100_000, + min_value: None, + max_value: None, + }, + &[(10, 1_100_055, true), (20, 2_100_210, true)], + 1, + ) + } + + /// Given max_value < min_value, + /// When I try to use the token distribution function, + /// Then the token distribution function validation fails. + #[test] + fn fails_max_lt_min_should_fail() -> Result<(), String> { + test_polynomial( + Polynomial { + a: 1, + d: 1, + m: 1, + n: 1, + o: 1, + start_moment: Some(1), + b: 100_000, + min_value: Some(100_000), + max_value: Some(10_000), + }, + &[(10, 100_000, false), (20, 100_000, false)], + 1, + ) + } + + #[test] + fn negative_a() -> Result<(), String> { + test_polynomial( + Polynomial { + a: -1, + d: 1, + m: 1, + n: 1, + o: 1, + start_moment: Some(1), + b: 100_000, + min_value: None, + max_value: None, + }, + &[(1, 199_999, true), (4, 499_990, true)], + 1, + ) + } + + #[test] + fn fails_a_min() -> Result<(), String> { + test_polynomial( + Polynomial { + a: i64::MIN, + d: 1, + m: 1, + n: 1, + o: 1, + start_moment: Some(1), + b: 100_000, + min_value: None, + max_value: None, + }, + &[(1, 100_000, false), (4, 100_000, true)], + 1, + ) + } + #[test] + fn a_minus_1_b_0() -> Result<(), String> { + test_polynomial( + Polynomial { + a: -1, + d: 1, + m: 1, + 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=i64::MIN, + /// When I try to use the token distribution function, + /// Then the token distribution function validation fails on creation. + #[test] + fn fails_o_min() -> Result<(), String> { + test_polynomial( + Polynomial { + a: 1, + d: 1, + m: 1, + n: 1, + o: i64::MIN, + start_moment: Some(1), + b: 0, + min_value: None, + max_value: None, + }, + &[(1, 100_000, false), (4, 100_000, false)], + 1, + ) + } + + #[test] + fn o_max() -> Result<(), String> { + test_polynomial( + Polynomial { + a: 1, + d: 1, + m: 1, + n: 1, + o: i64::MAX, + start_moment: Some(1), + b: 0, + min_value: None, + max_value: None, + }, + &[(1, 100_000, false), (4, 100_000, false)], + 1, + ) + } + + #[test] + #[should_panic(expected = "invalid distribution function")] + fn zero_pow_minus_1_at_h_1_invalid() { + test_polynomial( + Polynomial { + a: 1, + d: 1, + m: -1, + n: 1, + o: 0, + start_moment: Some(1), + b: 0, + min_value: None, + max_value: None, + }, + &[(1, 100_000, false), (2, 100_001, true)], + 1, + ) + .expect("should panic"); + unreachable!("should panic"); + } + #[test] + fn fails_zero_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_004, true), // 3.pow(1/2) == 1.73 - should round to 2; FAILS + (5, 100_006, true), // 4.pow(1/2) == 2 + (6, 100_008, true), // 5.pow(1/2) == 2.23 - should round to 2 + ], + 1, + ) + } + + #[test] + fn fails_o_max_m_2() -> Result<(), String> { + test_polynomial( + Polynomial { + a: 1, + d: 1, + m: 2, + n: 1, + o: i64::MAX, + start_moment: Some(1), + b: 0, + min_value: None, + max_value: None, + }, + &[(1, 100_000, false), (10, 100_000, false)], + 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 fails_poynomial_power() -> Result<(), String> { + for m in [i64::MIN, -1, 0, 1, i64::MAX] { + for n in [0, 1, u64::MAX] { + let dist = Polynomial { + a: 1, + d: 1, + m, + n, + o: 1, + start_moment: Some(1), + b: 100_000, + 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}; + + #[test] + fn fails_zeros() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: 0, // a: i64, + d: 0, // d: u64, + m: 0, // m: u64, + n: 0, // 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, + }, + &[(4, 100_000, true)], + 1, + ) + } + + /// "fails: ones - use of ln instead of log as documented + #[test] + fn fails_ones() -> 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), // log(0)+1 = 1 + (2, 100_002, true), // log(1)+1 = 1 + (3, 100_003, true), // log(3)+1 = 1 + (4, 100_005, true), // log(4)+1 = 2 (log(4) == 0.6, rounded up to 1) + ], + 1, + ) + } + #[test] + fn fails_div_by_0() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 0, // 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, + }, + &[(2, 100_002, false)], + 1, + ) + } + #[test] + fn fails_log_0() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 0, // 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), (5, 100_001, true)], + 1, + ) + } + + /// min == max means linear + #[test] + fn min_eq_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: 0, // b: TokenAmount, + min_value: Some(10), // min_value: Option, + max_value: Some(10), // max_value: Option, + }, + &[(1, 100_010, true), (5, 100_050, true)], + 1, + ) + } + #[test] + fn min_eq_max_interval_5() -> 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: 0, // b: TokenAmount, + min_value: Some(10), // min_value: Option, + max_value: Some(10), // max_value: Option, + }, + &[(5, 100_010, true), (10, 100_020, true)], + 5, + ) + } + #[test] + fn fails_min_gt_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: 1, // b: TokenAmount, + min_value: Some(10), // min_value: Option, + max_value: Some(5), // max_value: Option, + }, + &[(5, 100_000, false), (10, 100_000, false)], + 5, + ) + } + #[test] + fn fails_a_min() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: i64::MIN, // 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_000, false), // should be false, as the balance after claim == initial balance + (2, 100_001, true), + (9, 100_001, false), + (10, 100_001, false), + ], + 1, + ) + } + /// Given a logarithmic distribution function with a=MAX, + /// When I try to claim tokens, + /// Then I get an error different than InternalError. + /// + /// + #[test] + fn fails_a_max_overflows() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: i64::MAX, // 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_000, false), + (9, 100_000, false), + (10, 100_000, false), + ], + 1, + ) + } + #[test] + fn a_0_b_0() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: 0, // 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: 0, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }, + &[ + (1, 100_000, false), + (9, 100_000, false), + (10, 100_000, false), + ], + 1, + ) + } + #[test] + fn fails_log_negative() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 1, // n: u64, + o: -10, // o: i64, + start_moment: Some(1), // start_moment: Option, + b: 0, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }, + &[ + (1, 100_000, false), + (9, 100_000, false), + (10, 100_000, false), + ], + 1, + ) + } + #[test] + fn fails_o_min() -> Result<(), String> { + test_logarithmic( + Logarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 1, // n: u64, + o: i64::MIN, // o: i64, + start_moment: Some(1), // start_moment: Option, + b: 0, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }, + &[ + (1, 100_000, false), + (9, 100_000, false), + (10, 100_000, false), + ], + 1, + ) + } + #[test] + fn fails_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: u64::MAX, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }, + &[ + (1, 100_000, false), + (9, 100_000, false), + (10, 100_000, false), + ], + 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 fails_zeros() -> Result<(), String> { + let dist = InvertedLogarithmic { + a: 0, // a: i64, + d: 0, // d: u64, + m: 0, // m: u64, + n: 0, // 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 = [(4, 100_000, true)]; + + run_test(dist, &steps, 1) + } + + #[test] + fn fails_ones() -> Result<(), String> { + 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_002, true), + (3, 100_003, true), + (4, 100_005, true), + ]; + + run_test(dist, &steps, 1) + } + + #[test] + fn fails_divide_by_zero() -> Result<(), String> { + let dist = InvertedLogarithmic { + a: 1, // a: i64, + d: 0, // 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 = [(2, 100_002, false)]; + + run_test(dist, &steps, 1) + } + + #[test] + fn fails_n_zero_log_zero() -> Result<(), String> { + let dist = InvertedLogarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 0, // 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), (5, 100_001, true)]; + + run_test(dist, &steps, 1) + } + + #[test] + fn min_eq_max_means_linear() -> Result<(), String> { + 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: 0, // b: TokenAmount, + min_value: Some(10), // min_value: Option, + max_value: Some(10), // max_value: Option, + }; + let steps = [(1, 100_010, true), (5, 100_050, true)]; + + run_test(dist, &steps, 1) + } + + #[test] + fn min_eq_max_means_linear_interval_5() -> Result<(), String> { + 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: 0, // b: TokenAmount, + min_value: Some(10), // min_value: Option, + max_value: Some(10), // max_value: Option, + }; + let steps = [(5, 100_010, true), (10, 100_020, true)]; + + run_test(dist, &steps, 5) + } + + #[test] + fn fails_min_gt_max() -> Result<(), String> { + 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: Some(10), // min_value: Option, + max_value: Some(5), // max_value: Option, + }; + let steps = [(5, 100_000, false), (10, 100_000, false)]; + + run_test(dist, &steps, 5) + } + + #[test] + fn fails_a_min() -> Result<(), String> { + let dist = InvertedLogarithmic { + a: i64::MIN, // 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_000, false), // f(1) should be < 0, is 1 + (9, 100_000, false), + (10, 100_000, false), + ]; + + run_test(dist, &steps, 1) + } + + #[test] + fn a_max() -> Result<(), String> { + let dist = InvertedLogarithmic { + a: i64::MAX, // 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), // f(x) = 0 for x>1 + (9, 100_001, false), + (10, 100_001, false), + ]; + + run_test(dist, &steps, 1) + } + + #[test] + fn a_zero_b_zero() -> Result<(), String> { + let dist = InvertedLogarithmic { + a: 0, // 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: 0, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }; + let steps = [ + (1, 100_000, false), + (9, 100_000, false), + (10, 100_000, false), + ]; + + run_test(dist, &steps, 1) + } + + #[test] + fn fails_log_negative() -> Result<(), String> { + let dist = InvertedLogarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 1, // n: u64, + o: -10, // o: i64, + start_moment: Some(1), // start_moment: Option, + b: 0, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }; + let steps = [ + (1, 100_000, false), + (9, 100_000, false), + (10, 100_000, false), + ]; + + run_test(dist, &steps, 1) + } + + #[test] + fn fails_o_min() -> Result<(), String> { + let dist = InvertedLogarithmic { + a: 1, // a: i64, + d: 1, // d: u64, + m: 1, // m: u64, + n: 1, // n: u64, + o: i64::MIN, // o: i64, + start_moment: Some(1), // start_moment: Option, + b: 0, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }; + let steps = [ + (1, 100_000, false), + (9, 100_000, false), + (10, 100_000, false), + ]; + + run_test(dist, &steps, 1) + } + + #[test] + fn fails_b_max() -> Result<(), String> { + 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: u64::MAX, // b: TokenAmount, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, + }; + let steps = [ + (1, 100_000, false), + (9, 100_000, false), + (10, 100_000, false), + ]; + + 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(10); + /// 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 conveniance, 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"); + + perpetual_distribution + .distribution_type + .function() + .validate(contract_start_time) + .map_err(|e| format!("invalid distribution function: {:?}", e))?; + + 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) + } + } +} From d475e0f992d60658451ae9ef41f5fc57fd3e7d7f Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Wed, 16 Apr 2025 01:43:23 +0700 Subject: [PATCH 2/6] test(platform): token distribution step decreasing tests and improvements (#2545) --- .../distribution_function/encode.rs | 28 +- .../distribution_function/evaluate.rs | 80 +- .../distribution_function/mod.rs | 46 +- .../distribution_function/validation.rs | 74 +- .../reward_distribution_type/mod.rs | 19 +- .../distribution/perpetual/block_based.rs | 880 ++++++++++++------ 6 files changed, 779 insertions(+), 348 deletions(-) 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..102141a6f07 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)?; @@ -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 => { @@ -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, }) } 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 97316e441cd..e0013a0a34b 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,7 @@ 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, +}; use crate::ProtocolError; impl DistributionFunction { @@ -51,42 +53,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 @@ -501,8 +513,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 +534,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), }; 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 c04a5b3521d..a7762b72fe8 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 @@ -11,6 +11,15 @@ 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 DEFAULT_STEP_DECREASING_AMOUNT_MAX_CYCLES_BEFORE_TRAILING_DISTRIBUTION: u16 = 128; + pub const MAX_LINEAR_SLOPE_PARAM: u64 = 256; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)] @@ -90,8 +99,13 @@ pub enum DistributionFunction { /// # 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 @@ -105,8 +119,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, }, @@ -530,23 +546,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(()) } 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..1af60a59758 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 @@ -17,12 +17,12 @@ impl DistributionFunction { 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(), @@ -60,27 +60,67 @@ impl 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, } => { // Validate n. - if *n == 0 || *n > u32::MAX as u64 { + if *distribution_start_amount == 0 + || *distribution_start_amount > MAX_DISTRIBUTION_PARAM as u64 + { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( "n".to_string(), 1, - u32::MAX as i64, + MAX_DISTRIBUTION_PARAM as i64, None, ) .into(), )); } + + if *trailing_distribution_interval_amount > MAX_DISTRIBUTION_PARAM { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "trailing_distribution_interval_amount".to_string(), + 0, + MAX_DISTRIBUTION_PARAM as i64, + None, + ) + .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 +137,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(), @@ -921,7 +961,7 @@ 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); assert!(result @@ -939,8 +979,10 @@ 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); @@ -956,8 +998,10 @@ 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); @@ -973,8 +1017,10 @@ 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); 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-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 13919b9e88d..5e95a903ae2 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 @@ -509,10 +509,11 @@ mod fixed_amount { 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> { - super::test_suite::check_heights( + check_heights( DistributionFunction::FixedAmount { amount: 1 }, &[ TestStep::new(1, 100_001, true), @@ -527,14 +528,15 @@ mod fixed_amount { } // Given some token configuration, - // When a claim is made at block 42, + // 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() { - super::test_suite::check_heights( + check_heights( DistributionFunction::FixedAmount { amount: 50 }, &[ - TestStep::new(1, 100_000, true), + TestStep::new(1, 100_000, false), TestStep::new(41, 100_200, true), TestStep::new(46, 100_200, false), TestStep::new(50, 100_250, true), @@ -549,12 +551,11 @@ mod fixed_amount { /// Test case for overflow error. /// - /// TODO: Fails, please fix. /// /// claim at height 1000000000000: claim failed: assertion 0 failed: expected SuccessfulExecution, /// got [InternalError(\"storage: protocol: overflow error: Overflow in FixedAmount evaluation\")]" #[test] - fn fail_fixed_amount_1_000_000_000() { + fn fixed_amount_at_trillionth_block() { check_heights( DistributionFunction::FixedAmount { amount: 1_000_000_000, @@ -564,10 +565,17 @@ mod fixed_amount { 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 + 5 * 1_000_000_000, - false, + 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, @@ -584,17 +592,12 @@ mod fixed_amount { fn fixed_amount_0() { check_heights( DistributionFunction::FixedAmount { amount: 0 }, - &[ - (41, 100000, false), - (46, 100000, false), - (50, 100000, false), - (1000, 100000, false), - ], + &[(41, 100000, false)], None, 10, None, ) - .expect("\nfixed amount zero increase\n"); + .expect_err("\namount should not be 0\n"); } #[test] @@ -629,7 +632,7 @@ mod fixed_amount { /// When I claim tokens, /// Then I don't get an InternalError. #[test] - fn fail_test_block_based_perpetual_fixed_amount_u64_max() { + 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)], @@ -637,7 +640,28 @@ mod fixed_amount { 10, None, ) - .expect("\nfixed amount u64::MAX should pass\n"); + .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 { @@ -652,6 +676,7 @@ mod random { 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, @@ -711,16 +736,31 @@ mod random { .expect("no rewards"); } #[test] - fn fails_test_block_based_perpetual_random_0_max() { + 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] + fn test_block_based_perpetual_random_0_MAX_distribution_param() { + check_heights( + DistributionFunction::Random { + min: 0, + max: MAX_DISTRIBUTION_PARAM, + }, &[ - TestStep::new(41, INITIAL_BALANCE, false), - TestStep::new(50, INITIAL_BALANCE, false), - TestStep::new(100, INITIAL_BALANCE, false), + TestStep::new(41, 382777733174502, true), + TestStep::new(50, 447703202535488, true), + TestStep::new(100, 1080112432401531, true), ], None, 10, @@ -829,172 +869,591 @@ mod random { mod step_decreasing { use dpp::balances::credits::TokenAmount; - use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; - use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; - use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters; - 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::data_contract::TokenConfiguration; - use rust_decimal::prelude::ToPrimitive; + 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, platform_types::state_transitions_processing_result::StateTransitionExecutionResult}; use crate::execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::INITIAL_BALANCE; - use super::test_suite::{TestStep, TestSuite}; + 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_100_blocks() -> Result<(), String> { + fn claim_every_block() { run_test( 1, 1, 100, None, - 100_000, + None, + 10_000, + 0, Some(1), - Some((1..1000).step_by(100).collect()), + (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_100_blocks_with_1_percent_increase() -> Result<(), String> { + 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), - Some((1..1000).step_by(100).collect()), + (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_every_500_blocks_fails_due_to_max_token_redemption_cycles() -> Result<(), String> { - let result = run_test( + fn claim_with_no_decrease_should_fail() { + let result_str = run_test( 1, - 101, + 0, 100, None, + None, 100_000, + 0, Some(1), - Some((1..1000).step_by(500).collect()), + (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}" ); - assert!(result.is_err_and( - |s| s.contains("claim at height 501: expected balance Some(100510) but got 100138") - )); - Ok(()) } #[test] - fn fails_with_1000x_increase_overflow() -> Result<(), String> { - run_test(1, 1000, 1, None, 100_000, Some(1), Some(vec![1, 7]), 1) + 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..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(|_| *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 full_decrease_min_1_100() -> Result<(), String> { + 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, - 1, + 100, + None, None, 100_000, + 0, Some(min), - Some(vec![1, 2, 3, 10, 100]), + claim_heights.clone(), 1, + expected_amounts.clone(), ) - .map_err(|e| format!("failed with min {}: {}", min, e))?; + .map_err(|e| format!("failed with min {}: {}", min, e)) + .expect("should pass"); } + } - Ok(()) + #[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 fails_full_decrease_min_eq_u64_max() -> Result<(), String> { - run_test( - 1, - 1, + fn full_decrease_min_eq_u64_max() { + let result_str = run_test( 1, + u16::MAX - 1, + u16::MAX, None, - 100_000, + None, + MAX_DISTRIBUTION_PARAM, + 0, Some(u64::MAX), - Some(vec![1, 2, 3, 10, 100]), + 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 no_decrease_changing_min() -> Result<(), String> { - for min in [None, Some(0), Some(1), Some(100)] { - run_test(1, 0, 1, None, 100_000, min, Some(vec![1, 2, 3, 10, 100]), 1) - .map_err(|e| format!("failed with min {:?}: {}", min, e))?; - } - Ok(()) + 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 full_decrease_step_10_interval_1() -> Result<(), String> { - run_test(10, 1, 1, None, 100_000, None, Some(vec![2, 7, 9]), 1) + 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 full_decrease_start_5_step_10_interval_1() -> Result<(), String> { - run_test( - 10, + fn start_over_max_distribution_param_should_fail() { + let result_str = run_test( 1, 1, - Some(5), - 100_000, + u16::MAX, + None, + None, + MAX_DISTRIBUTION_PARAM + 1, + 0, None, - Some(vec![2, 7, 9, 13, 14]), + 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 full_decrease_start_5_step_10_interval_1_err_at_15() -> Result<(), String> { - let result = run_test(10, 1, 1, Some(5), 100_000, None, Some(vec![14, 15]), 1); - assert!(result.is_err_and(|s| s.contains("claim at height 15: claim failed"))); - Ok(()) + 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 fails_half_decrease_changing_step_and_interval() -> Result<(), String> { - for step in [5, 10] { - for distribution_interval in [1, 5] { - run_test( - step, - 1, - 2, - None, - 100_000, - None, - Some(vec![5, 10, 18, 22, 100]), - distribution_interval, - ) - .map_err(|e| { - format!( - "failed with step {} interval {}: {}", - step, distribution_interval, e - ) - })?; - } - } - - Ok(()) + 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_chainging_s() -> Result<(), String> { - for s in [None, Some(1), Some(5)] { - run_test(1, 10, 100, s, 100_000, None, Some(vec![5, 10, 15, 20]), 1) - .map_err(|e| format!("failed with s {:?}: {}", s, e))?; - } - Ok(()) + 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. @@ -1003,47 +1462,45 @@ mod step_decreasing { step_count: u32, decrease_per_interval_numerator: u16, decrease_per_interval_denominator: u16, - s: Option, - n: TokenAmount, - min_value: Option, - claim_heights: Option>, - distribution_interval: u64, + 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, - s, - n, + start_decreasing_offset, + max_interval_count, + distribution_start_amount, + trailing_distribution_interval_amount, min_value, }; - let claim_heights = - claim_heights.unwrap_or(vec![1, 2, 3, 4, 5, 10, 20, 30, 50, 100, 1_000_000]); - let expected_balances = claim_heights - .iter() - .map(|&h| { - // initial balance, defined in contract js - let mut expected_balance: i128 = INITIAL_BALANCE as i128; - // loop over blocks, starting with S, with step PERPETUAL_DISTRIBUTION_INTERVAL - for i in (1..=h).step_by(distribution_interval as usize) { - expected_balance += expected_emission(i, &dist); - } - tracing::debug!("expected balance at height {}: {}", h, expected_balance); - expected_balance.to_u64().unwrap_or_else(|| { - tracing::error!("overflow in expected balance at height {}", h); - 0 - }) // to handle tests that overflow - }) - .collect::>(); - // we expect all tests to pass + 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)| (h, b, true)) + .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 comparision easier in test_case + // we return Err(()) to make result comparison easier in test_case check_heights( dist, &claims, @@ -1055,141 +1512,6 @@ mod step_decreasing { tracing::error!(e); }) } - - /// Given that we have a distribution function distributing some tokens, - /// When I claim tokens with delay bigger than [platform_version.system_limits.max_token_redemption_cycles], - /// Then I need to run the claim more than once to get correct balance. - #[test] - fn test_claim_more_than_max_token_redemption_cycles() { - let dist = DistributionFunction::StepDecreasingAmount { - step_count: 1, - decrease_per_interval_numerator: 101, - decrease_per_interval_denominator: 100, - s: None, - n: 100_000, - min_value: Some(1), - }; - - let dist_clone = dist.clone(); - let mut suite = TestSuite::new( - 10_200_000_000, - 1, - TokenDistributionType::Perpetual, - Some(|token_configuration: &mut TokenConfiguration| { - token_configuration - .distribution_rules_mut() - .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( - TokenPerpetualDistributionV0 { - distribution_type: RewardDistributionType::BlockBasedDistribution { - interval: 1, - function: dist_clone, - }, - distribution_recipient: TokenDistributionRecipient::ContractOwner, - }, - ))); - }), - ); - - for (height, balance) in [(1, 100_001), (101, 100_110), (501, 100_510)] { - // claim at height 500;loop until we have no more coins - let step = TestStep { - name: format!("height {}", height), - base_height: height - 1, - base_time_ms: 10_200_000_000, - expected_balance: None, - claim_transition_assertions: vec![|v| match v { - [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => Ok(()), - _ => Err(format!("got {:?}", v)), - }], - }; - - let mut loops = 0; - let err = loop { - if let Err(err) = suite.execute_step(&step) { - break err; - } - loops += 1; - }; - - // max_token_redemption_cycles is 128 - if height == 501 { - assert_eq!(loops, (501 - 101) / 128 + 1); - } else { - assert_eq!(loops, 1); - } - - assert!( - err.contains("InvalidTokenClaimNoCurrentRewards"), - "expected InvalidTokenClaimNoCurrentRewards error, got {}", - err - ); - - assert_eq!( - suite - .get_balance() - .expect("get balance") - .unwrap_or_default(), - balance, - "expected balance at height {}: {}", - height, - balance - ); - } - } - - // ===== HELPER FUNCTIONS ===== // - - /// Calculate expected emission at provided height. - /// - /// We use [i128] to ensure we handle overflows better than the original code. - /// - // f(x) = n * (1 - (decrease_per_interval_numerator / decrease_per_interval_denominator))^((x - s) / step_count) - pub(super) fn expected_emission(x: u64, dist: &DistributionFunction) -> i128 { - let x = x as i128; - let ( - step_count, - decrease_per_interval_numerator, - decrease_per_interval_denominator, - s, - n, - min_value, - ) = match dist { - DistributionFunction::StepDecreasingAmount { - step_count, - decrease_per_interval_numerator, - decrease_per_interval_denominator, - s, - n, - min_value, - } => ( - *step_count as i128, - *decrease_per_interval_numerator as i128, - *decrease_per_interval_denominator as i128, - s.unwrap_or_default() as i128, - *n as i128, - min_value.unwrap_or(1) as i128, - ), - _ => panic!("expected StepDecreasingAmount"), - }; - - if x < s { - n - } else { - // let's simplify it to a form like: - // f(x) = N * a ^ b - let a = 1f64 - - (decrease_per_interval_numerator as f64 - / decrease_per_interval_denominator as f64); - let b = (x - s) / step_count; // integer by purpose, we want to round down - let f_x = n as f64 * a.powi(b.to_i32().expect("overflow")); - f_x.to_i128() - .unwrap_or_else(|| { - tracing::error!("overflow in expected_emission({})", f_x); - 0 - }) - .max(min_value) - } - } } mod stepwise { @@ -2515,14 +2837,14 @@ mod test_suite { /// 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(); + .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. @@ -2821,12 +3143,16 @@ mod test_suite { .perpetual_distribution() .expect("expected perpetual distribution"); - perpetual_distribution + let consensus_result = perpetual_distribution .distribution_type .function() .validate(contract_start_time) .map_err(|e| format!("invalid distribution function: {:?}", e))?; + if let Some(error) = consensus_result.first_error() { + return Err(error.to_string()); + } + Ok(()) } From ea6d63f1bf1832584e89989605ac25f1ed7f2978 Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Wed, 16 Apr 2025 17:12:19 +0700 Subject: [PATCH 3/6] test(platform): distribution inverted log tests (#2547) --- .../distribution_function/evaluate.rs | 54 ++- .../distribution_function/mod.rs | 34 +- .../distribution_function/validation.rs | 117 +++++- ...distribution_function_incoherence_error.rs | 2 +- .../distribution/perpetual/block_based.rs | 338 ++++++------------ .../rs-platform-version/src/version/v9.rs | 1 - 6 files changed, 274 insertions(+), 272 deletions(-) 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 e0013a0a34b..f88a3faf8c0 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 @@ -420,7 +420,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", @@ -437,26 +441,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 { 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 a7762b72fe8..6f59b4d855d 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 @@ -22,6 +22,10 @@ pub const DEFAULT_STEP_DECREASING_AMOUNT_MAX_CYCLES_BEFORE_TRAILING_DISTRIBUTION pub const MAX_LINEAR_SLOPE_PARAM: u64 = 256; +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; + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)] pub enum DistributionFunction { /// Emits a constant (fixed) number of tokens for every period. @@ -499,22 +503,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 * log(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. 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 1af60a59758..95023f2e647 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 @@ -5,7 +5,8 @@ use crate::consensus::basic::data_contract::{ InvalidTokenDistributionFunctionInvalidParameterTupleError, }; 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_LINEAR_SLOPE_PARAM, + MAX_LOG_A_PARAM, MIN_LOG_A_PARAM, }; use crate::validation::SimpleConsensusValidationResult; use crate::ProtocolError; @@ -60,7 +61,7 @@ impl DistributionFunction { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - start_decreasing_offset: s, + start_decreasing_offset, max_interval_count, distribution_start_amount, trailing_distribution_interval_amount, @@ -68,7 +69,7 @@ impl DistributionFunction { } => { // Validate n. if *distribution_start_amount == 0 - || *distribution_start_amount > MAX_DISTRIBUTION_PARAM as u64 + || *distribution_start_amount > MAX_DISTRIBUTION_PARAM { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidTokenDistributionFunctionInvalidParameterError::new( @@ -149,7 +150,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( @@ -460,12 +461,13 @@ impl DistributionFunction { .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_LOG_A_PARAM, None, ) .into(), @@ -638,13 +640,14 @@ 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, - MAX_DISTRIBUTION_PARAM as i64, - None, + MIN_LOG_A_PARAM, + MAX_LOG_A_PARAM, + Some(0), ) .into(), )); @@ -764,6 +767,18 @@ 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(), + )); + } // Check for division by zero. if *d == 0 { return Ok(SimpleConsensusValidationResult::new_with_error( @@ -2209,6 +2224,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); + 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); + 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); + 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 { @@ -2349,6 +2424,28 @@ mod tests { ); } + #[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); + 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 { 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/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 5e95a903ae2..7d7c0689ae5 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 @@ -173,7 +173,7 @@ mod perpetual_distribution_block { assert_matches!( processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( + [PaidConsensusError( ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), _ )] @@ -354,7 +354,7 @@ mod perpetual_distribution_block { assert_matches!( processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( + [PaidConsensusError( ConsensusError::StateError(StateError::InvalidTokenClaimWrongClaimant(_)), _ )] @@ -816,7 +816,7 @@ mod random { suite.execute(&tests).expect("should execute"); let data = balances_result.lock().unwrap(); - // substract balance from previous step (for first step, substract initial balance of 100_000) + // subtract balance from previous step (for first step, subtract initial balance of 100_000) let diffs: Vec = data .iter() .scan(INITIAL_BALANCE, |prev, &x| { @@ -871,7 +871,7 @@ 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, platform_types::state_transitions_processing_result::StateTransitionExecutionResult}; + 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] = [ @@ -1755,7 +1755,7 @@ mod polynomial { /// Divide by 0 /// claim at height 10: claim failed: assertion 1 failed: expected SuccessfulExecution, got - /// [InternalError(\"storage: protocol: divide by zero error: Polynomial function: divisor d is 0\")]\nexpected balance Some(1100055) but got 100000\n\n--> + /// [InternalError(\"storage: protocol: divide by zero error: Polynomial function: divisor d is 0\")]\n expected balance Some(1100055) but got 100000\n\n--> #[test] fn fails_divide_by_0() -> Result<(), String> { test_polynomial( @@ -1979,11 +1979,11 @@ mod polynomial { }) } - /// Test various combinations of `m/n` in [DistributionFunction::Polynomial] distribution. + /// Test various combinations of `m/n` in `[DistributionFunction::Polynomial]` distribution. /// /// We expect this test not to end with InternalError. #[test] - fn fails_poynomial_power() -> Result<(), String> { + fn fails_polynomial_power() -> Result<(), String> { for m in [i64::MIN, -1, 0, 1, i64::MAX] { for n in [0, 1, u64::MAX] { let dist = Polynomial { @@ -2059,25 +2059,6 @@ mod logarithmic { use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,Logarithmic}; - #[test] - fn fails_zeros() -> Result<(), String> { - test_logarithmic( - Logarithmic { - a: 0, // a: i64, - d: 0, // d: u64, - m: 0, // m: u64, - n: 0, // 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, - }, - &[(4, 100_000, true)], - 1, - ) - } - /// "fails: ones - use of ln instead of log as documented #[test] fn fails_ones() -> Result<(), String> { @@ -2220,7 +2201,7 @@ mod logarithmic { } /// Given a logarithmic distribution function with a=MAX, /// When I try to claim tokens, - /// Then I get an error different than InternalError. + /// Then I get an error different from InternalError. /// /// #[test] @@ -2357,25 +2338,8 @@ mod inverted_logarithmic { use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,InvertedLogarithmic}; #[test] - fn fails_zeros() -> Result<(), String> { - let dist = InvertedLogarithmic { - a: 0, // a: i64, - d: 0, // d: u64, - m: 0, // m: u64, - n: 0, // 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 = [(4, 100_000, true)]; - - run_test(dist, &steps, 1) - } - - #[test] - fn fails_ones() -> Result<(), String> { + fn ones() -> 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, @@ -2389,235 +2353,145 @@ mod inverted_logarithmic { }; let steps = [ (1, 100_001, true), - (2, 100_002, true), - (3, 100_003, true), - (4, 100_005, true), - ]; - - run_test(dist, &steps, 1) - } - - #[test] - fn fails_divide_by_zero() -> Result<(), String> { - let dist = InvertedLogarithmic { - a: 1, // a: i64, - d: 0, // 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 = [(2, 100_002, false)]; - - run_test(dist, &steps, 1) - } - - #[test] - fn fails_n_zero_log_zero() -> Result<(), String> { - let dist = InvertedLogarithmic { - a: 1, // a: i64, - d: 1, // d: u64, - m: 1, // m: u64, - n: 0, // 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), (5, 100_001, true)]; - - run_test(dist, &steps, 1) - } - - #[test] - fn min_eq_max_means_linear() -> Result<(), String> { - 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: 0, // b: TokenAmount, - min_value: Some(10), // min_value: Option, - max_value: Some(10), // max_value: Option, - }; - let steps = [(1, 100_010, true), (5, 100_050, true)]; - - run_test(dist, &steps, 1) - } - - #[test] - fn min_eq_max_means_linear_interval_5() -> Result<(), String> { - 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: 0, // b: TokenAmount, - min_value: Some(10), // min_value: Option, - max_value: Some(10), // max_value: Option, - }; - let steps = [(5, 100_010, true), (10, 100_020, true)]; - - run_test(dist, &steps, 5) - } - - #[test] - fn fails_min_gt_max() -> Result<(), String> { - 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: Some(10), // min_value: Option, - max_value: Some(5), // max_value: Option, - }; - let steps = [(5, 100_000, false), (10, 100_000, false)]; - - run_test(dist, &steps, 5) - } - - #[test] - fn fails_a_min() -> Result<(), String> { - let dist = InvertedLogarithmic { - a: i64::MIN, // 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_000, false), // f(1) should be < 0, is 1 - (9, 100_000, false), - (10, 100_000, false), + (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 a_max() -> Result<(), String> { + fn inv_log_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: i64::MAX, // a: i64, + a: 10000, // 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), // f(x) = 0 for x>1 - (9, 100_001, false), - (10, 100_001, false), - ]; - - run_test(dist, &steps, 1) - } - - #[test] - fn a_zero_b_zero() -> Result<(), String> { - let dist = InvertedLogarithmic { - a: 0, // 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, + 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, 100_000, false), - (9, 100_000, false), - (10, 100_000, false), + (1, 185_171, true), + (2, 263_411, true), + (1000, 6_110_958, true), ]; run_test(dist, &steps, 1) } #[test] - fn fails_log_negative() -> Result<(), String> { + fn inv_log_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: 1, // a: i64, + a: 100, // a: i64, d: 1, // d: u64, m: 1, // m: u64, - n: 1, // n: u64, - o: -10, // o: i64, - start_moment: Some(1), // start_moment: Option, + 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, 100_000, false), - (9, 100_000, false), - (10, 100_000, false), + (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 fails_o_min() -> Result<(), String> { + fn inv_log_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: 1, // a: i64, + a: -2200, // a: i64, d: 1, // d: u64, m: 1, // m: u64, - n: 1, // n: u64, - o: i64::MIN, // o: i64, - start_moment: Some(1), // start_moment: Option, - b: 0, // b: TokenAmount, + 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, 100_000, false), - (9, 100_000, false), - (10, 100_000, false), + (1, 101351, true), + (2, 102703, true), + (100, 238739, true), + (210, 399539, true), + (300, 537282, true), ]; run_test(dist, &steps, 1) } - #[test] - fn fails_b_max() -> Result<(), String> { - 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: u64::MAX, // b: TokenAmount, - min_value: None, // min_value: Option, - max_value: None, // max_value: Option, - }; - let steps = [ - (1, 100_000, false), - (9, 100_000, false), - (10, 100_000, false), - ]; - - run_test(dist, &steps, 1) - } /// f(x) = (a * log( n / (m * (x - s + o)) )) / d + b fn run_test( dist: DistributionFunction, @@ -2686,7 +2560,7 @@ mod test_suite { /// * `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 conveniance, you can provide `steps` as a [`TestStep`] or a slice of tuples, where each tuple contains: + /// 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 @@ -2738,7 +2612,7 @@ mod test_suite { identity: dpp::prelude::Identity, signer: SimpleSigner, identity_public_key: IdentityPublicKey, - token_id: Option, + token_id: Option, contract: Option, start_time: Option, token_distribution_type: TokenDistributionType, diff --git a/packages/rs-platform-version/src/version/v9.rs b/packages/rs-platform-version/src/version/v9.rs index abcc4fa5a54..3ff77c6d9b6 100644 --- a/packages/rs-platform-version/src/version/v9.rs +++ b/packages/rs-platform-version/src/version/v9.rs @@ -17,7 +17,6 @@ use crate::version::dpp_versions::DPPVersion; use crate::version::drive_abci_versions::drive_abci_method_versions::v6::DRIVE_ABCI_METHOD_VERSIONS_V6; use crate::version::drive_abci_versions::drive_abci_query_versions::v1::DRIVE_ABCI_QUERY_VERSIONS_V1; use crate::version::drive_abci_versions::drive_abci_structure_versions::v1::DRIVE_ABCI_STRUCTURE_VERSIONS_V1; -use crate::version::drive_abci_versions::drive_abci_validation_versions::v4::DRIVE_ABCI_VALIDATION_VERSIONS_V4; use crate::version::drive_abci_versions::drive_abci_validation_versions::v5::DRIVE_ABCI_VALIDATION_VERSIONS_V5; use crate::version::drive_abci_versions::drive_abci_withdrawal_constants::v2::DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V2; use crate::version::drive_abci_versions::DriveAbciVersion; From b6b5f350792a42375303c30751fac0a4d5cb6606 Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Wed, 16 Apr 2025 18:49:53 +0700 Subject: [PATCH 4/6] test(platform): distribution log tests (#2548) --- .../distribution_function/encode.rs | 20 +- .../distribution_function/evaluate.rs | 101 ++++++-- .../distribution_function/mod.rs | 28 +- .../distribution_function/validation.rs | 98 +++++-- .../distribution/perpetual/block_based.rs | 242 +++++------------- 5 files changed, 238 insertions(+), 251 deletions(-) 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 102141a6f07..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 @@ -87,7 +87,7 @@ impl Encode for DistributionFunction { n, o, start_moment: s, - c, + b, min_value, max_value, } => { @@ -98,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)?; } @@ -234,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 { @@ -244,8 +244,8 @@ impl Decode for DistributionFunction { m, n, o, - start_moment: s, - c, + start_moment, + b, min_value, max_value, }) @@ -384,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 { @@ -394,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 f88a3faf8c0..3478e904543 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 @@ -254,7 +254,7 @@ impl DistributionFunction { n, o, start_moment, - c, + b, min_value, max_value, } => { @@ -284,7 +284,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 { @@ -346,22 +346,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 { @@ -374,6 +425,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 { @@ -900,7 +957,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: None, max_value: None, }; @@ -918,7 +975,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: None, max_value: None, }; @@ -938,7 +995,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 5, + b: 5, min_value: None, max_value: None, }; @@ -957,7 +1014,7 @@ mod tests { n: 10, o: 0, start_moment: Some(0), - c: 0, + b: 0, min_value: None, max_value: None, }; @@ -976,7 +1033,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 0, + b: 0, min_value: None, max_value: Some(100000000), }; @@ -997,7 +1054,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: None, max_value: None, }; @@ -1016,7 +1073,7 @@ mod tests { n: 1, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(11), max_value: None, }; @@ -1035,7 +1092,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 }; @@ -1061,7 +1118,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 6f59b4d855d..e2e70b3b345 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 @@ -355,7 +355,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 @@ -364,7 +364,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 @@ -390,7 +390,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 @@ -403,12 +403,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: @@ -440,7 +440,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: @@ -452,7 +452,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:** @@ -476,13 +476,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 @@ -504,7 +504,7 @@ pub enum DistributionFunction { /// /// # Example /// ```text - /// f(x) = 10000 * log(5000 / x) + /// f(x) = 10000 * ln(5000 / x) /// ``` /// - Values: a = 10000 n = 5000 m = 1 o = 0 b = 0 d = 0 /// y @@ -651,18 +651,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 95023f2e647..ae87ad0c97e 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 @@ -428,7 +428,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, @@ -436,7 +436,7 @@ impl DistributionFunction { n, o, start_moment: s, - c, + b, min_value, max_value, } => { @@ -514,10 +514,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, @@ -572,7 +572,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, } @@ -607,7 +607,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, @@ -629,7 +629,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(), @@ -653,6 +653,18 @@ impl DistributionFunction { )); } + if *b > MAX_DISTRIBUTION_PARAM { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidTokenDistributionFunctionInvalidParameterError::new( + "b".to_string(), + 0, + MAX_DISTRIBUTION_PARAM as i64, + None, + ) + .into(), + )); + } + if let Some(s) = s { if *s > MAX_DISTRIBUTION_PARAM { return Ok(SimpleConsensusValidationResult::new_with_error( @@ -779,6 +791,19 @@ impl DistributionFunction { .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( @@ -1622,7 +1647,7 @@ mod tests { n: 2, o: -3999, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(1000000), }; @@ -1647,7 +1672,7 @@ mod tests { n: 0, o: 1, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(100), }; @@ -1667,7 +1692,7 @@ mod tests { n: 2, o: 1, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(100), }; @@ -1690,7 +1715,7 @@ mod tests { n: 2, o: 1, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(100), }; @@ -1713,7 +1738,7 @@ 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 }; @@ -1736,7 +1761,7 @@ 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), }; @@ -1759,7 +1784,7 @@ 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), }; @@ -1782,7 +1807,7 @@ mod tests { n: 4, o: 2, start_moment: Some(START_MOMENT), - c: 8, + b: 8, min_value: Some(2), max_value: Some(50), }; @@ -1802,7 +1827,7 @@ 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 }; @@ -1822,7 +1847,7 @@ mod tests { n: 1, o: 1, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(1), max_value: Some(MAX_DISTRIBUTION_PARAM), }; @@ -1845,7 +1870,7 @@ 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` }; @@ -1868,7 +1893,7 @@ 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), }; @@ -1891,7 +1916,7 @@ mod tests { n: 2, o: 1, start_moment: Some(0), - c: 5, + b: 5, min_value: Some(1), max_value: None, // Should fail }; @@ -1914,7 +1939,7 @@ 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), }; @@ -1937,7 +1962,7 @@ mod tests { n: 2, o: 0, start_moment: Some(0), - c: 10, + b: 10, min_value: Some(10), max_value: Some(100), }; @@ -1960,7 +1985,7 @@ mod tests { n: 10, o: -3, start_moment: Some(0), - c: 5, + b: 5, min_value: Some(10), max_value: Some(1000), }; @@ -1991,7 +2016,7 @@ mod tests { n: 4, o: 2, start_moment: Some(START_MOMENT), - c: 8, + b: 8, min_value: Some(5), max_value: Some(100), }; @@ -2011,7 +2036,7 @@ 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), }; @@ -2090,6 +2115,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); + 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 { 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 7d7c0689ae5..75c6a982b86 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 @@ -2058,10 +2058,10 @@ 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}; - /// "fails: ones - use of ln instead of log as documented #[test] - fn fails_ones() -> Result<(), String> { + fn log_distribution_basic() -> Result<(), String> { test_logarithmic( Logarithmic { a: 1, // a: i64, @@ -2075,111 +2075,60 @@ mod logarithmic { max_value: None, // max_value: Option, }, &[ - (1, 100_001, true), // log(0)+1 = 1 - (2, 100_002, true), // log(1)+1 = 1 - (3, 100_003, true), // log(3)+1 = 1 - (4, 100_005, true), // log(4)+1 = 2 (log(4) == 0.6, rounded up to 1) + (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 fails_div_by_0() -> Result<(), String> { + 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: 0, // 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, - }, - &[(2, 100_002, false)], - 1, - ) - } - #[test] - fn fails_log_0() -> Result<(), String> { - test_logarithmic( - Logarithmic { - a: 1, // a: i64, - d: 1, // d: u64, - m: 0, // m: u64, - n: 1, // n: u64, - o: 1, // o: i64, - start_moment: Some(1), // start_moment: Option, - b: 1, // b: TokenAmount, + 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_001, true), (5, 100_001, true)], + &[(1, 100_000, false), (5, 100_000, false)], 1, ) } - /// min == max means linear #[test] - fn min_eq_max() -> Result<(), String> { + 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, + 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, + n: u64::MAX, // n: u64, + o: 0, // o: i64, + start_moment: Some(0), // start_moment: Option, b: 0, // b: TokenAmount, - min_value: Some(10), // min_value: Option, - max_value: Some(10), // max_value: Option, + min_value: None, // min_value: Option, + max_value: None, // max_value: Option, }, - &[(1, 100_010, true), (5, 100_050, true)], + &[(1, 100_044, true), (5, 100_214, true)], 1, ) } + #[test] - fn min_eq_max_interval_5() -> 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: 0, // b: TokenAmount, - min_value: Some(10), // min_value: Option, - max_value: Some(10), // max_value: Option, - }, - &[(5, 100_010, true), (10, 100_020, true)], - 5, - ) - } - #[test] - fn fails_min_gt_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: 1, // b: TokenAmount, - min_value: Some(10), // min_value: Option, - max_value: Some(5), // max_value: Option, - }, - &[(5, 100_000, false), (10, 100_000, false)], - 5, - ) - } - #[test] - fn fails_a_min() -> Result<(), String> { + fn log_distribution_a_min() -> Result<(), String> { test_logarithmic( Logarithmic { - a: i64::MIN, // a: i64, + a: MIN_LOG_A_PARAM, // a: i64, d: 1, // d: u64, m: 1, // m: u64, n: 1, // n: u64, @@ -2191,125 +2140,58 @@ mod logarithmic { }, // f(x) = (a * log(m * (x - s + o) / n)) / d + b &[ - (1, 100_000, false), // should be false, as the balance after claim == initial balance - (2, 100_001, true), + (1, 100_001, true), + (2, 100_001, false), (9, 100_001, false), (10, 100_001, false), ], 1, ) } - /// Given a logarithmic distribution function with a=MAX, - /// When I try to claim tokens, - /// Then I get an error different from InternalError. - /// - /// - #[test] - fn fails_a_max_overflows() -> Result<(), String> { - test_logarithmic( - Logarithmic { - a: i64::MAX, // 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_000, false), - (9, 100_000, false), - (10, 100_000, false), - ], - 1, - ) - } - #[test] - fn a_0_b_0() -> Result<(), String> { - test_logarithmic( - Logarithmic { - a: 0, // 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: 0, // b: TokenAmount, - min_value: None, // min_value: Option, - max_value: None, // max_value: Option, - }, - &[ - (1, 100_000, false), - (9, 100_000, false), - (10, 100_000, false), - ], - 1, - ) - } - #[test] - fn fails_log_negative() -> Result<(), String> { - test_logarithmic( - Logarithmic { - a: 1, // a: i64, - d: 1, // d: u64, - m: 1, // m: u64, - n: 1, // n: u64, - o: -10, // o: i64, - start_moment: Some(1), // start_moment: Option, - b: 0, // b: TokenAmount, - min_value: None, // min_value: Option, - max_value: None, // max_value: Option, - }, - &[ - (1, 100_000, false), - (9, 100_000, false), - (10, 100_000, false), - ], - 1, - ) - } + #[test] - fn fails_o_min() -> Result<(), String> { + fn log_distribution_max_amounts() { test_logarithmic( Logarithmic { - a: 1, // a: i64, - d: 1, // d: u64, - m: 1, // m: u64, - n: 1, // n: u64, - o: i64::MIN, // o: i64, - start_moment: Some(1), // start_moment: Option, - b: 0, // b: TokenAmount, - min_value: None, // min_value: Option, - max_value: None, // max_value: Option, + 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, 100_000, false), - (9, 100_000, false), - (10, 100_000, false), + (1, 281474978991040, true), + (9, 2533274810119360, true), + (10, 2814749789010400, true), + (200, 38843547087063520, true), ], 1, ) + .expect("expect to pass"); } + #[test] - fn fails_b_max() -> Result<(), String> { + 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: u64::MAX, // b: TokenAmount, - min_value: None, // min_value: Option, - max_value: None, // max_value: Option, + 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, 100_000, false), - (9, 100_000, false), - (10, 100_000, false), + (1, 281474976810655, true), // We start at 1 + (9, 2533274790495904, true), + (10, 2814749767206561, true), ], 1, ) @@ -2338,7 +2220,7 @@ mod inverted_logarithmic { use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,InvertedLogarithmic}; #[test] - fn ones() -> Result<(), String> { + 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, @@ -2364,7 +2246,7 @@ mod inverted_logarithmic { } #[test] - fn inv_log_reduced_emission() -> Result<(), String> { + fn inv_log_distribution_reduced_emission() -> Result<(), String> { // y // ↑ // 10000 |* @@ -2412,7 +2294,7 @@ mod inverted_logarithmic { } #[test] - fn inv_log_reduced_emission_passing_0() -> Result<(), String> { + fn inv_log_distribution_reduced_emission_passing_0() -> Result<(), String> { // y // ↑ // 350 |* @@ -2447,7 +2329,7 @@ mod inverted_logarithmic { } #[test] - fn inv_log_negative_a_increase_emission() -> Result<(), String> { + fn inv_log_distribution_negative_a_increase_emission() -> Result<(), String> { // y // ↑ // 10000 | From d0b562687b12c24022a2f843a3841cc889044c17 Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Fri, 18 Apr 2025 04:27:03 +0700 Subject: [PATCH 5/6] test(platform): tests for exp and polynomial distributions (#2556) --- Cargo.lock | 41 ++ .../distribution_function/evaluate.rs | 109 ++-- .../distribution_function/mod.rs | 21 +- .../distribution_function/validation.rs | 444 +++++++++++---- packages/rs-drive-abci/Cargo.toml | 1 + .../distribution/perpetual/block_based.rs | 539 ++++++++++-------- .../distribution/perpetual/time_based.rs | 10 +- packages/rs-drive-abci/src/logging/logger.rs | 24 +- packages/rs-drive-abci/src/logging/mod.rs | 4 +- 9 files changed, 777 insertions(+), 416 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc5ee9e0bfd..1bf29211de1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1649,6 +1649,7 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", + "serial_test", "simple-signer", "strategy-tests", "tempfile", @@ -4360,6 +4361,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" @@ -4375,6 +4385,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" @@ -4616,6 +4632,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/evaluate.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs index 3478e904543..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,6 +1,7 @@ use crate::balances::credits::TokenAmount; 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; @@ -204,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 { @@ -218,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 }; @@ -244,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 { @@ -752,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() { @@ -767,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), @@ -787,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) @@ -845,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); } @@ -871,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() { 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 e2e70b3b345..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,7 +10,6 @@ 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. @@ -20,12 +19,28 @@ pub const MAX_DISTRIBUTION_CYCLES_PARAM: u64 = 32_767; //u15::Max 2^(63 - 48) - pub const DEFAULT_STEP_DECREASING_AMOUNT_MAX_CYCLES_BEFORE_TRAILING_DISTRIBUTION: u16 = 128; -pub const MAX_LINEAR_SLOPE_PARAM: u64 = 256; +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 { /// Emits a constant (fixed) number of tokens for every period. @@ -136,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. 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 ae87ad0c97e..06bfe831185 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,16 +4,20 @@ 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_EXP_A_PARAM, MAX_LINEAR_SLOPE_PARAM, - MAX_LOG_A_PARAM, MIN_LOG_A_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 } => { @@ -30,31 +34,38 @@ impl DistributionFunction { )); } } - 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 { @@ -184,7 +195,7 @@ impl DistributionFunction { a, d, start_step: s, - starting_amount: b, + starting_amount, min_value, max_value, } => { @@ -193,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( @@ -262,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, } @@ -321,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( @@ -450,12 +497,23 @@ 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(), @@ -969,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() @@ -979,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() @@ -991,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() @@ -1003,7 +1061,7 @@ mod tests { let dist = DistributionFunction::FixedAmount { 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() @@ -1025,7 +1083,7 @@ mod tests { 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() @@ -1044,7 +1102,7 @@ mod tests { 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() @@ -1063,7 +1121,7 @@ mod tests { 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() @@ -1079,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() @@ -1091,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() @@ -1111,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 { @@ -1133,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() @@ -1150,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() @@ -1167,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") @@ -1187,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") @@ -1207,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") @@ -1227,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") @@ -1247,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") @@ -1267,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") @@ -1287,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") @@ -1307,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) => { @@ -1337,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() @@ -1354,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() @@ -1377,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) => { @@ -1390,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 { @@ -1403,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() @@ -1424,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" @@ -1445,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" @@ -1466,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" @@ -1487,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() { @@ -1508,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" @@ -1529,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" @@ -1552,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" @@ -1575,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" @@ -1596,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" @@ -1626,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") @@ -1651,7 +1853,7 @@ mod tests { 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); } @@ -1676,7 +1878,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_exponential_invalid_zero_n") .first_error() @@ -1696,7 +1898,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_exponential_invalid_zero_m") @@ -1719,7 +1921,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_exponential_invalid_zero_a") @@ -1742,7 +1944,7 @@ mod tests { 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") @@ -1765,7 +1967,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_exponential_invalid_o_too_large") @@ -1788,7 +1990,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_exponential_invalid_min_greater_than_max") @@ -1811,7 +2013,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_exponential_valid_with_negative_m") .first_error() @@ -1831,7 +2033,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_exponential_valid_with_max_boundary") .first_error() @@ -1851,7 +2053,7 @@ mod tests { 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") @@ -1874,7 +2076,7 @@ mod tests { 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") @@ -1897,7 +2099,7 @@ mod tests { 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") @@ -1920,7 +2122,7 @@ mod tests { 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") @@ -1943,7 +2145,7 @@ mod tests { 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") @@ -1966,7 +2168,7 @@ mod tests { 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") @@ -1990,7 +2192,7 @@ mod tests { max_value: Some(1000), }; - let result = dist.validate(5); + let result = dist.validate(5, PlatformVersion::latest()); match result { Ok(validation_result) => { @@ -2020,7 +2222,7 @@ mod tests { 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() @@ -2040,7 +2242,7 @@ mod tests { 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() @@ -2062,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() @@ -2082,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") @@ -2105,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") @@ -2128,7 +2330,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_m") @@ -2151,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") @@ -2174,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") @@ -2197,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") @@ -2220,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() @@ -2240,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() @@ -2262,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") @@ -2285,7 +2487,7 @@ 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_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)" @@ -2305,7 +2507,7 @@ 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_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)" @@ -2325,7 +2527,7 @@ 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_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)" @@ -2345,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)" @@ -2365,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)" @@ -2385,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)" @@ -2405,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" @@ -2425,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" @@ -2445,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" @@ -2465,7 +2667,7 @@ 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" @@ -2487,7 +2689,7 @@ 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_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)" @@ -2507,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" @@ -2527,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-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index 49ea54b19d4..25d9f9f38a8 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -106,6 +106,7 @@ 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 75c6a982b86..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 @@ -696,6 +696,7 @@ mod random { /// 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), @@ -721,6 +722,7 @@ mod random { /// 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 }, @@ -736,6 +738,7 @@ mod random { .expect("no rewards"); } #[test] + #[ignore] fn test_block_based_perpetual_random_0_u64_max_should_error_at_validation() { check_heights( DistributionFunction::Random { @@ -751,6 +754,7 @@ mod random { } #[test] + #[ignore] fn test_block_based_perpetual_random_0_MAX_distribution_param() { check_heights( DistributionFunction::Random { @@ -773,6 +777,7 @@ mod random { /// 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; @@ -1091,10 +1096,10 @@ mod step_decreasing { #[test] fn claim_every_block_on_100k_128_default_steps() { - let steps = (1..200).step_by(1).collect::>(); + 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..200).step_by(1).collect::>(); + let later_steps = (129..140).step_by(1).collect::>(); let later_steps_expected_amounts = later_steps .iter() .map(|_| *start_steps_expected_amounts.last().unwrap()) @@ -1520,11 +1525,11 @@ mod stepwise { use std::collections::BTreeMap; #[test] - fn fails_stepwise_correct() { + fn distribution_stepwise_correct() { let distribution_interval = 10; let periods = BTreeMap::from([ - (0, 10_000), // h 1-30 - (2, 20_000), // h 31+ + (0, 10_000), // h 1-20 + (2, 20_000), // h 20+ (45, 30_000), (50, 40_000), (70, 50_000), @@ -1539,19 +1544,18 @@ mod stepwise { (10, 110_000, true), (11, 110_000, false), (19, 110_000, false), - (20, 120_000, true), - (21, 120_000, false), - (24, 120_000, false), - (35, 140_000, true), // since 20, we should get one more distribution of 20k at height 30 - (39, 140_000, false), - (46, 160_000, true), - (49, 160_000, false), - (51, 180_000, true), - (52, 180_000, false), - (70, 270_000, true), + (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, - 270_000 + 50_000 * (1_000_000 - 70_000) / distribution_interval, + 1_000_000, 6_370_000, // because we only do 128 steps at a time. true, ), ]; @@ -1572,29 +1576,12 @@ mod stepwise { mod linear { use super::test_suite::check_heights; - use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::{DistributionFunction, MAX_LINEAR_SLOPE_A_PARAM, MIN_LINEAR_SLOPE_A_PARAM}; - /// Given linear distribution with d=0, - /// When I create a token, - /// Then I get an error. - #[test] - fn fails_divide_by_0() -> Result<(), String> { - test_linear( - 1, // a - 0, // d - None, // start_step - 100_000, // starting_amount - None, // min_value - None, // max_value - &[(10, 100_000, false)], // heights - 1, // distribution_interval - ) - } - /// Given linear distribution with d=MAX and starting amount of 1, - /// When I claim tokens, - /// Then I have only one success, and subsequent claims fail because the calculated distribution is lower than 1 #[test] - fn divide_my_max() -> Result<(), String> { + 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 @@ -1608,21 +1595,7 @@ mod linear { } #[test] - fn min_eq_max() -> Result<(), String> { - test_linear( - 1, - 1, - None, - 0, - Some(10), - Some(10), - &[(1, 100_010, true), (2, 100_020, true)], - 1, - ) - } - - #[test] - fn fx_eq_x_matrix() -> Result<(), String> { + fn linear_distribution_x_matrix() -> Result<(), String> { let steps = [ (1, 100_001, true), (2, 100_003, true), @@ -1640,35 +1613,20 @@ mod linear { Ok(()) } #[test] - fn negative_a() -> Result<(), String> { - for a in [-1, -100_000, i64::MIN] { - test_linear( - a, - 1, - None, - 0, - None, - None, - &[(1, 100_000, false), (20, 100_000, false)], - 1, - )?; - } - Ok(()) - } - - #[test] - fn fails_max_lt_min() -> Result<(), String> { - for max in [0, 99] { - test_linear( - 1, - 1, - None, - 0, - Some(100), - Some(max), - &[(1, 100_000, false), (20, 100_000, false)], - 1, - )?; + 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(()) } @@ -1715,7 +1673,8 @@ mod linear { } } -mod polynomial { +#[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::{ @@ -1724,7 +1683,7 @@ mod polynomial { token_distribution_key::TokenDistributionType, token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters, token_perpetual_distribution::{ - distribution_function::DistributionFunction::{self, Polynomial}, + distribution_function::DistributionFunction::{self, Exponential}, distribution_recipient::TokenDistributionRecipient, reward_distribution_type::RewardDistributionType, v0::TokenPerpetualDistributionV0, @@ -1733,114 +1692,209 @@ mod polynomial { }, 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 ones() -> Result<(), String> { - test_polynomial( - Polynomial { + fn exponential_distribution_growth_basic() -> Result<(), String> { + test_exponential( + Exponential { a: 1, d: 1, - m: 1, + m: 1, // positive ⇒ growth n: 1, - o: 1, + o: 0, start_moment: Some(1), - b: 100_000, + b: 0, min_value: None, - max_value: None, + max_value: Some(1_000_000), }, - &[(10, 1_100_055, true), (20, 2_100_210, true)], + // heights 10 and 20 should both succeed – balances are illustrative + &[(10, 112_814, true), (20, 6_799_881, true)], 1, ) } - /// Divide by 0 - /// claim at height 10: claim failed: assertion 1 failed: expected SuccessfulExecution, got - /// [InternalError(\"storage: protocol: divide by zero error: Polynomial function: divisor d is 0\")]\n expected balance Some(1100055) but got 100000\n\n--> + // ───────────────────────────────────────────────────────────────────────── + // 2. Basic negative‑decay example (m < 0) + // ───────────────────────────────────────────────────────────────────────── #[test] - fn fails_divide_by_0() -> Result<(), String> { - test_polynomial( - Polynomial { - a: 1, - d: 0, - m: 1, + fn exponential_distribution_decay_basic() -> Result<(), String> { + test_exponential( + Exponential { + a: 5, + d: 1, + m: -1, // negative ⇒ decay n: 1, - o: 1, + o: 0, start_moment: Some(1), b: 100_000, - min_value: None, + min_value: Some(50_000), max_value: None, }, - &[(10, 1_100_055, true), (20, 2_100_210, true)], + &[(1, 200_005, true), (4, 500_006, true)], 1, ) } - /// Given max_value < min_value, - /// When I try to use the token distribution function, - /// Then the token distribution function validation fails. + // ───────────────────────────────────────────────────────────────────────── + // 3. o at −MAX_DISTRIBUTION_PARAM ⇒ argument very negative ▶ min / 0 + // ───────────────────────────────────────────────────────────────────────── #[test] - fn fails_max_lt_min_should_fail() -> Result<(), String> { - test_polynomial( - Polynomial { + fn exponential_distribution_o_min() -> Result<(), String> { + test_exponential( + Exponential { a: 1, d: 1, m: 1, n: 1, - o: 1, + o: -(MAX_DISTRIBUTION_PARAM as i64), start_moment: Some(1), - b: 100_000, - min_value: Some(100_000), - max_value: Some(10_000), + b: 0, + min_value: None, + max_value: Some(MAX_DISTRIBUTION_PARAM), }, - &[(10, 100_000, false), (20, 100_000, false)], + &[(1, 100_000, false), (4, 100_000, false)], 1, ) } + // ───────────────────────────────────────────────────────────────────────── + // 4. o at +MAX_DISTRIBUTION_PARAM (huge positive shift) + // ───────────────────────────────────────────────────────────────────────── #[test] - fn negative_a() -> Result<(), String> { - test_polynomial( - Polynomial { - a: -1, + fn exponential_distribution_o_max() -> Result<(), String> { + test_exponential( + Exponential { + a: MAX_EXP_A_PARAM, d: 1, - m: 1, - n: 1, - o: 1, + m: -1, + n: 32, + o: MAX_DISTRIBUTION_PARAM as i64, start_moment: Some(1), - b: 100_000, + b: 10, min_value: None, - max_value: None, + max_value: Some(MAX_DISTRIBUTION_PARAM), }, - &[(1, 199_999, true), (4, 499_990, true)], + &[(1, 100010, true), (10, 100100, true)], 1, ) } + // ───────────────────────────────────────────────────────────────────────── + // 5. Exhaustive combination of extreme parameter values + // ‑ ensure no `InternalError` + // ───────────────────────────────────────────────────────────────────────── #[test] - fn fails_a_min() -> Result<(), String> { - test_polynomial( - Polynomial { - a: i64::MIN, - d: 1, - m: 1, - n: 1, - o: 1, - start_moment: Some(1), - b: 100_000, - min_value: None, - max_value: None, - }, - &[(1, 100_000, false), (4, 100_000, true)], - 1, - ) + 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 a_minus_1_b_0() -> Result<(), String> { + fn polynomial_distribution_basic() -> Result<(), String> { test_polynomial( Polynomial { - a: -1, + a: 1, d: 1, - m: 1, + m: 2, n: 1, o: 1, start_moment: Some(1), @@ -1848,42 +1902,39 @@ mod polynomial { min_value: None, max_value: None, }, - &[(1, 100_000, false), (4, 100_000, false)], + &[(10, 100_385, true), (20, 102_870, true)], 1, ) } - /// Given a polynomial distribution function with o=i64::MIN, - /// When I try to use the token distribution function, - /// Then the token distribution function validation fails on creation. #[test] - fn fails_o_min() -> Result<(), String> { + fn polynomial_distribution_negative_a() -> Result<(), String> { test_polynomial( Polynomial { - a: 1, + a: -1, d: 1, - m: 1, + m: 3, n: 1, - o: i64::MIN, + o: 1, start_moment: Some(1), - b: 0, + b: 100_000, min_value: None, max_value: None, }, - &[(1, 100_000, false), (4, 100_000, false)], + &[(1, 199_999, true), (4, 499_900, true)], 1, ) } #[test] - fn o_max() -> Result<(), String> { + fn polynomial_distribution_a_minus_1_b_0() -> Result<(), String> { test_polynomial( Polynomial { - a: 1, + a: -1, d: 1, - m: 1, + m: 2, n: 1, - o: i64::MAX, + o: 1, start_moment: Some(1), b: 0, min_value: None, @@ -1894,29 +1945,29 @@ mod polynomial { ) } + /// Given a polynomial distribution function with o=-MAX_DISTRIBUTION_PARAM, we should + /// have no rewards #[test] - #[should_panic(expected = "invalid distribution function")] - fn zero_pow_minus_1_at_h_1_invalid() { + fn polynomial_distribution_o_min() -> Result<(), String> { test_polynomial( Polynomial { a: 1, d: 1, - m: -1, + m: 2, n: 1, - o: 0, + o: -(MAX_DISTRIBUTION_PARAM as i64), start_moment: Some(1), b: 0, min_value: None, max_value: None, }, - &[(1, 100_000, false), (2, 100_001, true)], + &[(1, 100_000, false), (4, 100_000, false)], 1, ) - .expect("should panic"); - unreachable!("should panic"); } + #[test] - fn fails_zero_pow_minus_1_at_h_2() -> Result<(), String> { + fn polynomial_distribution_pow_minus_1_at_h_2() -> Result<(), String> { test_polynomial( Polynomial { a: 1, @@ -1933,29 +1984,29 @@ mod polynomial { (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_004, true), // 3.pow(1/2) == 1.73 - should round to 2; FAILS - (5, 100_006, true), // 4.pow(1/2) == 2 - (6, 100_008, true), // 5.pow(1/2) == 2.23 - should round to 2 + (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 fails_o_max_m_2() -> Result<(), String> { + fn polynomial_distribution_o_max() -> Result<(), String> { test_polynomial( Polynomial { a: 1, d: 1, m: 2, n: 1, - o: i64::MAX, + o: MAX_DISTRIBUTION_PARAM as i64, start_moment: Some(1), b: 0, min_value: None, max_value: None, }, - &[(1, 100_000, false), (10, 100_000, false)], + &[(1, 281474976810655, true), (10, 2814749767206550, true)], 1, ) } @@ -1983,70 +2034,82 @@ mod polynomial { /// /// We expect this test not to end with InternalError. #[test] - fn fails_polynomial_power() -> Result<(), String> { - for m in [i64::MIN, -1, 0, 1, i64::MAX] { - for n in [0, 1, u64::MAX] { - let dist = Polynomial { - a: 1, - d: 1, - m, - n, - o: 1, - start_moment: Some(1), - b: 100_000, - 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))?; + 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))?; + } + } + } } } @@ -2410,7 +2473,7 @@ mod test_suite { use dpp::prelude::{DataContract, IdentityPublicKey, TimestampMillis}; use simple_signer::signer::SimpleSigner; - const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(10); + 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( @@ -2902,7 +2965,7 @@ mod test_suite { let consensus_result = perpetual_distribution .distribution_type .function() - .validate(contract_start_time) + .validate(contract_start_time, PlatformVersion::latest()) .map_err(|e| format!("invalid distribution function: {:?}", e))?; if let Some(error) = consensus_result.first_error() { 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/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"; From 9448791cbfd4b170ad069fc249440164b378b38d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 18 Apr 2025 05:32:05 +0700 Subject: [PATCH 6/6] test(platform): Add data contract basic validation of distributions --- .../distribution_function/validation.rs | 12 +- .../basic_structure/v0/mod.rs | 17 + .../data_contract_create/mod.rs | 347 ++++++++++++++++++ .../basic_structure/v0/mod.rs | 17 + 4 files changed, 387 insertions(+), 6 deletions(-) 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 06bfe831185..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 @@ -93,13 +93,13 @@ impl DistributionFunction { )); } - if *trailing_distribution_interval_amount > MAX_DISTRIBUTION_PARAM { + // Ensure trailing amount does not exceed the initial amount + if *trailing_distribution_interval_amount > *distribution_start_amount { return Ok(SimpleConsensusValidationResult::new_with_error( - InvalidTokenDistributionFunctionInvalidParameterError::new( + InvalidTokenDistributionFunctionInvalidParameterTupleError::new( "trailing_distribution_interval_amount".to_string(), - 0, - MAX_DISTRIBUTION_PARAM as i64, - None, + "distribution_start_amount".to_string(), + "smaller than or equal to".to_string(), ) .into(), )); @@ -525,7 +525,7 @@ impl DistributionFunction { InvalidTokenDistributionFunctionInvalidParameterError::new( "a".to_string(), 1, - MAX_LOG_A_PARAM, + MAX_EXP_A_PARAM as i64, None, ) .into(), 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())